|
|
|
|
| import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; |
|
|
| type Theme = 'light' | 'dark' | 'system'; |
|
|
| interface ThemeContextType { |
| theme: Theme; |
| setTheme: (theme: Theme) => void; |
| resolvedTheme: 'light' | 'dark'; |
| } |
|
|
| const ThemeContext = createContext<ThemeContextType | undefined>(undefined); |
|
|
| export function ThemeProvider({ children }: { children: ReactNode }) { |
| const [theme, setThemeState] = useState<Theme>('system'); |
| const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>('light'); |
|
|
| const resolvedTheme = theme === 'system' ? systemTheme : theme; |
|
|
| |
| |
| useEffect(() => { |
| const stored = localStorage.getItem('theme') as Theme | null; |
| if (stored && ['light', 'dark', 'system'].includes(stored)) { |
| setThemeState(stored); |
| } |
| setSystemTheme(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); |
| }, []); |
| |
|
|
| |
| useEffect(() => { |
| const root = document.documentElement; |
| if (resolvedTheme === 'dark') { |
| root.classList.add('dark'); |
| } else { |
| root.classList.remove('dark'); |
| } |
| }, [resolvedTheme]); |
|
|
| |
| useEffect(() => { |
| const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); |
| const handleChange = () => { |
| setSystemTheme(mediaQuery.matches ? 'dark' : 'light'); |
| }; |
| mediaQuery.addEventListener('change', handleChange); |
| return () => mediaQuery.removeEventListener('change', handleChange); |
| }, []); |
|
|
| |
| const handleSetTheme = (newTheme: Theme) => { |
| setThemeState(newTheme); |
| localStorage.setItem('theme', newTheme); |
| }; |
|
|
| return ( |
| <ThemeContext.Provider value={{ theme, setTheme: handleSetTheme, resolvedTheme }}> |
| {children} |
| </ThemeContext.Provider> |
| ); |
| } |
|
|
| export function useTheme() { |
| const context = useContext(ThemeContext); |
| if (!context) { |
| throw new Error('useTheme must be used within ThemeProvider'); |
| } |
| return context; |
| } |
|
|