feat: add persistent light/dark mode toggle

- Add CSS custom property token system to globals.css (dark + light variants)
- Add anti-FOUC inline script in layout.tsx head to apply stored theme before hydration
- Create useTheme hook (src/hooks/useTheme.ts) backed by usePersistentState
- Add Sun/Moon toggle button to LeftToolbar above Settings
- Wire useTheme into ThreePanelLayout and pass theme/onThemeToggle to LeftToolbar
- Migrate hardcoded hex values to CSS vars in LeftToolbar and ThreePanelLayout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-03-17 19:32:06 +11:00
co-authored by Claude Sonnet 4.6
parent 4802187f5e
commit bf175ad125
5 changed files with 144 additions and 13 deletions
+67 -6
View File
@@ -36,6 +36,67 @@
@tailwind components;
@tailwind utilities;
:root,
html[data-theme="dark"] {
color-scheme: dark;
--rah-bg-base: #0a0a0a;
--rah-bg-surface: #111111;
--rah-bg-subtle: #121212;
--rah-bg-panel: #141414;
--rah-bg-hover: #151515;
--rah-bg-elevated: #161616;
--rah-bg-active: #1a1a1a;
--rah-bg-modal: #141414;
--rah-border: #1a1a1a;
--rah-border-strong: #2a2a2a;
--rah-border-stronger: #3a3a3a;
--rah-scrollbar: #353535;
--rah-scrollbar-hover: #4a4a4a;
--rah-text-base: #e5e5e5;
--rah-text-secondary: #c7c7c7;
--rah-text-muted: #7a7a7a;
--rah-text-soft: #a3a3a3;
--rah-text-active: #f0f0f0;
--rah-text-inverse: #0a0a0a;
--rah-accent-green: #22c55e;
--rah-accent-green-hover: #16a34a;
--rah-accent-green-soft: rgba(34, 197, 94, 0.14);
--rah-accent-green-soft-strong: rgba(34, 197, 94, 0.22);
--rah-backdrop: rgba(0, 0, 0, 0.85);
--rah-shadow-modal: 0 8px 32px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05);
--rah-shadow-floating: 0 4px 12px rgba(0, 0, 0, 0.4);
}
html[data-theme="light"] {
color-scheme: light;
--rah-bg-base: #ffffff;
--rah-bg-surface: #fafafa;
--rah-bg-subtle: #f7f7f7;
--rah-bg-panel: #f5f5f5;
--rah-bg-hover: #f0f0f0;
--rah-bg-elevated: #ececec;
--rah-bg-active: #e8e8e8;
--rah-bg-modal: #ffffff;
--rah-border: #e5e5e5;
--rah-border-strong: #d4d4d4;
--rah-border-stronger: #bdbdbd;
--rah-scrollbar: #d4d4d4;
--rah-scrollbar-hover: #b5b5b5;
--rah-text-base: #111111;
--rah-text-secondary: #333333;
--rah-text-muted: #6b7280;
--rah-text-soft: #4b5563;
--rah-text-active: #000000;
--rah-text-inverse: #ffffff;
--rah-accent-green: #16a34a;
--rah-accent-green-hover: #15803d;
--rah-accent-green-soft: rgba(22, 163, 74, 0.12);
--rah-accent-green-soft-strong: rgba(22, 163, 74, 0.2);
--rah-backdrop: rgba(0, 0, 0, 0.5);
--rah-shadow-modal: 0 10px 36px rgba(0, 0, 0, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.04);
--rah-shadow-floating: 0 6px 20px rgba(0, 0, 0, 0.16);
}
/* Modern Clean Design System - RA-H */
* {
margin: 0;
@@ -46,8 +107,8 @@
html, body {
height: 100%;
overflow: hidden;
background-color: #0a0a0a;
color: #e5e5e5;
background-color: var(--rah-bg-base);
color: var(--rah-text-base);
/* Geist Sans for optimal reading/writing experience */
font-family: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
@@ -71,21 +132,21 @@ html, body {
}
::-webkit-scrollbar-track {
background: #0a0a0a;
background: var(--rah-bg-base);
}
::-webkit-scrollbar-thumb {
background: #353535;
background: var(--rah-scrollbar);
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
background: #4a4a4a;
background: var(--rah-scrollbar-hover);
}
/* Utility classes */
.border-divider {
border-color: #1a1a1a;
border-color: var(--rah-border);
}
.text-muted {
+25 -1
View File
@@ -13,7 +13,31 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en">
<html lang="en" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var rawTheme = localStorage.getItem('ui.theme');
var theme = 'dark';
if (rawTheme !== null) {
try {
theme = JSON.parse(rawTheme) === 'light' ? 'light' : 'dark';
} catch (parseError) {
theme = rawTheme === 'light' ? 'light' : 'dark';
}
}
document.documentElement.setAttribute('data-theme', theme);
} catch (error) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
`,
}}
/>
</head>
<body>
<DimensionIconsProvider>
<ExternalNavigationManager />