Files
buzz/web/src/shared/theme/ThemeProvider.tsx

87 lines
2.1 KiB
TypeScript

import {
type ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
type Theme = "light" | "dark" | "system";
type ThemeContextValue = {
theme: Theme;
isDark: boolean;
setTheme: (theme: Theme) => void;
};
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
function getSystemDark(): boolean {
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
function getInitialTheme(): Theme {
if (!import.meta.env.DEV) return "system";
const previewTheme = new URLSearchParams(window.location.search).get(
"previewTheme",
);
return previewTheme === "light" || previewTheme === "dark"
? previewTheme
: "system";
}
function applyClass(isDark: boolean) {
const root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(isDark ? "dark" : "light");
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
const [isDark, setIsDark] = useState<boolean>(() => {
const initialTheme = getInitialTheme();
const dark =
initialTheme === "system" ? getSystemDark() : initialTheme === "dark";
applyClass(dark);
return dark;
});
useEffect(() => {
const dark = theme === "system" ? getSystemDark() : theme === "dark";
applyClass(dark);
setIsDark(dark);
}, [theme]);
useEffect(() => {
if (theme !== "system") return;
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const handler = (e: MediaQueryListEvent) => {
applyClass(e.matches);
setIsDark(e.matches);
};
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, [theme]);
const setTheme = useCallback((t: Theme) => {
setThemeState(t);
}, []);
return (
<ThemeContext.Provider value={{ theme, isDark, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}