import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'; /** * Themes: 'light' | 'dark' | 'system' * Persisted to localStorage under key 'bt-theme'. * * 'system' follows the device's `prefers-color-scheme` and updates live when the * OS flips. `resolvedTheme` is always the concrete 'light' | 'dark' actually * applied — consume it for anything that needs a real value (e.g. Sonner). * * Class mapping on document.documentElement: * light → (no classes) dark → 'dark' */ type Theme = 'light' | 'dark' | 'system'; type ResolvedTheme = 'light' | 'dark'; const STORAGE_KEY = 'bt-theme'; const VALID_THEMES: Theme[] = ['light', 'dark', 'system']; const DEFAULT_THEME: Theme = 'dark'; // Keep in sync with the inline pre-bundle script in index.html. const THEME_COLORS: Record = { light: '#f8faf9', dark: '#101417' }; function systemPrefersDark(): boolean { return typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-color-scheme: dark)').matches; } function resolveTheme(theme: Theme): ResolvedTheme { if (theme === 'system') return systemPrefersDark() ? 'dark' : 'light'; return theme; } function applyTheme(theme: Theme): ResolvedTheme { const resolved = resolveTheme(theme); const root = document.documentElement; root.classList.toggle('dark', resolved === 'dark'); const meta = document.querySelector('meta[name="theme-color"]'); if (meta) meta.setAttribute('content', THEME_COLORS[resolved]); return resolved; } function loadStoredTheme(): Theme { try { const stored = localStorage.getItem(STORAGE_KEY); if (stored === 'dark-purple') return 'dark'; if (stored && (VALID_THEMES as string[]).includes(stored)) return stored as Theme; } catch { // localStorage unavailable } return DEFAULT_THEME; } interface ThemeContextValue { theme: Theme; resolvedTheme: ResolvedTheme; setTheme: (theme: string) => void; } const ThemeContext = createContext(null); export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setThemeState] = useState(() => { const stored = loadStoredTheme(); // Apply immediately (before first paint) to avoid flash. applyTheme(stored); return stored; }); const [resolvedTheme, setResolvedTheme] = useState(() => resolveTheme(loadStoredTheme())); const setTheme = (newTheme: string) => { if (!(VALID_THEMES as string[]).includes(newTheme)) return; const next = newTheme as Theme; setResolvedTheme(applyTheme(next)); setThemeState(next); try { localStorage.setItem(STORAGE_KEY, next); } catch { // localStorage unavailable } }; // Keep DOM in sync if theme state ever changes externally. useEffect(() => { setResolvedTheme(applyTheme(theme)); }, [theme]); // While on 'system', re-apply live when the OS light/dark preference flips. useEffect(() => { if (theme !== 'system' || typeof window === 'undefined' || !window.matchMedia) return; const mq = window.matchMedia('(prefers-color-scheme: dark)'); const onChange = () => setResolvedTheme(applyTheme('system')); mq.addEventListener('change', onChange); return () => mq.removeEventListener('change', onChange); }, [theme]); return ( {children} ); } export function useTheme() { const ctx = useContext(ThemeContext); if (!ctx) { throw new Error('useTheme must be used within a ThemeProvider'); } return ctx; }