From 7d1a69cf12d96a1f6e4cd91d0a56bc8c1f277009 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 7 Sep 2026 17:02:48 +0200 Subject: [PATCH] fix(mobile): renforce le catalogue de recettes et la liste de courses sur mobile (#90) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deux correctifs, regroupés en un seul ticket comme prévu : - Catalogue de recettes (`recipes.scss`) : `.recipe-table-wrap` n'avait aucun garde-fou de scroll horizontal, contrairement à la grille planning — sur un viewport étroit, les 4 colonnes (photo/nom/ allergènes/régimes) se compressaient au lieu de scroller. Ajout d'`overflow-x: auto` sur le wrapper + `min-width: 36rem` sur la table (échelle réduite par rapport aux 62rem de la grille planning : 4 colonnes plus étroites, pas 7). Partagé par le catalogue complet ET par `RecipePickerDialog`, qui hérite du même correctif au passage. - Liste de courses (`shopping-list-page.scss`) : audit dédié — la page n'a en réalité aucune case à cocher ni bouton de suppression (elle est volontairement en lecture seule, voir le commentaire de ShoppingListPage.tsx), contrairement à ce que supposait le ticket d'origine ; rien à agrandir comme cible tactile de ce côté. La structure ne déborde déjà pas non plus (`&-name` a déjà flex:1/ min-width:0). Amélioration retenue à la place : une ligne un peu plus aérée et une icône plus grande sous le breakpoint phone, pour une lecture plus confortable en faisant les courses (liste tenue d'une main, coups d'œil répétés). Couverture Cypress : nouveau describe "Recipe catalog (phone)" dans recipes.cy.ts (garde-fou de scroll horizontal effectivement actif) ; nouveau fichier shopping-list-page.cy.ts, spec layout simple (pas Gherkin, cette page n'a pas encore ce type de fichier) pour le padding de ligne. Suite complète : biome check, tsc -b --force, build, 146/146 e2e, 71/71 composants (un skip ponctuel de TechStepCorrectionPopover en run groupé, reconfirmé 10/10 isolément — flake local sans lien avec ce ticket) — tous verts. Closes #90 Co-Authored-By: Claude Sonnet 5 --- apps/web/cypress/e2e/recipes.cy.ts | 37 ++++++++++ apps/web/cypress/e2e/shopping-list-page.cy.ts | 67 +++++++++++++++++++ apps/web/src/features/recipes/recipes.scss | 12 ++++ .../shopping-list/shopping-list-page.scss | 24 +++++++ 4 files changed, 140 insertions(+) create mode 100644 apps/web/cypress/e2e/shopping-list-page.cy.ts diff --git a/apps/web/cypress/e2e/recipes.cy.ts b/apps/web/cypress/e2e/recipes.cy.ts index c6eebf6..34d2ca4 100644 --- a/apps/web/cypress/e2e/recipes.cy.ts +++ b/apps/web/cypress/e2e/recipes.cy.ts @@ -5,6 +5,8 @@ // The favorite-toggle and delete-recipe journeys moved to recipes.feature — // this file now only covers catalog browsing/display. +import { PHONE_VIEWPORT } from "../support/viewports"; + const authenticatedProfile = { id: 1, firstName: "Alice", @@ -460,3 +462,38 @@ describe("Recipe catalog", () => { ); }); }); + +// Ticket #90 — `.recipe-table-wrap` had no horizontal-scroll guard at all +// (unlike the planning grid's own wrapper), so a narrow viewport just let +// the browser compress every column instead of scrolling. Below the +// tablet breakpoint (1024px) the master-detail split already collapses to +// one column (recipes.scss's own `bp.tablet-down` rule) and gives the +// table the full page width — still comfortably under `.recipe-table`'s +// own 36rem floor on a phone, which is what this covers. +describe("Recipe catalog (phone)", () => { + beforeEach(() => { + cy.setViewport("phone"); + interceptAuth(); + }); + + it("keeps the table on its own horizontal scroll, at full width, instead of squeezing its columns", () => { + cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [ratatouille, omelette] }); + + cy.visit("/recettes"); + + cy.contains(".recipe-table__name", "Ratatouille").should("be.visible"); + cy.contains(".recipe-table__name", "Omelette").should("be.visible"); + + cy.get(".recipe-table-wrap").should(($el) => { + const s = getComputedStyle($el[0]); + expect(s.overflowX).to.be.oneOf(["auto", "scroll"]); + expect($el[0].scrollWidth).to.be.greaterThan($el[0].clientWidth); + }); + + // ...and that overflow stays *inside* the wrapper — same global rule + // every other page is held to. + cy.document() + .its("documentElement.scrollWidth") + .should("be.lte", PHONE_VIEWPORT.width + 1); + }); +}); diff --git a/apps/web/cypress/e2e/shopping-list-page.cy.ts b/apps/web/cypress/e2e/shopping-list-page.cy.ts new file mode 100644 index 0000000..79ed527 --- /dev/null +++ b/apps/web/cypress/e2e/shopping-list-page.cy.ts @@ -0,0 +1,67 @@ +// Plain layout-mode spec (not Gherkin) for ShoppingListPage's own +// phone-viewport pass (issue #90) — the actual grouped-list rendering, +// aggregation and week-navigation journey is already covered end-to-end by +// shopping-list.feature; this only exercises the responsive change added +// here. Mocks the API via cy.intercept — this job doesn't run a live +// backend (see .github/workflows/ci.yml). + +import { PHONE_VIEWPORT } from "../support/viewports"; + +const authenticatedProfile = { + id: 1, + firstName: "Alice", + lastName: "Martin", + email: "alice@example.com", + tokenVersion: 0, + houseId: 1, + dietId: null, +}; + +/** Same minimal-fixture convention as shopping-list.ts's own `buildShoppingListItem` step-def helper — only the fields this page actually reads. */ +function shoppingListItem(id: number, ingredientKey: string, icon: string, category: string) { + return { + ingredient: { + id, + key: ingredientKey, + icon, + category, + subcategory: category, + reproducible: false, + allergens: [], + diets: [], + }, + quantity: 2, + unit: { id, key: "piece", type: "COUNT", toBaseFactor: 1 }, + }; +} + +describe("Shopping list page (phone)", () => { + beforeEach(() => { + cy.setViewport("phone"); + cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); + }); + + it("gives each row a taller, more legible padding than on desktop, with no horizontal body scroll", () => { + cy.intercept("GET", /\/shopping-list\?/, { + statusCode: 200, + body: { + startDate: "2026-08-17", + finishDate: "2026-08-23", + items: [shoppingListItem(1, "egg", "EGG", "dairyAndCheese")], + }, + }).as("getList"); + + cy.visit("/liste-de-courses"); + cy.wait("@getList"); + + cy.get(".shopping-list__item").should(($el) => { + // --space-md (16px) on phone vs. the desktop default --space-sm + // (8px) — see shopping-list-page.scss's phone block. + expect(getComputedStyle($el[0]).paddingTop).to.equal("16px"); + }); + + cy.document() + .its("documentElement.scrollWidth") + .should("be.lte", PHONE_VIEWPORT.width + 1); + }); +}); diff --git a/apps/web/src/features/recipes/recipes.scss b/apps/web/src/features/recipes/recipes.scss index a87c0a8..6cb8f8c 100644 --- a/apps/web/src/features/recipes/recipes.scss +++ b/apps/web/src/features/recipes/recipes.scss @@ -260,6 +260,17 @@ height: 100%; min-height: 0; overflow-y: auto; + // Issue #90: unlike the planning grid's own wrapper (planning-page.scss), + // this one had no horizontal guard at all — on a narrow viewport the + // table's `width: 100%` below just let the browser compress every column + // (photo/name/allergens/diets) to fit instead of scrolling. Same fix, + // scaled down: 4 narrower columns don't need anywhere near the planning + // grid's 62rem, but they do need *some* floor so the badge columns don't + // get squeezed to the point of wrapping every single pill onto its own + // line. Shared by both the full catalog (`/recettes`) and + // `RecipePickerDialog`'s own instance of this same table/wrapper — fixes + // the identical compression there too, not just the full-page case. + overflow-x: auto; background: var(--color-surface); border-radius: var(--radius-md); box-shadow: var(--shadow-sm); @@ -267,6 +278,7 @@ .recipe-table { width: 100%; + min-width: 36rem; border-collapse: collapse; text-align: left; diff --git a/apps/web/src/pages/shopping-list/shopping-list-page.scss b/apps/web/src/pages/shopping-list/shopping-list-page.scss index 5b391f9..2ce9ba7 100644 --- a/apps/web/src/pages/shopping-list/shopping-list-page.scss +++ b/apps/web/src/pages/shopping-list/shopping-list-page.scss @@ -5,6 +5,8 @@ // `__status`), a simple grouped list rather than a grid below it. // ============================================================================= +@use "../../styles/breakpoints" as bp; + .shopping-list-page { height: 100%; display: flex; @@ -100,3 +102,25 @@ font-variant-numeric: tabular-nums; } } + +// --- Phone (issue #90) ------------------------------------------------------ +// This list has no checkbox/delete button at all — it's deliberately +// read-only (see ShoppingListPage.tsx's own doc comment) — so, unlike the +// planning grid or the cooking-session task cards, there's no *tap* target +// to enlarge here; the chantier's original ticket assumed some existed +// (written before this page's actual code was read). The structure also +// already doesn't overflow: `&-name` is `flex: 1; min-width: 0` and wraps +// rather than pushing `&-quantity` out. What phone-viewport reading +// genuinely rewards here — held in one hand, glanced at repeatedly while +// pushing a cart — is a slightly roomier row and a clearer icon, not a +// structural change. +@include bp.phone { + .shopping-list__item { + padding: var(--space-md); + } + + .shopping-list__item-icon svg { + width: 1.5rem; + height: 1.5rem; + } +} -- 2.45.2