SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%
3.7 KB · 88 lines tsx
Raw Blame History
1"use client";23import { createContext, useCallback, useContext, useEffect, useMemo, useState, useSyncExternalStore, type ReactNode } from "react";45/**6 * User preferences (spec §32, §98, §110): density (compact / normal / comfortable), live paused,7 * exact timestamps. Persisted in localStorage; density is mirrored on <html data-density> so CSS8 * variables apply everywhere without re-rendering the tree.9 */10export type Density = "compact" | "normal" | "comfortable";1112interface Prefs {13  density: Density;14  setDensity: (d: Density) => void;15  paused: boolean;16  setPaused: (p: boolean) => void;17  exactTime: boolean;18  setExactTime: (v: boolean) => void;19  mounted: boolean;20}2122const Ctx = createContext<Prefs | null>(null);23const KEY = "ws_prefs";2425export function PrefsProvider({ children }: { children: ReactNode }) {26  // Stored preferences are applied once, right after hydration (reading storage during SSR would mismatch).27  const mounted = useSyncExternalStore(subscribeNoop, () => true, () => false);28  const stored = useSyncExternalStore(subscribeNoop, readStored, () => null);29  const [override, setOverride] = useState<{ density?: Density; exactTime?: boolean }>({});30  const density = override.density ?? stored?.density ?? "normal";31  const exactTime = override.exactTime ?? stored?.exactTime ?? false;32  const [paused, setPaused] = useState(false);33  const setDensityState = (d: Density): void => setOverride((o) => ({ ...o, density: d }));34  const setExactTimeState = (v: boolean): void => setOverride((o) => ({ ...o, exactTime: v }));35  useEffect(() => {36    if (!mounted) return;37    document.documentElement.dataset.density = density;38    try {39      window.localStorage.setItem(KEY, JSON.stringify({ density, exactTime }));40    } catch {41      // ignore42    }43  }, [density, exactTime, mounted]);4445  const setDensity = useCallback((d: Density) => setDensityState(d), []);46  const setExactTime = useCallback((v: boolean) => setExactTimeState(v), []);47  const value = useMemo(() => ({ density, setDensity, paused, setPaused, exactTime, setExactTime, mounted }), [density, setDensity, paused, exactTime, setExactTime, mounted]);48  return <Ctx.Provider value={value}>{children}</Ctx.Provider>;49}5051const subscribeNoop = (): (() => void) => () => {};52let storedCache: { raw: string | null; value: { density?: Density; exactTime?: boolean } | null } = { raw: undefined as unknown as string | null, value: null };53function readStored(): { density?: Density; exactTime?: boolean } | null {54  try {55    const raw = window.localStorage.getItem(KEY);56    if (raw === storedCache.raw) return storedCache.value;57    const value = raw ? (JSON.parse(raw) as { density?: Density; exactTime?: boolean }) : null;58    storedCache = { raw, value };59    return value;60  } catch {61    return null;62  }63}6465export function usePrefs(): Prefs {66  const v = useContext(Ctx);67  if (!v) return { density: "normal", setDensity: () => {}, paused: false, setPaused: () => {}, exactTime: false, setExactTime: () => {}, mounted: false };68  return v;69}7071export function DensityToggle({ compact = false }: { compact?: boolean }) {72  const { density, setDensity } = usePrefs();73  const opts: [Density, string][] = [74    ["compact", "Compact"],75    ["normal", "Normal"],76    ["comfortable", "Comfortable"],77  ];78  return (79    <div role="radiogroup" aria-label="Density" className="inline-flex items-center rounded-md border border-line bg-panel p-0.5">80      {opts.map(([d, label]) => (81        <button key={d} type="button" role="radio" aria-checked={density === d} onClick={() => setDensity(d)} title={label} className={`rounded-sm px-1.5 py-0.5 text-[11px] ${density === d ? "bg-panel-3 text-fg" : "text-fg-subtle hover:text-fg"}`}>82          {compact ? label[0] : label}83        </button>84      ))}85    </div>86  );87}88