From 7965e700c28bbc1006a3f1f9c6eef9da81af67a7 Mon Sep 17 00:00:00 2001 From: Atriiy Date: Sun, 15 Mar 2026 12:57:42 +0800 Subject: [PATCH 1/3] feat: add effect to like button --- app/components/Package/Header.vue | 146 ++++++++++++++++++++++++++---- 1 file changed, 126 insertions(+), 20 deletions(-) diff --git a/app/components/Package/Header.vue b/app/components/Package/Header.vue index db6ed1183..8b0039f4e 100644 --- a/app/components/Package/Header.vue +++ b/app/components/Package/Header.vue @@ -184,6 +184,18 @@ onKeyStroke( // TODO: Maybe set this where it's not loaded here every load? const { user } = useAtproto() +const likeAnimKey = shallowRef(0) +const showLikeFloat = shallowRef(false) + +const heartAnimStyle = computed(() => { + if (likeAnimKey.value === 0) return {} + return { + animation: likesData.value?.userHasLiked + ? 'heart-spring 0.55s cubic-bezier(0.34,1.56,0.64,1) forwards' + : 'heart-unlike 0.3s ease forwards', + } +}) + const authModal = useModal('auth-modal') const { data: likesData, status: likeStatus } = useFetch( @@ -211,6 +223,15 @@ const likeAction = async () => { const currentlyLiked = likesData.value?.userHasLiked ?? false const currentLikes = likesData.value?.totalLikes ?? 0 + likeAnimKey.value++ + + if (!currentlyLiked) { + showLikeFloat.value = true + setTimeout(() => { + showLikeFloat.value = false + }, 850) + } + // Optimistic update likesData.value = { totalLikes: currentlyLiked ? currentLikes - 1 : currentLikes + 1, @@ -293,26 +314,37 @@ const likeAction = async () => { class="items-center" strategy="fixed" > - - +
+ + + +
@@ -458,4 +490,78 @@ const likeAction = async () => { display: none; } } + +.likeWrapper { + position: relative; + display: inline-flex; +} + +.likeFloat { + position: absolute; + top: 0; + left: 50%; + font-size: 12px; + font-weight: 600; + color: var(--color-red-500, #ef4444); + pointer-events: none; + white-space: nowrap; + animation: float-up 0.75s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; +} + +@keyframes float-up { + 0% { + opacity: 0; + transform: translateX(-50%) translateY(0); + } + 15% { + opacity: 1; + transform: translateX(-50%) translateY(-4px); + } + 80% { + opacity: 1; + transform: translateX(-50%) translateY(-20px); + } + 100% { + opacity: 0; + transform: translateX(-50%) translateY(-28px); + } +} + + + From 02d0802b7d3069e1396c3da0675f8da8bebd0299 Mon Sep 17 00:00:00 2001 From: Atriiy Date: Sun, 15 Mar 2026 16:21:25 +0800 Subject: [PATCH 2/3] fix: fix +1 state issue --- app/components/Package/Header.vue | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/components/Package/Header.vue b/app/components/Package/Header.vue index 8b0039f4e..dfc5dd59e 100644 --- a/app/components/Package/Header.vue +++ b/app/components/Package/Header.vue @@ -186,6 +186,8 @@ const { user } = useAtproto() const likeAnimKey = shallowRef(0) const showLikeFloat = shallowRef(false) +const likeFloatKey = shallowRef(0) +let likeFloatTimer: ReturnType | null = null const heartAnimStyle = computed(() => { if (likeAnimKey.value === 0) return {} @@ -226,9 +228,15 @@ const likeAction = async () => { likeAnimKey.value++ if (!currentlyLiked) { + if (likeFloatTimer !== null) { + clearTimeout(likeFloatTimer) + likeFloatTimer = null + } + likeFloatKey.value++ showLikeFloat.value = true - setTimeout(() => { + likeFloatTimer = setTimeout(() => { showLikeFloat.value = false + likeFloatTimer = null }, 850) } @@ -315,7 +323,13 @@ const likeAction = async () => { strategy="fixed" >
- + Date: Sun, 15 Mar 2026 16:29:20 +0800 Subject: [PATCH 3/3] feat: respect the reduced motion perference --- app/components/Package/Header.vue | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/app/components/Package/Header.vue b/app/components/Package/Header.vue index dfc5dd59e..417ee8eae 100644 --- a/app/components/Package/Header.vue +++ b/app/components/Package/Header.vue @@ -184,13 +184,15 @@ onKeyStroke( // TODO: Maybe set this where it's not loaded here every load? const { user } = useAtproto() +const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)') + const likeAnimKey = shallowRef(0) const showLikeFloat = shallowRef(false) const likeFloatKey = shallowRef(0) let likeFloatTimer: ReturnType | null = null const heartAnimStyle = computed(() => { - if (likeAnimKey.value === 0) return {} + if (likeAnimKey.value === 0 || prefersReducedMotion.value) return {} return { animation: likesData.value?.userHasLiked ? 'heart-spring 0.55s cubic-bezier(0.34,1.56,0.64,1) forwards' @@ -227,7 +229,7 @@ const likeAction = async () => { likeAnimKey.value++ - if (!currentlyLiked) { + if (!currentlyLiked && !prefersReducedMotion.value) { if (likeFloatTimer !== null) { clearTimeout(likeFloatTimer) likeFloatTimer = null @@ -522,6 +524,12 @@ const likeAction = async () => { animation: float-up 0.75s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } +@media (prefers-reduced-motion: reduce) { + .likeFloat { + display: none; + } +} + @keyframes float-up { 0% { opacity: 0; @@ -578,4 +586,19 @@ const likeAction = async () => { transform: scale(1); } } + +@media (prefers-reduced-motion: reduce) { + @keyframes heart-spring { + from, + to { + transform: scale(1); + } + } + @keyframes heart-unlike { + from, + to { + transform: scale(1); + } + } +}