import { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode } from 'react'; import { writeUiPref } from './uiPref'; import { GetUIPref } from '../../wailsjs/go/main/App'; // 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' | 'light-cool' | 'light-sage' | 'dim-slate' | 'dark-warm' | 'dark-graphite' | 'high-contrast'; // Selectable, concrete themes (excludes 'auto') in display order. export const CONCRETE_THEMES: Exclude[] = [ 'light-warm', 'light-cool', 'light-sage', 'dim-slate', '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()); // Set once the operator changes the theme by hand, so the self-heal below // never clobbers a fresh choice with a value it read a moment earlier. const userPicked = useRef(false); const setTheme = useCallback((t: ThemeChoice) => { userPicked.current = true; setThemeState(t); applyThemeToDom(t); writeUiPref(LS_KEY, t); }, []); // Self-heal the persisted theme. The synchronous boot read (localStorage) can // miss it when the WebView cleared its storage, OR when syncPortablePrefs ran // while the backend was still starting (settings store not wired yet → GetUIPref // returned "" with no error, so nothing was restored) — the "restart lands on // the light theme sometimes" bug. Re-read the portable pref from the DB once the // backend is up and apply it, retrying briefly to ride out a slow startup. useEffect(() => { let cancelled = false; let tries = 0; const load = () => { tries += 1; GetUIPref(LS_KEY).then((raw) => { if (cancelled || userPicked.current) return; const v = raw as ThemeChoice; if (v && ALL.includes(v)) { try { localStorage.setItem(LS_KEY, v); } catch { /* quota */ } applyThemeToDom(v); // idempotent — safe to call unconditionally setThemeState(v); return; // restored } if (tries < 8) window.setTimeout(load, 300); // empty (unset or backend not ready yet) → retry }).catch(() => { if (!cancelled && tries < 8) window.setTimeout(load, 300); }); }; load(); return () => { cancelled = true; }; }, []); // 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}; }