"use client"; import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import type { Palette, ThemeMode, FontPair, Density, DashLayout, CalView, NavStyle, } from "@/modules/_core/themes"; import { navStyleToDataNav } from "@/modules/_core/themes"; import { setUserTheme } from "@/app/settings/actions"; const MOBILE_QUERY = "(max-width: 759px)"; const DARK_QUERY = "(prefers-color-scheme: dark)"; function applyTheme(state: { palette: Palette; mode: ThemeMode; fontPair: FontPair; density: Density; navStyle: NavStyle; }) { const html = document.documentElement; html.setAttribute("data-theme", state.palette); html.setAttribute("data-font-pair", state.fontPair); html.setAttribute("data-density", state.density); const isMobile = window.matchMedia(MOBILE_QUERY).matches; const desktopNav = navStyleToDataNav(state.navStyle); const dataNav = isMobile ? (state.navStyle === "fab-only" ? "fab" : "bottom") : desktopNav; html.setAttribute("data-nav", dataNav); const dark = state.mode === "dark" || (state.mode === "system" && window.matchMedia(DARK_QUERY).matches); html.classList.toggle("dark", dark); try { localStorage.setItem("themePalette", state.palette); localStorage.setItem("themeMode", state.mode); localStorage.setItem("themeFontPair", state.fontPair); localStorage.setItem("themeDensity", state.density); localStorage.setItem("themeNavStyle", state.navStyle); } catch { // storage blocked } } export function useTheme( initial: { palette?: Palette; mode?: ThemeMode; fontPair?: FontPair; density?: Density; dashLayout?: DashLayout; calView?: CalView; navStyle?: NavStyle; } = {}, signedIn = false, ) { const [palette, setPaletteState] = useState(initial.palette ?? "clay"); const [mode, setModeState] = useState(initial.mode ?? "system"); const [fontPair, setFontPairState] = useState(initial.fontPair ?? "serif-sans"); const [density, setDensityState] = useState(initial.density ?? "regular"); const [dashLayout, setDashLayoutState] = useState(initial.dashLayout ?? "classic"); const [calView, setCalViewState] = useState(initial.calView ?? "month"); const [navStyle, setNavStyleState] = useState(initial.navStyle ?? "rail-desktop"); const router = useRouter(); // Re-apply data-nav whenever the viewport crosses the mobile breakpoint. useEffect(() => { const mq = window.matchMedia(MOBILE_QUERY); const onChange = () => applyTheme({ palette, mode, fontPair, density, navStyle }); mq.addEventListener("change", onChange); return () => mq.removeEventListener("change", onChange); }, [palette, mode, fontPair, density, navStyle]); // Re-apply dark when system pref flips and we're in 'system' mode. useEffect(() => { if (mode !== "system") return; const mq = window.matchMedia(DARK_QUERY); const onChange = () => applyTheme({ palette, mode, fontPair, density, navStyle }); mq.addEventListener("change", onChange); return () => mq.removeEventListener("change", onChange); }, [palette, mode, fontPair, density, navStyle]); const persist = useCallback( (patch: Parameters[0]) => { if (signedIn) void setUserTheme(patch); }, [signedIn], ); const setPalette = useCallback( (next: Palette) => { setPaletteState(next); applyTheme({ palette: next, mode, fontPair, density, navStyle }); persist({ palette: next }); }, [mode, fontPair, density, navStyle, persist], ); const setMode = useCallback( (next: ThemeMode) => { setModeState(next); applyTheme({ palette, mode: next, fontPair, density, navStyle }); persist({ mode: next }); }, [palette, fontPair, density, navStyle, persist], ); const setFontPair = useCallback( (next: FontPair) => { setFontPairState(next); applyTheme({ palette, mode, fontPair: next, density, navStyle }); persist({ fontPair: next }); }, [palette, mode, density, navStyle, persist], ); const setDensity = useCallback( (next: Density) => { setDensityState(next); applyTheme({ palette, mode, fontPair, density: next, navStyle }); persist({ density: next }); }, [palette, mode, fontPair, navStyle, persist], ); const setDashLayout = useCallback( (next: DashLayout) => { setDashLayoutState(next); try { localStorage.setItem("themeDashLayout", next); } catch { // storage blocked } persist({ dashLayout: next }); }, [persist], ); const setCalView = useCallback( (next: CalView) => { setCalViewState(next); try { localStorage.setItem("themeCalView", next); } catch { // storage blocked } persist({ calView: next }); }, [persist], ); const setNavStyle = useCallback( (next: NavStyle) => { setNavStyleState(next); applyTheme({ palette, mode, fontPair, density, navStyle: next }); if (signedIn) { void setUserTheme({ navStyle: next }).then(() => router.refresh()); } }, [palette, mode, fontPair, density, signedIn, router], ); return { palette, mode, fontPair, density, dashLayout, calView, navStyle, setPalette, setMode, setFontPair, setDensity, setDashLayout, setCalView, setNavStyle, }; }