Layer 0 — Foundation: - Dark mode palette (30+ CSS var pairs in [data-theme='dark']) - CSS-first theme engine in styles.css (@theme + CSS vars) - UseTheme hook with light/dark/system support + localStorage persistence - Fixed animation keyframes (shimmer 1.5s canonical, glowPulse moved to CSS) - Smooth theme switch transitions via .theme-transitioning class Layer 1 — Shared UI Components: - Badge: success/warning variants now use CSS vars (bg-success-soft text-success) - Toast: rewritten with CSS vars, z-index fixed (40), repositioned top-right - Skeleton: added variant prop (rounded/circular/rectangular) - EmptyState: new component with icon + title + description + action - Button: added tertiary variant + icon-sm size - Card: added elevated/bordered variants - Input: added soft variant Layer 2 — Layout & Navigation: - TabStrip: new horizontal tab navigation with spring underline indicator (z-30) - Sidebar: expanded by default (w-64), brand assets always visible - Header: simplified brand bar, removed redundant page titles, added ThemeToggle - MobileTabBar: enhanced with spring dot indicator + glass bg + safe area - DashboardLayout: integrated TabStrip between Header and content - ParticleBackground: lazy render, skips on mobile/reduced-motion Layer 3 — Feature Components: - MessageCard: 30+ hardcoded hex replacements → semantic CSS vars - MessagesPanel: stat badges use Badge component variants - DashboardStats: StatCard with variant system (primary/success/warning/destructive) - UserSummaryList/UserProfileDetail/ChannelProfileDetail: all hardcoded colors → CSS vars - AudioVisualizer: reads --primary CSS var at paint time - ActiveSpeakers/RecordingsSubPanel: hardcoded colors → CSS vars Layer 4 — Polish: - EmptyState integrated across messages/dashboard/live panels - Theme toggle wired in Header + App root - Hover state audit for consistency - Entry animations verified (cardStagger/cardItem pattern in all panels) Resolves DESIGN_TOKENS.md §13.x issues: hardcoded colors, shimmer mismatch, glow-pulse fragmentation, z-index collisions, toast positioning.
83 lines
2.3 KiB
TypeScript
83 lines
2.3 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
|
|
interface AudioVisualizerProps {
|
|
levels: number[];
|
|
}
|
|
|
|
export function AudioVisualizer({ levels }: AudioVisualizerProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
const container = containerRef.current;
|
|
if (!canvas || !container) return;
|
|
|
|
const ro = new ResizeObserver(() => {
|
|
const rect = container.getBoundingClientRect();
|
|
const dpr = window.devicePixelRatio || 1;
|
|
canvas.width = rect.width * dpr;
|
|
canvas.height = 128 * dpr;
|
|
canvas.style.height = "128px";
|
|
});
|
|
ro.observe(container);
|
|
return () => ro.disconnect();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const width = canvas.width / dpr;
|
|
const height = canvas.height / dpr;
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
|
|
const barWidth = width / levels.length;
|
|
const maxBarHeight = height * 0.85;
|
|
|
|
const root = getComputedStyle(document.documentElement);
|
|
const primaryColor = root.getPropertyValue("--primary").trim() || "#23a1eb";
|
|
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
|
gradient.addColorStop(0, primaryColor);
|
|
gradient.addColorStop(1, primaryColor);
|
|
|
|
for (let i = 0; i < levels.length; i++) {
|
|
const level = levels[i];
|
|
const barHeight = Math.min(maxBarHeight, level * maxBarHeight);
|
|
const x = i * barWidth;
|
|
const y = height - barHeight;
|
|
|
|
ctx.fillStyle = gradient;
|
|
|
|
const radius = barWidth * 0.4;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + radius, y);
|
|
ctx.lineTo(x + barWidth - radius, y);
|
|
ctx.quadraticCurveTo(x + barWidth, y, x + barWidth, y + radius);
|
|
ctx.lineTo(x + barWidth, height);
|
|
ctx.lineTo(x, height);
|
|
ctx.lineTo(x, y + radius);
|
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
ctx.fill();
|
|
}
|
|
}, [levels]);
|
|
|
|
return (
|
|
<div ref={containerRef} className="relative w-full">
|
|
<canvas
|
|
ref={canvasRef}
|
|
width={0}
|
|
height={0}
|
|
className="w-full rounded-lg bg-primary/5"
|
|
style={{ height: "128px" }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|