diff --git a/apps/web/cypress/e2e/recipes.cy.ts b/apps/web/cypress/e2e/recipes.cy.ts index 318a410..f7a065c 100644 --- a/apps/web/cypress/e2e/recipes.cy.ts +++ b/apps/web/cypress/e2e/recipes.cy.ts @@ -156,6 +156,14 @@ describe("Recipe catalog", () => { }); it("opens a recipe's detail alongside the table when its row is selected", () => { + // Desktop-only master-detail layout, same reasoning as + // planning-page.cy.ts's "Planning grid" tests — wider/taller than + // Cypress's default 1000×660, which doesn't leave the detail panel + // (photo + header + ingredients + steps) enough height to show + // everything without needing its own internal scroll (by design, see + // `.recipe-detail-panel` in recipes.scss) — this test asserts full + // visibility without scrolling, so it needs the room. + cy.viewport(1600, 900); cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [ratatouille, omelette] }); cy.intercept("GET", "**/recipes/2", { statusCode: 200, body: omeletteDetail }).as("getRecipe"); diff --git a/apps/web/src/features/recipes/recipes.scss b/apps/web/src/features/recipes/recipes.scss index 9b70281..f772c39 100644 --- a/apps/web/src/features/recipes/recipes.scss +++ b/apps/web/src/features/recipes/recipes.scss @@ -124,12 +124,32 @@ min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) minmax(35vw, 38vw); + // Without this, the grid's single implicit row falls back to + // auto-sizing (tall enough for its content), not the full height of + // `&__catalog` itself — silently masked as long as `.app-content`'s own + // height was `min-height: 100vh` (auto-grew to fit anything, so + // nothing downstream ever actually needed to stretch to a fixed + // height). Fixing that (see AppLayout.scss) made `&__catalog`'s height + // genuinely definite for the first time, which surfaced this: the + // detail panel's own `height: 100%; overflow-y: auto` (see + // `.recipe-detail-panel` below) had nothing to stretch against, came + // out shorter than its content, and silently clipped it. + grid-template-rows: minmax(0, 1fr); gap: var(--space-lg); } @media (max-width: 1024px) { &__catalog { grid-template-columns: 1fr; + // The single-row `1fr` track above only makes sense for the desktop + // side-by-side layout — collapsed to one column, the table and the + // detail panel stack into two separate rows instead, so forcing them + // both into one `1fr` track split the available height in half and + // gave the table row 0px (its own content pushed below the fold, + // clipped by its `overflow-y: auto`). Reset to the default `auto` + // (each row sized to its content) — matches `height: auto` below, + // which already stopped assuming a single fixed-height row here. + grid-template-rows: auto; height: auto; } } diff --git a/apps/web/src/layouts/AppLayout.scss b/apps/web/src/layouts/AppLayout.scss index a58c09b..3615e4f 100644 --- a/apps/web/src/layouts/AppLayout.scss +++ b/apps/web/src/layouts/AppLayout.scss @@ -7,8 +7,14 @@ // No `@use` of the theme partial needed here — see HomePage.scss's identical // note: every token below is a CSS custom property, available at runtime. +// `height` (not `min-height`): the layout must stay pinned to exactly the +// viewport, never grow taller than it — otherwise a tall page scrolls the +// whole document, dragging the sidebar out of view with it. `overflow: +// hidden` backs that up so nothing can force the document itself to grow; +// `.app-content` below is the one place that actually scrolls. .app-layout { - min-height: 100vh; + height: 100vh; + overflow: hidden; display: flex; background: var(--color-background); } @@ -27,6 +33,10 @@ background: var(--color-surface); border-right: 1px solid var(--color-border); transition: width 0.15s ease; + // Own scroll, independent of `.app-content` — only matters on a very + // short viewport with a long nav, but keeps the rail from ever forcing + // `.app-layout` past 100vh. + overflow-y: auto; &__top { display: flex; @@ -315,6 +325,11 @@ // Content can scroll independently of the sidebar (e.g. a long planning // table) without the fixed-width rail ever needing to shrink. min-width: 0; + // The actual scroll container: `.app-layout` is pinned to `height: 100vh` + // above, so a page taller than the viewport scrolls in here instead of + // scrolling the document (which would drag the sidebar along with it). + min-height: 0; + overflow-y: auto; padding: var(--space-xl); } diff --git a/apps/web/src/pages/ComingSoonPage.scss b/apps/web/src/pages/ComingSoonPage.scss index 122f5dc..1121fff 100644 --- a/apps/web/src/pages/ComingSoonPage.scss +++ b/apps/web/src/pages/ComingSoonPage.scss @@ -2,8 +2,13 @@ // Styles for ComingSoonPage — shared by every stub section page. // ============================================================================= +// Centered, not pinned to `.app-content`'s left edge — same reasoning as +// `.settings-page` (settings-pages.scss): on a wide desktop viewport a +// left-aligned `max-width` here just left a lopsided gap down the right +// side instead of framing the placeholder copy. .coming-soon-page { max-width: 40rem; + margin: 0 auto; p { color: var(--color-text-muted);