feat: add environment-based CSS theming with configurable colors and branding

This commit is contained in:
2026-04-26 16:47:48 +09:30
parent 37e5de7f92
commit d8828d64f2
16 changed files with 262 additions and 39 deletions
+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);