feat: three-way theme toggle — Dark / Light / System
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) <noreply@anthropic.com>
This commit is contained in:
+28
-11
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Routes, Route, NavLink } from 'react-router-dom';
|
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 DashboardPage from './pages/DashboardPage';
|
||||||
import MatchesPage from './pages/MatchesPage';
|
import MatchesPage from './pages/MatchesPage';
|
||||||
import LeaderboardPage from './pages/LeaderboardPage';
|
import LeaderboardPage from './pages/LeaderboardPage';
|
||||||
@@ -18,18 +18,26 @@ let DevPanel: React.ComponentType<any> | null = null;
|
|||||||
// VITE_TEST_MODE wird erst zur Laufzeit geprüft, daher Import immer einbinden
|
// VITE_TEST_MODE wird erst zur Laufzeit geprüft, daher Import immer einbinden
|
||||||
import('./components/DevPanel').then(m => { DevPanel = m.default; }).catch(() => {});
|
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 {
|
try {
|
||||||
const stored = localStorage.getItem('theme') as Theme | null;
|
const stored = localStorage.getItem('theme') as ThemeSetting | null;
|
||||||
if (stored === 'light' || stored === 'dark') return stored;
|
if (stored === 'light' || stored === 'dark' || stored === 'system') return stored;
|
||||||
} catch {}
|
} catch {}
|
||||||
return 'dark';
|
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() {
|
export default function App() {
|
||||||
const [theme, setTheme] = useState<Theme>(getInitialTheme);
|
const [themeSetting, setThemeSetting] = useState<ThemeSetting>(getInitialSetting);
|
||||||
|
const theme = resolveTheme(themeSetting);
|
||||||
const { message: rankMsg, dismiss: dismissRank } = useRankChange();
|
const { message: rankMsg, dismiss: dismissRank } = useRankChange();
|
||||||
const [devUser, setDevUser] = useState(1);
|
const [devUser, setDevUser] = useState(1);
|
||||||
const [devMatches, setDevMatches] = useState<any[]>([]);
|
const [devMatches, setDevMatches] = useState<any[]>([]);
|
||||||
@@ -38,11 +46,20 @@ export default function App() {
|
|||||||
// Theme auf <html> setzen und in localStorage speichern
|
// Theme auf <html> setzen und in localStorage speichern
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
try { localStorage.setItem('theme', theme); } catch {}
|
try { localStorage.setItem('theme', themeSetting); } catch {}
|
||||||
}, [theme]);
|
}, [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() {
|
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
|
// DevUser als Query-Parameter im API-Fetch setzen
|
||||||
@@ -105,10 +122,10 @@ export default function App() {
|
|||||||
<button
|
<button
|
||||||
className={styles.themeToggle}
|
className={styles.themeToggle}
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
title={theme === 'dark' ? 'Light Mode aktivieren' : 'Dark Mode aktivieren'}
|
title={themeSetting === 'dark' ? 'Light Mode' : themeSetting === 'light' ? 'System' : 'Dark Mode'}
|
||||||
aria-label="Theme wechseln"
|
aria-label="Theme wechseln"
|
||||||
>
|
>
|
||||||
{theme === 'dark' ? <Sun size={16} /> : <Moon size={16} />}
|
{themeSetting === 'dark' ? <Sun size={16} /> : themeSetting === 'light' ? <Monitor size={16} /> : <Moon size={16} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user