45 lines
1.6 KiB
TypeScript
45 lines
1.6 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useSyncExternalStore } from "react";
|
|
import { applyTheme, currentTheme, DEFAULT_THEME, THEME_KEY, type Theme } from "./theme";
|
|
|
|
const listeners = new Set<() => void>();
|
|
|
|
function subscribe(listener: () => void): () => void {
|
|
listeners.add(listener);
|
|
const onStorage = (event: StorageEvent) => {
|
|
// Another tab changed the theme: follow it.
|
|
if (event.key !== THEME_KEY) return;
|
|
const next = event.newValue === "light" || event.newValue === "dark" ? event.newValue : DEFAULT_THEME;
|
|
applyTheme(next, document.documentElement, null);
|
|
listeners.forEach((l) => l());
|
|
};
|
|
window.addEventListener("storage", onStorage);
|
|
return () => {
|
|
listeners.delete(listener);
|
|
window.removeEventListener("storage", onStorage);
|
|
};
|
|
}
|
|
|
|
const getSnapshot = (): Theme => currentTheme(document.documentElement);
|
|
const getServerSnapshot = (): Theme => DEFAULT_THEME;
|
|
|
|
function safeStorage(): Storage | null {
|
|
try {
|
|
return window.localStorage;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Shared theme state for every toggle: the current theme and a setter. */
|
|
export function useTheme(): { theme: Theme; setTheme: (theme: Theme) => void; toggleTheme: () => void } {
|
|
const theme = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
const setTheme = useCallback((next: Theme) => {
|
|
applyTheme(next, document.documentElement, safeStorage());
|
|
listeners.forEach((l) => l());
|
|
}, []);
|
|
const toggleTheme = useCallback(() => setTheme(getSnapshot() === "dark" ? "light" : "dark"), [setTheme]);
|
|
return { theme, setTheme, toggleTheme };
|
|
}
|