Merge pull request #9 from CoryHawkless/custom-theme

feat: add environment-based CSS theming with configurable colors and …
This commit is contained in:
2026-04-26 15:20:03 +08:00
committed by GitHub
16 changed files with 263 additions and 39 deletions
+19 -1
View File
@@ -1 +1,19 @@
VITE_API_BASE_URL=http://localhost:5000
# ===========================================
# Secuird UI Configuration
# ===========================================
# Copy this file to .env.local for local development
# or use mode-specific env files (.env.development, .env.staging, .env.production)
# API Configuration
VITE_API_BASE_URL=https://api.gatehouse.local/api/v1
# Theme Configuration
# Options: default (teal), dev (red), preprod (purple)
VITE_THEME=default
# Optional: Override app name and favicon
# VITE_APP_NAME=Secuird
# VITE_FAVICON=/favicon.svg
# Host Configuration
VITE_ALLOWED_HOSTS=ui.gatehouse.local,localhost
+15 -15
View File
@@ -5,37 +5,37 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Primary Meta Tags -->
<title>Secuird — Enterprise Identity & Access Management</title>
<meta name="title" content="Secuird — Enterprise Identity & Access Management" />
<meta name="description" content="Secuird unifies social logins, MFA, and SSH certificate management in one platform. Enable enterprise SSO with Microsoft 365, Google Workspace, and GitHub without complex federation. Eliminate SSH key chaos with short-lived certificates tied to verified identities." />
<title>%VITE_APP_NAME% — Enterprise Identity & Access Management</title>
<meta name="title" content="%VITE_APP_NAME% — Enterprise Identity & Access Management" />
<meta name="description" content="%VITE_APP_NAME% unifies social logins, MFA, and SSH certificate management in one platform. Enable enterprise SSO with Microsoft 365, Google Workspace, and GitHub without complex federation. Eliminate SSH key chaos with short-lived certificates tied to verified identities." />
<meta name="keywords" content="identity management, access management, SSO, single sign-on, SSH certificates, MFA, multi-factor authentication, OIDC, enterprise security, Microsoft 365, Google Workspace, GitHub authentication" />
<meta name="author" content="Secuird" />
<meta name="author" content="%VITE_APP_NAME%" />
<meta name="robots" content="index, follow" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/gatehouse-logo.svg" />
<link rel="icon" type="image/svg+xml" href="%VITE_FAVICON%" />
<link rel="apple-touch-icon" href="%VITE_FAVICON%" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://securd.com/" />
<meta property="og:title" content="Secuird — Enterprise Identity & Access Management" />
<meta property="og:description" content="Secuird unifies social logins, MFA, and SSH certificate management in one platform. Enable enterprise SSO without complex federation. Eliminate SSH key chaos with short-lived certificates." />
<meta property="og:title" content="%VITE_APP_NAME% — Enterprise Identity & Access Management" />
<meta property="og:description" content="%VITE_APP_NAME% unifies social logins, MFA, and SSH certificate management in one platform. Enable enterprise SSO without complex federation. Eliminate SSH key chaos with short-lived certificates." />
<meta property="og:image" content="/og-image.png" />
<meta property="og:site_name" content="Secuird" />
<meta property="og:site_name" content="%VITE_APP_NAME%" />
<meta property="og:locale" content="en_US" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://securd.com/" />
<meta name="twitter:title" content="Secuird — Enterprise Identity & Access Management" />
<meta name="twitter:description" content="Secuird unifies social logins, MFA, and SSH certificate management in one platform. Enable enterprise SSO without complex federation. Eliminate SSH key chaos with short-lived certificates." />
<meta name="twitter:title" content="%VITE_APP_NAME% — Enterprise Identity & Access Management" />
<meta name="twitter:description" content="%VITE_APP_NAME% unifies social logins, MFA, and SSH certificate management in one platform. Enable enterprise SSO without complex federation. Eliminate SSH key chaos with short-lived certificates." />
<meta name="twitter:image" content="/og-image.png" />
<meta name="twitter:site" content="@securd" />
<!-- Theme color -->
<meta name="theme-color" content="#36b9a6" />
<meta name="msapplication-TileColor" content="#36b9a6" />
<meta name="theme-color" content="%VITE_THEME_COLOR%" />
<meta name="msapplication-TileColor" content="%VITE_THEME_COLOR%" />
<!-- Canonical URL -->
<link rel="canonical" href="https://securd.com/" />
@@ -45,7 +45,7 @@
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Secuird",
"name": "%VITE_APP_NAME%",
"applicationCategory": "SecurityApplication",
"operatingSystem": "Web",
"description": "Enterprise identity and access management platform providing secure authentication, organization-level security policy, SSH certificate management, and OIDC-based Single Sign-On.",
@@ -74,7 +74,7 @@
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Secuird",
"name": "%VITE_APP_NAME%",
"url": "https://securd.com",
"logo": "https://securd.com/gatehouse-logo.svg",
"sameAs": [
+16
View File
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<!-- Background - Dev theme color (Red #ef4444) -->
<rect width="24" height="24" rx="3" fill="#ef4444"/>
<!-- Left pillar -->
<path d="M4 4h3v16H4V4z" fill="#ffffff"/>
<!-- Right pillar -->
<path d="M17 4h3v16h-3V4z" fill="#ffffff"/>
<!-- Archway -->
<path d="M7 4h10v3H7V4z" fill="#ffffff" opacity="0.7"/>
<!-- Keyhole -->
<circle cx="12" cy="14" r="2" fill="#ffffff" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 496 B

+16
View File
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<!-- Background - Preprod theme color (Purple #a855f7) -->
<rect width="24" height="24" rx="3" fill="#a855f7"/>
<!-- Left pillar -->
<path d="M4 4h3v16H4V4z" fill="#ffffff"/>
<!-- Right pillar -->
<path d="M17 4h3v16h-3V4z" fill="#ffffff"/>
<!-- Archway -->
<path d="M7 4h10v3H7V4z" fill="#ffffff" opacity="0.7"/>
<!-- Keyhole -->
<circle cx="12" cy="14" r="2" fill="#ffffff" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

+3 -2
View File
@@ -1,6 +1,7 @@
import { Link, Outlet, useLocation } from "react-router-dom";
import { SecuirdLogo as GatehouseLogo } from "@/components/branding/SecuirdLogo";
import { Button } from "@/components/ui/button";
import { config } from "@/config";
import { cn } from "@/lib/utils";
import {
Shield,
@@ -35,7 +36,7 @@ return (
{/* Logo */}
<Link to="/" className="flex items-center gap-2.5">
<GatehouseLogo size="md" />
<span className="text-xl font-semibold text-foreground tracking-tight">Secuird</span>
<span className="text-xl font-semibold text-foreground tracking-tight">{config.app.name}</span>
</Link>
{/* Desktop Navigation */}
@@ -130,7 +131,7 @@ return (
<div className="col-span-2 lg:col-span-1">
<Link to="/" className="flex items-center gap-2.5">
<GatehouseLogo size="sm" />
<span className="text-lg font-semibold text-foreground tracking-tight">Secuird</span>
<span className="text-lg font-semibold text-foreground tracking-tight">{config.app.name}</span>
</Link>
<p className="mt-4 text-sm text-muted-foreground max-w-xs">
Enterprise identity and access management. Secure by design, simple by choice.
@@ -4,6 +4,7 @@ import { Shield, Smartphone, Fingerprint, AlertTriangle, CheckCircle, Loader2 }
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useAuth } from '@/contexts/AuthContext';
import { config } from '@/config';
import { AddPasskeyWizard } from '@/components/security/AddPasskeyWizard';
import { TotpEnrollmentWizard } from '@/components/security/TotpEnrollmentWizard';
import { api } from '@/lib/api';
@@ -98,7 +99,7 @@ export default function MfaEnforcementLayout() {
<header className="h-14 border-b border-border bg-card flex items-center justify-between px-4 flex-shrink-0">
<div className="flex items-center gap-3">
<Shield className="w-5 h-5 text-primary" />
<span className="font-semibold text-foreground">Secuird</span>
<span className="font-semibold text-foreground">{config.app.name}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
+3 -2
View File
@@ -1,5 +1,6 @@
import { Outlet, Link } from "react-router-dom";
import { SecuirdLogo } from "@/components/branding/SecuirdLogo";
import { config } from "@/config";
export default function PublicLayout() {
return (
@@ -12,7 +13,7 @@ export default function PublicLayout() {
<div className="max-w-md mx-auto">
<Link to="/" className="flex items-center gap-2.5 justify-center">
<SecuirdLogo size="md" />
<span className="text-xl font-semibold text-foreground tracking-tight">Secuird</span>
<span className="text-xl font-semibold text-foreground tracking-tight">{config.app.name}</span>
</Link>
</div>
</header>
@@ -28,7 +29,7 @@ export default function PublicLayout() {
<footer className="relative z-10 py-6 px-4">
<div className="max-w-md mx-auto text-center">
<p className="text-sm text-muted-foreground">
© {new Date().getFullYear()} Secuird. Identity & Access.
© {new Date().getFullYear()} {config.app.name}. Identity & Access.
</p>
</div>
</footer>
+3 -2
View File
@@ -23,6 +23,7 @@ import { SecuirdLogo } from "@/components/branding/SecuirdLogo";
import { NavLink } from "@/components/NavLink";
import { useAuth } from "@/contexts/AuthContext";
import { useOrg } from "@/contexts/OrgContext";
import { config } from "@/config";
import {
Sidebar,
SidebarContent,
@@ -104,7 +105,7 @@ export function AppSidebar() {
<SecuirdLogo size="sm" variant="light" />
{!collapsed && (
<span className="text-lg font-semibold text-sidebar-foreground tracking-tight">
Secuird
{config.app.name}
</span>
)}
</div>
@@ -221,7 +222,7 @@ export function AppSidebar() {
<SidebarFooter className="p-4 border-t border-sidebar-border">
{!collapsed && (
<div className="text-xs text-sidebar-muted">
{import.meta.env.VITE_APP_VERSION ?? 'Secuird'}
{import.meta.env.VITE_APP_VERSION ?? config.app.name}
</div>
)}
</SidebarFooter>
+63 -2
View File
@@ -4,6 +4,60 @@
// Base URL without /api/v1 suffix - used for CLI sign URL
const BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://192.168.64.7:8888";
// Theme configuration from environment
export type ThemeName = "default" | "dev" | "preprod";
interface ThemeColors {
primary: string;
primaryForeground: string;
sidebarPrimary: string;
sidebarPrimaryForeground: string;
ring: string;
}
const THEME_COLORS: Record<ThemeName, ThemeColors> = {
default: {
primary: "173 65% 36%", // teal
primaryForeground: "0 0% 100%",
sidebarPrimary: "173 65% 36%",
sidebarPrimaryForeground: "0 0% 100%",
ring: "173 65% 36%",
},
dev: {
primary: "0 72% 51%", // red
primaryForeground: "0 0% 100%",
sidebarPrimary: "0 72% 51%",
sidebarPrimaryForeground: "0 0% 100%",
ring: "0 72% 51%",
},
preprod: {
primary: "270 70% 55%", // purple
primaryForeground: "0 0% 100%",
sidebarPrimary: "270 70% 55%",
sidebarPrimaryForeground: "0 0% 100%",
ring: "270 70% 55%",
},
};
const THEME_DEFAULTS: Record<ThemeName, { appName: string; favicon: string }> = {
default: {
appName: "Secuird",
favicon: "/favicon.svg",
},
dev: {
appName: "Secuird Dev",
favicon: "/favicon-dev.svg",
},
preprod: {
appName: "Secuird Staging",
favicon: "/favicon-staging.svg",
},
};
const themeName = (import.meta.env.VITE_THEME || "default") as ThemeName;
const themeColors = THEME_COLORS[themeName] || THEME_COLORS.default;
const themeDefaults = THEME_DEFAULTS[themeName] || THEME_DEFAULTS.default;
export const config = {
// API Configuration
api: {
@@ -13,10 +67,17 @@ export const config = {
// Sign URL for CLI (same as base URL, no /api/v1)
signUrl: BASE_URL,
// App metadata
// App metadata - can be overridden per-theme or via env
app: {
name: "Secuird",
name: import.meta.env.VITE_APP_NAME || themeDefaults.appName,
description: "Identity & Access Platform",
favicon: import.meta.env.VITE_FAVICON || themeDefaults.favicon,
},
// Theme configuration for CSS variable injection
theme: {
name: themeName,
colors: themeColors,
},
// Feature flags
+15 -13
View File
@@ -1,5 +1,7 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
@import './styles/theme-vars.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -7,7 +9,7 @@
/* Secuird Design System - Enterprise Identity & Access Platform
Authoritative, infrastructure-grade aesthetic with slate/charcoal/muted blue palette
Colors are HSL for theming flexibility
*/
*/
@layer base {
:root {
@@ -21,9 +23,9 @@
--popover: 0 0% 100%;
--popover-foreground: 222 47% 9%;
/* Primary — teal, fully saturated, dark enough to read on white */
--primary: 173 65% 36%;
--primary-foreground: 0 0% 100%;
/* Primary — theme-configurable (teal default, red for dev, purple for staging) */
--primary: var(--theme-primary);
--primary-foreground: var(--theme-primary-foreground);
/* Secondary — cool blue-gray, clearly darker than bg */
--secondary: 216 20% 91%;
@@ -33,9 +35,9 @@
--muted: 216 18% 88%;
--muted-foreground: 222 18% 42%;
/* Accent — same teal as primary */
--accent: 173 65% 36%;
--accent-foreground: 0 0% 100%;
/* Accent — same as primary */
--accent: var(--theme-primary);
--accent-foreground: var(--theme-primary-foreground);
/* Semantic */
--destructive: 0 72% 48%;
@@ -53,24 +55,24 @@
/* UI chrome */
--border: 216 18% 84%; /* clearly visible on white card */
--input: 216 18% 92%;
--ring: 173 65% 36%;
--ring: var(--theme-ring);
--radius: 0.5rem;
/* Sidebar */
--sidebar-background: 222 30% 95%;
--sidebar-foreground: 222 47% 18%;
--sidebar-primary: 173 65% 36%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-primary: var(--theme-sidebar-primary);
--sidebar-primary-foreground: var(--theme-sidebar-primary-foreground);
--sidebar-accent: 216 20% 88%;
--sidebar-accent-foreground: 222 47% 9%;
--sidebar-border: 216 18% 84%;
--sidebar-ring: 173 65% 36%;
--sidebar-ring: var(--theme-ring);
--sidebar-muted: 222 20% 48%;
/* Gradients */
--gradient-brand: linear-gradient(135deg, hsl(173 65% 36%), hsl(173 65% 28%));
--gradient-accent: linear-gradient(135deg, hsl(173 65% 36%), hsl(173 65% 28%));
--gradient-brand: linear-gradient(135deg, hsl(var(--theme-primary)), hsl(var(--theme-primary) / 0.8));
--gradient-accent: linear-gradient(135deg, hsl(var(--theme-primary)), hsl(var(--theme-primary) / 0.8));
--gradient-subtle: linear-gradient(135deg, hsl(216 28% 97%), hsl(216 18% 93%));
/* Shadows — stronger alpha so cards lift off the bg */
+8
View File
@@ -0,0 +1,8 @@
/* Default theme - Secuird (Teal) */
@theme {
--color-primary: hsl(173 65% 36%);
--color-primary-foreground: hsl(0 0% 100%);
--color-sidebar-primary: hsl(173 65% 36%);
--color-sidebar-primary-foreground: hsl(0 0% 100%);
--color-ring: hsl(173 65% 36%);
}
+8
View File
@@ -0,0 +1,8 @@
/* Dev theme - Red */
@theme {
--color-primary: hsl(0 72% 51%);
--color-primary-foreground: hsl(0 0% 100%);
--color-sidebar-primary: hsl(0 72% 51%);
--color-sidebar-primary-foreground: hsl(0 0% 100%);
--color-ring: hsl(0 72% 51%);
}
+8
View File
@@ -0,0 +1,8 @@
/* Preprod theme - Purple */
@theme {
--color-primary: hsl(270 70% 55%);
--color-primary-foreground: hsl(0 0% 100%);
--color-sidebar-primary: hsl(270 70% 55%);
--color-sidebar-primary-foreground: hsl(0 0% 100%);
--color-ring: hsl(270 70% 55%);
}
+9
View File
@@ -0,0 +1,9 @@
/* Theme variables - values injected by Vite at build time */
:root {
--theme-primary: %%VITE_THEME_PRIMARY%%;
--theme-primary-foreground: 0 0% 100%;
--theme-sidebar-primary: %%VITE_THEME_SIDEBAR_PRIMARY%%;
--theme-sidebar-primary-foreground: 0 0% 100%;
--theme-ring: %%VITE_THEME_RING%%;
--theme-color: %%VITE_THEME_COLOR%%;
}
+29
View File
@@ -0,0 +1,29 @@
/* Theme configuration - injected at runtime from config */
const themeConfig = {
default: {
primary: "173 65% 36%",
sidebarPrimary: "173 65% 36%",
ring: "173 65% 36%",
},
dev: {
primary: "0 72% 51%",
sidebarPrimary: "0 72% 51%",
ring: "0 72% 51%",
},
preprod: {
primary: "270 70% 55%",
sidebarPrimary: "270 70% 55%",
ring: "270 70% 55%",
},
};
const themeName = import.meta.env.VITE_THEME || "default";
const theme = themeConfig[themeName as keyof typeof themeConfig] || themeConfig.default;
const root = document.documentElement;
root.style.setProperty("--primary", theme.primary);
root.style.setProperty("--primary-foreground", "0 0% 100%");
root.style.setProperty("--sidebar-primary", theme.sidebarPrimary);
root.style.setProperty("--sidebar-primary-foreground", "0 0% 100%");
root.style.setProperty("--ring", theme.ring);
+46 -1
View File
@@ -5,6 +5,20 @@ import { componentTagger } from "lovable-tagger";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
const themeName = env.VITE_THEME || "default";
const themeColors: Record<string, { primary: string; sidebarPrimary: string; ring: string }> = {
default: { primary: "173 65% 36%", sidebarPrimary: "173 65% 36%", ring: "173 65% 36%" },
dev: { primary: "0 72% 51%", sidebarPrimary: "0 72% 51%", ring: "0 72% 51%" },
preprod: { primary: "270 70% 55%", sidebarPrimary: "270 70% 55%", ring: "270 70% 55%" },
};
const colors = themeColors[themeName] || themeColors.default;
const appName = env.VITE_APP_NAME || (themeName === "dev" ? "Secuird Dev" : themeName === "preprod" ? "Secuird Staging" : "Secuird");
const favicon = env.VITE_FAVICON || (themeName === "dev" ? "/favicon-dev.svg" : themeName === "preprod" ? "/favicon-staging.svg" : "/favicon.svg");
const themeColor = `hsl(${colors.primary})`;
return {
server: {
@@ -15,7 +29,38 @@ export default defineConfig(({ mode }) => {
"gatehouse-ui.hawkvelt.tech",
],
},
plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
plugins: [
react(),
mode === "development" && componentTagger(),
{
name: "vite-plugin-theme-injection",
transformIndexHtml: {
order: "pre",
handler: (html) => {
return html
.replace(/%VITE_APP_NAME%/g, appName)
.replace(/%VITE_FAVICON%/g, favicon)
.replace(/%VITE_THEME_COLOR%/g, themeColor);
},
},
},
],
css: {
preprocessorOptions: {
css: {
additionalData: `
:root {
--theme-primary: ${colors.primary};
--theme-primary-foreground: 0 0% 100%;
--theme-sidebar-primary: ${colors.sidebarPrimary};
--theme-sidebar-primary-foreground: 0 0% 100%;
--theme-ring: ${colors.ring};
--theme-color: hsl(${colors.primary});
}
`,
},
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),