// visuals.jsx - reusable visual elements: thumbnails, waveforms, sparklines, filmstrips const { thumbGrad } = window.ZAMPP_DATA; function AssetThumb({ asset, size = "md" }) { const aspect = size === "tall" ? "9 / 16" : "16 / 9"; const seed = asset.seed || 1; if (asset.type === "audio") { return (
); } return (
); } function FauxFrame({ seed }) { const shots = ["stage", "track", "interview", "aerial", "crowd", "trophy"]; const shot = shots[seed % shots.length]; return ( {shot === "stage" && ( <> )} {shot === "track" && ( <> )} {shot === "interview" && ( <> )} {shot === "aerial" && ( <> )} {shot === "crowd" && ( <> {Array.from({ length: 40 }).map((_, i) => { const x = (i * 4.5) % 160; const y = 50 + Math.sin(i * 1.7) * 4; return ; })} )} {shot === "trophy" && ( <> )} ); } function Waveform({ seed = 1, color = "var(--accent)", className = "" }) { const bars = 60; const pts = React.useMemo(() => { return Array.from({ length: bars }).map((_, i) => { const x = i / bars; const n = Math.sin(i * 0.7 + seed) * 0.5 + Math.sin(i * 2.1 + seed * 1.3) * 0.3 + Math.sin(i * 4.3 + seed * 0.7) * 0.2; return Math.max(0.1, Math.min(1, 0.5 + n * 0.5)); }); }, [seed]); return ( {pts.map((p, i) => ( ))} ); } function LiveStrip({ seed = 1, count = 8 }) { const [tick, setTick] = React.useState(0); React.useEffect(() => { const i = setInterval(() => setTick(t => t + 1), 2000); return () => clearInterval(i); }, []); return (
{Array.from({ length: count }).map((_, i) => (
))}
NOW
); } function Sparkline({ data, color = "var(--accent)", height = 28, fill = true }) { const max = Math.max(...data, 1); const min = Math.min(...data, 0); const range = max - min || 1; const w = 100; const pts = data.map((d, i) => { const x = (i / (data.length - 1)) * w; const y = height - ((d - min) / range) * height; return `${x},${y}`; }).join(" "); const area = `0,${height} ${pts} ${w},${height}`; return ( {fill && } ); } function AudioMeter({ level = 0.7, peak = 0.85, vertical = false }) { const segs = 20; return (
{Array.from({ length: segs }).map((_, i) => { const v = i / segs; const on = v < level; const color = v < 0.6 ? "var(--success)" : v < 0.85 ? "var(--warning)" : "var(--danger)"; return
; })}
); } function StatusDot({ status }) { const map = { online: { color: "var(--success)", pulse: false }, recording: { color: "var(--live)", pulse: true }, armed: { color: "var(--accent)", pulse: false }, idle: { color: "var(--text-4)", pulse: false }, error: { color: "var(--danger)", pulse: true }, offline: { color: "var(--text-4)", pulse: false }, processing: { color: "var(--warning)", pulse: true }, ready: { color: "var(--success)", pulse: false }, live: { color: "var(--live)", pulse: true }, queued: { color: "var(--text-3)", pulse: false }, running: { color: "var(--accent)", pulse: true }, done: { color: "var(--success)", pulse: false }, failed: { color: "var(--danger)", pulse: false }, }; const s = map[status] || { color: "var(--text-3)" }; return ( ); } function Elapsed({ seconds, live = false }) { const [t, setT] = React.useState(seconds); React.useEffect(() => { if (!live) return; const i = setInterval(() => setT(x => x + 1), 1000); return () => clearInterval(i); }, [live]); const h = Math.floor(t / 3600); const m = Math.floor((t % 3600) / 60); const s = t % 60; return {String(h).padStart(2, "0")}:{String(m).padStart(2, "0")}:{String(s).padStart(2, "0")}; } Object.assign(window, { AssetThumb, FauxFrame, Waveform, LiveStrip, Sparkline, AudioMeter, StatusDot, Elapsed });