import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useState, } from "react"; import { normalizeColorSchemePreference, resolveColorScheme, } from "../../application/policies/color-scheme.js"; import { useApplication } from "./application-provider.js"; /** * @typedef {{ * preference: "system" | "light" | "dark", * resolvedTheme: "light" | "dark", * setPreference: (preference: "system" | "light" | "dark") => void * }} ThemeContextValue */ const ThemeContext = createContext(/** @type {ThemeContextValue | null} */ (null)); function systemPrefersDark() { return ( typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches ); } /** * @param {{ children: React.ReactNode }} props */ export function ThemeProvider({ children }) { const { preferences } = useApplication(); const [preference, updatePreference] = useState( preferences.getColorScheme, ); const [darkSystemTheme, setDarkSystemTheme] = useState(systemPrefersDark); const resolvedTheme = resolveColorScheme(preference, darkSystemTheme); useEffect(() => { if (typeof window.matchMedia !== "function") return undefined; const query = window.matchMedia("(prefers-color-scheme: dark)"); /** @param {MediaQueryListEvent} event */ const update = (event) => setDarkSystemTheme(event.matches); setDarkSystemTheme(query.matches); query.addEventListener("change", update); return () => query.removeEventListener("change", update); }, []); useLayoutEffect(() => { document.documentElement.dataset.theme = resolvedTheme; document.documentElement.dataset.themePreference = preference; document.documentElement.style.colorScheme = resolvedTheme; }, [preference, resolvedTheme]); const value = useMemo( () => Object.freeze({ preference, resolvedTheme, /** @param {"system" | "light" | "dark"} next */ setPreference(next) { const normalized = normalizeColorSchemePreference(next); updatePreference(normalized); preferences.setColorScheme(normalized); }, }), [preference, preferences, resolvedTheme], ); return {children}; } export function useTheme() { const theme = useContext(ThemeContext); if (!theme) throw new Error("ThemeProvider is required"); return theme; }