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>
Neuer vierter Admin-Tab (Smartphone-Icon) mit drei Karten:
1. Installieren — fängt beforeinstallprompt (Android), zeigt
iOS-Teilen-Hinweis, sonst Info "nicht verfügbar".
2. Offline-Synchronisation — Status + "Jetzt synchronisieren"-
Button, disabled wenn offline.
3. Cache — "Offline-Cache leeren" löscht alle kochwas-*-Caches
via caches.keys() + delete.
install-prompt.svelte.ts hält das deferred-Event und die Plattform
(android/ios/other) per UA-Detection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/service-worker.ts installiert die App-Shell-Assets (build +
files aus $service-worker) beim install-Event in kochwas-shell-
<version>, räumt alte Shell-Caches beim activate und dispatcht
jeden Fetch via resolveStrategy — shell/images cache-first, swr
stale-while-revalidate, network-only unangetastet. Pre-Cache-
Orchestrator kommt in Task 9.
Client-seitig registriert sw-register.ts den SW und verdrahtet
Messages vom SW in den sync-status-Store.
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>
Der neu eingefügte Admin-Pill war zu prominent für seinen Zweck. Raus
damit. Stattdessen zeigt der Haupt-Header auf allen Nicht-Hauptseiten
links einen ArrowLeft-Icon-Link zur Startseite — platzsparend und
konsistent über alle Sub-Seiten (Admin, Rezept, Preview, Wunschliste…).
Auf der Startseite selbst bleibt das „Kochwas"-Wortmarke stehen.
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>
Die Header-Suche war Pill-förmig mit grünlichem Hintergrund, die Home-
Suche ein abgerundetes Rechteck mit weißem Hintergrund. Jetzt beide:
white background, border-radius: 12px, gleicher Border, gleicher Fokus-
Outline. Nur die Höhe bleibt unterschiedlich (40px Header vs. 52px
Home), damit der Header kompakt bleibt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der SearchFilter-Store galt schon global, aber das UI zum Ändern gab es
nur auf der Home-Seite — auf Rezept-/Preview-Seiten konnte man den
Filter nicht sehen, geschweige denn setzen. Jetzt zeigt die Header-Suche
denselben inline-Trigger links vom Input. Nav-Form bekommt Border +
Background als Container, Input wird transparent, Fokus-Outline landet
auf dem Container (wie auf der Home-Seite).
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>
Vorher war der „+ weitere"-Link im Header-Dropdown ein Navigations-Link
auf /?q=. Jetzt blättert der Button stattdessen im offenen Dropdown
direkt nach — erst weitere lokale Treffer, dann (wenn lokal erschöpft)
SearXNG-Seiten. Lokale und Web-Treffer werden beide im Dropdown
angezeigt, getrennt durch „Aus dem Internet"-Zwischenüberschrift.
Identische Logik wie auf der Home-Seite, nur im Dropdown-Scope. Dedup
per ID (lokal) bzw. URL (web) gegen SearXNG-Doppler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Menü-Button ist ein <button>, nicht <a>, und hat deshalb nicht
vom globalen Link-Color geerbt. Farbe jetzt explizit auf #2b6a3d
gesetzt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Suchfeld-Dropdown auf Rezept-/Preview-Seiten hatte nur bei lokalen
Treffern einen Fuß-Link. Bei reinem Web-Ergebnis fehlte die Weiterführung.
Jetzt steht „+ weitere Ergebnisse" unter jeder Trefferliste und
navigiert auf /?q=, wo die Hauptseite inline weiter paginieren kann.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Die separaten /search und /search/web Routen sind weg. Auf der Hauptseite
gibt es jetzt einen einzigen „+ weitere Ergebnisse"-Button am Ende der
Trefferliste, der erst weitere lokale Treffer lädt und — sobald die
erschöpft sind — auf die SearXNG-Web-Suche umschaltet und dort Seite für
Seite weiterblättert. Web-Treffer werden unter die lokalen angehängt,
getrennt durch eine „Aus dem Internet"-Zwischenüberschrift.
Alte Layout-Links auf /search bzw. /search/web zeigen jetzt auf /?q=.
Der Snapshot der Suche merkt sich auch Paginations-Zustand, damit
Rücknavigation vom Rezept/Preview die volle Liste wiederherstellt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ersetzt das Settings-Zahnrad im Header durch ein Dreistriche-Menü. Das
Menü enthält zwei Punkte: „Register" führt zu einer neuen /recipes-Route
mit allen Rezepten alphabetisch gruppiert (A-Z-Buchstabenchips zum
Scrollen, Live-Filter oben, Umlaut-normalisiert). „Einstellungen" zeigt
wie bisher /admin.
Auf Mobile <520px wird das App-Icon komplett ausgeblendet, damit die
Suchleiste mehr Platz bekommt.
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) Trash-Button auf Wunschliste wieder da. Im Gegensatz zum Heart
entfernt er den Eintrag NICHT nur für das aktive Profil, sondern
löscht alle Memberships auf diesem Rezept. Bestätigungsdialog macht
das explizit ("wird für alle Profile aus der Wunschliste gestrichen").
- repository.ts: neue Funktion removeFromWishlistForAll(recipeId)
- DELETE /api/wishlist/:id?all=true → family-wide
DELETE /api/wishlist/:id?profile_id=X → nur mein Eintrag
- UI: zwei Action-Buttons untereinander (Heart, Trash)
2) wishlistStore.refresh() läuft jetzt in afterNavigate des Root-Layouts.
Vorher wurde der Badge nur aktualisiert, wenn derselbe Tab die Aktion
ausgelöst hat. Wenn ein anderer Tab / anderes Gerät etwas ändert,
bleibt der Badge stale bis zum nächsten Full-Reload. Mit afterNavigate
reicht eine Client-Navigation, um ihn zu aktualisieren — was deutlich
näher an dem liegt, was der User erwartet.
Schema-Änderung (Migration 005):
- Tabelle wishlist umgestellt auf PK (recipe_id, profile_id)
- wishlist_like-Tabelle zusammengelegt — Liken WAR schon "will ich auch",
also werden alle bestehenden Likes Memberships auf der neuen Tabelle.
- Alt-Einträge mit added_by_profile_id werden migriert, anonyme gehen
verloren (war inkonsistent, jetzt erzwingen wir profile_id NOT NULL).
Repository:
- listWishlist aggregiert pro Rezept: wanted_by_count, wanted_by_names
(kommagetrennt), on_my_wishlist für das aktive Profil
- listWishlistProfileIds(recipeId) für den Recipe-Page-Loader
- countWishlistRecipes für das Header-Badge (DISTINCT recipe_id)
- addToWishlist/removeFromWishlist/isOnMyWishlist alle mit profile_id
als Pflicht
API:
- POST /api/wishlist: profile_id jetzt Pflicht (nullable raus)
- DELETE /api/wishlist/[recipe_id]?profile_id=X (nur eigenes Entry)
- /api/wishlist/[recipe_id]/like komplett entfernt (Konzept obsolet)
- Neu: GET /api/wishlist/count → { count: <distinct recipes> }
UI:
- Header-Heart bekommt rotes Badge mit Zahl der Wunschliste-Rezepte.
wishlistStore in $lib/client/wishlist.svelte.ts hält den Count reaktiv;
Refresh auf Mount, nach Add/Remove, beim Öffnen der Wunschliste.
- Recipe-Detail: Loader liefert wishlist_profile_ids; onMyWishlist ist
ein $derived. Toggle fragt aktives Profil (alertAction sonst), mutiert
die lokale Liste + ruft wishlistStore.refresh.
- Wunschliste-Seite: Heart toggelt eigenen Wunsch, Count zeigt Gesamt-
wünsche, kommagetrennte Namen zeigen "wer will". Trash-Button
entfernt — Heart-off reicht jetzt.
Tests (99 → 99, 8 neu geschrieben):
- Per-User-Add/Remove, aggregierte Counts, on_my_wishlist, Cascades bei
Recipe/Profile-Delete, countWishlistRecipes = DISTINCT.
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).
Home:
- "Suchen"-Button entfernt. Die Suche feuert ohnehin debounced beim
Tippen; der Button war ein Relikt aus dem Submit-Modell. Enter auf
dem Input löst weiterhin einen Submit aus (geht zur /search-Seite).
Header (< 520 px):
- Sobald das Suchfeld fokussiert wird, wandert das nav-search-wrap
via :focus-within auf position: absolute und dehnt sich bis zum
rechten Rand (1 rem Abstand) aus. Die Action-Icons werden dabei
vom Suchfeld überlagert (z-index 60), sodass der Anwender auf
engen Displays deutlich mehr Platz zum Tippen hat.
- Bar-Inner bekam position: relative, damit das absolute Ausdehnen
innerhalb der Header-Zeile greift.
Homepage (/):
- Keine lokalen Treffer → automatisch die Internet-Suche auslösen und
die Ergebnisse als Karten unterhalb der Suche anzeigen.
- Mindestens ein lokaler Treffer → Karten zeigen + darunter ein
dezenter Link "🌐 Im Internet weitersuchen" (geht zur /search/web
Vollseite), keine automatische Internet-Suche.
Header-Dropdown (auf Rezept- und Vorschau-Seiten):
- Gleiche Logik: lokale Treffer oben + Fuß-Link; keine lokalen
Treffer → Internet-Ergebnisse werden direkt im Dropdown angezeigt.
- Abschnittsüberschrift "Keine lokalen Rezepte – aus dem Internet:"
trennt den Fallback visuell ab.
Race-Safety bleibt bestehen: Query-Vergleich vor jedem State-Write,
sodass spät ankommende Antworten keinen neueren Suchstand überschreiben.
Auf Nicht-Rezept-Seiten (Home, Wishlist, Admin) ist das Header-Suchfeld
ausgeblendet. Ohne flex-Spacer rutschten 🍽️/⚙️/Profil direkt neben das
Brand-Badge — besonders auffällig im Mobile-Layout.
margin-left: auto auf .bar-right schiebt die Action-Icons immer an den
rechten Rand, unabhängig davon ob das Suchfeld sichtbar ist.
Homepage (/):
- Tippen > 3 Zeichen + 300 ms Debounce → lokale Suche feuert automatisch
- Treffer erscheinen direkt unter dem Suchfeld als Karten-Grid
- "Zuletzt hinzugefügt" wird ausgeblendet, sobald aktiv gesucht wird
- 0 Treffer + fertig gesucht → Inline-Button "Im Internet weitersuchen"
Header (nur auf /recipes/[id] und /preview):
- Gleiche Debounce-Logik, aber Treffer in einem Dropdown unterm Feld
- Dropdown: kompakte Zeilen mit Thumbnail, Titel, Domain
- Fußzeile des Dropdown: "Im Internet weitersuchen"
- Click-outside und Escape schließen das Dropdown
- afterNavigate setzt Query nach dem Klick auf einen Treffer zurück
- Header-Breite ist jetzt auf 760 px begrenzt (gleich wie Rezept-Content),
damit die Suchleiste nie breiter wird als das Rezept darunter
Race-Safety: Ein zweites Tippen während laufender Fetch überschreibt
die Ergebnisse des ersten Requests nicht (Query-Vergleich vor Write).
- "← Lokale Suche"-Breadcrumb auf der Web-Suchseite entfernt (überflüssig,
da die lokale Suche automatisch zur Web-Suche weiterleitet, wenn leer).
- Header-Bar enthält jetzt ein Pill-Suchfeld, das von jeder Unterseite
aus direkt auf /search?q=... navigiert — kein Zurück mehr nötig,
wenn man aus einem offenen Rezept weiter sucht.
- Auf der Startseite bleibt die große Hero-Suche; das Header-Feld ist
dort ausgeblendet, damit es keine doppelte Eingabestelle gibt.
- Auf /search und /search/web spiegelt das Header-Feld die aktuelle
Query wider, sodass man den Begriff verfeinern kann.
- Mobile < 520px: Brand schrumpft zu einem 🍳-Badge, damit Platz für
das Suchfeld + Icons bleibt.
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>
- /wishlist renders cards with avatar-badge of who added it, like count,
heart toggle for active profile, delete button. Sort dropdown switches
between popular / newest / oldest.
- /recipes/[id] gets 'Auf Wunschliste (setzen)' button alongside favorite.
- Layout header shows 🍽️ link to /wishlist next to the admin ⚙️.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Homepage with search and recent recipes
- Search page listing local hits (FTS5)
- Recipe page with ratings, favorites, cooking log, comments
- Wake-Lock on recipe view for mobile kitchen use
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>