batchCooking/apps/web/src/features/recipes/recipes.scss
Nicolas 7d1a69cf12
Some checks failed
CI / lint (push) Successful in 3m2s
CI / build (push) Successful in 3m5s
CI / intent-service-test (push) Successful in 5m14s
CI / e2e (push) Successful in 9m38s
CI / test (push) Failing after 18m46s
CI / build-android (push) Has been skipped
fix(mobile): renforce le catalogue de recettes et la liste de courses sur mobile (#90)
Deux correctifs, regroupés en un seul ticket comme prévu :

- Catalogue de recettes (`recipes.scss`) : `.recipe-table-wrap` n'avait
  aucun garde-fou de scroll horizontal, contrairement à la grille
  planning — sur un viewport étroit, les 4 colonnes (photo/nom/
  allergènes/régimes) se compressaient au lieu de scroller. Ajout
  d'`overflow-x: auto` sur le wrapper + `min-width: 36rem` sur la
  table (échelle réduite par rapport aux 62rem de la grille planning :
  4 colonnes plus étroites, pas 7). Partagé par le catalogue complet
  ET par `RecipePickerDialog`, qui hérite du même correctif au passage.

- Liste de courses (`shopping-list-page.scss`) : audit dédié — la page
  n'a en réalité aucune case à cocher ni bouton de suppression (elle
  est volontairement en lecture seule, voir le commentaire de
  ShoppingListPage.tsx), contrairement à ce que supposait le ticket
  d'origine ; rien à agrandir comme cible tactile de ce côté. La
  structure ne déborde déjà pas non plus (`&-name` a déjà flex:1/
  min-width:0). Amélioration retenue à la place : une ligne un peu
  plus aérée et une icône plus grande sous le breakpoint phone, pour
  une lecture plus confortable en faisant les courses (liste tenue
  d'une main, coups d'œil répétés).

Couverture Cypress : nouveau describe "Recipe catalog (phone)" dans
recipes.cy.ts (garde-fou de scroll horizontal effectivement actif) ;
nouveau fichier shopping-list-page.cy.ts, spec layout simple (pas
Gherkin, cette page n'a pas encore ce type de fichier) pour le padding
de ligne. Suite complète : biome check, tsc -b --force, build, 146/146
e2e, 71/71 composants (un skip ponctuel de TechStepCorrectionPopover
en run groupé, reconfirmé 10/10 isolément — flake local sans lien avec
ce ticket) — tous verts.

Closes #90

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

2031 lines
No EOL
54 KiB
SCSS

// =============================================================================
// Recipe catalog — shared by RecipesPage, RecipeFormPage and every component
// under features/recipes/. Colocated here (rather than split per-component)
// since the catalog is one cohesive visual unit and most rules are small
// enough that splitting them would just add files to jump between — same
// choice as settings-pages.scss for the settings pages.
//
// The master-detail layout (tabs + table + detail panel) was ported from a
// reviewed standalone HTML mockup onto the app's real design tokens — no
// light/dark duplication needed here, every `var(--color-*)` below already
// resolves per-theme globally (see styles/_theme.scss).
// =============================================================================
@use "../../styles/breakpoints" as bp;
// --- Allergen / diet / disliked-ingredient badges ---------------------------
// Three visually distinct pill languages, each with its own meaning — never
// mixed, so a glance tells them apart without reading the label:
// - allergens: --color-allergen, solid fill (a food-safety warning)
// - diets: --color-tag (turmeric — "category/classification tag" per
// _theme.scss), solid fill (a neutral reminder, not a warning)
// - disliked ingredients: neutral outline, dashed border (a personal
// taste preference — deliberately *not* on the allergen/warning scale)
.allergen-badges,
.diet-badges,
.disliked-badges {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
margin: var(--space-sm) 0 0;
padding: 0;
list-style: none;
}
.allergen-badge,
.diet-badge,
.disliked-badge {
padding: 0.15rem 0.6rem;
font-size: var(--font-size-xs);
font-weight: 600;
border-radius: var(--radius-pill);
}
.allergen-badge {
color: var(--color-allergen-ink);
background: var(--color-allergen);
}
.diet-badge {
color: var(--color-tag-ink);
background: var(--color-tag);
}
.disliked-badge {
color: var(--color-text-muted);
background: none;
border: 1px dashed var(--color-border);
}
// A fourth, distinct pill language for `ReproducibleBadge` — neither a
// warning (allergen) nor a classification (diet) nor a negative (disliked):
// a positive nudge ("this could be homemade"), so it borrows
// --color-primary (the same tinted-fill treatment as `.is-selected` in
// global.scss) rather than any of the three above. `margin-top` matches its
// siblings' `.allergen-badges`/`.diet-badges` list wrappers even though this
// is a lone element, not a list, so it lines up the same way when it's the
// only badge present.
.reproducible-badge {
display: inline-block;
margin: var(--space-sm) 0 0;
padding: 0.15rem 0.6rem;
font-size: var(--font-size-xs);
font-weight: 600;
color: var(--color-primary);
background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
border-radius: var(--radius-pill);
}
.reproducible-badge--link {
text-decoration: none;
cursor: pointer;
&:hover {
background: color-mix(in srgb, var(--color-primary) 22%, var(--color-surface));
}
}
// --- Catalog page -------------------------------------------------------------
// `.app-content` (AppLayout.scss) already stretches to the full viewport
// height — same reasoning as `planning-page.scss`. `.recipes-page` fills
// that box as a column so `__catalog` can grow to fill whatever's left
// under the header/tabs, instead of being only as tall as its content.
.recipes-page {
height: 100%;
display: flex;
flex-direction: column;
&__header {
flex-shrink: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--space-md);
margin-bottom: var(--space-md);
}
&__search {
flex: 1 1 16rem;
max-width: 24rem;
padding: 0.5rem var(--space-md);
font-family: var(--font-body);
font-size: var(--font-size-sm);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface);
color: var(--color-text);
}
&__new-button {
flex: none;
padding: 0.5rem var(--space-md);
font-family: var(--font-body);
font-size: var(--font-size-sm);
font-weight: 600;
color: #fff;
background: var(--color-primary);
border: none;
border-radius: var(--radius-base);
cursor: pointer;
text-decoration: none;
&:hover {
background: var(--color-primary-hover);
}
}
&__status {
margin-top: var(--space-xl);
color: var(--color-text-muted);
font-size: var(--font-size-md);
}
&__status--error {
color: var(--color-error);
}
// The master-detail row: table (flexible) + detail panel (never under
// 35% of the *viewport* width — `minmax(35vw, 38vw)`, not a fraction of
// this grid, per the reviewed mockup), both stretched to the same full
// height so the detail panel never just shrink-wraps to its own content.
&__catalog {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(35vw, 38vw);
// Without this, the grid's single implicit row falls back to
// auto-sizing (tall enough for its content), not the full height of
// `&__catalog` itself — silently masked as long as `.app-content`'s own
// height was `min-height: 100vh` (auto-grew to fit anything, so
// nothing downstream ever actually needed to stretch to a fixed
// height). Fixing that (see AppLayout.scss) made `&__catalog`'s height
// genuinely definite for the first time, which surfaced this: the
// detail panel's own `height: 100%; overflow-y: auto` (see
// `.recipe-detail-panel` below) had nothing to stretch against, came
// out shorter than its content, and silently clipped it.
grid-template-rows: minmax(0, 1fr);
gap: var(--space-lg);
}
@include bp.tablet-down {
&__catalog {
grid-template-columns: 1fr;
// The single-row `1fr` track above only makes sense for the desktop
// side-by-side layout — collapsed to one column, the table and the
// detail panel stack into two separate rows instead, so forcing them
// both into one `1fr` track split the available height in half and
// gave the table row 0px (its own content pushed below the fold,
// clipped by its `overflow-y: auto`). Reset to the default `auto`
// (each row sized to its content) — matches `height: auto` below,
// which already stopped assuming a single fixed-height row here.
grid-template-rows: auto;
height: auto;
}
}
}
// --- Tabs ------------------------------------------------------------------
.recipe-tabs {
flex-shrink: 0;
display: flex;
gap: var(--space-xs);
// Never lets a tab overflow the page (which would force the whole body
// to scroll horizontally, see global.scss's rule against that) — scrolls
// within itself instead once the tabs (favoris/perso/foyer/publique,
// plus one per household-enabled source) don't all fit, same pattern as
// the sidebar's own nav.
overflow-x: auto;
border-bottom: 1px solid var(--color-border);
margin-bottom: var(--space-md);
&__tab {
appearance: none;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0.4rem;
padding: var(--space-sm) var(--space-md);
font-family: var(--font-display);
font-size: var(--font-size-sm);
font-weight: 700;
letter-spacing: 0.02em;
color: var(--color-text-muted);
background: none;
border: none;
border-bottom: 2.5px solid transparent;
cursor: pointer;
transition:
color 0.15s ease,
border-color 0.15s ease;
svg {
width: 1.05rem;
height: 1.05rem;
flex: none;
}
// A source tab's own icon (`SourceView.iconUrl`) — sized to match the
// Lucide `svg` icons above so a source tab doesn't stand out from the
// four real ones.
.recipe-tabs__source-icon {
width: 1.05rem;
height: 1.05rem;
flex: none;
object-fit: contain;
}
&:hover {
color: var(--color-text);
}
&.active {
color: var(--color-primary);
border-color: var(--color-primary);
}
&.placeholder {
color: var(--color-border);
cursor: not-allowed;
&:hover {
color: var(--color-border);
}
}
}
}
// --- Catalog table -----------------------------------------------------------
.recipe-table-wrap {
height: 100%;
min-height: 0;
overflow-y: auto;
// Issue #90: unlike the planning grid's own wrapper (planning-page.scss),
// this one had no horizontal guard at all — on a narrow viewport the
// table's `width: 100%` below just let the browser compress every column
// (photo/name/allergens/diets) to fit instead of scrolling. Same fix,
// scaled down: 4 narrower columns don't need anywhere near the planning
// grid's 62rem, but they do need *some* floor so the badge columns don't
// get squeezed to the point of wrapping every single pill onto its own
// line. Shared by both the full catalog (`/recettes`) and
// `RecipePickerDialog`'s own instance of this same table/wrapper — fixes
// the identical compression there too, not just the full-page case.
overflow-x: auto;
background: var(--color-surface);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.recipe-table {
width: 100%;
min-width: 36rem;
border-collapse: collapse;
text-align: left;
th {
position: sticky;
top: 0;
z-index: 1;
background: var(--color-surface);
font-family: var(--font-display);
font-size: var(--font-size-xs);
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--color-text-muted);
padding: var(--space-sm) var(--space-md);
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
}
td {
padding: var(--space-sm) var(--space-md);
border-bottom: 1px solid var(--color-border);
vertical-align: middle;
}
tbody tr {
cursor: pointer;
transition: background-color 0.12s ease;
&:hover {
background: var(--color-surface-alt);
}
&.selected {
background: color-mix(in srgb, var(--color-primary) 10%, var(--color-surface));
td:first-child {
box-shadow: inset 3px 0 0 var(--color-primary);
}
}
&:last-child td {
border-bottom: none;
}
}
&__photo {
width: 2.6rem;
height: 2.6rem;
border-radius: var(--radius-base);
background: var(--color-surface-alt);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
&__name {
font-weight: 600;
}
&__fav-mark {
color: var(--color-tag);
margin-left: 0.3rem;
font-size: var(--font-size-sm);
}
&__muted {
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
}
// --- Sources tab (RecipeSourcesPanel) ---------------------------------------
// Reuses .recipes-page__header/__search/__catalog and .recipe-table(-wrap)
// as-is (see RecipeSourcesPanel.tsx/SourceItemTable.tsx) — only what's
// actually new to this tab gets its own rules here.
.recipes-page__header--sources {
gap: var(--space-sm);
}
.source-item-table__imported-badge {
padding: 0.1rem 0.5rem;
font-size: var(--font-size-xs);
font-weight: 600;
color: var(--color-text-muted);
background: var(--color-surface-alt);
border-radius: var(--radius-pill);
white-space: nowrap;
}
.source-items-column {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
// Browsing a source scrolls infinitely (SourceItemTable's own sentinel row
// triggers RecipeSourcesPanel's handleLoadMore) — this is only the inline
// retry action shown alongside recipes.sources.loadMoreError when a page
// fetch actually fails, not a persistent "load more" control.
.source-items-retry {
padding: 0;
font-family: inherit;
font-size: inherit;
font-weight: 600;
color: var(--color-primary);
background: none;
border: none;
text-decoration: underline;
cursor: pointer;
}
// Pulsing placeholder rows `SourceItemTable` appends below the real items
// while `RecipeSourcesPanel`'s "load more" fetch is in flight — with that
// panel prefetching the next page ahead of time (as soon as the sentinel
// row scrolls near view), this is usually a very brief flash rather than
// an actual wait, but it keeps the list
// filling in instead of looking like nothing happened either way.
@keyframes source-item-skeleton-pulse {
0%,
100% {
opacity: 0.4;
}
50% {
opacity: 0.9;
}
}
// Invisible marker row `SourceItemTable`'s `IntersectionObserver` watches
// to trigger infinite scroll — no padding/border of its own, unlike a real
// row, so it doesn't show up as a stray empty stripe at the bottom of the
// list.
.source-item-table__sentinel td {
padding: 0;
border: none;
}
.source-item-table__row--skeleton {
cursor: default;
pointer-events: none;
&:hover {
background: none;
}
}
.source-item-table__photo--skeleton,
.source-item-table__skeleton-bar {
background: var(--color-border);
animation: source-item-skeleton-pulse 1.4s ease-in-out infinite;
}
.source-item-table__skeleton-bar {
display: block;
width: 60%;
height: 0.9rem;
border-radius: var(--radius-pill);
}
@media (prefers-reduced-motion: reduce) {
.source-item-table__photo--skeleton,
.source-item-table__skeleton-bar {
animation: none;
opacity: 0.6;
}
}
.source-item-preview__hint {
margin: 0 0 var(--space-sm);
padding: var(--space-xs) var(--space-sm);
font-size: var(--font-size-sm);
color: var(--color-warning);
background: color-mix(in srgb, var(--color-warning) 12%, var(--color-surface));
border-radius: var(--radius-base);
}
// --- Recipe detail panel -----------------------------------------------------
// 100% of the grid row's height (see `.recipes-page__catalog` above): the
// panel itself never scrolls, only its content does past that height
// (`overflow-y: auto`) — the photo/star/name stay visible while reading.
.recipe-detail-panel {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
background: var(--color-surface);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
&__status {
padding: var(--space-lg);
color: var(--color-text-muted);
font-size: var(--font-size-md);
}
&__status--error {
color: var(--color-error);
}
&__header {
flex-shrink: 0;
position: relative;
}
&__photo {
height: 9rem;
width: 100%;
background: linear-gradient(160deg,
color-mix(in srgb, var(--color-primary) 22%, var(--color-surface-alt)),
var(--color-surface-alt));
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
&__title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-md);
padding: var(--space-md) var(--space-md) 0;
h2 {
font-size: var(--font-size-lg);
}
}
&__title-main {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 0;
h2 {
margin: 0;
}
}
&__portions {
margin: 0;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
&__title-badges {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.3rem;
.allergen-badges,
.disliked-badges {
justify-content: flex-end;
margin-top: 0;
}
}
&__actions {
flex-shrink: 0;
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-md);
}
&__danger-button {
padding: 0.5rem var(--space-md);
font-family: var(--font-body);
font-size: var(--font-size-sm);
font-weight: 600;
color: #fff;
background: var(--color-error);
border: none;
border-radius: var(--radius-base);
cursor: pointer;
&:hover {
opacity: 0.9;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
&__delete-confirm {
display: flex;
align-items: center;
gap: var(--space-sm);
button:not(.recipe-detail-panel__danger-button) {
padding: 0.5rem 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);
}
}
&__section {
flex-shrink: 0;
padding: var(--space-md);
&+& {
border-top: 1px solid var(--color-border);
}
h3 {
font-size: var(--font-size-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
margin-bottom: var(--space-sm);
}
}
// The description gets its own, more generous section — the only "free
// text" on the panel, it needs more room to breathe than a badge or a
// short step.
&__section--description {
padding: var(--space-lg) var(--space-md);
}
&__description {
color: var(--color-text);
font-size: var(--font-size-base);
line-height: 1.7;
margin: 0;
}
&__steps {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-md);
counter-reset: step;
// `> li`, not a bare descendant `li` — this used to match *any* `<li>`
// nested anywhere inside a step, not just the step's own direct one.
// Harmless while `StepDescription`'s editable mode only ever added
// plain text/buttons, but `TechStepCorrectionPopover` renders its own
// `<li>`s a few levels down (each attached ingredient/utensil/
// temperature row, `.tech-step-correction-popover__row`; the technique
// picker's `.catalog-search-picker__list`, up to ~74 entries) —
// every one of those was incrementing this *same* counter, throwing
// every step's own number off as soon as any correction popover with
// attached metadata (or its technique picker) was open.
> li {
counter-increment: step;
&::before {
content: counter(step) ". ";
font-weight: 700;
color: var(--color-primary);
}
// Same descendant-selector fix as `> li` above, for the same reason:
// both the step's own picture and its `StepDescription`-rendered `<p>`
// are direct children of this `<li>` — `TechStepCorrectionPopover`'s
// own `<p>`s (the selection line, hints, `.field-error`) sit deeper
// inside it and must not inherit "inline, no margin" meant only for
// the step's own paragraph.
> img {
display: block;
max-width: 100%;
border-radius: var(--radius-base);
margin-bottom: var(--space-xs);
}
> p {
display: inline;
margin: 0;
}
}
}
}
// A step's detected-technique keyword (see StepDescription.tsx) — a real
// <button> (natively focusable, so the Tooltip it wraps is
// keyboard-reachable, not just hoverable — see that component), reset here
// to read as inline highlighted text rather than a button: a subtle dotted
// underline + tinted background, same "primary-tinted" language as
// `.source-select__badge.is-official`, not a loud/distracting highlight
// since it sits inline within normal reading text.
.step-tech-step {
display: inline;
padding: 0 0.15em;
margin: 0;
font: inherit;
color: inherit;
background: color-mix(in srgb, var(--color-primary) 14%, transparent);
border: none;
border-radius: 0.2em;
text-decoration: underline dotted var(--color-primary);
text-decoration-thickness: 1px;
text-underline-offset: 0.15em;
// A real clickable trigger (opens `TechStepCorrectionPopover`), not just
// a hover-for-a-tooltip target — `pointer`, not `help`, same reasoning
// now shared by `.step-temperature` below.
cursor: pointer;
&:hover,
&:focus-visible {
background: color-mix(in srgb, var(--color-primary) 24%, transparent);
outline: none;
}
}
// A `"manual"`-sourced match (a viewer's correction, applied immediately —
// see `StepTechStepView.source`) — same shape as `.step-tech-step`, but in
// `--color-tag` (Turmeric) instead of `--color-primary` (Basil), so the two
// origins are distinguishable at a glance, not just via the tooltip text.
.step-tech-step--manual {
background: color-mix(in srgb, var(--color-tag) 18%, transparent);
text-decoration-color: var(--color-tag);
&:hover,
&:focus-visible {
background: color-mix(in srgb, var(--color-tag) 28%, transparent);
}
}
// A detected temperature mention ("180 °C", "th. 6", "feu doux") — same
// inline "highlighted reading text" language as `.step-tech-step`, but in
// `--color-accent` (Vermillion, the "heat" accent) rather than
// `--color-primary`, so a temperature reads as its own kind of metadata at
// a glance. `--low`/`--medium`/`--high` only vary the underline weight —
// the qualitative scale is a nuance, not three different colors.
//
// Renders as a `<button>` (not a `<span>`) in editable mode when it has an
// owning technique occurrence to reopen (see `StepDescription.tsx`'s
// `findTechStepForTemperature`) — same button-reset-to-inline-text
// treatment as `.step-tech-step` above (`font`/`color`/`margin`/`border`),
// so the two read identically whether they're a `<button>` or a plain
// `<span>` (read-only, or no owner to reopen).
.step-temperature {
display: inline;
padding: 0 0.15em;
margin: 0;
font: inherit;
color: inherit;
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
border: none;
border-radius: 0.2em;
text-decoration: underline dotted var(--color-accent);
text-decoration-thickness: 1px;
text-underline-offset: 0.15em;
cursor: pointer;
&:hover,
&:focus-visible {
background: color-mix(in srgb, var(--color-accent) 22%, transparent);
outline: none;
}
&--high {
text-decoration-thickness: 2px;
}
&--low {
text-decoration-style: dashed;
}
}
// `.step-tech-step-context` (the wider clause a `.step-tech-step` keyword
// was found in) used to be highlighted here too, more subtly — turned back
// off (see `StepDescription.tsx`'s doc comment): the backend still
// computes and persists `contextStart`/`contextEnd`, this file just no
// longer gives that class any styling to render with.
// Discoverability hint above the steps list (RecipeDetailPanel.tsx) —
// muted so it reads as a small aside, not competing with the steps
// themselves for attention.
.recipe-detail-panel__tech-step-hint {
margin: 0 0 var(--space-sm);
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
// --- Tech-step correction (StepDescription.tsx editable mode) ---------------
// Deliberately *not* `position: absolute` (unlike `.calendar-popover`) — see
// `TechStepCorrectionPopover.tsx`'s doc comment for why this renders inline
// in the document flow right below the step's own description instead of
// floating anchored at the selection.
.tech-step-correction-popover {
display: flex;
flex-direction: column;
gap: var(--space-md);
margin-top: var(--space-xs);
padding: var(--space-md);
background: var(--color-surface-alt);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
&__selection {
font-weight: 600;
margin: 0;
}
// One shared shape for every section (Technique/Ingrédients/Ustensiles/
// Température) — see `TechStepCorrectionPopover.tsx`'s own doc comment
// for why all four render together, always, rather than one at a time.
&__section {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
&__section-header {
display: flex;
align-items: center;
gap: var(--space-xs);
padding-bottom: var(--space-xs);
border-bottom: 1px solid var(--color-border);
svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
}
// Same "kicker" label language as `.cooking-phase__recipe-group-title`
// (cooking-session-page.scss) — a small bold uppercase eyebrow, not a
// full heading, since four of these stack in one popover.
&__section-title {
font-family: var(--font-display);
font-weight: 700;
font-size: var(--font-size-sm);
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--color-text-muted);
}
&__section-count {
font-weight: 400;
text-transform: none;
letter-spacing: 0;
}
// One icon color per category — set on the icon itself (not the whole
// section) so the title text next to it stays neutral/muted.
&__technique-section &__section-header svg {
color: var(--color-primary);
}
&__ingredients-section &__section-header svg,
&__utensils-section &__section-header svg {
color: var(--color-tag);
}
&__temperature-section &__section-header svg {
color: var(--color-accent);
}
&__chosen-technique {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-sm);
}
&__technique-pill {
display: inline-flex;
align-items: center;
background: var(--color-primary);
color: var(--color-surface);
padding: var(--space-xs) var(--space-md);
border-radius: var(--radius-pill);
font-family: var(--font-display);
font-weight: 700;
font-size: var(--font-size-sm);
}
&__technique-change {
background: none;
border: none;
color: var(--color-primary);
text-decoration: underline;
font-size: var(--font-size-sm);
font-weight: 600;
cursor: pointer;
padding: 0;
}
&__remove-match {
padding: 0.2rem 0.5rem;
font-size: var(--font-size-sm);
color: var(--color-error);
background: none;
border: 1px solid var(--color-error);
border-radius: var(--radius-pill);
cursor: pointer;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
&__cancel,
&__cancel-mini {
background: none;
border: none;
color: var(--color-text-muted);
text-decoration: underline;
cursor: pointer;
padding: 0;
font-size: var(--font-size-sm);
}
// Shown in place of a section's own "+ Ajouter…" button while
// `StepDescription` is waiting on a second text selection for *that*
// section (see `TechStepCorrectionPopover.tsx`'s own doc comment) — same
// styling intent as `.recipe-detail-panel__tech-step-hint`, a small
// muted aside.
&__hint {
margin: 0;
color: var(--color-text-muted);
font-style: italic;
font-size: var(--font-size-sm);
}
&__rows {
display: flex;
flex-direction: column;
gap: var(--space-xs);
list-style: none;
margin: 0;
padding: 0;
}
// One attached ingredient/utensil/temperature — a small card of its own
// (not a chip pill anymore: showing the source quote alongside the name
// needs more room than a pill comfortably gives).
&__row {
display: flex;
align-items: flex-start;
gap: var(--space-sm);
background: var(--color-surface);
border-radius: var(--radius-base);
box-shadow: var(--shadow-sm);
padding: var(--space-sm);
}
&__row-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
&__row-name {
font-weight: 600;
}
&__row-quote {
font-size: var(--font-size-xs);
color: var(--color-text-muted);
font-style: italic;
}
&__row-badge {
flex-shrink: 0;
align-self: center;
background: var(--color-surface-alt);
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-variant-numeric: tabular-nums;
padding: 2px var(--space-sm);
border-radius: var(--radius-pill);
}
&__row-remove {
flex-shrink: 0;
border: none;
background: none;
color: var(--color-text-muted);
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-base);
cursor: pointer;
svg {
width: 16px;
height: 16px;
}
&:hover:not(:disabled) {
color: var(--color-error);
background: var(--color-surface-alt);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// "+ Ajouter…" — a dashed ghost row, not a plain text link: at-a-glance
// an "add a new item" affordance, same weight as the rows it adds to.
&__add-row {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-xs);
border: 1.5px dashed var(--color-border);
border-radius: var(--radius-base);
padding: var(--space-sm);
color: var(--color-text-muted);
background: none;
font-size: var(--font-size-sm);
font-weight: 600;
cursor: pointer;
svg {
width: 16px;
height: 16px;
}
&:hover:not(:disabled) {
border-color: var(--color-accent);
color: var(--color-accent);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// A section's own inline "adding a new mention" sub-form — replaces just
// that section's `__add-row` button, every other section (and the
// technique picker) stays visible and untouched alongside it.
&__add-form {
display: flex;
flex-direction: column;
gap: var(--space-sm);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
padding: var(--space-sm);
}
&__add-form-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: var(--space-sm);
}
&__quantity-line,
&__value-line {
display: flex;
align-items: center;
gap: var(--space-xs);
input[type="number"] {
width: 5rem;
padding: var(--space-xs) var(--space-sm);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface);
color: var(--color-text);
font: inherit;
}
}
&__value-unit {
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
// Température's 3-way mode switch (°C / Thermostat / Qualitatif) — a
// segmented control, same connected-pill language as the qualitative
// sub-choice below, in the accent color (Vermillion, the "heat" accent —
// same as `.step-temperature`) so the active mode reads at a glance.
&__mode-switch {
display: flex;
gap: 2px;
background: var(--color-surface-alt);
border-radius: var(--radius-pill);
padding: 2px;
button {
flex: 1;
border: none;
background: none;
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-pill);
font-size: var(--font-size-sm);
font-weight: 700;
color: var(--color-text-muted);
cursor: pointer;
&.active {
background: var(--color-accent);
color: var(--color-surface);
}
}
}
&__qual-switch {
display: flex;
gap: var(--space-xs);
button {
flex: 1;
border: 1.5px solid var(--color-border);
background: var(--color-surface);
color: var(--color-text);
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-base);
font-size: var(--font-size-sm);
font-weight: 600;
cursor: pointer;
&.active {
border-color: var(--color-accent);
background: var(--color-accent);
color: var(--color-surface);
}
}
}
&__confirm-mini {
background: var(--color-accent);
color: var(--color-surface);
border: none;
border-radius: var(--radius-base);
font-size: var(--font-size-sm);
font-weight: 700;
padding: var(--space-xs) var(--space-md);
cursor: pointer;
&:hover {
background: var(--color-accent-hover);
}
}
&__actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: var(--space-md);
padding-top: var(--space-xs);
}
&__confirm-button {
padding: var(--space-sm) var(--space-lg);
font-family: var(--font-display);
font-weight: 700;
letter-spacing: 0.02em;
color: var(--color-surface);
background: var(--color-primary);
border: none;
border-radius: var(--radius-base);
cursor: pointer;
&:hover:not(:disabled) {
background: var(--color-primary-hover);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
// Reused by both the ingredient and utensil "attach to this correction"
// sub-flows (`TechStepCorrectionPopover.tsx`) — deliberately lighter than
// `.ingredient-picker` (no category/subcategory grid, no allergen/diet
// toggles), sized for a small popover rather than a full recipe form.
.catalog-search-picker {
display: flex;
flex-direction: column;
gap: var(--space-xs);
&__input {
width: 100%;
}
&__empty {
margin: 0;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
&__list {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
list-style: none;
margin: 0;
padding: 0;
// Raised from the original 8rem — this component is now also the
// technique picker (~74 entries, see this file's own doc comment),
// where 8rem left only a couple of rows visible before scrolling.
max-height: 14rem;
overflow-y: auto;
button {
padding: 0.3rem 0.6rem;
font-size: var(--font-size-sm);
color: var(--color-text);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-pill);
cursor: pointer;
&:hover {
background: color-mix(in srgb, var(--color-primary) 14%, transparent);
border-color: var(--color-primary);
}
// The technique picker's current pick (`selectedId` prop) — stays
// visually marked even while filtered/scrolled past, so re-opening
// this popover's picker doesn't read as "nothing chosen yet" when
// something already is. Unused by the ingredient/utensil sub-flows
// (they never pass `selectedId` — each pick there just appends a
// fresh mention, nothing to mark as "current").
&.catalog-search-picker__item--selected {
background: color-mix(in srgb, var(--color-primary) 20%, transparent);
border-color: var(--color-primary);
font-weight: 600;
}
}
}
}
// --- Favorite star toggle (detail panel header) -----------------------------
.favorite-star-button {
position: absolute;
top: var(--space-sm);
right: var(--space-sm);
width: 2.2rem;
height: 2.2rem;
border-radius: 50%;
border: none;
cursor: pointer;
background: color-mix(in srgb, var(--color-surface) 82%, transparent);
box-shadow: var(--shadow-sm);
color: var(--color-border);
display: flex;
align-items: center;
justify-content: center;
transition:
transform 0.12s ease,
color 0.12s ease;
svg {
width: 1.2rem;
height: 1.2rem;
}
&:hover:not(:disabled) {
transform: scale(1.08);
}
&:disabled {
cursor: not-allowed;
}
// Overrides the icon's own `fill="none"` (a presentation attribute, lower
// CSS priority than this rule) — solid star once favorited, outline
// otherwise.
&.is-favorite {
color: var(--color-tag);
svg {
fill: currentColor;
}
}
}
// --- External source link (draft preview header) ----------------------------
// Same overlay slot/sizing as `.favorite-star-button` above — a draft
// preview never has both (nothing to favorite yet), so the two never
// compete for the corner. Deliberately muted/small ("discret" per the
// product decision this button follows): a way out to the original page,
// not a call to action the way the buttons it replaced were.
.recipe-detail-panel__source-link {
position: absolute;
top: var(--space-sm);
right: var(--space-sm);
width: 2.2rem;
height: 2.2rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--color-surface) 82%, transparent);
box-shadow: var(--shadow-sm);
color: var(--color-text-muted);
transition:
transform 0.12s ease,
color 0.12s ease;
svg {
width: 1.1rem;
height: 1.1rem;
}
&:hover,
&:focus-visible {
color: var(--color-primary);
transform: scale(1.08);
outline: none;
}
}
// --- Recipe form (create/edit) ----------------------------------------------
// Per-field validation and whole-form error messages — same small rules as
// auth-form.scss/profile-forms.scss, redeclared here rather than shared
// since this page only imports recipes.scss (see global.scss's doc comment
// on colocated, per-scope styles).
.field-error {
color: var(--color-error);
font-size: var(--font-size-xs);
margin: 0;
}
.form-error {
color: var(--color-error);
font-size: var(--font-size-sm);
}
.recipe-form {
display: flex;
flex-direction: column;
gap: var(--space-xs);
label {
font-size: var(--font-size-sm);
font-weight: 600;
margin-top: var(--space-sm);
}
input,
textarea,
select {
padding: var(--space-sm);
font-family: var(--font-body);
font-size: var(--font-size-base);
color: var(--color-text);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
resize: vertical;
}
&__section {
margin-top: var(--space-xl);
h2 {
font-size: var(--font-size-lg);
margin-bottom: var(--space-sm);
}
}
&__ingredient-list {
list-style: none;
margin: 0 0 var(--space-sm);
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
&__actions {
margin-top: var(--space-xl);
}
button[type="submit"] {
padding: 0.6rem var(--space-lg);
font-family: var(--font-body);
font-size: var(--font-size-base);
font-weight: 600;
cursor: pointer;
border: none;
border-radius: var(--radius-base);
background: var(--color-primary);
color: #fff;
&:hover:not(:disabled) {
background: var(--color-primary-hover);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
}
// --- Unresolved-ingredient review (ImportRecipePage) ------------------------
// Amber-tinted callout, same "warning" language as .source-item-preview__hint
// — each line needs a person to pick the right ingredient (or drop it)
// before the form can submit at all (see ImportRecipePage.tsx's canSubmit).
.import-recipe__unresolved {
margin: var(--space-sm) 0;
padding: var(--space-sm) var(--space-md);
background: color-mix(in srgb, var(--color-warning) 8%, var(--color-surface));
border: 1px solid color-mix(in srgb, var(--color-warning) 30%, var(--color-border));
border-radius: var(--radius-md);
h3 {
margin: 0 0 var(--space-xs);
font-size: var(--font-size-base);
}
}
.import-recipe__unresolved-list {
list-style: none;
margin: var(--space-sm) 0 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.import-recipe__unresolved-row {
display: flex;
align-items: center;
gap: var(--space-sm);
flex-wrap: wrap;
span {
flex: 1 1 auto;
font-size: var(--font-size-sm);
}
button {
flex: none;
padding: 0.3rem var(--space-sm);
font-family: var(--font-body);
font-size: var(--font-size-xs);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
cursor: pointer;
&:hover {
border-color: var(--color-primary);
}
}
}
// --- Diet tag multi-select (recipe form) -------------------------------------
// Checkbox-grid, same visual language as AllergySelect (profile-forms.scss)
// — global.scss's `label:has(> input[type="checkbox"])` rule already
// supplies the selected/unselected look, this only arranges the rows.
.diet-tag-select {
border: none;
padding: 0;
margin: var(--space-xs) 0 0;
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
legend {
width: 100%;
padding: 0 0 var(--space-xs);
font-size: var(--font-size-sm);
font-weight: 600;
}
label {
margin-top: 0;
font-weight: 400;
}
}
// --- Ingredient row (selected ingredient in the form) -----------------------
.ingredient-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm);
background: var(--color-surface);
border-radius: var(--radius-base);
&__icon {
display: flex;
flex-shrink: 0;
color: var(--color-text-muted);
svg {
width: 1.25rem;
height: 1.25rem;
}
}
&__name {
font-weight: 600;
margin-right: auto;
}
// Marks a free-text placeholder line (nothing in the catalog matched) —
// the ingredient still needs a real catalog entry created by a
// maintainer, but the line is usable (quantity/unit) in the meantime.
&__placeholder-badge {
margin-left: var(--space-xs);
padding: 0 var(--space-xs);
font-size: var(--font-size-xs);
font-weight: 500;
border-radius: var(--radius-base);
background: var(--color-surface-alt);
color: var(--color-text-muted);
}
&__quantity {
width: 5rem;
}
// Wide enough for the longest catalog label ("cuillère à soupe") not to
// clip — was sized for the old free-text input's short values ("g",
// "unité"…) before unit became a <select> over the reference catalog
// (see IngredientRow.tsx), left too narrow for that one.
&__unit {
width: 11rem;
}
&__remove {
padding: var(--space-xs) var(--space-sm);
font-size: var(--font-size-sm);
line-height: 1;
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface);
color: var(--color-text-muted);
&:hover {
color: var(--color-error);
border-color: var(--color-error);
}
}
// A resolved ingredient with no unit picked — the form's submit stays
// disabled until this is fixed, so it needs to be obvious *which* row is
// the reason why (see issue #53). `__error` takes the row's full width
// (it comes after the `flex-wrap`ped controls above, so it naturally
// drops to its own line).
&--incomplete &__unit {
border-color: var(--color-error);
}
&__error {
width: 100%;
margin: 0;
}
}
// --- Ingredient picker (recipe form + disliked-ingredients field) ----------
// Always-visible category chips + search + card grid — replaces the earlier
// type-to-filter dropdown (`.ingredient-autocomplete`, now gone), which
// stopped scaling once the reference list passed a couple hundred items.
// The category row uses the same "scroll, don't crush" pattern as the app
// sidebar nav (see AppLayout.scss's mobile breakpoint): chips never shrink
// below a tappable size, the row scrolls horizontally instead.
.ingredient-picker {
display: flex;
flex-direction: column;
gap: var(--space-sm);
// Every clickable chip/card below is a plain `<button>` with no text to
// actually select — without this, a click that moves even a pixel (a
// fast click, a trackpad) is read as a text-selection drag instead, and
// the browser's native selection highlight paints over the intended
// `.active` state. Reads as "the selection styling is flaky" from the
// outside; this is the actual cause, not the `.active` rules themselves.
user-select: none;
padding: var(--space-sm);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
&__search {
display: flex;
align-items: center;
gap: var(--space-xs);
input {
flex: 1 1 auto;
// Own min-width: 0 (not just relying on the flex row's) — same
// shrink-to-fit gotcha as `.disliked-ingredients-field`, belt and
// suspenders since a text input's natural min width is its content.
min-width: 0;
padding: var(--space-sm);
font-family: var(--font-body);
font-size: var(--font-size-base);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface-alt);
color: var(--color-text);
// Undo the container's `user-select: none` — that rule exists for
// the chip/card buttons, not this text input; a search box the user
// can't select/copy text from would be its own bug.
user-select: text;
}
}
// Settings menu for the allergen/diet badge rows on every card below —
// trailing the search input. A single gear button + labeled checkboxes
// (not two bare icon-only toggles, the earlier design here) — an icon
// alone couldn't say clearly enough which badge row it hid.
&__display-options {
position: relative;
flex-shrink: 0;
}
&__display-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
padding: 0;
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface-alt);
color: var(--color-text-muted);
svg {
width: 1.5rem;
height: 1.5rem;
}
&:hover {
border-color: var(--color-primary);
color: var(--color-text);
}
&.active {
border-color: var(--color-primary);
color: var(--color-primary);
}
}
// Anchored below-right of the toggle — same transient-overlay reasoning
// as `.app-sidebar__account-menu` (AppLayout.scss).
&__display-menu {
position: absolute;
z-index: 10;
top: calc(100% + var(--space-xs));
right: 0;
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-sm);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
box-shadow: var(--shadow-md);
// Box/checkmark look comes from the app-wide "selectable card" rule in
// global.scss (`label:has(> input[type="checkbox"])`) — this only adds
// what's specific to sitting inside a dropdown menu.
label {
font-size: var(--font-size-sm);
white-space: nowrap;
// The global rule trails the checkmark at the far right (`margin-left:
// auto`) — reads well as a selected chip in a grid (AllergySelect),
// but a vertical settings-menu list reads conventionally the other
// way: checkbox leading, label text after it. `.check-mark` already
// sits right after the (visually hidden) `<input>` and before the
// text in markup, so undoing the auto margin alone puts it there.
.check-mark {
margin-left: 0;
}
}
}
&__categories {
display: flex;
gap: var(--space-xs);
overflow-x: auto;
padding-bottom: var(--space-xs);
}
&__category {
display: flex;
align-items: center;
gap: var(--space-xs);
flex-shrink: 0;
appearance: none;
padding: var(--space-xs) var(--space-sm);
font-family: var(--font-body);
font-size: var(--font-size-sm);
white-space: nowrap;
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: var(--radius-pill);
background: var(--color-surface-alt);
color: var(--color-text-muted);
svg {
flex-shrink: 0;
width: 1rem;
height: 1rem;
}
&:hover {
border-color: var(--color-primary);
color: var(--color-text);
}
&.active {
border-color: var(--color-primary);
background: var(--color-primary);
color: #fff;
}
}
// Second-tier drill-down, shown once a main category is picked — same
// "scroll, don't crush" row as `&__categories` above, but visually
// subordinate: smaller, and using `--color-tag` (not `--color-primary`)
// for its active state so the two levels stay legible at a glance.
&__subcategories {
display: flex;
gap: var(--space-xs);
overflow-x: auto;
padding-bottom: var(--space-xs);
margin-top: calc(var(--space-xs) * -1);
}
&__subcategory {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
appearance: none;
padding: 2px var(--space-sm);
font-family: var(--font-body);
font-size: var(--font-size-xs);
white-space: nowrap;
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: var(--radius-pill);
background: transparent;
color: var(--color-text-muted);
svg {
flex-shrink: 0;
width: 0.85rem;
height: 0.85rem;
}
&:hover {
border-color: var(--color-tag);
color: var(--color-text);
}
&.active {
border-color: var(--color-tag);
background: var(--color-tag);
color: var(--color-tag-ink);
}
}
&__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
gap: var(--space-xs);
max-height: 18rem;
overflow-y: auto;
padding: 2px;
}
&__card {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-xs);
padding: var(--space-sm);
text-align: center;
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface-alt);
color: var(--color-text);
&:hover {
border-color: var(--color-primary);
background: var(--color-surface);
}
}
&__card-icon {
display: flex;
align-items: center;
justify-content: center;
svg {
width: 1.75rem;
height: 1.75rem;
}
}
&__card-name {
font-size: var(--font-size-sm);
}
// Now a container (not a bare `<p>`) — the "aucun ingrédient trouvé"
// message plus, when the picker allows it, an "ajouter « … » comme
// ingrédient à compléter" escape-hatch button stacked under it.
&__empty {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-sm);
padding: var(--space-md);
font-size: var(--font-size-sm);
color: var(--color-text-muted);
text-align: center;
p {
margin: 0;
}
}
&__add-placeholder {
padding: var(--space-xs) var(--space-md);
font-size: var(--font-size-sm);
cursor: pointer;
border: 1px dashed var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface);
color: var(--color-text);
&:hover {
border-color: var(--color-accent);
color: var(--color-accent);
}
}
}
// --- Step list editor -----------------------------------------------------
.step-list-editor {
&__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-sm);
counter-reset: step;
}
&__item {
display: flex;
align-items: flex-start;
gap: var(--space-sm);
padding: var(--space-sm);
background: var(--color-surface);
border-radius: var(--radius-base);
counter-increment: step;
&::before {
content: counter(step);
flex: none;
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
margin-top: var(--space-xs);
font-size: var(--font-size-xs);
font-weight: 600;
color: #fff;
background: var(--color-primary);
border-radius: 50%;
}
}
&__reorder {
display: flex;
flex-direction: column;
gap: 0.15rem;
button {
padding: 0.1rem 0.4rem;
line-height: 1;
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface);
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
}
&__fields {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-xs);
textarea,
input {
padding: var(--space-sm);
font-family: var(--font-body);
font-size: var(--font-size-sm);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-background);
color: var(--color-text);
resize: vertical;
}
}
&__remove {
padding: var(--space-xs) var(--space-sm);
font-size: var(--font-size-sm);
line-height: 1;
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface);
color: var(--color-text-muted);
&:hover {
color: var(--color-error);
border-color: var(--color-error);
}
}
&__add {
margin-top: var(--space-sm);
padding: 0.5rem var(--space-md);
font-family: var(--font-body);
font-size: var(--font-size-sm);
font-weight: 600;
cursor: pointer;
border: 1px dashed var(--color-border);
border-radius: var(--radius-base);
background: none;
color: var(--color-primary);
&:hover {
border-color: var(--color-primary);
}
}
}
// --- Disliked-ingredients field (preferences page) ---------------------------
// A search-and-add + removable-chips field, same idea as the recipe form's
// ingredient picker but without quantity/unit — see
// `features/profile/DislikedIngredientsField.tsx`.
.disliked-ingredients-field {
// `<fieldset>` carries a browser-default `min-width: min-content` that
// ordinary block elements don't — it refuses to shrink for its content,
// so `.ingredient-picker__grid` below (a `repeat(auto-fill, …)` grid)
// never gets narrow enough to wrap and blows out the whole page's width
// instead. Reset it back to the normal shrinkable behavior.
min-width: 0;
border: none;
padding: 0;
margin: var(--space-md) 0 0;
legend {
padding: 0 0 var(--space-xs);
font-size: var(--font-size-sm);
font-weight: 600;
}
&__chips {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
margin: 0 0 var(--space-sm);
padding: 0;
list-style: none;
}
&__chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.2rem 0.3rem 0.2rem 0.6rem;
font-size: var(--font-size-sm);
background: var(--color-surface-alt);
border-radius: var(--radius-pill);
svg {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
button {
display: flex;
align-items: center;
justify-content: center;
width: 1.1rem;
height: 1.1rem;
padding: 0;
font-size: var(--font-size-xs);
line-height: 1;
color: var(--color-text-muted);
background: none;
border: none;
border-radius: 50%;
cursor: pointer;
&:hover {
background: var(--color-border);
color: var(--color-text);
}
}
}
}