From 0bc10c373b25cd8436afd0cfdbd4c0c275228862 Mon Sep 17 00:00:00 2001 From: Krystie Date: Wed, 12 Aug 2026 18:04:46 -0700 Subject: [PATCH] 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 --- src/app/globals.css | 88 +++++++++++++++++++++++++++----- src/app/layout.tsx | 24 +++++++-- src/components/Footer.tsx | 19 ++++++- src/components/Navbar.tsx | 103 +++++++++++++++++++++++++++++--------- 4 files changed, 192 insertions(+), 42 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 7386e3f..6f0eec2 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 681526b..0b7803c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( - + + +