border-radius: 999px war 15x im CSS dupliziert. Ausgelagert als
:root --pill-radius Variable im globalen :root-Block in +layout.svelte,
Call-Sites auf var(--pill-radius) umgestellt.
Bewusst NICHT angefasst (plan war "nur Werte die mehrfach vorkommen"):
- z-index: 10 Distinct Values in 14 Sites, bilden ein implizites
Layer-System. Konsolidieren = behavior-change-Risiko ohne konkreten
Nutzen. Wenn kuenftig einheitliche Modal-/Popover-Layer noetig,
separate Phase.
- setTimeout(): 3 Sites, jeder mit eigener Semantik (Debounce/Print/
Spinner). Kein DRY-Nutzen durch Extraktion.
Gate: svelte-check 0 Warnings, 184/184 Tests, Build clean, kein
sichtbarer Unterschied (einzige Aenderung: selber Wert ueber Variable).
Refs docs/superpowers/plans/2026-04-19-post-review-roadmap.md Item F.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RecipeEditor war noch die letzte Stelle im UI, die das
handgeschriebene "if (!res.ok) { alertAction(...) }"-Pattern
benutzte, welches wir in review-fixes-2026-04-18 ueberall sonst
durch asyncFetch() ersetzt hatten.
Netto: -14 Zeilen, konsistenter Fehlermessage-Fallback (body.message
> res.status), eine Import-Zeile weniger (alertAction raus, asyncFetch
rein).
Gate: svelte-check clean, 184/184 Tests, Upload/Delete-Flow per
Hand zu testen beim naechsten Editor-Touch.
Refs docs/superpowers/plans/2026-04-19-post-review-roadmap.md Item H.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Alle 10 pre-existing svelte-check WARNINGs ("state_referenced_locally")
in RecipeEditor.svelte und recipes/[id]/+page.svelte addressiert.
Die betroffenen `let foo = $state(recipe.bar)`-Pattern sind
intentional Snapshots: der Editor soll User-Edits behalten und nicht
von prop-Updates ueberschrieben werden. untrack() macht die Intent
explizit und silenced die Warnung sauber statt sie unter den Teppich
zu kehren.
Scope: imagePath, title, description, servings, prepMin, cookMin,
totalMin, ingredients, steps (RecipeEditor) + recipeState
(recipes/[id]/+page).
Gate: svelte-check 0 Warnings (war 10), Tests 184/184.
Refs docs/superpowers/plans/2026-04-19-post-review-roadmap.md Item I.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Neuer Endpoint POST/DELETE /api/recipes/:id/image.
* Multipart-Upload mit Feld "file".
* Whitelist: JPG, PNG, WebP, GIF, AVIF. Max 10 MB.
* Dedupe per SHA-256-Filename analog zu downloadImage().
- updateImagePath()-Repo-Funktion ergänzt.
- RecipeEditor: neuer Block "Bild" ganz oben. Preview + Buttons
"Hochladen"/"Ersetzen"/"Entfernen". Upload passiert direkt beim
Auswählen, nicht erst bei "Speichern" — das Bild ist eigene
Ressource, Abbrechen rollt es nicht zurück (okay, da dedupliziert).
- onimagechange-Callback informiert die Detail-Ansicht, damit die
Preview im RecipeView auch nach Abbrechen aktuell bleibt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Dekorativer GripVertical raus, stattdessen zwei Pfeil-Buttons (↑/↓)
pro Zeile. An erster/letzter Stelle sind die Buttons disabled.
- moveIngredient() vertauscht Zeile mit Nachbarn; simpel und
tastatur-/touch-freundlich ohne Drag-and-Drop-Abhängigkeit.
- Grid-Spalten von 1fr 90px (Zutat/Notiz) auf 1fr 1fr — beide Felder
sind jetzt gleich breit, wie im Family-Feedback gewünscht.
- Mobile-Layout behält gestaffelte Note-Zeile, Move-Spalte rutscht
als eigene Spalte links daneben.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auf dem 10"-Tablet war die Schrift in Zutaten und Zubereitung zu klein.
Im 2-Spalten-Layout (>=820px) bumpen wir jetzt:
- Zutaten-Zeilen und Step-Text auf 1.2rem (vorher 1rem)
- qty-Spalte breiter (6rem statt 5rem)
- Portionen-Zahl größer
- Step-Badge auf 2.4rem + 1.1rem Font
Mobile bleibt unverändert — Lesedistanz ist dort anders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bottom-right Pill zeigt Sync-Fortschritt (Sync N/M) oder Offline-
Status. Klick öffnet Overlay mit "Zuletzt synchronisiert: vor
N Min" + manuellem Refresh-Button (postMessage type=sync-check an
den SW). prefers-reduced-motion noch nicht gehandhabt — Spin-Icon
dreht sich bewusst; kein UX-Schaden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Toast-Queue als $state-Store mit Auto-Dismiss nach 3 s und manuellem
dismiss(id). Drei Kinds: info/error/success (Farbe). Renderer als
<Toast /> im Root-Layout, fix-positioniert oben mittig. Wird
vom Offline-Check der Schreib-Aktionen genutzt und später auch für
Sync-Abschluss-Meldungen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Die Mobile-Only-Regel führte auf Desktop zu riesigen 16:10-Covers
(auf 1440px-Breite fast 900px hoch). Jetzt gilt der 30vh-Deckel
überall — Bild bleibt proportional, Zutaten/Zubereitung bleiben
sichtbar ohne Scroll.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Das 16:10-Cover-Bild konnte auf Smartphones im Hochformat locker 40-50%
des Viewports füllen und Aktionen wie Favorit/Wunschliste fast aus dem
ersten Screen drücken. Auf <820px jetzt max-height:30vh — object-fit:
cover sorgt dafür, dass das Bild schön im gekürzten Rahmen sitzt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auf der Rezept-Detail-Seite ein neuer „Bearbeiten"-Button (Pencil-Icon)
in der Action-Bar. Klick schaltet RecipeView auf RecipeEditor um.
Im Editor:
- Titel, Beschreibung, Portionen, Vorbereitungs-/Koch-/Gesamtzeit als
inline-Inputs.
- Zutaten: pro Zeile Menge, Einheit, Name, Notiz + Trash-Icon zum
Entfernen. „+ Zutat hinzufügen"-Dashed-Button am Listenende.
- Schritte: nummerierte Textareas, Trash-Icon, „+ Schritt hinzufügen".
- Mengen akzeptieren Komma- oder Punkt-Dezimalen.
- Empty-Items werden beim Speichern automatisch aussortiert.
Backend:
- Neue Repo-Funktionen updateRecipeMeta(id, patch), replaceIngredients,
replaceSteps — letztere in einer Transaction mit delete+insert und
FTS-Refresh.
- PATCH /api/recipes/[id] akzeptiert jetzt zusätzlich description,
servings_default, servings_unit, prep_time_min, cook_time_min,
total_time_min, cuisine, category, ingredients[], steps[]. Vorher
nur title/hidden_from_recent; diese beiden bleiben als
Kurz-Fall erhalten, damit bestehende Aufrufer unverändert laufen.
- Zod-Schema mit expliziten Grenzen (max-Länge, positive Mengen).
Tests: 3 neue Cases für updateRecipeMeta, replaceIngredients (inkl.
FTS-Update), replaceSteps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auf Viewports >=820px (Tablets im Querformat, Desktop) werden Zutaten
und Zubereitung nebeneinander angezeigt statt per Tab gewechselt. Der
Nutzer sieht beides gleichzeitig und nutzt die volle Display-Breite.
- Tabs bleiben für <820px (Smartphones + Tablet-Hochkant), dort schalten
sie weiterhin zwischen den Panels um.
- Ab 820px: Tabs versteckt, Grid minmax(260px,1fr) 1.6fr. Zutaten links
sticky top:1rem mit max-height 100vh-2rem, damit die Liste beim
Scrollen der Zubereitung sichtbar bleibt.
- Main-Container max-width 760px → 1040px erhöht, damit auf großen
Screens überhaupt Platz für zwei Spalten bleibt. Andere Listen-
Ansichten (Cards, Index) nutzen den Zugewinn automatisch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Klick-außerhalb-Handler fing gelegentlich Events von abgewählten
Checkbox-Zeilen ab, weil deren Layout beim Re-Render kurz verschob und
event.target außerhalb des Containers landete — das Menu schloss sich
dann mitten im Filtern. Handler komplett entfernt; Escape und die
expliziten Footer-Buttons bleiben die einzigen Wege zum Schließen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der .wrap-Div zwischen .search-box und .trigger war display:block, was
align-self:stretch am Trigger komplett neutralisiert hat — Button war
nur 16px hoch in einem 52px-Container. Playwright hat das klar gezeigt.
- .wrap:has(.trigger.inline) wird jetzt zu display:flex, damit der
Trigger darin überhaupt ein Flex-Item ist.
- .trigger.inline bekommt height:100% statt sich auf align-self zu
verlassen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Trigger nimmt volle Höhe des .search-box-Containers ein
(align-self: stretch, min-height: 0), damit der Hover-Hintergrund
bündig ausgefüllt wird statt nur innerhalb eines 44px-Rechtecks.
- Footer: justify-content: space-between — Abbrechen sitzt jetzt links,
OK rechts (übliche Platform-Konvention).
- Quick-Actions: zusätzlicher „Keine"-Button neben „Alle", beide setzen
den Draft-State ohne sofortigen Commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der „2/5"-Badge neben dem Slider-Icon war visuell laut und wiederholte
nur, was der User im Dropdown direkt sieht. Raus damit — Trigger zeigt
jetzt nur noch das Icon und den Chevron.
Ungenutzten label-Getter aus dem Store auch entfernt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Filter-Dropdown sammelt Checkbox-Klicks jetzt nur noch lokal und
wendet sie erst beim „OK"-Klick auf den Store an. Solange der User
herumklickt, läuft die aktive Suche unverändert weiter. Abbrechen (per
Button, Klick außerhalb oder Escape) verwirft die Draft-Auswahl.
- Neuer searchFilterStore.commit(Set) für One-Shot-Apply (triggert den
active-$effect nur ein einziges Mal).
- „Alle"-Quick-Action setzt draft = alle Domains explizit; erst beim
Commit wird das wieder in die leere Menge überführt, damit neu
freigeschaltete Admin-Domains weiterhin automatisch dabei sind.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Domain-Admin-Seite bekommt jetzt ein Favicon-Icon vor jedem Eintrag,
einen Pencil-Button zum Inline-Editieren von Domain und Anzeigename,
und Save/Cancel-Buttons. Beim Ändern des Domain-Namens wird das Favicon
zurückgesetzt und beim Speichern frisch nachgeladen (den Filter-Dropdown-
Icons reicht der neue favicon_path automatisch zu).
Der Filter-Button auf der Hauptseite sitzt jetzt IM weißen Suchfeld-
Container (neuer .search-box-Wrapper mit Border) statt daneben, analog
zum Referenz-Screenshot von rezeptwelt.de. Neue inline-Prop an
SearchFilter schaltet eigenen Border/Background ab und setzt stattdessen
einen vertikalen Divider nach rechts.
- Neuer PATCH /api/domains/[id] mit zod-Schema.
- Repository: updateDomain(id, patch) + getDomainById(id).
domain-Change nullt favicon_path → Caller lädt neu.
- Tests für updateDomain-Fälle und getDomainById.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Für jede Whitelist-Domain wird das Favicon jetzt einmalig geladen und
im image-Verzeichnis abgelegt. SearchFilter zeigt das Icon neben dem
Domain-Namen im Filter-Dropdown.
- Migration 009: allowed_domain.favicon_path (NULL = noch nicht geladen).
- Neues Modul $lib/server/domains/favicons.ts:
fetchAndStoreFavicon(domain, imageDir) + ensureFavicons(db, imageDir)
für Bulk-Nachzug; 8 parallele Worker mit 3s-Timeout.
- Reihenfolge: erst /favicon.ico der Domain, Fallback Google-Service.
- GET /api/domains zieht fehlende Favicons auf Abruf nach;
POST /api/domains lädt direkt im selben Call.
- .ico + .svg jetzt in der /images/[filename]-Route erlaubt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Links im großen Suchfeld ein Slider-Icon mit Badge („Alle" oder „2/5"),
das ein Dropdown-Menü mit allen Whitelist-Domains als Checkboxen öffnet.
Auswahl wird per localStorage persistiert und gilt global — Header-Such-
Dropdown konsumiert den gleichen Store und sendet den domains-Parameter
bei jedem Fetch mit.
Leere Menge heißt „alle aktiv", damit neu vom Admin freigeschaltete
Domains automatisch dabei sind. Aktive Auswahl landet als explizite
Intersection mit der Whitelist serverseitig.
- searchLocal nimmt jetzt optional string[] domains → `source_domain IN (…)`.
- searchWeb nimmt jetzt opts.domains → site:-Filter auf die Auswahl
eingeschränkt. Nicht-Whitelist-Einträge werden ignoriert.
- API-Endpoints: `?domains=a.de,b.de`.
- Neuer Client-Store $lib/client/search-filter.svelte.ts.
- Neue Komponente $lib/components/SearchFilter.svelte (mobile-tauglich,
44px Touch-Targets, Badge auf engen Screens versteckt).
Home-Seite re-runt die Suche bei Filter-Änderung automatisch (150ms debounce),
ohne dass der User neu tippen muss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pwaStore ($lib/client/pwa.svelte.ts):
- Hängt sich an navigator.serviceWorker.ready, hört auf updatefound und
setzt updateAvailable = true, sobald ein neuer SW im Status 'installed'
ist UND es einen aktiven controller gibt (= Update eines bestehenden
Tabs, nicht die erste Installation).
- Polling alle 30 Minuten via registration.update(), damit der User den
Toast auch sieht, wenn er die Seite lange offen hat ohne zu navigieren.
- reload() ruft location.reload(); dismiss() schließt den Toast nur.
UpdateToast.svelte:
- Schwarzer Pill-Toast unten zentriert, mit Text, grünem "Neu laden"-
Button (RefreshCw-Icon) und X zum Wegklicken.
- Slide-Up-Animation beim Erscheinen.
- Responsive: auf Mobile (<420px) wird's zum vollbreiten Banner statt
Pill.
Root-Layout mountet <UpdateToast /> direkt neben <ConfirmDialog />.
onMount ruft pwaStore.init().
Status-Check der Live-Instanz https://kochwas.siegeln.net:
- manifest.webmanifest wird korrekt als JSON ausgeliefert
- service-worker.js (3.4 KB) ist verfügbar
- iOS Apple-Meta-Tags + Android theme-color sind im HTML <head>
PWA selbst funktioniert also bereits; der Toast war das fehlende Teil
für transparente User-seitige Updates.
Profil-Modal:
- Default-Emoji '🍳' im "Neues Profil"-Input entfernt (war ein ver-
sehentlicher Platzhalter, den die meisten nicht überschrieben haben
→ alle Profile sahen gleich aus). Jetzt leer, mit 🙂 als Hint im
placeholder.
- Profil-Liste: avatar_emoji wird nur gezeigt, wenn wirklich gesetzt.
Sonst CircleUser-Lucide statt 🙂-Fallback.
Migration 006_clear_default_profile_emoji.sql räumt bestehende DB-
Einträge auf: UPDATE profile SET avatar_emoji = NULL WHERE avatar_emoji
= '🍳'. User, die wirklich einen Pfannen-Avatar wollten, können das in
/admin/profiles neu setzen.
Wunschliste-Header-Icon: Heart → CookingPot. Der Kontext ist "was wir
essen wollen", also passt ein Topf besser als ein Herz. Heart bleibt
im Rezept als "Favorit" und in der Wunschliste als "ich will auch"-
Toggle, keine Kollision.
Ungenutzten Heart-Import aus +layout.svelte entfernt.
1) ProfileSwitcher-Chip: Profil-Emoji (avatar_emoji) ist jetzt aus dem
Header-Badge raus — immer CircleUser-Icon vor dem Namen. Im Profil-
Auswahl-Modal bleiben die individuellen Emojis erhalten, damit User
ihr Profil dort weiterhin erkennen. Unused .emoji CSS entfernt.
2) Preview-Button: "In meine Sammlung speichern" → "Rezept in Kochwas
speichern". Klarer, was die App heißt.
3) Wake-Lock-Schalter:
- Erklärung: navigator.wakeLock.request('screen') hindert Android/iOS
daran, das Display zu dimmen/zu sperren, solange der Tab sichtbar
ist. Beim Kochen sehr nützlich — Hände sind klebrig.
- Neuer Toggle-Button im Rezept-Detail, zweite Aktion-Zeile zwischen
"Heute gekocht" und "Löschen": Lightbulb (an, gelb-gehighlighted)
oder LightbulbOff (aus).
- Preference wird in localStorage persistiert (kochwas.wakeLock),
Default an. Gilt für alle Rezepte.
- visibilitychange-Handler re-requestet den Sentinel, wenn User den
Tab wieder nach vorne holt und die Pref an ist.
- release-Event räumt wakeLock-Variable sauber auf.
Homepage:
- Neue Sektion "Deine Favoriten" über "Zuletzt hinzugefügt" (alphabetisch
sortiert, lädt wenn Profil aktiv ist; versteckt sonst)
- Jede Karte in "Zuletzt hinzugefügt" hat jetzt oben-rechts ein X-Icon
zum Ausblenden. Das Rezept selbst bleibt in der DB — nur die
Anzeige in der Recent-Liste wird per recipe.hidden_from_recent = 1
unterdrückt. Section versteckt sich, wenn die Liste leer wird.
DB:
- Neue Migration 004_recipe_hidden_from_recent.sql (+Index)
- listFavoritesForProfile in search-local.ts (ORDER BY title NOCASE)
- setRecipeHiddenFromRecent in actions.ts
API:
- GET /api/recipes/favorites?profile_id=X
- PATCH /api/recipes/[id] akzeptiert jetzt title und/oder
hidden_from_recent (Zod-Schema mit refine)
Rezept-Detail:
- Titel ist jetzt inline editierbar: kleines Stift-Icon rechts neben
H1. Click öffnet Input, Enter speichert (PATCH), Escape bricht ab.
Kein location.reload() mehr.
- RecipeView bekommt neuen Snippet-Prop titleSlot für Title-Override.
- Neue Aktionsreihenfolge:
Zeile 1: Favorit | Wunschliste | Drucken
Zeile 2: Heute gekocht | Löschen
(Umbenennen ist jetzt am Titel statt in der Leiste.)
Icons (lucide-svelte, neues Dep):
- Emoji-Icons durch Lucide-SVGs ersetzt auf Startseite, Header,
Rezept-Detail, Wunschliste, Header-Dropdown:
🍽️→Heart/Utensils, ⚙️→Settings, 🥘→CookingPot, 🌐→Globe,
♥/♡→Heart(filled), 🖨→Printer, ✎→Pencil, 🗑→Trash2, ✓→Check,
🍳→ChefHat, X→X
- Header-Brand-Badge auf Mobile behält sein 🍳 (ist im ::after-Pseudo,
Lucide käme da nicht sauber rein).
- SearchLoader-Emojis bleiben — die sind Teil der Animations-Charme.
Tests: 99/99 grün (bestehend), Typecheck 0 Fehler.
Neue Komponente src/lib/components/SearchLoader.svelte ersetzt die
stumpfen "Suche läuft …"-Zeilen an allen vier Stellen:
- Startseite (scope=local und scope=web)
- Header-Dropdown (size=sm, beide Scopes)
Was passiert:
- Ein Pfannen-Emoji (🍳 → 🥘 → 🍲 → 🍜 → 🥣) wechselt alle 900 ms
- Wobble-Animation kippt es im 1.4-s-Takt hin und her
- Drei Dampf-Punkte steigen zeitversetzt auf und fadeen
- Caption unten rotiert alle 1.8 s durch vier passende Sprüche
(lokal: "Stöbere im Rezeptbuch …", web: "Schnuppere in fremden
Küchen …" etc.)
Zwei Size-Varianten: md (Homepage) und sm (Header-Dropdown).
alertAction({title, message}) returns Promise<void> and renders the
same ConfirmDialog with infoOnly:true — single OK button, no Abbrechen.
Replaces:
- 'Bitte Profil wählen.' (recipe rating / favorite / cooked / comment)
- 'Bitte Profil wählen, um zu liken.' (wishlist)
- 'Profil konnte nicht angelegt werden' (ProfileSwitcher)
- 'Umbenennen fehlgeschlagen' (admin/profiles)
- 'Speichern fehlgeschlagen' (preview)
No window.alert() or window.confirm() left in the codebase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single reusable dialog with a promise-based API: confirmAction({...})
returns Promise<boolean>. Supports title, optional message body,
confirm/cancel labels, and a 'destructive' flag that paints the confirm
button red.
Accessibility: Escape cancels, Enter confirms, confirm button auto-focus,
role=dialog + aria-labelledby, backdrop click = cancel.
Rolled out to: recipe delete, domain remove, profile delete, wishlist
remove. Native confirm() is gone from the codebase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In the preview flow, recipe.image_path is still the external recipe-page
URL (we only download the image when persisting). The RecipeView
component always prefixed with /images/ and produced a 404.
Now RecipeView detects http(s):// prefix and uses it directly, else
treats the value as a local filename under /images/.
Also adds referrerpolicy=no-referrer on the preview image so remote
CDNs don't block us based on Origin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RecipeView needs scaleIngredients on the client for live portion scaling.
Moved scaler.ts from $lib/server/recipes/ to $lib/recipes/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>