Adds beforeEach(vi.clearAllMocks) to prevent shared vi.fn() state in
baseProps from leaking across tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds regression test for the {#if slot.id} guard on the remove button —
QA flagged the missing negative test case for optimistic slots.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- border-radius: 10px → var(--radius-lg) in both tile components
- opacity: 0.42 → var(--opacity-dimmed) in DesktopDayTile
- var(--yellow) → var(--color-ring-today) for today ring and date circle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces (t: any) with (t: TagItem) so the API response shape is
validated against the shared TagItem interface.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts sanitizeForCssUrl helper that strips '"()\ before the URL
is embedded in url("..."). Prevents CSS injection via the hero image
field in inline style bindings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes local TagItem, Recipe, SlotRecipe, Slot, SlotMap definitions
and imports Recipe, Slot, SlotMap from types.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes local TagItem, SuggestionRecipe, TopSuggestion, Slot interfaces
and imports Suggestion, SlotMap from types.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes local TagItem, SlotRecipe, Slot, Suggestion interfaces and
imports Recipe, Slot, Suggestion from types.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds shared Slot and SlotMap interfaces so DesktopDayTile,
EmptyDayTile, and reasoningTags can import rather than re-declare.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
transform-style:preserve-3d on a parent with box-shadow/transition
causes Chrome to fail backface-visibility:hidden. Replace with
independent per-face rotateY transforms:
front: 0deg → -180deg (flipped)
back: 180deg → 0deg (flipped)
No preserve-3d needed — each face is its own compositing layer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
overflow:hidden on direct children of preserve-3d flattens the 3D
context in Chrome, causing backface-visibility:hidden to fail.
Move border-radius + overflow to inner wrapper divs (.card-front-inner,
.card-back-inner) and keep the face elements themselves free of those
properties. Also add -webkit-backface-visibility:hidden and
will-change:transform for consistent GPU compositing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backface-visibility hides elements visually but not to pointer events;
disable pointer events on the hidden face explicitly so the X button
on the back face is clickable and the front face doesn't intercept clicks
- Add .scene-selected:hover rule so green ring is not overwritten by the
higher-specificity .scene:hover box-shadow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Front face:
- Full dual gradient overlay (dark top 32% → transparent → dark bottom 55%)
- Day abbreviation + date number pill at top of each tile
- Recipe name 13px/weight-300 with text-shadow
- Meta line (cookTimeMin · effort) below name
- Glassmorphism tag pills (protein + cuisine only)
- State rings via box-shadow (yellow for today, green for selected)
- Dimming (opacity 0.42) on non-selected filled tiles
Back face:
- Koch-Modus as green primary button
- Entfernen as red outline (transparent bg)
- All buttons 11px / weight 500
EmptyDayTile: add day header + spec-aligned suggestion list layout
Page: remove external column header (now rendered inside each tile)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SlotRecipe from the week-plan API carries no tags, so the protein
gradient lookup in DesktopDayTile always fell through to --color-surface.
Build a recipeById lookup from data.recipes and spread tags onto each
slot's recipe when constructing slotMap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename --gradient-protein-ei → --gradient-protein-eier (tag is 'Eier')
- Add --gradient-protein-kaese for tag 'Käse' (was missing entirely)
The only protein tags in seed data are Käse, Hülsenfrüchte, Eier.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The CSS variable key must match the actual tag name after umlaut
transliteration. 'veg' would never match a real tag named 'vegetarisch'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'Hähnchen'.toLowerCase() → 'hähnchen' which never matched the CSS var
--gradient-protein-haehnchen. Add toCssKey() to replace ä→ae, ö→oe,
ü→ue, ß→ss so gradient fallbacks actually resolve.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add dark gradient scrim on card front so recipe name is always readable
over images and protein/cuisine gradients
- Style card-back actions as proper buttons (border, padding, border-radius)
instead of unstyled browser defaults
- Add meta chips for cookTimeMin and effort
- Scope Entfernen inside isPlanner guard alongside Gericht tauschen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces 3-panel layout with 2-panel (sidebar + full-width grid):
- Remove persistent right panel and toolbar + Gericht hinzufügen button
- grid-cols-7 tiles use DesktopDayTile (CSS 3D card flip)
- RecipePickerDrawer slides in on tile CTA / Gericht tauschen
- Page-owned activeSlotId + drawerOpen/drawerSlotId state
- Single Escape handler: drawer > flip priority
- Extend server load to forward recipe tags from /v1/recipes API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CSS 3D card flip with scene/card/front/back structure. Filled slots
show gradient/image front face and action back face (Koch-Modus,
tauschen, entfernen). Empty slots delegate to EmptyDayTile.
Sibling dimming and aria-expanded via activeSlotId prop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Derives ReasoningTag[] from slotMap + recipe. Covers Neues Protein
(protein not yet in week) and Aufwand: leicht (cookTimeMin < 30 or
effort einfach/leicht). No component dependency — Vitest-testable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds --color-ring-today, --color-ring-selected, --opacity-dimmed,
9 protein gradient tokens and 5 cuisine gradient tokens as @theme
custom properties, integrating into the existing token layer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Was only red on hover — now always red at 60% opacity, full opacity on hover,
making the destructive intent immediately visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents a silent 400 from the backend when the user submits a form
where every ingredient row has quantity <= 0 or blank name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rejects non-allowlisted types (only JPEG, PNG, GIF, WebP accepted) with
an inline error message. Uses image/bmp as test vector since it passes
accept="image/*" but is not in the allowed set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Thumbnailator-based ImageCompressor that resizes uploaded images
to a 400px-wide JPEG preview stored in hero_image_preview. The recipe
list uses the preview instead of the full image URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Store hero image as base64 data URI in text column (V023 migration)
- Add file upload UI to RecipeForm with FileReader preview
- Remove isChildFriendly from RecipeCreateRequest (no form field)
- Fix 500 on save: effort values now lowercase, serves/cookTimeMin changed
from primitive short to nullable Integer to survive omitted fields
- Fix empty categories panel: removed stale tagType=category filter
- Group category tags by type with German headings in recipe form
- Split SuggestionResponse.SuggestionRecipe (no image) from SlotRecipe
- Seed 11 HelloFresh recipes with ingredients, steps and tags (V101)
- Add frontend e2e scaffold, specs and dev yml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- +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>
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>
Avoids floating-point display like 6.199999999999999 by using
score.toFixed(1) in VarietyScoreCard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>