import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'; import { writeUiPref } from './uiPref'; // Theme system. Each choice maps to a `data-theme` value on that the // CSS variables in style.css key off of. 'auto' follows the OS light/dark // preference. The choice is persisted (localStorage + portable UI pref, so it // travels with the data/ folder like the language). export type ThemeChoice = 'auto' | 'light-warm' | 'dark-warm' | 'dark-graphite' | 'high-contrast'; // Selectable, concrete themes (excludes 'auto') in display order. export const CONCRETE_THEMES: Exclude[] = [ 'light-warm', 'dark-warm', 'dark-graphite', 'high-contrast', ]; export const LS_KEY = 'opslog.theme'; const DEFAULT: ThemeChoice = 'light-warm'; const ALL: ThemeChoice[] = ['auto', ...CONCRETE_THEMES]; function systemDark(): boolean { try { return window.matchMedia('(prefers-color-scheme: dark)').matches; } catch { return false; } } // Resolve 'auto' to a concrete data-theme value (graphite dark / warm light). function resolve(choice: ThemeChoice): string { if (choice === 'auto') return systemDark() ? 'dark-graphite' : 'light-warm'; return choice; } function readStored(): ThemeChoice { try { const v = localStorage.getItem(LS_KEY) as ThemeChoice | null; if (v && ALL.includes(v)) return v; } catch { /* private mode */ } return DEFAULT; } // Stamp the resolved theme onto . export function applyThemeToDom(choice: ThemeChoice): void { try { document.documentElement.setAttribute('data-theme', resolve(choice)); } catch { /* no DOM */ } } // Call before the first render (main.tsx) so there is no flash of the default // palette while React boots. export function initTheme(): void { applyThemeToDom(readStored()); } type Ctx = { theme: ThemeChoice; setTheme: (t: ThemeChoice) => void }; const ThemeCtx = createContext({ theme: DEFAULT, setTheme: () => {} }); export function useTheme(): Ctx { return useContext(ThemeCtx); } export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setThemeState] = useState(() => readStored()); const setTheme = useCallback((t: ThemeChoice) => { setThemeState(t); applyThemeToDom(t); writeUiPref(LS_KEY, t); }, []); // While in 'auto', re-resolve when the OS light/dark preference flips. useEffect(() => { if (theme !== 'auto') return; const mq = window.matchMedia('(prefers-color-scheme: dark)'); const onChange = () => applyThemeToDom('auto'); mq.addEventListener('change', onChange); return () => mq.removeEventListener('change', onChange); }, [theme]); // Keep the DOM attribute in sync with state. useEffect(() => { applyThemeToDom(theme); }, [theme]); return {children}; }