diff --git a/specs/staples-settings-redesign.html b/specs/staples-settings-redesign.html new file mode 100644 index 0000000..4a39324 --- /dev/null +++ b/specs/staples-settings-redesign.html @@ -0,0 +1,1334 @@ + + +
+ + +Vorräte Settings — Redesign Concepts
+ + ++ Categories are collapsed by default. A count badge shows selected/total at a glance. + Tap to expand any category — all others stay collapsed. One category in focus at a time. +
+ +Wähle deine regelmäßig vorhandenen Zutaten.
+ + + + +Automatisch gespeichert.
+Wähle deine regelmäßig vorhandenen Zutaten. Automatisch gespeichert.
+ + ++ Trade-off: Solves visual overload immediately — only one category is in view at a time. + The Alle/Keine actions eliminate bulk-select friction. Count badges answer "where do I still need to configure?" + at a glance. Risk: accordion interaction adds a tap per category visit; power users who want + a full overview lose it. Works best when categories are <8. Keyboard nav via Enter/Space on row headers. +
++ Left panel: sticky category list with count badges — acts as a mini-nav. + Right panel: the selected category's full chip cloud. One category in focus, full list visible. + Mobile collapses to top tabs (scrollable). +
+ ++ Trade-off: Best scalability — the sidebar grows gracefully to 15+ categories without + visual breakdown. The "23 Vorräte insgesamt" footer answers the comprehension question for the whole household. + Active/inactive category coloring (green-tint bg + green-dark text) matches the existing nav convention exactly. + Risk: Two-panel layouts can feel over-engineered for simple tasks. Mobile tab strip + truncates long category names — needs abbreviation strategy. +
++ Search bar dominates the top. Below it: a single scrollable list with sticky category headers (like iOS Contacts). + Searching filters in real-time — category headers collapse when their section is empty. + For power users and anyone who knows what they want. +
+ +Automatisch gespeichert.
+Automatisch gespeichert. Gilt ab der nächsten Einkaufsliste.
+ + ++ Trade-off: Fastest path for users who know what they want. Search cuts through all category + hierarchy instantly. The checklist row style in search results (vs chips) is intentional — it's denser and + keyboard-navigable (arrow keys + Space). The idle state preserves the chip cloud so casual browsing stays familiar. + Risk: Two different interaction patterns (chips vs checklist rows in search) add cognitive load. + The sticky-header list can be hard to test cross-browser. +
++ Abandon chips entirely. A single scrollable table: category, ingredient name, checkbox. + Sorted by category. Extremely keyboard-friendly, screen-reader native, and + far denser — 40 ingredients fit in the same space as 10 chips. +
+ +23 gewählt · automatisch gespeichert
+ + +Automatisch gespeichert. Gilt ab der nächsten Einkaufsliste.
+ + ++ Trade-off: Highest information density. Excellent keyboard navigation (tab to checkbox, space to toggle). + Screen readers get a native checklist — no ARIA juggling needed. The two-column desktop layout keeps categories + side by side without the chip-cloud imbalance problem. + Risk: Loses the "selection feels satisfying" quality of chip toggling — a checkbox is functional, + not delightful. Row highlight (green-tint bg) on checked rows restores some of that signal. + May feel clinical for a consumer cooking app. +
++ Keep the chip paradigm — it works. Add three targeted fixes: + (1) category count badges + Alle/Keine micro-actions inline, + (2) a collapsible "overflow" per category beyond 8 chips, + (3) a global count in the header. Zero new navigation patterns to learn. +
+ +Automatisch gespeichert.
+ + +Automatisch gespeichert. Gilt ab der nächsten Einkaufsliste.
+ ++ Trade-off: Lowest risk, smallest diff — the three components (CategorySection, StapleChip, StaplesManager) + need only minor additions. No new navigation paradigm to learn. Count badges on category headers answer the + comprehension question. The "+N weitere…" overflow trigger caps visual height per category. + Alle/Keine bulk actions land where the eye already is (next to the label). + This is the recommendation for a first iteration — it fixes the acute pain without a redesign. + Concepts 01 or 02 are step-2 if the dataset grows substantially larger. +
+Tile redesign · unified with Settings + Members page language
++ Tile does not 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. +
+Automatisch gespeichert. Gilt ab der nächsten Einkaufsliste.
+ + +Automatisch gespeichert.
+ + +POST /v1/householdsisStaple = false — Nutzer entscheidet selbst+ 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. +
+ + +Enter zum Speichern · Esc zum Abbrechen
+„Pfeffer" existiert bereits in dieser Kategorie.
+Automatisch gespeichert.
+ ++ Automatisch gespeichert. Gilt ab der nächsten Einkaufsliste. +
+ + 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): ++ „{addInput}" existiert bereits in dieser Kategorie. +
+ {:else if addError === 'error'} ++ Konnte nicht gespeichert werden. +
+ {/if} + {/if} + +