From 9c25e0887c377f9e5be1fa8d05ba4129da6356e3 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Tue, 25 Aug 2026 23:23:19 +0700 Subject: [PATCH] feat(frontend): add tactical HUD header and GSAP speaking-pulse loop to voice route --- .../src/app/(dashboard)/voice/view.tsx | 48 ++++++++++++++++--- .../src/components/voice/voice-stage.tsx | 37 +++++++++++++- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/services/frontend/src/app/(dashboard)/voice/view.tsx b/services/frontend/src/app/(dashboard)/voice/view.tsx index 44e6ec2e..d0e885e1 100644 --- a/services/frontend/src/app/(dashboard)/voice/view.tsx +++ b/services/frontend/src/app/(dashboard)/voice/view.tsx @@ -29,6 +29,7 @@ import { useVoiceListen, useVoiceStatus, } from "@/hooks"; +import { useStaggerReveal } from "@/hooks/use-gsap-animation"; import type { Guild, VoiceStatus } from "@/lib/types"; import { useWebSocket } from "@/lib/ws/context"; @@ -63,6 +64,12 @@ export function VoiceView({ const [micVol, setMicVol] = useState(100); const [listenVol, setListenVol] = useState(75); + const hudRef = useStaggerReveal(".hud-tile", { + stagger: 0.06, + y: 14, + dependencies: [status], + }); + useEffect(() => { const unsub = subscribe(ws); return unsub; @@ -124,8 +131,37 @@ export function VoiceView({ }; return ( -
- +
+ {/* Tactical HUD Header Bar */} +
+
+
+ + +
+

+ VOICE MATRIX · LIVE_LINK +

+
+
+ LINK: + + {connected ? "ESTABLISHED" : "STANDBY"} + +
+
+ +
-
- +
+ @@ -283,7 +319,7 @@ export function VoiceView({ )} - +
{(status?.connections ?? []).map((c) => ( diff --git a/services/frontend/src/components/voice/voice-stage.tsx b/services/frontend/src/components/voice/voice-stage.tsx index 70b25092..ef49852a 100644 --- a/services/frontend/src/components/voice/voice-stage.tsx +++ b/services/frontend/src/components/voice/voice-stage.tsx @@ -16,6 +16,10 @@ export function VoiceStage({ speakers }: { speakers: ActiveSpeaker[] }) { const n = speakers.length; const speaking = speakers.filter((s) => s.speaking).length; const live = speaking > 0; + const speakingIds = speakers + .filter((s) => s.speaking) + .map((s) => s.userId) + .join(","); useGSAP( () => { @@ -39,6 +43,37 @@ export function VoiceStage({ speakers }: { speakers: ActiveSpeaker[] }) { { scope: containerRef, dependencies: [speakers.length] }, ); + // Reactive pulse ring loop keyed to active speaking set — cleared on stop. + useGSAP( + () => { + if (!containerRef.current) return; + const prefersReduced = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + if (prefersReduced) return; + + const ringNodes = containerRef.current.querySelectorAll( + ".speaker-pulse-ring", + ); + if (ringNodes.length === 0) return; + + const tl = gsap.timeline({ repeat: -1, yoyo: true }); + tl.to(ringNodes, { + scale: 1.18, + opacity: 0.35, + duration: 0.55, + ease: "sine.inOut", + stagger: { each: 0.08, from: "random" }, + }); + + return () => { + tl.kill(); + gsap.set(ringNodes, { clearProps: "scale,opacity" }); + }; + }, + { scope: containerRef, dependencies: [speakingIds] }, + ); + return (
{s.speaking && ( - + )}