fix(coach): cap drawer width on lg+ and route sources rail via tray

The drawer's lg:max-w-[1080px] cap left only ~200-286px of underlying
/insights visible on common laptop viewports (1280-1366px), turning
the "keep dashboard context behind it" sheet into a near-takeover.

Drop the lg cap to min(960px,75vw) with the inline sources rail
hidden below xl — the rail still surfaces on lg via the existing
mobile chevron tab. xl+ restores the full three-column layout. The
history rail stays visible inline on lg+ (it's the more frequently
referenced of the two).

Update the SSR test contract to match: history trigger hides on lg+,
sources trigger hides on xl+.

Fixes Design-HIGH-1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 18:06:18 +02:00
1 parent 8479e05a20
commit f9af100d9d
3 files changed
+35 -14

No files matched your search

@@ -9,10 +9,13 @@ import { CoachDrawerBody } from "../coach-drawer-body";
* *
* The drawer hides the history + sources rails on `<lg`. B4 adds two * The drawer hides the history + sources rails on `<lg`. B4 adds two
* chevron-button triggers along the edges of the message thread that * chevron-button triggers along the edges of the message thread that
* open side-sheets surfacing the same rails. The triggers live on * open side-sheets surfacing the same rails.
* `<CoachDrawerBody>` so the SSR test can pin their `lg:hidden` class *
* + slot markers without rendering the outer Radix `<Sheet>` portal * Phase D reconcile narrowed the lg drawer cap (was 1080px → now
* (which is client-only). * min(960px,75vw)) so the inline sources rail no longer fits below
* xl. The history rail surfaces inline on lg; the sources rail
* surfaces inline on xl. Mobile chevron triggers cover the missing
* rails: history hidden on lg+, sources hidden on xl+.
*/ */
function render(node: React.ReactNode, locale: "en" | "de" = "en") { function render(node: React.ReactNode, locale: "en" | "de" = "en") {
@@ -37,7 +40,7 @@ describe("<CoachDrawerBody> — mobile rail trays", () => {
expect(html).toMatch(/data-slot="coach-drawer-sources-tray-trigger"/); expect(html).toMatch(/data-slot="coach-drawer-sources-tray-trigger"/);
}); });
it("hides both triggers on >=lg via the lg:hidden class", () => { it("hides the history trigger on >=lg and the sources trigger on >=xl", () => {
const html = render(<CoachDrawerBody {...baseProps} />); const html = render(<CoachDrawerBody {...baseProps} />);
const historyTrigger = html.match( const historyTrigger = html.match(
/<button[^>]*data-slot="coach-drawer-history-tray-trigger"[^>]*>/, /<button[^>]*data-slot="coach-drawer-history-tray-trigger"[^>]*>/,
@@ -49,16 +52,16 @@ describe("<CoachDrawerBody> — mobile rail trays", () => {
/<button[^>]*data-slot="coach-drawer-sources-tray-trigger"[^>]*>/, /<button[^>]*data-slot="coach-drawer-sources-tray-trigger"[^>]*>/,
); );
expect(sourcesTrigger).not.toBeNull(); expect(sourcesTrigger).not.toBeNull();
expect(sourcesTrigger?.[0]).toContain("lg:hidden"); expect(sourcesTrigger?.[0]).toContain("xl:hidden");
}); });
it("renders the desktop history + sources rails (visible on >=lg)", () => { it("renders the desktop history rail (lg+) and sources rail (xl+)", () => {
const html = render(<CoachDrawerBody {...baseProps} />); const html = render(<CoachDrawerBody {...baseProps} />);
expect(html).toMatch( expect(html).toMatch(
/<aside[^>]*data-slot="coach-drawer-history"[^>]*lg:flex/, /<aside[^>]*data-slot="coach-drawer-history"[^>]*lg:flex/,
); );
expect(html).toMatch( expect(html).toMatch(
/<aside[^>]*data-slot="coach-drawer-sources"[^>]*lg:flex/, /<aside[^>]*data-slot="coach-drawer-sources"[^>]*xl:flex/,
); );
}); });
@@ -41,7 +41,11 @@ export function CoachDrawerBody({
return ( return (
<div <div
data-slot="coach-drawer-body" data-slot="coach-drawer-body"
className="grid min-h-0 flex-1 grid-cols-1 lg:grid-cols-[260px_1fr_280px]" // v1.4.20 phase D reconcile — narrower drawer cap at lg means the
// sources rail no longer fits inline below xl. lg gets the
// history rail + thread (sources surface via the chevron tray);
// xl+ restores the full three-column layout.
className="grid min-h-0 flex-1 grid-cols-1 lg:grid-cols-[260px_1fr] xl:grid-cols-[260px_1fr_280px]"
> >
{/* History rail — desktop only. Mobile users summon it via the {/* History rail — desktop only. Mobile users summon it via the
tray trigger inside the thread column. */} tray trigger inside the thread column. */}
@@ -71,6 +75,8 @@ export function CoachDrawerBody({
<ChevronRight className="size-3" aria-hidden="true" /> <ChevronRight className="size-3" aria-hidden="true" />
{t("insights.coach.historyTitle")} {t("insights.coach.historyTitle")}
</Button> </Button>
{/* Sources tray trigger stays visible up to xl since the
inline sources rail is xl+ only. */}
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@@ -78,7 +84,7 @@ export function CoachDrawerBody({
onClick={onOpenSourcesTray} onClick={onOpenSourcesTray}
data-slot="coach-drawer-sources-tray-trigger" data-slot="coach-drawer-sources-tray-trigger"
aria-label={t("insights.coach.sourcesTitle")} aria-label={t("insights.coach.sourcesTitle")}
className="bg-background/80 border-border/60 absolute right-2 top-2 z-10 h-7 gap-1 border px-2 text-[11px] backdrop-blur lg:hidden" className="bg-background/80 border-border/60 absolute right-2 top-2 z-10 h-7 gap-1 border px-2 text-[11px] backdrop-blur xl:hidden"
> >
{t("insights.coach.sourcesTitle")} {t("insights.coach.sourcesTitle")}
<ChevronLeft className="size-3" aria-hidden="true" /> <ChevronLeft className="size-3" aria-hidden="true" />
@@ -94,10 +100,12 @@ export function CoachDrawerBody({
</div> </div>
</main> </main>
{/* Sources rail — desktop only. */} {/* Sources rail — xl+ only. lg viewports surface it via the
mobile chevron tray to keep the underlying /insights column
readable behind the narrowed drawer cap. */}
<aside <aside
data-slot="coach-drawer-sources" data-slot="coach-drawer-sources"
className="border-border/70 hidden h-full min-h-0 border-l lg:flex lg:flex-col" className="border-border/70 hidden h-full min-h-0 border-l xl:flex xl:flex-col"
> >
{sourcesRail} {sourcesRail}
</aside> </aside>
@@ -134,7 +134,15 @@ export function CoachDrawer({
showCloseButton showCloseButton
data-slot="coach-drawer" data-slot="coach-drawer"
className={cn( className={cn(
"w-full p-0 sm:max-w-[720px] lg:max-w-[1080px]", // Drawer keeps the dashboard context behind it. On laptops
// (1280-1366px viewports) the previous lg:max-w-[1080px] cap
// left only ~200-286px of underlying /insights visible — a
// takeover, not a sheet. Drop the lg cap to min(960px,75vw)
// so /insights always retains a readable column; restore
// the wider three-column layout at xl+ (≥1280px sources rail
// is hidden below xl by the body's lg:hidden chevron rules).
"w-full p-0 sm:max-w-[720px]",
"lg:!max-w-[min(960px,75vw)] xl:!max-w-[1080px]",
"flex h-[100dvh] flex-col gap-0", "flex h-[100dvh] flex-col gap-0",
)} )}
> >
@@ -260,7 +268,9 @@ export function CoachDrawer({
<SheetContent <SheetContent
side="right" side="right"
data-slot="coach-drawer-sources-tray" data-slot="coach-drawer-sources-tray"
className="w-[88vw] max-w-[320px] p-0 lg:hidden" // Sources rail is xl+ inline; on lg it's only available
// via this tray to match the narrowed drawer cap.
className="w-[88vw] max-w-[320px] p-0 xl:hidden"
> >
<SheetHeader className="border-border/70 border-b p-3"> <SheetHeader className="border-border/70 border-b p-3">
<SheetTitle className="text-sm"> <SheetTitle className="text-sm">