Compare commits

..

37 Commits

Author SHA1 Message Date
16e1539ac0 chore: merge master — adopt SlotResponse.SlotRecipe in SuggestionItem
Resolves conflict by keeping master's refactor: SuggestionItem now reuses
SlotResponse.SlotRecipe instead of the dedicated SuggestionRecipe record,
removing the duplication and adding heroImageUrl to suggestion responses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:08:38 +02:00
f139dce82c docs(specs): add planner desktop redesign spec — flip tiles
Final design spec for the planner desktop layout overhaul:
full-bleed color tiles, CSS 3D card flip for recipe detail,
no persistent right panel, inline suggestions on empty days.
Includes interactive mockup and written component spec.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 18:20:01 +02:00
0596fddcd3 refactor(planning): extract applyPenalties helper to unify score formula
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
008c725813 test(planner): verify mobile swap sheet triggers suggestion fetch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
1739b70d54 feat(planner): change neutral badge copy to Kein Einfluss
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
3b829325f2 feat(planner): hide RecipePicker inner header in swap context
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
d139e5e28c refactor(planner): delete orphaned SwapSuggestionList component and tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
c9d6564fbe refactor(planner): remove dead SwapSuggestionList import and sortedRecipes derived
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
ba79cff4e7 feat(planner): show variety score in swap menu via RecipePicker
Replace SwapSuggestionList with RecipePicker in both mobile and desktop
swap contexts. RecipePicker now accepts excludeRecipeId, replacingRecipe,
and isDisabled props. Mobile swap sheet also triggers suggestion fetch
via activePickerDate so green/yellow/red score badges appear during swap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
55285e7d5d feat(planner): show score badges for all recipes in RecipePicker
- +server.ts: pass topN=100 so all recipes are scored in one request
- RecipePicker: Empfohlen keeps top 5 with scoreDelta > 0; builds a
  scoreMap from all suggestions; shows green/yellow/red delta badge on
  every recipe in Alle Rezepte that has a score entry
- Extracted scoreBadge snippet to avoid duplication between sections

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
055ae11fa3 feat(planner): show yellow neutral badge for scoreDelta = 0 in RecipePicker
Neutral suggestions (no variety impact) now show "= 0.0 Punkte" in yellow
instead of no badge, making the three states explicit: green (improves),
yellow (neutral), red (worsens).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
bf18f2bd84 fix(planner): format variety score to one decimal place
Avoids floating-point display like 6.199999999999999 by using
score.toFixed(1) in VarietyScoreCard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
da21a12222 feat(planner): replace Variationskonflikt with red delta badge
Shows the actual score delta (e.g. "↓ -1.5 Punkte") in red instead of a
generic ⚠ Variationskonflikt label, letting users compare the cost of each
recipe to make an informed swap decision.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
e9dc04b2a5 feat(planner): add remove meal with undo; fix RecipePicker badge for neutral delta
- MealActionSheet: new onremove prop + Entfernen button (guarded by #if)
- +page.svelte: handleRemoveMeal submits delete form, shows undo bar;
  undo re-adds via addSlot form; refactored handleUndo to undoCallback
  pattern; desktop day-detail panel also gets Entfernen button
- RecipePicker: only show green +delta badge when scoreDelta > 0;
  neutral (scoreDelta = 0) shows no badge instead of ⚠ Variationskonflikt
- Tests: page.test.ts remove-meal describe, RecipePicker neutral badge test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
8dfc3df06b fix(planning): hasConflict only when scoreDelta strictly negative
Neutral suggestions (scoreDelta = 0) are not conflicts — they simply
don't improve variety. Changing scoreDelta <= 0 to scoreDelta < 0
lets empty-plan additions and quality-neutral swaps show without a
misleading ⚠ Variationskonflikt warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
ea070b4760 fix(planning): replace existing slot in simulation instead of appending
simulateVarietyScore was adding the candidate recipe on top of the
existing slot for slotDate, keeping the old recipe's tag-repeat penalty
in the score. Now the existing slot is excluded before simulating, so
swapping a recipe for one with better variety correctly shows positive
scoreDelta and hasConflict=false.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
aecdf249d6 feat(planner): add onremove prop and Entfernen button to MealActionSheet
Button only renders when onremove callback is provided, keeping the
component usable in read-only contexts without the destructive action.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
e4345350ad fix(planner): RecipePicker UI polish from review
- Badge font-size 8px → 9px (WCAG minimum)
- Score badge toFixed(1) to avoid misleading "+0 Punkte"
- Self-contain @keyframes pulse in component <style> block
- Wählen buttons use var(--green-dark) per design system

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
56decf155d test(planner): clarify server.test.ts error-branch test name
"when backend returns error" → "when data is undefined (error response
without data)" — documents that the guard is data?.suggestions ?? [],
not error field inspection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
1de4b15e34 refactor(planner): extract Suggestion type to $lib/planner/types.ts
Removes the inline interface from RecipePicker.svelte and replaces
any[] in +page.svelte with Suggestion[] — compile-time safety.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
ccec0baa99 feat(planner): add AbortController to suggestion fetch $effect
Cancels the inflight request when activePickerDate changes or picker
closes, preventing stale responses from overwriting suggestions.
Adds page.test.ts covering fetch trigger, suggestion rendering,
and AbortSignal presence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
9928591b48 refactor(planner): extract computeCurrentScore helper in PlanningService
Eliminates duplicated currentSlots→score pattern that appeared in both
getSuggestions and getVarietyPreview.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
89a549a1c8 test(planner): assert hasConflict=true for neutral scoreDelta on empty plan
Documents the surprising-but-correct behavior: recipes on an empty plan
get scoreDelta=0.0, which satisfies scoreDelta<=0, so hasConflict=true.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
c24281dd4c test(planner): cover topN=0 and topN=-1 boundary in SuggestionsTest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
8051fcbe22 refactor(planner): extract MAX_VARIETY_SCORE constant in PlanningService
Replaces magic literal 10.0 with a named constant in all four
scoring sites: getSuggestions, getVarietyPreview, scoreFromSimulatedSlots,
and getVarietyScore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
b45ab0fd46 fix(planner): guard scoreDelta against undefined in RecipePicker badge
Defensive null-coalescing prevents crash when suggestion data arrives
without scoreDelta (e.g. stale backend or mismatched schema).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
2bbc3762e2 feat(planner): lazy-fetch variety suggestions in RecipePicker for empty slots
Derives activePickerDate from mobile pickerOpen/selectedDay and desktop
recipe-picker panel state, then uses $effect to fetch /planner?planId&date
on demand — wires suggestions and isLoading into both RecipePicker instances.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
a751b0758a feat(planner): add server.test.ts for GET /planner, fix sort + add error handling
- Sort uses scoreDelta instead of removed simulatedScore
- try/catch degrades gracefully to suggestions=[] on backend errors
- 6 tests cover: missing params, success, backend error, network throw, empty result

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
8234c2f162 feat(planner): RecipePicker uses scoreDelta/hasConflict, drop currentVarietyScore, add isLoading
- Suggestion interface: { recipe, scoreDelta, hasConflict } (no simulatedScore)
- Badge renders from hasConflict directly — no client-side delta computation needed
- New isLoading prop shows skeleton rows while suggestions fetch is in flight
- currentVarietyScore prop removed from component and both call sites follow in next commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
257808016d chore(api): update SuggestionItem schema — scoreDelta + hasConflict replace simulatedScore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
cd7f4a1ea0 chore(planner): delete orphaned SuggestionCard component and test
Unused since the suggestions route was removed (commit 4333dc0).
RecipePicker.test.ts is the active coverage for suggestion rendering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
b673a466e9 feat(planner): replace simulatedScore with scoreDelta + hasConflict in SuggestionItem
SuggestionItem now exposes scoreDelta (simulatedScore − currentScore) and
hasConflict (scoreDelta ≤ 0) so the frontend can render badges without
needing to pass currentVarietyScore as a separate prop.

PlanningService.getSuggestions() computes currentScore once per request
and derives scoreDelta + hasConflict per candidate. Sorting is unchanged
(scoreDelta desc = simulatedScore desc since currentScore is constant).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
e3066ec3e5 docs(specs): add C3 variety page rework mockups and V1 implementation spec
Three mockup variations (c3-variety-rework.html) for /planner/variety page,
plus detailed implementation spec for the chosen V1 "Erweiterte Karten" approach:
recipe names + swap links inside warning cards, minimal layout changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:31:14 +02:00
bd1604fc1d docs(specs): add detailed implementation spec for E4 variety settings (V2 Kontext-Preset)
5 states: S0 E1 hub update, S1 default, S2 preset selection + score simulation,
S3 advanced settings + Individuell chip, S4 reset confirmation dialog.
Includes API contract, preset mappings, weight multipliers, and LLM agent region.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:13:17 +02:00
c297403506 docs(specs): add 3 mockup variations for E4 variety settings screen
V1: Structured sections (toggles + segmented weight controls, low effort)
V2: Context preset chips (Omnivor/Vegetarisch/Vegan) with live score preview — recommended
V3: Rule cards with inline examples showing exact penalty impact

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:00:02 +02:00
fa4a4c9ef7 docs(specs): add J9 variety score config user journey and variety page rework spec
- Adds J9 (Configure variety score) to userjourneys.html — new journey for
  tuning the algorithm per household dietary context (e.g. disabling protein
  penalties for vegetarian households); introduces screen E4 (Variety settings)
- Adds specs/frontend/variety-page-rework.html with 3 design variations for
  the /planner/variety page rework: recipe-name pills, action rows (recommended),
  and week-grid with side panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 15:51:26 +02:00
6dd0b7ac93 docs(specs): add final frontend specs for members and settings Kachel views
Finalised implementation specs for /members (E2) and /settings (E1)
pages using the chosen Kachel (card grid) variation. Members spec
covers 6 states including role-change inline control and remove
confirmation dialog; notes backend gaps (DELETE/PATCH member
endpoints). Settings spec covers hub layout, D3 staples sub-page,
hover and empty states.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 15:06:11 +02:00
13 changed files with 8810 additions and 11 deletions

View File

@@ -153,7 +153,7 @@ public class PlanningService {
plan, candidate, slotDate, config, recentlyCookedIds);
double scoreDelta = simulatedScore - currentScore;
boolean hasConflict = scoreDelta < 0;
return new SuggestionResponse.SuggestionItem(toSuggestionRecipe(candidate), scoreDelta, hasConflict);
return new SuggestionResponse.SuggestionItem(toSlotRecipe(candidate), scoreDelta, hasConflict);
})
.sorted((a, b) -> Double.compare(b.scoreDelta(), a.scoreDelta()))
.limit(limit)
@@ -422,11 +422,6 @@ public class PlanningService {
recipe.getCookTimeMin(), recipe.getHeroImageUrl());
}
private SuggestionResponse.SuggestionRecipe toSuggestionRecipe(Recipe recipe) {
return new SuggestionResponse.SuggestionRecipe(recipe.getId(), recipe.getName(),
recipe.getEffort(), recipe.getCookTimeMin());
}
private boolean hasConsecutiveDays(List<LocalDate> days) {
if (days.size() < 2) return false;
List<LocalDate> sorted = days.stream().sorted().toList();

View File

@@ -1,14 +1,11 @@
package com.recipeapp.planning.dto;
import java.util.List;
import java.util.UUID;
public record SuggestionResponse(List<SuggestionItem> suggestions) {
public record SuggestionRecipe(UUID id, String name, String effort, short cookTimeMin) {}
public record SuggestionItem(
SuggestionRecipe recipe,
SlotResponse.SlotRecipe recipe,
double scoreDelta,
boolean hasConflict
) {}

View File

@@ -161,7 +161,7 @@ class WeekPlanControllerTest {
@Test
void getSuggestionsShouldReturn200() throws Exception {
var recipe = new SuggestionResponse.SuggestionRecipe(UUID.randomUUID(), "Stir Fry", "easy", (short) 15);
var recipe = new SlotResponse.SlotRecipe(UUID.randomUUID(), "Stir Fry", "easy", (short) 15, null);
var item = new SuggestionResponse.SuggestionItem(recipe, 1.5, false);
var response = new SuggestionResponse(List.of(item));

View File

@@ -0,0 +1,625 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Recipe App — C3 Abwechslungs-Analyse · Implementierungsspezifikation V1</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"/>
<style>
:root{
--color-page:#FAFAF7;--color-surface:#F5F4EE;--color-subtle:#EDECEA;
--color-border:#D8D7D0;--color-text-muted:#6B6A63;--color-text:#1C1C18;
--green-tint:#E8F5EA;--green-light:#AEDCB0;--green:#3D8C4A;--green-dark:#2E6E39;
--yellow-tint:#FDF6D8;--yellow-light:#F9E08A;--yellow:#F2C12E;--yellow-dark:#C49610;--yellow-text:#8A6800;
--blue-tint:#E6F1FB;--blue-light:#A4CFF4;--blue:#2D7DD2;--blue-dark:#185FA5;
--purple-tint:#EEEDFE;--purple-light:#CECBF6;--purple:#534AB7;--purple-dark:#3C3489;
--orange-tint:#FEF0E6;--orange:#E8862A;
--red-tint:#FDECEA;--red:#DC4C3E;--red-dark:#B03328;
--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;
--shadow-overlay:0 8px 32px rgba(28,28,24,.12),0 2px 8px rgba(28,28,24,.06);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
body{font-family:var(--font-sans);background:#DDDBD5;color:var(--color-text);font-size:14px;line-height:1.6;}
.doc{max-width:1100px;margin:0 auto;padding:48px 40px 120px;}
.doc-header{background:var(--color-page);border-radius:var(--radius-xl) var(--radius-xl) 0 0;padding:40px 40px 28px;margin:-48px -40px 48px;display:flex;justify-content:space-between;align-items:flex-end;border-bottom:1px solid var(--color-border);}
.doc-header h1{font-family:var(--font-display);font-size:26px;font-weight:500;letter-spacing:-.02em;margin-bottom:4px;}
.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;}
.pill{display:inline-block;padding:2px 8px;border-radius:var(--radius-sm);font-size:10px;font-weight:500;letter-spacing:.05em;}
.pill-ready{background:var(--green-tint);color:var(--green-dark);}
.pill-warn{background:var(--yellow-tint);color:var(--yellow-text);}
.section{margin-bottom:56px;}
.section-label{font-size:10px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--color-text-muted);padding-bottom:10px;border-bottom:1px solid var(--color-border);margin-bottom:20px;}
.prose{font-size:13px;color:var(--color-text-muted);line-height:1.7;max-width:720px;margin-bottom:16px;}
.prose strong{color:var(--color-text);font-weight:500;}
/* Code blocks */
.code{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:16px 20px;font-family:var(--font-mono);font-size:12px;line-height:1.7;overflow-x:auto;margin-bottom:16px;white-space:pre;}
.code .cm{color:var(--color-text-muted);}
.code .kw{color:var(--purple);}
.code .ty{color:var(--blue-dark);}
.code .st{color:var(--green-dark);}
.code .nu{color:var(--orange);}
/* Tables */
.tbl{width:100%;border-collapse:collapse;font-size:12px;background:var(--color-surface);border-radius:var(--radius-lg);overflow:hidden;border:1px solid var(--color-border);margin-bottom:16px;}
.tbl thead tr{background:var(--color-subtle);border-bottom:1px solid var(--color-border);}
.tbl th{text-align:left;padding:10px 14px;font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--color-text-muted);}
.tbl td{padding:9px 14px;border-bottom:1px solid var(--color-subtle);vertical-align:top;}
.tbl tr:last-child td{border-bottom:none;}
.tbl td:first-child{font-weight:500;color:var(--color-text-muted);white-space:nowrap;font-size:11px;}
.tbl td.mono{font-family:var(--font-mono);font-size:11px;}
/* Callout boxes */
.box{border-radius:var(--radius-lg);padding:16px 20px;margin-bottom:16px;}
.box-lbl{font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;margin-bottom:8px;}
.box ul{list-style:none;display:flex;flex-direction:column;gap:5px;}
.box li{font-size:12px;line-height:1.5;display:flex;align-items:flex-start;gap:8px;}
.box li::before{font-weight:500;flex-shrink:0;}
.box-y{background:var(--yellow-tint);border:1px solid var(--yellow-light);}
.box-y .box-lbl,.box-y li::before{color:var(--yellow-text);}
.box-y li{color:var(--yellow-text);}
.box-g{background:var(--green-tint);border:1px solid var(--green-light);}
.box-g .box-lbl,.box-g li::before{color:var(--green-dark);}
.box-g li{color:var(--green-dark);}
.box-b{background:var(--blue-tint);border:1px solid var(--blue-light);}
.box-b .box-lbl,.box-b li::before{color:var(--blue-dark);}
.box-b li{color:var(--blue-dark);}
.box ul.checks li::before{content:'✓';}
.box ul.arrows li::before{content:'→';}
/* State cards */
.state-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;margin-bottom:12px;}
.state-head{background:var(--color-subtle);padding:10px 16px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;}
.state-id{font-family:var(--font-mono);font-size:11px;font-weight:500;color:var(--color-text-muted);}
.state-title{font-size:13px;font-weight:500;}
.state-body{padding:14px 16px;font-size:12px;line-height:1.7;}
/* Device frames (compact preview) */
.prev-row{display:flex;gap:32px;align-items:flex-start;flex-wrap:wrap;margin-bottom:16px;}
.prev-col{display:flex;flex-direction:column;align-items:center;gap:8px;}
.bp-lbl{font-family:var(--font-mono);font-size:10px;color:var(--color-text-muted);}
.phone{width:300px;flex-shrink:0;background:var(--color-page);border-radius:32px;overflow:hidden;box-shadow:var(--shadow-overlay),0 0 0 1px rgba(0,0,0,.08);border:5px solid #1C1C18;}
.pst{padding:8px 16px 0;display:flex;justify-content:space-between;align-items:center;font-size:10px;background:var(--color-page);}
.pst b{font-weight:600;font-size:11px;}
/* Warning card preview */
.wcard{border-radius:8px;border:1px solid var(--yellow-light);background:var(--yellow-tint);overflow:hidden;margin-bottom:8px;}
.wcard:last-child{margin-bottom:0;}
.wcard-hd{padding:9px 14px;border-bottom:1px solid var(--yellow-light);}
.wcard-hd-t{font-size:13px;font-weight:500;color:var(--yellow-text);}
.wcard-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:9px 14px;border-bottom:1px solid rgba(249,224,138,.4);}
.wcard-row:last-child{border-bottom:none;}
.wcard-left{display:flex;align-items:center;gap:8px;min-width:0;}
.wcard-day{font-size:11px;font-weight:600;color:var(--yellow-text);width:20px;flex-shrink:0;}
.wcard-recipe{font-size:13px;color:var(--color-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wcard-swap{font-size:12px;font-weight:500;color:var(--yellow-text);white-space:nowrap;flex-shrink:0;}
.divider{border:none;border-top:1px solid var(--color-border);margin:40px 0;}
/* File diff style */
.diff{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);font-family:var(--font-mono);font-size:12px;line-height:1.6;overflow-x:auto;margin-bottom:16px;}
.diff-file{padding:8px 16px;background:var(--color-subtle);border-bottom:1px solid var(--color-border);font-size:11px;font-weight:500;color:var(--color-text-muted);}
.diff-body{padding:12px 16px;white-space:pre;}
.diff-add{color:var(--green-dark);background:rgba(61,140,74,.06);}
.diff-rem{color:var(--red-dark);background:rgba(220,76,62,.06);}
.diff-ctx{color:var(--color-text-muted);}
</style>
</head>
<body>
<div class="doc">
<!-- Header -->
<div class="doc-header">
<div>
<h1>C3 — Abwechslungs-Analyse · Implementierungsspezifikation</h1>
<p>Recipe App · Variation V1 "Erweiterte Karten" · Rezeptnamen + Tausch-Links in Warnkarten</p>
</div>
<div class="doc-meta">
<span class="pill pill-ready">Final</span><br>
Erstellt: 2026-04<br>
Screen: C3<br>
Bezug: c3-variety-rework.html
</div>
</div>
<!-- ── 1. ÜBERBLICK ── -->
<div class="section">
<div class="section-label">1 · Überblick</div>
<p class="prose">Die Seite <strong>/planner/variety</strong> zeigt derzeit Warnkarten mit technischen Tages-Codes (<code style="font-family:var(--font-mono);font-size:11px;">MON, WED — erwäge einen Tausch</code>). Der Planer muss manuell nachschlagen, welches Gericht an diesen Tagen eingeplant ist, und dann zurück zum Planer navigieren um es zu tauschen.</p>
<p class="prose"><strong>V1 "Erweiterte Karten"</strong> löst dies mit minimalem Umbauaufwand: Die Warnkarten erhalten eine strukturierte Zeile pro betroffenem Tag — mit Wochentag-Abkürzung, Rezeptname und direktem "Tauschen →" Link. Score-Hero, Bewertungsdetails und das Gesamt-Layout bleiben unverändert.</p>
<div class="box box-b">
<div class="box-lbl">Scope</div>
<ul class="arrows">
<li>Kein neues Backend-Endpoint — alle nötigen Daten sind bereits im weekPlan-Load vorhanden</li>
<li>Kein Layout-Umbau — nur VarietyWarningCards.svelte und die Datenvorbereitung in +page.svelte ändern sich</li>
<li>Protein-Grid und EffortBar bleiben wie bisher (Desktop)</li>
</ul>
</div>
</div>
<!-- ── 2. PROBLEM IM DETAIL ── -->
<div class="section">
<div class="section-label">2 · Aktueller Ist-Zustand und Problem</div>
<table class="tbl">
<thead><tr><th>Element</th><th>Aktuell</th><th>Soll (V1)</th></tr></thead>
<tbody>
<tr>
<td>Warnkarte Inhalt</td>
<td><code style="font-family:var(--font-mono);font-size:11px;">title + explanation (String)</code></td>
<td>Strukturierte Zeilen: Wochentag · Rezeptname · Tauschen-Link</td>
</tr>
<tr>
<td>Tages-Angabe</td>
<td>API-Code <code style="font-family:var(--font-mono);font-size:11px;">MON, WED</code></td>
<td>Abkürzung <code style="font-family:var(--font-mono);font-size:11px;">Mo, Mi</code></td>
</tr>
<tr>
<td>Rezeptname</td>
<td>Fehlt</td>
<td>Aus <code style="font-family:var(--font-mono);font-size:11px;">weekPlan.slots[].recipe.name</code></td>
</tr>
<tr>
<td>Tausch-Navigation</td>
<td>Fehlt — Nutzer verlässt die Seite manuell</td>
<td><code style="font-family:var(--font-mono);font-size:11px;">/planner?week={weekStart}&amp;swap={slotId}</code></td>
</tr>
<tr>
<td>Datenbasis</td>
<td><code style="font-family:var(--font-mono);font-size:11px;">computeWarnings()</code> aus variety.ts</td>
<td>Inline <code style="font-family:var(--font-mono);font-size:11px;">$derived.by()</code> in +page.svelte, direkt aus API-Daten</td>
</tr>
</tbody>
</table>
</div>
<!-- ── 3. DATENFLUSS ── -->
<div class="section">
<div class="section-label">3 · Datenfluss</div>
<p class="prose">Alle nötigen Daten werden bereits im Server-Load geladen. Kein neuer API-Call erforderlich.</p>
<table class="tbl">
<thead><tr><th>Quelle</th><th>Feld</th><th>Verwendung</th></tr></thead>
<tbody>
<tr>
<td>weekPlan.slots[]</td>
<td class="mono">{ id, dayOfWeek, recipe: { id, name } }</td>
<td>Aufbau der <code style="font-family:var(--font-mono);font-size:11px;">slotsByDay</code>-Map: DayCode → { slotId, recipeName }</td>
</tr>
<tr>
<td>varietyScore.tagRepeats[]</td>
<td class="mono">{ tagType, tagName, days: string[] }</td>
<td>Warnkarten für wiederholte Tags (Protein, Cuisine). days[] enthält API-Codes: "MON", "TUE" …</td>
</tr>
<tr>
<td>varietyScore.ingredientOverlaps[]</td>
<td class="mono">{ ingredientName, days: string[] }</td>
<td>Warnkarten für Zutaten-Überschneidungen</td>
</tr>
<tr>
<td>varietyScore.duplicatesInPlan[]</td>
<td class="mono">string[] (Rezeptnamen)</td>
<td>Warnkarte: "X doppelt geplant". Alle Slots mit diesem Rezeptnamen liefern die Items.</td>
</tr>
<tr>
<td>data.weekStart</td>
<td class="mono">string (YYYY-MM-DD)</td>
<td>Swap-URL-Parameter</td>
</tr>
</tbody>
</table>
<p class="prose">Tag-Code → Abkürzung Mapping (konstant):</p>
<div class="code"><span class="cm">// Day code → German short label</span>
<span class="kw">const</span> DAY_SHORT: Record&lt;<span class="ty">string</span>, <span class="ty">string</span>&gt; = {
MON: <span class="st">'Mo'</span>, TUE: <span class="st">'Di'</span>, WED: <span class="st">'Mi'</span>,
THU: <span class="st">'Do'</span>, FRI: <span class="st">'Fr'</span>, SAT: <span class="st">'Sa'</span>, SUN: <span class="st">'So'</span>
};</div>
</div>
<!-- ── 4. TYPEN ── -->
<div class="section">
<div class="section-label">4 · Typen</div>
<p class="prose">Die bestehende <code style="font-family:var(--font-mono);font-size:11px;">VarietyWarningCards.svelte</code> definiert bereits die korrekten Interfaces. Diese bleiben unverändert:</p>
<div class="code"><span class="cm">// In VarietyWarningCards.svelte (bereits vorhanden, nicht ändern)</span>
<span class="kw">interface</span> <span class="ty">WarningItem</span> {
dayShort: <span class="ty">string</span>; <span class="cm">// 'Mo', 'Di', …</span>
recipeName: <span class="ty">string</span>; <span class="cm">// aus weekPlan.slots[].recipe.name</span>
slotId: <span class="ty">number</span>; <span class="cm">// für Swap-Link</span>
}
<span class="kw">interface</span> <span class="ty">ActionWarning</span> {
title: <span class="ty">string</span>; <span class="cm">// z.B. "Tofu mehrfach diese Woche"</span>
items: <span class="ty">WarningItem</span>[]; <span class="cm">// eine Zeile pro betroffenem Tag</span>
}</div>
<p class="prose">Die alte <code style="font-family:var(--font-mono);font-size:11px;">Warning</code>-Schnittstelle aus <code style="font-family:var(--font-mono);font-size:11px;">variety.ts</code> (<code style="font-family:var(--font-mono);font-size:11px;">{ title, explanation }</code>) wird nicht mehr verwendet.</p>
</div>
<!-- ── 5. IMPLEMENTIERUNG ── -->
<div class="section">
<div class="section-label">5 · Implementierung</div>
<p class="prose">Es gibt drei Änderungen:</p>
<!-- 5.1 slotsByDay -->
<div class="state-card">
<div class="state-head">
<div class="state-id">5.1</div>
<div class="state-title">+page.svelte — slotsByDay Map aufbauen</div>
</div>
<div class="state-body">
<p style="margin-bottom:10px;">Füge direkt nach den bestehenden <code style="font-family:var(--font-mono);font-size:11px;">$derived</code>-Deklarationen hinzu:</p>
<div class="code" style="margin-bottom:0"><span class="kw">const</span> DAY_SHORT: Record&lt;<span class="ty">string</span>, <span class="ty">string</span>&gt; = {
MON: <span class="st">'Mo'</span>, TUE: <span class="st">'Di'</span>, WED: <span class="st">'Mi'</span>,
THU: <span class="st">'Do'</span>, FRI: <span class="st">'Fr'</span>, SAT: <span class="st">'Sa'</span>, SUN: <span class="st">'So'</span>
};
<span class="cm">// dayOfWeek (API code) → { slotId, recipeName }</span>
<span class="kw">let</span> slotsByDay = $derived.by(() => {
<span class="kw">const</span> map: Record&lt;<span class="ty">string</span>, { slotId: <span class="ty">number</span>; recipeName: <span class="ty">string</span> }&gt; = {};
<span class="kw">for</span> (<span class="kw">const</span> slot <span class="kw">of</span> weekPlan?.slots ?? []) {
<span class="kw">if</span> (slot.dayOfWeek && slot.recipe?.name && slot.id) {
map[slot.dayOfWeek] = { slotId: slot.id, recipeName: slot.recipe.name };
}
}
<span class="kw">return</span> map;
});</div>
</div>
</div>
<!-- 5.2 actionWarnings -->
<div class="state-card">
<div class="state-head">
<div class="state-id">5.2</div>
<div class="state-title">+page.svelte — actionWarnings ersetzen computeWarnings()</div>
</div>
<div class="state-body">
<p style="margin-bottom:10px;">Ersetze den bestehenden <code style="font-family:var(--font-mono);font-size:11px;">let warnings = $derived.by(() =&gt; computeWarnings(…))</code>-Block vollständig:</p>
<div class="code" style="margin-bottom:0"><span class="kw">interface</span> <span class="ty">WarningItem</span> { dayShort: <span class="ty">string</span>; recipeName: <span class="ty">string</span>; slotId: <span class="ty">number</span>; }
<span class="kw">interface</span> <span class="ty">ActionWarning</span> { title: <span class="ty">string</span>; items: <span class="ty">WarningItem</span>[]; }
<span class="kw">let</span> actionWarnings = $derived.by((): <span class="ty">ActionWarning</span>[] => {
<span class="kw">const</span> result: <span class="ty">ActionWarning</span>[] = [];
<span class="kw">const</span> vs = varietyScore;
<span class="kw">if</span> (!vs) <span class="kw">return</span> result;
<span class="cm">// Tag repeats (protein, cuisine, …)</span>
<span class="kw">for</span> (<span class="kw">const</span> repeat <span class="kw">of</span> vs.tagRepeats ?? []) {
<span class="kw">if</span> ((repeat.days?.length ?? <span class="nu">0</span>) &lt; <span class="nu">2</span>) <span class="kw">continue</span>;
<span class="kw">const</span> items: <span class="ty">WarningItem</span>[] = (repeat.days ?? [])
.map((day) => {
<span class="kw">const</span> slot = slotsByDay[day];
<span class="kw">return</span> slot
? { dayShort: DAY_SHORT[day] ?? day, recipeName: slot.recipeName, slotId: slot.slotId }
: <span class="kw">null</span>;
})
.filter((x): x is <span class="ty">WarningItem</span> => x !== <span class="kw">null</span>);
<span class="kw">if</span> (items.length > <span class="nu">0</span>) {
result.push({ title: `${repeat.tagName} mehrfach diese Woche`, items });
}
}
<span class="cm">// Ingredient overlaps</span>
<span class="kw">for</span> (<span class="kw">const</span> overlap <span class="kw">of</span> vs.ingredientOverlaps ?? []) {
<span class="kw">if</span> ((overlap.days?.length ?? <span class="nu">0</span>) &lt; <span class="nu">2</span>) <span class="kw">continue</span>;
<span class="kw">const</span> items: <span class="ty">WarningItem</span>[] = (overlap.days ?? [])
.map((day) => {
<span class="kw">const</span> slot = slotsByDay[day];
<span class="kw">return</span> slot
? { dayShort: DAY_SHORT[day] ?? day, recipeName: slot.recipeName, slotId: slot.slotId }
: <span class="kw">null</span>;
})
.filter((x): x is <span class="ty">WarningItem</span> => x !== <span class="kw">null</span>);
<span class="kw">if</span> (items.length > <span class="nu">0</span>) {
result.push({ title: `${overlap.ingredientName} in mehreren Gerichten`, items });
}
}
<span class="cm">// Duplicate recipes — find all slots with that recipe name</span>
<span class="kw">for</span> (<span class="kw">const</span> name <span class="kw">of</span> vs.duplicatesInPlan ?? []) {
<span class="kw">const</span> items: <span class="ty">WarningItem</span>[] = Object.entries(slotsByDay)
.filter(([, s]) => s.recipeName === name)
.map(([day, s]) => ({ dayShort: DAY_SHORT[day] ?? day, recipeName: s.recipeName, slotId: s.slotId }));
<span class="kw">if</span> (items.length > <span class="nu">0</span>) {
result.push({ title: `${name} doppelt geplant`, items });
}
}
<span class="kw">return</span> result;
});</div>
</div>
</div>
<!-- 5.3 Template update -->
<div class="state-card">
<div class="state-head">
<div class="state-id">5.3</div>
<div class="state-title">+page.svelte — Template: warnings → actionWarnings, weekStart übergeben</div>
</div>
<div class="state-body">
<p style="margin-bottom:10px;">An beiden Stellen im Template (Mobile + Desktop) ersetzen:</p>
<div class="diff">
<div class="diff-file">+page.svelte (Mobile, ~Zeile 110 / Desktop, ~Zeile 222)</div>
<div class="diff-body"><span class="diff-rem">- {#if warnings.length > 0}</span>
<span class="diff-rem">- &lt;VarietyWarningCards {warnings} /&gt;</span>
<span class="diff-add">+ {#if actionWarnings.length > 0}</span>
<span class="diff-add">+ &lt;VarietyWarningCards warnings={actionWarnings} {weekStart} /&gt;</span></div>
</div>
<p style="font-size:12px;color:var(--color-text-muted);">Achtung: <code style="font-family:var(--font-mono);font-size:11px;">weekStart</code> ist für die Swap-URL erforderlich und muss explizit übergeben werden.</p>
</div>
</div>
<!-- 5.4 Import cleanup -->
<div class="state-card">
<div class="state-head">
<div class="state-id">5.4</div>
<div class="state-title">+page.svelte — Import aufräumen</div>
</div>
<div class="state-body">
<p style="margin-bottom:10px;">Entferne den nicht mehr genutzten Import:</p>
<div class="diff">
<div class="diff-file">+page.svelte (Script-Block, oben)</div>
<div class="diff-body"><span class="diff-rem">- import { computeSubScores, computeWarnings } from '$lib/planner/variety';</span>
<span class="diff-add">+ import { computeSubScores } from '$lib/planner/variety';</span></div>
</div>
<p style="font-size:12px;color:var(--color-text-muted);">computeSubScores wird noch für die Score-Breakdown-Anzeige genutzt.</p>
</div>
</div>
</div>
<!-- ── 6. KOMPONENTE: VarietyWarningCards ── -->
<div class="section">
<div class="section-label">6 · VarietyWarningCards.svelte — bereits korrekt</div>
<p class="prose">Die Komponente wurde bereits auf das neue <code style="font-family:var(--font-mono);font-size:11px;">ActionWarning</code>-Format aktualisiert. <strong>Keine Änderung erforderlich.</strong> Zur Referenz die erwartete Props-Schnittstelle:</p>
<div class="code"><span class="cm">// Props (bereits implementiert)</span>
<span class="kw">let</span> { warnings, weekStart }: {
warnings: <span class="ty">ActionWarning</span>[];
weekStart: <span class="ty">string</span>;
} = $props();</div>
<p class="prose">Die Komponente rendert für jede Warnung:</p>
<ul style="font-size:12px;color:var(--color-text-muted);margin-left:20px;margin-bottom:16px;line-height:1.9;">
<li>Gelbe Karte (<code style="font-family:var(--font-mono);font-size:11px;">border: yellow-light, bg: yellow-tint</code>) mit Header-Zeile (Titel)</li>
<li>Pro Item: Zeile mit Wochentag-Abkürzung (W=20px, fixed) · Rezeptname (truncate) · "Tauschen →" Link (rechts)</li>
<li>Swap-URL: <code style="font-family:var(--font-mono);font-size:11px;">/planner?week={weekStart}&amp;swap={item.slotId}</code></li>
</ul>
<!-- Visual preview -->
<div class="prev-row">
<div class="prev-col">
<div class="bp-lbl">Warnkarte · Referenz-Darstellung</div>
<div style="width:340px;">
<div class="wcard">
<div class="wcard-hd"><div class="wcard-hd-t">Tofu mehrfach diese Woche</div></div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mo</span><span class="wcard-recipe">Tofu-Gemüse-Pfanne</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mi</span><span class="wcard-recipe">Tofu-Curry mit Reis</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
</div>
<div class="wcard">
<div class="wcard-hd"><div class="wcard-hd-t">Paprika in mehreren Gerichten</div></div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Di</span><span class="wcard-recipe">Paprika-Linsen-Eintopf</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mi</span><span class="wcard-recipe">Tofu-Curry mit Reis</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ── 7. EDGE CASES ── -->
<div class="section">
<div class="section-label">7 · Edge Cases</div>
<table class="tbl">
<thead><tr><th>Fall</th><th>Verhalten</th></tr></thead>
<tbody>
<tr>
<td>Tag im tagRepeat hat keinen Slot</td>
<td>Filter-Schritt (.filter(x => x !== null)) entfernt das Item. Warnkarte erscheint nur wenn ≥1 Item vorhanden.</td>
</tr>
<tr>
<td>weekPlan hat keine Slots (leere Woche)</td>
<td>slotsByDay ist {}, actionWarnings ist []. Keine Warnkarten sichtbar.</td>
</tr>
<tr>
<td>varietyScore ist null</td>
<td>Bestehende {#if !varietyScore}-Guard greift — actionWarnings wird nie gerendert.</td>
</tr>
<tr>
<td>Slot hat kein Rezept (slot.recipe === null)</td>
<td>slot.recipe?.name ist undefined → Slot wird nicht in slotsByDay aufgenommen.</td>
</tr>
<tr>
<td>duplicatesInPlan: Rezeptname kommt in slotsByDay nicht vor</td>
<td>items ist leer → Warnkarte wird nicht gepusht.</td>
</tr>
<tr>
<td>Unbekannter Tag-Code (z.B. zukünftige API-Erweiterung)</td>
<td>DAY_SHORT[day] ?? day — Fallback auf den rohen Code.</td>
</tr>
<tr>
<td>Sehr langer Rezeptname</td>
<td>CSS truncate auf .wcard-recipe — kein Überlauf, Swap-Link bleibt sichtbar.</td>
</tr>
</tbody>
</table>
</div>
<!-- ── 8. ABNAHMEKRITERIEN ── -->
<div class="section">
<div class="section-label">8 · Abnahmekriterien</div>
<div class="box box-g">
<div class="box-lbl">Acceptance Criteria</div>
<ul class="checks">
<li>AC-1: Warnkarte zeigt pro betroffenem Tag eine eigene Zeile (nicht mehr einen langen Erklärungstext)</li>
<li>AC-2: Jede Zeile enthält die deutsche Wochentag-Abkürzung (Mo, Di, Mi, Do, Fr, Sa, So)</li>
<li>AC-3: Jede Zeile enthält den Namen des eingeplanten Rezepts</li>
<li>AC-4: Jede Zeile enthält einen "Tauschen →" Link, der zu /planner?week={weekStart}&amp;swap={slotId} führt</li>
<li>AC-5: Tags mit nur einem betroffenen Tag (days.length &lt; 2) erzeugen keine Warnkarte</li>
<li>AC-6: Score-Hero, Bewertungsdetails und Protein-Grid (Desktop) bleiben unverändert</li>
<li>AC-7: Wenn varietyScore null ist, werden keine Warnkarten gerendert (leere-Woche-State bleibt)</li>
<li>AC-8: Der Import von computeWarnings ist entfernt, TypeScript kompiliert fehlerfrei</li>
<li>AC-9: Auf Mobilgerät sind Tausch-Links touch-freundlich (mind. 44px Zeilenhöhe)</li>
</ul>
</div>
<div class="box box-y">
<div class="box-lbl">Nicht in Scope</div>
<ul class="arrows">
<li>Neues Backend-Endpoint — alle Daten kommen aus dem bestehenden Load</li>
<li>Layout-Umbau der Seite — Score bleibt oben, Warnungen unten wie bisher</li>
<li>Protein-Grid oder EffortBar Änderungen</li>
<li>computeSubScores aus variety.ts — bleibt unverändert</li>
<li>Entfernen von computeWarnings aus variety.ts (Funktion bleibt, wird nur nicht mehr aufgerufen)</li>
</ul>
</div>
</div>
<!-- ── 9. DATEIEN ── -->
<div class="section">
<div class="section-label">9 · Betroffene Dateien</div>
<table class="tbl">
<thead><tr><th>Datei</th><th>Änderung</th></tr></thead>
<tbody>
<tr>
<td class="mono">frontend/src/routes/(app)/planner/variety/+page.svelte</td>
<td>DAY_SHORT-Konstante, slotsByDay-Derived, actionWarnings-Derived, Template-Update (2×), Import-Bereinigung</td>
</tr>
<tr>
<td class="mono">frontend/src/lib/planner/VarietyWarningCards.svelte</td>
<td>Keine — bereits auf ActionWarning-Format aktualisiert</td>
</tr>
<tr>
<td class="mono">frontend/src/lib/planner/variety.ts</td>
<td>Keine — computeWarnings bleibt (ungenutzt, aber nicht entfernen um Regressions-Risiko zu vermeiden)</td>
</tr>
</tbody>
</table>
</div>
<!-- ── LLM AGENT REGION ── -->
<div class="section">
<div class="section-label">LLM-Agent-Lesbereich</div>
<p class="prose">Dieser Abschnitt enthält maschinenlesbare Regeln für einen KI-Agenten der die Implementierung durchführt.</p>
<div class="code"><span class="cm">SCREEN: C3 /planner/variety
VARIATION: V1 "Erweiterte Karten"
STATUS: Final spec — ready for implementation
FILES TO MODIFY:
frontend/src/routes/(app)/planner/variety/+page.svelte
FILES NOT TO MODIFY:
frontend/src/lib/planner/VarietyWarningCards.svelte (already correct)
frontend/src/lib/planner/variety.ts (keep computeWarnings, remove only import)
STEP 1 — Add DAY_SHORT constant (in &lt;script&gt; block, after imports):
const DAY_SHORT: Record&lt;string, string&gt; = {
MON: 'Mo', TUE: 'Di', WED: 'Mi',
THU: 'Do', FRI: 'Fr', SAT: 'Sa', SUN: 'So'
};
STEP 2 — Add slotsByDay derived (after $derived declarations for weekPlan, etc.):
let slotsByDay = $derived.by(() => {
const map: Record&lt;string, { slotId: number; recipeName: string }&gt; = {};
for (const slot of weekPlan?.slots ?? []) {
if (slot.dayOfWeek && slot.recipe?.name && slot.id) {
map[slot.dayOfWeek] = { slotId: slot.id, recipeName: slot.recipe.name };
}
}
return map;
});
STEP 3 — Define inline interfaces + actionWarnings derived:
interface WarningItem { dayShort: string; recipeName: string; slotId: number; }
interface ActionWarning { title: string; items: WarningItem[]; }
let actionWarnings = $derived.by((): ActionWarning[] => {
const result: ActionWarning[] = [];
const vs = varietyScore;
if (!vs) return result;
for (const repeat of vs.tagRepeats ?? []) {
if ((repeat.days?.length ?? 0) &lt; 2) continue;
const items: WarningItem[] = (repeat.days ?? [])
.map((day) => {
const slot = slotsByDay[day];
return slot ? { dayShort: DAY_SHORT[day] ?? day, recipeName: slot.recipeName, slotId: slot.slotId } : null;
})
.filter((x): x is WarningItem => x !== null);
if (items.length &gt; 0) result.push({ title: `${repeat.tagName} mehrfach diese Woche`, items });
}
for (const overlap of vs.ingredientOverlaps ?? []) {
if ((overlap.days?.length ?? 0) &lt; 2) continue;
const items: WarningItem[] = (overlap.days ?? [])
.map((day) => {
const slot = slotsByDay[day];
return slot ? { dayShort: DAY_SHORT[day] ?? day, recipeName: slot.recipeName, slotId: slot.slotId } : null;
})
.filter((x): x is WarningItem => x !== null);
if (items.length &gt; 0) result.push({ title: `${overlap.ingredientName} in mehreren Gerichten`, items });
}
for (const name of vs.duplicatesInPlan ?? []) {
const items: WarningItem[] = Object.entries(slotsByDay)
.filter(([, s]) => s.recipeName === name)
.map(([day, s]) => ({ dayShort: DAY_SHORT[day] ?? day, recipeName: s.recipeName, slotId: s.slotId }));
if (items.length &gt; 0) result.push({ title: `${name} doppelt geplant`, items });
}
return result;
});
STEP 4 — Replace template occurrences (both mobile and desktop sections):
OLD: {#if warnings.length > 0} / &lt;VarietyWarningCards {warnings} /&gt;
NEW: {#if actionWarnings.length > 0} / &lt;VarietyWarningCards warnings={actionWarnings} {weekStart} /&gt;
STEP 5 — Fix import:
OLD: import { computeSubScores, computeWarnings } from '$lib/planner/variety';
NEW: import { computeSubScores } from '$lib/planner/variety';
INVARIANTS (do not change):
- VarietyScoreHero, ScoreBreakdownList, EffortBar remain untouched
- Desktop protein grid (proteinByDay) remains untouched
- Layout structure (score top, warnings bottom) stays identical
- No new server load or API calls</span></div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,790 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Recipe App — C3 Abwechslungs-Analyse · 3 Mockup-Variationen</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"/>
<style>
:root{
--color-page:#FAFAF7;--color-surface:#F5F4EE;--color-subtle:#EDECEA;
--color-border:#D8D7D0;--color-text-muted:#6B6A63;--color-text:#1C1C18;
--green-tint:#E8F5EA;--green-light:#AEDCB0;--green:#3D8C4A;--green-dark:#2E6E39;
--yellow-tint:#FDF6D8;--yellow-light:#F9E08A;--yellow:#F2C12E;--yellow-dark:#C49610;--yellow-text:#8A6800;
--blue-tint:#E6F1FB;--blue-light:#A4CFF4;--blue:#2D7DD2;--blue-dark:#185FA5;
--purple-tint:#EEEDFE;--purple-light:#CECBF6;--purple:#534AB7;--purple-dark:#3C3489;
--orange-tint:#FEF0E6;--orange:#E8862A;
--red-tint:#FDECEA;--red:#DC4C3E;--red-dark:#B03328;
--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;
--shadow-overlay:0 8px 32px rgba(28,28,24,.12),0 2px 8px rgba(28,28,24,.06);
--shadow-card:0 1px 3px rgba(28,28,24,.06),0 1px 2px rgba(28,28,24,.04);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
body{font-family:var(--font-sans);background:#DDDBD5;color:var(--color-text);font-size:14px;line-height:1.6;}
.doc{max-width:1200px;margin:0 auto;padding:48px 40px 120px;}
/* Header */
.doc-header{background:var(--color-page);border-radius:var(--radius-xl) var(--radius-xl) 0 0;padding:40px 40px 28px;margin:-48px -40px 48px;display:flex;justify-content:space-between;align-items:flex-end;border-bottom:1px solid var(--color-border);}
.doc-header h1{font-family:var(--font-display);font-size:26px;font-weight:500;letter-spacing:-.02em;margin-bottom:4px;}
.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;}
.pill{display:inline-block;padding:2px 8px;border-radius:var(--radius-sm);font-size:10px;font-weight:500;letter-spacing:.05em;}
.pill-draft{background:var(--yellow-tint);color:var(--yellow-text);}
.pill-rec{background:var(--green-tint);color:var(--green-dark);}
/* Section */
.section{margin-bottom:80px;}
.section-label{font-size:10px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--color-text-muted);padding-bottom:10px;border-bottom:1px solid var(--color-border);margin-bottom:24px;}
.prose{font-size:13px;color:var(--color-text-muted);line-height:1.7;max-width:720px;margin-bottom:24px;}
/* Variation header */
.var-head{padding:20px 24px;border-radius:var(--radius-xl);margin-bottom:32px;display:flex;align-items:flex-start;gap:16px;}
.var-num{font-family:var(--font-display);font-size:48px;font-weight:300;line-height:1;opacity:.5;flex-shrink:0;}
.var-id{font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;margin-bottom:4px;}
.var-title{font-family:var(--font-display);font-size:20px;font-weight:500;letter-spacing:-.02em;margin-bottom:6px;}
.var-desc{font-size:13px;line-height:1.6;max-width:600px;}
.var-y{background:var(--yellow-tint);border:1px solid var(--yellow-light);}
.var-y .var-num,.var-y .var-id{color:var(--yellow-dark);}
.var-y .var-desc{color:var(--yellow-text);}
.var-g{background:var(--green-tint);border:1px solid var(--green-light);}
.var-g .var-num,.var-g .var-id{color:var(--green);}
.var-g .var-desc{color:var(--green-dark);}
.var-p{background:var(--purple-tint);border:1px solid var(--purple-light);}
.var-p .var-num,.var-p .var-id{color:var(--purple);}
.var-p .var-desc{color:var(--purple-dark);}
/* Device frames */
.previews{display:flex;gap:40px;flex-wrap:wrap;justify-content:center;align-items:flex-start;margin-bottom:28px;}
.prev-col{display:flex;flex-direction:column;align-items:center;gap:10px;}
.bp-lbl{font-family:var(--font-mono);font-size:10px;color:var(--color-text-muted);}
.phone{width:320px;flex-shrink:0;background:var(--color-page);border-radius:36px;overflow:hidden;box-shadow:var(--shadow-overlay),0 0 0 1px rgba(0,0,0,.08);border:6px solid #1C1C18;}
.pst{padding:10px 20px 0;display:flex;justify-content:space-between;align-items:center;font-size:11px;background:var(--color-page);}
.pst b{font-weight:600;font-size:12px;}
.desk{width:100%;max-width:1040px;background:var(--color-page);border-radius:var(--radius-xl);overflow:hidden;box-shadow:var(--shadow-overlay),0 0 0 1px rgba(0,0,0,.06);display:flex;min-height:500px;}
/* Nav chrome */
.mtb{padding:10px 16px;background:var(--color-page);border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;}
.mtb-back{font-size:20px;color:var(--color-text-muted);line-height:1;}
.mtb-t{font-family:var(--font-display);font-size:18px;font-weight:300;letter-spacing:-.02em;flex:1;}
.mbt{border-top:1px solid var(--color-border);background:var(--color-surface);padding:8px 16px 28px;display:flex;justify-content:space-around;flex-shrink:0;}
.mt-i{display:flex;flex-direction:column;align-items:center;gap:2px;}
.mt-ic{width:20px;height:20px;border-radius:4px;background:var(--color-subtle);font-size:11px;display:flex;align-items:center;justify-content:center;}
.mt-i.a .mt-ic{background:var(--yellow-tint);}
.mt-l{font-size:9px;font-weight:500;color:var(--color-text-muted);}
.mt-i.a .mt-l{color:var(--yellow-text);}
/* Desktop sidebar */
.dsb{width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);display:flex;flex-direction:column;}
.dsb-logo{padding:20px 16px 16px;border-bottom:1px solid var(--color-border);}
.dsb-lm{display:flex;align-items:center;gap:8px;margin-bottom:2px;}
.dsb-ic{width:24px;height:24px;border-radius:5px;background:var(--green);font-size:12px;display:flex;align-items:center;justify-content:center;}
.dsb-nm{font-family:var(--font-display);font-size:16px;font-weight:500;letter-spacing:-.02em;}
.dsb-sub{font-size:10px;color:var(--color-text-muted);padding-left:32px;}
.dsb-nav{padding:12px 10px;flex:1;}
.dsb-nl{font-size:8px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 8px;margin-bottom:4px;}
.dsb-ni{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:var(--radius-md);font-size:13px;color:var(--color-text-muted);margin-bottom:2px;}
.dsb-ni.a{background:var(--yellow-tint);color:var(--yellow-text);font-weight:500;}
.dsb-nc{font-size:13px;width:18px;text-align:center;}
.dm{flex:1;display:flex;flex-direction:column;min-width:0;}
.dtb{padding:14px 24px;border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:8px;flex-shrink:0;}
.dtb-bc{font-size:12px;color:var(--color-text-muted);}
.dtb-t{font-family:var(--font-display);font-size:18px;font-weight:300;letter-spacing:-.02em;}
.dmc{padding:24px;flex:1;overflow-y:auto;}
/* Shared components */
.score-num{font-family:var(--font-display);font-weight:300;letter-spacing:-.02em;line-height:1;}
.prog{height:6px;border-radius:3px;background:var(--color-border);overflow:hidden;margin-top:8px;}
.prog-fill{height:100%;border-radius:3px;background:var(--yellow);}
/* Warning card styles */
.wcard{border-radius:var(--radius-lg);border:1px solid var(--yellow-light);background:var(--yellow-tint);overflow:hidden;margin-bottom:8px;}
.wcard:last-child{margin-bottom:0;}
.wcard-hd{padding:10px 14px;border-bottom:1px solid var(--yellow-light);}
.wcard-hd-t{font-size:13px;font-weight:500;color:var(--yellow-text);}
.wcard-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:9px 14px;border-bottom:1px solid rgba(249,224,138,.4);}
.wcard-row:last-child{border-bottom:none;}
.wcard-left{display:flex;align-items:baseline;gap:8px;min-width:0;}
.wcard-day{font-size:11px;font-weight:600;color:var(--yellow-text);width:20px;flex-shrink:0;}
.wcard-recipe{font-size:13px;color:var(--color-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wcard-swap{font-size:12px;font-weight:500;color:var(--yellow-text);white-space:nowrap;flex-shrink:0;}
.wcard-swap:hover{text-decoration:underline;}
/* Score breakdown rows */
.sb-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 0;border-bottom:1px solid var(--color-subtle);}
.sb-row:last-child{border-bottom:none;}
.sb-label{font-size:12px;color:var(--color-text-muted);}
.sb-val{font-family:var(--font-mono);font-size:12px;font-weight:500;}
/* Collapsible details */
.det summary{font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);cursor:default;list-style:none;display:flex;align-items:center;justify-content:space-between;}
.det summary::after{content:'▾';font-size:11px;}
.det[open] summary::after{content:'▴';}
.det-body{padding-top:8px;}
/* Notes block */
.notes{border-radius:var(--radius-lg);padding:16px 20px;margin-top:20px;}
.notes-lbl{font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;margin-bottom:8px;}
.notes ul{list-style:none;display:flex;flex-direction:column;gap:5px;}
.notes li{font-size:12px;line-height:1.5;display:flex;align-items:flex-start;gap:8px;}
.notes li::before{content:'→';font-weight:500;flex-shrink:0;}
.notes-y{background:var(--yellow-tint);border:1px solid var(--yellow-light);}
.notes-y .notes-lbl,.notes-y li::before{color:var(--yellow-text);}
.notes-y li{color:var(--yellow-text);}
.notes-g{background:var(--green-tint);border:1px solid var(--green-light);}
.notes-g .notes-lbl,.notes-g li::before{color:var(--green-dark);}
.notes-g li{color:var(--green-dark);}
.notes-p{background:var(--purple-tint);border:1px solid var(--purple-light);}
.notes-p .notes-lbl,.notes-p li::before{color:var(--purple-dark);}
.notes-p li{color:var(--purple-dark);}
.divider{border:none;border-top:1px solid var(--color-border);margin:48px 0;}
/* Comparison table */
.ct{width:100%;border-collapse:collapse;font-size:13px;background:var(--color-surface);border-radius:var(--radius-lg);overflow:hidden;border:1px solid var(--color-border);}
.ct thead tr{background:var(--color-subtle);border-bottom:1px solid var(--color-border);}
.ct th{text-align:left;padding:10px 16px;font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--color-text-muted);}
.ct td{padding:10px 16px;border-bottom:1px solid var(--color-subtle);font-size:12px;vertical-align:top;}
.ct tr:last-child td{border-bottom:none;}
.ct td:first-child{font-weight:500;font-size:11px;color:var(--color-text-muted);white-space:nowrap;}
</style>
</head>
<body>
<div class="doc">
<!-- Header -->
<div class="doc-header">
<div>
<h1>C3 — Abwechslungs-Analyse · Rework</h1>
<p>Recipe App · 3 Mockup-Variationen · Aktuell: technische Tages-Codes, keine Rezeptnamen, kein direkter Tausch</p>
</div>
<div class="doc-meta">
<span class="pill pill-draft">Entwurf</span><br>
Erstellt: 2026-04<br>
Variationen: 3<br>
Screen: C3
</div>
</div>
<!-- Context -->
<div class="section">
<div class="section-label">Problem</div>
<p class="prose">Die aktuelle Seite zeigt Warnungen wie <strong style="font-family:var(--font-mono);font-size:12px;">"MON, WED — erwäge einen Tausch"</strong>. Der Planer muss selbst nachschlagen, welches Gericht an Montag und Mittwoch geplant ist, und dann manuell zum Planer navigieren um zu tauschen. Zwei Probleme:</p>
<p class="prose"><strong>1. Keine Rezeptnamen</strong> — Tag-Codes statt echter Gerichte. <strong>2. Kein direkter Tausch</strong> — der Planer muss die Seite verlassen, zurück zum Planer, das richtige Gericht suchen und dann tauschen.</p>
</div>
<!-- ═══════════════════════════════════════
V1 — ERWEITERTE KARTEN
════════════════════════════════════════ -->
<div class="section">
<div class="var-head var-y">
<div class="var-num">V1</div>
<div>
<div class="var-id">Variation 1</div>
<div class="var-title">Erweiterte Karten</div>
<div class="var-desc">Minimale Änderung: bestehende gelbe Karten bleiben, aber der Text wird durch strukturierte Zeilen ersetzt — eine pro betroffenem Gericht, mit Wochentag, Rezeptname und "Tauschen →" Link. Score-Bereich und Layout bleiben unverändert.</div>
</div>
</div>
<div class="previews">
<!-- Mobile V1 -->
<div class="prev-col">
<div class="bp-lbl">Mobile · 320px</div>
<div class="phone">
<div class="pst"><b>9:41</b><span>●●●</span></div>
<!-- topbar -->
<div class="mtb">
<div class="mtb-back"></div>
<div class="mtb-t">Abwechslungs-Analyse</div>
</div>
<div style="flex:1;overflow-y:auto;padding:20px 16px 16px;">
<!-- Score hero -->
<div style="margin-bottom:24px;">
<div style="display:flex;align-items:baseline;gap:8px;">
<span class="score-num" style="font-size:56px;color:var(--color-text);">5.8</span>
<span style="font-size:16px;color:var(--color-text-muted);">/ 10</span>
<span style="font-size:14px;font-weight:500;color:var(--yellow-text);margin-left:4px;">Verbesserbar</span>
</div>
<div class="prog" style="width:120px;"><div class="prog-fill" style="width:58%;"></div></div>
</div>
<!-- Sub-scores -->
<div style="margin-bottom:24px;">
<div style="font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Bewertung im Detail</div>
<div class="sb-row"><span class="sb-label">Quellen-Vielfalt</span><span class="sb-val" style="color:var(--red-dark);">4 / 10</span></div>
<div class="sb-row"><span class="sb-label">Zutaten-Überschneidung</span><span class="sb-val" style="color:var(--yellow-text);">7 / 10</span></div>
<div class="sb-row"><span class="sb-label">Aufwandsbalance</span><span class="sb-val" style="color:var(--green-dark);">8 / 10</span></div>
</div>
<!-- Warnings — V1 style: same card structure, but rows inside -->
<div style="font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Hinweise</div>
<div class="wcard">
<div class="wcard-hd"><div class="wcard-hd-t">Tofu mehrfach diese Woche</div></div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mo</span><span class="wcard-recipe">Tofu-Gemüse-Pfanne</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mi</span><span class="wcard-recipe">Tofu-Curry mit Reis</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
</div>
<div class="wcard">
<div class="wcard-hd"><div class="wcard-hd-t">Paprika in mehreren Gerichten</div></div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Di</span><span class="wcard-recipe">Paprika-Linsen-Eintopf</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mi</span><span class="wcard-recipe">Tofu-Curry mit Reis</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
</div>
</div>
<div class="mbt">
<div class="mt-i"><div class="mt-ic">📅</div><div class="mt-l">Plan</div></div>
<div class="mt-i"><div class="mt-ic">🛒</div><div class="mt-l">Einkauf</div></div>
<div class="mt-i"><div class="mt-ic">🍳</div><div class="mt-l">Rezepte</div></div>
<div class="mt-i a"><div class="mt-ic">📊</div><div class="mt-l">Analyse</div></div>
</div>
</div>
</div>
<!-- Desktop V1 -->
<div class="prev-col" style="flex:1;min-width:580px;">
<div class="bp-lbl">Desktop · 1040px</div>
<div class="desk">
<div class="dsb">
<div class="dsb-logo"><div class="dsb-lm"><div class="dsb-ic">🥦</div><div class="dsb-nm">Mealprep</div></div><div class="dsb-sub">Familie Raddatz</div></div>
<div class="dsb-nav">
<div class="dsb-nl">Planung</div>
<div class="dsb-ni a"><span class="dsb-nc">📅</span> Wochenplan</div>
<div class="dsb-ni"><span class="dsb-nc">🛒</span> Einkaufsliste</div>
<div class="dsb-ni"><span class="dsb-nc">🍳</span> Rezepte</div>
<div class="dsb-nl" style="margin-top:12px;">Haushalt</div>
<div class="dsb-ni"><span class="dsb-nc">⚙️</span> Einstellungen</div>
</div>
</div>
<div class="dm">
<div class="dtb">
<span class="dtb-bc">Planer /</span>
<span class="dtb-t">Abwechslungs-Analyse</span>
</div>
<div class="dmc">
<!-- 2-col: left score + breakdown, right warnings -->
<div style="display:flex;gap:32px;">
<!-- Left -->
<div style="flex:1;">
<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:12px;">
<span class="score-num" style="font-size:72px;color:var(--color-text);">5.8</span>
<span style="font-size:18px;color:var(--color-text-muted);">/ 10</span>
<span style="font-size:14px;font-weight:500;color:var(--yellow-text);margin-left:4px;">Verbesserbar</span>
</div>
<div class="prog" style="width:200px;"><div class="prog-fill" style="width:58%;"></div></div>
<div style="margin-top:20px;">
<div style="font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Bewertung im Detail</div>
<div class="sb-row"><span class="sb-label">Quellen-Vielfalt</span><span class="sb-val" style="color:var(--red-dark);">4 / 10</span></div>
<div class="sb-row"><span class="sb-label">Zutaten-Überschneidung</span><span class="sb-val" style="color:var(--yellow-text);">7 / 10</span></div>
<div class="sb-row"><span class="sb-label">Aufwandsbalance</span><span class="sb-val" style="color:var(--green-dark);">8 / 10</span></div>
</div>
</div>
<!-- Right: warnings -->
<div style="width:340px;flex-shrink:0;">
<div style="font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Hinweise</div>
<div class="wcard">
<div class="wcard-hd"><div class="wcard-hd-t">Tofu mehrfach diese Woche</div></div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mo</span><span class="wcard-recipe">Tofu-Gemüse-Pfanne</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mi</span><span class="wcard-recipe">Tofu-Curry mit Reis</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
</div>
<div class="wcard">
<div class="wcard-hd"><div class="wcard-hd-t">Paprika in mehreren Gerichten</div></div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Di</span><span class="wcard-recipe">Paprika-Linsen-Eintopf</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mi</span><span class="wcard-recipe">Tofu-Curry mit Reis</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes notes-y">
<div class="notes-lbl">Design-Notizen V1</div>
<ul>
<li>Geringster Umbauaufwand — nur VarietyWarningCards.svelte ändert sich, keine Layout-Umstrukturierung.</li>
<li>Behält die bekannte Score-Hierarchie bei: Zahl oben, dann Detail, dann Hinweise.</li>
<li>Schwachstelle: Hinweise sind trotzdem am Ende der Seite versteckt — auf kurzen Telefon-Bildschirmen muss gescrollt werden, bevor der Planer die Tausch-Links sieht.</li>
<li>Die Sub-Scores bleiben immer sichtbar, auch wenn der Planer nur die Tausch-Aktionen braucht.</li>
</ul>
</div>
</div>
<hr class="divider"/>
<!-- ═══════════════════════════════════════
V2 — AKTIONS-LISTE (EMPFOHLEN)
════════════════════════════════════════ -->
<div class="section">
<div class="var-head var-g">
<div class="var-num">V2</div>
<div>
<div class="var-id">Variation 2 · Empfohlen</div>
<div class="var-title">Aktions-Liste</div>
<div class="var-desc">Hinweise rücken nach oben — direkt unter dem Score. Der Planer sieht sofort, was zu tun ist. Sub-Scores wandern in ein ausklappbares "Bewertung im Detail" (native &lt;details&gt;, kein JS). Kompakterer Score-Hero gibt Hinweisen mehr Raum.</div>
</div>
</div>
<div class="previews">
<!-- Mobile V2 -->
<div class="prev-col">
<div class="bp-lbl">Mobile · 320px</div>
<div class="phone">
<div class="pst"><b>9:41</b><span>●●●</span></div>
<div class="mtb">
<div class="mtb-back"></div>
<div class="mtb-t">Abwechslungs-Analyse</div>
</div>
<div style="flex:1;overflow-y:auto;padding:16px;">
<!-- Compact score strip -->
<div style="display:flex;align-items:center;gap:12px;padding:14px 16px;background:var(--color-surface);border-radius:var(--radius-lg);border:1px solid var(--color-border);margin-bottom:16px;">
<div>
<span class="score-num" style="font-size:40px;color:var(--color-text);">5.8</span>
<span style="font-size:13px;color:var(--color-text-muted);margin-left:4px;">/ 10</span>
</div>
<div style="flex:1;">
<div style="font-size:12px;font-weight:500;color:var(--yellow-text);margin-bottom:4px;">Verbesserbar</div>
<div class="prog"><div class="prog-fill" style="width:58%;"></div></div>
</div>
</div>
<!-- Warnings — primary content -->
<div style="font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:8px;">2 Hinweise</div>
<div class="wcard">
<div class="wcard-hd"><div class="wcard-hd-t">Tofu mehrfach diese Woche</div></div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mo</span><span class="wcard-recipe">Tofu-Gemüse-Pfanne</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mi</span><span class="wcard-recipe">Tofu-Curry mit Reis</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
</div>
<div class="wcard" style="margin-bottom:16px;">
<div class="wcard-hd"><div class="wcard-hd-t">Paprika in mehreren Gerichten</div></div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Di</span><span class="wcard-recipe">Paprika-Linsen-Eintopf</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mi</span><span class="wcard-recipe">Tofu-Curry mit Reis</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
</div>
<!-- Sub-scores — collapsed -->
<details class="det" style="border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:10px 14px;background:var(--color-surface);">
<summary>Bewertung im Detail</summary>
<div class="det-body">
<div class="sb-row"><span class="sb-label">Quellen-Vielfalt</span><span class="sb-val" style="color:var(--red-dark);">4 / 10</span></div>
<div class="sb-row"><span class="sb-label">Zutaten-Überschneidung</span><span class="sb-val" style="color:var(--yellow-text);">7 / 10</span></div>
<div class="sb-row"><span class="sb-label">Aufwandsbalance</span><span class="sb-val" style="color:var(--green-dark);">8 / 10</span></div>
</div>
</details>
</div>
<div class="mbt">
<div class="mt-i"><div class="mt-ic">📅</div><div class="mt-l">Plan</div></div>
<div class="mt-i"><div class="mt-ic">🛒</div><div class="mt-l">Einkauf</div></div>
<div class="mt-i"><div class="mt-ic">🍳</div><div class="mt-l">Rezepte</div></div>
<div class="mt-i a"><div class="mt-ic">📊</div><div class="mt-l">Analyse</div></div>
</div>
</div>
</div>
<!-- Desktop V2 -->
<div class="prev-col" style="flex:1;min-width:580px;">
<div class="bp-lbl">Desktop · 1040px</div>
<div class="desk">
<div class="dsb">
<div class="dsb-logo"><div class="dsb-lm"><div class="dsb-ic">🥦</div><div class="dsb-nm">Mealprep</div></div><div class="dsb-sub">Familie Raddatz</div></div>
<div class="dsb-nav">
<div class="dsb-nl">Planung</div>
<div class="dsb-ni a"><span class="dsb-nc">📅</span> Wochenplan</div>
<div class="dsb-ni"><span class="dsb-nc">🛒</span> Einkaufsliste</div>
<div class="dsb-ni"><span class="dsb-nc">🍳</span> Rezepte</div>
<div class="dsb-nl" style="margin-top:12px;">Haushalt</div>
<div class="dsb-ni"><span class="dsb-nc">⚙️</span> Einstellungen</div>
</div>
</div>
<div class="dm">
<div class="dtb">
<span class="dtb-bc">Planer /</span>
<span class="dtb-t">Abwechslungs-Analyse</span>
</div>
<div class="dmc">
<!-- Top: compact score strip + effort -->
<div style="display:flex;gap:16px;align-items:center;padding:16px;background:var(--color-surface);border-radius:var(--radius-lg);border:1px solid var(--color-border);margin-bottom:24px;">
<div>
<span class="score-num" style="font-size:52px;color:var(--color-text);">5.8</span>
<span style="font-size:14px;color:var(--color-text-muted);margin-left:6px;">/ 10</span>
</div>
<div style="flex:1;">
<div style="font-size:13px;font-weight:500;color:var(--yellow-text);margin-bottom:4px;">Verbesserbar — 2 Hinweise</div>
<div class="prog"><div class="prog-fill" style="width:58%;"></div></div>
</div>
<!-- Sub-scores inline on desktop -->
<div style="border-left:1px solid var(--color-border);padding-left:16px;display:flex;gap:16px;">
<div style="text-align:center;">
<div style="font-family:var(--font-display);font-size:20px;font-weight:300;color:var(--red-dark);">4</div>
<div style="font-size:10px;color:var(--color-text-muted);">Quellen</div>
</div>
<div style="text-align:center;">
<div style="font-family:var(--font-display);font-size:20px;font-weight:300;color:var(--yellow-text);">7</div>
<div style="font-size:10px;color:var(--color-text-muted);">Zutaten</div>
</div>
<div style="text-align:center;">
<div style="font-family:var(--font-display);font-size:20px;font-weight:300;color:var(--green-dark);">8</div>
<div style="font-size:10px;color:var(--color-text-muted);">Aufwand</div>
</div>
</div>
</div>
<!-- Warnings full-width -->
<div style="font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Hinweise</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="wcard">
<div class="wcard-hd"><div class="wcard-hd-t">Tofu mehrfach diese Woche</div></div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mo</span><span class="wcard-recipe">Tofu-Gemüse-Pfanne</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mi</span><span class="wcard-recipe">Tofu-Curry mit Reis</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
</div>
<div class="wcard">
<div class="wcard-hd"><div class="wcard-hd-t">Paprika in mehreren Gerichten</div></div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Di</span><span class="wcard-recipe">Paprika-Linsen-Eintopf</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
<div class="wcard-row">
<div class="wcard-left"><span class="wcard-day">Mi</span><span class="wcard-recipe">Tofu-Curry mit Reis</span></div>
<span class="wcard-swap">Tauschen →</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes notes-g">
<div class="notes-lbl">Design-Notizen V2</div>
<ul>
<li>Hinweise erscheinen direkt unter dem Score — kein Scrollen nötig auf typischen Telefon-Bildschirmen.</li>
<li>Kompakter Score-Strip auf Mobile spart ~80px gegenüber dem aktuellen großen Hero — mehr Raum für die eigentlichen Tausch-Aktionen.</li>
<li>Desktop: Sub-Scores werden als kompakte Zahlen-Spalte in die Score-Leiste integriert — kein separater Abschnitt mehr nötig.</li>
<li>Native &lt;details&gt; auf Mobile braucht kein JavaScript; funktioniert auch ohne hydration.</li>
<li>"2 Hinweise" im Score-Strip auf Desktop gibt dem Planer sofort Kontext, ohne zu scrollen.</li>
</ul>
</div>
</div>
<hr class="divider"/>
<!-- ═══════════════════════════════════════
V3 — HINWEISE ZUERST
════════════════════════════════════════ -->
<div class="section">
<div class="var-head var-p">
<div class="var-num">V3</div>
<div>
<div class="var-id">Variation 3</div>
<div class="var-title">Hinweise zuerst</div>
<div class="var-desc">Invertiertes Layout: die Seite öffnet mit den konkreten Problem-Karten — groß und klar. Score und Breakdown erscheinen darunter als unterstützende Information. Jede Warnung ist eine eigenständige "Aufgaben-Karte" mit prominentem Tausch-Button statt Link.</div>
</div>
</div>
<div class="previews">
<!-- Mobile V3 -->
<div class="prev-col">
<div class="bp-lbl">Mobile · 320px</div>
<div class="phone">
<div class="pst"><b>9:41</b><span>●●●</span></div>
<div class="mtb">
<div class="mtb-back"></div>
<div class="mtb-t">Abwechslungs-Analyse</div>
</div>
<div style="flex:1;overflow-y:auto;padding:16px;">
<!-- Problem cards — full width, prominent -->
<div style="font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Was zu tun ist</div>
<!-- Problem card 1 -->
<div style="border:1px solid var(--yellow-light);border-radius:var(--radius-xl);overflow:hidden;margin-bottom:10px;background:var(--yellow-tint);">
<div style="padding:12px 14px;border-bottom:1px solid var(--yellow-light);">
<div style="font-size:10px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;color:var(--yellow-text);margin-bottom:2px;">Quellen-Wiederholung</div>
<div style="font-size:14px;font-weight:500;color:var(--color-text);">Tofu an 2 Tagen</div>
</div>
<!-- Row 1 -->
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px;border-bottom:1px solid rgba(249,224,138,.5);">
<div style="display:flex;flex-direction:column;gap:1px;">
<div style="font-size:10px;font-weight:600;color:var(--yellow-text);">Montag</div>
<div style="font-size:13px;font-weight:500;color:var(--color-text);">Tofu-Gemüse-Pfanne</div>
</div>
<a href="#" style="font-size:12px;font-weight:600;padding:6px 12px;background:var(--yellow-text);color:#fff;border-radius:var(--radius-md);white-space:nowrap;text-decoration:none;">Tauschen</a>
</div>
<!-- Row 2 -->
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px;">
<div style="display:flex;flex-direction:column;gap:1px;">
<div style="font-size:10px;font-weight:600;color:var(--yellow-text);">Mittwoch</div>
<div style="font-size:13px;font-weight:500;color:var(--color-text);">Tofu-Curry mit Reis</div>
</div>
<a href="#" style="font-size:12px;font-weight:600;padding:6px 12px;background:var(--yellow-text);color:#fff;border-radius:var(--radius-md);white-space:nowrap;text-decoration:none;">Tauschen</a>
</div>
</div>
<!-- Problem card 2 -->
<div style="border:1px solid var(--yellow-light);border-radius:var(--radius-xl);overflow:hidden;margin-bottom:16px;background:var(--yellow-tint);">
<div style="padding:12px 14px;border-bottom:1px solid var(--yellow-light);">
<div style="font-size:10px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;color:var(--yellow-text);margin-bottom:2px;">Zutaten-Überschneidung</div>
<div style="font-size:14px;font-weight:500;color:var(--color-text);">Paprika an 2 aufeinanderfolgenden Tagen</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px;border-bottom:1px solid rgba(249,224,138,.5);">
<div style="display:flex;flex-direction:column;gap:1px;">
<div style="font-size:10px;font-weight:600;color:var(--yellow-text);">Dienstag</div>
<div style="font-size:13px;font-weight:500;color:var(--color-text);">Paprika-Linsen-Eintopf</div>
</div>
<a href="#" style="font-size:12px;font-weight:600;padding:6px 12px;background:var(--yellow-text);color:#fff;border-radius:var(--radius-md);white-space:nowrap;text-decoration:none;">Tauschen</a>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px;">
<div style="display:flex;flex-direction:column;gap:1px;">
<div style="font-size:10px;font-weight:600;color:var(--yellow-text);">Mittwoch</div>
<div style="font-size:13px;font-weight:500;color:var(--color-text);">Tofu-Curry mit Reis</div>
</div>
<a href="#" style="font-size:12px;font-weight:600;padding:6px 12px;background:var(--yellow-text);color:#fff;border-radius:var(--radius-md);white-space:nowrap;text-decoration:none;">Tauschen</a>
</div>
</div>
<!-- Score — secondary, at bottom -->
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:14px 16px;">
<div style="font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:8px;">Gesamt-Score</div>
<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:8px;">
<span class="score-num" style="font-size:36px;color:var(--color-text);">5.8</span>
<span style="font-size:13px;color:var(--color-text-muted);">/ 10 · Verbesserbar</span>
</div>
<div class="prog"><div class="prog-fill" style="width:58%;"></div></div>
<div class="det" style="margin-top:10px;">
<details>
<summary style="font-size:11px;color:var(--color-text-muted);cursor:default;">Aufschlüsselung anzeigen</summary>
<div style="padding-top:8px;">
<div class="sb-row"><span class="sb-label">Quellen-Vielfalt</span><span class="sb-val" style="color:var(--red-dark);">4 / 10</span></div>
<div class="sb-row"><span class="sb-label">Zutaten-Überschneidung</span><span class="sb-val" style="color:var(--yellow-text);">7 / 10</span></div>
<div class="sb-row"><span class="sb-label">Aufwandsbalance</span><span class="sb-val" style="color:var(--green-dark);">8 / 10</span></div>
</div>
</details>
</div>
</div>
</div>
<div class="mbt">
<div class="mt-i"><div class="mt-ic">📅</div><div class="mt-l">Plan</div></div>
<div class="mt-i"><div class="mt-ic">🛒</div><div class="mt-l">Einkauf</div></div>
<div class="mt-i"><div class="mt-ic">🍳</div><div class="mt-l">Rezepte</div></div>
<div class="mt-i a"><div class="mt-ic">📊</div><div class="mt-l">Analyse</div></div>
</div>
</div>
</div>
<!-- Desktop V3 -->
<div class="prev-col" style="flex:1;min-width:580px;">
<div class="bp-lbl">Desktop · 1040px</div>
<div class="desk">
<div class="dsb">
<div class="dsb-logo"><div class="dsb-lm"><div class="dsb-ic">🥦</div><div class="dsb-nm">Mealprep</div></div><div class="dsb-sub">Familie Raddatz</div></div>
<div class="dsb-nav">
<div class="dsb-nl">Planung</div>
<div class="dsb-ni a"><span class="dsb-nc">📅</span> Wochenplan</div>
<div class="dsb-ni"><span class="dsb-nc">🛒</span> Einkaufsliste</div>
<div class="dsb-ni"><span class="dsb-nc">🍳</span> Rezepte</div>
<div class="dsb-nl" style="margin-top:12px;">Haushalt</div>
<div class="dsb-ni"><span class="dsb-nc">⚙️</span> Einstellungen</div>
</div>
</div>
<div class="dm">
<div class="dtb">
<span class="dtb-bc">Planer /</span>
<span class="dtb-t">Abwechslungs-Analyse</span>
</div>
<div class="dmc">
<div style="display:grid;grid-template-columns:1fr 240px;gap:24px;">
<!-- Left: problem cards -->
<div>
<div style="font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Was zu tun ist</div>
<div style="border:1px solid var(--yellow-light);border-radius:var(--radius-xl);overflow:hidden;margin-bottom:10px;background:var(--yellow-tint);">
<div style="padding:10px 16px;border-bottom:1px solid var(--yellow-light);">
<div style="font-size:10px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;color:var(--yellow-text);margin-bottom:1px;">Quellen-Wiederholung</div>
<div style="font-size:14px;font-weight:500;color:var(--color-text);">Tofu an 2 Tagen</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 16px;border-bottom:1px solid rgba(249,224,138,.5);">
<div style="display:flex;align-items:baseline;gap:10px;">
<span style="font-size:11px;font-weight:600;color:var(--yellow-text);width:60px;">Montag</span>
<span style="font-size:13px;font-weight:500;color:var(--color-text);">Tofu-Gemüse-Pfanne</span>
</div>
<a href="#" style="font-size:12px;font-weight:600;padding:6px 14px;background:var(--yellow-text);color:#fff;border-radius:var(--radius-md);white-space:nowrap;text-decoration:none;">Tauschen</a>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 16px;">
<div style="display:flex;align-items:baseline;gap:10px;">
<span style="font-size:11px;font-weight:600;color:var(--yellow-text);width:60px;">Mittwoch</span>
<span style="font-size:13px;font-weight:500;color:var(--color-text);">Tofu-Curry mit Reis</span>
</div>
<a href="#" style="font-size:12px;font-weight:600;padding:6px 14px;background:var(--yellow-text);color:#fff;border-radius:var(--radius-md);white-space:nowrap;text-decoration:none;">Tauschen</a>
</div>
</div>
<div style="border:1px solid var(--yellow-light);border-radius:var(--radius-xl);overflow:hidden;background:var(--yellow-tint);">
<div style="padding:10px 16px;border-bottom:1px solid var(--yellow-light);">
<div style="font-size:10px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;color:var(--yellow-text);margin-bottom:1px;">Zutaten-Überschneidung</div>
<div style="font-size:14px;font-weight:500;color:var(--color-text);">Paprika an 2 aufeinanderfolgenden Tagen</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 16px;border-bottom:1px solid rgba(249,224,138,.5);">
<div style="display:flex;align-items:baseline;gap:10px;">
<span style="font-size:11px;font-weight:600;color:var(--yellow-text);width:60px;">Dienstag</span>
<span style="font-size:13px;font-weight:500;color:var(--color-text);">Paprika-Linsen-Eintopf</span>
</div>
<a href="#" style="font-size:12px;font-weight:600;padding:6px 14px;background:var(--yellow-text);color:#fff;border-radius:var(--radius-md);white-space:nowrap;text-decoration:none;">Tauschen</a>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 16px;">
<div style="display:flex;align-items:baseline;gap:10px;">
<span style="font-size:11px;font-weight:600;color:var(--yellow-text);width:60px;">Mittwoch</span>
<span style="font-size:13px;font-weight:500;color:var(--color-text);">Tofu-Curry mit Reis</span>
</div>
<a href="#" style="font-size:12px;font-weight:600;padding:6px 14px;background:var(--yellow-text);color:#fff;border-radius:var(--radius-md);white-space:nowrap;text-decoration:none;">Tauschen</a>
</div>
</div>
</div>
<!-- Right: score panel -->
<div>
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:16px;margin-bottom:12px;">
<div style="font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:8px;">Score</div>
<div style="display:flex;align-items:baseline;gap:6px;margin-bottom:8px;">
<span class="score-num" style="font-size:40px;color:var(--color-text);">5.8</span>
<span style="font-size:13px;color:var(--color-text-muted);">/ 10</span>
</div>
<div style="font-size:12px;font-weight:500;color:var(--yellow-text);margin-bottom:6px;">Verbesserbar</div>
<div class="prog"><div class="prog-fill" style="width:58%;"></div></div>
</div>
<div style="background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:16px;">
<div style="font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:10px;">Aufschlüsselung</div>
<div class="sb-row"><span class="sb-label">Quellen</span><span class="sb-val" style="color:var(--red-dark);">4 / 10</span></div>
<div class="sb-row"><span class="sb-label">Zutaten</span><span class="sb-val" style="color:var(--yellow-text);">7 / 10</span></div>
<div class="sb-row"><span class="sb-label">Aufwand</span><span class="sb-val" style="color:var(--green-dark);">8 / 10</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes notes-p">
<div class="notes-lbl">Design-Notizen V3</div>
<ul>
<li>Klarer Fokus: Das erste, was der Planer sieht, ist "Was zu tun ist" — keine Score-Hierarchie die von der Aktion ablenkt.</li>
<li>Prominente "Tauschen"-Buttons (gefüllt, dunkelgelb) statt Links — erhöht die Tipp-Fläche auf Mobile und macht die Aktion offensichtlicher.</li>
<li>Voller Wochentag ("Montag" statt "Mo") — lesbarer, besonders auf Desktop.</li>
<li>Schwachstelle: Wenn es keine Hinweise gibt (Score ≥ 9), wirkt die Seite leer — der Score müsste dann nach oben rücken. Erfordert einen separaten Empty-State.</li>
<li>Höherer Umbauaufwand gegenüber V1 und V2 — die Page-Struktur ändert sich grundlegend.</li>
</ul>
</div>
</div>
<hr class="divider"/>
<!-- Comparison -->
<div class="section">
<div class="section-label">Vergleich</div>
<table class="ct">
<thead>
<tr>
<th>Kriterium</th>
<th style="color:var(--yellow-text);">V1 Erweiterte Karten</th>
<th style="color:var(--green-dark);">V2 Aktions-Liste ★</th>
<th style="color:var(--purple-dark);">V3 Hinweise zuerst</th>
</tr>
</thead>
<tbody>
<tr>
<td>Rezeptnamen sichtbar</td>
<td>✓ Ja</td>
<td>✓ Ja</td>
<td>✓ Ja, prominent</td>
</tr>
<tr>
<td>Direkter Tausch</td>
<td>Link</td>
<td>Link</td>
<td>Button (größere Tap-Fläche)</td>
</tr>
<tr>
<td>Hinweise sichtbar ohne Scrollen</td>
<td>Nein (Score + Breakdown zuerst)</td>
<td>Ja (direkt unter kompaktem Score)</td>
<td>Ja (ganz oben)</td>
</tr>
<tr>
<td>Umbauaufwand</td>
<td>Niedrig</td>
<td>Mittel</td>
<td>Hoch</td>
</tr>
<tr>
<td>Layout-Änderung</td>
<td>Keine</td>
<td>Score kompakter, Details kollabierbar</td>
<td>Grundlegende Umstrukturierung</td>
</tr>
<tr>
<td>Empfehlung</td>
<td>Wenn schnelle Lieferung Prio</td>
<td><strong>Empfohlen ★</strong></td>
<td>Wenn Aktions-Fokus Prio</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,700 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>E1 — Einstellungen · Kachel-Ansicht · Finale Spezifikation</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: E1 Einstellungen Kachel-Ansicht, Finale Spezifikation
version: 1.0
journey: J8 Edit pantry staples
routes: /settings (E1 hub) → /household/staples?ctx=settings (D3)
screens: E1, D3
chosen-variation: V2 Kachel-Ansicht (Card sections)
last-updated: 2026-04-09
NAVIGATION STRUCTURE:
E1 (/settings) → Hub with 3 cards:
Card 1 "Vorräte" → navigates to D3 (/household/staples?ctx=settings)
Card 2 "Mitglieder" → navigates to E2 (/members)
Card 3 "Profil" → navigates to /profile (not yet implemented)
DATA:
Vorräte count: derived from GET /v1/ingredient-categories response
(count ingredients where isStaple === true)
Mitglieder count: from layout data (locals.haushalt via GET /v1/households/mine/members)
Profil name/email: from locals.benutzer
NOTE: D3 = A3. StaplesManager component is reused with context="settings".
StaplesManager renders categories as StapleChip pill grids, NOT checkboxes.
Auto-save on toggle (debounced PATCH 300ms). No save button.
-->
<style>
:root {
--color-page: #FAFAF7;
--color-surface: #F5F4EE;
--color-subtle: #EDECEA;
--color-border: #D8D7D0;
--color-text-muted: #6B6A63;
--color-text: #1C1C18;
--green-tint: #E8F5EA;
--green-light: #AEDCB0;
--green: #3D8C4A;
--green-dark: #2E6E39;
--green-deeper: #1E4A26;
--yellow-tint: #FDF6D8;
--yellow-light: #F9E08A;
--yellow-text: #8A6800;
--color-error: #DC4C3E;
--blue-tint: #E6F1FB;
--blue: #185FA5;
--blue-dark: #0C447C;
--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,.10), 0 2px 4px rgba(28,28,24,.05);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-sans); background: var(--color-page); 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 { padding-bottom: 28px; border-bottom: 1px solid var(--color-border); margin-bottom: 48px; display: flex; justify-content: space-between; align-items: flex-end; }
.doc-header h1 { font-family: var(--font-display); font-size: 26px; font-weight: 500; letter-spacing: -0.02em; }
.doc-header p { font-size: 13px; color: var(--color-text-muted); margin-top: 4px; }
.doc-meta { font-family: var(--font-mono); font-size: 11px; color: var(--color-text-muted); text-align: right; line-height: 1.9; }
.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 var(--color-border); margin-bottom: 32px; margin-top: 56px; }
.intro { font-size: 14px; line-height: 1.75; max-width: 640px; margin-bottom: 40px; }
/* ── State sections ── */
.state { margin-bottom: 64px; }
.state-header { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
.state-id { font-family: var(--font-mono); font-size: 10px; font-weight: 500; background: var(--color-subtle); color: var(--color-text-muted); padding: 2px 8px; border-radius: var(--radius-sm); white-space: nowrap; margin-top: 3px; }
.state-title { font-size: 16px; font-weight: 500; letter-spacing: -0.01em; }
.state-desc { font-size: 13px; color: var(--color-text-muted); margin-top: 2px; max-width: 540px; }
/* ── Preview containers ── */
.preview-wrap { display: flex; gap: 24px; align-items: flex-start; margin-bottom: 20px; }
.preview-d-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-m-wrap { flex-shrink: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-label { font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: var(--color-text-muted); }
.preview-d-clip { height: 340px; overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-page); }
.preview-d-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
.preview-m-clip { width: 196px; height: 340px; overflow: hidden; border: 1.5px solid var(--color-border); border-radius: 24px; background: var(--color-page); }
.preview-m-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
/* ── Notes ── */
.notes { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 14px 18px; }
.notes-label { font-size: 9px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 8px; }
.notes ul { list-style: none; display: flex; flex-direction: column; gap: 4px; }
.notes li { font-size: 12px; color: var(--color-text-muted); line-height: 1.5; display: flex; align-items: flex-start; gap: 8px; }
.notes li::before { content: '→'; color: var(--green); font-weight: 500; flex-shrink: 0; }
/* ── AppShell chrome ── */
.shell { display: flex; min-height: 100vh; background: var(--color-page); font-family: var(--font-sans); }
.sidebar { width: 224px; min-width: 224px; background: white; border-right: 1px solid var(--color-border); display: flex; flex-direction: column; }
.sidebar-brand { padding: 14px 18px; border-bottom: 1px solid var(--color-border); }
.sidebar-brand-row { display: flex; align-items: center; gap: 8px; }
.sidebar-logo { width: 22px; height: 22px; background: var(--green); border-radius: var(--radius-sm); }
.sidebar-app { font-family: var(--font-display); font-size: 15px; font-weight: 500; }
.sidebar-household { font-size: 10px; color: var(--color-text-muted); margin-top: 1px; }
.sidebar-nav { flex: 1; padding: 4px 8px; }
.sidebar-group-label { font-size: 8px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-text-muted); padding: 16px 12px 4px; }
.sidebar-item { display: flex; align-items: center; gap: 8px; padding: 7px 12px; border-radius: var(--radius-md); font-size: 13px; color: var(--color-text); text-decoration: none; }
.sidebar-item.active { background: var(--green-tint); color: var(--green-dark); font-weight: 500; }
.sidebar-item:not(.active):hover { background: var(--color-subtle); }
.sidebar-icon { width: 20px; text-align: center; font-size: 16px; }
/* ── Page content ── */
.page-content { flex: 1; padding: 32px 40px; }
.page-title { font-family: var(--font-display); font-size: 24px; font-weight: 500; letter-spacing: -0.02em; margin-bottom: 4px; }
.page-subtitle { font-size: 13px; color: var(--color-text-muted); margin-bottom: 28px; }
/* ── Settings card grid ── */
.settings-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; }
.settings-grid-bottom { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
/* ── Setting card ── */
.setting-card { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 24px; box-shadow: var(--shadow-card); cursor: pointer; text-decoration: none; color: inherit; display: flex; flex-direction: column; }
.setting-card:hover { box-shadow: var(--shadow-raised); border-color: #C0BFB8; }
.setting-card.primary { border-left: 3px solid var(--green-dark); }
.setting-card.primary:hover { border-left-color: var(--green-dark); }
.card-icon { font-size: 22px; margin-bottom: 12px; }
.card-stat { font-family: var(--font-display); font-size: 36px; font-weight: 500; letter-spacing: -0.02em; color: var(--green-dark); line-height: 1; margin-bottom: 2px; }
.card-stat-label { font-size: 11px; color: var(--color-text-muted); margin-bottom: 12px; }
.card-title { font-size: 15px; font-weight: 500; margin-bottom: 4px; }
.card-desc { font-size: 12px; color: var(--color-text-muted); line-height: 1.5; flex: 1; }
.card-cta { margin-top: 16px; display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; color: var(--green-dark); }
.card-cta-secondary { margin-top: 16px; display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; color: var(--color-text-muted); }
.card-meta { font-size: 12px; color: var(--color-text-muted); margin-bottom: 4px; }
/* ── D3 Staples page chrome ── */
.breadcrumb { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--color-text-muted); margin-bottom: 20px; }
.breadcrumb a { color: var(--color-text-muted); text-decoration: none; }
.breadcrumb a:hover { color: var(--color-text); }
.breadcrumb-sep { font-size: 10px; }
/* ── Staple chips ── */
.category-block { margin-bottom: 24px; }
.category-name { font-size: 10px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 10px; }
.chip-wrap { display: flex; flex-wrap: wrap; gap: 6px; }
.chip { padding: 5px 12px; border-radius: var(--radius-full); border: 1px solid var(--color-border); font-size: 12px; font-weight: 500; cursor: pointer; white-space: nowrap; }
.chip.on { background: var(--green-dark); color: white; border-color: var(--green-dark); }
.chip.off { background: transparent; color: var(--color-text-muted); }
.chip.off:hover { border-color: var(--green-light); color: var(--green-dark); }
.save-note { font-size: 11px; color: var(--color-text-muted); margin-top: 16px; font-style: italic; }
/* ── Mobile shell ── */
.m-shell { display: flex; flex-direction: column; background: var(--color-page); }
.m-header { padding: 16px; background: white; border-bottom: 1px solid var(--color-border); }
.m-header-title { font-size: 16px; font-weight: 500; }
.m-content { flex: 1; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.m-card { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 16px; box-shadow: var(--shadow-card); }
.m-card.primary { border-left: 3px solid var(--green-dark); }
.m-card-stat { font-family: var(--font-display); font-size: 28px; font-weight: 500; color: var(--green-dark); line-height: 1; margin-bottom: 2px; }
.m-card-stat-label { font-size: 10px; color: var(--color-text-muted); margin-bottom: 8px; }
.m-card-title { font-size: 14px; font-weight: 500; margin-bottom: 3px; }
.m-card-desc { font-size: 11px; color: var(--color-text-muted); }
.m-card-cta { margin-top: 12px; font-size: 11px; font-weight: 500; color: var(--green-dark); }
.m-tabbar { display: flex; border-top: 1px solid var(--color-border); background: white; }
.m-tab { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 8px 4px 4px; font-size: 10px; color: var(--color-text-muted); gap: 2px; }
.m-tab.active { color: var(--green-dark); }
.m-tab-icon { font-size: 20px; }
/* ── Agent section ── */
.agent-section { background: var(--color-text); color: #E8E8E2; padding: 40px 48px; margin-top: 64px; }
.agent-section h2 { font-size: 10px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: #6B6A63; margin-bottom: 4px; }
.agent-section > p { font-size: 13px; color: #9A9990; margin-bottom: 28px; line-height: 1.6; max-width: 640px; }
.spec-comment { font-family: var(--font-mono); font-size: 11px; color: #3A3A36; margin-bottom: 32px; line-height: 1.9; white-space: pre-wrap; }
.agent-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; margin-bottom: 40px; }
.agent-table thead tr { border-bottom: 1px solid #2A2A26; }
.agent-table th { text-align: left; padding: 8px 14px; font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #5A5A55; font-family: var(--font-sans); }
.agent-table td { padding: 9px 14px; border-bottom: 1px solid #1E1E1A; vertical-align: top; line-height: 1.5; }
.agent-table tr:last-child td { border-bottom: none; }
.agent-table td:first-child { color: #7A7A72; white-space: nowrap; }
.agent-table td:nth-child(2) { color: #E8E8E2; font-weight: 500; }
.agent-table td:nth-child(3) { color: #5A5A55; }
.group-row td { padding-top: 20px; font-family: var(--font-sans); font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #3A3A36; border-bottom: none; }
</style>
</head>
<body>
<div class="doc">
<!-- Header -->
<div class="doc-header">
<div>
<h1>E1 — Einstellungen</h1>
<p>Kachel-Ansicht · Finale Spezifikation · Route: <code>/settings</code><code>/household/staples?ctx=settings</code></p>
</div>
<div class="doc-meta">
screens: E1, D3<br/>
journey: J8<br/>
variation: Kachel (V2)<br/>
version: 1.0<br/>
date: 2026-04-09
</div>
</div>
<p class="intro">
Die Einstellungsseite dient als Hub mit drei Kacheln: Vorräte (primäre Aktion, navigiert zu D3),
Mitglieder (navigiert zu E2) und Profil. Die Vorräte-Kachel zeigt die aktive Zutatenanzahl als
Display-Font-Zahl. D3 verwendet die bestehende StaplesManager-Komponente mit <code>context="settings"</code>.
</p>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S1 — Hub-Ansicht (E1 /settings)</div>
<div class="state">
<div class="state-header">
<div class="state-id">S1</div>
<div>
<div class="state-title">Einstellungs-Hub — drei Kacheln</div>
<div class="state-desc">Vorräte-Kachel (2fr, primär mit grünem Akzentstreifen), Mitglieder-Kachel (1fr), Profil-Kachel (1fr). Desktop 2-spaltig oben, dann 2-spaltig unten.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div>
<div class="sidebar-household">Familie Raddatz</div>
</div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Plan</div>
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Einstellungen</div>
<div class="page-subtitle">Familie Raddatz</div>
<div class="settings-grid">
<!-- Vorräte card (2fr, primary) -->
<a class="setting-card primary" href="#">
<div class="card-icon">🥫</div>
<div class="card-stat">14</div>
<div class="card-stat-label">von 32 Zutaten als Vorrat markiert</div>
<div class="card-title">Vorräte</div>
<div class="card-desc">Lege fest, welche Zutaten immer zu Hause sind. Sie werden beim Einkaufen automatisch herausgefiltert.</div>
<div class="card-cta">Vorräte bearbeiten →</div>
</a>
<!-- Mitglieder card (1fr) -->
<a class="setting-card" href="#">
<div class="card-icon">👥</div>
<div class="card-title">Mitglieder</div>
<div class="card-meta" style="margin-top:4px;">3 Mitglieder</div>
<div class="card-desc" style="margin-top:8px;">Haushaltsmitglieder einladen, Rollen verwalten.</div>
<div class="card-cta-secondary">Mitglieder verwalten →</div>
</a>
</div>
<div class="settings-grid-bottom">
<!-- Profil card -->
<a class="setting-card" href="#">
<div class="card-icon">👤</div>
<div class="card-title">Profil</div>
<div class="card-meta" style="margin-top:4px;">Marcel R.</div>
<div class="card-desc" style="margin-top:8px;">Name und E-Mail-Adresse anpassen.</div>
<div class="card-cta-secondary">Profil bearbeiten →</div>
</a>
<!-- Placeholder / future -->
<div style="border: 1.5px dashed var(--color-border); border-radius: var(--radius-xl); padding: 24px; display:flex; align-items:center; justify-content:center; color: var(--color-text-muted); font-size: 12px;">Weitere Einstellungen folgen</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div class="m-shell" style="min-height:680px;">
<div class="m-header"><div class="m-header-title">Einstellungen</div></div>
<div class="m-content">
<div class="m-card primary">
<div class="m-card-stat">14</div>
<div class="m-card-stat-label">von 32 Vorräten aktiv</div>
<div class="m-card-title">Vorräte</div>
<div class="m-card-desc">Welche Zutaten hast du immer zu Hause?</div>
<div class="m-card-cta">Vorräte bearbeiten →</div>
</div>
<div class="m-card">
<div class="m-card-title">👥 Mitglieder</div>
<div class="m-card-desc" style="margin-top:4px;">3 Mitglieder · Einladen &amp; Rollen</div>
<div class="m-card-cta">Verwalten →</div>
</div>
<div class="m-card">
<div class="m-card-title">👤 Profil</div>
<div class="m-card-desc" style="margin-top:4px;">Marcel R.</div>
<div class="m-card-cta">Bearbeiten →</div>
</div>
</div>
<div class="m-tabbar">
<div class="m-tab"><div class="m-tab-icon">📅</div>Planer</div>
<div class="m-tab"><div class="m-tab-icon">🍽</div>Rezepte</div>
<div class="m-tab"><div class="m-tab-icon">🛒</div>Einkauf</div>
<div class="m-tab active"><div class="m-tab-icon">⚙️</div>Einstellungen</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Vorräte-Kachel: <code>grid-column: span 1</code> aber <code>2fr</code> Spaltenbreite im 2-Spalten-Grid. Grüner Linksstreifen (<code>border-left: 3px solid --green-dark</code>).</li>
<li>Stat-Zahl: Anzahl Zutaten mit <code>isStaple === true</code>, aus dem gleichen Load-Call der D3-Seite</li>
<li>Mitglieder-Karte: Anzahl aus <code>locals.haushalt</code> oder separatem API-Call; navigiert zu <code>/members</code></li>
<li>Profil-Karte: Name aus <code>locals.benutzer.name</code>; Zielseite <code>/profile</code> (noch nicht implementiert — Link disabled oder Placeholder)</li>
<li>Hover: <code>box-shadow: --shadow-raised</code>, leicht dunklerer Border</li>
<li>Alle Kacheln sind <code>&lt;a&gt;</code>-Tags für korrekte Navigation und Accessibility</li>
<li>Mobile: Kacheln stapeln sich vertikal in voller Breite, kein Grid</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S2 — Vorräte-Seite (D3 /household/staples?ctx=settings)</div>
<div class="state">
<div class="state-header">
<div class="state-id">S2</div>
<div>
<div class="state-title">D3 — Vorräte bearbeiten (StaplesManager, context="settings")</div>
<div class="state-desc">Navigiert man von der Vorräte-Kachel aus, erscheint die bestehende StaplesManager-Komponente mit Breadcrumb zurück zu Einstellungen.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Plan</div>
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<!-- Breadcrumb -->
<div class="breadcrumb">
<a href="#">← Einstellungen</a>
<span class="breadcrumb-sep">/</span>
<span>Vorräte</span>
</div>
<div class="page-title">Vorräte</div>
<div class="page-subtitle">Markierte Zutaten werden beim Einkaufen herausgefiltert.</div>
<!-- StaplesManager content (context="settings") -->
<div class="category-block">
<div class="category-name">Gewürze &amp; Öle</div>
<div class="chip-wrap">
<span class="chip on">Salz</span>
<span class="chip on">Pfeffer</span>
<span class="chip on">Olivenöl</span>
<span class="chip off">Paprika</span>
<span class="chip off">Kreuzkümmel</span>
<span class="chip on">Knoblauch</span>
<span class="chip off">Chili</span>
</div>
</div>
<div class="category-block">
<div class="category-name">Grundnahrung</div>
<div class="chip-wrap">
<span class="chip on">Reis</span>
<span class="chip off">Nudeln</span>
<span class="chip on">Mehl</span>
<span class="chip on">Zucker</span>
<span class="chip off">Linsen</span>
<span class="chip off">Hülsenfrüchte</span>
</div>
</div>
<div class="category-block">
<div class="category-name">Kühlschrank</div>
<div class="chip-wrap">
<span class="chip on">Butter</span>
<span class="chip on">Eier</span>
<span class="chip off">Milch</span>
<span class="chip off">Käse</span>
<span class="chip off">Joghurt</span>
</div>
</div>
<div class="save-note">Änderungen werden automatisch gespeichert. Gilt ab der nächsten Einkaufsliste.</div>
</div>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div class="m-shell" style="min-height:680px;">
<div class="m-header">
<div style="font-size:11px;color:var(--color-text-muted);margin-bottom:2px;">← Einstellungen</div>
<div class="m-header-title">Vorräte</div>
</div>
<div class="m-content" style="gap:16px;">
<div>
<div class="category-name">Gewürze &amp; Öle</div>
<div class="chip-wrap">
<span class="chip on" style="font-size:11px;">Salz</span>
<span class="chip on" style="font-size:11px;">Pfeffer</span>
<span class="chip on" style="font-size:11px;">Olivenöl</span>
<span class="chip off" style="font-size:11px;">Paprika</span>
<span class="chip on" style="font-size:11px;">Knoblauch</span>
</div>
</div>
<div>
<div class="category-name">Grundnahrung</div>
<div class="chip-wrap">
<span class="chip on" style="font-size:11px;">Reis</span>
<span class="chip off" style="font-size:11px;">Nudeln</span>
<span class="chip on" style="font-size:11px;">Mehl</span>
<span class="chip on" style="font-size:11px;">Zucker</span>
</div>
</div>
</div>
<div class="m-tabbar">
<div class="m-tab"><div class="m-tab-icon">📅</div>Planer</div>
<div class="m-tab"><div class="m-tab-icon">🍽</div>Rezepte</div>
<div class="m-tab"><div class="m-tab-icon">🛒</div>Einkauf</div>
<div class="m-tab active"><div class="m-tab-icon">⚙️</div>Einstellungen</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Breadcrumb "← Einstellungen" navigiert zurück zu <code>/settings</code></li>
<li>"Einstellungen" bleibt in der Sidebar aktiv (kein eigener Nav-Eintrag für Vorräte)</li>
<li>StaplesManager-Komponente unverändert mit <code>context="settings"</code> (3-spaltig auf md+)</li>
<li>Kein Speichern-Button. Hinweistext "Änderungen werden automatisch gespeichert." unter den Chips</li>
<li>Mobile: Chips statt 3-spaltig 1-spaltig (volle Breite), Flex-Wrap bleibt bestehen</li>
<li>D3 hat eigene <code>+page.server.ts</code> die <code>+page.svelte</code> bei <code>/household/staples</code> gibt es bereits</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S3 — Hover-Zustand der Kacheln</div>
<div class="state">
<div class="state-header">
<div class="state-id">S3</div>
<div>
<div class="state-title">Kachel-Hover — visuelles Feedback</div>
<div class="state-desc">Alle Kacheln sind anklickbare Links. Hover hebt die Kachel visuell an.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop — Vorräte-Kachel im Hover</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Einstellungen</div>
<div class="page-subtitle">Familie Raddatz</div>
<div class="settings-grid">
<!-- Hovered Vorräte card -->
<a class="setting-card primary" href="#" style="box-shadow:var(--shadow-raised);border-color:#C0BFB8;cursor:pointer;">
<div class="card-icon">🥫</div>
<div class="card-stat">14</div>
<div class="card-stat-label">von 32 Zutaten als Vorrat markiert</div>
<div class="card-title">Vorräte</div>
<div class="card-desc">Lege fest, welche Zutaten immer zu Hause sind.</div>
<div class="card-cta">Vorräte bearbeiten →</div>
</a>
<a class="setting-card" href="#">
<div class="card-icon">👥</div>
<div class="card-title">Mitglieder</div>
<div class="card-meta" style="margin-top:4px;">3 Mitglieder</div>
<div class="card-desc" style="margin-top:8px;">Haushaltsmitglieder einladen, Rollen verwalten.</div>
<div class="card-cta-secondary">Mitglieder verwalten →</div>
</a>
</div>
<div class="settings-grid-bottom">
<a class="setting-card" href="#"><div class="card-icon">👤</div><div class="card-title">Profil</div><div class="card-meta" style="margin-top:4px;">Marcel R.</div><div class="card-desc" style="margin-top:8px;">Name und E-Mail anpassen.</div><div class="card-cta-secondary">Profil bearbeiten →</div></a>
<div style="border:1.5px dashed var(--color-border);border-radius:var(--radius-xl);padding:24px;display:flex;align-items:center;justify-content:center;color:var(--color-text-muted);font-size:12px;">Weitere folgen</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Hover: <code>box-shadow: --shadow-raised</code> + <code>border-color: #C0BFB8</code></li>
<li>Vorräte-Kachel behält den grünen Linksstreifen auch im Hover</li>
<li>Transition: <code>box-shadow 150ms ease, border-color 150ms ease</code></li>
<li>Cursor: <code>pointer</code> auf allen Kacheln</li>
<li>Focus-visible: <code>outline: 2px solid --green-dark; outline-offset: 2px</code></li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S4 — Leerer Zustand (kein Vorrat gesetzt)</div>
<div class="state">
<div class="state-header">
<div class="state-id">S4</div>
<div>
<div class="state-title">Vorräte-Kachel bei 0 aktiven Vorräten</div>
<div class="state-desc">Wenn noch kein Vorrat gesetzt wurde, zeigt die Kachel eine Einladung zur Aktion statt der Zahl.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop — 0 Vorräte</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Einstellungen</div>
<div class="page-subtitle">Familie Raddatz</div>
<div class="settings-grid">
<a class="setting-card primary" href="#">
<div class="card-icon">🥫</div>
<!-- Empty state: no big number, instead prompt -->
<div style="font-size:13px;color:var(--color-text-muted);margin-bottom:8px;">Noch keine Vorräte eingerichtet</div>
<div class="card-title">Vorräte</div>
<div class="card-desc">Lege fest, welche Zutaten immer zu Hause sind. Sie werden beim Einkaufen automatisch herausgefiltert.</div>
<div class="card-cta">Jetzt einrichten →</div>
</a>
<a class="setting-card" href="#">
<div class="card-icon">👥</div>
<div class="card-title">Mitglieder</div>
<div class="card-meta" style="margin-top:4px;">1 Mitglied</div>
<div class="card-desc" style="margin-top:8px;">Haushaltsmitglieder einladen, Rollen verwalten.</div>
<div class="card-cta-secondary">Mitglieder verwalten →</div>
</a>
</div>
<div class="settings-grid-bottom">
<a class="setting-card" href="#"><div class="card-icon">👤</div><div class="card-title">Profil</div><div class="card-meta" style="margin-top:4px;">Marcel R.</div><div class="card-cta-secondary" style="margin-top:8px;">Bearbeiten →</div></a>
<div style="border:1.5px dashed var(--color-border);border-radius:var(--radius-xl);padding:24px;display:flex;align-items:center;justify-content:center;color:var(--color-text-muted);font-size:12px;">Weitere folgen</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Wenn <code>stapleCount === 0</code>: Stat-Zahl weglassen, stattdessen "Noch keine Vorräte eingerichtet" in muted</li>
<li>CTA-Text ändert sich: "Jetzt einrichten →" statt "Vorräte bearbeiten →"</li>
<li>Kachel navigiert weiterhin zu D3 — StaplesManager lädt immer, unabhängig vom Count</li>
</ul>
</div>
</div>
<!-- ─── Machine-readable agent section ─── -->
<div class="agent-section">
<h2>Maschinen-lesbare Spezifikation</h2>
<p>Diese Sektion enthält verbindliche Implementierungsregeln für den Coding-Agenten.</p>
<pre class="spec-comment">
/* spec:rules — E1 Einstellungen Kachel
*
* ROUTE: /settings (E1 hub)
* DATA LOAD (page.server.ts):
* - GET /v1/ingredient-categories to count stapleCount
* stapleCount = sum of ingredients where isStaple === true
* - member count available from layout data (locals.haushalt)
* or fetch GET /v1/households/mine/members and count length
* - profile name from locals.benutzer.name
*
* LAYOUT: E1 HUB
* grid: 2 columns (2fr 1fr) top row + 2 columns (1fr 1fr) bottom row; gap 16px
* mobile: single column, full-width cards, gap 12px
*
* CARD: all cards are <a> tags (href to target route)
* border-radius: --radius-xl
* border: 1px solid --color-border
* bg: white
* padding: 24px desktop / 16px mobile
* hover: box-shadow --shadow-raised, border-color #C0BFB8
* transition: box-shadow 150ms ease, border-color 150ms ease
* cursor: pointer
* focus-visible: outline 2px solid --green-dark, offset 2px
*
* VORRÄTE CARD (primary)
* border-left: 3px solid --green-dark
* stat number: font-family --font-display, font-size 36px, color --green-dark
* stat label: "von {total} Zutaten als Vorrat markiert", 11px, --color-text-muted
* empty state (stapleCount === 0): hide stat, show "Noch keine Vorräte eingerichtet"
* cta: "Vorräte bearbeiten →" (empty: "Jetzt einrichten →")
* href: /household/staples?ctx=settings
*
* MITGLIEDER CARD
* meta: "{memberCount} Mitglieder"
* href: /members
*
* PROFIL CARD
* meta: locals.benutzer.name
* href: /profile (not yet implemented — render as disabled or placeholder)
*
* ROUTE: /household/staples?ctx=settings (D3)
* component: StaplesManager with context="settings" (already exists)
* breadcrumb: "← Einstellungen" linking back to /settings
* sidebar: "Einstellungen" stays active (no separate nav item for staples)
* no save button — StaplesManager auto-saves via debounced PATCH 300ms
* hint text below grid: "Änderungen werden automatisch gespeichert. Gilt ab der nächsten Einkaufsliste."
* grid: 3-col on md+ (context="settings" already sets this in StaplesManager)
*
* CHIP STYLES (for reference — rendered by StapleChip, do NOT reimplement)
* selected: bg --green-dark, color white, border-color --green-dark
* unselected: bg transparent, color --color-text-muted, border 1px solid --color-border
* hover unselected: border-color --green-light, color --green-dark
*
* CATEGORY LABEL TYPOGRAPHY
* font-size: 10px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase
* color: --color-text-muted; margin-bottom: 10px
*/
</pre>
<table class="agent-table">
<thead>
<tr><th>Property</th><th>Value</th><th>Notes</th></tr>
</thead>
<tbody>
<tr class="group-row"><td colspan="3">E1 Hub Layout</td></tr>
<tr><td>grid-desktop</td><td>2fr 1fr / 1fr 1fr</td><td>top row / bottom row</td></tr>
<tr><td>grid-mobile</td><td>1fr</td><td>full-width stack</td></tr>
<tr><td>gap</td><td>16px desktop / 12px mobile</td><td></td></tr>
<tr class="group-row"><td colspan="3">Vorräte Card</td></tr>
<tr><td>stat-font</td><td>--font-display, 36px, --green-dark</td><td>Fraunces</td></tr>
<tr><td>accent-border</td><td>border-left: 3px solid --green-dark</td><td>primary indicator</td></tr>
<tr><td>stat-source</td><td>count isStaple=true from /v1/ingredient-categories</td><td>load in page.server.ts</td></tr>
<tr><td>empty-state</td><td>hide stat; show muted text</td><td>when stapleCount === 0</td></tr>
<tr><td>href</td><td>/household/staples?ctx=settings</td><td>D3 route</td></tr>
<tr class="group-row"><td colspan="3">D3 Staples Page</td></tr>
<tr><td>component</td><td>StaplesManager context="settings"</td><td>existing, do not modify</td></tr>
<tr><td>breadcrumb</td><td>← Einstellungen → /settings</td><td>above page title</td></tr>
<tr><td>active-nav</td><td>Einstellungen in sidebar</td><td>not a separate nav entry</td></tr>
<tr><td>save-hint</td><td>"Änderungen werden automatisch gespeichert."</td><td>below chip grid</td></tr>
<tr><td>debounce</td><td>300ms (in StaplesManager)</td><td>do not add extra debounce</td></tr>
</tbody>
</table>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,905 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>E2 — Mitglieder · Kachel-Ansicht · Finale Spezifikation</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: E2 Mitglieder Kachel-Ansicht, Finale Spezifikation
version: 1.0
journey: J7 Manage household members
route: /members
screen: E2
chosen-variation: V2 Kachel-Ansicht (Card grid)
last-updated: 2026-04-09
BACKEND GAPS (must be implemented before this page can ship):
- DELETE /v1/households/mine/members/{userId} → remove member
- PATCH /v1/households/mine/members/{userId} → body: { role: "planer"|"mitglied" }
- GET /v1/households/mine/invites → list active invites with expiry
These endpoints do not exist in the current API schema (schema.d.ts).
Existing: GET /v1/households/mine/members, POST /v1/households/mine/invites
ROLE ACCESS:
- rolle === 'planer': sees kebab menu on all cards except own
- rolle === 'mitglied': sees all cards read-only, no kebab, no invite card CTA
-->
<style>
:root {
--color-page: #FAFAF7;
--color-surface: #F5F4EE;
--color-subtle: #EDECEA;
--color-border: #D8D7D0;
--color-text-muted: #6B6A63;
--color-text: #1C1C18;
--green-tint: #E8F5EA;
--green-light: #AEDCB0;
--green: #3D8C4A;
--green-dark: #2E6E39;
--yellow-tint: #FDF6D8;
--yellow-light: #F9E08A;
--yellow-text: #8A6800;
--color-error: #DC4C3E;
--error-tint: #FDECEA;
--blue-tint: #E6F1FB;
--blue: #185FA5;
--blue-dark: #0C447C;
--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,.10), 0 2px 4px rgba(28,28,24,.05);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-sans); background: var(--color-page); 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 { padding-bottom: 28px; border-bottom: 1px solid var(--color-border); margin-bottom: 48px; display: flex; justify-content: space-between; align-items: flex-end; }
.doc-header h1 { font-family: var(--font-display); font-size: 26px; font-weight: 500; letter-spacing: -0.02em; }
.doc-header p { font-size: 13px; color: var(--color-text-muted); margin-top: 4px; }
.doc-meta { font-family: var(--font-mono); font-size: 11px; color: var(--color-text-muted); text-align: right; line-height: 1.9; }
.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 var(--color-border); margin-bottom: 32px; margin-top: 56px; }
.intro { font-size: 14px; line-height: 1.75; max-width: 640px; margin-bottom: 40px; }
/* ── State sections ── */
.state { margin-bottom: 64px; }
.state-header { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
.state-id { font-family: var(--font-mono); font-size: 10px; font-weight: 500; background: var(--color-subtle); color: var(--color-text-muted); padding: 2px 8px; border-radius: var(--radius-sm); white-space: nowrap; margin-top: 3px; }
.state-title { font-size: 16px; font-weight: 500; letter-spacing: -0.01em; }
.state-desc { font-size: 13px; color: var(--color-text-muted); margin-top: 2px; max-width: 540px; }
/* ── Preview ── */
.preview-wrap { display: flex; gap: 24px; align-items: flex-start; margin-bottom: 20px; }
.preview-d-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-m-wrap { flex-shrink: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-label { font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: var(--color-text-muted); }
.preview-d-clip { height: 340px; overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-page); }
.preview-d-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
.preview-m-clip { width: 196px; height: 340px; overflow: hidden; border: 1.5px solid var(--color-border); border-radius: 24px; background: var(--color-page); }
.preview-m-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
/* ── Notes ── */
.notes { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 14px 18px; }
.notes-label { font-size: 9px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 8px; }
.notes ul { list-style: none; display: flex; flex-direction: column; gap: 4px; }
.notes li { font-size: 12px; color: var(--color-text-muted); line-height: 1.5; display: flex; align-items: flex-start; gap: 8px; }
.notes li::before { content: '→'; color: var(--green); font-weight: 500; flex-shrink: 0; }
.notes li.warn::before { content: '⚠'; color: var(--yellow-text); }
.notes li.gap::before { content: '✗'; color: var(--color-error); }
/* ── Warning banner ── */
.backend-warning { background: var(--yellow-tint); border: 1px solid var(--yellow-light); border-radius: var(--radius-lg); padding: 14px 18px; margin-bottom: 40px; }
.backend-warning h3 { font-size: 12px; font-weight: 600; color: var(--yellow-text); margin-bottom: 6px; }
.backend-warning ul { list-style: none; display: flex; flex-direction: column; gap: 3px; }
.backend-warning li { font-family: var(--font-mono); font-size: 11px; color: var(--yellow-text); display: flex; gap: 8px; }
.backend-warning li::before { content: '○'; }
/* ── AppShell chrome ── */
.shell { display: flex; min-height: 100vh; background: var(--color-page); font-family: var(--font-sans); }
.sidebar { width: 224px; min-width: 224px; background: white; border-right: 1px solid var(--color-border); display: flex; flex-direction: column; }
.sidebar-brand { padding: 14px 18px; border-bottom: 1px solid var(--color-border); }
.sidebar-brand-row { display: flex; align-items: center; gap: 8px; }
.sidebar-logo { width: 22px; height: 22px; background: var(--green); border-radius: var(--radius-sm); }
.sidebar-app { font-family: var(--font-display); font-size: 15px; font-weight: 500; }
.sidebar-household { font-size: 10px; color: var(--color-text-muted); margin-top: 1px; }
.sidebar-nav { flex: 1; padding: 4px 8px; }
.sidebar-group-label { font-size: 8px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-text-muted); padding: 16px 12px 4px; }
.sidebar-item { display: flex; align-items: center; gap: 8px; padding: 7px 12px; border-radius: var(--radius-md); font-size: 13px; color: var(--color-text); text-decoration: none; }
.sidebar-item.active { background: var(--green-tint); color: var(--green-dark); font-weight: 500; }
.sidebar-item:not(.active):hover { background: var(--color-subtle); }
.sidebar-icon { width: 20px; text-align: center; font-size: 16px; }
/* ── Page content ── */
.page-content { flex: 1; padding: 32px 40px; }
.page-title { font-family: var(--font-display); font-size: 24px; font-weight: 500; letter-spacing: -0.02em; margin-bottom: 4px; }
.page-subtitle { font-size: 13px; color: var(--color-text-muted); margin-bottom: 28px; }
/* ── Member card grid ── */
.member-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
.member-card { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 24px 20px 20px; box-shadow: var(--shadow-card); position: relative; display: flex; flex-direction: column; align-items: center; text-align: center; }
.member-card.hovered { box-shadow: var(--shadow-raised); border-color: #C0BFB8; }
.member-card.own { border-color: var(--green-light); }
.avatar { width: 56px; height: 56px; border-radius: var(--radius-full); display: flex; align-items: center; justify-content: center; font-family: var(--font-display); font-size: 20px; font-weight: 500; color: white; margin-bottom: 12px; flex-shrink: 0; }
.avatar-planer { background: var(--green-dark); }
.avatar-mitglied { background: var(--blue); }
.member-name { font-size: 14px; font-weight: 500; margin-bottom: 6px; }
.role-badge { font-size: 10px; font-weight: 500; letter-spacing: 0.04em; padding: 2px 8px; border-radius: var(--radius-full); white-space: nowrap; }
.role-badge.planer { background: var(--green-tint); color: var(--green-dark); }
.role-badge.mitglied { background: var(--blue-tint); color: var(--blue-dark); }
.join-date { font-size: 11px; color: var(--color-text-muted); margin-top: 8px; }
.self-badge { font-size: 10px; font-weight: 500; letter-spacing: 0.04em; padding: 2px 8px; border-radius: var(--radius-full); background: var(--green-tint); color: var(--green-dark); }
/* ── Kebab button ── */
.kebab-btn { position: absolute; top: 12px; right: 12px; width: 28px; height: 28px; border-radius: var(--radius-md); background: transparent; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; color: var(--color-text-muted); }
.kebab-btn:hover, .kebab-btn.open { background: var(--color-subtle); color: var(--color-text); }
/* ── Dropdown menu ── */
.dropdown { position: absolute; top: 44px; right: 12px; background: white; border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: var(--shadow-raised); min-width: 160px; z-index: 10; overflow: hidden; }
.dropdown-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; font-size: 13px; color: var(--color-text); cursor: pointer; white-space: nowrap; }
.dropdown-item:hover { background: var(--color-subtle); }
.dropdown-item.danger { color: var(--color-error); }
.dropdown-item.danger:hover { background: var(--error-tint); }
.dropdown-icon { font-size: 14px; width: 16px; text-align: center; }
.dropdown-divider { height: 1px; background: var(--color-border); margin: 2px 0; }
/* ── Role segmented control (inline on card) ── */
.role-control { display: flex; border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; margin-top: 8px; width: 100%; }
.role-control-btn { flex: 1; padding: 6px 8px; font-size: 11px; font-weight: 500; background: white; border: none; cursor: pointer; color: var(--color-text-muted); }
.role-control-btn.active { background: var(--green-dark); color: white; }
.role-control-btn:first-child { border-right: 1px solid var(--color-border); }
/* ── Invite card ── */
.invite-card { background: white; border: 1.5px dashed var(--color-border); border-radius: var(--radius-xl); padding: 24px 20px 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; cursor: pointer; min-height: 180px; gap: 10px; }
.invite-card:hover { border-color: var(--green-light); background: var(--green-tint); }
.invite-plus { width: 44px; height: 44px; border-radius: var(--radius-full); background: var(--color-subtle); display: flex; align-items: center; justify-content: center; font-size: 22px; color: var(--color-text-muted); }
.invite-card:hover .invite-plus { background: var(--green-light); color: var(--green-dark); }
.invite-label { font-size: 13px; font-weight: 500; color: var(--color-text-muted); }
.invite-card:hover .invite-label { color: var(--green-dark); }
/* ── Invite panel (expanded inline) ── */
.invite-panel { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 24px; margin-top: 8px; }
.invite-panel-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
.invite-panel-desc { font-size: 12px; color: var(--color-text-muted); margin-bottom: 16px; }
.invite-link-row { display: flex; gap: 8px; align-items: center; }
.invite-link-box { flex: 1; background: var(--color-subtle); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 8px 12px; font-family: var(--font-mono); font-size: 12px; color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.btn-copy { padding: 8px 14px; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: white; font-size: 12px; font-weight: 500; cursor: pointer; white-space: nowrap; }
.btn-copy:hover { background: var(--color-subtle); }
.invite-expiry { font-size: 11px; color: var(--color-text-muted); margin-top: 8px; }
.invite-expiry span { background: var(--yellow-tint); color: var(--yellow-text); padding: 1px 6px; border-radius: var(--radius-sm); font-weight: 500; }
.btn-regen { margin-top: 12px; font-size: 12px; color: var(--color-text-muted); background: none; border: none; cursor: pointer; text-decoration: underline; }
.btn-regen:hover { color: var(--color-text); }
/* ── Dialog overlay ── */
.overlay { position: absolute; inset: 0; background: rgba(28,28,24,.45); display: flex; align-items: center; justify-content: center; z-index: 50; }
.dialog { background: white; border-radius: var(--radius-xl); padding: 28px 32px; max-width: 380px; width: 100%; box-shadow: var(--shadow-raised); }
.dialog-title { font-size: 16px; font-weight: 500; margin-bottom: 8px; }
.dialog-body { font-size: 13px; color: var(--color-text-muted); line-height: 1.6; margin-bottom: 24px; }
.dialog-body strong { color: var(--color-text); font-weight: 500; }
.dialog-actions { display: flex; gap: 10px; justify-content: flex-end; }
.btn-cancel { padding: 9px 18px; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: white; font-size: 13px; font-weight: 500; cursor: pointer; }
.btn-cancel:hover { background: var(--color-subtle); }
.btn-remove { padding: 9px 18px; border-radius: var(--radius-md); border: none; background: var(--color-error); color: white; font-size: 13px; font-weight: 500; cursor: pointer; }
.btn-remove:hover { background: #C43A2E; }
/* ── Mobile shell ── */
.m-shell { display: flex; flex-direction: column; background: var(--color-page); }
.m-header { padding: 16px; background: white; border-bottom: 1px solid var(--color-border); display: flex; align-items: center; justify-content: space-between; }
.m-header-title { font-size: 16px; font-weight: 500; }
.m-header-btn { width: 36px; height: 36px; border-radius: var(--radius-full); background: var(--green-dark); display: flex; align-items: center; justify-content: center; font-size: 18px; color: white; border: none; cursor: pointer; }
.m-content { flex: 1; padding: 16px; }
.m-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.m-card { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 16px; display: flex; flex-direction: column; align-items: center; text-align: center; position: relative; box-shadow: var(--shadow-card); }
.m-avatar { width: 44px; height: 44px; border-radius: var(--radius-full); display: flex; align-items: center; justify-content: center; font-family: var(--font-display); font-size: 16px; font-weight: 500; color: white; margin-bottom: 8px; }
.m-avatar.planer { background: var(--green-dark); }
.m-avatar.mitglied { background: var(--blue); }
.m-name { font-size: 12px; font-weight: 500; margin-bottom: 4px; }
.m-role { font-size: 10px; font-weight: 500; padding: 2px 6px; border-radius: var(--radius-full); }
.m-role.planer { background: var(--green-tint); color: var(--green-dark); }
.m-role.mitglied { background: var(--blue-tint); color: var(--blue-dark); }
.m-kebab { position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 14px; color: var(--color-text-muted); background: none; border: none; }
.m-invite-card { background: white; border: 1.5px dashed var(--color-border); border-radius: var(--radius-xl); padding: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 120px; gap: 6px; }
.m-invite-plus { width: 36px; height: 36px; border-radius: var(--radius-full); background: var(--color-subtle); display: flex; align-items: center; justify-content: center; font-size: 18px; color: var(--color-text-muted); }
.m-invite-label { font-size: 11px; color: var(--color-text-muted); font-weight: 500; }
.m-tabbar { display: flex; border-top: 1px solid var(--color-border); background: white; }
.m-tab { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 8px 4px 4px; font-size: 10px; color: var(--color-text-muted); gap: 2px; }
.m-tab.active { color: var(--green-dark); }
.m-tab-icon { font-size: 20px; }
/* ── Agent section ── */
.agent-section { background: var(--color-text); color: #E8E8E2; padding: 40px 48px; margin-top: 64px; }
.agent-section h2 { font-size: 10px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: #6B6A63; margin-bottom: 4px; }
.agent-section > p { font-size: 13px; color: #9A9990; margin-bottom: 28px; line-height: 1.6; max-width: 640px; }
.spec-comment { font-family: var(--font-mono); font-size: 11px; color: #3A3A36; margin-bottom: 32px; line-height: 1.9; white-space: pre-wrap; }
.agent-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; margin-bottom: 40px; }
.agent-table thead tr { border-bottom: 1px solid #2A2A26; }
.agent-table th { text-align: left; padding: 8px 14px; font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #5A5A55; font-family: var(--font-sans); }
.agent-table td { padding: 9px 14px; border-bottom: 1px solid #1E1E1A; vertical-align: top; line-height: 1.5; }
.agent-table tr:last-child td { border-bottom: none; }
.agent-table td:first-child { color: #7A7A72; white-space: nowrap; }
.agent-table td:nth-child(2) { color: #E8E8E2; font-weight: 500; }
.agent-table td:nth-child(3) { color: #5A5A55; }
.group-row td { padding-top: 20px; font-family: var(--font-sans); font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #3A3A36; border-bottom: none; }
</style>
</head>
<body>
<div class="doc">
<!-- Header -->
<div class="doc-header">
<div>
<h1>E2 — Mitglieder</h1>
<p>Kachel-Ansicht · Finale Spezifikation · Route: <code>/members</code></p>
</div>
<div class="doc-meta">
screen: E2<br/>
journey: J7<br/>
variation: Kachel (V2)<br/>
version: 1.0<br/>
date: 2026-04-09
</div>
</div>
<p class="intro">
Die Mitgliederseite zeigt alle Haushaltsmitglieder als Kacheln. Der Planer kann Rollen ändern und Mitglieder
entfernen über ein Kebab-Menü auf jeder Kachel. Eine Einladekachel ermöglicht das Generieren und Kopieren des
Einlade-Links. Mitglieder sehen alle Kacheln nur lesend.
</p>
<div class="backend-warning">
<h3>Backend-Lücken — vor Implementierung schließen</h3>
<ul>
<li>DELETE /v1/households/mine/members/{userId} — Mitglied entfernen</li>
<li>PATCH /v1/households/mine/members/{userId} — Rolle ändern (body: { role })</li>
<li>GET /v1/households/mine/invites — aktive Einladungen auflisten (inkl. expiresAt)</li>
</ul>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S1 — Standardansicht (Planer)</div>
<div class="state">
<div class="state-header">
<div class="state-id">S1</div>
<div>
<div class="state-title">Standardansicht — Planer sieht vollständige Kacheln</div>
<div class="state-desc">Alle Mitglieder als Kacheln, dahinter die Einladekachel. Kebab-Button erscheint on hover.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-row">
<div class="sidebar-logo"></div>
<span class="sidebar-app">Mealplan</span>
</div>
<div class="sidebar-household">Familie Raddatz</div>
</div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Plan</div>
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Mitglieder</div>
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
<div class="member-grid">
<!-- Own card -->
<div class="member-card own">
<div class="avatar avatar-planer">MR</div>
<div class="member-name">Marcel R.</div>
<span class="role-badge planer">Planer</span>
<div class="join-date">seit 02.04.2026</div>
<div style="margin-top:8px;"><span class="self-badge">Du</span></div>
</div>
<!-- Member 2 -->
<div class="member-card">
<button class="kebab-btn"></button>
<div class="avatar avatar-mitglied">SR</div>
<div class="member-name">Sandra R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 03.04.2026</div>
</div>
<!-- Member 3 -->
<div class="member-card">
<button class="kebab-btn"></button>
<div class="avatar avatar-mitglied">LR</div>
<div class="member-name">Lena R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 05.04.2026</div>
</div>
<!-- Invite card -->
<div class="invite-card">
<div class="invite-plus">+</div>
<div class="invite-label">Mitglied einladen</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div class="m-shell" style="min-height:680px;">
<div class="m-header">
<span class="m-header-title">Mitglieder</span>
<button class="m-header-btn">+</button>
</div>
<div class="m-content">
<div class="m-grid">
<div class="m-card" style="border-color:var(--green-light);">
<div class="m-avatar planer">MR</div>
<div class="m-name">Marcel R.</div>
<span class="m-role planer">Planer</span>
<div style="margin-top:6px;font-size:10px;color:var(--color-text-muted);">Du</div>
</div>
<div class="m-card">
<button class="m-kebab"></button>
<div class="m-avatar mitglied">SR</div>
<div class="m-name">Sandra R.</div>
<span class="m-role mitglied">Mitglied</span>
</div>
<div class="m-card">
<button class="m-kebab"></button>
<div class="m-avatar mitglied">LR</div>
<div class="m-name">Lena R.</div>
<span class="m-role mitglied">Mitglied</span>
</div>
<div class="m-invite-card">
<div class="m-invite-plus">+</div>
<div class="m-invite-label">Einladen</div>
</div>
</div>
</div>
<div class="m-tabbar">
<div class="m-tab"><div class="m-tab-icon">📅</div>Planer</div>
<div class="m-tab"><div class="m-tab-icon">🍽</div>Rezepte</div>
<div class="m-tab"><div class="m-tab-icon">🛒</div>Einkauf</div>
<div class="m-tab active"><div class="m-tab-icon">⚙️</div>Einstellungen</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Eigene Kachel (Du): grüner Kartenrahmen (<code>border: var(--green-light)</code>), "Du"-Badge statt Kebab</li>
<li>Kebab-Button (<code></code>): immer im DOM, <code>opacity:0</code> bis hover/focus, dann <code>opacity:1</code>. Auf Touch-Geräten immer sichtbar.</li>
<li>Avatar-Initialen: erste zwei Buchstaben des displayName. Planer = green-dark, Mitglied = blue</li>
<li>Kachel-Reihenfolge: eigene Kachel immer zuerst, dann joinedAt aufsteigend, Einladekachel immer zuletzt</li>
<li>Mobile: "+" Button in der Header-Zeile öffnet Einlade-Panel. Einladekachel bleibt zusätzlich im Grid.</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S2 — Kebab-Menü offen</div>
<div class="state">
<div class="state-header">
<div class="state-id">S2</div>
<div>
<div class="state-title">Kebab-Menü geöffnet</div>
<div class="state-desc">Klick auf ⋯ öffnet Dropdown mit zwei Aktionen. Klick außerhalb schließt das Menü.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop — Menü offen auf "Sandra R."</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar" style="width:224px;min-width:224px;">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Plan</div>
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Mitglieder</div>
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
<div class="member-grid">
<div class="member-card own">
<div class="avatar avatar-planer">MR</div>
<div class="member-name">Marcel R.</div>
<span class="role-badge planer">Planer</span>
<div class="join-date">seit 02.04.2026</div>
<div style="margin-top:8px;"><span class="self-badge">Du</span></div>
</div>
<!-- Card with open menu -->
<div class="member-card hovered" style="z-index:20;">
<button class="kebab-btn open"></button>
<div class="dropdown">
<div class="dropdown-item"><span class="dropdown-icon">🔄</span>Rolle ändern</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item danger"><span class="dropdown-icon"></span>Entfernen</div>
</div>
<div class="avatar avatar-mitglied">SR</div>
<div class="member-name">Sandra R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 03.04.2026</div>
</div>
<div class="member-card">
<button class="kebab-btn"></button>
<div class="avatar avatar-mitglied">LR</div>
<div class="member-name">Lena R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 05.04.2026</div>
</div>
<div class="invite-card">
<div class="invite-plus">+</div>
<div class="invite-label">Mitglied einladen</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- No mobile preview needed for this state; same as desktop but full-screen -->
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Dropdown: <code>position: absolute; top: 44px; right: 12px</code> relativ zur Kachel</li>
<li>Zwei Einträge: "Rolle ändern" (neutrales Icon 🔄) und "Entfernen" (rot, Icon ✕)</li>
<li>Klick außerhalb des Dropdowns schließt diesen (click-away listener)</li>
<li>Nur ein Menü gleichzeitig offen. ESC schließt ebenfalls.</li>
<li>Mobile: Tap auf ⋯ öffnet Bottom Sheet mit denselben zwei Einträgen (44px min-height pro Eintrag)</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S3 — Rolle ändern (inline auf der Kachel)</div>
<div class="state">
<div class="state-header">
<div class="state-id">S3</div>
<div>
<div class="state-title">Rolle ändern — Segmented Control erscheint</div>
<div class="state-desc">Wahl von "Rolle ändern" ersetzt das Rolle-Badge durch einen 2-Button-Schalter [Planer | Mitglied]. Aktive Rolle vorausgewählt. Bestätigung sofort mit PATCH-Request.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop — Rolle-Control auf "Sandra R." aktiv</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar" style="width:224px;min-width:224px;">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Mitglieder</div>
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
<div class="member-grid">
<div class="member-card own">
<div class="avatar avatar-planer">MR</div>
<div class="member-name">Marcel R.</div>
<span class="role-badge planer">Planer</span>
<div class="join-date">seit 02.04.2026</div>
<div style="margin-top:8px;"><span class="self-badge">Du</span></div>
</div>
<!-- Card in role-edit mode -->
<div class="member-card" style="border-color:#B5D4F4;">
<div class="avatar avatar-mitglied">SR</div>
<div class="member-name">Sandra R.</div>
<!-- Role control replaces badge -->
<div class="role-control" style="width:100%;">
<button class="role-control-btn">Planer</button>
<button class="role-control-btn active">Mitglied</button>
</div>
<div class="join-date">seit 03.04.2026</div>
<button style="margin-top:8px;font-size:11px;color:var(--color-text-muted);background:none;border:none;cursor:pointer;text-decoration:underline;">Abbrechen</button>
</div>
<div class="member-card">
<button class="kebab-btn"></button>
<div class="avatar avatar-mitglied">LR</div>
<div class="member-name">Lena R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 05.04.2026</div>
</div>
<div class="invite-card">
<div class="invite-plus">+</div>
<div class="invite-label">Mitglied einladen</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Role-Control ersetzt das Badge in-place auf der Kachel. Kein Dialog, kein Page-Change.</li>
<li>Klick auf die inaktive Rolle → optimistisches Update → PATCH /v1/households/mine/members/{userId} { role }</li>
<li>Bei Erfolg: Role-Control durch neues Badge ersetzen</li>
<li>Bei Fehler: Rollback + Toast "Rolle konnte nicht geändert werden."</li>
<li>"Abbrechen" bringt ohne PATCH-Call das Badge zurück</li>
<li>Der Planer kann seinen eigenen Planer-Status nicht abgeben, solange er der einzige Planer ist</li>
<li>Kachel bekommt blauen Rahmen (<code>border-color: #B5D4F4</code>) als Editier-Indikator</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S4 — Entfernen-Bestätigung (Dialog)</div>
<div class="state">
<div class="state-header">
<div class="state-id">S4</div>
<div>
<div class="state-title">Bestätigungsdialog "Mitglied entfernen"</div>
<div class="state-desc">Klick auf "Entfernen" im Dropdown öffnet einen modalen Dialog. Kein direktes Löschen ohne Bestätigung.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop — Dialog über der Seite</div>
<div class="preview-d-clip">
<div class="preview-d-scale" style="position:relative;">
<div class="shell" style="position:relative;">
<div class="sidebar" style="width:224px;min-width:224px;">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
</div>
</div>
<div class="page-content" style="opacity:0.4;pointer-events:none;">
<div class="page-title">Mitglieder</div>
<div class="member-grid">
<div class="member-card own"><div class="avatar avatar-planer">MR</div><div class="member-name">Marcel R.</div><span class="role-badge planer">Planer</span></div>
<div class="member-card"><div class="avatar avatar-mitglied">SR</div><div class="member-name">Sandra R.</div><span class="role-badge mitglied">Mitglied</span></div>
<div class="member-card"><div class="avatar avatar-mitglied">LR</div><div class="member-name">Lena R.</div><span class="role-badge mitglied">Mitglied</span></div>
<div class="invite-card"><div class="invite-plus">+</div><div class="invite-label">Mitglied einladen</div></div>
</div>
</div>
<!-- Dialog -->
<div class="overlay" style="position:absolute;">
<div class="dialog">
<div class="dialog-title">Mitglied entfernen?</div>
<div class="dialog-body"><strong>Sandra R.</strong> wird aus dem Haushalt entfernt und verliert sofort den Zugang zu allen Plänen und Rezepten.</div>
<div class="dialog-actions">
<button class="btn-cancel">Abbrechen</button>
<button class="btn-remove">Entfernen</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div class="m-shell" style="min-height:680px;position:relative;">
<div class="m-header"><span class="m-header-title">Mitglieder</span><button class="m-header-btn">+</button></div>
<div class="m-content" style="opacity:0.35;pointer-events:none;">
<div class="m-grid">
<div class="m-card"><div class="m-avatar planer">MR</div><div class="m-name">Marcel R.</div><span class="m-role planer">Planer</span></div>
<div class="m-card"><div class="m-avatar mitglied">SR</div><div class="m-name">Sandra R.</div><span class="m-role mitglied">Mitglied</span></div>
</div>
</div>
<!-- Mobile dialog -->
<div class="overlay" style="position:absolute;align-items:flex-end;padding-bottom:0;">
<div class="dialog" style="border-radius:var(--radius-xl) var(--radius-xl) 0 0;max-width:100%;padding:24px 24px 32px;">
<div class="dialog-title" style="font-size:15px;">Mitglied entfernen?</div>
<div class="dialog-body" style="font-size:12px;"><strong>Sandra R.</strong> wird aus dem Haushalt entfernt.</div>
<div class="dialog-actions">
<button class="btn-cancel" style="font-size:12px;">Abbrechen</button>
<button class="btn-remove" style="font-size:12px;">Entfernen</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Dialog zeigt den <strong>displayName</strong> des Mitglieds explizit</li>
<li>Bestätigung → DELETE /v1/households/mine/members/{userId} → Kachel aus Grid entfernen</li>
<li>Planer kann sich nicht selbst entfernen (eigene Kachel hat kein Kebab-Menü)</li>
<li>Letzter verbleibender Planer kann nicht entfernt werden → Fehlermeldung im Dialog</li>
<li>Mobile: Dialog als Bottom Sheet (<code>border-radius</code> nur oben, kein max-width)</li>
<li>Hintergrund leicht gedimmt: <code>rgba(28,28,24,.45)</code>, Klick außerhalb schließt nicht (explizite Bestätigung erforderlich)</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S5 — Einladekachel: Einlade-Panel</div>
<div class="state">
<div class="state-header">
<div class="state-id">S5</div>
<div>
<div class="state-title">Einlade-Panel — nach Klick auf die Einladekachel</div>
<div class="state-desc">Kachel expandiert zum Panel unterhalb der Grid-Reihe. Zeigt generierten Link + Ablaufdatum + Regenerieren-Option.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar" style="width:224px;min-width:224px;">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Mitglieder</div>
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
<div class="member-grid">
<div class="member-card own"><div class="avatar avatar-planer">MR</div><div class="member-name">Marcel R.</div><span class="role-badge planer">Planer</span><div class="join-date">seit 02.04.2026</div><div style="margin-top:8px;"><span class="self-badge">Du</span></div></div>
<div class="member-card"><button class="kebab-btn"></button><div class="avatar avatar-mitglied">SR</div><div class="member-name">Sandra R.</div><span class="role-badge mitglied">Mitglied</span><div class="join-date">seit 03.04.2026</div></div>
<div class="member-card"><button class="kebab-btn"></button><div class="avatar avatar-mitglied">LR</div><div class="member-name">Lena R.</div><span class="role-badge mitglied">Mitglied</span><div class="join-date">seit 05.04.2026</div></div>
<div class="invite-card" style="border-color:var(--green-light);background:var(--green-tint);">
<div class="invite-plus" style="background:var(--green-light);color:var(--green-dark);">+</div>
<div class="invite-label" style="color:var(--green-dark);">Mitglied einladen</div>
</div>
</div>
<!-- Invite panel below grid -->
<div class="invite-panel" style="margin-top:16px;">
<div class="invite-panel-title">Einladelink teilen</div>
<div class="invite-panel-desc">Wer diesen Link öffnet, kann dem Haushalt als Mitglied beitreten.</div>
<div class="invite-link-row">
<div class="invite-link-box">https://mealplan.app/join/X4K9-RZMQ</div>
<button class="btn-copy">Kopieren</button>
</div>
<div class="invite-expiry">Läuft ab: <span>12.04.2026</span></div>
<button class="btn-regen">Neuen Link generieren</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Klick auf Einladekachel → POST /v1/households/mine/invites (falls kein aktiver Code vorhanden) oder GET /v1/households/mine/invites</li>
<li>Invite-Panel erscheint unterhalb der Grid-Reihe (kein Modal, kein Page-Change)</li>
<li>"Kopieren" → navigator.clipboard.writeText(shareUrl) → Button zeigt kurz "Kopiert ✓"</li>
<li>"Neuen Link generieren" → POST /v1/households/mine/invites → alten Code invalidieren → neuen Code anzeigen</li>
<li>Ablaufdatum <code>expiresAt</code> in gelbem Badge wenn ≤ 24h verbleibend</li>
<li>Nur Planer sehen den Einlade-CTA. Mitglied sieht keine Einladekachel.</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S6 — Mitglied-Perspektive (read-only)</div>
<div class="state">
<div class="state-header">
<div class="state-id">S6</div>
<div>
<div class="state-title">Ansicht als Haushaltsmitglied (rolle = mitglied)</div>
<div class="state-desc">Mitglieder sehen die Kacheln ohne Kebab-Menü und ohne Einladekachel.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop — Mitglied-Perspektive</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar" style="width:224px;min-width:224px;">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Plan</div>
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Mitglieder</div>
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
<div class="member-grid" style="grid-template-columns:repeat(3,1fr);">
<div class="member-card own">
<div class="avatar avatar-mitglied">SR</div>
<div class="member-name">Sandra R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 03.04.2026</div>
<div style="margin-top:8px;"><span class="self-badge">Du</span></div>
</div>
<div class="member-card">
<div class="avatar avatar-planer">MR</div>
<div class="member-name">Marcel R.</div>
<span class="role-badge planer">Planer</span>
<div class="join-date">seit 02.04.2026</div>
</div>
<div class="member-card">
<div class="avatar avatar-mitglied">LR</div>
<div class="member-name">Lena R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 05.04.2026</div>
</div>
<!-- No invite card for members -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Mitglied sieht keine Einladekachel und keine Kebab-Buttons auf anderen Kacheln</li>
<li>Eigene Kachel zeigt "Du"-Badge (grüner Rahmen), aber kein Kebab</li>
<li>Grid passt sich an: bei 3 Kacheln → <code>grid-template-columns: repeat(3, 1fr)</code> (kein leerer Slot für Einladen)</li>
<li>Server-seitige Prüfung: Aktionen (DELETE, PATCH) geben 403 für nicht-Planer zurück</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<!-- ─── Machine-readable agent section ─── -->
<div class="agent-section">
<h2>Maschinen-lesbare Spezifikation</h2>
<p>Diese Sektion enthält verbindliche Implementierungsregeln für den Coding-Agenten.</p>
<pre class="spec-comment">
/* spec:rules — E2 Mitglieder Kachel
*
* LAYOUT
* grid: repeat(4, 1fr) gap 16px desktop; repeat(2, 1fr) gap 12px mobile
* card: bg white, border 1px solid --color-border, border-radius --radius-xl
* card padding: 24px 20px 20px desktop; 16px mobile
*
* AVATAR
* size: 56px desktop / 44px mobile; border-radius 50%
* initials: first two chars of displayName, uppercase
* planer color: --green-dark (#2E6E39)
* mitglied color: --blue (#185FA5)
*
* ROLE BADGE
* planer: bg --green-tint, color --green-dark
* mitglied: bg --blue-tint, color --blue-dark
* font-size 10px, font-weight 500, padding 2px 8px, border-radius --radius-full
*
* OWN CARD (benutzer.id === member.userId)
* border-color: --green-light
* show "Du" badge below join-date
* hide kebab button entirely
*
* KEBAB BUTTON
* position absolute, top 12px, right 12px
* opacity 0 by default; 1 on card:hover, card:focus-within, touch devices always 1
* opens dropdown: [Rolle ändern, divider, Entfernen(danger)]
* click-away closes; ESC closes
*
* ROLE CHANGE (S3)
* replaces badge in-place with segmented control [Planer | Mitglied]
* active button: bg --green-dark, color white
* inactive button: bg white, color --color-text-muted
* on select: PATCH /v1/households/mine/members/{userId} body { role }
* optimistic update; on error: rollback + toast
* Abbrechen link below control: reverts to badge without API call
* guard: planer cannot demote self if sole planer
*
* REMOVE CONFIRM (S4)
* modal dialog, backdrop rgba(28,28,24,.45), backdrop does NOT close on click
* shows member displayName in body text
* confirm → DELETE /v1/households/mine/members/{userId}
* on success: remove card from grid with fade-out
* mobile: bottom-sheet (border-radius top only)
*
* INVITE (S5)
* invite card always last in grid, only visible to planer
* click → POST /v1/households/mine/invites OR GET /v1/households/mine/invites
* panel below grid (not modal)
* copy: navigator.clipboard.writeText(shareUrl) → button text "Kopiert ✓" for 2s
* regenerate: POST new invite → invalidate old
* expiresAt badge yellow if ≤ 24h remaining
*
* MEMBER VIEW (S6)
* rolle === 'mitglied': hide all kebab buttons, hide invite card
* grid auto-adjusts columns (no empty slot)
*
* CARD ORDER
* 1. own card (benutzer.id === userId)
* 2. other members sorted by joinedAt ASC
* 3. invite card (planer only)
*
* BACKEND GAPS (must exist before page ships)
* DELETE /v1/households/mine/members/{userId}
* PATCH /v1/households/mine/members/{userId} body: { role: "planer"|"mitglied" }
* GET /v1/households/mine/invites
*/
</pre>
<table class="agent-table">
<thead>
<tr><th>Property</th><th>Value</th><th>Notes</th></tr>
</thead>
<tbody>
<tr class="group-row"><td colspan="3">Component: MemberCard</td></tr>
<tr><td>card-width</td><td>1fr (grid)</td><td>4-col desktop, 2-col mobile</td></tr>
<tr><td>card-min-height</td><td>180px</td><td>desktop; auto mobile</td></tr>
<tr><td>avatar-size</td><td>56px / 44px</td><td>desktop / mobile</td></tr>
<tr><td>avatar-radius</td><td>50%</td><td>full circle</td></tr>
<tr><td>kebab-target</td><td>44×44px</td><td>WCAG 2.2 minimum touch target</td></tr>
<tr><td>dropdown-min-width</td><td>160px</td><td>right-aligned to kebab</td></tr>
<tr class="group-row"><td colspan="3">Role Control</td></tr>
<tr><td>control-height</td><td>32px</td><td>segmented, full card width</td></tr>
<tr><td>active-bg</td><td>--green-dark</td><td>selected role button</td></tr>
<tr><td>api-endpoint</td><td>PATCH /v1/households/mine/members/{userId}</td><td>body: { role }</td></tr>
<tr class="group-row"><td colspan="3">Remove Dialog</td></tr>
<tr><td>confirm-btn-bg</td><td>--color-error (#DC4C3E)</td><td>danger action</td></tr>
<tr><td>api-endpoint</td><td>DELETE /v1/households/mine/members/{userId}</td><td></td></tr>
<tr><td>backdrop</td><td>rgba(28,28,24,.45)</td><td>click-outside does NOT close</td></tr>
<tr class="group-row"><td colspan="3">Invite</td></tr>
<tr><td>api-create</td><td>POST /v1/households/mine/invites</td><td>returns InviteResponse</td></tr>
<tr><td>api-list</td><td>GET /v1/households/mine/invites</td><td>backend gap</td></tr>
<tr><td>copy-feedback</td><td>"Kopiert ✓" for 2000ms</td><td>then revert to "Kopieren"</td></tr>
</tbody>
</table>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,981 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Recipe App — E4 Vielfalt-Einstellungen · Implementierungsspezifikation</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"/>
<style>
:root{--color-page:#FAFAF7;--color-surface:#F5F4EE;--color-subtle:#EDECEA;--color-border:#D8D7D0;--color-text-muted:#6B6A63;--color-text:#1C1C18;--green-tint:#E8F5EA;--green-light:#AEDCB0;--green:#3D8C4A;--green-dark:#2E6E39;--green-deeper:#1E4A26;--yellow-tint:#FDF6D8;--yellow-light:#F9E08A;--yellow:#F2C12E;--yellow-dark:#C49610;--yellow-text:#8A6800;--blue-tint:#E6F1FB;--blue-light:#A4CFF4;--blue:#2D7DD2;--blue-dark:#185FA5;--purple-tint:#EEEDFE;--purple-light:#CECBF6;--purple:#534AB7;--purple-dark:#3C3489;--orange-tint:#FEF0E6;--orange:#E8862A;--orange-dark:#B46820;--red-tint:#FDECEA;--red:#DC4C3E;--red-dark:#B03328;--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;--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);--shadow-overlay:0 8px 32px rgba(28,28,24,.12),0 2px 8px rgba(28,28,24,.06);}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
body{font-family:var(--font-sans);background:#E8E7E2;color:var(--color-text);font-size:14px;line-height:1.6;}
.doc{max-width:1200px;margin:0 auto;padding:48px 40px 120px;}
.doc-header{display:flex;justify-content:space-between;align-items:flex-end;padding-bottom:28px;border-bottom:1px solid var(--color-border);margin-bottom:48px;background:var(--color-page);margin:-48px -40px 48px;padding:48px 40px 28px;border-radius:var(--radius-xl) var(--radius-xl) 0 0;}
.doc-header h1{font-family:var(--font-display);font-size:28px;font-weight:500;letter-spacing:-.02em;margin-bottom:4px;}
.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;}
.pill{display:inline-block;padding:2px 8px;border-radius:var(--radius-sm);font-size:10px;font-weight:500;letter-spacing:.05em;background:var(--green-tint);color:var(--green-dark);}
.section{margin-bottom:64px;}
.section-title{font-size:10px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--color-text-muted);padding-bottom:10px;border-bottom:1px solid var(--color-border);margin-bottom:24px;}
.prose{font-size:13px;color:var(--color-text-muted);line-height:1.65;max-width:720px;margin-bottom:20px;}
/* Journey header */
.jh{padding:20px 24px;border-radius:var(--radius-xl);margin-bottom:40px;display:flex;align-items:center;gap:16px;}
.jh .jn{font-family:var(--font-display);font-size:48px;font-weight:300;line-height:1;opacity:.5;}
.jh h2{font-family:var(--font-display);font-size:22px;font-weight:500;letter-spacing:-.02em;margin-bottom:4px;}
.jh p{font-size:13px;line-height:1.5;}.jh .fl{font-family:var(--font-mono);font-size:11px;margin-top:6px;opacity:.7;}
.jh-p{background:var(--purple-tint);border:1px solid var(--purple-light);}.jh-p .jn{color:var(--purple);}.jh-p p,.jh-p .fl{color:var(--purple-dark);}
/* Screen block */
.scr{margin-bottom:56px;}
.scr-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;}
.scr-head h3{font-family:var(--font-display);font-size:20px;font-weight:500;letter-spacing:-.02em;}
.scr-id{font-family:var(--font-mono);font-size:11px;color:var(--color-text-muted);padding:2px 8px;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-page);}
.scr-desc{font-size:12px;color:var(--color-text-muted);line-height:1.6;max-width:720px;margin-bottom:6px;}
.scr-var{font-size:11px;color:var(--color-text-muted);margin-bottom:20px;}.scr-var strong{color:var(--color-text);}
/* Device frames */
.previews{display:flex;gap:32px;flex-wrap:wrap;justify-content:center;align-items:flex-start;margin-bottom:20px;}
.prev-col{display:flex;flex-direction:column;align-items:center;gap:10px;}
.bp-lbl{font-family:var(--font-mono);font-size:10px;color:var(--color-text-muted);}
.phone{width:320px;flex-shrink:0;background:var(--color-page);border-radius:36px;overflow:hidden;box-shadow:var(--shadow-overlay),0 0 0 1px rgba(0,0,0,.07);display:flex;flex-direction:column;border:6px solid #1C1C18;}
.pst{padding:10px 20px 0;display:flex;justify-content:space-between;align-items:center;font-size:12px;background:var(--color-page);}.pst b{font-weight:600;}.pst span{font-size:10px;}
.pb{flex:1;overflow-y:auto;display:flex;flex-direction:column;}
.desk{width:100%;max-width:1040px;background:var(--color-page);border-radius:var(--radius-xl);overflow:hidden;box-shadow:var(--shadow-overlay),0 0 0 1px rgba(0,0,0,.06);display:flex;min-height:520px;}
/* Agent spec block */
.agent{background:var(--color-text);color:#E8E8E2;padding:24px;border-radius:var(--radius-lg);margin-top:20px;}
.agent h4{font-size:9px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:#5A5A55;margin-bottom:12px;}
.agent pre{font-family:var(--font-mono);font-size:10px;color:#444440;margin-bottom:16px;line-height:1.8;white-space:pre-wrap;}
.at{width:100%;border-collapse:collapse;font-family:var(--font-mono);font-size:10px;}
.at thead tr{border-bottom:1px solid #2A2A26;}.at th{text-align:left;padding:6px 10px;font-size:8px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:#5A5A55;font-family:var(--font-sans);}.at td{padding:5px 10px;border-bottom:1px solid #1E1E1A;vertical-align:top;line-height:1.5;}.at tr:last-child td{border-bottom:none;}.at td:first-child{color:#7A7A72;}.at td:nth-child(2){color:#E8E8E2;font-weight:500;}.at td:nth-child(3){color:#5A5A55;}.at .grp td{padding-top:14px;font-family:var(--font-sans);font-size:8px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:#3A3A36;}
/* LLM section */
.llm{background:var(--color-page);border:2px solid var(--green);border-radius:var(--radius-xl);padding:32px 40px;margin-top:64px;}
.llm h2{font-family:var(--font-display);font-size:22px;font-weight:500;letter-spacing:-.02em;margin-bottom:8px;color:var(--green-dark);}
.llm h3{font-size:14px;font-weight:600;margin:20px 0 8px;color:var(--color-text);}
.llm p,.llm li{font-size:13px;color:var(--color-text-muted);line-height:1.65;}
.llm ul,.llm ol{padding-left:20px;margin-bottom:12px;}
.llm li{margin-bottom:4px;}
.llm code{font-family:var(--font-mono);font-size:11px;background:var(--color-surface);padding:1px 5px;border-radius:3px;}
.llm table{width:100%;border-collapse:collapse;margin:12px 0;font-size:12px;}
.llm th,.llm td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--color-border);}
.llm th{font-weight:500;color:var(--color-text);font-size:11px;text-transform:uppercase;letter-spacing:.05em;}
.llm td{color:var(--color-text-muted);}
/* Shared nav chrome */
.mtb{padding:10px 16px;background:var(--color-page);border-bottom:1px solid var(--color-border);display:flex;align-items:center;gap:10px;}
.mtb-back{font-size:12px;color:var(--color-text-muted);display:flex;align-items:center;gap:4px;flex-shrink:0;}
.mtb-t{font-family:var(--font-display);font-size:18px;font-weight:500;letter-spacing:-.02em;flex:1;}
.mbt{border-top:1px solid var(--color-border);background:var(--color-surface);padding:8px 16px 28px;display:flex;justify-content:space-around;flex-shrink:0;}
.mt-i{display:flex;flex-direction:column;align-items:center;gap:2px;}
.mt-ic{width:20px;height:20px;border-radius:4px;background:var(--color-subtle);display:flex;align-items:center;justify-content:center;font-size:11px;}
.mt-i.a .mt-ic{background:var(--green-tint);}
.mt-l{font-size:9px;font-weight:500;color:var(--color-text-muted);}
.mt-i.a .mt-l{color:var(--green-dark);}
.dsb{width:224px;flex-shrink:0;background:var(--color-surface);border-right:1px solid var(--color-border);display:flex;flex-direction:column;}
.dsb-logo{padding:20px 16px 16px;border-bottom:1px solid var(--color-border);}
.dsb-lm{display:flex;align-items:center;gap:8px;margin-bottom:2px;}
.dsb-ic{width:24px;height:24px;border-radius:5px;background:var(--green);display:flex;align-items:center;justify-content:center;font-size:12px;}
.dsb-nm{font-family:var(--font-display);font-size:16px;font-weight:500;letter-spacing:-.02em;}
.dsb-sub{font-size:10px;color:var(--color-text-muted);padding-left:32px;}
.dsb-nav{padding:12px 10px;flex:1;}
.dsb-nl{font-size:8px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);padding:0 8px;margin-bottom:4px;}
.dsb-ni{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:var(--radius-md);font-size:13px;color:var(--color-text-muted);margin-bottom:2px;}
.dsb-ni.a{background:var(--green-tint);color:var(--green-dark);font-weight:500;}
.dsb-nc{font-size:13px;width:18px;text-align:center;}
.dm{flex:1;display:flex;flex-direction:column;min-width:0;}
.dtb{padding:14px 24px;border-bottom:1px solid var(--color-border);display:flex;justify-content:space-between;align-items:center;flex-shrink:0;}
.dtb-t{font-family:var(--font-display);font-size:18px;font-weight:500;letter-spacing:-.02em;}
.dtb-bc{font-size:12px;color:var(--color-text-muted);display:flex;align-items:center;gap:6px;margin-bottom:2px;}
.dtb-bc span{color:var(--color-border);}
.dmc{padding:24px;flex:1;overflow-y:auto;}
/* UI components */
.tog{display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid var(--color-subtle);}
.tog:last-child{border-bottom:none;}
.tog-l{display:flex;flex-direction:column;gap:2px;}
.tog-name{font-size:13px;font-weight:500;}
.tog-hint{font-size:11px;color:var(--color-text-muted);}
.tog-sw{width:36px;height:20px;border-radius:10px;background:var(--green);flex-shrink:0;position:relative;}
.tog-sw::after{content:'';position:absolute;width:16px;height:16px;border-radius:50%;background:#fff;top:2px;right:2px;box-shadow:0 1px 3px rgba(0,0,0,.2);}
.tog-sw.off{background:var(--color-border);}
.tog-sw.off::after{right:auto;left:2px;}
.seg{display:flex;border:1px solid var(--color-border);border-radius:var(--radius-md);overflow:hidden;background:var(--color-surface);}
.seg-o{flex:1;text-align:center;font-size:11px;font-weight:500;padding:6px 0;color:var(--color-text-muted);}
.seg-o.a{background:var(--color-page);color:var(--color-text);box-shadow:var(--shadow-card);}
.seg-o.a-r{background:var(--red-tint);color:var(--red-dark);}
.seg-o.a-g{background:var(--green-tint);color:var(--green-dark);}
.grp{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;margin-bottom:12px;}
.grp-hd{padding:10px 14px;border-bottom:1px solid var(--color-border);background:var(--color-subtle);}
.grp-hd-t{font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--color-text-muted);}
.grp-b{padding:0 14px;}
.wr{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 0;border-bottom:1px solid var(--color-subtle);}
.wr:last-child{border-bottom:none;}
.wr-l{display:flex;flex-direction:column;gap:1px;min-width:0;}
.wr-name{font-size:12px;font-weight:500;}
.wr-sub{font-size:10px;color:var(--color-text-muted);}
/* Context chips */
.ctx-chips{display:flex;gap:8px;margin-bottom:16px;}
.ctx-chip{flex:1;padding:14px 12px;border-radius:var(--radius-xl);border:1.5px solid var(--color-border);background:var(--color-surface);display:flex;flex-direction:column;gap:3px;cursor:default;}
.ctx-chip.sel{border-color:var(--green-light);background:var(--green-tint);}
.ctx-chip.ind{border-color:var(--purple-light);background:var(--purple-tint);}
.ctx-em{font-size:18px;}
.ctx-name{font-size:12px;font-weight:600;color:var(--color-text);}
.ctx-chip.sel .ctx-name{color:var(--green-dark);}
.ctx-chip.ind .ctx-name{color:var(--purple-dark);}
.ctx-sub{font-size:10px;color:var(--color-text-muted);line-height:1.3;}
.ctx-chip.sel .ctx-sub{color:var(--green-dark);opacity:.7;}
.ctx-chip.ind .ctx-sub{color:var(--purple-dark);opacity:.7;}
/* Summary pills */
.s-pills{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:14px;}
.s-pill{font-size:10px;font-weight:500;padding:3px 8px;border-radius:20px;display:flex;align-items:center;gap:3px;}
.s-pill.on{background:var(--green-tint);color:var(--green-dark);}
.s-pill.off{background:var(--color-subtle);color:var(--color-text-muted);}
.s-pill.warn{background:var(--yellow-tint);color:var(--yellow-text);}
/* Accordion */
.acc{border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;margin-bottom:12px;}
.acc-hd{padding:12px 14px;display:flex;justify-content:space-between;align-items:center;background:var(--color-surface);}
.acc-hd-t{font-size:13px;font-weight:500;}
.acc-hd-r{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--color-text-muted);}
.acc-b{padding:14px;border-top:1px solid var(--color-border);background:var(--color-page);}
/* Score preview */
.score-banner{background:var(--color-text);border-radius:var(--radius-lg);padding:14px 16px;margin-bottom:14px;display:flex;align-items:center;justify-content:space-between;gap:12px;}
.score-banner-l{}
.score-banner-label{font-size:10px;color:#6B6A63;margin-bottom:2px;}
.score-banner-val{font-family:var(--font-display);font-size:30px;font-weight:300;letter-spacing:-.02em;color:#E8E8E2;line-height:1;}
.score-banner-sub{font-size:10px;margin-top:3px;}
.score-banner-up{color:#6FCF97;}
.score-banner-neutral{color:#6B6A63;}
.score-banner-r{font-size:28px;opacity:.7;}
/* Summary detail rows (desktop right column) */
.sum-rows{display:flex;flex-direction:column;gap:5px;}
.sum-row{display:flex;justify-content:space-between;align-items:center;padding:7px 10px;border-radius:var(--radius-md);font-size:12px;}
.sum-row.on{background:var(--green-tint);}
.sum-row.off{background:var(--color-subtle);}
.sum-row.warn{background:var(--yellow-tint);}
.sum-row-name{font-weight:500;}
.sum-row.on .sum-row-name{color:var(--green-dark);}
.sum-row.off .sum-row-name{color:var(--color-text-muted);}
.sum-row.warn .sum-row-name{color:var(--yellow-text);}
.sum-row-val{font-size:10px;font-weight:500;}
.sum-row.on .sum-row-val{color:var(--green-dark);}
.sum-row.off .sum-row-val{color:var(--color-text-muted);}
.sum-row.warn .sum-row-val{color:var(--yellow-text);}
/* Reset link */
.reset-link{font-size:12px;color:var(--red-dark);padding:10px 0;display:block;text-align:center;}
/* Divider */
.sec-lbl{font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:8px;}
/* Modal overlay */
.overlay{position:relative;border-radius:var(--radius-xl);overflow:hidden;}
.modal-backdrop{position:absolute;inset:0;background:rgba(28,28,24,.4);display:flex;align-items:center;justify-content:center;padding:24px;}
.modal{background:var(--color-page);border-radius:var(--radius-xl);padding:24px;width:100%;max-width:280px;box-shadow:var(--shadow-overlay);}
.modal-title{font-family:var(--font-display);font-size:18px;font-weight:500;letter-spacing:-.02em;margin-bottom:8px;}
.modal-body{font-size:13px;color:var(--color-text-muted);line-height:1.6;margin-bottom:20px;}
.modal-acts{display:flex;flex-direction:column;gap:8px;}
.btn-dest{padding:11px 16px;border-radius:var(--radius-md);background:var(--red);color:#fff;font-weight:500;font-size:13px;text-align:center;}
.btn-ghost{padding:11px 16px;border-radius:var(--radius-md);background:var(--color-surface);border:1px solid var(--color-border);color:var(--color-text-muted);font-weight:500;font-size:13px;text-align:center;}
/* E1 hub card */
.hub-grid{display:grid;grid-template-columns:2fr 1fr;gap:12px;margin-bottom:12px;}
.hub-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:16px;}
.hub-card.primary{border-left:3px solid var(--green-dark);}
.hub-card.variety{border-left:3px solid var(--purple);}
.hub-stat{font-family:var(--font-display);font-size:36px;font-weight:300;letter-spacing:-.02em;line-height:1;margin-bottom:4px;}
.hub-stat.green{color:var(--green-dark);}
.hub-stat.purple{color:var(--purple);}
.hub-name{font-size:12px;font-weight:500;margin-bottom:2px;}
.hub-sub{font-size:11px;color:var(--color-text-muted);}
.hub-arr{font-size:12px;color:var(--color-text-muted);margin-top:10px;}
/* Settings hub bottom row */
.hub-row{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
</style>
</head>
<body>
<div class="doc">
<!-- ── Header ── -->
<div class="doc-header">
<div>
<h1>E4 — Vielfalt-Einstellungen</h1>
<p>Implementierungsspezifikation · V2 Kontext-Preset · Journey J9</p>
</div>
<div class="doc-meta">
<span class="pill">v1.0</span><br>
Screens: E1 (Update) + E4<br>
States: 5<br>
Rolle: Planer only
</div>
</div>
<!-- ── Journey context ── -->
<div class="jh jh-p">
<div class="jn">J9</div>
<div>
<h2>Vielfalt-Algorithmus konfigurieren</h2>
<p>Planer passt Bewertungsregeln an den Haushaltskontext an — primär das Deaktivieren der Protein-Prüfung für vegetarische Haushalte.</p>
<div class="fl">E1 → E4 → C3 · Planer only · Auto-Save · Reset benötigt Bestätigung</div>
</div>
</div>
<!-- ════════════════════════════════════════
E1 — SETTINGS HUB UPDATE
═════════════════════════════════════════ -->
<div class="section">
<div class="section-title">E1 — Settings-Hub (Update)</div>
<p class="prose">Der bestehende Settings-Hub (E1) erhält eine dritte Kachel: "Vielfalt-Einstellungen". Die Kachel zeigt den aktuellen Vielfalt-Score als Kennzahl. Das Grid-Layout wird von 2-spaltig zu einem Mix aus Hauptkachel oben und zwei gleichbreiten Kacheln unten angepasst.</p>
<!-- S0: E1 Hub -->
<div class="scr">
<div class="scr-head"><h3>S0 · Settings-Hub mit Vielfalt-Kachel</h3><span class="scr-id">E1</span></div>
<div class="scr-desc">Die neue Vielfalt-Kachel erscheint in der unteren Reihe neben der Haushalt-Kachel. Zeigt den aktuellen Score als lila Kennzahl. Bei Score &lt; 6.0 färbt sich die Kennzahl orange als Aufmerksamkeitshinweis.</div>
<div class="scr-var"><strong>Änderung gegenüber E1 v1:</strong> dritte Kachel + Grid-Anpassung. Vorräte-Kachel bleibt primär (2fr oben).</div>
<div class="previews">
<div class="prev-col">
<div class="bp-lbl">Mobile · 320px</div>
<div class="phone">
<div class="pst"><b>9:41</b><span>●●●</span></div>
<div class="pb">
<div class="mtb"><div class="mtb-t">Einstellungen</div></div>
<div style="padding:16px;flex:1;">
<!-- Vorräte (primary, full width) -->
<div class="hub-card primary" style="margin-bottom:12px;">
<div class="hub-stat green">12</div>
<div class="hub-name">Vorräte</div>
<div class="hub-sub">Zutaten immer vorrätig</div>
<div class="hub-arr">Bearbeiten →</div>
</div>
<!-- Bottom row: 2 cards -->
<div class="hub-row">
<div class="hub-card">
<div class="hub-stat" style="font-size:28px;color:var(--blue-dark);">3</div>
<div class="hub-name">Haushalt</div>
<div class="hub-sub">Mitglieder</div>
<div class="hub-arr">Verwalten →</div>
</div>
<div class="hub-card variety">
<div class="hub-stat purple" style="font-size:28px;">7.4</div>
<div class="hub-name">Vielfalt</div>
<div class="hub-sub">Diese Woche</div>
<div class="hub-arr">Einstellungen →</div>
</div>
</div>
</div>
<div class="mbt">
<div class="mt-i"><div class="mt-ic">📅</div><div class="mt-l">Plan</div></div>
<div class="mt-i"><div class="mt-ic">🛒</div><div class="mt-l">Einkauf</div></div>
<div class="mt-i"><div class="mt-ic">🍳</div><div class="mt-l">Rezepte</div></div>
<div class="mt-i a"><div class="mt-ic">⚙️</div><div class="mt-l">Einstellungen</div></div>
</div>
</div>
</div>
</div>
<div class="prev-col" style="flex:1;min-width:600px;">
<div class="bp-lbl">Desktop · 1040px</div>
<div class="desk">
<div class="dsb">
<div class="dsb-logo"><div class="dsb-lm"><div class="dsb-ic">🥦</div><div class="dsb-nm">Mealprep</div></div><div class="dsb-sub">Familie Raddatz</div></div>
<div class="dsb-nav">
<div class="dsb-nl">Planung</div>
<div class="dsb-ni"><span class="dsb-nc">📅</span> Wochenplan</div>
<div class="dsb-ni"><span class="dsb-nc">🛒</span> Einkaufsliste</div>
<div class="dsb-ni"><span class="dsb-nc">🍳</span> Rezepte</div>
<div class="dsb-nl" style="margin-top:12px;">Haushalt</div>
<div class="dsb-ni a"><span class="dsb-nc">⚙️</span> Einstellungen</div>
</div>
</div>
<div class="dm">
<div class="dtb"><div class="dtb-t">Einstellungen</div></div>
<div class="dmc">
<div style="max-width:640px;">
<!-- Vorräte primary -->
<div class="hub-card primary" style="margin-bottom:16px;display:flex;align-items:center;justify-content:space-between;">
<div>
<div class="hub-stat green">12</div>
<div class="hub-name">Vorräte</div>
<div class="hub-sub">Zutaten, die immer vorrätig sind und nicht auf die Einkaufsliste kommen</div>
</div>
<div style="font-size:13px;font-weight:500;color:var(--green-dark);">Bearbeiten →</div>
</div>
<!-- Bottom row: 2 cards -->
<div class="hub-row">
<div class="hub-card" style="display:flex;align-items:center;justify-content:space-between;">
<div>
<div class="hub-stat" style="font-size:28px;color:var(--blue-dark);">3</div>
<div class="hub-name">Haushalt</div>
<div class="hub-sub">Mitglieder &amp; Rollen</div>
</div>
<div style="font-size:13px;font-weight:500;color:var(--blue-dark);">Verwalten →</div>
</div>
<div class="hub-card variety" style="display:flex;align-items:center;justify-content:space-between;">
<div>
<div class="hub-stat purple" style="font-size:28px;">7.4</div>
<div class="hub-name">Vielfalt-Einstellungen</div>
<div class="hub-sub">Algorithmus anpassen</div>
</div>
<div style="font-size:13px;font-weight:500;color:var(--purple-dark);">Einstellungen →</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="agent">
<h4>E1 Hub Update · S0</h4>
<pre>/* E1 grid: Vorräte (full width, 2fr, border-left: 3px solid --green-dark) on top row.
* Bottom row: 2 equal columns — Haushalt + Vielfalt-Einstellungen.
* Vielfalt card: border-left: 3px solid --purple. Stat color: --purple (7.4).
* If score < 6.0: stat color switches to --orange (Aufmerksamkeit) with no other change.
* Score value: load from GET /v1/week-plans?weekStart=current GET /v1/week-plans/{id}/variety-score.
* If no current plan: show "" as stat value, sub: "Kein Plan".
* Tap/click Vielfalt card navigate to E4. */</pre>
<table class="at"><thead><tr><th>Element</th><th>Wert</th><th>Notizen</th></tr></thead><tbody>
<tr class="grp"><td colspan="3">Vielfalt-Kachel</td></tr>
<tr><td>Kennzahl</td><td>varietyScore.score, 1 Dezimalstelle</td><td>Farbe: --purple normal, --orange wenn &lt; 6.0</td></tr>
<tr><td>Label</td><td>Vielfalt-Einstellungen</td><td>Desktop; Mobile: "Vielfalt"</td></tr>
<tr><td>Sub-Label</td><td>"Diese Woche" / "Kein Plan" / ""</td><td>Kein Plan = kein weekPlan für aktuelle Woche</td></tr>
<tr><td>Rand</td><td>border-left: 3px solid --purple</td><td>Analog zu Vorräte → --green-dark</td></tr>
<tr><td>Aktion</td><td>Tap → navigate /settings/variety</td><td>Route: +page.svelte unter (app)/settings/variety/</td></tr>
<tr class="grp"><td colspan="3">Grid-Layout</td></tr>
<tr><td>Mobile</td><td>Vorräte fullwidth + grid-template-columns: 1fr 1fr unten</td><td>Gap: 12px</td></tr>
<tr><td>Desktop</td><td>Vorräte fullwidth + grid-template-columns: 1fr 1fr unten</td><td>Max-width: 640px, gap: 16px</td></tr>
</tbody></table>
</div>
</div>
</div>
<!-- ════════════════════════════════════════
S1 — DEFAULT (KEIN CUSTOM-CONFIG)
═════════════════════════════════════════ -->
<div class="section">
<div class="section-title">E4 — Vielfalt-Einstellungen · States</div>
<div class="scr">
<div class="scr-head"><h3>S1 · Standard (kein Custom-Config)</h3><span class="scr-id">E4</span></div>
<div class="scr-desc">Erster Aufruf, kein haushaltsindividueller Config-Eintrag. Omnivor-Chip ist ausgewählt (Default-Zustand). Score-Preview zeigt den aktuellen tatsächlichen Score — keine Simulation nötig, da noch nichts geändert wurde. Hinweis-Text erklärt kurz den Zweck der Seite.</div>
<div class="scr-var"><strong>S1</strong> · Omnivor selected · Score-Preview = aktueller Score · Erweiterte Einstellungen eingeklappt</div>
<div class="previews">
<div class="prev-col">
<div class="bp-lbl">Mobile · 320px</div>
<div class="phone">
<div class="pst"><b>9:41</b><span>●●●</span></div>
<div class="pb">
<div class="mtb"><div class="mtb-back">← Einstellungen</div><div class="mtb-t">Vielfalt</div></div>
<div style="padding:16px;flex:1;overflow-y:auto;">
<div style="font-size:12px;color:var(--color-text-muted);line-height:1.6;margin-bottom:14px;">Passe den Algorithmus an deinen Haushalt an. Änderungen werden sofort übernommen.</div>
<div class="sec-lbl">Haushaltskontext</div>
<div class="ctx-chips">
<div class="ctx-chip sel">
<div class="ctx-em">🥩</div>
<div class="ctx-name">Omnivor</div>
<div class="ctx-sub">Alle Regeln aktiv</div>
</div>
<div class="ctx-chip">
<div class="ctx-em">🥦</div>
<div class="ctx-name">Vegetarisch</div>
<div class="ctx-sub">Protein deaktiviert</div>
</div>
<div class="ctx-chip">
<div class="ctx-em">🌱</div>
<div class="ctx-name">Vegan</div>
<div class="ctx-sub">Protein deaktiviert</div>
</div>
</div>
<div class="sec-lbl">Aktive Regeln</div>
<div class="s-pills">
<div class="s-pill on">✓ Protein</div>
<div class="s-pill on">✓ Küche</div>
<div class="s-pill on">✓ Zutaten · Mittel</div>
<div class="s-pill on">✓ Letzte Wochen · Mittel</div>
<div class="s-pill warn">⚠ Duplikate · Hoch</div>
</div>
<div class="acc">
<div class="acc-hd">
<div class="acc-hd-t">Erweiterte Einstellungen</div>
<div class="acc-hd-r"></div>
</div>
</div>
<div class="score-banner">
<div class="score-banner-l">
<div class="score-banner-label">Aktueller Score</div>
<div class="score-banner-val">7.4</div>
<div class="score-banner-sub score-banner-neutral">Keine Änderungen</div>
</div>
<div class="score-banner-r">📊</div>
</div>
<div class="reset-link" style="color:var(--color-text-muted);">Bereits Standard-Einstellungen</div>
</div>
<div class="mbt">
<div class="mt-i"><div class="mt-ic">📅</div><div class="mt-l">Plan</div></div>
<div class="mt-i"><div class="mt-ic">🛒</div><div class="mt-l">Einkauf</div></div>
<div class="mt-i"><div class="mt-ic">🍳</div><div class="mt-l">Rezepte</div></div>
<div class="mt-i a"><div class="mt-ic">⚙️</div><div class="mt-l">Einstellungen</div></div>
</div>
</div>
</div>
</div>
<div class="prev-col" style="flex:1;min-width:600px;">
<div class="bp-lbl">Desktop · 1040px</div>
<div class="desk">
<div class="dsb">
<div class="dsb-logo"><div class="dsb-lm"><div class="dsb-ic">🥦</div><div class="dsb-nm">Mealprep</div></div><div class="dsb-sub">Familie Raddatz</div></div>
<div class="dsb-nav">
<div class="dsb-nl">Planung</div>
<div class="dsb-ni"><span class="dsb-nc">📅</span> Wochenplan</div>
<div class="dsb-ni"><span class="dsb-nc">🛒</span> Einkaufsliste</div>
<div class="dsb-ni"><span class="dsb-nc">🍳</span> Rezepte</div>
<div class="dsb-nl" style="margin-top:12px;">Haushalt</div>
<div class="dsb-ni a"><span class="dsb-nc">⚙️</span> Einstellungen</div>
</div>
</div>
<div class="dm">
<div class="dtb"><div><div class="dtb-bc">Einstellungen <span></span> Vielfalt-Einstellungen</div><div class="dtb-t">Vielfalt-Einstellungen</div></div></div>
<div class="dmc">
<div style="display:grid;grid-template-columns:1fr 280px;gap:24px;max-width:800px;">
<div>
<div style="font-size:12px;color:var(--color-text-muted);line-height:1.6;margin-bottom:16px;">Passe den Algorithmus an deinen Haushaltskontext an. Änderungen werden sofort übernommen und wirken sich auf den nächsten Score-Abruf aus.</div>
<div class="sec-lbl">Haushaltskontext</div>
<div class="ctx-chips" style="margin-bottom:20px;">
<div class="ctx-chip sel"><div class="ctx-em">🥩</div><div class="ctx-name">Omnivor</div><div class="ctx-sub">Alle Regeln aktiv</div></div>
<div class="ctx-chip"><div class="ctx-em">🥦</div><div class="ctx-name">Vegetarisch</div><div class="ctx-sub">Protein deaktiviert</div></div>
<div class="ctx-chip"><div class="ctx-em">🌱</div><div class="ctx-name">Vegan</div><div class="ctx-sub">Protein deaktiviert</div></div>
</div>
<div class="acc">
<div class="acc-hd"><div class="acc-hd-t">Erweiterte Einstellungen</div><div class="acc-hd-r"></div></div>
</div>
<div class="reset-link" style="text-align:left;color:var(--color-text-muted);">Bereits Standard-Einstellungen</div>
</div>
<div>
<div class="score-banner">
<div>
<div class="score-banner-label">Aktueller Score</div>
<div class="score-banner-val">7.4 <span style="font-size:13px;opacity:.5;">/ 10</span></div>
<div class="score-banner-sub score-banner-neutral">Keine Änderungen aktiv</div>
</div>
<div class="score-banner-r">📊</div>
</div>
<div class="sec-lbl">Aktive Regeln</div>
<div class="sum-rows">
<div class="sum-row on"><span class="sum-row-name">Protein</span><span class="sum-row-val">Mittel</span></div>
<div class="sum-row on"><span class="sum-row-name">Küche</span><span class="sum-row-val">Mittel</span></div>
<div class="sum-row on"><span class="sum-row-name">Zutaten</span><span class="sum-row-val">Niedrig</span></div>
<div class="sum-row on"><span class="sum-row-name">Letzte Wochen</span><span class="sum-row-val">Mittel</span></div>
<div class="sum-row warn"><span class="sum-row-name">Duplikate</span><span class="sum-row-val">Hoch</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="agent">
<h4>E4 · S1 Default</h4>
<pre>/* Load: GET /v1/households/mine/variety-config → 404 if no custom config.
* On 404: use defaults (Omnivor preset), show Omnivor chip as selected.
* Score banner: show actual GET /v1/week-plans/{id}/variety-score (no simulation).
* "Bereits Standard-Einstellungen" replaces reset link if no custom config exists.
* Accordion: closed. */</pre>
<table class="at"><thead><tr><th>Element</th><th>Wert</th><th>Notizen</th></tr></thead><tbody>
<tr class="grp"><td colspan="3">Laden</td></tr>
<tr><td>Config-Load</td><td>GET /v1/households/mine/variety-config</td><td>404 → Defaults verwenden, Omnivor selected</td></tr>
<tr><td>Score-Load</td><td>GET /v1/week-plans/{id}/variety-score</td><td>Nur wenn weekPlan existiert; sonst Score-Banner ausblenden</td></tr>
<tr class="grp"><td colspan="3">Kontext-Chips</td></tr>
<tr><td>Omnivor</td><td>repeatTagTypes: ["protein","cuisine"], alle Gewichte Standard</td><td>Default-Preset = backend defaults</td></tr>
<tr><td>Vegetarisch</td><td>repeatTagTypes: ["cuisine"], wTagRepeat Standard</td><td>Protein deaktiviert</td></tr>
<tr><td>Vegan</td><td>repeatTagTypes: ["cuisine"], wTagRepeat Standard</td><td>Identisch zu Vegetarisch in v1</td></tr>
<tr><td>Individuell</td><td>Erscheint automatisch wenn Advanced abweicht vom Preset</td><td>Kein manuell wählbarer Chip — nur automatisch</td></tr>
<tr class="grp"><td colspan="3">Score-Banner (S1)</td></tr>
<tr><td>Wert</td><td>Aktueller Score (keine Simulation)</td><td>Label: "Aktueller Score"</td></tr>
<tr><td>Sub-Label</td><td>"Keine Änderungen"</td><td>Neutral-Farbe (#6B6A63)</td></tr>
</tbody></table>
</div>
</div>
<!-- S2: Vegetarisch selected -->
<div class="scr">
<div class="scr-head"><h3>S2 · Vegetarisch ausgewählt — Score-Simulation</h3><span class="scr-id">E4</span></div>
<div class="scr-desc">Planer tippt auf "Vegetarisch". Config wird sofort per PATCH gespeichert. Score-Banner lädt die simulierte Punktzahl: wie würde der aktuelle Plan mit der neuen Config abschneiden. Delta wird grün hervorgehoben. Protein-Pill wechselt zu "off". Erweiterte Einstellungen zeigt Protein-Toggle als deaktiviert.</div>
<div class="scr-var"><strong>S2</strong> · Vegetarisch selected · Score-Preview = simuliert · Protein-Pill = off</div>
<div class="previews">
<div class="prev-col">
<div class="bp-lbl">Mobile · 320px</div>
<div class="phone">
<div class="pst"><b>9:41</b><span>●●●</span></div>
<div class="pb">
<div class="mtb"><div class="mtb-back">← Einstellungen</div><div class="mtb-t">Vielfalt</div></div>
<div style="padding:16px;flex:1;overflow-y:auto;">
<div style="font-size:12px;color:var(--color-text-muted);line-height:1.6;margin-bottom:14px;">Passe den Algorithmus an deinen Haushalt an. Änderungen werden sofort übernommen.</div>
<div class="sec-lbl">Haushaltskontext</div>
<div class="ctx-chips">
<div class="ctx-chip"><div class="ctx-em">🥩</div><div class="ctx-name">Omnivor</div><div class="ctx-sub">Alle Regeln aktiv</div></div>
<div class="ctx-chip sel"><div class="ctx-em">🥦</div><div class="ctx-name">Vegetarisch</div><div class="ctx-sub">Protein deaktiviert</div></div>
<div class="ctx-chip"><div class="ctx-em">🌱</div><div class="ctx-name">Vegan</div><div class="ctx-sub">Protein deaktiviert</div></div>
</div>
<div class="sec-lbl">Aktive Regeln</div>
<div class="s-pills">
<div class="s-pill off"> Protein</div>
<div class="s-pill on">✓ Küche</div>
<div class="s-pill on">✓ Zutaten · Mittel</div>
<div class="s-pill on">✓ Letzte Wochen · Mittel</div>
<div class="s-pill warn">⚠ Duplikate · Hoch</div>
</div>
<div class="acc">
<div class="acc-hd"><div class="acc-hd-t">Erweiterte Einstellungen</div><div class="acc-hd-r"></div></div>
</div>
<div class="score-banner">
<div class="score-banner-l">
<div class="score-banner-label">Mit diesen Einstellungen</div>
<div class="score-banner-val">8.9</div>
<div class="score-banner-sub score-banner-up">↑ +1.5 gegenüber vorher</div>
</div>
<div class="score-banner-r">📈</div>
</div>
<div class="reset-link">Auf Standard zurücksetzen</div>
</div>
<div class="mbt">
<div class="mt-i"><div class="mt-ic">📅</div><div class="mt-l">Plan</div></div>
<div class="mt-i"><div class="mt-ic">🛒</div><div class="mt-l">Einkauf</div></div>
<div class="mt-i"><div class="mt-ic">🍳</div><div class="mt-l">Rezepte</div></div>
<div class="mt-i a"><div class="mt-ic">⚙️</div><div class="mt-l">Einstellungen</div></div>
</div>
</div>
</div>
</div>
<div class="prev-col" style="flex:1;min-width:600px;">
<div class="bp-lbl">Desktop · 1040px</div>
<div class="desk">
<div class="dsb">
<div class="dsb-logo"><div class="dsb-lm"><div class="dsb-ic">🥦</div><div class="dsb-nm">Mealprep</div></div><div class="dsb-sub">Familie Raddatz</div></div>
<div class="dsb-nav">
<div class="dsb-nl">Planung</div>
<div class="dsb-ni"><span class="dsb-nc">📅</span> Wochenplan</div>
<div class="dsb-ni"><span class="dsb-nc">🛒</span> Einkaufsliste</div>
<div class="dsb-ni"><span class="dsb-nc">🍳</span> Rezepte</div>
<div class="dsb-nl" style="margin-top:12px;">Haushalt</div>
<div class="dsb-ni a"><span class="dsb-nc">⚙️</span> Einstellungen</div>
</div>
</div>
<div class="dm">
<div class="dtb"><div><div class="dtb-bc">Einstellungen <span></span> Vielfalt-Einstellungen</div><div class="dtb-t">Vielfalt-Einstellungen</div></div></div>
<div class="dmc">
<div style="display:grid;grid-template-columns:1fr 280px;gap:24px;max-width:800px;">
<div>
<div style="font-size:12px;color:var(--color-text-muted);line-height:1.6;margin-bottom:16px;">Passe den Algorithmus an deinen Haushaltskontext an. Änderungen werden sofort übernommen und wirken sich auf den nächsten Score-Abruf aus.</div>
<div class="sec-lbl">Haushaltskontext</div>
<div class="ctx-chips" style="margin-bottom:20px;">
<div class="ctx-chip"><div class="ctx-em">🥩</div><div class="ctx-name">Omnivor</div><div class="ctx-sub">Alle Regeln aktiv</div></div>
<div class="ctx-chip sel"><div class="ctx-em">🥦</div><div class="ctx-name">Vegetarisch</div><div class="ctx-sub">Protein deaktiviert</div></div>
<div class="ctx-chip"><div class="ctx-em">🌱</div><div class="ctx-name">Vegan</div><div class="ctx-sub">Protein deaktiviert</div></div>
</div>
<div class="acc">
<div class="acc-hd"><div class="acc-hd-t">Erweiterte Einstellungen</div><div class="acc-hd-r"></div></div>
</div>
<div class="reset-link" style="text-align:left;">Auf Standard zurücksetzen</div>
</div>
<div>
<div class="score-banner">
<div>
<div class="score-banner-label">Mit diesen Einstellungen</div>
<div class="score-banner-val">8.9 <span style="font-size:13px;opacity:.5;">/ 10</span></div>
<div class="score-banner-sub score-banner-up">↑ +1.5 gegenüber vorher</div>
</div>
<div class="score-banner-r">📈</div>
</div>
<div class="sec-lbl">Aktive Regeln</div>
<div class="sum-rows">
<div class="sum-row off"><span class="sum-row-name">Protein</span><span class="sum-row-val">Deaktiviert</span></div>
<div class="sum-row on"><span class="sum-row-name">Küche</span><span class="sum-row-val">Mittel</span></div>
<div class="sum-row on"><span class="sum-row-name">Zutaten</span><span class="sum-row-val">Niedrig</span></div>
<div class="sum-row on"><span class="sum-row-name">Letzte Wochen</span><span class="sum-row-val">Mittel</span></div>
<div class="sum-row warn"><span class="sum-row-name">Duplikate</span><span class="sum-row-val">Hoch</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="agent">
<h4>E4 · S2 Vegetarisch</h4>
<pre>/* On chip tap (Vegetarisch):
* 1. Optimistic UI: swap selected chip, update pills, update sum-rows immediately.
* 2. PATCH /v1/households/mine/variety-config { repeatTagTypes: ["cuisine"],
* wTagRepeat: 1.5, wIngredientOverlap: 0.3, wRecentRepeat: 1.0, wPlanDuplicate: 2.0 }
* 3. On PATCH success: fire GET /v1/week-plans/{id}/variety-score?simulate=true
* with same config body → update score-banner with simulated score + delta.
* 4. On PATCH error: rollback to previous chip selection + show toast "Fehler beim Speichern".
* Score-Banner during load: show spinner in place of val. */</pre>
<table class="at"><thead><tr><th>Element</th><th>Wert</th><th>Notizen</th></tr></thead><tbody>
<tr class="grp"><td colspan="3">Score-Banner (S2)</td></tr>
<tr><td>Label</td><td>"Mit diesen Einstellungen"</td><td>Statt "Aktueller Score"</td></tr>
<tr><td>Delta</td><td>"↑ +X.X gegenüber vorher"</td><td>Grün (#6FCF97) wenn positiv; rot wenn negativ; neutral wenn = 0</td></tr>
<tr><td>Simulation-Endpoint</td><td>POST /v1/week-plans/{id}/variety-score/simulate</td><td>Body: VarietyScoreConfig-Felder. Neuer Endpoint nötig (Backend-Task).</td></tr>
<tr><td>Kein Plan</td><td>Score-Banner ausblenden</td><td>Kein simulierter Score ohne Plan möglich</td></tr>
<tr class="grp"><td colspan="3">Chip-Preset Vegetarisch</td></tr>
<tr><td>repeatTagTypes</td><td>["cuisine"]</td><td>Protein entfernt</td></tr>
<tr><td>wTagRepeat</td><td>1.5 (Standard)</td><td>Unverändert</td></tr>
<tr><td>wIngredientOverlap</td><td>0.3 (Standard)</td><td>Unverändert</td></tr>
<tr><td>wRecentRepeat</td><td>1.0 (Standard)</td><td>Unverändert</td></tr>
<tr><td>wPlanDuplicate</td><td>2.0 (Standard)</td><td>Unverändert</td></tr>
</tbody></table>
</div>
</div>
<!-- S3: Erweiterte Einstellungen -->
<div class="scr">
<div class="scr-head"><h3>S3 · Erweiterte Einstellungen geöffnet</h3><span class="scr-id">E4</span></div>
<div class="scr-desc">Planer öffnet das Accordion "Erweiterte Einstellungen". Er sieht Segmented Controls (Niedrig / Mittel / Hoch) für jeden Gewichts-Parameter. Ändert er einen Wert, der nicht mehr dem aktuellen Preset entspricht, erscheint automatisch ein vierter Chip "Individuell" (lila) und ersetzt den aktiven Preset-Chip. Score-Banner aktualisiert sich nach jeder Änderung.</div>
<div class="scr-var"><strong>S3</strong> · Erweiterte Einstellungen offen · "Individuell"-Chip erschienen (Planer hat Zutaten-Gewicht angepasst)</div>
<div class="previews">
<div class="prev-col">
<div class="bp-lbl">Mobile · 320px</div>
<div class="phone">
<div class="pst"><b>9:41</b><span>●●●</span></div>
<div class="pb">
<div class="mtb"><div class="mtb-back">← Einstellungen</div><div class="mtb-t">Vielfalt</div></div>
<div style="padding:16px;flex:1;overflow-y:auto;">
<div class="sec-lbl">Haushaltskontext</div>
<div class="ctx-chips" style="flex-wrap:wrap;">
<div class="ctx-chip" style="flex:1;min-width:60px;"><div class="ctx-em">🥩</div><div class="ctx-name">Omnivor</div></div>
<div class="ctx-chip" style="flex:1;min-width:60px;"><div class="ctx-em">🥦</div><div class="ctx-name">Vegetarisch</div></div>
<div class="ctx-chip" style="flex:1;min-width:60px;"><div class="ctx-em">🌱</div><div class="ctx-name">Vegan</div></div>
<div class="ctx-chip ind" style="flex:1;min-width:60px;"><div class="ctx-em"></div><div class="ctx-name">Individuell</div></div>
</div>
<div class="s-pills" style="margin-top:10px;">
<div class="s-pill off"> Protein</div>
<div class="s-pill on">✓ Küche</div>
<div class="s-pill on">✓ Zutaten · Hoch</div>
<div class="s-pill on">✓ Letzte Wochen · Mittel</div>
<div class="s-pill warn">⚠ Duplikate · Hoch</div>
</div>
<div class="acc">
<div class="acc-hd"><div class="acc-hd-t">Erweiterte Einstellungen</div><div class="acc-hd-r"></div></div>
<div class="acc-b">
<div style="font-size:10px;color:var(--color-text-muted);margin-bottom:10px;">Protein ist über den Kontext deaktiviert. Die übrigen Gewichte kannst du hier anpassen.</div>
<div class="wr">
<div class="wr-l"><div class="wr-name">Küche</div><div class="wr-sub">Tag-Wiederholung</div></div>
<div class="seg" style="width:108px"><div class="seg-o">Niedrig</div><div class="seg-o a">Mittel</div><div class="seg-o">Hoch</div></div>
</div>
<div class="wr">
<div class="wr-l"><div class="wr-name">Zutaten</div><div class="wr-sub">Überschneidung</div></div>
<div class="seg" style="width:108px"><div class="seg-o">Niedrig</div><div class="seg-o">Mittel</div><div class="seg-o a-r">Hoch</div></div>
</div>
<div class="wr">
<div class="wr-l"><div class="wr-name">Letzte Wochen</div><div class="wr-sub">Kochverlauf</div></div>
<div class="seg" style="width:108px"><div class="seg-o">Niedrig</div><div class="seg-o a">Mittel</div><div class="seg-o">Hoch</div></div>
</div>
<div class="wr">
<div class="wr-l"><div class="wr-name">Duplikate</div><div class="wr-sub">Im Plan</div></div>
<div class="seg" style="width:108px"><div class="seg-o">Niedrig</div><div class="seg-o">Mittel</div><div class="seg-o a-r">Hoch</div></div>
</div>
</div>
</div>
<div class="score-banner">
<div class="score-banner-l">
<div class="score-banner-label">Mit diesen Einstellungen</div>
<div class="score-banner-val">8.1</div>
<div class="score-banner-sub score-banner-up">↑ +0.7 gegenüber vorher</div>
</div>
<div class="score-banner-r">📈</div>
</div>
<div class="reset-link">Auf Standard zurücksetzen</div>
</div>
<div class="mbt">
<div class="mt-i"><div class="mt-ic">📅</div><div class="mt-l">Plan</div></div>
<div class="mt-i"><div class="mt-ic">🛒</div><div class="mt-l">Einkauf</div></div>
<div class="mt-i"><div class="mt-ic">🍳</div><div class="mt-l">Rezepte</div></div>
<div class="mt-i a"><div class="mt-ic">⚙️</div><div class="mt-l">Einstellungen</div></div>
</div>
</div>
</div>
</div>
<div class="prev-col" style="flex:1;min-width:600px;">
<div class="bp-lbl">Desktop · 1040px</div>
<div class="desk">
<div class="dsb">
<div class="dsb-logo"><div class="dsb-lm"><div class="dsb-ic">🥦</div><div class="dsb-nm">Mealprep</div></div><div class="dsb-sub">Familie Raddatz</div></div>
<div class="dsb-nav">
<div class="dsb-nl">Planung</div>
<div class="dsb-ni"><span class="dsb-nc">📅</span> Wochenplan</div>
<div class="dsb-ni"><span class="dsb-nc">🛒</span> Einkaufsliste</div>
<div class="dsb-ni"><span class="dsb-nc">🍳</span> Rezepte</div>
<div class="dsb-nl" style="margin-top:12px;">Haushalt</div>
<div class="dsb-ni a"><span class="dsb-nc">⚙️</span> Einstellungen</div>
</div>
</div>
<div class="dm">
<div class="dtb"><div><div class="dtb-bc">Einstellungen <span></span> Vielfalt-Einstellungen</div><div class="dtb-t">Vielfalt-Einstellungen</div></div></div>
<div class="dmc">
<div style="display:grid;grid-template-columns:1fr 280px;gap:24px;max-width:800px;">
<div>
<div class="sec-lbl">Haushaltskontext</div>
<div class="ctx-chips" style="margin-bottom:20px;">
<div class="ctx-chip"><div class="ctx-em">🥩</div><div class="ctx-name">Omnivor</div><div class="ctx-sub">Alle Regeln</div></div>
<div class="ctx-chip"><div class="ctx-em">🥦</div><div class="ctx-name">Vegetarisch</div><div class="ctx-sub">Protein aus</div></div>
<div class="ctx-chip"><div class="ctx-em">🌱</div><div class="ctx-name">Vegan</div><div class="ctx-sub">Protein aus</div></div>
<div class="ctx-chip ind"><div class="ctx-em"></div><div class="ctx-name">Individuell</div><div class="ctx-sub">Benutzerdefiniert</div></div>
</div>
<div class="acc">
<div class="acc-hd"><div class="acc-hd-t">Erweiterte Einstellungen</div><div class="acc-hd-r"></div></div>
<div class="acc-b">
<div style="font-size:11px;color:var(--color-text-muted);margin-bottom:12px;">Protein ist über den Haushaltskontext deaktiviert. Passe die Stärke der übrigen Regeln an.</div>
<div class="wr">
<div class="wr-l"><div class="wr-name">Küchen-Wiederholung</div><div class="wr-sub">Gleiche Küche an aufeinanderfolgenden Tagen</div></div>
<div class="seg" style="width:160px"><div class="seg-o">Niedrig</div><div class="seg-o a">Mittel</div><div class="seg-o">Hoch</div></div>
</div>
<div class="wr">
<div class="wr-l"><div class="wr-name">Zutaten-Überschneidung</div><div class="wr-sub">Gleiche Zutaten an aufeinanderfolgenden Tagen</div></div>
<div class="seg" style="width:160px"><div class="seg-o">Niedrig</div><div class="seg-o">Mittel</div><div class="seg-o a-r">Hoch</div></div>
</div>
<div class="wr">
<div class="wr-l"><div class="wr-name">Letzte Wochen</div><div class="wr-sub">Kochverlauf (14 Tage)</div></div>
<div class="seg" style="width:160px"><div class="seg-o">Niedrig</div><div class="seg-o a">Mittel</div><div class="seg-o">Hoch</div></div>
</div>
<div class="wr">
<div class="wr-l"><div class="wr-name">Doppelte Rezepte</div><div class="wr-sub">Gleiches Rezept mehrfach im Plan</div></div>
<div class="seg" style="width:160px"><div class="seg-o">Niedrig</div><div class="seg-o">Mittel</div><div class="seg-o a-r">Hoch</div></div>
</div>
</div>
</div>
<div class="reset-link" style="text-align:left;">Auf Standard zurücksetzen</div>
</div>
<div>
<div class="score-banner">
<div>
<div class="score-banner-label">Mit diesen Einstellungen</div>
<div class="score-banner-val">8.1 <span style="font-size:13px;opacity:.5;">/ 10</span></div>
<div class="score-banner-sub score-banner-up">↑ +0.7 gegenüber vorher</div>
</div>
<div class="score-banner-r">📈</div>
</div>
<div class="sec-lbl">Aktive Regeln</div>
<div class="sum-rows">
<div class="sum-row off"><span class="sum-row-name">Protein</span><span class="sum-row-val">Deaktiviert</span></div>
<div class="sum-row on"><span class="sum-row-name">Küche</span><span class="sum-row-val">Mittel</span></div>
<div class="sum-row warn"><span class="sum-row-name">Zutaten</span><span class="sum-row-val">Hoch ↑</span></div>
<div class="sum-row on"><span class="sum-row-name">Letzte Wochen</span><span class="sum-row-val">Mittel</span></div>
<div class="sum-row warn"><span class="sum-row-name">Duplikate</span><span class="sum-row-val">Hoch</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="agent">
<h4>E4 · S3 Erweiterte Einstellungen</h4>
<pre>/* Accordion öffnet sich per Click/Tap auf acc-hd. Keine Animation nötig — display toggle reicht.
* Erweiterte Einstellungen zeigt NUR aktive Tag-Typen als Gewichts-Rows.
* Wenn Protein deaktiviert (über Preset): Protein-Row wird in acc-b NICHT angezeigt.
* "Individuell"-Chip: erscheint automatisch wenn die Kombination repeatTagTypes+weights
* nicht exakt einem der drei Presets entspricht. Kein manueller Auslöser.
* Gewichts-Änderung → PATCH → Score-Simulation → Banner-Update.
* Debounce der Simulation: 300ms nach letzter Interaktion. */</pre>
<table class="at"><thead><tr><th>Element</th><th>Wert</th><th>Notizen</th></tr></thead><tbody>
<tr class="grp"><td colspan="3">Gewicht-Mapping</td></tr>
<tr><td>Niedrig</td><td>Faktor 0.5 × Standard-Gewicht</td><td>wTagRepeat: 0.75, wIngredient: 0.15, wRecent: 0.5, wDuplicate: 1.0</td></tr>
<tr><td>Mittel</td><td>Faktor 1.0 (Standard)</td><td>wTagRepeat: 1.5, wIngredient: 0.3, wRecent: 1.0, wDuplicate: 2.0</td></tr>
<tr><td>Hoch</td><td>Faktor 1.5 × Standard-Gewicht</td><td>wTagRepeat: 2.25, wIngredient: 0.45, wRecent: 1.5, wDuplicate: 3.0</td></tr>
<tr class="grp"><td colspan="3">Individuell-Chip</td></tr>
<tr><td>Trigger</td><td>Wenn gespeicherter Config ≠ Omnivor, Vegetarisch, oder Vegan Preset</td><td>Lila Border + Hintergrund</td></tr>
<tr><td>Symbol</td><td>✦ (U+2726)</td><td>Statt Emoji</td></tr>
<tr><td>Label</td><td>Individuell</td><td>Nicht anklickbar — nur Status-Indikator</td></tr>
<tr class="grp"><td colspan="3">Simulation-Debounce</td></tr>
<tr><td>Delay</td><td>300ms</td><td>Nach letzter Segmented-Control-Interaktion</td></tr>
<tr><td>Während Laden</td><td>Score-Wert zeigt Spinner (CSS animation)</td><td>Kein Skeleton — nur val-Bereich</td></tr>
</tbody></table>
</div>
</div>
<!-- S4: Reset confirmation -->
<div class="scr">
<div class="scr-head"><h3>S4 · Reset-Bestätigung</h3><span class="scr-id">E4</span></div>
<div class="scr-desc">Planer tippt "Auf Standard zurücksetzen". Ein Dialog erscheint und benennt explizit, was zurückgesetzt wird. Bestätigung löscht den Custom-Config-Eintrag (DELETE) und stellt die Omnivor-Defaults wieder her. Kein Backdrop-Dismiss — der Planer muss explizit wählen.</div>
<div class="scr-var"><strong>S4</strong> · Modal über S2-Zustand · Backdrop nicht anklickbar · Mobile: Bottom Sheet</div>
<div class="previews">
<div class="prev-col">
<div class="bp-lbl">Mobile · 320px (Bottom Sheet)</div>
<div class="phone">
<div class="pst"><b>9:41</b><span>●●●</span></div>
<div class="pb">
<!-- Blurred background state -->
<div class="mtb"><div class="mtb-back">← Einstellungen</div><div class="mtb-t">Vielfalt</div></div>
<div style="padding:16px;flex:1;overflow-y:auto;opacity:.35;pointer-events:none;">
<div class="ctx-chips">
<div class="ctx-chip sel"><div class="ctx-em">🥦</div><div class="ctx-name">Vegetarisch</div></div>
</div>
</div>
<!-- Bottom sheet -->
<div style="background:var(--color-page);border-radius:20px 20px 0 0;padding:20px;border-top:1px solid var(--color-border);box-shadow:0 -4px 24px rgba(0,0,0,.12);">
<div style="width:36px;height:4px;background:var(--color-border);border-radius:2px;margin:0 auto 16px;"></div>
<div style="font-family:var(--font-display);font-size:18px;font-weight:500;margin-bottom:8px;">Auf Standard zurücksetzen?</div>
<div style="font-size:12px;color:var(--color-text-muted);line-height:1.6;margin-bottom:16px;">Alle individuellen Einstellungen werden gelöscht. Der Algorithmus verwendet dann wieder:<br><br>• Protein: Aktiv<br>• Küche: Aktiv<br>• Alle Gewichte: Mittel</div>
<div class="btn-dest" style="margin-bottom:8px;">Zurücksetzen</div>
<div class="btn-ghost">Abbrechen</div>
</div>
<div class="mbt">
<div class="mt-i"><div class="mt-ic">📅</div><div class="mt-l">Plan</div></div>
<div class="mt-i"><div class="mt-ic">🛒</div><div class="mt-l">Einkauf</div></div>
<div class="mt-i"><div class="mt-ic">🍳</div><div class="mt-l">Rezepte</div></div>
<div class="mt-i a"><div class="mt-ic">⚙️</div><div class="mt-l">Einstellungen</div></div>
</div>
</div>
</div>
</div>
<div class="prev-col" style="flex:1;min-width:600px;">
<div class="bp-lbl">Desktop · 1040px (Centered Modal)</div>
<div class="desk overlay">
<div class="dsb" style="opacity:.35;">
<div class="dsb-logo"><div class="dsb-lm"><div class="dsb-ic">🥦</div><div class="dsb-nm">Mealprep</div></div></div>
</div>
<div class="dm" style="opacity:.35;">
<div class="dtb"><div class="dtb-t">Vielfalt-Einstellungen</div></div>
<div class="dmc"><div class="ctx-chips"><div class="ctx-chip sel"><div class="ctx-em">🥦</div><div class="ctx-name">Vegetarisch</div></div></div></div>
</div>
<div class="modal-backdrop">
<div class="modal">
<div class="modal-title">Auf Standard zurücksetzen?</div>
<div class="modal-body">Alle individuellen Einstellungen werden gelöscht. Der Algorithmus verwendet dann wieder die Standard-Werte:<br><br>
<strong>Protein-Prüfung:</strong> Aktiv<br>
<strong>Küchen-Vielfalt:</strong> Aktiv<br>
<strong>Alle Gewichte:</strong> Mittel</div>
<div class="modal-acts">
<div class="btn-dest">Zurücksetzen</div>
<div class="btn-ghost">Abbrechen</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="agent">
<h4>E4 · S4 Reset-Bestätigung</h4>
<pre>/* Reset-Link Tap → Dialog öffnet (kein Backdrop-Dismiss, kein Escape-Dismiss).
* "Zurücksetzen" → DELETE /v1/households/mine/variety-config
* On success: optimistic reset von UI zu S1 (Omnivor), Score-Banner zeigt echten Score.
* On error: Toast "Fehler beim Zurücksetzen".
* Mobile: Bottom Sheet (position:fixed, bottom 0, border-radius 20px 20px 0 0).
* Desktop: centered modal, backdrop rgba(28,28,24,0.4), max-width 380px. */</pre>
<table class="at"><thead><tr><th>Element</th><th>Wert</th><th>Notizen</th></tr></thead><tbody>
<tr class="grp"><td colspan="3">Dialog-Inhalt</td></tr>
<tr><td>Titel</td><td>"Auf Standard zurücksetzen?"</td><td>Fraunces 18px (Mobile), 20px (Desktop)</td></tr>
<tr><td>Body</td><td>Auflistung der zurückgesetzten Werte</td><td>Muss konkret benennen: Protein aktiv, Küche aktiv, alle Gewichte Mittel</td></tr>
<tr><td>Primär-Aktion</td><td>"Zurücksetzen" → DELETE</td><td>Hintergrund: --red, Text: weiß</td></tr>
<tr><td>Sekundär-Aktion</td><td>"Abbrechen"</td><td>Ghost-Button, schließt Dialog</td></tr>
<tr class="grp"><td colspan="3">API</td></tr>
<tr><td>Endpoint</td><td>DELETE /v1/households/mine/variety-config</td><td>Löscht Custom-Config-Row; Backend fällt auf Defaults zurück</td></tr>
<tr><td>On Success</td><td>UI reset zu S1</td><td>Omnivor chip selected, Score-Banner: echter Score</td></tr>
</tbody></table>
</div>
</div>
</div>
<!-- ════════════════════════════════════════
LLM / AGENT REGION
═════════════════════════════════════════ -->
<div class="llm">
<h2>Maschinenlesbare Spezifikation — E4 Vielfalt-Einstellungen</h2>
<h3>Screens</h3>
<table>
<thead><tr><th>Screen</th><th>Route</th><th>Zugriff</th><th>Zweck</th></tr></thead>
<tbody>
<tr><td>E1 (Update)</td><td>/settings</td><td>Planer</td><td>Settings-Hub: dritte Kachel "Vielfalt-Einstellungen" mit aktuellem Score</td></tr>
<tr><td>E4</td><td>/settings/variety</td><td>Planer only</td><td>Vielfalt-Algorithmus per Kontext-Preset und Feineinstellungen konfigurieren</td></tr>
</tbody>
</table>
<h3>States</h3>
<table>
<thead><tr><th>State</th><th>Trigger</th><th>Beschreibung</th></tr></thead>
<tbody>
<tr><td>S0</td><td>E1 load</td><td>Settings-Hub zeigt Score-Kachel (lila Kennzahl)</td></tr>
<tr><td>S1</td><td>E4 load, kein Custom-Config</td><td>Omnivor chip selected, Score = aktueller echter Score, Reset-Link = deaktiviert/neutral</td></tr>
<tr><td>S2</td><td>Preset-Chip tap</td><td>Chip wechselt, PATCH, Score-Simulation lädt und zeigt Delta</td></tr>
<tr><td>S3</td><td>Accordion öffnen + Gewicht ändern</td><td>Individuell-Chip erscheint, Score-Simulation mit Debounce 300ms</td></tr>
<tr><td>S4</td><td>Reset-Link tap</td><td>Modal/Bottom Sheet — Bestätigung vor DELETE</td></tr>
</tbody>
</table>
<h3>API-Endpoints (neu + bestehend)</h3>
<table>
<thead><tr><th>Method</th><th>Endpoint</th><th>Neu?</th><th>Zweck</th></tr></thead>
<tbody>
<tr><td>GET</td><td>/v1/households/mine/variety-config</td><td>Neu</td><td>Aktuellen Config laden; 404 = Defaults verwenden</td></tr>
<tr><td>PATCH</td><td>/v1/households/mine/variety-config</td><td>Neu</td><td>Config speichern (auto-save bei jedem Preset/Gewicht-Wechsel)</td></tr>
<tr><td>DELETE</td><td>/v1/households/mine/variety-config</td><td>Neu</td><td>Custom-Config löschen, Backend fällt auf Defaults zurück</td></tr>
<tr><td>POST</td><td>/v1/week-plans/{id}/variety-score/simulate</td><td>Neu</td><td>Score simulieren mit temporärem Config-Body (nicht persistiert)</td></tr>
<tr><td>GET</td><td>/v1/week-plans/{id}/variety-score</td><td>Bestehend</td><td>Aktuellen Score laden (für S1 Banner + E1 Kachel)</td></tr>
</tbody>
</table>
<h3>Kontext-Preset Mapping</h3>
<table>
<thead><tr><th>Preset</th><th>repeatTagTypes</th><th>wTagRepeat</th><th>wIngredientOverlap</th><th>wRecentRepeat</th><th>wPlanDuplicate</th></tr></thead>
<tbody>
<tr><td>Omnivor (Default)</td><td>["protein","cuisine"]</td><td>1.5</td><td>0.3</td><td>1.0</td><td>2.0</td></tr>
<tr><td>Vegetarisch</td><td>["cuisine"]</td><td>1.5</td><td>0.3</td><td>1.0</td><td>2.0</td></tr>
<tr><td>Vegan</td><td>["cuisine"]</td><td>1.5</td><td>0.3</td><td>1.0</td><td>2.0</td></tr>
<tr><td>Individuell</td><td>Beliebig (≠ obige Presets)</td><td>Beliebig</td><td>Beliebig</td><td>Beliebig</td><td>Beliebig</td></tr>
</tbody>
</table>
<h3>Gewicht-Preset Mapping</h3>
<table>
<thead><tr><th>Stufe</th><th>Faktor</th><th>wTagRepeat</th><th>wIngredientOverlap</th><th>wRecentRepeat</th><th>wPlanDuplicate</th></tr></thead>
<tbody>
<tr><td>Niedrig</td><td>×0.5</td><td>0.75</td><td>0.15</td><td>0.5</td><td>1.0</td></tr>
<tr><td>Mittel</td><td>×1.0</td><td>1.5</td><td>0.3</td><td>1.0</td><td>2.0</td></tr>
<tr><td>Hoch</td><td>×1.5</td><td>2.25</td><td>0.45</td><td>1.5</td><td>3.0</td></tr>
</tbody>
</table>
<h3>Implementierungsregeln (für Agenten)</h3>
<ul>
<li>E4 ist nur für <code>rolle === 'planer'</code> zugänglich. Mitglieder werden auf E1 redirected.</li>
<li>Auto-Save auf jede Preset- oder Gewicht-Änderung. Kein expliziter Speichern-Button.</li>
<li>Optimistic Update: UI wechselt sofort; Rollback mit Toast bei API-Fehler.</li>
<li>Score-Simulation: Debounce 300ms. Während Laden: Spinner im Score-Wert-Bereich (nicht Skeleton).</li>
<li>"Individuell"-Chip ist nicht anklickbar — er ist ein reiner Status-Indikator.</li>
<li>Reset-Bestätigung: Backdrop-Dismiss deaktiviert (nicht schließbar durch Klick/Tap auf Overlay).</li>
<li>Mobile Reset: Bottom Sheet mit Handle-Bar (36×4px, --color-border, border-radius 2px). Kein Backdrop-Dismiss.</li>
<li>Desktop Reset: Zentriertes Modal, max-width 380px. Backdrop rgba(28,28,24,0.4).</li>
<li>E1 Vielfalt-Kachel: Score < 6.0 Kennzahl in --orange; Score 6.0 Kennzahl in --purple.</li>
<li>E4-Route: <code>(app)/settings/variety/+page.svelte</code>. Load-Funktion: <code>+page.server.ts</code> → Promise.all([GET variety-config, GET variety-score]).</li>
</ul>
</div>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,841 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Variety Page Rework · 3 Variationen</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: /planner/variety — Variety Page Rework, 3 Variationen
version: 1.0
journey: J4 Swap (adjacent)
route: /planner/variety
screen: C2
variations: V1 Recipe Pills | V2 Action Rows | V3 Week Grid
last-updated: 2026-04-09
PROBLEMS ADDRESSED:
1. Warnings show day abbreviations ("MON, WED") — replace with recipe names
2. No swap action reachable from warnings — add inline swap CTA per recipe
3. Protein score is meat-centric for vegetarian households (backend concern, noted below)
FRONTEND-ONLY CHANGE (no backend schema changes required for items 1+2):
weekPlan.slots has { dayOfWeek: "MON", recipe: { id, name } }
tagRepeats.days[] contains day keys matching dayOfWeek
→ build slotsByDay map frontend-side, look up recipeName + slotId per day
→ swap CTA links to /planner?week={weekStart}&swap={slotId}
PROTEIN SCORE — VEGETARIAN HOUSEHOLDS (backend concern, TBD):
Current: proteinDiversity = 10 - proteinRepeats * 2
Problem: vegetarian protein sources (Tofu, Linsen, Ei) may repeat more than
omnivore households; penalty of -2 per repeat is calibrated for meat variety.
Backend discussed: tag filtering or weight adjustment needed.
Frontend impact: if backend changes tagRepeats to exclude non-meat or adjusts score,
the frontend ScoreBreakdownList label "Protein-Vielfalt" may need renaming.
Until resolved: the rework does NOT change protein score display — only warnings.
-->
<style>
:root {
--color-page: #FAFAF7;
--color-surface: #F5F4EE;
--color-subtle: #EDECEA;
--color-border: #D8D7D0;
--color-text-muted: #6B6A63;
--color-text: #1C1C18;
--green-tint: #E8F5EA;
--green-light: #AEDCB0;
--green: #3D8C4A;
--green-dark: #2E6E39;
--yellow-tint: #FDF6D8;
--yellow-light: #F9E08A;
--yellow: #E8B400;
--yellow-text: #8A6800;
--color-error: #DC4C3E;
--blue-tint: #E6F1FB;
--blue: #185FA5;
--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,.10), 0 2px 4px rgba(28,28,24,.05);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-sans); background: var(--color-page); color: var(--color-text); font-size: 14px; line-height: 1.6; }
.doc { max-width: 1040px; margin: 0 auto; padding: 48px 40px 96px; }
.doc-header { padding-bottom: 28px; border-bottom: 1px solid var(--color-border); margin-bottom: 32px; display: flex; justify-content: space-between; align-items: flex-end; }
.doc-header h1 { font-family: var(--font-display); font-size: 26px; font-weight: 500; letter-spacing: -0.02em; }
.doc-header p { font-size: 13px; color: var(--color-text-muted); margin-top: 4px; }
.doc-meta { font-family: var(--font-mono); font-size: 11px; color: var(--color-text-muted); text-align: right; line-height: 1.9; }
.intro { font-size: 14px; line-height: 1.75; max-width: 680px; margin-bottom: 16px; }
.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 var(--color-border); margin-bottom: 32px; margin-top: 56px; }
/* Notice box */
.notice { background: var(--yellow-tint); border: 1px solid var(--yellow-light); border-radius: var(--radius-lg); padding: 14px 18px; margin-bottom: 40px; }
.notice h3 { font-size: 12px; font-weight: 600; color: var(--yellow-text); margin-bottom: 4px; }
.notice p { font-size: 12px; color: var(--yellow-text); line-height: 1.6; }
.notice code { font-family: var(--font-mono); background: rgba(0,0,0,.07); padding: 1px 4px; border-radius: 3px; }
/* Variation sections */
.variation { margin-bottom: 72px; }
.var-header { display: flex; align-items: flex-start; gap: 20px; margin-bottom: 24px; }
.var-num { font-family: var(--font-display); font-size: 44px; font-weight: 300; color: var(--yellow-light); line-height: 1; flex-shrink: 0; width: 56px; letter-spacing: -0.03em; }
.var-meta { flex: 1; padding-top: 4px; }
.var-title { font-size: 18px; font-weight: 500; letter-spacing: -0.01em; margin-bottom: 4px; }
.var-desc { font-size: 13px; color: var(--color-text-muted); line-height: 1.6; max-width: 540px; }
.var-tag { font-size: 10px; font-weight: 500; letter-spacing: 0.07em; text-transform: uppercase; padding: 3px 8px; border-radius: var(--radius-sm); background: var(--color-subtle); color: var(--color-text-muted); margin-top: 6px; display: inline-block; }
.var-tag.rec { background: var(--green-tint); color: var(--green-dark); }
.var-tag.amb { background: var(--blue-tint); color: var(--blue); }
/* Preview containers */
.preview-pair { display: flex; gap: 24px; align-items: flex-start; margin-bottom: 20px; }
.preview-d-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-m-wrap { flex-shrink: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-label { font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: var(--color-text-muted); }
.preview-d-clip { height: 340px; overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-page); }
.preview-d-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
.preview-m-clip { width: 196px; height: 340px; overflow: hidden; border: 1.5px solid var(--color-border); border-radius: 24px; background: var(--color-page); }
.preview-m-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
/* Notes */
.notes { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 14px 18px; }
.notes-label { font-size: 9px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 8px; }
.notes ul { list-style: none; display: flex; flex-direction: column; gap: 4px; }
.notes li { font-size: 12px; color: var(--color-text-muted); line-height: 1.5; display: flex; align-items: flex-start; gap: 8px; }
.notes li::before { content: '→'; color: var(--green); font-weight: 500; flex-shrink: 0; }
/* ── AppShell chrome ── */
.shell { display: flex; background: var(--color-page); font-family: var(--font-sans); overflow: hidden; }
.sidebar { width: 224px; min-width: 224px; background: white; border-right: 1px solid var(--color-border); display: flex; flex-direction: column; }
.sidebar-brand { padding: 14px 18px; border-bottom: 1px solid var(--color-border); }
.sidebar-brand-row { display: flex; align-items: center; gap: 8px; }
.sidebar-logo { width: 22px; height: 22px; background: var(--green); border-radius: var(--radius-sm); }
.sidebar-app { font-family: var(--font-display); font-size: 15px; font-weight: 500; }
.sidebar-household { font-size: 10px; color: var(--color-text-muted); margin-top: 1px; }
.sidebar-nav { flex: 1; padding: 4px 8px; }
.sidebar-group-label { font-size: 8px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-text-muted); padding: 16px 12px 4px; }
.sidebar-item { display: flex; align-items: center; gap: 8px; padding: 7px 12px; border-radius: var(--radius-md); font-size: 13px; color: var(--color-text); text-decoration: none; }
.sidebar-item.active { background: var(--green-tint); color: var(--green-dark); font-weight: 500; }
.sidebar-icon { width: 20px; text-align: center; font-size: 16px; }
/* Page chrome */
.topbar { display: flex; align-items: center; gap: 12px; border-bottom: 1px solid var(--color-border); background: var(--color-page); padding: 14px 24px; }
.topbar-back { font-size: 13px; color: var(--color-text-muted); text-decoration: none; }
.topbar-sep { font-size: 13px; color: var(--color-text-muted); }
.topbar-title { font-family: var(--font-display); font-size: 20px; font-weight: 300; }
.main { flex: 1; padding: 24px; overflow: hidden; }
/* Score hero (shared across variations) */
.score-hero { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; margin-bottom: 20px; }
.score-num { font-family: var(--font-display); font-size: 64px; font-weight: 300; line-height: 1; letter-spacing: -0.03em; }
.score-denom { font-family: var(--font-display); font-size: 24px; font-weight: 300; color: var(--color-text-muted); }
.score-label { font-size: 13px; font-weight: 500; color: var(--yellow-text); }
.score-bar { height: 6px; background: var(--color-subtle); border-radius: var(--radius-full); overflow: hidden; width: 160px; }
.score-fill { height: 100%; border-radius: var(--radius-full); }
.score-fill.good { background: var(--green-dark); }
.score-fill.warn { background: var(--yellow); }
/* Sub-scores */
.sub-scores { border: 1px solid var(--color-border); border-radius: var(--radius-lg); overflow: hidden; background: white; margin-bottom: 20px; }
.sub-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid var(--color-border); }
.sub-row:last-child { border-bottom: none; }
.sub-label { font-size: 13px; }
.sub-val { font-size: 13px; font-weight: 500; color: var(--yellow-text); }
.sub-val.ok { color: var(--green-dark); }
/* Section heading */
.section-hd { font-size: 10px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 10px; }
/* ────────────────────────────────────────────
V1: Recipe Pill Warning Cards
──────────────────────────────────────────── */
.warn-card { background: var(--yellow-tint); border: 1px solid var(--yellow-light); border-radius: var(--radius-lg); padding: 14px 16px; margin-bottom: 10px; }
.warn-title { font-size: 13px; font-weight: 500; color: var(--yellow-text); margin-bottom: 8px; }
.pill-row { display: flex; flex-wrap: wrap; gap: 6px; }
.recipe-pill { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px 5px 12px; border-radius: var(--radius-full); background: white; border: 1px solid var(--yellow-light); font-size: 12px; font-weight: 500; color: var(--color-text); }
.recipe-pill-day { font-size: 10px; color: var(--color-text-muted); font-weight: 400; }
.pill-swap-btn { width: 22px; height: 22px; border-radius: var(--radius-full); background: var(--color-subtle); border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 11px; color: var(--color-text-muted); flex-shrink: 0; }
.pill-swap-btn:hover { background: var(--green-tint); color: var(--green-dark); }
/* ────────────────────────────────────────────
V2: Action Rows
──────────────────────────────────────────── */
/* Compact score header for V2 */
.score-compact { display: flex; align-items: center; gap: 14px; padding: 14px 20px; background: white; border: 1px solid var(--color-border); border-radius: var(--radius-lg); margin-bottom: 20px; }
.score-compact-num { font-family: var(--font-display); font-size: 36px; font-weight: 300; line-height: 1; }
.score-compact-denom { font-family: var(--font-display); font-size: 16px; font-weight: 300; color: var(--color-text-muted); }
.score-compact-right { flex: 1; }
.score-compact-label { font-size: 12px; font-weight: 500; color: var(--yellow-text); margin-bottom: 4px; }
.score-compact-bar { height: 5px; background: var(--color-subtle); border-radius: var(--radius-full); overflow: hidden; }
.score-compact-fill { height: 100%; border-radius: var(--radius-full); background: var(--yellow); }
.action-row { display: flex; align-items: flex-start; gap: 14px; padding: 14px 16px; background: white; border: 1px solid var(--color-border); border-radius: var(--radius-lg); margin-bottom: 8px; }
.action-icon { width: 32px; height: 32px; border-radius: var(--radius-md); background: var(--yellow-tint); display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0; margin-top: 1px; }
.action-body { flex: 1; }
.action-title { font-size: 13px; font-weight: 500; margin-bottom: 6px; }
.action-recipe-row { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px; background: var(--color-subtle); border-radius: var(--radius-md); margin-bottom: 4px; }
.action-recipe-name { font-size: 12px; font-weight: 500; }
.action-recipe-day { font-size: 10px; color: var(--color-text-muted); margin-left: 4px; }
.btn-swap { padding: 4px 10px; background: white; border: 1px solid var(--color-border); border-radius: var(--radius-md); font-size: 11px; font-weight: 500; color: var(--color-text-muted); cursor: pointer; white-space: nowrap; }
.btn-swap:hover { border-color: var(--green-light); color: var(--green-dark); }
/* ────────────────────────────────────────────
V3: Week Grid + Side Panel
──────────────────────────────────────────── */
.v3-layout { display: flex; gap: 0; height: 680px; }
.v3-main { flex: 1; padding: 24px; overflow-y: auto; }
.v3-panel { width: 280px; min-width: 280px; border-left: 1px solid var(--color-border); background: white; padding: 20px; overflow-y: auto; display: flex; flex-direction: column; }
.week-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; margin-bottom: 20px; }
.day-col { display: flex; flex-direction: column; gap: 4px; }
.day-header { font-size: 10px; font-weight: 500; color: var(--color-text-muted); text-align: center; padding-bottom: 4px; }
.recipe-slot { border-radius: var(--radius-md); border: 1px solid var(--color-border); background: white; padding: 6px 5px; min-height: 52px; font-size: 10px; font-weight: 500; text-align: center; display: flex; align-items: center; justify-content: center; cursor: pointer; line-height: 1.3; }
.recipe-slot.warn { border-color: var(--yellow); background: var(--yellow-tint); color: var(--yellow-text); box-shadow: 0 0 0 2px rgba(232,180,0,.25); }
.recipe-slot.warn:hover { box-shadow: 0 0 0 2px var(--yellow); }
.recipe-slot.selected { border-color: var(--green-dark); box-shadow: 0 0 0 2px var(--green-light); }
.recipe-slot.empty { background: var(--color-subtle); color: var(--color-text-muted); font-weight: 400; font-size: 9px; }
.warn-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--yellow); display: inline-block; margin-left: 3px; vertical-align: middle; }
.panel-score { display: flex; align-items: baseline; gap: 4px; margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid var(--color-border); }
.panel-score-num { font-family: var(--font-display); font-size: 48px; font-weight: 300; line-height: 1; }
.panel-score-denom { font-family: var(--font-display); font-size: 18px; font-weight: 300; color: var(--color-text-muted); }
.panel-warn-title { font-size: 13px; font-weight: 500; margin-bottom: 4px; }
.panel-warn-desc { font-size: 12px; color: var(--color-text-muted); margin-bottom: 14px; line-height: 1.5; }
.panel-recipe-entry { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; background: var(--color-subtle); border-radius: var(--radius-md); margin-bottom: 6px; }
.panel-recipe-name { font-size: 12px; font-weight: 500; }
.panel-recipe-day { font-size: 10px; color: var(--color-text-muted); }
.btn-swap-primary { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 9px 16px; background: var(--green-dark); color: white; border-radius: var(--radius-md); font-size: 12px; font-weight: 500; border: none; cursor: pointer; width: 100%; margin-top: 12px; }
.btn-swap-primary:hover { background: var(--green); }
.panel-hint { font-size: 11px; color: var(--color-text-muted); margin-top: 8px; }
.panel-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 8px; }
.panel-empty-icon { font-size: 24px; opacity: .4; }
.panel-empty-text { font-size: 12px; color: var(--color-text-muted); }
/* ── Mobile ── */
.m-shell { display: flex; flex-direction: column; background: var(--color-page); }
.m-topbar { display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--color-border); background: var(--color-page); padding: 12px 16px; position: sticky; top: 0; z-index: 10; }
.m-back { font-size: 20px; color: var(--color-text-muted); }
.m-title { font-family: var(--font-display); font-size: 16px; font-weight: 300; }
.m-content { flex: 1; padding: 16px; overflow-y: auto; }
.m-score-hero { display: flex; align-items: baseline; gap: 4px; margin-bottom: 16px; }
.m-score-num { font-family: var(--font-display); font-size: 52px; font-weight: 300; line-height: 1; }
.m-score-denom { font-family: var(--font-display); font-size: 20px; font-weight: 300; color: var(--color-text-muted); }
.m-score-bar { height: 5px; background: var(--color-subtle); border-radius: var(--radius-full); overflow: hidden; margin-bottom: 4px; }
.m-score-fill { height: 100%; border-radius: var(--radius-full); background: var(--yellow); }
.m-score-label { font-size: 12px; font-weight: 500; color: var(--yellow-text); margin-bottom: 16px; }
.m-section-hd { font-size: 9px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 8px; }
.m-warn-card { background: var(--yellow-tint); border: 1px solid var(--yellow-light); border-radius: var(--radius-lg); padding: 12px 14px; margin-bottom: 8px; }
.m-warn-title { font-size: 12px; font-weight: 500; color: var(--yellow-text); margin-bottom: 6px; }
.m-pill-row { display: flex; flex-wrap: wrap; gap: 6px; }
.m-pill { display: inline-flex; align-items: center; gap: 5px; padding: 4px 8px 4px 10px; background: white; border: 1px solid var(--yellow-light); border-radius: var(--radius-full); font-size: 11px; font-weight: 500; }
.m-pill-swap { width: 18px; height: 18px; border-radius: 50%; background: var(--color-subtle); display: flex; align-items: center; justify-content: center; font-size: 10px; flex-shrink: 0; }
.m-tabbar { display: flex; border-top: 1px solid var(--color-border); background: white; }
.m-tab { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 8px 4px 4px; font-size: 10px; color: var(--color-text-muted); gap: 2px; }
.m-tab.active { color: var(--green-dark); }
.m-tab-icon { font-size: 20px; }
/* Agent section */
.agent-section { background: var(--color-text); color: #E8E8E2; padding: 40px 48px; margin-top: 64px; }
.agent-section h2 { font-size: 10px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: #6B6A63; margin-bottom: 4px; }
.agent-section > p { font-size: 13px; color: #9A9990; margin-bottom: 28px; line-height: 1.6; max-width: 640px; }
.spec-comment { font-family: var(--font-mono); font-size: 11px; color: #3A3A36; margin-bottom: 32px; line-height: 1.9; white-space: pre-wrap; }
.agent-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; margin-bottom: 40px; }
.agent-table thead tr { border-bottom: 1px solid #2A2A26; }
.agent-table th { text-align: left; padding: 8px 14px; font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #5A5A55; font-family: var(--font-sans); }
.agent-table td { padding: 9px 14px; border-bottom: 1px solid #1E1E1A; vertical-align: top; line-height: 1.5; }
.agent-table tr:last-child td { border-bottom: none; }
.agent-table td:first-child { color: #7A7A72; white-space: nowrap; }
.agent-table td:nth-child(2) { color: #E8E8E2; font-weight: 500; }
.agent-table td:nth-child(3) { color: #5A5A55; }
.group-row td { padding-top: 20px; font-family: var(--font-sans); font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #3A3A36; border-bottom: none; }
</style>
</head>
<body>
<div class="doc">
<div class="doc-header">
<div>
<h1>Variety Page — Rework</h1>
<p>3 Design-Variationen · Route: <code>/planner/variety</code></p>
</div>
<div class="doc-meta">
screen: C2<br/>
journey: J4<br/>
version: 1.0<br/>
date: 2026-04-09
</div>
</div>
<p class="intro">
Zwei Kernprobleme werden adressiert: (1) Warnungen zeigen aktuell Wochentag-Kürzel ("MON, WED")
statt Rezeptnamen — rein frontend-seitig lösbar über <code>weekPlan.slots</code>-Mapping.
(2) Es gibt keine Swap-Aktion direkt aus den Warnungen heraus. Das Protein-Score-Problem
für vegetarische Haushalte ist ein Backend-Thema und separat zu behandeln.
</p>
<div class="notice">
<h3>Protein-Score: Vegetarische Haushalte — Backend TBD</h3>
<p>
Die aktuelle Formel <code>proteinDiversity = 10 repeats × 2</code> bestraft vegetarische
Proteinquellen (Tofu, Linsen, Ei) stärker als in omnivoren Haushalten üblich.
Frontend-seitig ändert sich das Label "Protein-Vielfalt" ggf. zu "Quellen-Vielfalt" sobald
das Backend die Score-Gewichtung anpasst. Bis dahin: keine Änderung an <code>ScoreBreakdownList</code>.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">V1 — Rezept-Pills in Warnkarten</div>
<div class="variation">
<div class="var-header">
<div class="var-num">1</div>
<div class="var-meta">
<div class="var-title">Rezept-Pills in Warnkarten</div>
<div class="var-desc">Minimale Änderung an der bestehenden Seitenstruktur. Warnkarten zeigen statt "MON, WED" konkrete Rezept-Pills mit Tauschen-Button. Seitenaufbau und Score-Hero bleiben identisch.</div>
<span class="var-tag">Vertraut · Geringer Aufwand</span>
</div>
</div>
<div class="preview-pair">
<div class="preview-d-wrap">
<div class="preview-label">Desktop</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell" style="min-height:680px;">
<div class="sidebar">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Plan</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">📅</span>Planer</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
</div>
</div>
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
<div class="topbar">
<a class="topbar-back" href="#">Planer</a>
<span class="topbar-sep">/</span>
<span class="topbar-title">Abwechslungs-Analyse</span>
</div>
<div class="main" style="display:flex;gap:32px;align-items:flex-start;overflow-y:auto;">
<!-- Left -->
<div style="flex:1;">
<div class="score-hero">
<div><span class="score-num" style="color:var(--yellow-text);">6.5</span><span class="score-denom">/10</span></div>
<div class="score-bar" style="width:200px;"><div class="score-fill warn" style="width:65%;"></div></div>
<div class="score-label">Verbesserbar</div>
</div>
<div class="section-hd">Bewertung im Detail</div>
<div class="sub-scores">
<div class="sub-row"><span class="sub-label">Quellen-Vielfalt</span><span class="sub-val">6/10</span></div>
<div class="sub-row"><span class="sub-label">Zutaten-Überlappung</span><span class="sub-val ok">8/10</span></div>
<div class="sub-row"><span class="sub-label">Aufwandsbalance</span><span class="sub-val ok">9/10</span></div>
</div>
<!-- Warnings with recipe pills -->
<div class="section-hd" style="margin-top:20px;">Hinweise</div>
<div class="warn-card">
<div class="warn-title">Tofu mehrfach diese Woche</div>
<div class="pill-row">
<span class="recipe-pill"><span class="recipe-pill-day">Mo</span>Tofu-Curry<button class="pill-swap-btn"></button></span>
<span class="recipe-pill"><span class="recipe-pill-day">Mi</span>Tofu-Bowl<button class="pill-swap-btn"></button></span>
</div>
</div>
<div class="warn-card">
<div class="warn-title">Linsen in mehreren Gerichten</div>
<div class="pill-row">
<span class="recipe-pill"><span class="recipe-pill-day">Di</span>Linsen-Suppe<button class="pill-swap-btn"></button></span>
<span class="recipe-pill"><span class="recipe-pill-day">Fr</span>Linsen-Dal<button class="pill-swap-btn"></button></span>
</div>
</div>
</div>
<!-- Right -->
<div style="width:280px;flex-shrink:0;">
<div class="section-hd">Quellen-Verteilung</div>
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:5px;margin-bottom:16px;">
<div style="display:flex;flex-direction:column;align-items:center;gap:3px;"><span style="font-size:9px;color:var(--color-text-muted);">Mo</span><div style="width:100%;height:40px;background:var(--yellow-tint);border:2px solid var(--yellow);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:600;color:var(--yellow-text);">TOF</div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:3px;"><span style="font-size:9px;color:var(--color-text-muted);">Di</span><div style="width:100%;height:40px;background:var(--green-tint);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:600;color:var(--green-dark);">LIN</div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:3px;"><span style="font-size:9px;color:var(--color-text-muted);">Mi</span><div style="width:100%;height:40px;background:var(--yellow-tint);border:2px solid var(--yellow);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:600;color:var(--yellow-text);">TOF</div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:3px;"><span style="font-size:9px;color:var(--color-text-muted);">Do</span><div style="width:100%;height:40px;background:var(--green-tint);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:600;color:var(--green-dark);">GEM</div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:3px;"><span style="font-size:9px;color:var(--color-text-muted);">Fr</span><div style="width:100%;height:40px;background:var(--yellow-tint);border:2px solid var(--yellow);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:600;color:var(--yellow-text);">LIN</div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:3px;"><span style="font-size:9px;color:var(--color-text-muted);">Sa</span><div style="width:100%;height:40px;background:var(--color-subtle);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;color:var(--color-text-muted);"></div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:3px;"><span style="font-size:9px;color:var(--color-text-muted);">So</span><div style="width:100%;height:40px;background:var(--color-subtle);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;color:var(--color-text-muted);"></div></div>
</div>
<div class="section-hd">Aufwandsverteilung</div>
<div style="display:flex;height:16px;border-radius:var(--radius-full);overflow:hidden;gap:2px;">
<div style="flex:3;background:var(--green-dark);"></div>
<div style="flex:2;background:var(--yellow);"></div>
</div>
<div style="display:flex;justify-content:space-between;margin-top:6px;font-size:10px;color:var(--color-text-muted);">
<span>Einfach ×3</span><span>Mittel ×2</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div class="m-shell" style="min-height:680px;">
<div class="m-topbar"><span class="m-back"></span><span class="m-title">Abwechslungs-Analyse</span></div>
<div class="m-content">
<div class="m-score-hero"><span class="m-score-num" style="color:var(--yellow-text);">6.5</span><span class="m-score-denom">/10</span></div>
<div class="m-score-bar"><div class="m-score-fill" style="width:65%;"></div></div>
<div class="m-score-label">Verbesserbar</div>
<div class="m-section-hd">Hinweise</div>
<div class="m-warn-card">
<div class="m-warn-title">Tofu mehrfach diese Woche</div>
<div class="m-pill-row">
<span class="m-pill"><span style="font-size:9px;color:var(--color-text-muted);">Mo</span>Tofu-Curry<span class="m-pill-swap"></span></span>
<span class="m-pill"><span style="font-size:9px;color:var(--color-text-muted);">Mi</span>Tofu-Bowl<span class="m-pill-swap"></span></span>
</div>
</div>
<div class="m-warn-card">
<div class="m-warn-title">Linsen in mehreren Gerichten</div>
<div class="m-pill-row">
<span class="m-pill"><span style="font-size:9px;color:var(--color-text-muted);">Di</span>Linsen-Suppe<span class="m-pill-swap"></span></span>
<span class="m-pill"><span style="font-size:9px;color:var(--color-text-muted);">Fr</span>Linsen-Dal<span class="m-pill-swap"></span></span>
</div>
</div>
</div>
<div class="m-tabbar">
<div class="m-tab active"><div class="m-tab-icon">📅</div>Planer</div>
<div class="m-tab"><div class="m-tab-icon">🍽</div>Rezepte</div>
<div class="m-tab"><div class="m-tab-icon">🛒</div>Einkauf</div>
<div class="m-tab"><div class="m-tab-icon">⚙️</div>Einstellungen</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Kein Backend-Change nötig. Frontend mappt <code>tagRepeat.days[]</code><code>weekPlan.slots.find(s => s.dayOfWeek === day)</code><code>recipe.name</code></li>
<li>Pill-Swap-Button (↔): navigiert zu <code>/planner?week={weekStart}&amp;swap={slotId}</code> — öffnet RecipePicker für den betreffenden Slot</li>
<li>Pill-Label links: Wochentag-Kürzel (Mo, Di, …) aus <code>dayOfWeek</code>-Mapping</li>
<li>Wenn ein Slot leer ist (Rezept wurde bereits entfernt): Pill zeigt nur den Wochentag, kein Swap-Button</li>
<li>Geringe Änderung: nur <code>VarietyWarningCards.svelte</code> + <code>variety.ts</code> anpassen; Rest der Seite bleibt</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">V2 — Aktions-Zeilen (Action-first)</div>
<div class="variation">
<div class="var-header">
<div class="var-num">2</div>
<div class="var-meta">
<div class="var-title">Aktions-Zeilen</div>
<div class="var-desc">Warnungen stehen oben, Score-Hero wird kompakt. Pro Warnung gibt es eine vollständige Rezept-Zeile mit Wochentag und dediziertem "Tauschen"-Button. Fokus auf sofortige Handlung statt auf Metrik-Verständnis.</div>
<span class="var-tag rec">Empfohlen · Aktionsfokus</span>
</div>
</div>
<div class="preview-pair">
<div class="preview-d-wrap">
<div class="preview-label">Desktop</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell" style="min-height:680px;">
<div class="sidebar">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Plan</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">📅</span>Planer</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
</div>
</div>
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
<div class="topbar">
<a class="topbar-back" href="#">Planer</a>
<span class="topbar-sep">/</span>
<span class="topbar-title">Abwechslungs-Analyse</span>
</div>
<div class="main" style="overflow-y:auto;">
<!-- Compact score -->
<div class="score-compact">
<div><span class="score-compact-num" style="color:var(--yellow-text);">6.5</span><span class="score-compact-denom">/10</span></div>
<div class="score-compact-right">
<div class="score-compact-label">Verbesserbar — 2 Hinweise</div>
<div class="score-compact-bar"><div class="score-compact-fill" style="width:65%;"></div></div>
</div>
</div>
<div style="display:flex;gap:24px;align-items:flex-start;">
<div style="flex:1;">
<div class="section-hd">Empfehlenswerte Tausche</div>
<!-- Action row 1 -->
<div class="action-row">
<div class="action-icon">🔄</div>
<div class="action-body">
<div class="action-title">Tofu mehrfach diese Woche</div>
<div class="action-recipe-row">
<span><span class="action-recipe-name">Tofu-Curry</span><span class="action-recipe-day">· Montag</span></span>
<button class="btn-swap">Tauschen →</button>
</div>
<div class="action-recipe-row">
<span><span class="action-recipe-name">Tofu-Bowl</span><span class="action-recipe-day">· Mittwoch</span></span>
<button class="btn-swap">Tauschen →</button>
</div>
</div>
</div>
<!-- Action row 2 -->
<div class="action-row">
<div class="action-icon">🔄</div>
<div class="action-body">
<div class="action-title">Linsen in mehreren Gerichten</div>
<div class="action-recipe-row">
<span><span class="action-recipe-name">Linsen-Suppe</span><span class="action-recipe-day">· Dienstag</span></span>
<button class="btn-swap">Tauschen →</button>
</div>
<div class="action-recipe-row">
<span><span class="action-recipe-name">Linsen-Dal</span><span class="action-recipe-day">· Freitag</span></span>
<button class="btn-swap">Tauschen →</button>
</div>
</div>
</div>
<!-- Collapsible detail scores -->
<details style="margin-top:16px;">
<summary style="font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text-muted);cursor:pointer;list-style:none;padding:8px 0;">Bewertung im Detail ▾</summary>
<div class="sub-scores" style="margin-top:10px;">
<div class="sub-row"><span class="sub-label">Quellen-Vielfalt</span><span class="sub-val">6/10</span></div>
<div class="sub-row"><span class="sub-label">Zutaten-Überlappung</span><span class="sub-val ok">8/10</span></div>
<div class="sub-row"><span class="sub-label">Aufwandsbalance</span><span class="sub-val ok">9/10</span></div>
</div>
</details>
</div>
<div style="width:240px;flex-shrink:0;">
<div class="section-hd">Quellen-Verteilung</div>
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:4px;margin-bottom:12px;">
<div style="display:flex;flex-direction:column;align-items:center;gap:2px;"><span style="font-size:9px;color:var(--color-text-muted);">Mo</span><div style="width:100%;height:36px;background:var(--yellow-tint);border:2px solid var(--yellow);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:600;color:var(--yellow-text);">TOF</div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:2px;"><span style="font-size:9px;color:var(--color-text-muted);">Di</span><div style="width:100%;height:36px;background:var(--green-tint);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:600;color:var(--green-dark);">LIN</div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:2px;"><span style="font-size:9px;color:var(--color-text-muted);">Mi</span><div style="width:100%;height:36px;background:var(--yellow-tint);border:2px solid var(--yellow);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:600;color:var(--yellow-text);">TOF</div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:2px;"><span style="font-size:9px;color:var(--color-text-muted);">Do</span><div style="width:100%;height:36px;background:var(--green-tint);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:600;color:var(--green-dark);">GEM</div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:2px;"><span style="font-size:9px;color:var(--color-text-muted);">Fr</span><div style="width:100%;height:36px;background:var(--yellow-tint);border:2px solid var(--yellow);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:600;color:var(--yellow-text);">LIN</div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:2px;"><span style="font-size:9px;color:var(--color-text-muted);">Sa</span><div style="width:100%;height:36px;background:var(--color-subtle);border-radius:3px;"></div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:2px;"><span style="font-size:9px;color:var(--color-text-muted);">So</span><div style="width:100%;height:36px;background:var(--color-subtle);border-radius:3px;"></div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div class="m-shell" style="min-height:680px;">
<div class="m-topbar"><span class="m-back"></span><span class="m-title">Abwechslungs-Analyse</span></div>
<div class="m-content">
<!-- Compact score mobile -->
<div style="display:flex;align-items:center;gap:12px;padding:12px 14px;background:white;border:1px solid var(--color-border);border-radius:var(--radius-lg);margin-bottom:16px;">
<div><span style="font-family:var(--font-display);font-size:32px;font-weight:300;color:var(--yellow-text);">6.5</span><span style="font-family:var(--font-display);font-size:14px;font-weight:300;color:var(--color-text-muted);">/10</span></div>
<div style="flex:1;"><div style="font-size:11px;font-weight:500;color:var(--yellow-text);margin-bottom:4px;">Verbesserbar</div><div style="height:4px;background:var(--color-subtle);border-radius:99px;overflow:hidden;"><div style="width:65%;height:100%;background:var(--yellow);border-radius:99px;"></div></div></div>
</div>
<div class="m-section-hd">Empfehlenswerte Tausche</div>
<!-- Action row mobile -->
<div style="background:white;border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:12px;margin-bottom:8px;">
<div style="font-size:12px;font-weight:500;margin-bottom:8px;">🔄 Tofu mehrfach diese Woche</div>
<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 8px;background:var(--color-subtle);border-radius:var(--radius-md);margin-bottom:4px;"><span style="font-size:11px;font-weight:500;">Tofu-Curry <span style="color:var(--color-text-muted);font-weight:400;">Mo</span></span><button style="font-size:10px;font-weight:500;padding:3px 8px;background:white;border:1px solid var(--color-border);border-radius:var(--radius-md);color:var(--color-text-muted);">Tauschen →</button></div>
<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 8px;background:var(--color-subtle);border-radius:var(--radius-md);"><span style="font-size:11px;font-weight:500;">Tofu-Bowl <span style="color:var(--color-text-muted);font-weight:400;">Mi</span></span><button style="font-size:10px;font-weight:500;padding:3px 8px;background:white;border:1px solid var(--color-border);border-radius:var(--radius-md);color:var(--color-text-muted);">Tauschen →</button></div>
</div>
<div style="background:white;border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:12px;margin-bottom:8px;">
<div style="font-size:12px;font-weight:500;margin-bottom:8px;">🔄 Linsen in mehreren Gerichten</div>
<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 8px;background:var(--color-subtle);border-radius:var(--radius-md);margin-bottom:4px;"><span style="font-size:11px;font-weight:500;">Linsen-Suppe <span style="color:var(--color-text-muted);font-weight:400;">Di</span></span><button style="font-size:10px;font-weight:500;padding:3px 8px;background:white;border:1px solid var(--color-border);border-radius:var(--radius-md);color:var(--color-text-muted);">Tauschen →</button></div>
<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 8px;background:var(--color-subtle);border-radius:var(--radius-md);"><span style="font-size:11px;font-weight:500;">Linsen-Dal <span style="color:var(--color-text-muted);font-weight:400;">Fr</span></span><button style="font-size:10px;font-weight:500;padding:3px 8px;background:white;border:1px solid var(--color-border);border-radius:var(--radius-md);color:var(--color-text-muted);">Tauschen →</button></div>
</div>
</div>
<div class="m-tabbar">
<div class="m-tab active"><div class="m-tab-icon">📅</div>Planer</div>
<div class="m-tab"><div class="m-tab-icon">🍽</div>Rezepte</div>
<div class="m-tab"><div class="m-tab-icon">🛒</div>Einkauf</div>
<div class="m-tab"><div class="m-tab-icon">⚙️</div>Einstellungen</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Score-Hero wird kompakt: Zahl + Label + Balken in einer horizontal komprimierten Leiste oben</li>
<li>Sub-Scores in aufklappbarem <code>&lt;details&gt;</code>-Element — zugänglich, kein JavaScript nötig</li>
<li>Jeder "Tauschen"-Button navigiert zum Planer mit dem spezifischen Slot vorselektiert</li>
<li>Wochentag als ausgeschriebenes Wort ("Montag") — nicht Kürzel — für bessere Lesbarkeit</li>
<li>Mobile: Score-Hero bleibt kompakt oben, Action-Rows nehmen den Hauptraum ein</li>
<li>Größerer Aufwand als V1: <code>VarietyWarningCards</code> grundlegend neu strukturieren</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">V3 — Wochenraster mit Kontext-Panel</div>
<div class="variation">
<div class="var-header">
<div class="var-num">3</div>
<div class="var-meta">
<div class="var-title">Wochenraster mit Kontext-Panel</div>
<div class="var-desc">Das bestehende Protein-Raster wird zum Haupt-Interface. Alle 7 Tage zeigen das vollständige Rezept. Problematische Slots sind gelb markiert — Klick öffnet das rechte Panel mit Erklärung und Swap-CTA.</div>
<span class="var-tag amb">Ambitiös · Meiste Übersicht</span>
</div>
</div>
<div class="preview-pair">
<div class="preview-d-wrap">
<div class="preview-label">Desktop</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell" style="min-height:680px;">
<div class="sidebar">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Plan</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">📅</span>Planer</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
</div>
</div>
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
<div class="topbar">
<a class="topbar-back" href="#">Planer</a>
<span class="topbar-sep">/</span>
<span class="topbar-title">Abwechslungs-Analyse</span>
<!-- Score badge in topbar -->
<div style="margin-left:auto;display:flex;align-items:center;gap:8px;">
<span style="font-size:11px;color:var(--color-text-muted);">Abwechslung</span>
<span style="font-family:var(--font-display);font-size:20px;font-weight:300;color:var(--yellow-text);">6.5</span>
<span style="font-family:var(--font-display);font-size:12px;color:var(--color-text-muted);">/10</span>
</div>
</div>
<div style="display:flex;flex:1;overflow:hidden;">
<!-- Main: week grid -->
<div class="v3-main">
<div class="section-hd">Wochenübersicht — gelb markierte Gerichte haben Hinweise</div>
<div class="week-grid">
<!-- Mon - Tofu-Curry WARN -->
<div class="day-col">
<div class="day-header">Mo</div>
<div class="recipe-slot warn selected">Tofu-Curry</div>
</div>
<!-- Tue - Linsen-Suppe WARN -->
<div class="day-col">
<div class="day-header">Di</div>
<div class="recipe-slot warn">Linsen-Suppe</div>
</div>
<!-- Wed - Tofu-Bowl WARN -->
<div class="day-col">
<div class="day-header">Mi</div>
<div class="recipe-slot warn">Tofu-Bowl</div>
</div>
<!-- Thu - Gemüse OK -->
<div class="day-col">
<div class="day-header">Do</div>
<div class="recipe-slot">Gemüse-Stir-Fry</div>
</div>
<!-- Fri - Linsen-Dal WARN -->
<div class="day-col">
<div class="day-header">Fr</div>
<div class="recipe-slot warn">Linsen-Dal</div>
</div>
<!-- Sat - empty -->
<div class="day-col">
<div class="day-header">Sa</div>
<div class="recipe-slot empty"></div>
</div>
<!-- Sun - empty -->
<div class="day-col">
<div class="day-header">So</div>
<div class="recipe-slot empty"></div>
</div>
</div>
<div class="section-hd" style="margin-top:16px;">Aufwandsverteilung</div>
<div style="display:flex;height:18px;border-radius:var(--radius-full);overflow:hidden;gap:2px;max-width:280px;">
<div style="flex:3;background:var(--green-dark);"></div>
<div style="flex:2;background:var(--yellow);"></div>
</div>
<div style="display:flex;gap:16px;margin-top:6px;font-size:11px;color:var(--color-text-muted);">
<span>Einfach ×3</span><span>Mittel ×2</span>
</div>
<div class="sub-scores" style="margin-top:20px;max-width:360px;">
<div class="sub-row"><span class="sub-label">Quellen-Vielfalt</span><span class="sub-val">6/10</span></div>
<div class="sub-row"><span class="sub-label">Zutaten-Überlappung</span><span class="sub-val ok">8/10</span></div>
<div class="sub-row"><span class="sub-label">Aufwandsbalance</span><span class="sub-val ok">9/10</span></div>
</div>
</div>
<!-- Right panel: context for selected slot -->
<div class="v3-panel">
<div class="panel-score">
<span class="panel-score-num" style="color:var(--yellow-text);">6.5</span>
<span class="panel-score-denom">/10</span>
</div>
<div class="panel-warn-title">Tofu-Curry — Montag</div>
<div class="panel-warn-desc">Tofu taucht diese Woche auch am Mittwoch auf (Tofu-Bowl). Ein Tausch würde die Quellen-Vielfalt verbessern.</div>
<div class="section-hd">Andere betroffene Gerichte</div>
<div class="panel-recipe-entry">
<div><div class="panel-recipe-name">Tofu-Bowl</div><div class="panel-recipe-day">Mittwoch</div></div>
</div>
<button class="btn-swap-primary">↔ Tofu-Curry tauschen</button>
<div class="panel-hint">Öffnet den Rezept-Picker für Montag.</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile (Tab-Navigation)</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div class="m-shell" style="min-height:680px;">
<div class="m-topbar"><span class="m-back"></span><span class="m-title">Abwechslungs-Analyse</span><span style="margin-left:auto;font-family:var(--font-display);font-size:18px;font-weight:300;color:var(--yellow-text);">6.5<span style="font-size:12px;color:var(--color-text-muted);">/10</span></span></div>
<div class="m-content">
<!-- Tab switcher for mobile (Übersicht | Hinweise) -->
<div style="display:flex;border:1px solid var(--color-border);border-radius:var(--radius-md);overflow:hidden;margin-bottom:14px;">
<button style="flex:1;padding:7px;font-size:11px;font-weight:500;background:var(--color-subtle);color:var(--color-text-muted);border:none;">Übersicht</button>
<button style="flex:1;padding:7px;font-size:11px;font-weight:500;background:var(--green-dark);color:white;border:none;">Hinweise (2)</button>
</div>
<!-- Hinweise tab active -->
<div style="background:white;border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:14px;margin-bottom:10px;">
<div style="font-size:12px;font-weight:500;margin-bottom:8px;color:var(--yellow-text);">Tofu mehrfach diese Woche</div>
<div style="display:flex;align-items:center;justify-content:space-between;padding:7px 10px;background:var(--yellow-tint);border:1px solid var(--yellow-light);border-radius:var(--radius-md);margin-bottom:5px;"><span style="font-size:11px;font-weight:500;">Tofu-Curry <span style="color:var(--color-text-muted);font-weight:400;">Mo</span></span><button style="font-size:10px;font-weight:500;padding:4px 8px;background:var(--green-dark);color:white;border:none;border-radius:var(--radius-md);">Tauschen</button></div>
<div style="display:flex;align-items:center;justify-content:space-between;padding:7px 10px;background:var(--yellow-tint);border:1px solid var(--yellow-light);border-radius:var(--radius-md);"><span style="font-size:11px;font-weight:500;">Tofu-Bowl <span style="color:var(--color-text-muted);font-weight:400;">Mi</span></span><button style="font-size:10px;font-weight:500;padding:4px 8px;background:var(--green-dark);color:white;border:none;border-radius:var(--radius-md);">Tauschen</button></div>
</div>
<div style="background:white;border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:14px;">
<div style="font-size:12px;font-weight:500;margin-bottom:8px;color:var(--yellow-text);">Linsen in mehreren Gerichten</div>
<div style="display:flex;align-items:center;justify-content:space-between;padding:7px 10px;background:var(--yellow-tint);border:1px solid var(--yellow-light);border-radius:var(--radius-md);margin-bottom:5px;"><span style="font-size:11px;font-weight:500;">Linsen-Suppe <span style="color:var(--color-text-muted);font-weight:400;">Di</span></span><button style="font-size:10px;font-weight:500;padding:4px 8px;background:var(--green-dark);color:white;border:none;border-radius:var(--radius-md);">Tauschen</button></div>
<div style="display:flex;align-items:center;justify-content:space-between;padding:7px 10px;background:var(--yellow-tint);border:1px solid var(--yellow-light);border-radius:var(--radius-md);"><span style="font-size:11px;font-weight:500;">Linsen-Dal <span style="color:var(--color-text-muted);font-weight:400;">Fr</span></span><button style="font-size:10px;font-weight:500;padding:4px 8px;background:var(--green-dark);color:white;border:none;border-radius:var(--radius-md);">Tauschen</button></div>
</div>
</div>
<div class="m-tabbar">
<div class="m-tab active"><div class="m-tab-icon">📅</div>Planer</div>
<div class="m-tab"><div class="m-tab-icon">🍽</div>Rezepte</div>
<div class="m-tab"><div class="m-tab-icon">🛒</div>Einkauf</div>
<div class="m-tab"><div class="m-tab-icon">⚙️</div>Einstellungen</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Wochenraster ersetzt das bisherige Protein-Grid (7 Spalten, Rezeptname statt Kürzel, größere Zellen)</li>
<li>Gelber Slot = mindestens ein Hinweis vorhanden. Klick selektiert den Slot, Panel rechts aktualisiert sich.</li>
<li>Panel zeigt: betroffenes Rezept + Wochentag + Erklärung + andere betroffene Slots + primären "Tauschen"-Button</li>
<li>Score-Zahl wandert in die Topbar-Leiste (kompakt, immer sichtbar)</li>
<li>Mobile: kein Panel — stattdessen Tab-Switcher "Übersicht | Hinweise (N)" mit aufklappbaren Einträgen</li>
<li>Größter Umbau: <code>+page.svelte</code> Struktur und alle beteiligten Komponenten müssen neu aufgebaut werden</li>
</ul>
</div>
</div>
<!-- ─── Agent section ─── -->
<div class="agent-section">
<h2>Maschinen-lesbare Spezifikation</h2>
<p>Gilt für alle drei Variationen. Implementierungs-Details werden nach Variantenwahl konkretisiert.</p>
<pre class="spec-comment">
/* spec:rules — Variety Page Rework (alle Variationen)
*
* RECIPE NAME MAPPING (frontend, no backend change)
* Source: weekPlan.slots[] → { dayOfWeek: "MON"|"TUE"|..., recipe: { id, name } }
* tagRepeats[].days[] contains dayOfWeek keys (e.g. "MON")
* slotsByDay = Object.fromEntries(weekPlan.slots.map(s => [s.dayOfWeek, s]))
* recipeName = slotsByDay[day]?.recipe?.name ?? day
* slotId = slotsByDay[day]?.id
*
* SWAP NAVIGATION
* "Tauschen" button href: /planner?week={weekStart}&swap={slotId}
* weekStart available in page data
* slotId from weekPlan.slots mapping above
* Opens RecipePicker for that slot (existing functionality in planner page)
*
* DAY LABEL MAPPING (for display)
* MON → "Montag" TUE → "Dienstag" WED → "Mittwoch" THU → "Donnerstag"
* FRI → "Freitag" SAT → "Samstag" SUN → "Sonntag"
* Short: Mo, Di, Mi, Do, Fr, Sa, So
*
* EMPTY SLOT HANDLING
* If slotsByDay[day] is undefined: show day key only, no swap button
* This can happen if slot was deleted since varietyScore was computed
*
* PROTEIN SCORE — VEGETARIAN NOTE
* Label "Protein-Vielfalt" in ScoreBreakdownList may change to "Quellen-Vielfalt"
* pending backend decision on scoring weight adjustment.
* No frontend change required until backend ships the updated score.
*
* VARIATION-SPECIFIC
* V1: Modify VarietyWarningCards + Warning type (add slots: { day, recipeName, slotId }[])
* computeWarnings() now returns slots[] instead of string days[]
* V2: Restructure VarietyWarningCards to ActionRows; VarietyScoreHero → compact variant
* <details> for sub-scores (no JS needed)
* V3: Replace protein grid with full week grid (recipe names); add side panel component
* Mobile: tab switcher (Übersicht | Hinweise) using $state activeTab
*/
</pre>
<table class="agent-table">
<thead>
<tr><th>Property</th><th>Value</th><th>Notes</th></tr>
</thead>
<tbody>
<tr class="group-row"><td colspan="3">Shared: Recipe Mapping</td></tr>
<tr><td>data-source</td><td>weekPlan.slots[].dayOfWeek + recipe</td><td>already in page data</td></tr>
<tr><td>swap-url</td><td>/planner?week={weekStart}&amp;swap={slotId}</td><td>RecipePicker pre-selects slot</td></tr>
<tr><td>day-long</td><td>MON→Montag, TUE→Dienstag…</td><td>for V2 display</td></tr>
<tr><td>day-short</td><td>MON→Mo, TUE→Di…</td><td>for V1 pills + V3 grid</td></tr>
<tr class="group-row"><td colspan="3">V1 Recipe Pills</td></tr>
<tr><td>pill-padding</td><td>5px 10px 5px 12px</td><td>left more for text</td></tr>
<tr><td>swap-btn-size</td><td>22×22px, border-radius 50%</td><td>within pill</td></tr>
<tr><td>pill-bg</td><td>white, border --yellow-light</td><td>on yellow-tint card</td></tr>
<tr class="group-row"><td colspan="3">V2 Action Rows</td></tr>
<tr><td>score-compact-height</td><td>~64px</td><td>replaces 180px hero</td></tr>
<tr><td>details-summary</td><td>native &lt;details&gt;, no JS</td><td>sub-scores hidden by default</td></tr>
<tr><td>recipe-row-bg</td><td>--color-subtle</td><td>within white action card</td></tr>
<tr class="group-row"><td colspan="3">V3 Week Grid</td></tr>
<tr><td>slot-height</td><td>52px min</td><td>enough for 2-line recipe name</td></tr>
<tr><td>warn-slot-ring</td><td>2px solid --yellow + yellow-tint bg</td><td>problem indicator</td></tr>
<tr><td>selected-slot-ring</td><td>2px solid --green-dark</td><td>active selection</td></tr>
<tr><td>panel-width</td><td>280px</td><td>fixed, right side</td></tr>
<tr><td>mobile-tab-active-bg</td><td>--green-dark</td><td>selected tab button</td></tr>
</tbody>
</table>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,762 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Planner — Flip Tiles</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@300;400&family=DM+Sans:wght@400;500;600&family=DM+Mono&display=swap" rel="stylesheet">
<style>
:root {
--page: #fafaf7;
--surface: #f5f4ee;
--subtle: #edecea;
--border: #d8d7d0;
--text: #1c1c18;
--muted: #6b6a63;
--gt: #e8f5ea; --gl: #aedcb0; --g: #3d8c4a; --gd: #2e6e39;
--yt: #fdf6d8; --yl: #f9e08a; --y: #f2c12e; --yx: #8a6800;
--pt: #eeedfe; --p: #534ab7;
--ot: #fef0e6; --od: #b46820;
--err: #dc4c3e;
--r-sm: 4px; --r-md: 6px; --r-lg: 10px; --r-full: 9999px;
--sh-card: 0 1px 3px rgba(28,28,24,.06),0 1px 2px rgba(28,28,24,.04);
--sh-raised: 0 6px 18px rgba(28,28,24,.14),0 2px 6px rgba(28,28,24,.08);
--fd: 'Fraunces', Georgia, serif;
--fs: 'DM Sans', system-ui, sans-serif;
--fm: 'DM Mono', monospace;
}
/* ── Ingredient / cuisine colour palette ──────────────────────── */
/* Protein-based */
--col-haehnchen: linear-gradient(160deg,#d4923a 0%,#a85e1a 50%,#7a3d0c 100%);
--col-rind: linear-gradient(160deg,#c04545 0%,#8b2020 50%,#5a1010 100%);
--col-fisch: linear-gradient(160deg,#5b9fd4 0%,#2868a0 50%,#10406e 100%);
--col-tofu: linear-gradient(160deg,#5fa85e 0%,#2e7031 50%,#1a4a1e 100%);
--col-veg: linear-gradient(160deg,#7bc47b 0%,#3d8c3d 50%,#1e5a1e 100%);
--col-schwein: linear-gradient(160deg,#d4785a 0%,#a04535 50%,#6e2418 100%);
--col-lamm: linear-gradient(160deg,#9e6b3a 0%,#6b3f1a 50%,#3e2208 100%);
--col-ei: linear-gradient(160deg,#d4b832 0%,#a07010 50%,#6e4800 100%);
--col-linsen: linear-gradient(160deg,#8b6b3a 0%,#5e421a 50%,#3a2408 100%);
/* Cuisine-based */
--col-italienisch: linear-gradient(160deg,#c04545 0%,#7a1e1e 50%,#4a0f0f 100%);
--col-asiatisch: linear-gradient(160deg,#3a6e3a 0%,#1e4a1e 50%,#0e2e0e 100%);
--col-mexikanisch: linear-gradient(160deg,#d4923a 0%,#8b4e10 50%,#5a2e00 100%);
--col-indisch: linear-gradient(160deg,#c49010 0%,#8b5e00 50%,#5a3800 100%);
--col-mediterran: linear-gradient(160deg,#5b9fd4 0%,#1e5a8b 50%,#0a3456 100%);
*{box-sizing:border-box;margin:0;padding:0;}
body{
font-family:var(--fs);background:#dddcd7;color:var(--text);
padding:40px 24px 80px;line-height:1.4;
}
.eyebrow{font-family:var(--fs);font-size:11px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-bottom:6px;}
.pg-title{font-family:var(--fd);font-size:34px;font-weight:300;margin-bottom:6px;}
.pg-sub{font-family:var(--fs);font-size:14px;color:var(--muted);max-width:700px;line-height:1.65;margin-bottom:44px;}
.block{margin-bottom:60px;}
.bl-hd{display:flex;align-items:baseline;gap:10px;margin-bottom:14px;}
.bl-num{font-family:var(--fm);font-size:11px;background:var(--subtle);color:var(--muted);padding:3px 8px;border-radius:var(--r-sm);}
.bl-name{font-family:var(--fd);font-size:22px;font-weight:300;}
.bl-sub{font-family:var(--fs);font-size:12px;color:var(--muted);margin-left:auto;}
.note{font-family:var(--fs);font-size:12px;color:var(--muted);border-left:3px solid var(--border);padding:10px 14px;margin-top:16px;line-height:1.6;}
.note strong{color:var(--text);font-weight:500;}
/* ── Colour palette swatches ─────────────────────────────────── */
.swatch-grid{display:flex;flex-wrap:wrap;gap:8px;}
.swatch{width:88px;border-radius:var(--r-md);overflow:hidden;box-shadow:var(--sh-card);}
.swatch-color{height:52px;}
.swatch-label{font-family:var(--fs);font-size:10px;font-weight:500;color:var(--text);padding:5px 7px;background:var(--page);border-top:1px solid var(--border);}
.swatch-sub{font-family:var(--fs);font-size:9px;color:var(--muted);padding:0 7px 5px;}
/* ── Frame ───────────────────────────────────────────────────── */
.frame{display:flex;flex-direction:column;background:var(--page);border:1px solid var(--border);border-radius:var(--r-lg);overflow:hidden;box-shadow:var(--sh-raised);}
.tb{display:flex;align-items:center;gap:7px;padding:11px 18px;border-bottom:1px solid var(--border);background:var(--page);flex-shrink:0;}
.tb-h1{font-family:var(--fd);font-size:17px;font-weight:300;}
.tb-range{font-family:var(--fs);font-size:11px;color:var(--muted);}
.tb-arr{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:var(--r-md);font-size:13px;color:var(--muted);}
.tb-btn{height:28px;padding:0 10px;border:1px solid var(--border);border-radius:var(--r-md);font-family:var(--fs);font-size:11px;font-weight:500;letter-spacing:.04em;color:var(--text);background:var(--page);}
.tb-ml{margin-left:auto;}
.tb-pri{background:var(--gd);color:#fff;border:none;}
.body{display:flex;flex:1;overflow:hidden;}
/* Sidebar */
.sb{width:184px;flex-shrink:0;border-right:1px solid var(--border);background:var(--surface);padding:13px;display:flex;flex-direction:column;gap:13px;}
.sb-lbl{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:5px;}
.score-box{background:var(--yt);border:1px solid var(--yl);border-radius:var(--r-md);padding:10px;}
.sc-big{font-family:var(--fd);font-size:27px;font-weight:300;line-height:1;}
.sc-den{font-family:var(--fs);font-size:11px;color:var(--muted);}
.pbar{height:4px;border-radius:var(--r-full);overflow:hidden;margin-top:6px;}
.pb-y{background:var(--yl);} .pb-t{background:var(--border);}
.pb-fill{height:100%;border-radius:var(--r-full);}
.pb-fg-y{background:var(--y);} .pb-fg-g{background:var(--g);}
.sr{display:flex;align-items:center;gap:6px;margin-top:6px;}
.sr-l{font-family:var(--fs);font-size:10px;color:var(--muted);width:68px;flex-shrink:0;}
.sr-b{flex:1;height:3px;border-radius:var(--r-full);background:var(--border);overflow:hidden;}
.sr-f{height:100%;border-radius:var(--r-full);}
.sr-v{font-family:var(--fm);font-size:9px;color:var(--muted);width:18px;text-align:right;}
.w-item{font-family:var(--fs);font-size:10px;color:var(--yx);margin-top:4px;line-height:1.4;}
.dp{display:flex;gap:2px;margin-top:5px;}
.dp-s{flex:1;height:4px;border-radius:var(--r-full);}
.sb-link{font-family:var(--fs);font-size:10px;font-weight:500;color:var(--yx);display:block;margin-top:8px;}
/* Main */
.main{flex:1;overflow-y:auto;padding:12px;}
.grid7{display:grid;grid-template-columns:repeat(7,1fr);gap:7px;}
/* ═══════════════════════════════════════════════
CARD FLIP SYSTEM
Each tile is a .scene > .card > .front + .back
═══════════════════════════════════════════════ */
.scene{
border-radius:var(--r-lg);
/* Perspective for 3D depth */
perspective:900px;
cursor:pointer;
}
.card{
position:relative;
width:100%;height:100%;
transform-style:preserve-3d;
transition:transform .45s cubic-bezier(.4,0,.2,1);
border-radius:var(--r-lg);
}
.card.flipped{transform:rotateY(180deg);}
/* Both faces */
.card-front,
.card-back{
position:absolute;inset:0;
border-radius:var(--r-lg);
overflow:hidden;
backface-visibility:hidden;
-webkit-backface-visibility:hidden;
}
/* ── FRONT face: full-bleed image ─── */
.card-front{
background-size:cover;
background-position:center;
}
/* Gradient: dark top (header), clear middle, dark bottom (text) */
.front-overlay{
position:absolute;inset:0;
background:
linear-gradient(to bottom,
rgba(0,0,0,.38) 0%,
rgba(0,0,0,0) 28%,
rgba(0,0,0,0) 48%,
rgba(0,0,0,.62) 100%
);
border-radius:inherit;
}
.front-head{
position:absolute;top:0;left:0;right:0;
display:flex;align-items:center;justify-content:space-between;
padding:8px 9px;z-index:2;
}
.front-abbr{font-family:var(--fs);font-size:9px;text-transform:uppercase;letter-spacing:.06em;color:rgba(255,255,255,.85);font-weight:500;}
.front-badge{
width:20px;height:20px;border-radius:var(--r-full);
display:flex;align-items:center;justify-content:center;
font-family:var(--fs);font-size:10px;font-weight:500;
color:rgba(255,255,255,.9);background:rgba(255,255,255,.22);
}
.fb-today{background:var(--y) !important;color:#fff !important;}
.front-info{
position:absolute;bottom:0;left:0;right:0;
padding:8px 9px 10px;z-index:2;
}
.front-name{
font-family:var(--fd);font-size:13px;font-weight:300;
color:#fff;line-height:1.3;
text-shadow:0 1px 4px rgba(0,0,0,.5);
}
.front-meta{font-family:var(--fs);font-size:10px;color:rgba(255,255,255,.78);margin-top:2px;}
.front-tags{display:flex;gap:3px;flex-wrap:wrap;margin-top:5px;}
.ftag{
font-family:var(--fs);font-size:8px;font-weight:500;
padding:2px 5px;border-radius:2px;
background:rgba(255,255,255,.2);color:rgba(255,255,255,.92);
backdrop-filter:blur(2px);
}
/* State rings via box-shadow (no layout shift) */
.card-front.st-default{box-shadow:var(--sh-card);}
.card-front.st-today{box-shadow:0 0 0 2px var(--y), var(--sh-card);}
.card-front.st-sel{box-shadow:0 0 0 2px var(--g), var(--sh-raised);}
.card-back.st-today{box-shadow:0 0 0 2px var(--y), var(--sh-raised);}
.card-back.st-sel{box-shadow:0 0 0 2px var(--g), var(--sh-raised);}
/* ── BACK face: recipe detail ─── */
.card-back{
transform:rotateY(180deg);
background:var(--page);
display:flex;flex-direction:column;
padding:0;
}
/* Thin colour strip at top of back = recipe's colour accent */
.back-strip{height:5px;flex-shrink:0;border-radius:var(--r-lg) var(--r-lg) 0 0;}
.back-inner{
display:flex;flex-direction:column;
flex:1;padding:8px 9px 9px;overflow:hidden;
}
.back-head{
display:flex;align-items:center;justify-content:space-between;
margin-bottom:6px;flex-shrink:0;
}
.back-day{font-family:var(--fs);font-size:9px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);}
.back-close{
width:18px;height:18px;border-radius:var(--r-full);
display:flex;align-items:center;justify-content:center;
background:var(--subtle);font-size:11px;line-height:1;
color:var(--muted);cursor:pointer;flex-shrink:0;
border:none;font-family:var(--fs);
}
.back-close:hover{background:var(--border);}
.back-name{
font-family:var(--fd);font-size:15px;font-weight:300;
line-height:1.25;color:var(--text);
margin-bottom:3px;flex-shrink:0;
}
.back-meta{font-family:var(--fs);font-size:10px;color:var(--muted);margin-bottom:8px;flex-shrink:0;}
.back-ings{display:flex;flex-wrap:wrap;gap:3px;margin-bottom:8px;flex-shrink:0;}
.bing{
font-family:var(--fs);font-size:9px;
background:var(--surface);border:1px solid var(--border);
border-radius:var(--r-full);padding:2px 6px;color:var(--text);
}
.bing-s{background:var(--subtle);border-color:var(--subtle);color:var(--muted);}
.back-actions{display:flex;flex-direction:column;gap:4px;margin-top:auto;}
.bact{
display:block;width:100%;padding:6px 8px;
border-radius:var(--r-md);border:1px solid var(--border);
background:var(--page);font-family:var(--fs);
font-size:10px;font-weight:500;letter-spacing:.04em;
text-align:center;color:var(--text);cursor:pointer;
}
.bact-pri{background:var(--gd);color:#fff;border:none;}
.bact-err{color:var(--err);border-color:var(--err);background:transparent;margin-top:2px;}
/* Tile faded (non-selected state) */
.scene-faded{opacity:.38;pointer-events:none;}
/* ── EMPTY TILE (no flip needed) ─── */
.tile-empty{
border-radius:var(--r-lg);
border:1.5px dashed var(--border);
background:var(--surface);
display:flex;flex-direction:column;
overflow:hidden;box-shadow:var(--sh-card);
cursor:pointer;
}
.te-sel{border:2px dashed var(--g);background:rgba(232,245,234,.5);}
.te-faded{opacity:.22;pointer-events:none;}
.te-head{display:flex;align-items:center;justify-content:space-between;padding:7px 8px 0;flex-shrink:0;}
.te-abbr{font-family:var(--fs);font-size:9px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);}
.te-num{font-family:var(--fs);font-size:10px;font-weight:500;color:var(--muted);}
.te-cta{display:flex;flex-direction:column;align-items:center;padding:7px 6px 5px;gap:2px;flex-shrink:0;border-bottom:1px solid var(--border);}
.te-plus{font-size:17px;color:var(--border);}
.te-label{font-family:var(--fs);font-size:9px;color:var(--muted);}
.sug-list{display:flex;flex-direction:column;padding:5px 7px 5px;flex:1;overflow:hidden;}
.sug-hd{font-family:var(--fs);font-size:8px;font-weight:500;letter-spacing:.07em;text-transform:uppercase;color:var(--muted);padding:3px 0 4px;border-bottom:1px solid var(--subtle);margin-bottom:2px;}
.sug-row{display:flex;align-items:center;gap:4px;padding:5px 0;border-bottom:1px solid var(--subtle);cursor:pointer;}
.sug-row:last-of-type{border-bottom:none;}
.sug-name{font-family:var(--fd);font-size:11px;font-weight:300;color:var(--text);flex:1;line-height:1.2;}
.stag{font-family:var(--fs);font-size:8px;font-weight:500;padding:1px 4px;border-radius:2px;white-space:nowrap;flex-shrink:0;}
.st-g{background:var(--gt);color:var(--gd);}
.st-y{background:var(--yt);color:var(--yx);}
.sug-more{font-family:var(--fs);font-size:9px;font-weight:500;color:var(--yx);text-align:center;padding-top:4px;margin-top:auto;}
/* ── Image backgrounds ───────────────────────── */
.img-haehnchen{background:linear-gradient(160deg,#d4923a 0%,#a85e1a 50%,#7a3d0c 100%);}
.img-rind {background:linear-gradient(160deg,#c04545 0%,#8b2020 50%,#5a1010 100%);}
.img-stirfry {background:linear-gradient(160deg,#5fa85e 0%,#2e7031 50%,#1a4a1e 100%);}
.img-fisch {background:linear-gradient(160deg,#5b9fd4 0%,#2868a0 50%,#10406e 100%);}
.img-pizza {background:linear-gradient(160deg,#d4a832 0%,#a07010 50%,#6e4a00 100%);}
/* Accent strip matches image colours */
.strip-haehnchen{background:linear-gradient(90deg,#d4923a,#a85e1a);}
.strip-rind {background:linear-gradient(90deg,#c04545,#8b2020);}
.strip-stirfry {background:linear-gradient(90deg,#5fa85e,#2e7031);}
.strip-fisch {background:linear-gradient(90deg,#5b9fd4,#2868a0);}
.strip-pizza {background:linear-gradient(90deg,#d4a832,#a07010);}
/* ── Demo controls ───────────────────────────── */
.demo-hint{
font-family:var(--fs);font-size:11px;color:var(--muted);
text-align:center;margin-bottom:10px;
}
.demo-hint span{
background:var(--subtle);border-radius:var(--r-sm);
padding:2px 8px;font-weight:500;color:var(--text);
}
.specimen-row{display:flex;gap:14px;margin-bottom:8px;flex-wrap:wrap;align-items:flex-start;}
.specimen-wrap{display:flex;flex-direction:column;align-items:center;gap:6px;}
.specimen-label{font-family:var(--fs);font-size:10px;font-weight:500;letter-spacing:.07em;text-transform:uppercase;color:var(--muted);text-align:center;}
</style>
</head>
<body>
<p class="eyebrow">Mealplan · Planer · Flip Tiles</p>
<h1 class="pg-title">Kachel-Flip + Zutaten-Farben</h1>
<p class="pg-sub">
Klick auf eine gefüllte Kachel → sie dreht sich um. Auf der Rückseite: Rezeptname, Hauptzutaten, Aktionen.
Kein Expansion-Panel mehr. Leere Kacheln bleiben unverändert mit Inline-Vorschlägen.
</p>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- SEKTION 1: FARB-PALETTE -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="block">
<div class="bl-hd">
<span class="bl-num">Palette</span>
<span class="bl-name">Farben nach Hauptzutat / Küchenstil</span>
<span class="bl-sub">Fallback wenn heroImageUrl fehlt</span>
</div>
<div class="swatch-grid">
<!-- Proteins -->
<div class="swatch"><div class="swatch-color img-haehnchen"></div><div class="swatch-label">Hähnchen</div><div class="swatch-sub">Protein</div></div>
<div class="swatch"><div class="swatch-color img-rind"></div><div class="swatch-label">Rind</div><div class="swatch-sub">Protein</div></div>
<div class="swatch"><div class="swatch-color img-fisch"></div><div class="swatch-label">Fisch</div><div class="swatch-sub">Protein</div></div>
<div class="swatch"><div class="swatch-color img-stirfry"></div><div class="swatch-label">Tofu</div><div class="swatch-sub">Protein</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#7bc47b 0%,#3d8c3d 50%,#1e5a1e 100%);"></div><div class="swatch-label">vegetarisch</div><div class="swatch-sub">Protein</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#d4785a 0%,#a04535 50%,#6e2418 100%);"></div><div class="swatch-label">Schwein</div><div class="swatch-sub">Protein</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#9e6b3a 0%,#6b3f1a 50%,#3e2208 100%);"></div><div class="swatch-label">Lamm</div><div class="swatch-sub">Protein</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#d4b832 0%,#a07010 50%,#6e4800 100%);"></div><div class="swatch-label">Ei</div><div class="swatch-sub">Protein</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#8b6b3a 0%,#5e421a 50%,#3a2408 100%);"></div><div class="swatch-label">Hülsenfrüchte</div><div class="swatch-sub">Protein</div></div>
<!-- Cuisine overrides -->
<div class="swatch"><div class="swatch-color img-pizza"></div><div class="swatch-label">Italienisch</div><div class="swatch-sub">Küche</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#3a6e3a 0%,#1e4a1e 50%,#0e2e0e 100%);"></div><div class="swatch-label">Asiatisch</div><div class="swatch-sub">Küche</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#c49010 0%,#8b5e00 50%,#5a3800 100%);"></div><div class="swatch-label">Indisch</div><div class="swatch-sub">Küche</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#c04545 0%,#7a1e1e 50%,#4a0f0f 100%);"></div><div class="swatch-label">Mexikanisch</div><div class="swatch-sub">Küche</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#4a90b8 0%,#1e5a8b 50%,#0a3456 100%);"></div><div class="swatch-label">Mediterran</div><div class="swatch-sub">Küche</div></div>
</div>
<div class="note">
<strong>Priorität:</strong> Wenn <code>heroImageUrl</code> vorhanden → echtes Foto.
Sonst: Farbe nach erstem Protein-Tag (z.B. <code>tagType=protein</code>, <code>tagName=Hähnchen</code>).
Wenn kein Protein-Tag → Farbe nach Küchenstil-Tag (<code>tagType=cuisine</code>).
Fallback auf <code>--color-surface</code> neutral.
Die Farbwerte werden als CSS-Klassen gemappt: <code>protein-haehnchen</code>, <code>cuisine-asiatisch</code> etc.
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- SEKTION 2: INTERACTIVE FLIP DEMO -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="block">
<div class="bl-hd">
<span class="bl-num">Demo</span>
<span class="bl-name">Flip-Interaktion — zum Klicken</span>
<span class="bl-sub">Echte CSS-3D-Transition</span>
</div>
<p class="demo-hint">Klicke auf eine Kachel um sie umzudrehen. <span>×</span> auf der Rückseite klappt zurück.</p>
<div class="specimen-row">
<!-- Tile 1: Hähnchen-Curry (normal) -->
<div class="specimen-wrap">
<div class="specimen-label">Standard</div>
<div class="scene" style="width:150px;height:240px;" onclick="flip(this)">
<div class="card">
<div class="card-front img-haehnchen st-default">
<div class="front-overlay"></div>
<div class="front-head">
<span class="front-abbr">Mo</span>
<span class="front-badge">7</span>
</div>
<div class="front-info">
<div class="front-name">Hähnchen-Curry</div>
<div class="front-meta">35 Min · mittel</div>
<div class="front-tags">
<span class="ftag">Hähnchen</span>
<span class="ftag">4 Port.</span>
</div>
</div>
</div>
<div class="card-back st-default">
<div class="back-strip strip-haehnchen"></div>
<div class="back-inner">
<div class="back-head">
<span class="back-day">Mo · 7. Apr</span>
<button class="back-close" onclick="unflip(event,this)">×</button>
</div>
<div class="back-name">Hähnchen-Curry</div>
<div class="back-meta">35 Min · mittel · 4 Port.</div>
<div class="back-ings">
<span class="bing">Hähnchen</span>
<span class="bing">Kokosmilch</span>
<span class="bing">Paprika</span>
<span class="bing">Spinat</span>
<span class="bing-s">Curry</span>
<span class="bing-s">Knoblauch</span>
</div>
<div class="back-actions">
<button class="bact bact-pri">Koch-Modus</button>
<button class="bact">Rezept ansehen</button>
<button class="bact">Gericht tauschen</button>
<button class="bact bact-err">Entfernen</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tile 2: Pasta Bolognese (today) -->
<div class="specimen-wrap">
<div class="specimen-label">Heute</div>
<div class="scene" style="width:150px;height:240px;" onclick="flip(this)">
<div class="card">
<div class="card-front img-rind st-today">
<div class="front-overlay"></div>
<div class="front-head">
<span class="front-abbr">Di</span>
<span class="front-badge fb-today">8</span>
</div>
<div class="front-info">
<div class="front-name">Pasta Bolognese</div>
<div class="front-meta">45 Min · mittel</div>
<div class="front-tags">
<span class="ftag" style="background:rgba(242,193,46,.35);">Rind</span>
<span class="ftag" style="background:rgba(242,193,46,.35);">Heute</span>
</div>
</div>
</div>
<div class="card-back st-today">
<div class="back-strip strip-rind"></div>
<div class="back-inner">
<div class="back-head">
<span class="back-day" style="color:var(--yx);">Di · Heute</span>
<button class="back-close" onclick="unflip(event,this)">×</button>
</div>
<div class="back-name">Pasta Bolognese</div>
<div class="back-meta">45 Min · mittel · 4 Port.</div>
<div class="back-ings">
<span class="bing">Rinderhack</span>
<span class="bing">Pasta</span>
<span class="bing">Tomaten</span>
<span class="bing">Zwiebeln</span>
<span class="bing-s">Olivenöl</span>
<span class="bing-s">Knoblauch</span>
</div>
<div class="back-actions">
<button class="bact bact-pri">Koch-Modus</button>
<button class="bact">Rezept ansehen</button>
<button class="bact">Gericht tauschen</button>
<button class="bact bact-err">Entfernen</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tile 3: Gemüse-Stir-fry (selected + flipped by default) -->
<div class="specimen-wrap">
<div class="specimen-label">Ausgewählt (bereits umgedreht)</div>
<div class="scene" style="width:150px;height:240px;" onclick="flip(this)">
<div class="card flipped">
<div class="card-front img-stirfry st-sel">
<div class="front-overlay"></div>
<div class="front-head">
<span class="front-abbr">Mi</span>
<span class="front-badge" style="background:var(--g);color:#fff;">9</span>
</div>
<div class="front-info">
<div class="front-name">Gemüse-Stir-fry</div>
<div class="front-meta">20 Min · einfach</div>
<div class="front-tags"><span class="ftag" style="background:rgba(61,140,74,.4);">Tofu</span></div>
</div>
</div>
<div class="card-back st-sel">
<div class="back-strip strip-stirfry"></div>
<div class="back-inner">
<div class="back-head">
<span class="back-day" style="color:var(--gd);">Mi · 9. Apr</span>
<button class="back-close" onclick="unflip(event,this)">×</button>
</div>
<div class="back-name">Gemüse-Stir-fry</div>
<div class="back-meta">20 Min · einfach · 2 Port.</div>
<div class="back-ings">
<span class="bing">Tofu</span>
<span class="bing">Paprika</span>
<span class="bing">Brokkoli</span>
<span class="bing">Karotten</span>
<span class="bing-s">Sesamöl</span>
<span class="bing-s">Sojasauce</span>
</div>
<div class="back-actions">
<button class="bact bact-pri">Koch-Modus</button>
<button class="bact">Rezept ansehen</button>
<button class="bact">Gericht tauschen</button>
<button class="bact bact-err">Entfernen</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Empty tile with suggestions -->
<div class="specimen-wrap">
<div class="specimen-label">Leer — kein Flip</div>
<div class="tile-empty" style="width:150px;height:240px;">
<div class="te-head">
<span class="te-abbr">Sa</span>
<span class="te-num">12</span>
</div>
<div class="te-cta">
<div class="te-plus">+</div>
<div class="te-label">Gericht wählen</div>
</div>
<div class="sug-list">
<div class="sug-hd">Vorschläge</div>
<div class="sug-row"><span class="sug-name">Ramen mit Ei</span><span class="stag st-g">Neues Protein</span></div>
<div class="sug-row"><span class="sug-name">Shakshuka</span><span class="stag st-g">Kein Overlap</span></div>
<div class="sug-row"><span class="sug-name">Tacos</span><span class="stag st-y">Aufwand: leicht</span></div>
<div class="sug-more">Alle Rezepte →</div>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Flip-Mechanik:</strong> CSS <code>transform:rotateY(180deg)</code> auf dem <code>.card</code> wrapper,
<code>backface-visibility:hidden</code> auf beiden Faces, <code>perspective:900px</code> auf der Scene.
Transition: <code>.45s cubic-bezier(.4,0,.2,1)</code> (Material-Easing — schnell herein, weich heraus).
Der <code>×</code> Button auf der Rückseite stoppt den Klick-Event mit <code>stopPropagation()</code>
und dreht die Karte zurück. Kein zusätzlicher State nötig — die Karte ist selbst das State-Element.
<br><br>
<strong>Farbstreifen</strong> oben auf der Rückseite = 5px Gradient, identisch mit der Front-Farbe.
Gibt visuelle Kontinuität zwischen Vorder- und Rückseite.
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- SEKTION 3: VOLLSTÄNDIGE SEITENANSICHT — Mi UMGEDREHT -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="block">
<div class="bl-hd">
<span class="bl-num">Seite</span>
<span class="bl-name">Vollansicht — Mittwoch umgedreht</span>
<span class="bl-sub">Kein rechtes Panel. Kacheln bis zum Rand.</span>
</div>
<div class="frame" style="height:560px;">
<div class="tb">
<span class="tb-h1">Wochenplaner</span>
<span class="tb-range">7.13. Apr</span>
<div class="tb-arr"></div><div class="tb-arr"></div>
<button class="tb-btn tb-ml">Heute</button>
</div>
<div class="body">
<!-- Sidebar -->
<div class="sb">
<div class="score-box">
<div class="sb-lbl">Abwechslungs-Score</div>
<div style="display:flex;align-items:baseline;gap:4px;"><span class="sc-big">7.8</span><span class="sc-den">/10</span></div>
<div class="pbar pb-y"><div class="pb-fill pb-fg-y" style="width:78%;"></div></div>
<div class="sr"><span class="sr-l">Protein</span><div class="sr-b"><div class="sr-f" style="width:80%;background:var(--g);"></div></div><span class="sr-v">8.0</span></div>
<div class="sr"><span class="sr-l">Zutaten</span><div class="sr-b"><div class="sr-f" style="width:72%;background:var(--y);"></div></div><span class="sr-v">7.2</span></div>
<div class="sr"><span class="sr-l">Aufwand</span><div class="sr-b"><div class="sr-f" style="width:82%;background:var(--g);"></div></div><span class="sr-v">8.2</span></div>
<a class="sb-link">Variety-Analyse →</a>
</div>
<div>
<div class="sb-lbl">Überschneidungen</div>
<div class="w-item">⚠ Hähnchen an Mo + Do</div>
<div class="w-item">⚠ Tomaten an Di + Do</div>
</div>
<div>
<div class="sb-lbl">Geplant</div>
<div style="display:flex;align-items:baseline;gap:3px;"><span style="font-family:var(--fd);font-size:20px;font-weight:300;">5</span><span style="font-family:var(--fs);font-size:10px;color:var(--muted);">/ 7 Tage</span></div>
<div class="dp" style="margin-top:5px;">
<div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--g);"></div><div class="dp-s" style="background:var(--border);"></div><div class="dp-s" style="background:var(--border);"></div>
</div>
</div>
</div>
<!-- MAIN: grid fills full height, Mi is flipped -->
<div class="main">
<div class="grid7" style="height:100%;">
<!-- Mo: faded -->
<div class="scene scene-faded" style="height:100%;">
<div class="card">
<div class="card-front img-haehnchen st-default">
<div class="front-overlay"></div>
<div class="front-head"><span class="front-abbr">Mo</span><span class="front-badge">7</span></div>
<div class="front-info"><div class="front-name">Hähnchen-Curry</div><div class="front-meta">35 Min · mittel</div></div>
</div>
</div>
</div>
<!-- Di: today, faded -->
<div class="scene scene-faded" style="height:100%;">
<div class="card">
<div class="card-front img-rind st-today">
<div class="front-overlay"></div>
<div class="front-head"><span class="front-abbr">Di</span><span class="front-badge fb-today">8</span></div>
<div class="front-info"><div class="front-name">Pasta Bolognese</div><div class="front-meta">45 Min · mittel</div></div>
</div>
</div>
</div>
<!-- Mi: SELECTED + FLIPPED -->
<div class="scene" style="height:100%;" onclick="flip(this)">
<div class="card flipped">
<div class="card-front img-stirfry st-sel">
<div class="front-overlay"></div>
<div class="front-head">
<span class="front-abbr">Mi</span>
<span class="front-badge" style="background:var(--g);color:#fff;">9</span>
</div>
<div class="front-info">
<div class="front-name">Gemüse-Stir-fry</div>
<div class="front-meta">20 Min · einfach</div>
<div class="front-tags"><span class="ftag" style="background:rgba(61,140,74,.4);">Tofu</span></div>
</div>
</div>
<div class="card-back st-sel">
<div class="back-strip strip-stirfry"></div>
<div class="back-inner">
<div class="back-head">
<span class="back-day" style="color:var(--gd);">Mi · 9. Apr</span>
<button class="back-close" onclick="unflip(event,this)">×</button>
</div>
<div class="back-name">Gemüse-Stir-fry</div>
<div class="back-meta">20 Min · einfach · 2 Port.</div>
<div class="back-ings">
<span class="bing">Tofu</span>
<span class="bing">Paprika</span>
<span class="bing">Brokkoli</span>
<span class="bing">Karotten</span>
<span class="bing">Ingwer</span>
<span class="bing-s">Sesamöl</span>
<span class="bing-s">Sojasauce</span>
</div>
<div class="back-actions">
<button class="bact bact-pri">Koch-Modus</button>
<button class="bact">Rezept ansehen</button>
<button class="bact">Gericht tauschen</button>
<button class="bact bact-err">Entfernen</button>
</div>
</div>
</div>
</div>
</div>
<!-- Do: faded -->
<div class="scene scene-faded" style="height:100%;">
<div class="card">
<div class="card-front img-fisch st-default">
<div class="front-overlay"></div>
<div class="front-head"><span class="front-abbr">Do</span><span class="front-badge">10</span></div>
<div class="front-info"><div class="front-name">Lachs mit Kartoffeln</div><div class="front-meta">30 Min · einfach</div></div>
</div>
</div>
</div>
<!-- Fr: faded -->
<div class="scene scene-faded" style="height:100%;">
<div class="card">
<div class="card-front img-pizza st-default">
<div class="front-overlay"></div>
<div class="front-head"><span class="front-abbr">Fr</span><span class="front-badge">11</span></div>
<div class="front-info"><div class="front-name">Pizza Margherita</div><div class="front-meta">50 Min · aufwändig</div></div>
</div>
</div>
</div>
<!-- Sa: empty with suggestions -->
<div class="tile-empty te-faded" style="height:100%;">
<div class="te-head"><span class="te-abbr">Sa</span><span class="te-num">12</span></div>
<div class="te-cta"><div class="te-plus">+</div><div class="te-label">Gericht wählen</div></div>
<div class="sug-list">
<div class="sug-hd">Vorschläge</div>
<div class="sug-row"><span class="sug-name">Ramen mit Ei</span><span class="stag st-g">Neues Protein</span></div>
<div class="sug-row"><span class="sug-name">Shakshuka</span><span class="stag st-g">Kein Overlap</span></div>
<div class="sug-more">Alle Rezepte →</div>
</div>
</div>
<!-- So: empty with suggestions -->
<div class="tile-empty te-faded" style="height:100%;">
<div class="te-head"><span class="te-abbr">So</span><span class="te-num">13</span></div>
<div class="te-cta"><div class="te-plus">+</div><div class="te-label">Gericht wählen</div></div>
<div class="sug-list">
<div class="sug-hd">Vorschläge</div>
<div class="sug-row"><span class="sug-name">Grünes Thai-Curry</span><span class="stag st-g">Neues Protein</span></div>
<div class="sug-row"><span class="sug-name">Tacos</span><span class="stag st-y">Aufwand: leicht</span></div>
<div class="sug-more">Alle Rezepte →</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Layout:</strong> Linke Sidebar (Variety-Score) bleibt. Kein rechtes Panel mehr.
Die Kacheln füllen den gesamten verbleibenden Platz (<code>flex:1</code>) — 7 gleich breite Spalten,
volle Höhe (<code>height:100%</code> auf Grid und Kacheln). Kein Layout-Shift, kein After-Scroll.
<br><br>
<strong>Dimm-Effekt:</strong> Beim Flip werden alle anderen Kacheln auf 38% gedimmt.
Kein neuer API-Aufruf nötig — reine CSS-Klasse per JS.
<br><br>
<strong>„Gericht tauschen":</strong> Öffnet den Rezept-Picker als Slide-in-Drawer von rechts
(kein persistentes Panel). Drawer schließt sich nach Auswahl oder Abbruch.
<br><br>
<strong>Leere Kacheln:</strong> Zeigen Inline-Vorschläge auch im gedimmten Zustand (wenn
eine andere Kachel geflippt ist). Kein Flip auf leeren Kacheln.
</div>
</div>
<script>
function flip(scene) {
const card = scene.querySelector('.card');
const isFlipped = card.classList.toggle('flipped');
// Dim all other scenes in the same grid
const grid = scene.closest('.grid7');
if (!grid) return;
grid.querySelectorAll('.scene, .tile-empty').forEach(el => {
if (el === scene) return;
if (isFlipped) {
el.style.opacity = '0.38';
el.style.pointerEvents = 'none';
} else {
el.style.opacity = '';
el.style.pointerEvents = '';
}
});
}
function unflip(event, btn) {
event.stopPropagation();
const scene = btn.closest('.scene');
const card = scene.querySelector('.card');
card.classList.remove('flipped');
// Un-dim everything
const grid = scene.closest('.grid7');
if (!grid) return;
grid.querySelectorAll('.scene, .tile-empty').forEach(el => {
el.style.opacity = '';
el.style.pointerEvents = '';
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,459 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Planner Redesign — Flip Tiles · Final Spec</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: Planner Desktop Redesign — Flip Tiles
version: 1.0
route: /planner (desktop)
screens: Planner main area — tile grid, sidebar, recipe picker drawer
key-decisions:
- Full-bleed color/image tiles (no blank body space)
- CSS 3D card flip replaces expansion panel
- No persistent right panel — tiles fill full remaining width
- Ingredient/cuisine color palette as heroImageUrl fallback
- Inline suggestions on empty tiles (reasoning tags, no delta numbers)
- No "Gericht hinzufügen" toolbar button (empty tile CTA handles it)
- Recipe picker opens as slide-in drawer (on demand only)
last-updated: 2026-04
reference-mockups:
- specs/planner-flip-tiles.html (interactive demo, color palette)
-->
<style>
:root {
--color-page: #FAFAF7;
--color-surface: #F5F4EE;
--color-subtle: #EDECEA;
--color-border: #D8D7D0;
--color-text-muted: #6B6A63;
--color-text: #1C1C18;
--green-tint: #E8F5EA;
--green-light: #AEDCB0;
--green: #3D8C4A;
--green-dark: #2E6E39;
--yellow-tint: #FDF6D8;
--yellow-light: #F9E08A;
--yellow: #F2C12E;
--yellow-text: #8A6800;
--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-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,.10), 0 2px 4px rgba(28,28,24,.06);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-sans); background: var(--color-page); color: var(--color-text); font-size: 14px; line-height: 1.6; }
.doc { max-width: 900px; 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 var(--color-border); margin-bottom: 48px; }
.doc-header h1 { font-family: var(--font-display); font-size: 26px; font-weight: 500; letter-spacing: -0.02em; margin-bottom: 4px; }
.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; }
.intro { font-size: 14px; line-height: 1.75; color: var(--color-text); max-width: 700px; margin-bottom: 48px; }
.intro p + p { margin-top: 12px; }
.section { margin-bottom: 56px; }
.section-label { font-size: 10px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-text-muted); padding-bottom: 10px; border-bottom: 1px solid var(--color-border); margin-bottom: 28px; }
h2 { font-family: var(--font-display); font-size: 20px; font-weight: 400; margin-bottom: 14px; }
h3 { font-size: 13px; font-weight: 600; margin-bottom: 8px; color: var(--color-text); }
p { margin-bottom: 10px; font-size: 14px; line-height: 1.7; }
ul { padding-left: 20px; margin-bottom: 12px; }
li { font-size: 14px; line-height: 1.65; margin-bottom: 4px; }
code { font-family: var(--font-mono); font-size: 12px; background: var(--color-subtle); border-radius: 3px; padding: 1px 5px; }
pre { font-family: var(--font-mono); font-size: 12px; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 14px 16px; margin: 12px 0; overflow-x: auto; line-height: 1.6; }
.callout { background: var(--color-surface); border-left: 3px solid var(--color-border); border-radius: 0 var(--radius-md) var(--radius-md) 0; padding: 12px 16px; margin: 16px 0; font-size: 13px; line-height: 1.65; }
.callout.green { border-color: var(--green); background: var(--green-tint); }
.callout.yellow { border-color: var(--yellow); background: var(--yellow-tint); }
.callout strong { font-weight: 600; }
table { width: 100%; border-collapse: collapse; font-size: 13px; margin: 16px 0; }
th { font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); padding: 8px 12px; text-align: left; border-bottom: 2px solid var(--color-border); }
td { padding: 9px 12px; border-bottom: 1px solid var(--color-subtle); vertical-align: top; }
tr:last-child td { border-bottom: none; }
.swatch-row { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0; }
.swatch { width: 80px; border-radius: var(--radius-md); overflow: hidden; box-shadow: var(--shadow-card); }
.swatch-color { height: 44px; }
.swatch-name { font-size: 10px; font-weight: 500; padding: 4px 6px; background: var(--color-page); border-top: 1px solid var(--color-border); }
.swatch-sub { font-size: 9px; color: var(--color-text-muted); padding: 0 6px 4px; }
.state-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin: 16px 0; }
.state-card { border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 14px 16px; }
.state-name { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 6px; }
.state-desc { font-size: 13px; line-height: 1.6; }
.component-row { display: flex; gap: 8px; align-items: baseline; padding: 10px 0; border-bottom: 1px solid var(--color-subtle); }
.component-row:last-child { border-bottom: none; }
.comp-file { font-family: var(--font-mono); font-size: 12px; color: var(--color-text); flex: 0 0 auto; min-width: 280px; }
.comp-action { font-size: 13px; color: var(--color-text-muted); }
.badge { display: inline-block; font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: var(--radius-full); }
.badge-new { background: var(--green-tint); color: var(--green-dark); }
.badge-mod { background: var(--yellow-tint); color: var(--yellow-text); }
.badge-del { background: #fde8e8; color: var(--color-error); }
</style>
</head>
<body>
<div class="doc">
<div class="doc-header">
<div>
<h1>Planner Desktop Redesign</h1>
<p>Flip Tiles · Final Spec · Route: <code>/planner</code></p>
</div>
<div class="doc-meta">
Version 1.0<br>
2026-04<br>
Mockup: <code>specs/planner-flip-tiles.html</code>
</div>
</div>
<div class="intro">
<p>
Der Wochenplaner hat auf Desktop aktuell ~80 % vertikalen Leerraum unterhalb des 7-Spalten-Kalenders.
Zusätzlich ist das rechte Panel im Leerlauf nicht genutzt. Dieses Spec beschreibt ein vollständiges
Redesign der Desktop-Hauptfläche: Die Kacheln füllen die volle Höhe und Breite, Rezeptdetails werden
über einen CSS-3D-Flip direkt in der Kachel angezeigt, und leere Tage zeigen Inline-Vorschläge.
</p>
<p>
Das rechte Panel entfällt dauerhaft. Der Rezept-Picker öffnet sich als Slide-in-Drawer ausschließlich
auf Anfrage (Aktion „Gericht tauschen" auf der Kachel-Rückseite). Der Toolbar-Button
„Gericht hinzufügen" entfällt, da jede leere Kachel eine eigene CTA hat.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="section">
<div class="section-label">01 · Layout</div>
<h2>Seitenstruktur</h2>
<p>Desktop-Layout: 2 Spalten. Kein persistentes rechtes Panel mehr.</p>
<pre>┌─────────────────────────────────────────────────────────────┐
│ Toolbar (Wochenplaner · 7.13. Apr Heute) │
├──────────┬──────────────────────────────────────────────────┤
│ Sidebar │ 7-Spalten-Kachelgrid (flex: 1, height: 100%) │
│ 184 px │ │
│ Variety │ Mo Di Mi Do Fr Sa So │
│ Score │ ████ ████ ████ ████ ████ ░░░░ ░░░░ │
│ │ ████ ████ ████ ████ ████ ░+░░ ░+░░ │
│ │ ████ ████ ████ ████ ████ ░Vor░ ░Vor░ │
└──────────┴──────────────────────────────────────────────────┘</pre>
<ul>
<li><strong>Sidebar (184 px, flex-shrink: 0):</strong> Variety-Score-Card, Sub-Scores, Überschneidungs-Warnungen, Link zur Variety-Analyse. Unverändert.</li>
<li><strong>Main (flex: 1):</strong> <code>display: grid; grid-template-columns: repeat(7, 1fr); gap: 7px; height: 100%</code>. Kacheln füllen die gesamte verbleibende Breite und Höhe.</li>
<li><strong>Toolbar:</strong> Nur Navigation — Wochenbezeichnung, Zurück/Vor-Pfeile, Heute-Button. Kein „+ Gericht hinzufügen" mehr.</li>
</ul>
<div class="callout yellow">
<strong>Entfernt:</strong> Das rechte Panel (<code>width: 228px</code>) mit der „Heute Abend"-Karte und dem Leerlauf-Hinweis entfällt vollständig. Koch-Modus ist auf der Kachel-Rückseite zugänglich.
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="section">
<div class="section-label">02 · Kachel-Zustände</div>
<h2>Tile States</h2>
<div class="state-grid">
<div class="state-card">
<div class="state-name">Standard (gefüllt)</div>
<div class="state-desc">
Vollbild-Farbhintergrund (Gradient nach Zutat/Küche) oder <code>heroImageUrl</code>.
Dual-Gradient-Overlay (oben + unten dunkel, Mitte klar).
Oben: Tageskürzel + Datumsziffer. Unten: Rezeptname, Kochzeit, Tags.
<br><br>
<code>box-shadow: var(--sh-card)</code> — kein sichtbarer Ring.
</div>
</div>
<div class="state-card">
<div class="state-name">Heute (gefüllt)</div>
<div class="state-desc">
Identisch wie Standard, aber mit gelbem Ring via
<code>box-shadow: 0 0 0 2px var(--yellow), var(--sh-card)</code>.
Datumsziffer-Badge in <code>--yellow</code>. Tag-Label „Heute" zusätzlich als frosted Tag.
</div>
</div>
<div class="state-card">
<div class="state-name">Ausgewählt / Geflippt</div>
<div class="state-desc">
Grüner Ring: <code>box-shadow: 0 0 0 2px var(--green), var(--sh-raised)</code>.
Karte dreht sich 180° (CSS 3D, siehe §04). Alle anderen Kacheln werden auf 38 % Deckkraft
gedimmt und sind nicht klickbar.
</div>
</div>
<div class="state-card">
<div class="state-name">Leer</div>
<div class="state-desc">
Kein Flip. Gestrichelter Rahmen (<code>border: 1.5px dashed var(--color-border)</code>),
<code>background: var(--color-surface)</code>. Oben: Tageskürzel + Datum.
Darunter: <code>+</code> Icon + „Gericht wählen". Rest der Kachel: Inline-Vorschläge (§05).
</div>
</div>
</div>
<div class="callout">
<strong>box-shadow statt border:</strong> Statusringe werden via <code>box-shadow</code> gesetzt, nicht via <code>border</code>,
um Layout-Shift zu vermeiden. Die Kacheln behalten identische Außenmaße in allen Zuständen.
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="section">
<div class="section-label">03 · Farb-Palette</div>
<h2>Ingredient &amp; Cuisine Colors</h2>
<p>
Wenn <code>heroImageUrl</code> vorhanden ist, wird das echte Foto als <code>background-image</code> gesetzt.
Fehlt es, greift die folgende Prioritätskette:
</p>
<ol style="padding-left:20px;margin-bottom:16px;">
<li>Ersten Tag mit <code>tagType = "protein"</code> finden → Protein-Farbe</li>
<li>Ersten Tag mit <code>tagType = "cuisine"</code> finden → Küchenstil-Farbe</li>
<li>Fallback: <code>background: var(--color-surface)</code> (neutral)</li>
</ol>
<h3>Protein-Farben</h3>
<div class="swatch-row">
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#d4923a,#a85e1a,#7a3d0c)"></div><div class="swatch-name">Hähnchen</div><div class="swatch-sub">protein-haehnchen</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#c04545,#8b2020,#5a1010)"></div><div class="swatch-name">Rind</div><div class="swatch-sub">protein-rind</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#5b9fd4,#2868a0,#10406e)"></div><div class="swatch-name">Fisch</div><div class="swatch-sub">protein-fisch</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#5fa85e,#2e7031,#1a4a1e)"></div><div class="swatch-name">Tofu</div><div class="swatch-sub">protein-tofu</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#7bc47b,#3d8c3d,#1e5a1e)"></div><div class="swatch-name">Vegetarisch</div><div class="swatch-sub">protein-veg</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#d4785a,#a04535,#6e2418)"></div><div class="swatch-name">Schwein</div><div class="swatch-sub">protein-schwein</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#9e6b3a,#6b3f1a,#3e2208)"></div><div class="swatch-name">Lamm</div><div class="swatch-sub">protein-lamm</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#d4b832,#a07010,#6e4800)"></div><div class="swatch-name">Ei</div><div class="swatch-sub">protein-ei</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#8b6b3a,#5e421a,#3a2408)"></div><div class="swatch-name">Hülsen­früchte</div><div class="swatch-sub">protein-huelsenfruechte</div></div>
</div>
<h3>Küchenstil-Farben</h3>
<div class="swatch-row">
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#c04545,#7a1e1e,#4a0f0f)"></div><div class="swatch-name">Italienisch</div><div class="swatch-sub">cuisine-italienisch</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#3a6e3a,#1e4a1e,#0e2e0e)"></div><div class="swatch-name">Asiatisch</div><div class="swatch-sub">cuisine-asiatisch</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#c49010,#8b5e00,#5a3800)"></div><div class="swatch-name">Indisch</div><div class="swatch-sub">cuisine-indisch</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#d4923a,#8b4e10,#5a2e00)"></div><div class="swatch-name">Mexikanisch</div><div class="swatch-sub">cuisine-mexikanisch</div></div>
<div class="swatch"><div class="swatch-color" style="background:linear-gradient(160deg,#4a90b8,#1e5a8b,#0a3456)"></div><div class="swatch-name">Mediterran</div><div class="swatch-sub">cuisine-mediterran</div></div>
</div>
<p>
Die CSS-Klassen (<code>protein-haehnchen</code>, <code>cuisine-asiatisch</code>, …) werden
serverseitig aus den Rezept-Tags abgeleitet und als Svelte-Prop übergeben, z.B.
<code>colorClass="protein-haehnchen"</code>. Das Component setzt die Klasse auf dem Kachel-Wrapper.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="section">
<div class="section-label">04 · Flip-Mechanik</div>
<h2>CSS 3D Card Flip</h2>
<p>Jede gefüllte Kachel besteht aus drei verschachtelten Elementen:</p>
<pre>.scene → perspective: 900px; border-radius: var(--radius-lg); cursor: pointer
.card → position: relative; transform-style: preserve-3d
transition: transform .45s cubic-bezier(.4,0,.2,1)
.card.flipped → transform: rotateY(180deg)
.card-front → backface-visibility: hidden; position: absolute; inset: 0
.card-back → backface-visibility: hidden; transform: rotateY(180deg)
position: absolute; inset: 0; background: var(--color-page)</pre>
<h3>Vorderseite</h3>
<ul>
<li>Vollbild-Farbe oder <code>background-image: url(heroImageUrl)</code> mit <code>background-size: cover</code></li>
<li>Dual-Gradient-Overlay als absolutes <code>::after</code>-Pseudo-Element:<br>
<code>linear-gradient(to bottom, rgba(0,0,0,.38) 0%, transparent 28%, transparent 48%, rgba(0,0,0,.62) 100%)</code></li>
<li>Oben links: Tageskürzel (9px uppercase). Oben rechts: Datums-Badge (Kreis)</li>
<li>Unten: Rezeptname (Fraunces 13px), Meta-Zeile (Kochzeit · Aufwand), Tag-Chips</li>
</ul>
<h3>Rückseite</h3>
<ul>
<li><strong>Farbstreifen (5 px)</strong> oben — identischer Gradient wie die Vorderseite. Gibt visuelle Kontinuität.</li>
<li>Tageskürzel + Datum (links) · × Schließen-Button (rechts)</li>
<li>Rezeptname (Fraunces 15px)</li>
<li>Meta: Kochzeit · Aufwand · Portionen</li>
<li>Zutaten-Pills: normale Zutaten als <code>.ingredient</code>, Vorrats-Zutaten (Staples) gedimmt als <code>.ingredient--staple</code></li>
<li>Aktionen (gestapelt, volle Breite):</li>
</ul>
<table>
<thead><tr><th>Aktion</th><th>Stil</th><th>Verhalten</th></tr></thead>
<tbody>
<tr><td>Koch-Modus starten</td><td>Primary (grün ausgefüllt)</td><td>Navigiert zu <code>/planner/cook/[slotId]</code></td></tr>
<tr><td>Rezept ansehen</td><td>Secondary (Rahmen)</td><td>Navigiert zu <code>/recipes/[recipeId]</code></td></tr>
<tr><td>Gericht tauschen</td><td>Secondary (Rahmen)</td><td>Öffnet Rezept-Picker-Drawer (§06)</td></tr>
<tr><td>Entfernen</td><td>Danger (roter Text, transparenter BG)</td><td>Löscht den Slot, Kachel wird leer</td></tr>
</tbody>
</table>
<h3>Interaction Flow</h3>
<ul>
<li>Klick auf <code>.scene</code><code>.card.classList.toggle('flipped')</code></li>
<li>Alle Geschwister-Kacheln im Grid → <code>opacity: 0.38; pointer-events: none</code></li>
<li>× Button auf Rückseite → <code>event.stopPropagation()</code>, <code>classList.remove('flipped')</code>, Geschwister-Opacity zurücksetzen</li>
<li>Escape-Taste → aktive Kachel zurückdrehen</li>
</ul>
<div class="callout green">
<strong>Kein API-Aufruf beim Flip.</strong> Alle dargestellten Daten (Name, Zutaten, Aktionen) sind bereits
im vorhandenen <code>slotMap</code>-State vorhanden. Der Flip ist eine rein visuelle Operation.
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="section">
<div class="section-label">05 · Leere Kacheln</div>
<h2>Empty Tile — Inline Suggestions</h2>
<p>Leere Kacheln haben denselben <code>height: 100%</code> wie gefüllte Kacheln. Kein Flip.</p>
<pre>┌─────────────────┐
│ Sa 12 │ ← Tageskürzel + Datum
│─────────────────│
│ + │
│ Gericht wählen │ ← Klick öffnet Rezept-Picker-Drawer
│─────────────────│
│ VORSCHLÄGE │
│ Ramen mit Ei [Neues Protein] │
│ Shakshuka [Kein Overlap] │
│ Tacos [Aufwand: leicht]│
│ │
│ Alle Rezepte → │
└────────────────────────────────┘</pre>
<h3>Vorschlag-Tags (Reasoning)</h3>
<p>Anstelle numerischer Score-Deltas (die für leere Slots immer positiv sind und daher keine Information tragen)
werden Begründungs-Tags angezeigt:</p>
<table>
<thead><tr><th>Tag</th><th>Farbe</th><th>Bedeutung</th></tr></thead>
<tbody>
<tr><td>Neues Protein</td><td>Grün</td><td>Proteinquelle kommt diese Woche noch nicht vor</td></tr>
<tr><td>Kein Overlap</td><td>Grün</td><td>Keine Zutaten-Überschneidung mit anderen Tagen</td></tr>
<tr><td>Aufwand: leicht</td><td>Gelb</td><td>Kochzeit &lt; 30 Min oder Aufwand = einfach</td></tr>
<tr><td>Aufwand: mittel</td><td>Neutral</td><td>Mittlerer Aufwand</td></tr>
</tbody>
</table>
<div class="callout">
<strong>Datenquelle:</strong> Die vorhandene <code>GET /api/suggestions?weekId=&amp;dayOfWeek=</code> API liefert
<code>SuggestionItem { recipe, scoreDelta, hasConflict }</code>. Die Reasoning-Tags werden frontend-seitig
aus den Rezept-Tags und dem vorhandenen <code>slotMap</code> abgeleitet, kein Backend-Änderungsbedarf.
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="section">
<div class="section-label">06 · Rezept-Picker</div>
<h2>Recipe Picker Drawer</h2>
<p>
Der Rezept-Picker öffnet sich als Slide-in-Drawer von rechts — ausschließlich auf explizite Anfrage.
Er hat keinen persistenten Platz im Layout mehr.
</p>
<h3>Trigger</h3>
<ul>
<li>Klick auf <strong>„Gericht tauschen"</strong> auf der Kachel-Rückseite</li>
<li>Klick auf <strong>„Gericht wählen"</strong> CTA oder Vorschlag-Zeile auf einer leeren Kachel</li>
</ul>
<h3>Drawer-Verhalten</h3>
<ul>
<li>Slide-in von rechts, überlagert den Inhalt (kein Layout-Shift)</li>
<li>Breite: <code>min(480px, 90vw)</code></li>
<li>Backdrop (halbtransparent) schließt den Drawer bei Klick</li>
<li>Nach Auswahl: Drawer schließt sich, Slot wird aktualisiert, Kachel zeigt neues Rezept</li>
</ul>
<div class="callout">
Der bestehende <code>RecipePicker</code>-Komponente (aktuell im rechten Panel) wird in einen
generischen Drawer gewrappt. Der Drawer-Wrapper ist neu; der Picker selbst bleibt unverändert.
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="section">
<div class="section-label">07 · Mobile</div>
<h2>Mobile — Out of Scope</h2>
<p>
Dieses Spec betrifft ausschließlich die Desktop-Ansicht (<code>≥ 768px</code>).
Das mobile Layout (vertikaler Stack, DayMealCard, ActionSheet) bleibt unverändert.
CSS-3D-Flips auf Touch-Geräten haben bekannte Rendering-Unterschiede auf älteren Android-Browsern —
ein separates Issue sollte die mobile Interaktion (ggf. Slide-up Sheet statt Flip) spezifizieren.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="section">
<div class="section-label">08 · Komponenten</div>
<h2>Komponenten-Übersicht</h2>
<div class="component-row">
<span class="comp-file">src/routes/(app)/planner/+page.svelte</span>
<span class="badge badge-mod">Ändern</span>
<span class="comp-action">Rechtes Panel entfernen. Layout auf 2-spaltig (sidebar + main) umstellen. Toolbar-Button entfernen. Grid-Höhe auf 100% setzen.</span>
</div>
<div class="component-row">
<span class="comp-file">src/lib/planner/DayMealCard.svelte</span>
<span class="badge badge-mod">Ersetzen / umbenennen</span>
<span class="comp-action">Zur Flip-Kachel umbauen: .scene → .card → .card-front + .card-back. Farb-Klassen-Prop, Gradient-Overlay, Back-Face mit Aktionen.</span>
</div>
<div class="component-row">
<span class="comp-file">src/lib/planner/EmptyDayTile.svelte</span>
<span class="badge badge-new">Neu</span>
<span class="comp-action">Leere Kachel: + CTA + Inline-Suggestion-Liste mit Reasoning-Tags. Ersetzt den bisherigen leeren Slot-Platzhalter.</span>
</div>
<div class="component-row">
<span class="comp-file">src/lib/planner/RecipePickerDrawer.svelte</span>
<span class="badge badge-new">Neu</span>
<span class="comp-action">Drawer-Wrapper um den bestehenden RecipePicker. Slide-in von rechts, Backdrop, Schließ-Logik.</span>
</div>
<div class="component-row">
<span class="comp-file">src/lib/planner/RecipePicker.svelte</span>
<span class="badge badge-mod">Ändern</span>
<span class="comp-action">Aus dem rechten Panel lösen. Bekommt slotId als Prop. Keine Änderung an der Such-/Auswahl-Logik nötig.</span>
</div>
<div class="component-row">
<span class="comp-file">src/app.css</span>
<span class="badge badge-mod">Ergänzen</span>
<span class="comp-action">14 Farb-Klassen für Protein- und Küchenstil-Gradients hinzufügen (<code>.protein-haehnchen</code>, <code>.cuisine-asiatisch</code>, …).</span>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="section">
<div class="section-label">09 · Accessibility</div>
<h2>A11y-Anforderungen</h2>
<ul>
<li><code>.scene</code>: <code>role="button"</code>, <code>tabindex="0"</code>, <code>aria-expanded="false|true"</code>, <code>aria-label="[Rezeptname] — Details anzeigen"</code></li>
<li><code>.card-back</code>: <code>aria-hidden="true"</code> solange nicht geflippt</li>
<li>× Schließen-Button: <code>aria-label="Schließen"</code>, <code>type="button"</code></li>
<li>Keyboard: <code>Enter</code> / <code>Space</code> flippt, <code>Escape</code> dreht zurück</li>
<li>Dimming: gedimmte Kacheln bekommen <code>aria-hidden="true"</code> wenn eine andere geflippt ist</li>
</ul>
</div>
</div>
</body>
</html>

1606
specs/userjourneys.html Normal file

File diff suppressed because it is too large Load Diff