81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
import {
|
|
createContext,
|
|
useContext,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
|
|
import type { ColorSchemePreference } from "../../application/ports/in/application-api.ts";
|
|
import {
|
|
normalizeColorSchemePreference,
|
|
resolveColorScheme,
|
|
} from "../../application/policies/color-scheme.ts";
|
|
import { useApplication } from "./application-provider.tsx";
|
|
|
|
export type ThemeContextValue = Readonly<{
|
|
preference: ColorSchemePreference;
|
|
resolvedTheme: "light" | "dark";
|
|
setPreference(preference: ColorSchemePreference): void;
|
|
}>;
|
|
|
|
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
|
|
function systemPrefersDark(): boolean {
|
|
return (
|
|
typeof window.matchMedia === "function" &&
|
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
);
|
|
}
|
|
|
|
export function ThemeProvider({
|
|
children,
|
|
}: Readonly<{ children: ReactNode }>) {
|
|
const { preferences } = useApplication();
|
|
const [preference, updatePreference] = useState<ColorSchemePreference>(
|
|
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)");
|
|
const update = (event: MediaQueryListEvent) =>
|
|
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<ThemeContextValue>(
|
|
() =>
|
|
Object.freeze({
|
|
preference,
|
|
resolvedTheme,
|
|
setPreference(next: ColorSchemePreference) {
|
|
const normalized = normalizeColorSchemePreference(next);
|
|
updatePreference(normalized);
|
|
preferences.setColorScheme(normalized);
|
|
},
|
|
}),
|
|
[preference, preferences, resolvedTheme],
|
|
);
|
|
|
|
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
}
|
|
|
|
export function useTheme(): ThemeContextValue {
|
|
const theme = useContext(ThemeContext);
|
|
if (!theme) throw new Error("ThemeProvider is required");
|
|
return theme;
|
|
}
|