.recipe-detail-panel__steps utilisait un compteur CSS (counter-reset/ counter-increment: step) sur un sélecteur `li` — un descendant, pas seulement les enfants directs. Depuis la refonte du popover de correction (tech-step-metadata-editor-redesign), celui-ci imbrique ses propres <li> plusieurs niveaux plus bas (une par ingrédient/ustensile/température attaché, plus la liste du picker de technique — jusqu'à ~74 entrées) : chacun incrémentait ce même compteur "step", faussant le numéro affiché de toutes les étapes suivantes dès qu'une correction avec métadonnées attachées (ou son picker de technique) était ouverte. C'est le bug rapporté : "les étapes affichées sont numérotées n'importe comment". Correctif : `li`/`img`/`p` → `> li`/`> img`/`> p` (enfants directs uniquement) dans cette règle. Même principe pour `img`/`p` : leurs styles (image bloc, paragraphe inline sans marge) ne doivent s'appliquer qu'au propre <p>/<img> de l'étape, pas à ceux du popover imbriqué plus loin. Test de non-régression ajouté (recipes.cy.ts) : ouvre une correction avec ingrédient + ustensile attachés puis le picker de technique, vérifie via getComputedStyle que ces <li> imbriqués n'ont pas rejoint la chaîne du compteur (`counter-increment: none`) alors que les <li> des étapes elles-mêmes l'ont toujours (`counter-increment: step 1`) — confirmé que ce test échoue bien sans le correctif (`step 1` au lieu de `none`) avant de le restaurer. Vérifié : pnpm lint + build clean. recipes.cy.ts (9/9, dont le nouveau test), recipes.feature (5/5), recipe-sources.feature (6/6). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2017 lines
No EOL
53 KiB
SCSS
2017 lines
No EOL
53 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).
|
|
// =============================================================================
|
|
|
|
// --- 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);
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
&__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;
|
|
background: var(--color-surface);
|
|
border-radius: var(--radius-md);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.recipe-table {
|
|
width: 100%;
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
} |