From aba77d47f5f9f370a9080e772f1594a3904a1351 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 8 Jul 2026 20:25:36 -0400 Subject: [PATCH] fix(web): restore resize handle + refine column close/animation behavior (#1616) - Fix the resize handle disappearing: the animation wrapper is a Mantine Group (default align: center), and the full-height handle has no intrinsic height, so it collapsed to 0. Set align: stretch on the wrapper. - Closing the column now leaves the primary on the screen currently selected in the header (commits the active tab first), instead of swapping in the column's screen. - Keep the primary screen's standard ScreenStage transition; only the column (handle + column) slides open/closed from the right with a fade. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../InspectorView/InspectorView.test.tsx | 22 +++++++++++++--- .../views/InspectorView/InspectorView.tsx | 26 ++++++++++++------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/clients/web/src/components/views/InspectorView/InspectorView.test.tsx b/clients/web/src/components/views/InspectorView/InspectorView.test.tsx index 1c93bbd6..ec979431 100644 --- a/clients/web/src/components/views/InspectorView/InspectorView.test.tsx +++ b/clients/web/src/components/views/InspectorView/InspectorView.test.tsx @@ -1400,24 +1400,38 @@ describe("InspectorView", () => { ).toBeInTheDocument(); }); - it("carries the column's selection to the primary view on close", async () => { + it("keeps the current header screen (not the column's) when closed", async () => { monitorWide.value = true; renderWithMantine(); const user = await gotoTab("Logs"); await user.click( await screen.findByRole("button", { name: "Pin as column" }), ); + // Pinning moved the primary to the first non-Servers header tab (Tools). + const header = screen.getByRole("banner"); + expect( + within(header).getByRole("radio", { name: "Tools" }), + ).toBeChecked(); + // Switch the column to History, then close it. await user.click(await screen.findByRole("radio", { name: "History" })); await user.click( await screen.findByRole("button", { name: "Close monitoring column" }), ); - // The primary header now has History selected (not the pre-pin Logs). - const header = screen.getByRole("banner"); + // The primary stays on the header's current screen (Tools), not the + // column's History, and the monitor group returns to the header. + await waitFor(() => + expect( + screen.queryByRole("button", { name: "Close monitoring column" }), + ).toBeNull(), + ); + expect( + within(header).getByRole("radio", { name: "Tools" }), + ).toBeChecked(); expect( within(header).getByRole("radio", { name: "History" }), - ).toBeChecked(); + ).toBeInTheDocument(); }); it("persists the pin preference and reopens the column when wide", () => { diff --git a/clients/web/src/components/views/InspectorView/InspectorView.tsx b/clients/web/src/components/views/InspectorView/InspectorView.tsx index afd36d3a..bd520298 100644 --- a/clients/web/src/components/views/InspectorView/InspectorView.tsx +++ b/clients/web/src/components/views/InspectorView/InspectorView.tsx @@ -253,11 +253,13 @@ const MonitoringColumn = Stack.withProps({ }); // Column open/close animation (#1616): the handle + column slide in from the -// right edge and fade as they mount, reversing on close, so pinning reads as the -// screen moving into a side column rather than snapping in. `AppShell.Main`'s -// `overflow: hidden` clips the off-screen portion during the slide. +// right edge and fade as they mount, reversing on close, so pinning reads as a +// side column sliding open rather than snapping in. The primary screen keeps its +// standard `ScreenStage` transition. Mantine plays `out → in` on enter and +// `in → out` on exit. `AppShell.Main`'s `overflow: hidden` clips the off-screen +// portion during the slide. const COLUMN_ANIM_MS = 300; -const slideInFromRight: MantineTransition = { +const columnSlide: MantineTransition = { in: { opacity: 1, transform: "translateX(0)" }, out: { opacity: 0, transform: "translateX(100%)" }, common: { transformOrigin: "right center" }, @@ -265,9 +267,12 @@ const slideInFromRight: MantineTransition = { }; // Flex-row wrapper holding the resize handle + column so the whole unit animates -// as one (the Transition interpolation is applied to it via `style`). +// as one (the Transition interpolation is applied to it via `style`). `align: +// stretch` overrides Group's default `center` so the full-height resize handle +// (which has no intrinsic height) fills the column instead of collapsing. const MonitorColumnGroup = Group.withProps({ wrap: "nowrap", + align: "stretch", gap: 0, h: "100%", flex: "0 0 auto", @@ -773,11 +778,12 @@ export function InspectorView({ /* v8 ignore next */ if (isMonitorTab(tab)) setMonitorTab(tab); } - // Closing the column carries its selection back to the primary area: the - // screen the user was watching in the column becomes the active header tab, - // rather than snapping back to whatever was selected before they pinned. + // Closing the column leaves the primary area on whatever screen is currently + // selected in the header — it does not swap in the column's screen. We commit + // the current `activeTab` to the lifted state first so unpinning can't resurrect + // the (now stale) monitor tab that was selected before pinning. function closeMonitorColumn() { - onActiveTabChange(effectiveMonitorTab); + onActiveTabChange(activeTab); setMonitorPinned(false); } function commitMonitorWidth(next: number) { @@ -1062,7 +1068,7 @@ export function InspectorView({