All checks were successful
Fondation du chantier UI mobile & tablette (#91) : jusqu'ici, toute adaptation mobile de ce repo etait du CSS pur sur le meme DOM (aucun composant ne branchait son JSX selon la largeur/plateforme). Ce ticket introduit le premier vrai branchement, et les primitifs que les 8 tickets suivants du chantier reutiliseront. - useIsMobileViewport() (apps/web/src/lib/) : point de verite unique, base sur matchMedia, live (reagit a un resize/une rotation). - _breakpoints.scss : $phone/$tablet Sass + mixins phone/phone-up/ tablet-down, remplacant les nombres magiques 640/1024 repetes en dur dans 6 fichiers (AppLayout, landing, recipes, recipe-picker-dialog, planning-page, dialog, tours-page). Les deux occurrences de 860px (landing hero, tours-page) sont deliberement laissees telles quelles - un collapse de grille 2 colonnes qui n'a rien a voir avec le breakpoint tablette du catalogue recettes, les unifier aurait change un comportement visuel reel sans raison. - useNativeDialogBehavior : mecanique (showModal/close event/clic exterieur) extraite de Dialog.tsx, partagee avec le nouveau BottomSheet.tsx (feuille mobile plein-largeur, meme fondation <dialog> natif que Dialog, juste ancree en bas). - AppLayout : sur telephone, la sidebar n'est plus reflow-ee en CSS en barre horizontale (l'ancien traitement) - elle est remplacee par une vraie barre d'onglets basse (Planning/Recettes/Liste de courses + Plus), le "Plus" ouvrant une BottomSheet combinant Parametres et Compte (deux dropdowns flottants separes cote desktop -> un seul point d'entree mobile, IA plus simple, un seul emplacement d'onglet). - dialog.scss : vrai plein-ecran (bord a bord, sans arrondi) sous le breakpoint phone au lieu d'une marge simplement reduite. Verifie en local : pnpm --filter web exec tsc -b --force, biome check, pnpm --filter web build, suite Cypress e2e complete (140/140) et composants (63/63) - y compris layout.cy.ts, reecrit pour tester la nouvelle barre basse (l'ancien test de la barre horizontale n'a plus de sens, ce traitement n'existe plus). Closes #91 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
223 lines
7.2 KiB
SCSS
223 lines
7.2 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);
|
|
}
|
|
|
|
// 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.
|
|
@include bp.phone {
|
|
.recipes-page__catalog {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
}
|
|
|
|
.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);
|
|
}
|
|
}
|