Skip to content

Commit 477db49

Browse files
committed
Fix for resizable side panel getting stuck at its min-size
1 parent f8ddb76 commit 477db49

2 files changed

Lines changed: 79 additions & 10 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Fix the run-view inspector panel locking at minimum width on reload when the persisted layout snapshot is in a state the underlying library can't safely restore.

apps/webapp/app/components/primitives/Resizable.tsx

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,78 @@ import React, { useRef } from "react";
44
import { PanelGroup, Panel, PanelResizer } from "react-window-splitter";
55
import { 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

1780
const ResizablePanel = Panel;
1881

@@ -71,7 +134,7 @@ const ResizableHandle = ({
71134

72135
const RESIZABLE_PANEL_ANIMATION = {
73136
easing: "ease-in-out" as const,
74-
duration: 200,
137+
duration: 300,
75138
};
76139

77140
const COLLAPSIBLE_HANDLE_CLASSNAME = "transition-opacity duration-200";

0 commit comments

Comments
 (0)