Merge pull request #10 from CoryHawkless/custom-theme

Custom theme
This commit is contained in:
2026-04-26 16:09:58 +08:00
committed by GitHub
7 changed files with 119 additions and 85 deletions
+8 -25
View File
@@ -7,9 +7,8 @@ interface SecuirdLogoProps {
}
/**
* Secuird Logo - Abstract gate/doorway mark
* Represents controlled entry and policy enforcement
* Two vertical pillars forming a gateway with negative space
* Secuird Logo Abstract gate/doorway mark
* Uses CSS variables so the color automatically adapts to the active theme.
*/
export function SecuirdLogo({
size = "md",
@@ -22,6 +21,8 @@ export function SecuirdLogo({
lg: "w-12 h-12",
};
// Tailwind classes resolve to hsl(var(--primary)) / hsl(var(--sidebar-primary))
// which pick up the injected --theme-primary / --theme-sidebar-primary values.
const bgClasses = {
default: "bg-primary",
light: "bg-sidebar-primary",
@@ -48,28 +49,10 @@ export function SecuirdLogo({
size === "sm" ? "w-4 h-4" : size === "md" ? "w-5 h-5" : "w-6 h-6"
)}
>
{/* Abstract gate - two pillars with archway */}
<path
d="M4 4h3v16H4V4z"
fill="currentColor"
/>
<path
d="M17 4h3v16h-3V4z"
fill="currentColor"
/>
<path
d="M7 4h10v3H7V4z"
fill="currentColor"
opacity="0.7"
/>
{/* Keyhole/entry indicator */}
<circle
cx="12"
cy="14"
r="2"
fill="currentColor"
opacity="0.5"
/>
<path d="M4 4h3v16H4V4z" fill="currentColor" />
<path d="M17 4h3v16h-3V4z" fill="currentColor" />
<path d="M7 4h10v3H7V4z" fill="currentColor" opacity="0.7" />
<circle cx="12" cy="14" r="2" fill="currentColor" opacity="0.5" />
</svg>
</div>
);
@@ -0,0 +1,31 @@
import { config } from "@/config";
interface ThemeIndicatorProps {
showBanner?: boolean;
className?: string;
}
/**
* Visual theme indicator — solid-colored banner for dev/preprod environments.
*/
export function ThemeIndicator({ showBanner = true }: ThemeIndicatorProps) {
const theme = config.theme.name;
const appName = config.app.name;
if (theme === "default") return null;
const labels: Record<string, string> = {
dev: "Development Environment",
preprod: "Staging / Pre-Production",
};
if (showBanner) {
return <div className="env-banner">{labels[theme] || theme} {appName}</div>;
}
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary/15 text-primary">
{labels[theme] || theme}
</span>
);
}
@@ -3,11 +3,13 @@ import { SidebarProvider } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/navigation/AppSidebar";
import { TopBar } from "@/components/navigation/TopBar";
import ApiDevTools from "@/components/dev/ApiDevTools";
import { ThemeIndicator } from "@/components/branding/ThemeIndicator";
export default function AuthenticatedLayout() {
return (
<SidebarProvider>
<div className="min-h-screen flex w-full bg-background">
<ThemeIndicator />
<div className="min-h-screen flex w-full bg-background pt-8">
<AppSidebar />
<div className="flex-1 flex flex-col min-w-0">
<TopBar />
+12 -8
View File
@@ -1,16 +1,17 @@
import { Link, Outlet, useLocation } from "react-router-dom";
import { SecuirdLogo as GatehouseLogo } from "@/components/branding/SecuirdLogo";
import { ThemeIndicator } from "@/components/branding/ThemeIndicator";
import { Button } from "@/components/ui/button";
import { config } from "@/config";
import { cn } from "@/lib/utils";
import {
Shield,
Key,
CreditCard,
Play,
Lock,
Menu,
X
Shield,
Key,
CreditCard,
Play,
Lock,
Menu,
X
} from "lucide-react";
import { useState } from "react";
@@ -29,8 +30,11 @@ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
<div className="min-h-screen bg-background flex flex-col">
{/* Theme indicator banner for dev/staging */}
<ThemeIndicator />
{/* Header */}
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<header className="sticky top-8 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 marketing-header">
<nav className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
{/* Logo */}
+6 -2
View File
@@ -1,15 +1,19 @@
import { Outlet, Link } from "react-router-dom";
import { SecuirdLogo } from "@/components/branding/SecuirdLogo";
import { ThemeIndicator } from "@/components/branding/ThemeIndicator";
import { config } from "@/config";
export default function PublicLayout() {
return (
<div className="min-h-screen bg-background flex flex-col">
{/* Theme indicator banner for dev/staging */}
<ThemeIndicator />
{/* Subtle gradient background */}
<div className="fixed inset-0 bg-gradient-to-br from-background via-background to-secondary/30 pointer-events-none" />
{/* Header */}
<header className="relative z-10 w-full py-6 px-4">
{/* Header with theme-colored border */}
<header className="relative z-10 w-full py-6 px-4 marketing-header">
<div className="max-w-md mx-auto">
<Link to="/" className="flex items-center gap-2.5 justify-center">
<SecuirdLogo size="md" />
+39 -29
View File
@@ -1,7 +1,5 @@
@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;
@@ -14,16 +12,16 @@
@layer base {
:root {
/* Core palette - Light blue-gray with teal accent */
--background: 216 22% 94%; /* cool blue-gray — cards lift clearly off this */
--foreground: 222 47% 9%; /* near-black navy */
--background: 216 22% 94%;
--foreground: 222 47% 9%;
--card: 0 0% 100%; /* pure white — 6% lightness gap over bg */
--card: 0 0% 100%;
--card-foreground: 222 47% 9%;
--popover: 0 0% 100%;
--popover-foreground: 222 47% 9%;
/* Primary — theme-configurable (teal default, red for dev, purple for staging) */
/* Primary — theme-configurable */
--primary: var(--theme-primary);
--primary-foreground: var(--theme-primary-foreground);
@@ -31,7 +29,7 @@
--secondary: 216 20% 91%;
--secondary-foreground: 222 47% 18%;
/* Muted — noticeably darker than secondary, used for section bg */
/* Muted */
--muted: 216 18% 88%;
--muted-foreground: 222 18% 42%;
@@ -53,7 +51,7 @@
--info-foreground: 0 0% 100%;
/* UI chrome */
--border: 216 18% 84%; /* clearly visible on white card */
--border: 216 18% 84%;
--input: 216 18% 92%;
--ring: var(--theme-ring);
@@ -70,12 +68,7 @@
--sidebar-ring: var(--theme-ring);
--sidebar-muted: 222 20% 48%;
/* Gradients */
--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 */
/* Shadows */
--shadow-sm: 0 1px 2px 0 hsl(222 47% 9% / 0.10);
--shadow-md: 0 4px 6px -1px hsl(222 47% 9% / 0.14), 0 2px 4px -2px hsl(222 47% 9% / 0.10);
--shadow-lg: 0 10px 15px -3px hsl(222 47% 9% / 0.14), 0 4px 6px -4px hsl(222 47% 9% / 0.10);
@@ -153,6 +146,34 @@
}
@layer components {
/* Environment banner — solid theme color */
.env-banner {
@apply fixed top-0 left-0 right-0 z-[100] px-3 py-1.5 text-center text-xs font-bold uppercase tracking-wider;
background-color: hsl(var(--theme-primary));
color: hsl(var(--theme-primary-foreground));
box-shadow: 0 2px 4px 0 hsl(var(--theme-primary) / 0.3);
}
/* Sidebar header — solid theme color background */
.sidebar-header-themed {
background-color: hsl(var(--theme-sidebar-primary));
color: hsl(var(--theme-sidebar-primary-foreground));
border-bottom: 1px solid hsl(var(--theme-sidebar-primary) / 0.3);
}
/* Top bar — theme-colored bottom border + subtle tint */
.top-bar-themed {
border-bottom: 2px solid hsl(var(--theme-primary));
background-color: hsl(var(--theme-primary) / 0.04);
}
/* Active nav item — theme color background */
.active-nav-themed {
background-color: hsl(var(--theme-primary) / 0.12);
color: hsl(var(--theme-primary));
border-radius: var(--radius);
}
/* Auth card for public pages */
.auth-card {
@apply bg-card rounded-xl p-8 shadow-card border border-border;
@@ -207,7 +228,6 @@
}
@layer utilities {
/* Animation utilities */
.animate-fade-in {
animation: fadeIn 0.3s ease-out;
}
@@ -217,22 +237,12 @@
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
}
+20 -20
View File
@@ -17,9 +17,21 @@ export default defineConfig(({ mode }) => {
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})`;
const themeColorHsl = `hsl(${colors.primary})`;
// Build a CSS block that injects directly into <head> before any stylesheets load.
const themeStyles = `
<style data-theme="${themeName}">
: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: ${themeColorHsl};
}
</style>`;
return {
server: {
host: "::",
@@ -37,30 +49,18 @@ export default defineConfig(({ mode }) => {
transformIndexHtml: {
order: "pre",
handler: (html) => {
return html
const headClose = html.indexOf("</head>");
const withStyle = headClose > -1
? html.slice(0, headClose) + themeStyles + "\n" + html.slice(headClose)
: html;
return withStyle
.replace(/%VITE_APP_NAME%/g, appName)
.replace(/%VITE_FAVICON%/g, favicon)
.replace(/%VITE_THEME_COLOR%/g, themeColor);
.replace(/%VITE_THEME_COLOR%/g, themeColorHsl);
},
},
},
],
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"),