Repository Public
1'use client';2import { create } from 'zustand';3import { useLayoutEffect, useEffect } from 'react';4import type { Branding } from '@cr/shared';5import { DEFAULT_BRANDING, brandOf, confirmBranding, initialBranding, sameBranding, type Brand } from '@/lib/branding';67interface BrandingState {8 branding: Branding;9 brand: Brand;10 /** 'default' until the cache was read on the client; 'server' once /auth/status confirmed it. */11 source: 'default' | 'cache' | 'server';12}1314const storage = (): Storage | null => {15 try {16 return typeof window === 'undefined' ? null : window.localStorage;17 } catch {18 return null;19 }20};2122/**23 * Starts with the Claude default on the server AND on the client, so that hydration of the static24 * export never mismatches; the cached value is applied in a layout effect (before paint).25 */26export const useBrandingStore = create<BrandingState>(() => ({ branding: DEFAULT_BRANDING, brand: brandOf(DEFAULT_BRANDING), source: 'default' }));2728/** <html data-agent>, document title and the PWA title metas. `data-brand-ready` un-hides static brand text. */29export function applyBrandingDom(branding: Branding): void {30 if (typeof document === 'undefined') return;31 const root = document.documentElement;32 if (root.getAttribute('data-agent') !== branding.agent) root.setAttribute('data-agent', branding.agent);33 if (!root.hasAttribute('data-brand-ready')) root.setAttribute('data-brand-ready', '');34 if (document.title !== branding.productName) document.title = branding.productName;35 const set = (selector: string, value: string) => {36 const el = document.querySelector(selector);37 if (el && el.getAttribute('content') !== value) el.setAttribute('content', value);38 };39 set('meta[name="apple-mobile-web-app-title"]', branding.productName);40 set('meta[name="description"]', `${branding.productName} — MacLustr Command Center`);41}4243function commit(branding: Branding, source: BrandingState['source']): void {44 const cur = useBrandingStore.getState();45 if (!sameBranding(cur.branding, branding)) useBrandingStore.setState({ branding, brand: brandOf(branding), source });46 else if (cur.source !== source) useBrandingStore.setState({ source });47 applyBrandingDom(branding);48}4950/** Client start: the branding cached by the previous visit (instant paint). Never downgrades a server value. */51export function hydrateBrandingFromCache(): void {52 if (useBrandingStore.getState().source !== 'default') return applyBrandingDom(useBrandingStore.getState().branding);53 commit(initialBranding(storage()), 'cache');54}5556/** /auth/status answered: the server value wins and replaces the cache. */57export function confirmBrandingFromServer(raw: unknown): void {58 commit(confirmBranding(storage(), raw), 'server');59}6061export const useBrand = (): Brand => useBrandingStore((s) => s.brand);62export const getBrand = (): Brand => useBrandingStore.getState().brand;6364const useIsoLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;65/** Mounted once in <Providers>: hydrates from the cache and keeps title / data-agent right across navigations. */66export function useBrandingSync(pathname: string | null): void {67 const branding = useBrandingStore((s) => s.branding);68 useIsoLayoutEffect(() => {69 hydrateBrandingFromCache();70 }, []);71 useEffect(() => {72 if (useBrandingStore.getState().source !== 'default') applyBrandingDom(branding);73 }, [branding, pathname]);74 // Next re-renders <title> from the static metadata on client navigations: put the product name back.75 useEffect(() => {76 const head = document.head;77 const obs = new MutationObserver(() => {78 const s = useBrandingStore.getState();79 if (s.source !== 'default' && document.title !== s.branding.productName) document.title = s.branding.productName;80 });81 obs.observe(head, { childList: true, subtree: true, characterData: true });82 return () => obs.disconnect();83 }, []);84}85