From 2dabd1f958b6f685035e1bfdff41ad9fa7b327bb Mon Sep 17 00:00:00 2001 From: Ronny Date: Sun, 12 Apr 2026 16:17:57 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20three-way=20theme=20toggle=20=E2=80=94?= =?UTF-8?q?=20Dark=20/=20Light=20/=20System?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cycles: Sun (→ Light) → Monitor (→ System) → Moon (→ Dark) System mode follows OS prefers-color-scheme and updates live. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/App.tsx | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 896545d..24f3fbf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { Routes, Route, NavLink } from 'react-router-dom'; -import { Sun, Moon } from 'lucide-react'; +import { Sun, Moon, Monitor } from 'lucide-react'; import DashboardPage from './pages/DashboardPage'; import MatchesPage from './pages/MatchesPage'; import LeaderboardPage from './pages/LeaderboardPage'; @@ -18,18 +18,26 @@ let DevPanel: React.ComponentType | null = null; // VITE_TEST_MODE wird erst zur Laufzeit geprüft, daher Import immer einbinden import('./components/DevPanel').then(m => { DevPanel = m.default; }).catch(() => {}); -type Theme = 'dark' | 'light'; +type ThemeSetting = 'dark' | 'light' | 'system'; -function getInitialTheme(): Theme { +function getInitialSetting(): ThemeSetting { try { - const stored = localStorage.getItem('theme') as Theme | null; - if (stored === 'light' || stored === 'dark') return stored; + const stored = localStorage.getItem('theme') as ThemeSetting | null; + if (stored === 'light' || stored === 'dark' || stored === 'system') return stored; } catch {} return 'dark'; } +function resolveTheme(setting: ThemeSetting): 'dark' | 'light' { + if (setting === 'system') { + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + return setting; +} + export default function App() { - const [theme, setTheme] = useState(getInitialTheme); + const [themeSetting, setThemeSetting] = useState(getInitialSetting); + const theme = resolveTheme(themeSetting); const { message: rankMsg, dismiss: dismissRank } = useRankChange(); const [devUser, setDevUser] = useState(1); const [devMatches, setDevMatches] = useState([]); @@ -38,11 +46,20 @@ export default function App() { // Theme auf setzen und in localStorage speichern useEffect(() => { document.documentElement.setAttribute('data-theme', theme); - try { localStorage.setItem('theme', theme); } catch {} - }, [theme]); + try { localStorage.setItem('theme', themeSetting); } catch {} + }, [theme, themeSetting]); + + // Listen for OS theme changes when in system mode + useEffect(() => { + if (themeSetting !== 'system') return; + const mq = window.matchMedia('(prefers-color-scheme: light)'); + const handler = () => setRefreshKey(k => k + 1); // force re-render to re-resolve + mq.addEventListener('change', handler); + return () => mq.removeEventListener('change', handler); + }, [themeSetting]); function toggleTheme() { - setTheme(t => t === 'dark' ? 'light' : 'dark'); + setThemeSetting(t => t === 'dark' ? 'light' : t === 'light' ? 'system' : 'dark'); } // DevUser als Query-Parameter im API-Fetch setzen @@ -105,10 +122,10 @@ export default function App() {