From a8d86c7099f00aef04a9a07b70cf221ad4f39be0 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 6 Sep 2026 03:06:38 +0200 Subject: [PATCH] =?UTF-8?q?feat(web):=20passe=20responsive=20t=C3=A9l?= =?UTF-8?q?=C3=A9phone=20pour=20le=20contenu=20des=20pages=20(#39=20phase?= =?UTF-8?q?=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Réutilise la philosophie déjà en place pour la sidebar (défilement horizontal plutôt que compression illisible) au lieu d'un nouveau patron par page. Tout est strictement additif sous 640px — le rendu desktop est inchangé. - `AppLayout.scss` : `.app-content` passe de `--space-xl` (32px) à `--space-md` (16px) sous 640px — 64px de largeur utile récupérés sur un écran de ~375px. - `planning-page.scss` : la grille 7 jours défile déjà horizontalement (`overflow: auto` + `min-width: 62rem`) ; ce qui casse au tactile, c'est le `✕` de suppression d'une recette (`opacity: 0` hors survol) — épinglé visible + cible tactile agrandie sous 640px, idem le bouton « + ». - `dialog.scss` : `.dialog-panel` garde une gouttière latérale et gagne en hauteur utile sous 640px (le `--space-2xl` vertical est trop cher sur un écran court). - `recipe-picker-dialog.scss` : le split 2 colonnes forcé du catalogue (plus spécifique que le `@media` de recipes.scss) repasse à une colonne sous 640px. Tests : `layout.cy.ts` gagne un bloc « page content under 640px » — padding de `.app-content`, grille sur son propre scroll sans déborder la page, `✕` visible au tactile. 10/10 en local. Refs #39 Co-Authored-By: Claude Sonnet 5 --- apps/web/cypress/e2e/layout.cy.ts | 78 +++++++++++++++++++ apps/web/src/components/ui/dialog.scss | 10 +++ .../planning/recipe-picker-dialog.scss | 11 +++ apps/web/src/layouts/AppLayout.scss | 9 +++ .../web/src/pages/planning/planning-page.scss | 26 +++++++ 5 files changed, 134 insertions(+) diff --git a/apps/web/cypress/e2e/layout.cy.ts b/apps/web/cypress/e2e/layout.cy.ts index 05f1134..4369ea5 100644 --- a/apps/web/cypress/e2e/layout.cy.ts +++ b/apps/web/cypress/e2e/layout.cy.ts @@ -213,6 +213,84 @@ describe("Responsive breakpoint — sidebar becomes a horizontal top bar under 6 }); }); +describe("Responsive breakpoint — page content under 640px (issue #39)", () => { + const PHONE: [number, number] = [375, 812]; + + beforeEach(() => { + interceptAuth(); + }); + + it("tightens .app-content padding from 32px to 16px on a phone", () => { + cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }); + + cy.viewport(1600, 900); + cy.visit("/"); + cy.get(".app-content").should(($el) => { + const s = getComputedStyle($el[0]); + // Desktop: var(--space-xl) === 2rem === 32px, all four sides. + expect(s.paddingTop).to.equal("32px"); + expect(s.paddingLeft).to.equal("32px"); + }); + + cy.viewport(...PHONE); + cy.visit("/"); + cy.get(".app-content").should(($el) => { + const s = getComputedStyle($el[0]); + // Phone: var(--space-md) === 1rem === 16px. + expect(s.paddingTop).to.equal("16px"); + expect(s.paddingLeft).to.equal("16px"); + }); + }); + + it("keeps the 7-day planning grid on its own horizontal scroll rather than overflowing the page", () => { + cy.intercept("GET", /\/planning\?/, { + statusCode: 200, + body: { + id: 1, + startDate: "2026-08-17T00:00:00.000Z", + finishDate: "2026-08-23T00:00:00.000Z", + items: [ + { + id: 1, + weekDay: "lundi", + meal: "dejeuner", + portions: 4, + portionsRequested: 4, + discardedPortions: 0, + sourceItem: null, + recipe: { id: 1, name: "Curry de lentilles" }, + }, + ], + }, + }); + + cy.viewport(...PHONE); + cy.visit("/"); + + // The grid wrapper is the scroll container, and its content is genuinely + // wider than the phone viewport (7 columns never crushed). + cy.get(".planning-grid-wrapper").should(($el) => { + expect(getComputedStyle($el[0]).overflowX).to.be.oneOf(["auto", "scroll"]); + expect($el[0].scrollWidth).to.be.greaterThan($el[0].clientWidth); + }); + + // ...and that overflow stays *inside* the wrapper — the document itself + // never scrolls sideways (global rule against a horizontally-scrolling + // body). + cy.document() + .its("documentElement.scrollWidth") + .should("be.lte", 375 + 1); + + // The per-recipe remove "✕" is hover-gated on desktop; on touch it must + // be permanently visible or a planned recipe can't be removed at all. + cy.get(".recipe-chip__remove") + .first() + .should(($btn) => { + expect(Number(getComputedStyle($btn[0]).opacity)).to.be.greaterThan(0); + }); + }); +}); + describe("Color theme — light/dark tokens actually reach the rendered chrome", () => { beforeEach(() => { interceptAuth(); diff --git a/apps/web/src/components/ui/dialog.scss b/apps/web/src/components/ui/dialog.scss index 8e8b04e..5948032 100644 --- a/apps/web/src/components/ui/dialog.scss +++ b/apps/web/src/components/ui/dialog.scss @@ -23,6 +23,16 @@ &::backdrop { background: rgba(0, 0, 0, 0.45); } + + // On a phone the `48rem` cap is irrelevant (the viewport is narrower), + // but the `var(--space-2xl)` top/bottom breathing room is a large slice + // of a short screen — tighten it so a form-heavy dialog isn't + // needlessly cramped, and keep a small side gutter so the panel never + // sits edge to edge. + @media (max-width: 640px) { + max-width: calc(100vw - var(--space-md)); + max-height: calc(100vh - var(--space-md)); + } } .dialog-panel__header { diff --git a/apps/web/src/features/planning/recipe-picker-dialog.scss b/apps/web/src/features/planning/recipe-picker-dialog.scss index 805e933..45c78d8 100644 --- a/apps/web/src/features/planning/recipe-picker-dialog.scss +++ b/apps/web/src/features/planning/recipe-picker-dialog.scss @@ -55,6 +55,17 @@ .recipes-page__catalog { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); } + + // recipes.scss already collapses `&__catalog` to a single column under + // 1024px, but the rule just above (2 classes) outweighs that page-scoped + // `@media` block and would keep two ~90px columns side by side on a + // phone. Re-collapse it here for the dialog's own narrow case — the list + // then stacks above the detail pane, same as the full `/recettes` page. + @media (max-width: 640px) { + .recipes-page__catalog { + grid-template-columns: 1fr; + } + } } .recipe-picker__filters { diff --git a/apps/web/src/layouts/AppLayout.scss b/apps/web/src/layouts/AppLayout.scss index e8d5365..efa950a 100644 --- a/apps/web/src/layouts/AppLayout.scss +++ b/apps/web/src/layouts/AppLayout.scss @@ -365,6 +365,15 @@ flex-direction: column; } + // The desktop `var(--space-xl)` (32px) all around eats a big share of a + // ~375px screen — every page then has ~64px less usable width than the + // viewport already gives it. Drop to `--space-md` (16px) on phones; the + // grid/table pages that still overflow fall back to their own horizontal + // scroll (same philosophy as the sidebar below). + .app-content { + padding: var(--space-md); + } + .app-sidebar { flex-direction: row; align-items: center; diff --git a/apps/web/src/pages/planning/planning-page.scss b/apps/web/src/pages/planning/planning-page.scss index 12b63d5..82a633a 100644 --- a/apps/web/src/pages/planning/planning-page.scss +++ b/apps/web/src/pages/planning/planning-page.scss @@ -264,3 +264,29 @@ table.planning-grid { background: color-mix(in srgb, var(--color-primary) 8%, transparent); } } + +// --- Phone (issue #39, responsive pass) ----------------------------------- +// The grid itself already degrades gracefully on a narrow screen — the +// wrapper's `overflow: auto` + the table's `min-width: 62rem` mean it just +// scrolls horizontally rather than crushing seven day-columns into +// illegibility (same philosophy as the sidebar's own fallback). What does +// NOT survive the switch to touch are the two hover-gated affordances +// below. +@media (max-width: 640px) { + // `.recipe-chip__remove` sits at `opacity: 0` until its chip is hovered + // — invisible, and untappable, on a touch screen where nothing is ever + // "hovered". Pin it visible so a planned recipe can actually be removed + // on a phone. Also give it a real tap target. + .recipe-chip__remove { + opacity: 0.7; + width: 1.5rem; + height: 1.5rem; + font-size: 0.8rem; + } + + // A slightly taller "+" so adding to a slot is a comfortable tap, not a + // 5px sliver. + .add-recipe-btn { + padding: var(--space-sm); + } +}