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>
1457 lines
75 KiB
HTML
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 > 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 & 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 & 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 & 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 & 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 & 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 & Essig:</strong> Olivenöl, Rapsöl, Sesamöl, Kokosöl, Apfelessig, Balsamico …</li>
|
|
<li><strong>Milch & Käse:</strong> Butter, Milch, Sahne, Joghurt, Parmesan, Mozzarella, Quark …</li>
|
|
<li><strong>Getreide & Hülsenfrüchte:</strong> Reis, Nudeln, Linsen, Kichererbsen, Haferflocken, Quinoa, Couscous …</li>
|
|
<li><strong>Fleisch & 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 & 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>
|