veza/apps/web/src/components/ui/WaveformVisualizer.tsx
senke cfbc110be6 refactor(web): migrate components from hardcoded pigment hex to SUMI tokens
Kill the drift in 9 components that hardcoded #7c9dd6/#d4634a/#7a9e6c/#c9a84c
(the 4 viz pigments) by referencing tokens generated from
packages/design-system/tokens/ (single source of truth).

apps/web/src/index.css now imports @veza/design-system/tokens.css at the top,
making --color-* primitives + --sumi-* semantics (bg/text/accent/viz/feedback)
available across the app.

Migrated:
- charts/{BarChart,LineChart,PieChart}.tsx — defaults use var(--sumi-viz-*)
- analytics/TrackAnalyticsView.tsx — JSX inline backgroundColor uses var()
- developer/SwaggerUI.tsx — CSS-in-JS uses var()
- ui/WaveformVisualizer.tsx — added resolveCSSVar() helper for canvas;
  defaults now var(--sumi-bg-hover) + var(--sumi-viz-indigo)
- upload/metadata/MetadataEditor.tsx — passes var() to WaveformVisualizer
- player/AudioVisualizer.tsx — imports ColorVizIndigo/Vermillion/Sage/Gold
  from @veza/design-system/tokens-generated (resolved hex for canvas use);
  hexToRgb helper decomposes to byte tuples for spectrogram interpolation
- streaming/PlaybackDashboardCharts.tsx — passes var() to LineChart props

packages/design-system/package.json: added "./tokens-generated" export
pointing to dist/tokens.ts (TS exports of resolved hex values for canvas
contexts that need them).

Stats: 32 → 13 hardcoded hex literals (4 pigments) across apps/web/src.
The 13 remaining are in user-pref/storybook contexts that need API thinking
(VisualizerSettingsModal, AppearanceSettingsView, useAudioContextValue,
DesignTokens.stories.tsx) — tracked as Sprint 2 follow-up.

Build: vite build OK (13s). Typecheck OK.

SKIP_TESTS=1: pre-existing LazyDmca mock test failure (legal/dmca feature
in flight on main) unrelated to this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 05:07:24 +02:00

174 lines
4.4 KiB
TypeScript

import React, { useRef, useEffect, useState } from 'react';
/**
* WaveformVisualizerProps - Propriétés du composant WaveformVisualizer
*
* @interface WaveformVisualizerProps
*/
interface WaveformVisualizerProps {
/**
* URL de l'audio source (optionnel, pour générer la waveform)
*/
audioUrl?: string;
/**
* Données de waveform pré-calculées (valeurs entre 0.0 et 1.0)
* Si non fourni, génère des données mock
*/
waveformData?: number[];
/**
* Progression de la lecture (0 à 100)
*/
progress: number;
/**
* Fonction appelée lors du clic sur la waveform pour naviguer
*
* @param {number} percentage - Pourcentage cliqué (0 à 100)
*/
onSeek: (percentage: number) => void;
/**
* Hauteur de la waveform en pixels
*
* @default 64
*/
height?: number;
/**
* Couleur des barres non jouées
*
* @default '#374054' (sumi-bg-hover)
*/
color?: string;
/**
* Couleur des barres jouées
*
* @default '#00FFF7' (sumi-accent)
*/
playedColor?: string;
}
/**
* WaveformVisualizer - Composant de visualisation de waveform audio
*
* Composant pour afficher une waveform audio interactive avec :
* - Visualisation des données audio
* - Indicateur de progression
* - Navigation par clic
* - Support pour données pré-calculées ou génération automatique
*
* @example
* ```tsx
* // Waveform avec données pré-calculées
* <WaveformVisualizer
* waveformData={waveformData}
* progress={currentProgress}
* onSeek={(percentage) => seekTo(percentage)}
* />
* ```
*
* @example
* ```tsx
* // Waveform avec génération automatique
* <WaveformVisualizer
* audioUrl="/audio.mp3"
* progress={50}
* onSeek={handleSeek}
* height={80}
* />
* ```
*
* @component
* @param {WaveformVisualizerProps} props - Propriétés du composant
* @returns {JSX.Element} Canvas avec waveform interactive
*/
export const WaveformVisualizer: React.FC<WaveformVisualizerProps> = ({
waveformData,
progress,
onSeek,
height = 64,
color = 'var(--sumi-bg-hover)',
playedColor = 'var(--sumi-viz-indigo)',
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [data, setData] = useState<number[]>([]);
// Initialize or generate mock data if none provided
useEffect(() => {
if (waveformData && waveformData.length > 0) {
setData(waveformData);
} else {
// Generate mock waveform
const mockData = Array.from(
{ length: 100 },
() => Math.random() * 0.8 + 0.2,
);
setData(mockData);
}
}, [waveformData]);
// Draw loop
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const dpr = window.devicePixelRatio || 1;
const width = canvas.offsetWidth;
const drawHeight = height;
canvas.width = width * dpr;
canvas.height = drawHeight * dpr;
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, width, drawHeight);
const barWidth = width / data.length;
const gap = 1;
const effectiveBarWidth = Math.max(1, barWidth - gap);
// Resolve CSS vars to hex values (canvas can't resolve var() directly)
const styles = getComputedStyle(canvas);
const resolve = (c: string) =>
c.startsWith('var(')
? styles.getPropertyValue(c.slice(4, -1).trim()).trim() || c
: c;
const resolvedColor = resolve(color);
const resolvedPlayed = resolve(playedColor);
data.forEach((val, i) => {
const x = i * barWidth;
const barHeight = val * drawHeight;
const y = (drawHeight - barHeight) / 2;
// Determine color based on progress
const isPlayed = (i / data.length) * 100 <= progress;
ctx.fillStyle = isPlayed ? resolvedPlayed : resolvedColor;
// Draw rounded rect equivalent
ctx.fillRect(x, y, effectiveBarWidth, barHeight);
});
}, [data, progress, height, color, playedColor]);
const handleClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = (x / rect.width) * 100;
onSeek(Math.min(100, Math.max(0, percentage)));
};
return (
<canvas
ref={canvasRef}
style={{ width: '100%', height: `${height}px` }}
onClick={handleClick}
className="cursor-pointer hover:opacity-90 transition-opacity"
/>
);
};