batchCooking/apps/web/src/features/planning/recipe-picker-dialog.scss
Nicolas 642b37ebec
All checks were successful
CI / lint (push) Successful in 1m39s
CI / build (push) Successful in 1m43s
CI / intent-service-test (push) Successful in 3m56s
CI / e2e (push) Successful in 8m37s
CI / test (push) Successful in 16m12s
CI / build-android (push) Has been skipped
fix(planning): affiche les recettes du picker sur mobile (#111)
Sur téléphone, le dialogue d'ajout d'une recette au planning devenait
plein écran mais `.recipe-picker-dialog` gardait un `height: 80vh` figé.
Dans cette colonne flex de hauteur bornée, le bloc `.recipe-picker__filters`
(rendu pour les 4 onglets non-source) consommait toute la place et
`.recipes-page__catalog` (`flex: 1`) s'effondrait à ~0 : la table des
recettes était dans le DOM mais dans un scrollport de hauteur nulle, donc
les onglets Favoris / Perso / Foyer / Publique paraissaient vides.

Sous le breakpoint phone : le dialogue passe vraiment plein écran (comme
tout autre `Dialog`), `.recipe-picker__filters` ne se laisse plus
compresser, et le catalogue prend sa hauteur de contenu — c'est
`.dialog-panel__body` qui scrolle.

Closes #111

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 01:18:17 +02:00

257 lines
8.8 KiB
SCSS

// RecipePickerDialog — the dialog panel itself is styled generically by
// Dialog.tsx's dialog.scss; only this feature's own filter bar / chips /
// portions-confirmation step live here. `.recipes-page__status` and
// `.field-error` come from recipes.scss (already loaded — RecipeTable /
// RecipeTabs import it themselves).
@use "../../styles/breakpoints" as bp;
.recipe-picker-dialog {
width: 95vw;
max-width: 85rem;
// Fixed, not just capped — `.dialog-panel`'s own `max-height` (dialog.scss)
// only bounds how tall a shrink-to-fit dialog can grow, which is right
// for every other dialog's small form but leaves this one's browsing
// step at the mercy of how much content happens to be on screen (a short
// personal-recipe tab vs. a ~25-item source browse). Pinning both
// `height`/`max-height` to the same `80vh` keeps the browsing area a
// consistent, generous size regardless of tab/content, on top of the
// `.dialog-panel__body` fix below that makes that area actually use the
// space instead of overflowing it.
height: 80vh;
max-height: 80vh;
// `.dialog-panel__body` (dialog.scss) is a plain block-flow scroll
// container by default — fine for every other dialog's small form, but
// this one's browsing step embeds the same components `/recettes` uses
// (`RecipeTable`'s `.recipe-table-wrap`, `RecipeSourcesPanel`'s
// `.recipes-page__catalog`), both of which size themselves with
// `flex: 1; min-height: 0` and need a `display: flex` ancestor for that
// to mean anything — on the real page that ancestor is `.recipes-page`
// itself (see its own doc comment for the identical fix that page
// needed once); this dialog never renders that wrapper, so without this
// the catalog/table just grew to its full content height instead of
// being clipped and independently scrollable within the dialog's own
// bounds — harmless for the handful of rows a personal recipe tab
// usually has, but a source tab's ~25-item browse list made it obvious:
// everything past the dialog's fixed height rendered, technically, just
// never inside the visible/scrollable area. Scoped to this dialog only
// — every other `Dialog` caller keeps the plain block layout.
.dialog-panel__body {
display: flex;
flex-direction: column;
}
.recipe-table-wrap {
flex: 1;
min-height: 0;
}
// `.recipes-page__catalog`'s own column split (recipes.scss) sizes the
// detail pane with `minmax(35vw, 38vw)` — a fraction of the *viewport*,
// which tracked `/recettes`' own width there (that grid spans nearly the
// whole page) but has nothing to do with this dialog's width, now a
// fixed 92vw/75rem of its own. Overridden here as a fraction of the
// dialog itself instead, so the two panes stay proportionate to each
// other regardless of viewport size.
.recipes-page__catalog {
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
}
// On a phone this dialog goes truly full screen — same as every other
// `Dialog` (see `dialog.scss`'s own `bp.phone` block) — rather than
// keeping the 80vh pin above. That pin sizes a roomy desktop browsing
// area, but on a ~375px-wide screen it left far too little height once
// the tall `.recipe-picker__filters` block (search + ingredient chips +
// `IngredientPicker` + `DietTagSelect` + household toggle, rendered only
// for the four non-source tabs) was laid out first — the catalog below
// it collapsed to ~0 and the recipe table, though in the DOM, was
// rendered in a zero-height scrollport, so every non-source tab looked
// empty (issue #111).
@include bp.phone {
height: 100vh;
max-height: 100vh;
// recipes.scss already collapses `&__catalog` to a single column under
// 1024px, but the rule further up (2 classes) outweighs that
// page-scoped `@media` block and would keep two ~90px columns side by
// side on a phone. Re-collapse it here — the list stacks above the
// detail pane, same as the full `/recettes` page.
.recipes-page__catalog {
grid-template-columns: 1fr;
// ...and stop it competing for leftover flex space it won't get. As a
// `flex: 1` (⇒ `flex-basis: 0`) child of the flex-column
// `.dialog-panel__body`, with the filter block above consuming the
// column, it otherwise shrinks to ~0. On a phone the whole body
// scrolls instead (`.dialog-panel__body`'s own `overflow-y: auto`),
// so the catalog just takes its content height.
flex: none;
}
// Same reasoning: size the table wrapper to its content and let it
// scroll with the body, instead of clipping inside a collapsed catalog.
.recipe-table-wrap {
flex: none;
height: auto;
}
// The filter block must stay fully reachable — it's the one part a
// person needs to actually narrow the list down — so never let the
// flex column squeeze it.
.recipe-picker__filters {
flex-shrink: 0;
}
}
}
.recipe-picker__filters {
display: flex;
flex-direction: column;
gap: var(--space-md);
margin-bottom: var(--space-md);
}
.recipe-picker__search {
width: 100%;
padding: var(--space-sm) var(--space-md);
border: 1.5px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface);
color: var(--color-text);
font-size: var(--font-size-base);
}
.recipe-picker__filter-label {
display: block;
margin-bottom: var(--space-xs);
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--color-text-muted);
}
.recipe-picker__chips {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-xs);
}
.filter-chip {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-pill);
background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
color: var(--color-primary);
font-size: var(--font-size-sm);
font-weight: 600;
button {
border: none;
background: transparent;
color: inherit;
cursor: pointer;
padding: 0;
font-size: var(--font-size-xs);
}
}
.recipe-picker__toggle-ingredient-picker {
padding: var(--space-xs) var(--space-sm);
border: 1.5px dashed var(--color-border);
border-radius: var(--radius-pill);
background: transparent;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
cursor: pointer;
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
}
// --- Portions confirmation step -----------------------------------------
.recipe-picker-confirm {
display: flex;
flex-direction: column;
gap: var(--space-sm);
label {
font-weight: 600;
}
input[type="number"] {
padding: var(--space-sm) var(--space-md);
border: 1.5px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface);
color: var(--color-text);
font-size: var(--font-size-base);
max-width: 8rem;
}
}
// "→ N portions préparées, M en restes" — shown under the portions field
// only when the requested count doesn't land on a whole batch of the
// recipe, so the user sees the leftover a batch-cooking round-up creates
// before confirming (issue #34).
.recipe-picker-confirm__batched-hint {
margin: 0;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
.recipe-picker-confirm__actions {
display: flex;
justify-content: flex-end;
gap: var(--space-sm);
margin-top: var(--space-sm);
}
.recipe-picker-confirm__confirm {
background: var(--color-primary);
color: var(--color-surface);
border: none;
border-radius: var(--radius-base);
padding: var(--space-sm) var(--space-md);
font-family: var(--font-body);
font-size: var(--font-size-sm);
font-weight: 600;
cursor: pointer;
&:hover {
background: var(--color-primary-hover);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
// The dialog-level footer (`Dialog`'s `footer` prop, used by this dialog's
// main browsing step) — "Confirmer" reuses `.recipe-picker-confirm__confirm`
// above (same primary-button look as the portions step's own "Ajouter au
// planning"), "Fermer" needs its own secondary style since a bare
// `<button>` here would render with the browser's own default sizing/
// font instead of matching it — same "bordered, no fill" secondary look
// as `.recipe-detail-panel__delete-confirm`'s cancel button.
.dialog-panel__footer button:not(.recipe-picker-confirm__confirm) {
padding: var(--space-sm) var(--space-md);
font-family: var(--font-body);
font-size: var(--font-size-sm);
font-weight: 600;
cursor: pointer;
border-radius: var(--radius-base);
border: 1px solid var(--color-border);
background: var(--color-surface);
color: var(--color-text);
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
}