Mercurial
comparison hg-web/src/components/theme.tsx @ 193:9f4429c49733 hg-web
[HgWeb] Making progress....
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 25 Jan 2026 20:04:55 -0800 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 192:b818a4561a3c | 193:9f4429c49733 |
|---|---|
| 1 import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; | |
| 2 | |
| 3 interface ThemeContextType { | |
| 4 isDark: boolean; | |
| 5 toggleTheme: () => void; | |
| 6 } | |
| 7 | |
| 8 const ThemeContext = createContext<ThemeContextType | undefined>(undefined); | |
| 9 | |
| 10 // Apply theme class to document root | |
| 11 function applyTheme(isDark: boolean) { | |
| 12 const root = document.documentElement; | |
| 13 if (isDark) { | |
| 14 root.classList.add('dark'); | |
| 15 root.classList.remove('light'); | |
| 16 } else { | |
| 17 root.classList.add('light'); | |
| 18 root.classList.remove('dark'); | |
| 19 } | |
| 20 } | |
| 21 | |
| 22 interface ThemeProviderProps { | |
| 23 children: ReactNode; | |
| 24 } | |
| 25 | |
| 26 function ThemeProvider({ children }: ThemeProviderProps) { | |
| 27 const [isDark, setIsDark] = useState(() => { | |
| 28 const saved = localStorage.getItem('theme'); | |
| 29 if (saved) return saved === 'dark'; | |
| 30 return window.matchMedia('(prefers-color-scheme: dark)').matches; | |
| 31 }); | |
| 32 | |
| 33 // Apply theme on mount and change | |
| 34 useEffect(() => { | |
| 35 applyTheme(isDark); | |
| 36 localStorage.setItem('theme', isDark ? 'dark' : 'light'); | |
| 37 }, [isDark]); | |
| 38 | |
| 39 // Listen for system theme changes | |
| 40 useEffect(() => { | |
| 41 const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); | |
| 42 const handleChange = (e: MediaQueryListEvent) => { | |
| 43 // Only apply if no explicit preference is saved | |
| 44 if (!localStorage.getItem('theme')) { | |
| 45 setIsDark(e.matches); | |
| 46 } | |
| 47 }; | |
| 48 | |
| 49 mediaQuery.addEventListener('change', handleChange); | |
| 50 return () => mediaQuery.removeEventListener('change', handleChange); | |
| 51 }, []); | |
| 52 | |
| 53 const toggleTheme = useCallback(() => { | |
| 54 setIsDark(prev => !prev); | |
| 55 }, []); | |
| 56 | |
| 57 return ( | |
| 58 <ThemeContext.Provider value={{ isDark, toggleTheme }}> | |
| 59 {children} | |
| 60 </ThemeContext.Provider> | |
| 61 ); | |
| 62 } | |
| 63 | |
| 64 function useTheme(): ThemeContextType { | |
| 65 const context = useContext(ThemeContext); | |
| 66 if (context === undefined) { | |
| 67 throw new Error('useTheme must be used within a ThemeProvider'); | |
| 68 } | |
| 69 return context; | |
| 70 } | |
| 71 | |
| 72 export { ThemeProvider, useTheme }; |