From 3bede8230bde8d8928731d526bb2ab4efdc4e65e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 5 Oct 2026 17:25:24 +0200 Subject: [PATCH] fix(coach): mirror the panel toggle glyph and keep it beside the open panel The toggle shows the panel-right open glyph while the panel is open and the close glyph while it is shut, both mirrored so the chevron points the way the panel moves on a click. Docked and open, the toggle keeps the panel's width as its right margin, so it sits beside the panel's leading edge and travels with it (200 ms, none under reduced motion). Labels and aria-expanded are unchanged. --- e2e/coach-layout.spec.ts | 13 ++++++++++ .../coach-panel/conversations-panel.tsx | 24 +++++++++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/e2e/coach-layout.spec.ts b/e2e/coach-layout.spec.ts index 07b36d83e..af710b8a4 100644 --- a/e2e/coach-layout.spec.ts +++ b/e2e/coach-layout.spec.ts @@ -358,6 +358,12 @@ test.describe("Coach page frame", () => { const aside = (await panel(page).boundingBox())!; expect(aside.x).toBeGreaterThanOrEqual(open.threadX + open.thread); await expectPanelFlush(page, width, 900); + // Open, the toggle stands beside the panel's leading edge. + const openToggle = (await toggle(page).boundingBox())!; + expect(openToggle.x + openToggle.width).toBeLessThanOrEqual(aside.x); + expect(aside.x - (openToggle.x + openToggle.width)).toBeLessThanOrEqual( + 24, + ); await expectNoSidewaysScroll(page); await shot(page, testInfo, `coach-frame-${width}-open`); @@ -366,6 +372,13 @@ test.describe("Coach page frame", () => { await waitForWidth(panel(page), 0); await expect(toggle(page)).toHaveAttribute("aria-expanded", "false"); const closed = await columnWidths(page); + // Closed, it returns to the trailing edge of the top bar. + await expect + .poll(async () => { + const box = (await toggle(page).boundingBox())!; + return Math.round(width - (box.x + box.width)); + }) + .toBeLessThanOrEqual(24); expect(Math.abs(closed.thread - open.thread)).toBeLessThanOrEqual(1); expect(Math.abs(closed.composer - open.composer)).toBeLessThanOrEqual(1); // Closed, nothing inside the panel is reachable. diff --git a/src/components/insights/coach-panel/conversations-panel.tsx b/src/components/insights/coach-panel/conversations-panel.tsx index eda23e3df..c2c323764 100644 --- a/src/components/insights/coach-panel/conversations-panel.tsx +++ b/src/components/insights/coach-panel/conversations-panel.tsx @@ -37,7 +37,8 @@ import { useDeleteCoachConversationWithUndo } from "./use-coach"; * Below 1280 px it is a sheet from the right that opens when asked and * closes again when a conversation is picked or a new chat starts. * - * The toggle sits at the trailing edge of the top bar. The panel header + * The toggle sits at the trailing edge of the top bar and, while the docked + * panel is open, beside the panel's leading edge. The panel header * carries the title, a link to Plans, the settings gear and, in the sheet, * a close button. The round New chat button sits at the bottom right. * @@ -245,12 +246,27 @@ export function ConversationsPanel({ aria-label={toggleLabel} title={toggleLabel} data-slot="coach-panel-toggle" - className="text-muted-foreground hover:text-foreground size-11" + className={cn( + "text-muted-foreground hover:text-foreground size-11", + // Docked and open, the toggle keeps the panel's width as its + // right margin, so it stays beside the panel's leading edge and + // travels with it as the panel opens and shuts. + "transition-[margin] duration-200 ease-linear motion-reduce:transition-none", + docked && dockedOpen && "mr-72", + )} > + {/* Mirrored, so the chevron points the way the panel moves on a + click: right to shut it, left to open it. */} {expanded ? ( -