SPB Git

Repository Public

branding.ts 3.8 KB 85 lines typescript

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
Files main
Menu

Theme