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" > -