All checks were successful
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>
257 lines
8.8 KiB
SCSS
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);
|
|
}
|
|
}
|