"use client"; import { useCallback, useState } from "react"; /** * localStorage-backed persistence for a numeric ratio in [min, max]. * * Used by the collection-confirm PO card to remember the operator's * split-view ratio (items table ↔ invoice preview) across reloads. * The ratio is a fraction of the parent container width — e.g. 0.42 * means "items table takes 42% of the card body width". * * SSR-safe: `localStorage` is gated on `typeof window`. The hook * reads the stored value lazily on first render and ignores writes * during SSR so server-rendered HTML matches the client's initial * paint (avoids hydration warnings). * * Clamping is the hook's job — callers shouldn't have to defend * against stale localStorage values that fell outside the new bounds * (e.g. after a layout refactor narrowed the min). */ const isBrowser = typeof window !== "undefined"; function readStored(key: string): number | null { if (!isBrowser) return null; try { const raw = window.localStorage.getItem(key); if (raw === null) return null; const parsed = Number.parseFloat(raw); return Number.isFinite(parsed) ? parsed : null; } catch { // Safari private mode / quota errors / SecurityError — treat as no value. return null; } } function writeStored(key: string, value: number): void { if (!isBrowser) return; try { window.localStorage.setItem(key, String(value)); } catch { // Best-effort persistence. A failed write must not break the UI — // the in-memory state is the source of truth for the current session. } } export type UseSplitRatioOptions = { /** localStorage key. Namespace it per-feature to avoid collisions. */ storageKey: string; /** Initial ratio when nothing is stored (default 0.42 — items ≈ preview). */ initial: number; /** Inclusive lower bound. The hook clamps writes + reads to this. */ min?: number; /** Inclusive upper bound. The hook clamps writes + reads to this. */ max?: number; }; export type UseSplitRatioResult = { ratio: number; setRatio: (next: number) => void; }; /** * Persistent split-ratio hook. * * Returns the current ratio and a setter. The setter clamps to the * configured min/max before persisting, so the splitter can pass * raw cursor positions without worrying about overflow. */ export function useSplitRatio({ storageKey, initial, min = 0.15, max = 0.85, }: UseSplitRatioOptions): UseSplitRatioResult { const clamp = useCallback( (value: number) => Math.min(Math.max(value, min), max), [min, max], ); // Lazy init from localStorage so the very first render uses the // stored value when present (no flicker on a reload). We don't // need a follow-up `useEffect` to re-read on mount — `useState`'s // initialiser already runs once per mount with the current // localStorage value, so a hot-reload or fresh page load lands on // the right value on the very first render. const [ratio, setRatioState] = useState(() => { const stored = readStored(storageKey); if (stored === null) return clamp(initial); return clamp(stored); }); const setRatio = useCallback( (next: number) => { const clamped = clamp(next); setRatioState(clamped); writeStored(storageKey, clamped); }, [clamp, storageKey], ); return { ratio, setRatio }; }