feat(web): passe responsive téléphone pour le contenu des pages (#39 phase 3)
All checks were successful
CI / lint (push) Successful in 2m28s
CI / build (push) Successful in 2m42s
CI / intent-service-test (push) Successful in 4m27s
CI / e2e (push) Successful in 8m24s
CI / test (push) Successful in 15m28s

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 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-09-06 03:06:38 +02:00
parent b0ac42441f
commit a8d86c7099
5 changed files with 134 additions and 0 deletions

View file

@ -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", () => { describe("Color theme — light/dark tokens actually reach the rendered chrome", () => {
beforeEach(() => { beforeEach(() => {
interceptAuth(); interceptAuth();

View file

@ -23,6 +23,16 @@
&::backdrop { &::backdrop {
background: rgba(0, 0, 0, 0.45); 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 { .dialog-panel__header {

View file

@ -55,6 +55,17 @@
.recipes-page__catalog { .recipes-page__catalog {
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); 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 { .recipe-picker__filters {

View file

@ -365,6 +365,15 @@
flex-direction: column; 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 { .app-sidebar {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;

View file

@ -264,3 +264,29 @@ table.planning-grid {
background: color-mix(in srgb, var(--color-primary) 8%, transparent); 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);
}
}