Full build: Vite/React acid-terminal frontend (BEARER design system, Panchang/Sentient/Fragment Mono, neon layer, palette audit script), HoodquidityVaults Foundry contracts (single+dual deposits via Chainlink-style feeds, hybrid exit, 10% platform fee, boost, Genesis LP), Reown AppKit wallet connect with Robinhood Chain testnet (46630), live data from Dexscreener and onchain reads, partner portal, design-sync setup for claude.ai/design. Deploy: static via Forgejo Actions platform, BUILD_DIR=apps/web/dist (.env). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
64 lines
1.8 KiB
TypeScript
64 lines
1.8 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
||
import { motionEnabled } from "@/lib/motion";
|
||
|
||
const GLYPHS =
|
||
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789▚▞▮◎";
|
||
|
||
/**
|
||
* Cyberdeck decode: the label periodically dissolves into katakana and
|
||
* terminal glyphs, then settles back left to right. Decorative labels only,
|
||
* never live figures.
|
||
*/
|
||
export function Scramble({
|
||
text,
|
||
interval = 9000,
|
||
className,
|
||
}: {
|
||
text: string;
|
||
interval?: number;
|
||
className?: string;
|
||
}) {
|
||
const [display, setDisplay] = useState(text);
|
||
const raf = useRef(0);
|
||
|
||
useEffect(() => {
|
||
if (!motionEnabled) {
|
||
setDisplay(text);
|
||
return;
|
||
}
|
||
let cancelled = false;
|
||
|
||
const decode = () => {
|
||
const start = performance.now();
|
||
const dur = 950;
|
||
const tick = (t: number) => {
|
||
if (cancelled) return;
|
||
const p = Math.min((t - start) / dur, 1);
|
||
const settled = Math.floor(p * text.length);
|
||
let out = text.slice(0, settled);
|
||
for (let i = settled; i < text.length; i++) {
|
||
const ch = text[i];
|
||
out +=
|
||
ch === " "
|
||
? " "
|
||
: GLYPHS[(Math.floor(t / 48) + i * 7) % GLYPHS.length];
|
||
}
|
||
setDisplay(out);
|
||
if (p < 1) raf.current = requestAnimationFrame(tick);
|
||
else setDisplay(text);
|
||
};
|
||
raf.current = requestAnimationFrame(tick);
|
||
};
|
||
|
||
decode();
|
||
const jitter = (text.length * 137) % 3000;
|
||
const timer = setInterval(decode, interval + jitter);
|
||
return () => {
|
||
cancelled = true;
|
||
clearInterval(timer);
|
||
cancelAnimationFrame(raf.current);
|
||
};
|
||
}, [text, interval]);
|
||
|
||
return <span className={className}>{display}</span>;
|
||
}
|