import { createContext, useContext, useEffect, useRef, useState, useCallback, useMemo } from 'react' import { api } from '../api/client.js' import { applyThemeTokens } from '../lib/themeVars.js' const SiteContext = createContext(null) // Public site settings + mode (always reachable, even during maintenance). export function SiteProvider({ children }) { const [settings, setSettings] = useState({}) const [loading, setLoading] = useState(true) // Whether a fetch has actually SUCCEEDED, as distinct from `loading` — which // also goes false when the request failed and we fell back to {}. The boot // theme handoff below turns on this distinction. const [settled, setSettled] = useState(false) const refresh = useCallback(async () => { try { const data = await api.publicSettings() setSettings(data || {}) setSettled(true) } catch { setSettings({}) } finally { setLoading(false) } }, []) useEffect(() => { refresh() }, [refresh]) const brand = useMemo(() => settings.brand || {}, [settings]) // Apply the admin's theme. The whole effective token set is resolved // server-side, so this only writes it and takes back what it wrote before — // see lib/themeVars.js for why the removal half matters. No theme block means // the admin never themed this instance, and the shipped :root stands. const appliedTokens = useRef([]) useEffect(() => { appliedTokens.current = applyThemeTokens(document.documentElement.style, settings.theme, appliedTokens.current) // Take over from the shell's boot block. The server injects the same tokens // into
so a themed instance does not paint the shipped palette for a // frame first (utils/htmlShell.js); from here on this effect is the // authority, and leaving the block behind would mean a later reset removed // the inline properties only to reveal the stale block underneath. // // Gated on a SUCCESSFUL fetch, not merely a finished one: a failed request // leaves us with no theme at all, and dropping the block then would strip a // themed instance back to the shipped palette for no reason. if (settled) document.getElementById('theme-boot')?.remove() }, [settings.theme, settled]) // Apply the instance accent color to the CSS variable the theme is built on, // so branding flows to every `var(--accent)` at runtime (no rebuild). This is // the *effective* accent — the admin theme overrides BRAND_ACCENT_COLOR // server-side (docs/website/THEMING_AND_NAV.md §4.5) — so it agrees with the // theme block rather than fighting it. // // Deliberately ordered after the theme effect and re-run on any theme change: // resetting a theme removes --accent from the token map, and this has to be // the write that lands last or an instance with a custom BRAND_ACCENT_COLOR // would drop to the stylesheet's default accent until the next reload. useEffect(() => { if (brand.accent) document.documentElement.style.setProperty('--accent', brand.accent) }, [brand.accent, settings.theme]) // Memoized so consumers don't re-render on every provider render (brand is a // fresh object each render, which would otherwise churn the context value). const value = useMemo( () => ({ settings, loading, refresh, brand, mode: settings.site_mode || 'live', siteTitle: brand.name || settings.site_title || 'Runic Gateway', siteShortName: brand.shortName || brand.name || settings.site_title || 'Runic Gateway', contactEmail: brand.contactEmail || settings.contact_email || '', heroImage: brand.hero || '/assets/img/runic-emblem.png', }), [settings, loading, refresh, brand], ) return