2026-06-26 15:59:40 +00:00
|
|
|
"use client";
|
|
|
|
|
|
2026-07-07 15:22:03 +00:00
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
|
|
import { DEFAULT_THEME_ID, getThemeById } from "@/lib/themes";
|
|
|
|
|
|
|
|
|
|
const STORAGE_KEY = "wfdash-theme";
|
2026-06-26 15:59:40 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* useThemeId — reads / writes the active visual theme id.
|
|
|
|
|
*
|
2026-07-07 15:22:03 +00:00
|
|
|
* The theme id is applied to <html> via the `data-theme` attribute; the
|
|
|
|
|
* matching tokens live in src/styles/themes.css.
|
2026-06-26 15:59:40 +00:00
|
|
|
*
|
2026-07-07 15:22:03 +00:00
|
|
|
* 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.
|
2026-06-26 15:59:40 +00:00
|
|
|
*/
|
|
|
|
|
export function useThemeId() {
|
2026-07-07 15:22:03 +00:00
|
|
|
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);
|
2026-06-26 15:59:40 +00:00
|
|
|
}
|
2026-07-07 15:22:03 +00:00
|
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
2026-06-26 15:59:40 +00:00
|
|
|
|
|
|
|
|
const setThemeId = useCallback((id: string) => {
|
2026-07-07 15:22:03 +00:00
|
|
|
const t = getThemeById(id);
|
|
|
|
|
setThemeIdState(t.id);
|
2026-06-26 15:59:40 +00:00
|
|
|
try {
|
2026-07-07 15:22:03 +00:00
|
|
|
localStorage.setItem(STORAGE_KEY, t.id);
|
2026-06-26 15:59:40 +00:00
|
|
|
} catch {
|
|
|
|
|
/* ignore */
|
|
|
|
|
}
|
|
|
|
|
if (typeof document !== "undefined") {
|
|
|
|
|
const root = document.documentElement;
|
2026-07-07 15:22:03 +00:00
|
|
|
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);
|
2026-06-26 15:59:40 +00:00
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return { themeId, setThemeId };
|
|
|
|
|
}
|