"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from "react"; import { type ThemeDefinition, darkTheme, getThemeOrDefault, isValidThemeId, themeToVariables, } from "@/themes"; interface ThemeContextValue { /** User preference: a theme ID (e.g. "dark", "nord") or "system" */ theme: string; /** The active theme's ID after resolving "system" */ themeId: string; /** The full active ThemeDefinition object */ themeDefinition: ThemeDefinition; /** "light" or "dark" classification of the active theme */ resolvedTheme: "light" | "dark"; /** Set theme by ID or "system" */ setTheme: (theme: string) => void; /** Quick toggle between "dark" and "light" themes */ toggleTheme: () => void; } const ThemeContext = createContext(null); const STORAGE_KEY = "mosaic-theme"; function getSystemThemeId(): "light" | "dark" { if (typeof window === "undefined") return "dark"; return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } function getStoredPreference(): string { if (typeof window === "undefined") return "system"; const stored = localStorage.getItem(STORAGE_KEY); if (stored && (stored === "system" || isValidThemeId(stored))) { return stored; } return "system"; } function resolveThemeId(preference: string): string { if (preference === "system") return getSystemThemeId(); return preference; } function applyThemeVariables(themeDef: ThemeDefinition): void { const root = document.documentElement; const vars = themeToVariables(themeDef); for (const [prop, value] of Object.entries(vars)) { root.style.setProperty(prop, value); } // Set data-theme attribute for CSS selectors that depend on light/dark root.setAttribute("data-theme", themeDef.isDark ? "dark" : "light"); } interface ThemeProviderProps { children: ReactNode; defaultTheme?: string; } export function ThemeProvider({ children, defaultTheme = "system", }: ThemeProviderProps): React.JSX.Element { const [preference, setPreference] = useState(defaultTheme); const [mounted, setMounted] = useState(false); const themeId = useMemo(() => resolveThemeId(preference), [preference]); const themeDefinition = useMemo(() => getThemeOrDefault(themeId), [themeId]); const resolvedTheme = themeDefinition.isDark ? "dark" : "light"; // Initialize from storage on mount useEffect(() => { setMounted(true); const stored = getStoredPreference(); setPreference(stored); const id = resolveThemeId(stored); const def = getThemeOrDefault(id); applyThemeVariables(def); }, []); // Apply theme whenever preference changes (after mount) useEffect(() => { if (!mounted) return; applyThemeVariables(themeDefinition); }, [themeDefinition, mounted]); // Listen for system theme changes when preference is "system" useEffect(() => { if (!mounted || preference !== "system") return; const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const handleChange = (e: MediaQueryListEvent): void => { const id = e.matches ? "dark" : "light"; const def = getThemeOrDefault(id); applyThemeVariables(def); // Force re-render by updating preference to trigger useMemo recalc setPreference("system"); }; mediaQuery.addEventListener("change", handleChange); return (): void => { mediaQuery.removeEventListener("change", handleChange); }; }, [preference, mounted]); const setTheme = useCallback((newPreference: string) => { setPreference(newPreference); localStorage.setItem(STORAGE_KEY, newPreference); }, []); const toggleTheme = useCallback(() => { setTheme(resolvedTheme === "dark" ? "light" : "dark"); }, [resolvedTheme, setTheme]); // SSR placeholder — render children but with dark defaults if (!mounted) { return ( { /* no-op during SSR */ }, toggleTheme: (): void => { /* no-op during SSR */ }, }} > {children} ); } return ( {children} ); } export function useTheme(): ThemeContextValue { const context = useContext(ThemeContext); if (!context) { throw new Error("useTheme must be used within a ThemeProvider"); } return context; }