Files
clean-architecture-frontend…/src/presentation/providers/theme-provider.jsx
T

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;
}