Files
tech-log-frontend/src/presentation/providers/theme-provider.tsx
T

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