// ============================================================================= // 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); } .source-items-load-more { flex-shrink: 0; align-self: center; padding: 0.4rem var(--space-lg); font-family: var(--font-body); font-size: var(--font-size-sm); color: var(--color-primary); background: none; border: 1px solid var(--color-border); border-radius: var(--radius-pill); cursor: pointer; &:hover { border-color: var(--color-primary); } } .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 { counter-increment: step; &::before { content: counter(step) ". "; font-weight: 700; color: var(--color-primary); } 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 //