83 lines
2.4 KiB
React
83 lines
2.4 KiB
React
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 <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
}
|
|
|
|
export function useTheme() {
|
|
const theme = useContext(ThemeContext);
|
|
if (!theme) throw new Error("ThemeProvider is required");
|
|
return theme;
|
|
}
|