diff --git a/components/split-pane/split-pane.tsx b/components/split-pane/split-pane.tsx index 52d1710..1e169c7 100644 --- a/components/split-pane/split-pane.tsx +++ b/components/split-pane/split-pane.tsx @@ -3,34 +3,15 @@ import { Fragment, useRef, useState, useEffect, useCallback } from 'react' import { cn } from '@/lib/utils' import { debugProps } from '@/lib/debug' +import { useUserSettingsStore } from '@/stores/user-settings/store' -const COOKIE_PREFIX = 'sp:' -const COOKIE_MAX_AGE = 60 * 60 * 24 * 365 - -function readSplits(cookieKey: string, n: number): number[] | null { - if (typeof document === 'undefined') return null - const match = document.cookie.match( - new RegExp(`(?:^|; )${COOKIE_PREFIX}${cookieKey}=([^;]+)`) +function isValidPositions(value: unknown, n: number): value is number[] { + return ( + Array.isArray(value) && + value.length === n && + value.every((v) => typeof v === 'number') && + Math.abs((value as number[]).reduce((a, b) => a + b, 0) - 100) <= 1 ) - if (!match) return null - try { - const parsed: unknown = JSON.parse(decodeURIComponent(match[1])) - if ( - !Array.isArray(parsed) || - parsed.length !== n || - parsed.some((v) => typeof v !== 'number') || - Math.abs((parsed as number[]).reduce((a, b) => a + b, 0) - 100) > 1 - ) return null - return parsed as number[] - } catch { - return null - } -} - -function writeSplits(cookieKey: string, splits: number[]) { - document.cookie = `${COOKIE_PREFIX}${cookieKey}=${encodeURIComponent( - JSON.stringify(splits) - )}; max-age=${COOKIE_MAX_AGE}; path=/; samesite=lax` } export interface SplitPaneProps { @@ -59,9 +40,16 @@ export function SplitPane({ const containerRef = useRef(null) const splitsRef = useRef(defaultSplit) - const [splits, setSplits] = useState(() => { - return readSplits(cookieKey, n) ?? defaultSplit - }) + const persisted = useUserSettingsStore( + (s) => s.entities.settings.layout?.splitPanePositions?.[cookieKey], + ) + const setPref = useUserSettingsStore((s) => s.setPref) + + // While dragging we keep splits in local state to avoid round-tripping every + // mousemove through the store. Outside of a drag, the store is the source of + // truth so cross-tab updates flow in automatically. + const [dragSplits, setDragSplits] = useState(null) + const splits = dragSplits ?? (isValidPositions(persisted, n) ? persisted : defaultSplit) const [dragging, setDragging] = useState(null) // divider index (0..n-2) const [isMobile, setIsMobile] = useState(false) const [internalTab, setInternalTab] = useState(0) @@ -96,20 +84,20 @@ export function SplitPane({ const newLeft = Math.min(Math.max(cursorPct - leftEdge, minPct), combinedWidth - minPct) const newRight = combinedWidth - newLeft - setSplits((prev) => { - const next = [...prev] - next[dragging] = newLeft - next[dragging + 1] = newRight - return next - }) + const base = splitsRef.current + const next = [...base] + next[dragging] = newLeft + next[dragging + 1] = newRight + setDragSplits(next) }, [dragging, minSize]) const onMouseUp = useCallback(() => { if (dragging !== null) { - writeSplits(cookieKey, splitsRef.current) + void setPref(['layout', 'splitPanePositions', cookieKey], splitsRef.current) + setDragSplits(null) setDragging(null) } - }, [dragging, cookieKey]) + }, [dragging, cookieKey, setPref]) useEffect(() => { if (dragging !== null) {