Add light/dark theme toggle with CSS variable system
- globals.css: surface colors flip via CSS custom properties (.dark class) - Navbar: sun/moon toggle button, persists to localStorage, respects OS preference - Footer: theme-aware logo (light logo for dark mode, dark logo for light mode) - Layout: no-FOUC inline script applies theme before first paint - Both DashCaddy wide logos (light+dark) swap based on theme - Sami's personal samiahmed7777 logo in footer copyright is untouched
This commit is contained in:
4 files changed
+192
-42
No files matched your search
+75
-13
@@ -1,5 +1,9 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* ── Theme system: light is default, .dark activates dark mode ── */
|
||||
/* Surface colors are remapped so bg-surface-950 = page background, */
|
||||
/* text-surface-50 = main text — in BOTH themes. The scale flips. */
|
||||
|
||||
@theme inline {
|
||||
--color-brand-50: #eef6ff;
|
||||
--color-brand-100: #d9eaff;
|
||||
@@ -13,22 +17,55 @@
|
||||
--color-brand-900: #18378f;
|
||||
--color-brand-950: #142357;
|
||||
|
||||
--color-surface-50: #f8fafc;
|
||||
--color-surface-100: #f1f5f9;
|
||||
--color-surface-200: #e2e8f0;
|
||||
--color-surface-300: #cbd5e1;
|
||||
--color-surface-400: #94a3b8;
|
||||
--color-surface-500: #64748b;
|
||||
--color-surface-600: #475569;
|
||||
--color-surface-700: #334155;
|
||||
--color-surface-800: #1e293b;
|
||||
--color-surface-900: #0f172a;
|
||||
--color-surface-950: #020617;
|
||||
/* Surface scale — mapped to semantic vars that flip with theme */
|
||||
--color-surface-50: var(--s-50);
|
||||
--color-surface-100: var(--s-100);
|
||||
--color-surface-200: var(--s-200);
|
||||
--color-surface-300: var(--s-300);
|
||||
--color-surface-400: var(--s-400);
|
||||
--color-surface-500: var(--s-500);
|
||||
--color-surface-600: var(--s-600);
|
||||
--color-surface-700: var(--s-700);
|
||||
--color-surface-800: var(--s-800);
|
||||
--color-surface-900: var(--s-900);
|
||||
--color-surface-950: var(--s-950);
|
||||
|
||||
--font-sans: "Inter", system-ui, -apple-system, sans-serif;
|
||||
--font-mono: "JetBrains Mono", "Fira Code", monospace;
|
||||
}
|
||||
|
||||
/* ── Light theme (default) ── */
|
||||
:root {
|
||||
--s-50: #020617; /* main text — near black */
|
||||
--s-100: #0f172a; /* heading text */
|
||||
--s-200: #1e293b; /* strong text */
|
||||
--s-300: #475569; /* secondary text */
|
||||
--s-400: #64748b; /* muted text */
|
||||
--s-500: #94a3b8; /* very muted */
|
||||
--s-600: #cbd5e1; /* faint text */
|
||||
--s-700: #e2e8f0; /* borders */
|
||||
--s-800: #f1f5f9; /* card backgrounds */
|
||||
--s-900: #f8fafc; /* secondary page bg */
|
||||
--s-950: #ffffff; /* page background — white */
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* ── Dark theme ── */
|
||||
.dark {
|
||||
--s-50: #f8fafc; /* main text — near white */
|
||||
--s-100: #f1f5f9; /* heading text */
|
||||
--s-200: #e2e8f0; /* strong text */
|
||||
--s-300: #cbd5e1; /* secondary text */
|
||||
--s-400: #94a3b8; /* muted text */
|
||||
--s-500: #64748b; /* very muted */
|
||||
--s-600: #475569; /* faint text */
|
||||
--s-700: #334155; /* borders */
|
||||
--s-800: #1e293b; /* card backgrounds */
|
||||
--s-900: #0f172a; /* secondary page bg */
|
||||
--s-950: #020617; /* page background — near black */
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -39,6 +76,7 @@ body {
|
||||
background-color: var(--color-surface-950);
|
||||
color: var(--color-surface-100);
|
||||
font-family: var(--font-sans);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@@ -54,9 +92,18 @@ body {
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(30, 41, 59, 0.5);
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(148, 163, 184, 0.1);
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
:root {
|
||||
--glass-bg: rgba(241, 245, 249, 0.5);
|
||||
--glass-border: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.dark {
|
||||
--glass-bg: rgba(30, 41, 59, 0.5);
|
||||
--glass-border: rgba(148, 163, 184, 0.1);
|
||||
}
|
||||
|
||||
.glow-border {
|
||||
@@ -71,3 +118,18 @@ body {
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
/* Theme toggle button */
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.theme-toggle:hover {
|
||||
background-color: var(--color-surface-800);
|
||||
}
|
||||
+20
-4
@@ -4,7 +4,7 @@ import "./globals.css";
|
||||
export const metadata: Metadata = {
|
||||
title: "DashCaddy - Self-Hosting Made Beautiful",
|
||||
description:
|
||||
"Deploy 50+ Docker apps with one click. Automatic SSL, DNS, and reverse proxy configuration. The all-in-one self-hosting dashboard.",
|
||||
"Deploy 76+ Docker apps with one click. AI-powered self-hosting with automatic SSL, DNS, reverse proxy, security center, and fleet management.",
|
||||
keywords: [
|
||||
"self-hosting",
|
||||
"docker",
|
||||
@@ -14,11 +14,12 @@ export const metadata: Metadata = {
|
||||
"ssl",
|
||||
"dns",
|
||||
"homelab",
|
||||
"AI self-hosting",
|
||||
],
|
||||
openGraph: {
|
||||
title: "DashCaddy - Self-Hosting Made Beautiful",
|
||||
description:
|
||||
"Deploy 50+ Docker apps with one click. Automatic SSL, DNS, and reverse proxy configuration.",
|
||||
"Deploy 76+ Docker apps with one click. AI-powered self-hosting with automatic SSL, DNS, and reverse proxy.",
|
||||
url: "https://dashcaddy.net",
|
||||
siteName: "DashCaddy",
|
||||
type: "website",
|
||||
@@ -27,17 +28,32 @@ export const metadata: Metadata = {
|
||||
card: "summary_large_image",
|
||||
title: "DashCaddy - Self-Hosting Made Beautiful",
|
||||
description:
|
||||
"Deploy 50+ Docker apps with one click. Automatic SSL, DNS, and reverse proxy.",
|
||||
"Deploy 76+ Docker apps with one click. AI-powered self-hosting.",
|
||||
},
|
||||
};
|
||||
|
||||
// Inline script to set theme class BEFORE first paint — prevents FOUC
|
||||
const themeScript = `
|
||||
(function() {
|
||||
try {
|
||||
var stored = localStorage.getItem('theme');
|
||||
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var theme = stored || (prefersDark ? 'dark' : 'light');
|
||||
if (theme === 'dark') document.documentElement.classList.add('dark');
|
||||
} catch(e) {}
|
||||
})();
|
||||
`;
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="h-full antialiased">
|
||||
<html lang="en" className="h-full antialiased" suppressHydrationWarning>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||
</head>
|
||||
<body className="min-h-full flex flex-col bg-surface-950 text-surface-100" style={{ fontFamily: "'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif" }}>
|
||||
{children}
|
||||
</body>
|
||||
|
||||
Reference in new issue
Block a user