From 784a914ebffedc0b6a3cccbcc9ed0697e8608e80 Mon Sep 17 00:00:00 2001 From: fahed Date: Wed, 25 Mar 2026 18:29:39 +0300 Subject: [PATCH] Colorize: Add dark mode with system/dark/light toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add prefers-color-scheme: dark media query for automatic dark mode - Add data-theme attribute for manual override (persisted to localStorage) - 3-state cycle: system → dark → light → system - Theme toggle button in nav with contextual icon (sun/moon/half) - Dark palette: slate-900 bg, slate-800 surfaces, adjusted text/accent/success/danger Co-Authored-By: Claude Opus 4.6 (1M context) --- src/App.css | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/App.tsx | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/src/App.css b/src/App.css index e9fb7f2..49fa02f 100644 --- a/src/App.css +++ b/src/App.css @@ -33,6 +33,70 @@ --radius: 12px; } +/* Dark mode */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg: #0f172a; + --surface: #1e293b; + --border: #334155; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-muted: #94a3b8; + --accent: #3b82f6; + --primary: #3b82f6; + --accent-light: #1e3a5f; + --success: #34d399; + --success-light: #064e3b; + --danger: #f87171; + --danger-light: #7f1d1d; + --brand-icon: #60a5fa; + --brand-text: #93c5fd; + --text-inverse: #0f172a; + --warning-bg: #451a03; + --warning-text: #fbbf24; + --warning-border: #78350f; + --accent-hover: #60a5fa; + --purple: #a78bfa; + --muted-light: #1e293b; + --dark-surface: #0f172a; + --dark-muted: #64748b; + --shadow-sm: 0 1px 2px rgba(0,0,0,0.3); + --shadow: 0 4px 12px rgba(0,0,0,0.4); + color-scheme: dark; + } +} + +/* Manual theme override */ +:root[data-theme="dark"] { + --bg: #0f172a; + --surface: #1e293b; + --border: #334155; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-muted: #94a3b8; + --accent: #3b82f6; + --primary: #3b82f6; + --accent-light: #1e3a5f; + --success: #34d399; + --success-light: #064e3b; + --danger: #f87171; + --danger-light: #7f1d1d; + --brand-icon: #60a5fa; + --brand-text: #93c5fd; + --text-inverse: #0f172a; + --warning-bg: #451a03; + --warning-text: #fbbf24; + --warning-border: #78350f; + --accent-hover: #60a5fa; + --purple: #a78bfa; + --muted-light: #1e293b; + --dark-surface: #0f172a; + --dark-muted: #64748b; + --shadow-sm: 0 1px 2px rgba(0,0,0,0.3); + --shadow: 0 4px 12px rgba(0,0,0,0.4); + color-scheme: dark; +} + * { margin: 0; padding: 0; diff --git a/src/App.tsx b/src/App.tsx index 8422ffb..93aa32d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -44,6 +44,30 @@ function App() { const [showDataLabels, setShowDataLabels] = useState(false); const [includeVAT, setIncludeVAT] = useState(true); const [dataSource, setDataSource] = useState('museums'); + const [theme, setTheme] = useState(() => { + if (typeof window !== 'undefined') { + return localStorage.getItem('hihala_theme') || 'system'; + } + return 'system'; + }); + + useEffect(() => { + const root = document.documentElement; + if (theme === 'system') { + root.removeAttribute('data-theme'); + } else { + root.setAttribute('data-theme', theme); + } + localStorage.setItem('hihala_theme', theme); + }, [theme]); + + const toggleTheme = () => { + setTheme(prev => { + if (prev === 'system') return 'dark'; + if (prev === 'dark') return 'light'; + return 'system'; + }); + }; const dataSources: DataSource[] = [ { id: 'museums', labelKey: 'dataSources.museums', enabled: true }, @@ -178,6 +202,22 @@ function App() { +