WFDASH-WEBAPP/src/lib/use-theme-id.ts

53 lines
1.9 KiB
TypeScript
Raw Normal View History

"use client";
import { useCallback, useEffect, useState } from "react";
import { DEFAULT_THEME_ID, getThemeById } from "@/lib/themes";
const STORAGE_KEY = "wfdash-theme";
/**
* useThemeId — reads / writes the active visual theme id.
*
* The theme id is applied to <html> via the `data-theme` attribute; the
* matching tokens live in src/styles/themes.css.
*
* On the first render the value is `DEFAULT_THEME_ID` — the bootstrap
* script in app/layout.tsx applies the persisted value before hydration,
* then a tiny effect in this hook syncs the React state to whatever the
* DOM already says. This is the only safe way to avoid a hydration
* mismatch without rendering nothing on the first frame.
*/
export function useThemeId() {
const [themeId, setThemeIdState] = useState<string>(DEFAULT_THEME_ID);
useEffect(() => {
// Pull whatever the bootstrap script already set on <html> so the
// first React render matches the DOM. Intentionally a setState-in-effect:
// we need to read the DOM after hydration, not before.
const fromDom = document.documentElement.getAttribute("data-theme");
if (fromDom && fromDom !== themeId) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setThemeIdState(fromDom);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const setThemeId = useCallback((id: string) => {
const t = getThemeById(id);
setThemeIdState(t.id);
try {
localStorage.setItem(STORAGE_KEY, t.id);
} catch {
/* ignore */
}
if (typeof document !== "undefined") {
const root = document.documentElement;
root.setAttribute("data-theme", t.id);
root.style.setProperty("--radius", t.radius);
root.style.setProperty("--font-heading", t.fontHeading);
root.style.setProperty("--font-body", t.fontBody);
}
}, []);
return { themeId, setThemeId };
}