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
* chevron-button triggers along the edges of the message thread that
* open side-sheets surfacing the same rails. The triggers live on
* `<CoachDrawerBody>` so the SSR test can pin their `lg:hidden` class
* + slot markers without rendering the outer Radix `<Sheet>` portal
* (which is client-only).
* open side-sheets surfacing the same rails.
*
* Phase D reconcile narrowed the lg drawer cap (was 1080px → now
* 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") {
@@ -37,7 +40,7 @@ describe("<CoachDrawerBody> — mobile rail trays", () => {
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 historyTrigger = html.match(
/<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"[^>]*>/,
);
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} />);
expect(html).toMatch(
/<aside[^>]*data-slot="coach-drawer-history"[^>]*lg:flex/,
);
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 (
<div
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
tray trigger inside the thread column. */}
@@ -71,6 +75,8 @@ export function CoachDrawerBody({
<ChevronRight className="size-3" aria-hidden="true" />
{t("insights.coach.historyTitle")}
</Button>
{/* Sources tray trigger stays visible up to xl since the
inline sources rail is xl+ only. */}
<Button
type="button"
variant="ghost"
@@ -78,7 +84,7 @@ export function CoachDrawerBody({
onClick={onOpenSourcesTray}
data-slot="coach-drawer-sources-tray-trigger"
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")}
<ChevronLeft className="size-3" aria-hidden="true" />
@@ -94,10 +100,12 @@ export function CoachDrawerBody({
</div>
</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
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}
</aside>
@@ -134,7 +134,15 @@ export function CoachDrawer({
showCloseButton
data-slot="coach-drawer"
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",
)}
>
@@ -260,7 +268,9 @@ export function CoachDrawer({
<SheetContent
side="right"
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">
<SheetTitle className="text-sm">