feat(recipe): Pulse-Animation beim Aktivieren Favorit/Wunschliste
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s

Kurzer Scale-Bounce plus ausklingender Ring in der Aktionsfarbe
(rot für Favorit, grün für Wunschliste), sobald der Button eine
Markierung setzt. Beim Wieder-Abwählen bleibt es ruhig — hilft
die Bestätigung visuell abzusetzen.

Die Animation wird per tick()-Zwischenschritt (false → tick → true)
gestartet, damit mehrfache Klicks innerhalb weniger hundert ms die
Animation neu triggern. prefers-reduced-motion schaltet den Effekt
aus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-18 15:06:15 +02:00
parent 361164febd
commit 194aee269e

View File

@@ -41,6 +41,24 @@
let saving = $state(false);
let recipeState = $state(data.recipe);
// Einmalige Pulse-Animation beim Aktivieren (nicht beim Wieder-Abwählen).
// Per tick()-Zwischenschritt "aus → an" erzwingen, damit die Animation
// auch bei mehrmaligem Klick innerhalb weniger hundert ms neu startet.
let pulseFav = $state(false);
let pulseWish = $state(false);
async function firePulse(which: 'fav' | 'wish') {
if (which === 'fav') {
pulseFav = false;
await tick();
pulseFav = true;
} else {
pulseWish = false;
await tick();
pulseWish = true;
}
}
async function saveRecipe(patch: {
title: string;
description: string | null;
@@ -128,15 +146,17 @@
return;
}
const profileId = profileStore.active.id;
const method = isFav ? 'DELETE' : 'PUT';
const wasFav = isFav;
const method = wasFav ? 'DELETE' : 'PUT';
await fetch(`/api/recipes/${data.recipe.id}/favorite`, {
method,
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ profile_id: profileId })
});
favoriteProfileIds = isFav
favoriteProfileIds = wasFav
? favoriteProfileIds.filter((id) => id !== profileId)
: [...favoriteProfileIds, profileId];
if (!wasFav) void firePulse('fav');
}
async function logCooked() {
@@ -258,7 +278,8 @@
return;
}
const profileId = profileStore.active.id;
if (onMyWishlist) {
const wasOn = onMyWishlist;
if (wasOn) {
await fetch(`/api/wishlist/${data.recipe.id}?profile_id=${profileId}`, {
method: 'DELETE'
});
@@ -272,6 +293,7 @@
wishlistProfileIds = [...wishlistProfileIds, profileId];
}
void wishlistStore.refresh();
if (!wasOn) void firePulse('wish');
}
// Wake-Lock — Bildschirm beim Kochen nicht dimmen lassen.
@@ -387,11 +409,23 @@
{/if}
</div>
<div class="btn-row">
<button class="btn" class:heart={isFav} onclick={toggleFavorite}>
<button
class="btn"
class:heart={isFav}
class:pulse={pulseFav}
onclick={toggleFavorite}
onanimationend={() => (pulseFav = false)}
>
<Heart size={18} strokeWidth={2} fill={isFav ? 'currentColor' : 'none'} />
<span>Favorit</span>
</button>
<button class="btn" class:wish={onMyWishlist} onclick={toggleWishlist}>
<button
class="btn"
class:wish={onMyWishlist}
class:pulse={pulseWish}
onclick={toggleWishlist}
onanimationend={() => (pulseWish = false)}
>
{#if onMyWishlist}
<Check size={18} strokeWidth={2.5} />
<span>Auf Wunschliste</span>
@@ -590,11 +624,38 @@
color: #c53030;
border-color: #f1b4b4;
background: #fdf3f3;
--pulse-color: rgba(197, 48, 48, 0.45);
}
.btn.wish {
color: #2b6a3d;
border-color: #b7d6c2;
background: #eaf4ed;
--pulse-color: rgba(43, 106, 61, 0.45);
}
/* Einmalige Bestätigung beim Aktivieren der Aktion — kurzer Scale-Bounce
plus ausklingender Ring in der Aktionsfarbe (siehe --pulse-color).
prefers-reduced-motion: Ring aus, kein Scale. */
.btn.pulse {
animation: btnPulse 0.5s ease-out;
}
@keyframes btnPulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 var(--pulse-color, rgba(43, 106, 61, 0.45));
}
55% {
transform: scale(1.07);
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
}
}
@media (prefers-reduced-motion: reduce) {
.btn.pulse {
animation: none;
}
}
.btn.screen-on {
color: #b07e00;