67 lines
1.6 KiB
TypeScript
67 lines
1.6 KiB
TypeScript
"use client";
|
|
|
|
// Light/dark theme state. The actual `.dark` class is set on <html> by an inline
|
|
// script in the root layout (before paint, to avoid a flash) and kept in sync
|
|
// here. Preference persists in localStorage.
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
} from "react";
|
|
|
|
export const THEME_STORAGE_KEY = "aram_theme";
|
|
type Theme = "light" | "dark";
|
|
|
|
interface ThemeApi {
|
|
theme: Theme;
|
|
toggle: () => void;
|
|
setTheme: (t: Theme) => void;
|
|
}
|
|
|
|
const ThemeContext = createContext<ThemeApi | null>(null);
|
|
|
|
function applyClass(theme: Theme) {
|
|
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
}
|
|
|
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
const [theme, setThemeState] = useState<Theme>("light");
|
|
|
|
// Read whatever the no-flash script already decided.
|
|
useEffect(() => {
|
|
const isDark = document.documentElement.classList.contains("dark");
|
|
setThemeState(isDark ? "dark" : "light");
|
|
}, []);
|
|
|
|
const setTheme = useCallback((t: Theme) => {
|
|
setThemeState(t);
|
|
applyClass(t);
|
|
try {
|
|
window.localStorage.setItem(THEME_STORAGE_KEY, t);
|
|
} catch {
|
|
/* ignore (private mode) */
|
|
}
|
|
}, []);
|
|
|
|
const toggle = useCallback(() => {
|
|
setTheme(
|
|
document.documentElement.classList.contains("dark") ? "light" : "dark",
|
|
);
|
|
}, [setTheme]);
|
|
|
|
return (
|
|
<ThemeContext.Provider value={{ theme, toggle, setTheme }}>
|
|
{children}
|
|
</ThemeContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useTheme(): ThemeApi {
|
|
const ctx = useContext(ThemeContext);
|
|
if (!ctx) throw new Error("useTheme must be used within <ThemeProvider>");
|
|
return ctx;
|
|
}
|