import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'; import { ACCENTS, applyTheme, readAccent, readFonts, readTheme, type AccentId, type FontSetId, type ThemeMode } from './themes'; interface ThemeValue { mode: ThemeMode; accent: AccentId; fonts: FontSetId; setMode: (mode: ThemeMode) => void; setAccent: (accent: AccentId) => void; setFonts: (fonts: FontSetId) => void; } const Ctx = createContext(null); // The tokens live in CSS; this provider only owns the three attributes on // and the persisted choice. No component ever reads a colour from JS. export function ThemeProvider({ children }: { children: ReactNode }) { const [mode, setModeState] = useState(readTheme); const [accent, setAccentState] = useState(readAccent); const [fonts, setFontsState] = useState(readFonts); useEffect(() => { applyTheme(mode, accent, fonts); // Auto has to follow the OS while the page is open, not just at mount. if (mode !== 'auto') return; const media = window.matchMedia('(prefers-color-scheme: light)'); const onChange = () => applyTheme(mode, accent, fonts); media.addEventListener('change', onChange); return () => media.removeEventListener('change', onChange); }, [mode, accent, fonts]); const setMode = useCallback((next: ThemeMode) => setModeState(next), []); const setAccent = useCallback((next: AccentId) => setAccentState(next), []); const setFonts = useCallback((next: FontSetId) => setFontsState(next), []); const value = useMemo( () => ({ mode, accent, fonts, setMode, setAccent, setFonts }), [mode, accent, fonts, setMode, setAccent, setFonts] ); return {children}; } export function useTheme(): ThemeValue { const value = useContext(Ctx); if (!value) throw new Error('useTheme used outside ThemeProvider'); return value; } export { ACCENTS, FONT_SETS } from './themes'; export type { AccentId, FontSetId, ThemeMode } from './themes';