| 14 | const ThemeContext = createContext<ThemeContextValue | null>(null) |
| 15 | |
| 16 | export function ThemeProvider({ |
| 17 | children, |
| 18 | defaultTheme = 'dark', |
| 19 | storageKey = 'ui-theme', |
| 20 | }: { |
| 21 | children: React.ReactNode |
| 22 | defaultTheme?: Theme |
| 23 | storageKey?: string |
| 24 | }) { |
| 25 | const [theme, setThemeState] = useState<Theme>(defaultTheme) |
| 26 | const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>('dark') |
| 27 | |
| 28 | useEffect(() => { |
| 29 | const stored = localStorage.getItem(storageKey) as Theme | null |
| 30 | if (stored && ['dark', 'light', 'system'].includes(stored)) { |
| 31 | setThemeState(stored) |
| 32 | } |
| 33 | }, [storageKey]) |
| 34 | |
| 35 | useEffect(() => { |
| 36 | const root = document.documentElement |
| 37 | |
| 38 | const apply = (resolved: ResolvedTheme) => { |
| 39 | setResolvedTheme(resolved) |
| 40 | if (resolved === 'light') { |
| 41 | root.classList.add('light') |
| 42 | } else { |
| 43 | root.classList.remove('light') |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | if (theme === 'system') { |
| 48 | const mq = window.matchMedia('(prefers-color-scheme: light)') |
| 49 | apply(mq.matches ? 'light' : 'dark') |
| 50 | const handler = (e: MediaQueryListEvent) => apply(e.matches ? 'light' : 'dark') |
| 51 | mq.addEventListener('change', handler) |
| 52 | return () => mq.removeEventListener('change', handler) |
| 53 | } else { |
| 54 | apply(theme) |
| 55 | } |
| 56 | }, [theme]) |
| 57 | |
| 58 | const setTheme = (t: Theme) => { |
| 59 | setThemeState(t) |
| 60 | localStorage.setItem(storageKey, t) |
| 61 | } |
| 62 | |
| 63 | return ( |
| 64 | <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}> |
| 65 | {children} |
| 66 | </ThemeContext.Provider> |
| 67 | ) |
| 68 | } |
| 69 | |
| 70 | export function useTheme(): ThemeContextValue { |
| 71 | const ctx = useContext(ThemeContext) |