From c49a04bac6b5aa20fba405bb2bb6dea02241660d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 5 Oct 2026 17:24:49 +0200 Subject: [PATCH] fix(coach): dock the conversations panel flush against the right edge The Coach page cancelled the shell's container padding with negative margins, so
kept its reserved scrollbar gutter and painted a dead strip to the right of the panel on classic-scrollbar platforms. The shell now renders /coach full bleed: no centred container, no padding and no reserved gutter, and the page fills what
leaves below the top bar. A refusal card (shared record, module off) keeps the padded frame. The panel header takes one 12 px inset with a gap of 8 between title and icons, a text-lg title, and 28 px icon buttons beside a pointer (44 px on touch). Conversation rows take the control radius and a 36 px pointer floor. --- e2e/coach-layout.spec.ts | 42 ++++++++++++++++++- src/app/coach/page-client.tsx | 22 +++------- .../coach-panel/conversations-panel.tsx | 15 +++---- .../insights/coach-panel/history-rail.tsx | 2 +- src/components/layout/auth-shell.tsx | 32 ++++++++++++-- src/components/layout/module-page-gate.tsx | 14 ++++++- 6 files changed, 97 insertions(+), 30 deletions(-) diff --git a/e2e/coach-layout.spec.ts b/e2e/coach-layout.spec.ts index b933afd30..07b36d83e 100644 --- a/e2e/coach-layout.spec.ts +++ b/e2e/coach-layout.spec.ts @@ -14,7 +14,9 @@ import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities"; * * 1. From 1280 px the panel docks beside the thread, open on a first visit, * and the reading column (thread and composer) is exactly as wide with - * the panel open as with it closed. The choice survives a reload. + * the panel open as with it closed. The choice survives a reload. The + * panel is flush with the viewport's right edge (no scrollbar gutter + * beside it) and runs from the top bar to the bottom edge. * 2. Escape inside the docked panel closes it and returns focus to the * toggle. * 3. A row's menu renames by keyboard (Escape cancels without closing the @@ -230,6 +232,43 @@ async function expectNoSidewaysScroll(page: Page) { expect(overflow.main).toBeLessThanOrEqual(0); } +/** + * The docked panel is flush with the viewport: its right edge is the + * viewport's (no scrollbar gutter beside it), it runs from the top bar's + * bottom edge to the bottom of the viewport, and its own border is the left + * one only. + */ +async function expectPanelFlush(page: Page, width: number, height: number) { + const geometry = await page.evaluate(() => { + const aside = document.querySelector( + '[data-slot="coach-conversations-panel"]', + )!; + const inner = aside.firstElementChild as HTMLElement; + const bar = document.querySelector('[data-slot="top-bar"]')!; + const main = document.getElementById("main-content")!; + const style = getComputedStyle(inner); + const box = aside.getBoundingClientRect(); + return { + right: box.right, + top: box.top, + bottom: box.bottom, + barBottom: bar.getBoundingClientRect().bottom, + gutter: main.offsetWidth - main.clientWidth, + borders: [ + style.borderTopWidth, + style.borderRightWidth, + style.borderBottomWidth, + style.borderLeftWidth, + ], + }; + }); + expect(geometry.right).toBe(width); + expect(geometry.top).toBe(geometry.barBottom); + expect(geometry.bottom).toBe(height); + expect(geometry.gutter).toBe(0); + expect(geometry.borders).toEqual(["0px", "0px", "0px", "1px"]); +} + async function shot(page: Page, testInfo: TestInfo, name: string) { const path = testInfo.outputPath(`${name}.png`); await page.screenshot({ path }); @@ -318,6 +357,7 @@ test.describe("Coach page frame", () => { // The panel sits right of the column, never over it. const aside = (await panel(page).boundingBox())!; expect(aside.x).toBeGreaterThanOrEqual(open.threadX + open.thread); + await expectPanelFlush(page, width, 900); await expectNoSidewaysScroll(page); await shot(page, testInfo, `coach-frame-${width}-open`); diff --git a/src/app/coach/page-client.tsx b/src/app/coach/page-client.tsx index f22b463d5..01b044e6e 100644 --- a/src/app/coach/page-client.tsx +++ b/src/app/coach/page-client.tsx @@ -263,22 +263,12 @@ export default function CoachPageClient() {
` paints directly onto the page so it reads as - // one continuous conversation, not a window inside a window. The - // separate bottom-right FAB/overlay drawer stays the compact surface. - // - // v1.18.10 (W10) — height is mobile-aware. The fixed `` - // (64px + iOS home-indicator inset) is mobile-only; the desktop - // full-bleed layout has no bottom nav. On mobile the page must subtract - // the TopBar (`4rem`) AND the BottomNav band (`4rem` + - // `env(safe-area-inset-bottom)`) so the docked composer / Stop control - // always clears the nav instead of sitting under it. On `md+` the nav - // is hidden, so the page reclaims the full height below the TopBar. - className="bg-background -mx-4 -mt-6 -mb-20 flex h-[calc(100dvh-8rem-env(safe-area-inset-top,0px)-env(safe-area-inset-bottom,0px))] min-h-[32rem] flex-col overflow-hidden md:-mx-6 md:h-[calc(100dvh-4rem)]" + // Full-bleed conversation surface. The shell renders `/coach` without + // its centred container, padding or reserved scrollbar gutter, and its + // `
` already clears the top bar and, on phones, the bottom nav, + // so the page simply fills what is left. No inner card: the + // conversation paints directly onto the page. + className="bg-background flex min-h-[32rem] flex-1 flex-col overflow-hidden" > {/* `useSearchParams` requires a Suspense boundary so the client-search bailout never opts the whole route out of static optimisation. */} diff --git a/src/components/insights/coach-panel/conversations-panel.tsx b/src/components/insights/coach-panel/conversations-panel.tsx index c3082381b..eda23e3df 100644 --- a/src/components/insights/coach-panel/conversations-panel.tsx +++ b/src/components/insights/coach-panel/conversations-panel.tsx @@ -146,14 +146,14 @@ export function ConversationsPanel({ const header = (inSheet: boolean) => (
{inSheet ? ( - + {t("insights.coach.historyTitle")} ) : ( -

+

{t("insights.coach.historyTitle")}

)} @@ -161,7 +161,7 @@ export function ConversationsPanel({ asChild variant="ghost" size="icon" - className="text-muted-foreground hover:text-foreground size-11 shrink-0 sm:size-9" + className="text-muted-foreground hover:text-foreground size-11 shrink-0 md:size-7" > -