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) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-07-08 20:25:36 -04:00
commit aba77d47f5
2 changed files with 34 additions and 14 deletions

View file

@ -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(<StatefulInspectorViewHost {...connectedHttp()} />);
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", () => {

View file

@ -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({
</ScreenStageContainer>
<Transition
mounted={effectivePinned}
transition={slideInFromRight}
transition={columnSlide}
duration={COLUMN_ANIM_MS}
exitDuration={COLUMN_ANIM_MS}
timingFunction="ease"