diff --git a/apps/web/cypress/e2e/planning-page.cy.ts b/apps/web/cypress/e2e/planning-page.cy.ts index b3c23df..f63bb63 100644 --- a/apps/web/cypress/e2e/planning-page.cy.ts +++ b/apps/web/cypress/e2e/planning-page.cy.ts @@ -182,6 +182,49 @@ describe("Planning grid", () => { cy.contains("Semaine du 10 au 16 août 2026").should("be.visible"); }); + // Issue #110 — a slot with many recipes used to stretch its ``, and + // with it the whole row and the table, past `.planning-grid-wrapper` + // into a vertical scroll (the other cells of that row just gaining dead + // space). The chip list now scrolls inside its own cell instead. + it("keeps a heavy slot's recipes scrolling inside the cell, not stretching the grid", () => { + cy.intercept("GET", /\/planning\?/, { + statusCode: 200, + body: { + id: 1, + startDate: "2026-08-17T00:00:00.000Z", + finishDate: "2026-08-23T00:00:00.000Z", + items: Array.from({ length: 14 }, (_, i) => ({ + id: i + 1, + weekDay: "lundi", + meal: "dejeuner", + portions: 2, + recipe: { id: i + 1, name: `Recette ${i + 1}` }, + })), + }, + }); + + cy.visit("/"); + cy.get(".recipe-chip").should("have.length", 14); + + // The busy cell's own recipe list is now the scroller: it's capped and + // scrolls its own overflow instead of stretching the `` (and with + // it the whole row, then the table past `.planning-grid-wrapper` into a + // tall vertical scroll). + cy.get(".meal-cell__recipes").should(($lists) => { + const list = [...$lists].find( + (el) => el.querySelectorAll(".recipe-chip").length === 14, + ) as HTMLElement; + expect(list, "the 14-recipe cell's list").to.exist; + expect(getComputedStyle(list).overflowY).to.be.oneOf(["auto", "scroll"]); + expect(list.scrollHeight).to.be.greaterThan(list.clientHeight); + expect(list.clientHeight).to.be.lessThan(200); + }); + + cy.get(".planning-grid-wrapper").should(($w) => { + expect($w[0].scrollHeight).to.be.lessThan($w[0].clientHeight * 1.25); + }); + }); + it("jumps to an arbitrary week by picking a day in the calendar popover", () => { cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }).as("getPlanning"); diff --git a/apps/web/src/pages/planning/planning-page.scss b/apps/web/src/pages/planning/planning-page.scss index 0534110..b1d7f78 100644 --- a/apps/web/src/pages/planning/planning-page.scss +++ b/apps/web/src/pages/planning/planning-page.scss @@ -84,6 +84,18 @@ table.planning-grid { border-collapse: collapse; table-layout: fixed; + // Share the wrapper's height evenly across the five meal rows instead of + // letting each row size to its own content. A `height` on a table row is + // a minimum the browser then stretches to absorb the table's leftover + // height; an equal tiny value on every row spreads that leftover evenly, + // so the grid genuinely fills the screen rather than being only as tall + // as its busiest column (with dead space under the short ones). The + // per-cell scroll below is what then keeps a heavy slot from pushing its + // row past that share. + tbody tr { + height: 1px; + } + th, td { border: 1px solid var(--color-border); @@ -178,12 +190,32 @@ table.planning-grid { flex-direction: column; gap: var(--space-xs); height: 100%; + // The content must not be what defines the row's height: a slot with + // several recipes would otherwise stretch its ``, and with it the + // whole row (every other day's cell gaining the same dead height under + // `vertical-align: top`), then the table past the wrapper into a + // vertical scroll. `min-height: 0` lets this flex column shrink to its + // share of the row so `.meal-cell__recipes` below can take over the + // scrolling. + min-height: 0; } .meal-cell__recipes { display: flex; flex-direction: column; gap: var(--space-xs); + // A slot only ever holds a handful of recipes in real use; past that the + // list scrolls inside the cell instead of stretching the `` (and + // with it the whole row, then the table past `.planning-grid-wrapper` + // into a vertical scroll — issue #110). A `rem` cap rather than a `%` of + // the cell: percentage heights against a `` don't resolve + // consistently across engines. `flex: 1` still lets the list use the + // row's spare height up to that cap. `.add-recipe-btn` is a flex sibling + // (outside this box), so it stays pinned just below, always reachable. + flex: 1; + min-height: 0; + max-height: 8.5rem; + overflow-y: auto; } .recipe-chip {