fix: theme colors not applying due to incorrect CSS injection method

This commit is contained in:
2026-04-26 17:35:49 +09:30
parent e4735d3823
commit c34551b868
4 changed files with 85 additions and 168 deletions
+17 -44
View File
@@ -7,12 +7,8 @@ interface SecuirdLogoProps {
} }
/** /**
* Secuird Logo - Abstract gate/doorway mark * Secuird Logo Abstract gate/doorway mark
* Represents controlled entry and policy enforcement * Uses CSS variables so the color automatically adapts to the active theme.
* Two vertical pillars forming a gateway with negative space
*
* Uses inline styles for theme colors to ensure they work correctly
* across all theme configurations (default, dev, preprod)
*/ */
export function SecuirdLogo({ export function SecuirdLogo({
size = "md", size = "md",
@@ -25,61 +21,38 @@ export function SecuirdLogo({
lg: "w-12 h-12", lg: "w-12 h-12",
}; };
// Use inline styles for theme colors to ensure they work at runtime // Tailwind classes resolve to hsl(var(--primary)) / hsl(var(--sidebar-primary))
const getBgStyle = () => { // which pick up the injected --theme-primary / --theme-sidebar-primary values.
if (variant === "light") { const bgClasses = {
return { default: "bg-primary",
backgroundColor: "hsl(var(--theme-sidebar-primary, 173 65% 36%))", light: "bg-sidebar-primary",
color: "hsl(var(--theme-sidebar-primary-foreground, 0 0% 100%))"
};
}
return {
backgroundColor: "hsl(var(--theme-primary, 173 65% 36%))",
color: "hsl(var(--theme-primary-foreground, 0 0% 100%))"
};
}; };
const bgStyle = getBgStyle(); const iconColor = variant === "light"
? "text-sidebar-primary-foreground"
: "text-primary-foreground";
return ( return (
<div <div
className={cn( className={cn(
"rounded-lg flex items-center justify-center flex-shrink-0", "rounded-lg flex items-center justify-center flex-shrink-0",
sizeClasses[size], sizeClasses[size],
bgClasses[variant],
className className
)} )}
style={bgStyle}
> >
<svg <svg
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
className={cn( className={cn(
iconColor,
size === "sm" ? "w-4 h-4" : size === "md" ? "w-5 h-5" : "w-6 h-6" size === "sm" ? "w-4 h-4" : size === "md" ? "w-5 h-5" : "w-6 h-6"
)} )}
style={{ color: bgStyle.color }}
> >
{/* Abstract gate - two pillars with archway */} <path d="M4 4h3v16H4V4z" fill="currentColor" />
<path <path d="M17 4h3v16h-3V4z" fill="currentColor" />
d="M4 4h3v16H4V4z" <path d="M7 4h10v3H7V4z" fill="currentColor" opacity="0.7" />
fill="currentColor" <circle cx="12" cy="14" r="2" fill="currentColor" opacity="0.5" />
/>
<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"
/>
</svg> </svg>
</div> </div>
); );
+12 -23
View File
@@ -6,37 +6,26 @@ interface ThemeIndicatorProps {
} }
/** /**
* Visual theme indicator - shows environment name and color * Visual theme indicator solid-colored banner for dev/preprod environments.
* Appears as a banner in dev/preprod modes
*/ */
export function ThemeIndicator({ showBanner = true, className }: ThemeIndicatorProps) { export function ThemeIndicator({ showBanner = true }: ThemeIndicatorProps) {
const theme = config.theme.name; const theme = config.theme.name;
const appName = config.app.name; const appName = config.app.name;
// Only show for non-default themes if (theme === "default") return null;
if (theme === "default") {
return null; const labels: Record<string, string> = {
}
const themeLabels: Record<string, string> = {
dev: "Development Environment", dev: "Development Environment",
preprod: "Staging / Pre-Production", preprod: "Staging / Pre-Production",
}; };
const themeLabel = themeLabels[theme] || theme;
if (showBanner) { if (showBanner) {
return ( return <div className="env-banner">{labels[theme] || theme} {appName}</div>;
<div className="env-banner">
{themeLabel} {appName}
</div>
);
} }
return ( return (
<span className={`theme-badge ${className || ""}`}> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary/15 text-primary">
<span className="env-indicator" /> {labels[theme] || theme}
{themeLabel}
</span> </span>
); );
} }
+36 -81
View File
@@ -12,16 +12,16 @@
@layer base { @layer base {
:root { :root {
/* Core palette - Light blue-gray with teal accent */ /* Core palette - Light blue-gray with teal accent */
--background: 216 22% 94%; /* cool blue-gray — cards lift clearly off this */ --background: 216 22% 94%;
--foreground: 222 47% 9%; /* near-black navy */ --foreground: 222 47% 9%;
--card: 0 0% 100%; /* pure white — 6% lightness gap over bg */ --card: 0 0% 100%;
--card-foreground: 222 47% 9%; --card-foreground: 222 47% 9%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 222 47% 9%; --popover-foreground: 222 47% 9%;
/* Primary — theme-configurable (teal default, red for dev, purple for staging) */ /* Primary — theme-configurable */
--primary: var(--theme-primary); --primary: var(--theme-primary);
--primary-foreground: var(--theme-primary-foreground); --primary-foreground: var(--theme-primary-foreground);
@@ -29,7 +29,7 @@
--secondary: 216 20% 91%; --secondary: 216 20% 91%;
--secondary-foreground: 222 47% 18%; --secondary-foreground: 222 47% 18%;
/* Muted — noticeably darker than secondary, used for section bg */ /* Muted */
--muted: 216 18% 88%; --muted: 216 18% 88%;
--muted-foreground: 222 18% 42%; --muted-foreground: 222 18% 42%;
@@ -51,7 +51,7 @@
--info-foreground: 0 0% 100%; --info-foreground: 0 0% 100%;
/* UI chrome */ /* UI chrome */
--border: 216 18% 84%; /* clearly visible on white card */ --border: 216 18% 84%;
--input: 216 18% 92%; --input: 216 18% 92%;
--ring: var(--theme-ring); --ring: var(--theme-ring);
@@ -68,12 +68,7 @@
--sidebar-ring: var(--theme-ring); --sidebar-ring: var(--theme-ring);
--sidebar-muted: 222 20% 48%; --sidebar-muted: 222 20% 48%;
/* Gradients */ /* Shadows */
--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 */
--shadow-sm: 0 1px 2px 0 hsl(222 47% 9% / 0.10); --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-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); --shadow-lg: 0 10px 15px -3px hsl(222 47% 9% / 0.14), 0 4px 6px -4px hsl(222 47% 9% / 0.10);
@@ -151,40 +146,34 @@
} }
@layer components { @layer components {
/* Theme-specific branding enhancements */ /* Environment banner — solid theme color */
.env-banner {
/* Marketing header - theme colored */ @apply fixed top-0 left-0 right-0 z-[100] px-3 py-1.5 text-center text-xs font-bold uppercase tracking-wider;
.marketing-header { background-color: hsl(var(--theme-primary));
@apply border-b transition-colors duration-300; color: hsl(var(--theme-primary-foreground));
background-color: hsl(var(--theme-primary) / 0.03); box-shadow: 0 2px 4px 0 hsl(var(--theme-primary) / 0.3);
border-color: hsl(var(--theme-primary) / 0.2);
} }
/* Theme indicator badge */ /* Sidebar header — solid theme color background */
.theme-badge { .sidebar-header-themed {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; background-color: hsl(var(--theme-sidebar-primary));
background-color: hsl(var(--theme-primary) / 0.15); 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)); color: hsl(var(--theme-primary));
border-radius: var(--radius);
} }
/* Theme-colored sidebar header */
.sidebar-header-theme {
background: linear-gradient(135deg,
hsl(var(--theme-sidebar-primary)) 0%,
hsl(var(--theme-sidebar-primary) / 0.85) 100%
);
}
/* Theme-colored buttons */
.btn-theme {
@apply bg-primary text-primary-foreground hover:opacity-90 transition-opacity;
}
/* Theme ring focus */
.focus-ring:focus-visible {
@apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-background;
}
/* Auth card for public pages */ /* Auth card for public pages */
.auth-card { .auth-card {
@apply bg-card rounded-xl p-8 shadow-card border border-border; @apply bg-card rounded-xl p-8 shadow-card border border-border;
@@ -236,33 +225,9 @@
.page-description { .page-description {
@apply text-muted-foreground mt-1; @apply text-muted-foreground mt-1;
} }
/* Dev/Staging environment banner */
.env-banner {
@apply fixed top-0 left-0 right-0 z-50 px-3 py-1 text-center text-xs font-bold uppercase tracking-wider;
background: linear-gradient(90deg,
hsl(var(--theme-primary)) 0%,
hsl(var(--theme-primary) / 0.8) 50%,
hsl(var(--theme-primary)) 100%
);
color: hsl(var(--theme-primary-foreground));
}
/* Additional env indicator on page */
.env-indicator {
@apply inline-block w-2 h-2 rounded-full ml-2;
background-color: hsl(var(--theme-primary));
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
} }
@layer utilities { @layer utilities {
/* Animation utilities */
.animate-fade-in { .animate-fade-in {
animation: fadeIn 0.3s ease-out; animation: fadeIn 0.3s ease-out;
} }
@@ -272,22 +237,12 @@
} }
@keyframes fadeIn { @keyframes fadeIn {
from { from { opacity: 0; }
opacity: 0; to { opacity: 1; }
}
to {
opacity: 1;
}
} }
@keyframes slideIn { @keyframes slideIn {
from { from { opacity: 0; transform: translateY(-8px); }
opacity: 0; to { opacity: 1; transform: translateY(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 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 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 { return {
server: { server: {
host: "::", host: "::",
@@ -37,30 +49,18 @@ export default defineConfig(({ mode }) => {
transformIndexHtml: { transformIndexHtml: {
order: "pre", order: "pre",
handler: (html) => { 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_APP_NAME%/g, appName)
.replace(/%VITE_FAVICON%/g, favicon) .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: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),