Files
mealprep/specs/staples-settings-tile.html
Marcel Raddatz 2ad75cc1b7 spec(staples): tile redesign, seed catalog & add-ingredient flow
Adds two spec files for issue #59:
- staples-settings-redesign.html — 5 initial concept variations
- staples-settings-tile.html    — chosen direction: tile layout (matching
  SettingsCard shell), German seed catalog (~100 ingredients / 8 categories),
  and inline per-tile add-ingredient flow with duplicate detection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 20:28:57 +02:00

1457 lines
75 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>A3/D3 Vorräte Settings — Tile Redesign</title>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"/>
<!--
spec:agent
document: A3/D3 Vorräte Settings — Tile Redesign
version: 1.0
route: /household/staples (context=settings)
design-ref: SettingsCard.svelte, MemberCard.svelte, /settings +page.svelte
goal: Unify staples page with the app-wide tile language.
Each category becomes one tile (same shell as SettingsCard).
last-updated: 2026-04-10
-->
<style>
:root {
--color-page: #FAFAF7;
--color-surface: #F5F4EE;
--color-subtle: #EDECEA;
--color-border: #D8D7D0;
--color-border-hover: #C0BFB8;
--color-text: #1C1C18;
--color-text-muted: #6B6A63;
--green-tint: #E8F5EA;
--green-light: #AEDCB0;
--green-dark: #2E6E39;
--green-deeper: #1E4A26;
--color-error: #DC4C3E;
--font-display: 'Fraunces', Georgia, serif;
--font-sans: 'DM Sans', system-ui, sans-serif;
--font-mono: 'DM Mono', monospace;
--radius-sm: 4px; --radius-md: 6px; --radius-lg: 10px; --radius-xl: 16px; --radius-full: 9999px;
--shadow-card: 0 1px 3px rgba(28,28,24,.06), 0 1px 2px rgba(28,28,24,.04);
--shadow-raised: 0 4px 12px rgba(28,28,24,.08), 0 2px 4px rgba(28,28,24,.04);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-sans); background: #e0dfd8; color: var(--color-text); font-size: 14px; line-height: 1.6; }
/* ── Doc layout ── */
.doc { max-width: 1040px; margin: 0 auto; padding: 48px 40px 96px; }
.doc-header { display: flex; justify-content: space-between; align-items: flex-end; padding-bottom: 28px; border-bottom: 1px solid #c8c7c0; margin-bottom: 48px; }
.doc-header h1 { font-family: var(--font-display); font-size: 26px; font-weight: 500; letter-spacing: -0.02em; margin-bottom: 4px; color: var(--color-text); }
.doc-header p { font-size: 13px; color: var(--color-text-muted); }
.doc-meta { font-family: var(--font-mono); font-size: 11px; color: var(--color-text-muted); text-align: right; line-height: 1.9; }
/* ── Section labels ── */
.section-label { font-size: 10px; font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-text-muted); padding-bottom: 10px; border-bottom: 1px solid #c8c7c0; margin-bottom: 32px; }
/* ── Analysis box ── */
.analysis { background: var(--color-page); border: 1px solid #c8c7c0; border-radius: var(--radius-lg); padding: 24px 28px; margin-bottom: 48px; }
.analysis h3 { font-family: var(--font-display); font-size: 15px; font-weight: 500; margin-bottom: 16px; }
.analysis-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.analysis-col h4 { font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 10px; }
.analysis-col ul { list-style: none; display: flex; flex-direction: column; gap: 6px; }
.analysis-col li { font-size: 13px; color: var(--color-text); padding-left: 16px; position: relative; line-height: 1.4; }
.analysis-col li::before { content: '→'; position: absolute; left: 0; color: var(--color-text-muted); }
.analysis-col.good li::before { color: var(--green-dark); }
.analysis-col.bad li::before { color: var(--color-error); }
/* ── Tile anatomy diagram ── */
.anatomy { background: var(--color-page); border: 1px solid #c8c7c0; border-radius: var(--radius-lg); padding: 32px; margin-bottom: 48px; }
.anatomy-title { font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 24px; }
.anatomy-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; align-items: start; }
.anatomy-tile {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
padding: 28px;
box-shadow: var(--shadow-card);
position: relative;
}
.anno { position: absolute; font-size: 10px; font-family: var(--font-mono); color: var(--color-text-muted); white-space: nowrap; }
.anno-line { position: absolute; border-left: 1px dashed var(--color-border); }
.anatomy-specs { display: flex; flex-direction: column; gap: 10px; justify-content: center; }
.spec-row { display: flex; align-items: baseline; gap: 8px; }
.spec-token { font-family: var(--font-mono); font-size: 11px; color: var(--green-dark); background: var(--green-tint); padding: 2px 6px; border-radius: var(--radius-sm); white-space: nowrap; }
.spec-desc { font-size: 12px; color: var(--color-text-muted); }
/* ── Preview frames ── */
.preview-pair { display: flex; gap: 28px; align-items: flex-start; margin-bottom: 24px; }
.preview-d-wrap, .preview-m-wrap { display: flex; flex-direction: column; gap: 6px; }
.preview-d-wrap { flex: 1; min-width: 0; }
.preview-m-wrap { flex-shrink: 0; }
.preview-label { font-size: 9px; font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase; color: var(--color-text-muted); }
.preview-d-clip { height: 420px; overflow: hidden; border: 1px solid #c8c7c0; border-radius: var(--radius-lg); background: var(--color-page); }
.preview-d-scale { transform: scale(0.52); transform-origin: top left; width: 192%; }
.preview-m-clip { width: 200px; height: 420px; overflow: hidden; border: 1.5px solid #c8c7c0; border-radius: 22px; background: var(--color-page); }
.preview-m-scale { transform: scale(0.52); transform-origin: top left; width: 192%; }
/* ── Notes ── */
.notes { background: var(--color-page); border: 1px solid #c8c7c0; border-radius: var(--radius-lg); padding: 16px 20px; margin-top: 20px; }
.notes-label { font-size: 9px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 10px; }
.notes ul { list-style: none; display: flex; flex-direction: column; gap: 5px; }
.notes li { font-size: 12px; color: var(--color-text); padding-left: 14px; position: relative; line-height: 1.5; }
.notes li::before { content: '·'; position: absolute; left: 4px; color: var(--color-text-muted); }
/* ── State specimens ── */
.states { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 40px; }
.state-specimen { background: var(--color-page); border: 1px solid #c8c7c0; border-radius: var(--radius-lg); padding: 20px 24px; flex: 1; min-width: 200px; }
.state-label { font-size: 9px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 12px; }
/* ── Primitive resets ── */
button { cursor: pointer; }
/* ── App primitives (shared with real app) ── */
.chip {
display: inline-flex; align-items: center;
font-family: var(--font-sans); font-size: 13px; font-weight: 500; letter-spacing: 0.04em;
padding: 5px 12px; border-radius: var(--radius-full); border: 1px solid; white-space: nowrap;
}
.chip-on { background: var(--green-tint); border-color: var(--green-light); color: var(--green-dark); }
.chip-off { background: var(--color-surface); border-color: var(--color-border); color: var(--color-text-muted); }
.badge {
display: inline-flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 500; border-radius: var(--radius-full); padding: 1px 7px;
background: var(--green-tint); color: var(--green-dark);
}
.badge-zero { background: var(--color-subtle); color: var(--color-text-muted); }
.cat-action-btn {
font-family: var(--font-sans); font-size: 10px; font-weight: 500; letter-spacing: 0.04em;
padding: 2px 8px; border-radius: var(--radius-full); border: 1px solid var(--color-border);
background: none; color: var(--color-text-muted);
}
.overflow-trigger {
font-family: var(--font-sans); font-size: 12px; color: var(--color-text-muted);
background: none; border: none; padding: 3px 2px; text-decoration: underline; text-underline-offset: 2px;
}
/* ── The category tile ── */
.cat-tile {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
padding: 28px;
box-shadow: var(--shadow-card);
display: flex; flex-direction: column; gap: 0;
}
.cat-tile:hover { box-shadow: var(--shadow-raised); border-color: var(--color-border-hover); }
.cat-tile-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 16px;
}
.cat-tile-header-left { display: flex; align-items: center; gap: 8px; }
.cat-tile-header-right { display: flex; align-items: center; gap: 6px; }
.cat-title { font-family: var(--font-sans); font-size: 16px; font-weight: 500; color: var(--color-text); }
.cat-chips { display: flex; flex-wrap: wrap; gap: 6px; }
</style>
</head>
<body>
<div class="doc">
<!-- ── Header ── -->
<div class="doc-header">
<div>
<h1>A3 / D3 — Vorräte Settings</h1>
<p>Tile redesign · unified with Settings + Members page language</p>
</div>
<div class="doc-meta">
route /household/staples<br>
context settings<br>
Atlas · 2026-04-10
</div>
</div>
<!-- ── Analysis: current vs tile ── -->
<div class="section-label">Analysis</div>
<div class="analysis">
<h3>Why the current page looks out of place</h3>
<div class="analysis-grid">
<div class="analysis-col bad">
<h4>Current — plain divs</h4>
<ul>
<li>No surface background, no border, no shadow — floats naked on the page</li>
<li>Feels like a different app than /settings and /members</li>
<li>3-col grid has no visual container to balance uneven category sizes</li>
<li>No count feedback on category level</li>
<li>Chip overflow creates page-length sprawl for large categories</li>
</ul>
</div>
<div class="analysis-col good">
<h4>Tile approach — matches SettingsCard shell</h4>
<ul>
<li>Each category in a tile: radius-xl, shadow-card, surface bg — identical to SettingsCard</li>
<li>Tile acts as a visual container that naturally caps chip overflow</li>
<li>Count badge + Alle/Keine in tile header replaces raw category label</li>
<li>Grid: grid-cols-1 sm:grid-cols-2 gap-4 max-w-[820px] — identical to /settings</li>
<li>Zero new patterns — reuses the language the user already trusts</li>
</ul>
</div>
</div>
</div>
<!-- ── Tile anatomy ── -->
<div class="section-label">Tile Anatomy</div>
<div class="anatomy">
<div class="anatomy-title">CategoryTile — token mapping</div>
<div class="anatomy-grid">
<!-- Live tile specimen -->
<div style="position:relative; padding: 16px 0 16px 0;">
<div class="cat-tile" style="max-width:360px;">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Gemüse</span>
<span class="badge">7 / 18</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-on">Möhren</span>
<span class="chip chip-on">Zwiebeln</span>
<span class="chip chip-on">Knoblauch</span>
<span class="chip chip-on">Paprika</span>
<span class="chip chip-off">Zucchini</span>
<span class="chip chip-off">Aubergine</span>
<span class="chip chip-on">Tomaten</span>
<span class="chip chip-off">Spinat</span>
<button class="overflow-trigger">+10 weitere …</button>
</div>
</div>
</div>
<!-- Token spec table -->
<div class="anatomy-specs">
<div class="spec-row"><span class="spec-token">--radius-xl</span><span class="spec-desc">border-radius (same as SettingsCard)</span></div>
<div class="spec-row"><span class="spec-token">--color-surface</span><span class="spec-desc">tile background</span></div>
<div class="spec-row"><span class="spec-token">--color-border</span><span class="spec-desc">border (rest state)</span></div>
<div class="spec-row"><span class="spec-token">--color-border-hover</span><span class="spec-desc">border on hover</span></div>
<div class="spec-row"><span class="spec-token">--shadow-card</span><span class="spec-desc">elevation (rest)</span></div>
<div class="spec-row"><span class="spec-token">--shadow-raised</span><span class="spec-desc">elevation (hover)</span></div>
<div class="spec-row"><span class="spec-token">28px</span><span class="spec-desc">padding (all sides, same as SettingsCard)</span></div>
<div class="spec-row"><span class="spec-token">16px / 500</span><span class="spec-desc">category title (same as SettingsCard)</span></div>
<div class="spec-row"><span class="spec-token">--green-tint / --green-dark</span><span class="spec-desc">badge when selected &gt; 0</span></div>
<div class="spec-row"><span class="spec-token">--color-subtle / muted</span><span class="spec-desc">badge when selected === 0</span></div>
<div style="margin-top:4px; padding-top:12px; border-top:1px solid var(--color-border);">
<p style="font-size:11px; color:var(--color-text-muted); line-height:1.5;">
Tile does <strong>not</strong> have a link/href — it is an interactive container, not a navigation element.
Hover state is retained for visual feedback (user can see the tile is a unit), but no cursor:pointer on the tile itself.
</p>
</div>
</div>
</div>
</div>
<!-- ── State specimens ── -->
<div class="section-label">Tile States</div>
<div class="states">
<!-- Empty state -->
<div class="state-specimen">
<div class="state-label">Empty (0 selected)</div>
<div class="cat-tile" style="max-width:280px;">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Gewürze</span>
<span class="badge badge-zero">0 / 24</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-off">Salz</span>
<span class="chip chip-off">Pfeffer</span>
<span class="chip chip-off">Kurkuma</span>
<span class="chip chip-off">Zimt</span>
<span class="chip chip-off">Thymian</span>
<span class="chip chip-off">Oregano</span>
<span class="chip chip-off">Paprika</span>
<span class="chip chip-off">Koriander</span>
<button class="overflow-trigger">+16 weitere …</button>
</div>
</div>
</div>
<!-- Partially filled -->
<div class="state-specimen">
<div class="state-label">Partial</div>
<div class="cat-tile" style="max-width:280px;">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Öle &amp; Fette</span>
<span class="badge">2 / 6</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-on">Olivenöl</span>
<span class="chip chip-on">Rapsöl</span>
<span class="chip chip-off">Kokosöl</span>
<span class="chip chip-off">Butter</span>
<span class="chip chip-off">Ghee</span>
<span class="chip chip-off">Sesamöl</span>
</div>
</div>
</div>
<!-- Fully selected -->
<div class="state-specimen">
<div class="state-label">All selected</div>
<div class="cat-tile" style="max-width:280px;">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Milchprodukte</span>
<span class="badge">4 / 4</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-on">Butter</span>
<span class="chip chip-on">Milch</span>
<span class="chip chip-on">Joghurt</span>
<span class="chip chip-on">Parmesan</span>
</div>
</div>
</div>
</div>
<!-- ── Page previews ── -->
<div class="section-label">Page Previews</div>
<div class="preview-pair">
<!-- Desktop preview -->
<div class="preview-d-wrap">
<div class="preview-label">Desktop — /settings page header + 2-col tile grid</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div style="background:var(--color-page); min-height:800px; padding: 40px 56px;">
<!-- ← back link -->
<a href="#" style="font-family:var(--font-sans); font-size:12px; color:var(--color-text-muted); text-decoration:none; display:inline-block; margin-bottom:12px;">← Einstellungen</a>
<!-- h1 — identical to /settings and /members -->
<div style="display:flex; align-items:baseline; justify-content:space-between; margin-bottom:4px;">
<h1 style="font-family:var(--font-display); font-size:28px; font-weight:500; letter-spacing:-0.02em; color:var(--color-text);">Vorräte</h1>
<span style="font-family:var(--font-sans); font-size:13px; color:var(--color-text-muted);">23 gewählt</span>
</div>
<p style="font-family:var(--font-sans); font-size:11px; color:var(--color-text-muted); margin-bottom:32px;">Automatisch gespeichert. Gilt ab der nächsten Einkaufsliste.</p>
<!-- 2-col tile grid (max-w-[820px]) -->
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px; max-width:820px;">
<!-- Tile 1: Gemüse -->
<div class="cat-tile">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Gemüse</span>
<span class="badge">7 / 18</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-on">Möhren</span>
<span class="chip chip-on">Zwiebeln</span>
<span class="chip chip-on">Knoblauch</span>
<span class="chip chip-on">Paprika</span>
<span class="chip chip-on">Tomaten</span>
<span class="chip chip-off">Zucchini</span>
<span class="chip chip-off">Aubergine</span>
<span class="chip chip-on">Brokkoli</span>
<button class="overflow-trigger">+10 weitere …</button>
</div>
</div>
<!-- Tile 2: Gewürze -->
<div class="cat-tile">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Gewürze</span>
<span class="badge badge-zero">0 / 24</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-off">Salz</span>
<span class="chip chip-off">Pfeffer</span>
<span class="chip chip-off">Kurkuma</span>
<span class="chip chip-off">Zimt</span>
<span class="chip chip-off">Thymian</span>
<span class="chip chip-off">Oregano</span>
<span class="chip chip-off">Paprika</span>
<span class="chip chip-off">Koriander</span>
<button class="overflow-trigger">+16 weitere …</button>
</div>
</div>
<!-- Tile 3: Öle & Fette -->
<div class="cat-tile">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Öle &amp; Fette</span>
<span class="badge">2 / 6</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-on">Olivenöl</span>
<span class="chip chip-on">Rapsöl</span>
<span class="chip chip-off">Kokosöl</span>
<span class="chip chip-off">Butter</span>
<span class="chip chip-off">Ghee</span>
<span class="chip chip-off">Sesamöl</span>
</div>
</div>
<!-- Tile 4: Milchprodukte -->
<div class="cat-tile">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Milchprodukte</span>
<span class="badge">4 / 4</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-on">Butter</span>
<span class="chip chip-on">Milch</span>
<span class="chip chip-on">Joghurt</span>
<span class="chip chip-on">Parmesan</span>
</div>
</div>
<!-- Tile 5: Getreide -->
<div class="cat-tile">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Getreide &amp; Körner</span>
<span class="badge badge-zero">0 / 9</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-off">Reis</span>
<span class="chip chip-off">Nudeln</span>
<span class="chip chip-off">Quinoa</span>
<span class="chip chip-off">Haferflocken</span>
<span class="chip chip-off">Linsen</span>
<span class="chip chip-off">Kichererbsen</span>
<button class="overflow-trigger">+3 weitere …</button>
</div>
</div>
<!-- Tile 6: Fleisch -->
<div class="cat-tile">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Fleisch &amp; Fisch</span>
<span class="badge">3 / 11</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-on">Hühnerbrust</span>
<span class="chip chip-on">Hackfleisch</span>
<span class="chip chip-off">Lachs</span>
<span class="chip chip-off">Thunfisch</span>
<span class="chip chip-on">Eier</span>
<span class="chip chip-off">Speck</span>
<button class="overflow-trigger">+5 weitere …</button>
</div>
</div>
</div><!-- /grid -->
</div>
</div>
</div>
</div>
<!-- Mobile preview -->
<div class="preview-m-wrap">
<div class="preview-label">Mobile — single col</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div style="background:var(--color-page); min-height:800px; padding:16px 20px;">
<a href="#" style="font-family:var(--font-sans); font-size:12px; color:var(--color-text-muted); text-decoration:none; display:inline-block; margin-bottom:12px;">← Einstellungen</a>
<div style="display:flex; align-items:baseline; justify-content:space-between; margin-bottom:4px;">
<h1 style="font-family:var(--font-display); font-size:18px; font-weight:500; color:var(--color-text);">Vorräte</h1>
<span style="font-family:var(--font-sans); font-size:11px; color:var(--color-text-muted);">23 gewählt</span>
</div>
<p style="font-family:var(--font-sans); font-size:11px; color:var(--color-text-muted); margin-bottom:20px;">Automatisch gespeichert.</p>
<!-- Single col tiles -->
<div style="display:flex; flex-direction:column; gap:12px;">
<div class="cat-tile" style="padding:20px;">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span style="font-family:var(--font-sans); font-size:14px; font-weight:500; color:var(--color-text);">Gemüse</span>
<span class="badge">7/18</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn" style="font-size:9px; padding:1px 6px;">Alle</button>
<button class="cat-action-btn" style="font-size:9px; padding:1px 6px;">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-on" style="font-size:12px; padding:4px 10px;">Möhren</span>
<span class="chip chip-on" style="font-size:12px; padding:4px 10px;">Zwiebeln</span>
<span class="chip chip-on" style="font-size:12px; padding:4px 10px;">Knoblauch</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Zucchini</span>
<span class="chip chip-on" style="font-size:12px; padding:4px 10px;">Paprika</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Aubergine</span>
<span class="chip chip-on" style="font-size:12px; padding:4px 10px;">Tomaten</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Spinat</span>
<button class="overflow-trigger" style="font-size:11px;">+10 weitere …</button>
</div>
</div>
<div class="cat-tile" style="padding:20px;">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span style="font-family:var(--font-sans); font-size:14px; font-weight:500; color:var(--color-text);">Gewürze</span>
<span class="badge badge-zero">0/24</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn" style="font-size:9px; padding:1px 6px;">Alle</button>
<button class="cat-action-btn" style="font-size:9px; padding:1px 6px;">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Salz</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Pfeffer</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Kurkuma</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Thymian</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Oregano</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Zimt</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Paprika</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Koriander</span>
<button class="overflow-trigger" style="font-size:11px;">+16 weitere …</button>
</div>
</div>
<div class="cat-tile" style="padding:20px;">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span style="font-family:var(--font-sans); font-size:14px; font-weight:500; color:var(--color-text);">Öle &amp; Fette</span>
<span class="badge">2/6</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn" style="font-size:9px; padding:1px 6px;">Alle</button>
<button class="cat-action-btn" style="font-size:9px; padding:1px 6px;">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-on" style="font-size:12px; padding:4px 10px;">Olivenöl</span>
<span class="chip chip-on" style="font-size:12px; padding:4px 10px;">Rapsöl</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Kokosöl</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Butter</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Sesamöl</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- /preview-pair -->
<!-- ── Comparison: settings page context ── -->
<div class="section-label" style="margin-top:48px;">Context — how /settings and /household/staples sit side by side</div>
<div style="background:var(--color-page); border:1px solid #c8c7c0; border-radius:var(--radius-lg); overflow:hidden; margin-bottom:40px;">
<div style="padding:8px 16px; background:var(--color-subtle); border-bottom:1px solid #c8c7c0;">
<span style="font-family:var(--font-mono); font-size:11px; color:var(--color-text-muted);">/settings — same tile shell, consistent visual weight</span>
</div>
<div style="padding:32px 40px;">
<h1 style="font-family:var(--font-display); font-size:28px; font-weight:500; letter-spacing:-0.02em; color:var(--color-text); margin-bottom:32px;">Einstellungen</h1>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px; max-width:820px;">
<!-- SettingsCard: Vorräte (as it exists today) -->
<a href="#" style="display:flex; flex-direction:column; gap:12px; border-radius:var(--radius-xl); border:1px solid var(--color-border); background:var(--color-surface); padding:28px; text-decoration:none; box-shadow:var(--shadow-card);">
<span style="font-family:var(--font-sans); font-size:16px; font-weight:500; color:var(--color-text);">Vorräte</span>
<p style="font-family:var(--font-sans); font-size:28px; font-weight:300; letter-spacing:-0.02em; color:var(--green-dark); font-family:var(--font-display); line-height:1; margin:0;">23</p>
<span style="font-family:var(--font-sans); font-size:12px; font-weight:500; color:var(--green-dark); margin-top:auto;">Vorräte bearbeiten →</span>
</a>
<!-- SettingsCard: Haushalt -->
<a href="#" style="display:flex; flex-direction:column; gap:12px; border-radius:var(--radius-xl); border:1px solid var(--color-border); background:var(--color-surface); padding:28px; text-decoration:none; box-shadow:var(--shadow-card);">
<span style="font-family:var(--font-sans); font-size:16px; font-weight:500; color:var(--color-text);">Haushalt</span>
<p style="font-family:var(--font-sans); font-size:13px; color:var(--color-text-muted); margin:0;">3 Mitglieder</p>
<span style="font-family:var(--font-sans); font-size:12px; font-weight:500; color:var(--green-dark); margin-top:auto;">Mitglieder anzeigen →</span>
</a>
<!-- SettingsCard: Profil -->
<a href="#" style="display:flex; flex-direction:column; gap:12px; border-radius:var(--radius-xl); border:1px solid var(--color-border); background:var(--color-surface); padding:28px; text-decoration:none; box-shadow:var(--shadow-card);">
<span style="font-family:var(--font-sans); font-size:16px; font-weight:500; color:var(--color-text);">Profil</span>
<p style="font-family:var(--font-sans); font-size:13px; color:var(--color-text-muted); margin:0;">Max Mustermann</p>
<span style="font-family:var(--font-sans); font-size:12px; font-weight:500; color:var(--green-dark); margin-top:auto;">Profil bearbeiten →</span>
</a>
</div>
</div>
<div style="padding:8px 16px; background:var(--color-subtle); border-top:1px solid #c8c7c0; border-bottom:1px solid #c8c7c0;">
<span style="font-family:var(--font-mono); font-size:11px; color:var(--color-text-muted);">/household/staples — same tile shell applied to CategorySection</span>
</div>
<div style="padding:32px 40px;">
<div style="display:flex; align-items:baseline; justify-content:space-between; margin-bottom:32px; max-width:820px;">
<h1 style="font-family:var(--font-display); font-size:28px; font-weight:500; letter-spacing:-0.02em; color:var(--color-text);">Vorräte</h1>
<span style="font-family:var(--font-sans); font-size:13px; color:var(--color-text-muted);">23 gewählt</span>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px; max-width:820px;">
<div class="cat-tile">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Gemüse</span>
<span class="badge">7 / 18</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-on">Möhren</span>
<span class="chip chip-on">Zwiebeln</span>
<span class="chip chip-on">Knoblauch</span>
<span class="chip chip-off">Zucchini</span>
<span class="chip chip-on">Paprika</span>
<span class="chip chip-on">Tomaten</span>
<span class="chip chip-off">Aubergine</span>
<span class="chip chip-on">Brokkoli</span>
<button class="overflow-trigger">+10 weitere …</button>
</div>
</div>
<div class="cat-tile">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Gewürze</span>
<span class="badge badge-zero">0 / 24</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips">
<span class="chip chip-off">Salz</span>
<span class="chip chip-off">Pfeffer</span>
<span class="chip chip-off">Kurkuma</span>
<span class="chip chip-off">Thymian</span>
<span class="chip chip-off">Oregano</span>
<span class="chip chip-off">Paprika</span>
<span class="chip chip-off">Koriander</span>
<span class="chip chip-off">Zimt</span>
<button class="overflow-trigger">+16 weitere …</button>
</div>
</div>
</div>
</div>
</div>
<!-- ── Seed flow ── -->
<div class="section-label" style="margin-top:48px;">Seed Flow — Haushalt ohne Zutaten</div>
<div class="analysis" style="margin-bottom:32px;">
<h3>Wann und wie wird der Katalog befüllt?</h3>
<div class="analysis-grid">
<div class="analysis-col good">
<h4>Backend — automatisch beim Erstellen des Haushalts</h4>
<ul>
<li>Flyway-Migration oder HouseholdService seed bei <code>POST /v1/households</code></li>
<li>Seed-Daten: ~100 typisch deutsche Grundzutaten in 8 Kategorien</li>
<li>Alle Zutaten mit <code>isStaple = false</code> — Nutzer entscheidet selbst</li>
<li>Kategorien werden mitgeneriert (gleicher Haushalt-Scope)</li>
<li>Danach normal editierbar: toggle, add, PATCH name/kategorie</li>
</ul>
</div>
<div class="analysis-col good">
<h4>Seed-Katalog — 8 Kategorien (Auswahl)</h4>
<ul>
<li><strong>Gemüse:</strong> Möhren, Zwiebeln, Knoblauch, Kartoffeln, Tomaten, Paprika, Spinat, Brokkoli, Zucchini, Lauch, Sellerie, Kohl, Blumenkohl, Kürbis, Süßkartoffeln …</li>
<li><strong>Gewürze:</strong> Salz, Pfeffer, Kurkuma, Kreuzkümmel, Paprikapulver, Zimt, Thymian, Oregano, Rosmarin, Lorbeer, Muskat, Curry, Chili …</li>
<li><strong>Öle &amp; Essig:</strong> Olivenöl, Rapsöl, Sesamöl, Kokosöl, Apfelessig, Balsamico …</li>
<li><strong>Milch &amp; Käse:</strong> Butter, Milch, Sahne, Joghurt, Parmesan, Mozzarella, Quark …</li>
<li><strong>Getreide &amp; Hülsenfrüchte:</strong> Reis, Nudeln, Linsen, Kichererbsen, Haferflocken, Quinoa, Couscous …</li>
<li><strong>Fleisch &amp; Fisch:</strong> Hähnchenbrust, Hackfleisch, Lachs, Thunfisch (Dose), Eier, Speck …</li>
<li><strong>Backzutaten:</strong> Mehl, Zucker, Backpulver, Hefe, Speisestärke, Vanille …</li>
<li><strong>Saucen &amp; Würzmittel:</strong> Tomatenmark, Sojasoße, Senf, Ketchup, Brühe, Kokosmilch …</li>
</ul>
</div>
</div>
</div>
<!-- ── Add ingredient flow ── -->
<div class="section-label">Add-Ingredient Flow — Inline pro Tile</div>
<p style="font-size:13px; color:var(--color-text-muted); max-width:640px; margin-bottom:28px; line-height:1.6;">
Jedes Category-Tile hat am unteren Ende einen stillen „+ Zutat hinzufügen" Trigger.
Der Flow bleibt inline — kein Modal, kein Sheet. Neue Zutat erscheint sofort als ausgewählter Chip.
</p>
<!-- State specimens row -->
<div class="states" style="margin-bottom:48px;">
<!-- State 0: rest -->
<div class="state-specimen" style="flex:none; width:300px;">
<div class="state-label">1 — Rest (Trigger sichtbar)</div>
<div class="cat-tile" style="padding:20px;">
<div class="cat-tile-header" style="margin-bottom:12px;">
<div class="cat-tile-header-left">
<span class="cat-title" style="font-size:15px;">Gewürze</span>
<span class="badge badge-zero">0 / 24</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips" style="margin-bottom:10px;">
<span class="chip chip-off">Salz</span>
<span class="chip chip-off">Pfeffer</span>
<span class="chip chip-off">Kurkuma</span>
<span class="chip chip-off">Thymian</span>
<button class="overflow-trigger">+20 weitere …</button>
</div>
<!-- Trigger -->
<div style="margin-top:8px; padding-top:10px; border-top:1px solid var(--color-border);">
<button style="font-family:var(--font-sans); font-size:12px; font-weight:500; color:var(--color-text-muted); background:none; border:none; padding:0; cursor:pointer; letter-spacing:0.02em;">
+ Zutat hinzufügen
</button>
</div>
</div>
</div>
<!-- State 1: input active -->
<div class="state-specimen" style="flex:none; width:300px;">
<div class="state-label">2 — Eingabe aktiv</div>
<div class="cat-tile" style="padding:20px;">
<div class="cat-tile-header" style="margin-bottom:12px;">
<div class="cat-tile-header-left">
<span class="cat-title" style="font-size:15px;">Gewürze</span>
<span class="badge badge-zero">0 / 24</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips" style="margin-bottom:10px;">
<span class="chip chip-off">Salz</span>
<span class="chip chip-off">Pfeffer</span>
<span class="chip chip-off">Kurkuma</span>
<span class="chip chip-off">Thymian</span>
<button class="overflow-trigger">+20 weitere …</button>
</div>
<!-- Inline input (replaces trigger) -->
<div style="margin-top:8px; padding-top:10px; border-top:1px solid var(--color-border);">
<div style="display:flex; gap:6px; align-items:center;">
<input
type="text"
placeholder="Name eingeben …"
value="Safran"
style="flex:1; font-family:var(--font-sans); font-size:13px; color:var(--color-text);
background:var(--color-page); border:1px solid var(--green-light);
border-radius:var(--radius-md); padding:6px 10px; outline:none; min-width:0;"
/>
<button style="font-family:var(--font-sans); font-size:12px; font-weight:500; padding:6px 10px; border-radius:var(--radius-md); background:var(--green-dark); color:white; border:none; cursor:pointer; white-space:nowrap;">
</button>
<button style="font-family:var(--font-sans); font-size:12px; color:var(--color-text-muted); background:none; border:none; cursor:pointer; padding:4px;">
</button>
</div>
<p style="font-size:11px; color:var(--color-text-muted); margin-top:5px;">Enter zum Speichern · Esc zum Abbrechen</p>
</div>
</div>
</div>
<!-- State 2: success -->
<div class="state-specimen" style="flex:none; width:300px;">
<div class="state-label">3 — Zutat hinzugefügt (optimistic)</div>
<div class="cat-tile" style="padding:20px;">
<div class="cat-tile-header" style="margin-bottom:12px;">
<div class="cat-tile-header-left">
<span class="cat-title" style="font-size:15px;">Gewürze</span>
<span class="badge">1 / 25</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips" style="margin-bottom:10px;">
<span class="chip chip-off">Salz</span>
<span class="chip chip-off">Pfeffer</span>
<span class="chip chip-off">Kurkuma</span>
<span class="chip chip-off">Thymian</span>
<!-- Newly added — selected + subtle ring to draw eye -->
<span class="chip chip-on" style="outline:2px solid var(--green-light); outline-offset:2px;">Safran</span>
<button class="overflow-trigger">+20 weitere …</button>
</div>
<div style="margin-top:8px; padding-top:10px; border-top:1px solid var(--color-border);">
<button style="font-family:var(--font-sans); font-size:12px; font-weight:500; color:var(--color-text-muted); background:none; border:none; padding:0; cursor:pointer;">
+ Zutat hinzufügen
</button>
</div>
</div>
</div>
<!-- State 3: duplicate error -->
<div class="state-specimen" style="flex:none; width:300px;">
<div class="state-label">4 — Duplikat-Fehler</div>
<div class="cat-tile" style="padding:20px;">
<div class="cat-tile-header" style="margin-bottom:12px;">
<div class="cat-tile-header-left">
<span class="cat-title" style="font-size:15px;">Gewürze</span>
<span class="badge badge-zero">0 / 24</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips" style="margin-bottom:10px;">
<span class="chip chip-off">Salz</span>
<!-- Existing "Pfeffer" highlighted: user tried to add it again -->
<span class="chip chip-off" style="outline:2px solid var(--green-light); outline-offset:2px;">Pfeffer</span>
<span class="chip chip-off">Kurkuma</span>
<span class="chip chip-off">Thymian</span>
<button class="overflow-trigger">+20 weitere …</button>
</div>
<div style="margin-top:8px; padding-top:10px; border-top:1px solid var(--color-border);">
<div style="display:flex; gap:6px; align-items:center;">
<input
type="text"
value="Pfeffer"
style="flex:1; font-family:var(--font-sans); font-size:13px; color:var(--color-text);
background:var(--color-page); border:1px solid var(--color-error);
border-radius:var(--radius-md); padding:6px 10px; outline:none; min-width:0;"
/>
<button style="font-size:12px; font-weight:500; padding:6px 10px; border-radius:var(--radius-md); background:var(--green-dark); color:white; border:none; cursor:pointer;"></button>
<button style="font-size:12px; color:var(--color-text-muted); background:none; border:none; cursor:pointer; padding:4px;"></button>
</div>
<p style="font-size:11px; color:var(--color-error); margin-top:5px;">„Pfeffer" existiert bereits in dieser Kategorie.</p>
</div>
</div>
</div>
</div><!-- /states -->
<!-- Add flow page preview -->
<div class="preview-pair" style="margin-bottom:0;">
<div class="preview-d-wrap">
<div class="preview-label">Desktop — Tile mit aktiver Eingabe (Gemüse-Tile, Eingabe offen)</div>
<div class="preview-d-clip" style="height:360px;">
<div class="preview-d-scale">
<div style="background:var(--color-page); padding:40px 56px;">
<div style="display:flex; align-items:baseline; justify-content:space-between; margin-bottom:4px; max-width:820px;">
<h1 style="font-family:var(--font-display); font-size:28px; font-weight:500; letter-spacing:-0.02em; color:var(--color-text);">Vorräte</h1>
<span style="font-family:var(--font-sans); font-size:13px; color:var(--color-text-muted);">23 gewählt</span>
</div>
<p style="font-size:11px; color:var(--color-text-muted); margin-bottom:32px;">Automatisch gespeichert.</p>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px; max-width:820px;">
<!-- Tile with open input -->
<div class="cat-tile" style="border-color:var(--green-light); box-shadow:var(--shadow-raised);">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Gemüse</span>
<span class="badge">7 / 18</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips" style="margin-bottom:12px;">
<span class="chip chip-on">Möhren</span>
<span class="chip chip-on">Zwiebeln</span>
<span class="chip chip-on">Knoblauch</span>
<span class="chip chip-on">Paprika</span>
<span class="chip chip-off">Zucchini</span>
<span class="chip chip-on">Tomaten</span>
<span class="chip chip-off">Aubergine</span>
<span class="chip chip-on">Brokkoli</span>
<button class="overflow-trigger">+10 weitere …</button>
</div>
<!-- Active add row -->
<div style="padding-top:12px; border-top:1px solid var(--color-border);">
<div style="display:flex; gap:6px;">
<input type="text" placeholder="Name eingeben …" value="Fenchel"
style="flex:1; font-family:var(--font-sans); font-size:13px;
background:var(--color-page); border:1px solid var(--green-light);
border-radius:var(--radius-md); padding:7px 10px; min-width:0;" />
<button style="font-size:13px; font-weight:500; padding:7px 14px; border-radius:var(--radius-md); background:var(--green-dark); color:white; border:none; cursor:pointer;"></button>
<button style="font-size:13px; color:var(--color-text-muted); background:none; border:none; cursor:pointer; padding:7px 6px;"></button>
</div>
</div>
</div>
<!-- Normal tile -->
<div class="cat-tile">
<div class="cat-tile-header">
<div class="cat-tile-header-left">
<span class="cat-title">Gewürze</span>
<span class="badge badge-zero">0 / 24</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn">Alle</button>
<button class="cat-action-btn">Keine</button>
</div>
</div>
<div class="cat-chips" style="margin-bottom:12px;">
<span class="chip chip-off">Salz</span>
<span class="chip chip-off">Pfeffer</span>
<span class="chip chip-off">Kurkuma</span>
<span class="chip chip-off">Thymian</span>
<span class="chip chip-off">Oregano</span>
<span class="chip chip-off">Paprika</span>
<button class="overflow-trigger">+18 weitere …</button>
</div>
<div style="padding-top:10px; border-top:1px solid var(--color-border);">
<button style="font-family:var(--font-sans); font-size:12px; font-weight:500; color:var(--color-text-muted); background:none; border:none; padding:0; cursor:pointer;">+ Zutat hinzufügen</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile — Tile mit Eingabe</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div style="background:var(--color-page); padding:16px 20px;">
<div style="display:flex; align-items:baseline; justify-content:space-between; margin-bottom:20px;">
<h1 style="font-family:var(--font-display); font-size:18px; font-weight:500; color:var(--color-text);">Vorräte</h1>
<span style="font-size:11px; color:var(--color-text-muted);">23 gewählt</span>
</div>
<div style="display:flex; flex-direction:column; gap:12px;">
<div class="cat-tile" style="padding:20px; border-color:var(--green-light); box-shadow:var(--shadow-raised);">
<div class="cat-tile-header" style="margin-bottom:10px;">
<div class="cat-tile-header-left">
<span style="font-size:14px; font-weight:500;">Gemüse</span>
<span class="badge">7/18</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn" style="font-size:9px;">Alle</button>
<button class="cat-action-btn" style="font-size:9px;">Keine</button>
</div>
</div>
<div class="cat-chips" style="margin-bottom:10px;">
<span class="chip chip-on" style="font-size:12px; padding:4px 10px;">Möhren</span>
<span class="chip chip-on" style="font-size:12px; padding:4px 10px;">Zwiebeln</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Zucchini</span>
<span class="chip chip-on" style="font-size:12px; padding:4px 10px;">Paprika</span>
<button class="overflow-trigger" style="font-size:11px;">+14 weitere …</button>
</div>
<div style="padding-top:10px; border-top:1px solid var(--color-border);">
<div style="display:flex; gap:6px;">
<input type="text" placeholder="Name …" value="Fenchel"
style="flex:1; font-size:12px; background:var(--color-page);
border:1px solid var(--green-light); border-radius:var(--radius-md);
padding:6px 8px; min-width:0;" />
<button style="font-size:12px; padding:6px 10px; border-radius:var(--radius-md); background:var(--green-dark); color:white; border:none;"></button>
<button style="font-size:12px; color:var(--color-text-muted); background:none; border:none; padding:4px;"></button>
</div>
</div>
</div>
<div class="cat-tile" style="padding:20px;">
<div class="cat-tile-header" style="margin-bottom:10px;">
<div class="cat-tile-header-left">
<span style="font-size:14px; font-weight:500;">Gewürze</span>
<span class="badge badge-zero">0/24</span>
</div>
<div class="cat-tile-header-right">
<button class="cat-action-btn" style="font-size:9px;">Alle</button>
<button class="cat-action-btn" style="font-size:9px;">Keine</button>
</div>
</div>
<div class="cat-chips" style="margin-bottom:10px;">
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Salz</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Pfeffer</span>
<span class="chip chip-off" style="font-size:12px; padding:4px 10px;">Kurkuma</span>
<button class="overflow-trigger" style="font-size:11px;">+21 weitere …</button>
</div>
<div style="padding-top:10px; border-top:1px solid var(--color-border);">
<button style="font-size:12px; font-weight:500; color:var(--color-text-muted); background:none; border:none; padding:0; cursor:pointer;">+ Zutat hinzufügen</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- /preview-pair -->
<!-- ── Machine-readable ── -->
<div class="agent-section" aria-hidden="true" style="display:none;">
<!--
╔══════════════════════════════════════════════════════════════════╗
║ MACHINE-READABLE SPEC — A3/D3 Tile Redesign ║
╚══════════════════════════════════════════════════════════════════╝
GOAL: Replace CategorySection.svelte's plain <div> wrapper with a tile
shell identical to SettingsCard. Add count badge, Alle/Keine buttons,
and chip overflow. Grid matches /settings page exactly.
─────────────────────────────────────────────────────────────────
## 1. CategorySection.svelte — full rewrite of template + new props
NEW PROPS (additions only, existing props unchanged):
onSelectAll: () => void
onDeselectAll: () => void
visibleLimit: number = 8 // chips shown before overflow
COMPUTED:
selectedCount = ingredients.filter(i => i.isStaple).length
totalCount = ingredients.length
visibleChips = expanded ? ingredients : ingredients.slice(0, visibleLimit)
hasOverflow = !expanded && ingredients.length > visibleLimit
overflowCount = ingredients.length - visibleLimit
STATE:
expanded: boolean = false
TEMPLATE STRUCTURE:
<div
class="flex flex-col
rounded-[var(--radius-xl)]
border border-[var(--color-border)]
bg-[var(--color-surface)]
p-[28px]
shadow-[var(--shadow-card)]
hover:shadow-[var(--shadow-raised)]
hover:border-[var(--color-border-hover)]
transition-[box-shadow,border-color] duration-150 ease"
>
<!-- Tile header row -->
<div class="flex items-center justify-between mb-[16px]">
<!-- Left: title + count badge -->
<div class="flex items-center gap-[8px]">
<span class="font-[var(--font-sans)] text-[16px] font-medium text-[var(--color-text)]">
{name}
</span>
<span class="
inline-flex items-center justify-center
text-[11px] font-medium
rounded-[var(--radius-full)] px-[7px] py-[1px]
{selectedCount > 0
? 'bg-[var(--green-tint)] text-[var(--green-dark)]'
: 'bg-[var(--color-subtle)] text-[var(--color-text-muted)]'}
">
{selectedCount} / {totalCount}
</span>
</div>
<!-- Right: Alle / Keine -->
<div class="flex items-center gap-[6px]">
<button
type="button"
onclick={onSelectAll}
class="font-[var(--font-sans)] text-[10px] font-medium tracking-[0.04em]
px-[8px] py-[2px] rounded-[var(--radius-full)]
border border-[var(--color-border)] bg-transparent
text-[var(--color-text-muted)]
hover:border-[var(--color-border-hover)] hover:text-[var(--color-text)]"
>Alle</button>
<button
type="button"
onclick={onDeselectAll}
class="font-[var(--font-sans)] text-[10px] font-medium tracking-[0.04em]
px-[8px] py-[2px] rounded-[var(--radius-full)]
border border-[var(--color-border)] bg-transparent
text-[var(--color-text-muted)]
hover:border-[var(--color-border-hover)] hover:text-[var(--color-text)]"
>Keine</button>
</div>
</div><!-- /header -->
<!-- Chip cloud -->
<div class="flex flex-wrap gap-[6px]">
{#each visibleChips as ingredient (ingredient.id)}
<StapleChip
name={ingredient.name}
selected={ingredient.isStaple}
onToggle={(value) => onToggle(ingredient.id, value)}
/>
{/each}
{#if hasOverflow}
<button
type="button"
onclick={() => { expanded = true; }}
class="font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]
bg-transparent border-none underline underline-offset-2
py-[3px] px-[2px]"
>+{overflowCount} weitere …</button>
{/if}
</div>
</div><!-- /tile -->
─────────────────────────────────────────────────────────────────
## 2. StaplesManager.svelte — grid + selectAll/deselectAll wiring
GRID CLASS CHANGE:
BEFORE: "grid grid-cols-1 gap-[24px_32px] md:grid-cols-3"
AFTER: "grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-[820px]"
// matches /settings page exactly
TOTAL COUNT COMPUTATION (new):
totalSelected = $derived(Object.values(stapleState).filter(Boolean).length)
SELECT ALL HANDLER (new):
function handleSelectAll(categoryId: string) {
const category = categories.find(c => c.id === categoryId);
if (!category) return;
for (const ing of category.ingredients) {
if (!stapleState[ing.id]) {
stapleState[ing.id] = true;
getPatcher(ing.id)(ing.id, true);
}
}
}
DESELECT ALL HANDLER (new):
function handleDeselectAll(categoryId: string) {
const category = categories.find(c => c.id === categoryId);
if (!category) return;
for (const ing of category.ingredients) {
if (stapleState[ing.id]) {
stapleState[ing.id] = false;
getPatcher(ing.id)(ing.id, false);
}
}
}
PASS TO CategorySection:
onSelectAll={() => handleSelectAll(category.id)}
onDeselectAll={() => handleDeselectAll(category.id)}
EXPOSE totalSelected as prop or slot for +page.svelte to render in header.
─────────────────────────────────────────────────────────────────
## 3. +page.svelte (settings context) — h1 row
CHANGE header to match /settings and /members exactly:
<a href="/settings" >← Einstellungen</a>
<div class="flex items-baseline justify-between mb-[4px]">
<h1 class="font-[var(--font-display)] text-[18px] md:text-[28px] font-medium
tracking-[-0.02em] text-[var(--color-text)]">Vorräte</h1>
<span class="font-[var(--font-sans)] text-[13px] text-[var(--color-text-muted)]">
{totalSelected} gewählt
</span>
</div>
<p class="font-[var(--font-sans)] text-[11px] text-[var(--color-text-muted)] mb-8">
Automatisch gespeichert. Gilt ab der nächsten Einkaufsliste.
</p>
NOTE: mb-8 (32px) before grid matches /settings page spacing.
─────────────────────────────────────────────────────────────────
## 4. Token reference table
| Token | Value | Applied to |
|--------------------------|----------|-------------------------------|
| --radius-xl | 16px | tile border-radius |
| --color-surface | #F5F4EE | tile background |
| --color-border | #D8D7D0 | tile border (rest) |
| --color-border-hover | #C0BFB8 | tile border (hover) |
| --shadow-card | ... | tile elevation (rest) |
| --shadow-raised | ... | tile elevation (hover) |
| 28px | — | tile padding (desktop) |
| 20px | — | tile padding (mobile, md:28px)|
| --green-tint/dark | — | badge when selected > 0 |
| --color-subtle/muted | — | badge when selected === 0 |
| --color-text-muted | #6B6A63 | action btn text, overflow btn |
| --color-border | #D8D7D0 | action btn border |
| 16px / 500 / --font-sans | — | category title (= SettingsCard title) |
| gap-4 (16px) | — | grid gap (= /settings grid) |
| sm:grid-cols-2 | — | grid breakpoint |
| max-w-[820px] | — | grid max-width |
## 5. No-op: StapleChip.svelte
No changes needed. Chip tokens and hover/focus styles remain identical.
## 6. Mobile adjustment
On mobile (< sm), tile padding reduces to 20px:
class="p-[20px] md:p-[28px]"
Title font-size stays 16px/500 (no mobile reduction tile is smaller but label stays legible).
SEED FLOW
## 7. Backend Household seed on creation
TRIGGER: after successful household creation (HouseholdService or Flyway data migration)
SCOPE: per-household (all seeded rows carry the new household's ID)
DEFAULT: isStaple = false for all seeded ingredients
CATEGORIES TO SEED (with sortOrder):
1 Gemüse
2 Gewürze & Kräuter
3 Öle & Essig
4 Milch & Käse
5 Getreide & Hülsenfrüchte
6 Fleisch & Fisch
7 Backzutaten
8 Saucen & Würzmittel
SEED INGREDIENTS (representative list full list in seed SQL/Java constant):
Gemüse: Möhren, Zwiebeln, Knoblauch, Kartoffeln, Tomaten, Paprika (rot),
Paprika (gelb), Spinat, Brokkoli, Zucchini, Lauch, Sellerie,
Blumenkohl, Kohl, Kürbis, Süßkartoffeln, Aubergine, Gurke,
Erbsen (TK), Mais (Dose)
Gewürze & Kräuter: Salz, Pfeffer (schwarz), Kurkuma, Kreuzkümmel, Paprikapulver (edelsüß),
Paprikapulver (scharf), Zimt, Thymian, Oregano, Rosmarin, Lorbeerblätter,
Muskatnuss, Currypulver, Chilliflocken, Koriander (gemahlen),
Petersilie, Schnittlauch, Basilikum
Öle & Essig: Olivenöl, Rapsöl, Sesamöl, Kokosöl, Apfelessig, Balsamico-Essig,
Weinessig
Milch & Käse: Butter, Milch (3,5%), Sahne, Schmand, Joghurt (natur), Parmesan,
Mozzarella, Gouda, Quark, Frischkäse, Feta
Getreide & Hülsenfrüchte: Reis (Langkorn), Basmati-Reis, Nudeln, Spaghetti,
Linsen (rote), Linsen (grüne), Kichererbsen (Dose), Kidneybohnen (Dose),
Haferflocken, Quinoa, Couscous, Bulgur, Polenta
Fleisch & Fisch: Hähnchenbrust, Hähnchenkeule, Hackfleisch (gemischt), Rinderhack,
Lachs (Filet), Thunfisch (Dose), Garnelen (TK), Speck, Eier
Backzutaten: Mehl (Typ 405), Mehl (Typ 550), Zucker, Brauner Zucker,
Backpulver, Natron, Hefe (trocken), Speisestärke, Vanillezucker,
Puderzucker, Kakaopulver, Schokolade (Zartbitter)
Saucen & Würzmittel: Tomatenmark, Passierte Tomaten, Tomaten (Dose, ganz),
Sojasoße, Senf (mittelscharf), Ketchup, Mayonnaise,
Gemüsebrühe, Hühnerbrühe, Kokosmilch, Worcestershire-Sauce,
Tabasco, Honig, Ahornsirup
IMPLEMENTATION NOTE:
Option A (preferred): Java constant in HouseholdSeedService.java, called from
HouseholdService.createHousehold() after persisting the household entity.
Option B: Flyway repeatable migration R__seed_ingredients.sql only if global
seed makes sense (it doesn't here: ingredients are household-scoped).
Deduplication: before inserting, check if category/ingredient with same name
(citext) already exists for the household skip if found. This makes the
seed idempotent (safe to re-run in dev).
ADD INGREDIENT FLOW
## 8. Backend new POST /v1/ingredients endpoint
REQUEST: POST /v1/ingredients
Content-Type: application/json
{ "name": "Fenchel", "categoryId": "uuid-of-category" }
categoryId is optional if omitted, ingredient has no category
RESPONSE: 201 Created
{ "id": "new-uuid", "name": "Fenchel", "isStaple": true, "categoryId": "..." }
NOTE: newly user-created ingredients default to isStaple = true
(user explicitly added it reasonable to assume they want it tracked)
ERROR 409 Conflict:
{ "error": "DUPLICATE_INGREDIENT", "message": "..." }
Backend uses citext so "fenchel" == "Fenchel" == "FENCHEL"
AUTHORIZATION: planner role (same as PATCH)
## 9. Frontend CategorySection.svelte additions
NEW PROP:
onAddIngredient: (name: string) => Promise<{ id: string; name: string } | 'duplicate' | 'error'>
NEW STATE:
addMode: boolean = false
addInput: string = ''
addError: '' | 'duplicate' | 'error' = ''
addHighlightId: string | null = null // ID of duplicate chip to highlight briefly
TILE FOOTER (below chip cloud, always rendered):
<div class="pt-[10px] mt-[10px] border-t border-[var(--color-border)]">
{#if !addMode}
<button
type="button"
onclick={() => { addMode = true; addInput = ''; addError = ''; }}
class="font-[var(--font-sans)] text-[12px] font-medium tracking-[0.02em]
text-[var(--color-text-muted)] bg-transparent border-none cursor-pointer"
>+ Zutat hinzufügen</button>
{:else}
<div class="flex gap-[6px]">
<input
type="text"
bind:value={addInput}
placeholder="Name eingeben …"
autofocus
onkeydown={(e) => {
if (e.key === 'Enter') handleAdd();
if (e.key === 'Escape') { addMode = false; addError = ''; }
}}
class="flex-1 min-w-0
font-[var(--font-sans)] text-[13px] text-[var(--color-text)]
bg-[var(--color-page)]
border rounded-[var(--radius-md)] px-[10px] py-[6px]
{addError ? 'border-[var(--color-error)]' : 'border-[var(--green-light)]'}
focus:outline-none"
/>
<button
type="button"
onclick={handleAdd}
disabled={!addInput.trim()}
class="font-[var(--font-sans)] text-[12px] font-medium tracking-[0.04em]
px-[10px] py-[6px] rounded-[var(--radius-md)]
bg-[var(--green-dark)] text-white border-none cursor-pointer
disabled:opacity-40 disabled:cursor-not-allowed"
></button>
<button
type="button"
onclick={() => { addMode = false; addError = ''; }}
class="font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]
bg-transparent border-none cursor-pointer px-[4px]"
>✕</button>
</div>
{#if addError === 'duplicate'}
<p class="text-[11px] text-[var(--color-error)] mt-[5px]">
„{addInput}" existiert bereits in dieser Kategorie.
</p>
{:else if addError === 'error'}
<p class="text-[11px] text-[var(--color-error)] mt-[5px]">
Konnte nicht gespeichert werden.
</p>
{/if}
{/if}
</div>
ADD HANDLER:
async function handleAdd() {
const name = addInput.trim();
if (!name) return;
addError = '';
const result = await onAddIngredient(name);
if (result === 'duplicate') {
addError = 'duplicate';
// find matching ingredient by name (citext: case-insensitive compare)
const existing = ingredients.find(i => i.name.toLowerCase() === name.toLowerCase());
if (existing) {
addHighlightId = existing.id;
setTimeout(() => { addHighlightId = null; }, 2000);
}
} else if (result === 'error') {
addError = 'error';
} else {
// success — optimistically add chip (selected = true, from POST response)
// parent (StaplesManager) handles state update
addMode = false;
addInput = '';
addError = '';
}
}
DUPLICATE CHIP HIGHLIGHT:
In StapleChip.svelte, accept optional highlight prop:
let { ..., highlight = false }: { ...; highlight?: boolean } = $props();
add class: highlight ? 'outline-2 outline-offset-2 outline-[var(--green-light)]' : ''
## 10. StaplesManager.svelte — onAddIngredient handler
async function handleAddIngredient(categoryId: string, name: string) {
const res = await fetch('/household/staples/ingredients', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, categoryId })
});
if (res.status === 409) return 'duplicate';
if (!res.ok) return 'error';
const created: { id: string; name: string; isStaple: boolean } = await res.json();
// Optimistically add to categories state
categories = categories.map(cat =>
cat.id === categoryId
? { ...cat, ingredients: [...cat.ingredients, created] }
: cat
);
// Mark as staple (POST returns isStaple: true)
stapleState[created.id] = true;
return created;
}
PASS TO CategorySection:
onAddIngredient={(name) => handleAddIngredient(category.id, name)}
## 11. New SvelteKit API route: +server.ts at /household/staples/ingredients
File: frontend/src/routes/(app)/household/staples/ingredients/+server.ts
export const POST: RequestHandler = async ({ request, locals }) => {
const { name, categoryId } = await request.json();
const res = await fetch(`${BACKEND}/v1/ingredients`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: ... },
body: JSON.stringify({ name, categoryId })
});
return new Response(await res.text(), { status: res.status,
headers: { 'Content-Type': 'application/json' } });
};
-->
</div>
</div><!-- /doc -->
</body>
</html>