From f0f934c6785d617bdffc4f6da67c0fcbe2110dbf Mon Sep 17 00:00:00 2001 From: Marvell69 Date: Sat, 28 Mar 2026 10:23:39 +0100 Subject: [PATCH 1/2] ui: add premium glitch animation to copy button feedback --- frontend/src/components/CopyButton.tsx | 245 +++++++++++++++++++------ 1 file changed, 189 insertions(+), 56 deletions(-) diff --git a/frontend/src/components/CopyButton.tsx b/frontend/src/components/CopyButton.tsx index 97892dd..35a8578 100644 --- a/frontend/src/components/CopyButton.tsx +++ b/frontend/src/components/CopyButton.tsx @@ -1,23 +1,123 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useTranslations } from "next-intl"; -import { motion, AnimatePresence } from "framer-motion"; +import { motion, AnimatePresence, type Variants } from "framer-motion"; interface CopyButtonProps { text: string; className?: string; } +const glitchButtonVariants: Variants = { + idle: { + x: 0, + y: 0, + scale: 1, + rotate: 0, + boxShadow: "0 0 0 rgba(94, 242, 192, 0)", + borderColor: "rgba(255,255,255,0.1)", + transition: { + duration: 0.24, + ease: "easeOut" + } + }, + success: { + x: [0, -1.5, 1.75, -1.25, 0.75, 0], + y: [0, 0.5, -0.75, 0.5, 0, 0], + scale: [1, 1.02, 0.985, 1.015, 1], + rotate: [0, -1, 1, -0.5, 0], + boxShadow: [ + "0 0 0 rgba(94, 242, 192, 0)", + "0 0 18px rgba(94, 242, 192, 0.3)", + "0 0 28px rgba(56, 189, 248, 0.28)", + "0 0 22px rgba(94, 242, 192, 0.24)", + "0 0 0 rgba(94, 242, 192, 0)" + ], + borderColor: [ + "rgba(255,255,255,0.1)", + "rgba(94,242,192,0.55)", + "rgba(56,189,248,0.45)", + "rgba(94,242,192,0.45)", + "rgba(255,255,255,0.1)" + ], + transition: { + duration: 0.48, + times: [0, 0.18, 0.38, 0.7, 1], + ease: "easeInOut" + } + } +}; + +const glitchLayerVariants: Variants = { + initial: { + opacity: 0, + x: 0, + scaleX: 1 + }, + success: (xOffset: number) => ({ + opacity: [0, 0.75, 0.2, 0.55, 0], + x: [0, xOffset, -xOffset * 0.8, xOffset * 0.45, 0], + scaleX: [1, 1.06, 0.98, 1.02, 1], + transition: { + duration: 0.46, + times: [0, 0.16, 0.42, 0.72, 1], + ease: "easeInOut" + } + }) +}; + +const iconGlitchVariants: Variants = { + idle: { + x: 0, + filter: "drop-shadow(0 0 0 rgba(94, 242, 192, 0))" + }, + success: { + x: [0, -1, 1, -0.5, 0], + filter: [ + "drop-shadow(0 0 0 rgba(94, 242, 192, 0))", + "drop-shadow(0 0 6px rgba(94, 242, 192, 0.7))", + "drop-shadow(0 0 10px rgba(56, 189, 248, 0.55))", + "drop-shadow(0 0 6px rgba(94, 242, 192, 0.45))", + "drop-shadow(0 0 0 rgba(94, 242, 192, 0))" + ], + transition: { + duration: 0.4, + times: [0, 0.18, 0.42, 0.72, 1], + ease: "easeInOut" + } + } +}; + export default function CopyButton({ text, className = "" }: CopyButtonProps) { const t = useTranslations("copyButton"); const [copied, setCopied] = useState(false); + const resetTimerRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (resetTimerRef.current) { + clearTimeout(resetTimerRef.current); + } + }; + }, []); + + const showCopiedState = () => { + if (resetTimerRef.current) { + clearTimeout(resetTimerRef.current); + } + + setCopied(true); + resetTimerRef.current = setTimeout(() => { + setCopied(false); + resetTimerRef.current = null; + }, 2000); + }; const handleCopy = async () => { try { await navigator.clipboard.writeText(text); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + showCopiedState(); } catch { const el = document.createElement("textarea"); el.value = text; @@ -25,8 +125,7 @@ export default function CopyButton({ text, className = "" }: CopyButtonProps) { el.select(); document.execCommand("copy"); document.body.removeChild(el); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + showCopiedState(); } }; @@ -35,61 +134,95 @@ export default function CopyButton({ text, className = "" }: CopyButtonProps) { - - {copied ? ( - - + )} + + {copied && ( From 9a8f24a62464d66a94e98cef5982c3e67428f0cb Mon Sep 17 00:00:00 2001 From: Marvell69 Date: Sat, 28 Mar 2026 10:47:21 +0100 Subject: [PATCH 2/2] test: verify copy button glitch feedback --- frontend/src/app/(public)/vrt/page.tsx | 7 +++++++ frontend/tests/e2e/vrt.spec.ts | 16 ++++++++++++++++ .../copy-button-success-chromium-darwin.png | Bin 0 -> 1171 bytes 3 files changed, 23 insertions(+) create mode 100644 frontend/tests/e2e/vrt.spec.ts-snapshots/copy-button-success-chromium-darwin.png diff --git a/frontend/src/app/(public)/vrt/page.tsx b/frontend/src/app/(public)/vrt/page.tsx index f606e5a..d40a26d 100644 --- a/frontend/src/app/(public)/vrt/page.tsx +++ b/frontend/src/app/(public)/vrt/page.tsx @@ -4,6 +4,7 @@ import React, { useState } from "react"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { Modal } from "@/components/ui/Modal"; +import CopyButton from "@/components/CopyButton"; export default function VRTPage() { const [isModalOpen, setIsModalOpen] = useState(false); @@ -20,6 +21,12 @@ export default function VRTPage() { +
+ + sk_test_vrt_copy_glitch_signal + + +
diff --git a/frontend/tests/e2e/vrt.spec.ts b/frontend/tests/e2e/vrt.spec.ts index a52df08..b323ed6 100644 --- a/frontend/tests/e2e/vrt.spec.ts +++ b/frontend/tests/e2e/vrt.spec.ts @@ -16,6 +16,22 @@ test.describe("Visual Regression Tests for Core Components", () => { expect(await buttonsSection.screenshot()).toMatchSnapshot("buttons-core.png"); }); + test("Copy button shows premium glitch success feedback", async ({ page, browserName }) => { + test.skip(browserName !== "chromium", "Clipboard permission check is only verified in chromium."); + + await page.context().grantPermissions(["clipboard-read", "clipboard-write"]); + + const copyButton = page.getByRole("button", { name: "Copy to clipboard" }); + await expect(copyButton).toBeVisible(); + + await copyButton.click(); + + await expect(page.getByText("Copied!")).toBeVisible(); + await expect(copyButton).toHaveScreenshot("copy-button-success.png", { + animations: "allow", + }); + }); + test("Inputs should match visual baseline", async ({ page }) => { const inputsSection = page.locator("#vrt-inputs"); await expect(inputsSection).toBeVisible(); diff --git a/frontend/tests/e2e/vrt.spec.ts-snapshots/copy-button-success-chromium-darwin.png b/frontend/tests/e2e/vrt.spec.ts-snapshots/copy-button-success-chromium-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..0099843906229cb6a7b92735913f8374e510a684 GIT binary patch literal 1171 zcmV;E1Z?|>P)zED8~-#@@cu(4K>!bU5xv9VEXENuj}P%wg^ zwjx-Gq8N>CV$8p7mb7#Ku&37I%QL$JQ{xL=fkOBZZ03eq3 z7-MKIt}(h}Q50q%uEu{Q6a!1DXa}>qmxxo zIG(KTNd{IS91iI;<(B#c&B%h^IMa=%nn(-UC~g?YK5kuwO)z)*CVMvy$T%Vle#N&VZ3f8EQxJnx+tS%8W^2Nmjjms<`LNH_yhfHdM!>?|an&F6| zkP;gNBo`8Qlv+X3-$(2H0xLz(+UhH;xOPv1eq~wPa3BMbg60D9#wiinhHEh>4Mub-wL7v)=IQOy4jmaO+E}o}8d6xu$xL7U?_C@7{1<~CiL>teY_vH6y z1+jcRcWVj7!a$NDjqqcX6nTNjM@W#xm`sFduQg<#F%)Spz}QF}2N^JOyLA!rZ>@C~ ztpjVX5JO|D*cfXsEEkhu9mLTc5u$yB0Ic1)RrQpZsc;fWHp2$2JCuqFn4Edm*_?E& zO+K!Nj_f$_;Cgv@-Pb2?2n!r28)Gxg1xzld%rdkVFm!b1zWZ0JAHKf5dM9XYf-K-< z@s%K5MqICF=-H3o$4-wlrmB{y^FiS5>x{Hrb}VHta99Foa9mE-Y~SKWC<*28@xA*A z`rFuOsZJ2Lm>$MaK{8}dV_RDjNMw}B5n zXWi0ce%5}Tc=f<(GL=eVnK{QnwK z9wzR!@k(c0LkDZ$k+L~Cf!z7@u-UjMt|@W}N7lxFX0tIH*KB}~JnkFF)&L76*6BLpjbaJRRp*$JqI=&@P(?x4T zbCuuAzR%Dt2Na&GAXDR9Oj?`eXpQe?bt?TcEBH=hWsPjGVHVL%4}*ST9?vbz zU=mC%C9X}{s1fCQ4WmQCqMM#GAPn6CftqxoiB%vR0{?&PEftG4rM_^t5BvND00960 lpJRc^00006Nkl