@@ -4,15 +4,78 @@ import React, { useRef } from "react";
44import { PanelGroup , Panel , PanelResizer } from "react-window-splitter" ;
55import { cn } from "~/utils/cn" ;
66
7- const ResizablePanelGroup = ( { className, ...props } : React . ComponentProps < typeof PanelGroup > ) => (
8- < PanelGroup
9- className = { cn (
10- "flex w-full overflow-hidden data-[panel-group-direction=vertical]:flex-col" ,
11- className
12- ) }
13- { ...props }
14- />
15- ) ;
7+ const ResizablePanelGroup = ( {
8+ className,
9+ autosaveId,
10+ snapshot : snapshotProp ,
11+ ...props
12+ } : React . ComponentProps < typeof PanelGroup > ) => {
13+ return (
14+ < PanelGroup
15+ className = { cn (
16+ "flex w-full overflow-hidden data-[panel-group-direction=vertical]:flex-col" ,
17+ className
18+ ) }
19+ autosaveId = { autosaveId }
20+ snapshot = { getSafeSnapshot ( autosaveId , snapshotProp ) }
21+ { ...props }
22+ />
23+ ) ;
24+ } ;
25+
26+ // react-window-splitter reads the persisted snapshot from localStorage during
27+ // render and feeds it straight into prepareSnapshot + the state machine. If the
28+ // value is corrupt (extension interference, JSON parse failure) or in a shape
29+ // the library can't safely consume on restore — notably items committed with
30+ // percent-typed currentValues, which trip a `panelHasSpace only works with
31+ // number values` invariant on the next expand — the panel locks at min size
32+ // with no working drag.
33+ //
34+ // We read the snapshot ourselves with try/catch + structural validation. On
35+ // failure we pass `true` (the library's sentinel for "snapshot already
36+ // resolved") so it skips its own localStorage read and falls back to defaults.
37+ // Pure read — safe to call on every render. PanelGroup captures via useState
38+ // on first render, so later calls are wasted work but never wrong.
39+ function getSafeSnapshot (
40+ autosaveId : string | undefined ,
41+ ssrSnapshot : React . ComponentProps < typeof PanelGroup > [ "snapshot" ]
42+ ) {
43+ if ( typeof window === "undefined" ) return ssrSnapshot ;
44+ if ( ssrSnapshot && isValidSnapshot ( ssrSnapshot ) ) return ssrSnapshot ;
45+ if ( ! autosaveId ) return undefined ;
46+
47+ try {
48+ const raw = window . localStorage . getItem ( autosaveId ) ;
49+ if ( ! raw ) return SNAPSHOT_RESOLVED ;
50+ const parsed : unknown = JSON . parse ( raw ) ;
51+ if ( ! isValidSnapshot ( parsed ) ) return SNAPSHOT_RESOLVED ;
52+ return parsed as React . ComponentProps < typeof PanelGroup > [ "snapshot" ] ;
53+ } catch {
54+ return SNAPSHOT_RESOLVED ;
55+ }
56+ }
57+
58+ const SNAPSHOT_RESOLVED = true as unknown as React . ComponentProps < typeof PanelGroup > [ "snapshot" ] ;
59+
60+ function isValidSnapshot ( value : unknown ) : boolean {
61+ if ( ! value || typeof value !== "object" ) return false ;
62+ const obj = value as Record < string , unknown > ;
63+ if ( ! ( "status" in obj ) || ! ( "context" in obj ) ) return false ;
64+ const ctx = obj . context as Record < string , unknown > | null ;
65+ if ( ! ctx || typeof ctx !== "object" || ! Array . isArray ( ctx . items ) ) return false ;
66+
67+ for ( const item of ctx . items ) {
68+ if ( ! item || typeof item !== "object" ) return false ;
69+ const it = item as Record < string , unknown > ;
70+ if ( it . type !== "panel" ) continue ;
71+ const cv = it . currentValue as Record < string , unknown > | null ;
72+ if ( ! cv || typeof cv !== "object" || cv . type !== "pixel" ) return false ;
73+ // value must be numeric (number or numeric string) so prepareSnapshot's
74+ // `new Big(value)` rehydration can't throw on us.
75+ if ( typeof cv . value !== "string" && typeof cv . value !== "number" ) return false ;
76+ }
77+ return true ;
78+ }
1679
1780const ResizablePanel = Panel ;
1881
@@ -71,7 +134,7 @@ const ResizableHandle = ({
71134
72135const RESIZABLE_PANEL_ANIMATION = {
73136 easing : "ease-in-out" as const ,
74- duration : 200 ,
137+ duration : 300 ,
75138} ;
76139
77140const COLLAPSIBLE_HANDLE_CLASSNAME = "transition-opacity duration-200" ;
0 commit comments