feat(recipe): Pulse-Animation beim Aktivieren Favorit/Wunschliste
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user