diff --git a/.gitignore b/.gitignore index b9474a2..4268db8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist/ .env .env.local *.local +meta.json diff --git a/src/components/ThemeSwitcher.tsx b/src/components/ThemeSwitcher.tsx index 54174bb..0f59d1c 100644 --- a/src/components/ThemeSwitcher.tsx +++ b/src/components/ThemeSwitcher.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect } from 'react'; -import { Palette, Sun, Moon, Monitor, Check } from 'lucide-react'; +import { Palette, Sun, Moon, Monitor, Laptop, Check } from 'lucide-react'; import { useTheme } from '../hooks/useTheme'; import type { ThemeName, AccentColor } from '../contexts/ThemeContextDef'; import { useT } from '../hooks/useLocale'; @@ -7,6 +7,7 @@ import { useT } from '../hooks/useLocale'; import type { TranslationKey } from '../lib/i18n'; const themeOptions: { value: ThemeName; icon: typeof Sun; labelKey: TranslationKey }[] = [ + { value: 'system', icon: Laptop, labelKey: 'theme.system' }, { value: 'dark', icon: Moon, labelKey: 'theme.dark' }, { value: 'light', icon: Sun, labelKey: 'theme.light' }, { value: 'oled', icon: Monitor, labelKey: 'theme.oled' }, diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index b2af4d1..7b1948c 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -10,7 +10,8 @@ interface StoredTheme { accent: AccentColor; } -const themes: Record> = { +type ConcreteTheme = 'dark' | 'light' | 'oled'; +const themes: Record> = { dark: { '--pc-bg-base': '#1e1e24', '--pc-bg-surface': '#232329', @@ -131,12 +132,20 @@ function applyVars(vars: Record) { } } +/** Resolve 'system' to the actual theme based on OS preference. */ +function resolveTheme(name: ThemeName): 'dark' | 'light' | 'oled' { + if (name === 'system') { + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + return name; +} + function loadStored(): StoredTheme { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw); - if (parsed.theme in themes && parsed.accent in accents) return parsed; + if ((parsed.theme in themes || parsed.theme === 'system') && parsed.accent in accents) return parsed; } } catch { /* ignore invalid stored JSON */ } return { theme: 'dark', accent: 'cyan' }; @@ -153,7 +162,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { const setTheme = useCallback((t: ThemeName) => { setThemeState(t); - applyVars(themes[t]); + applyVars(themes[resolveTheme(t)]); persist(t, accent); }, [accent, persist]); @@ -165,9 +174,18 @@ export function ThemeProvider({ children }: { children: ReactNode }) { // Apply on mount useEffect(() => { - applyVars({ ...themes[theme], ...accents[accent] }); + applyVars({ ...themes[resolveTheme(theme)], ...accents[accent] }); }, []); // eslint-disable-line react-hooks/exhaustive-deps + // Listen to OS color scheme changes when theme is 'system' + useEffect(() => { + if (theme !== 'system') return; + const mq = window.matchMedia('(prefers-color-scheme: light)'); + const handler = () => applyVars(themes[mq.matches ? 'light' : 'dark']); + mq.addEventListener('change', handler); + return () => mq.removeEventListener('change', handler); + }, [theme]); + return ( {children} diff --git a/src/contexts/ThemeContextDef.ts b/src/contexts/ThemeContextDef.ts index cc7708f..6e7cb5d 100644 --- a/src/contexts/ThemeContextDef.ts +++ b/src/contexts/ThemeContextDef.ts @@ -1,6 +1,6 @@ import { createContext } from 'react'; -export type ThemeName = 'dark' | 'light' | 'oled'; +export type ThemeName = 'dark' | 'light' | 'oled' | 'system'; export type AccentColor = 'cyan' | 'violet' | 'emerald' | 'amber' | 'rose' | 'blue'; export interface ThemeContextValue { diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts index 4bcf5ad..3feac0a 100644 --- a/src/lib/i18n.ts +++ b/src/lib/i18n.ts @@ -113,6 +113,7 @@ const en = { 'theme.title': 'Theme', 'theme.mode': 'Mode', 'theme.accent': 'Accent', + 'theme.system': 'System', 'theme.dark': 'Dark', 'theme.light': 'Light', 'theme.oled': 'OLED', @@ -218,6 +219,7 @@ const fr: Record = { 'theme.title': 'Thème', 'theme.mode': 'Mode', 'theme.accent': 'Accent', + 'theme.system': 'Système', 'theme.dark': 'Sombre', 'theme.light': 'Clair', 'theme.oled': 'OLED',