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>
This commit is contained in:
senke 2026-04-27 05:07:24 +02:00
parent a25ad2e0b4
commit cfbc110be6
11 changed files with 80 additions and 41 deletions

View file

@ -155,10 +155,10 @@ export const TrackAnalyticsView: React.FC<TrackAnalyticsViewProps> = ({
width: `${val}%`,
backgroundColor:
range === '18-24'
? '#7c9dd6'
? 'var(--sumi-viz-indigo)'
: range === '25-34'
? '#7a9e6c'
: '#2a2a31',
? 'var(--sumi-viz-sage)'
: 'var(--sumi-bg-hover)',
}}
>
<div className="absolute inset-0 flex items-center justify-center text-xs font-bold text-foreground opacity-0 group-hover:opacity-100 transition-opacity">

View file

@ -22,7 +22,7 @@ export function BarChart({
data,
xAxisLabel,
yAxisLabel,
color = '#7c9dd6',
color = 'var(--sumi-viz-indigo)',
showGrid = true,
showValues = false,
height = 300,

View file

@ -22,7 +22,7 @@ export function LineChart({
data,
xAxisLabel,
yAxisLabel,
color = '#7c9dd6',
color = 'var(--sumi-viz-indigo)',
showGrid = true,
showDots = true,
height = 300,

View file

@ -15,15 +15,18 @@ export interface PieChartProps extends Omit<ChartProps, 'children'> {
colors?: string[];
}
// Data viz pigments — see CHARTE_GRAPHIQUE_TALAS §4.5 (data viz only).
// First 5 are canonical; sakura/terminal/magenta are app-specific extras pending
// canonical definition in tokens (follow-up Sprint 2).
const DEFAULT_COLORS = [
'#7c9dd6', // indigo
'#d4634a', // vermillion
'#7a9e6c', // sage
'#c9a84c', // gold
'#a8a4a0', // text-secondary
'#e0a0b8', // sakura
'#3eaa5e', // terminal-green
'#c840a0', // graffiti-magenta
'var(--sumi-viz-indigo)',
'var(--sumi-viz-vermillion)',
'var(--sumi-viz-sage)',
'var(--sumi-viz-gold)',
'var(--sumi-viz-neutral)',
'#e0a0b8', // sakura — TODO: canonize in tokens/primitive/color.json viz palette
'#3eaa5e', // terminal-green — TODO: canonize
'#c840a0', // graffiti-magenta — TODO: canonize
];
/**

View file

@ -240,7 +240,7 @@ export function SwaggerUIDoc({ specUrl, spec, useIframe = false }: SwaggerUIProp
color: rgba(255, 255, 255, 0.8);
}
.swagger-ui-container .swagger-ui .parameter__name {
color: #7c9dd6;
color: var(--sumi-viz-indigo);
}
.swagger-ui-container .swagger-ui .response-col_status {
color: #fff;
@ -256,7 +256,7 @@ export function SwaggerUIDoc({ specUrl, spec, useIframe = false }: SwaggerUIProp
color: #fff;
}
.swagger-ui-container .swagger-ui .btn {
background: #7c9dd6;
background: var(--sumi-viz-indigo);
color: #000;
border: none;
}
@ -264,7 +264,7 @@ export function SwaggerUIDoc({ specUrl, spec, useIframe = false }: SwaggerUIProp
background: #93afe0;
}
.swagger-ui-container .swagger-ui .btn.execute {
background: #7c9dd6;
background: var(--sumi-viz-indigo);
color: #000;
}
.swagger-ui-container .swagger-ui .btn.cancel {

View file

@ -91,8 +91,8 @@ export const WaveformVisualizer: React.FC<WaveformVisualizerProps> = ({
progress,
onSeek,
height = 64,
color = '#2a2a31', // sumi-bg-hover
playedColor = '#7c9dd6', // sumi-accent
color = 'var(--sumi-bg-hover)',
playedColor = 'var(--sumi-viz-indigo)',
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [data, setData] = useState<number[]>([]);
@ -133,6 +133,15 @@ export const WaveformVisualizer: React.FC<WaveformVisualizerProps> = ({
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;
@ -140,7 +149,7 @@ export const WaveformVisualizer: React.FC<WaveformVisualizerProps> = ({
// Determine color based on progress
const isPlayed = (i / data.length) * 100 <= progress;
ctx.fillStyle = isPlayed ? playedColor : color;
ctx.fillStyle = isPlayed ? resolvedPlayed : resolvedColor;
// Draw rounded rect equivalent
ctx.fillRect(x, y, effectiveBarWidth, barHeight);

View file

@ -175,8 +175,8 @@ export const MetadataEditor: React.FC<MetadataEditorProps> = ({
progress={progress}
onSeek={setProgress}
height={48}
color="#2a2a31"
playedColor="#7c9dd6"
color="var(--sumi-bg-hover)"
playedColor="var(--sumi-viz-indigo)"
/>
</div>
</Card>

View file

@ -13,6 +13,13 @@ import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { BarChart3, Activity, Radio } from 'lucide-react';
import type { VisualizerMode } from '../hooks/useSpectrumAnalyser';
import {
ColorVizIndigo,
ColorVizVermillion,
ColorVizSage,
ColorVizGold,
ColorVoidBase,
} from '@veza/design-system/tokens-generated';
interface AudioVisualizerProps {
/** Normalized frequency bands [0-1] */
@ -31,11 +38,24 @@ const MODES: { mode: VisualizerMode; icon: typeof BarChart3; label: string }[] =
{ mode: 'spectrogram', icon: Radio, label: 'Spectrogram' },
];
// SUMI colors
const ACCENT_COLOR = '#7c9dd6'; // --sumi-accent
const SAGE = '#7a9e6c'; // --sumi-sage
const GOLD = '#c9a84c'; // --sumi-gold
const BG_VOID = '#0c0c0f'; // --sumi-bg-void
// SUMI colors — resolved hex values from generated tokens (source of truth: packages/design-system/tokens/).
// Canvas can't resolve var(--sumi-*) directly, so we use the imported hex strings to enable
// gradient interpolation and string concatenation (alpha suffix) below.
const ACCENT_COLOR = ColorVizIndigo;
const VERMILLION = ColorVizVermillion;
const SAGE = ColorVizSage;
const GOLD = ColorVizGold;
const BG_VOID = ColorVoidBase;
// Decompose hex colors to RGB byte tuples for spectrogram interpolation.
const hexToRgb = (hex: string): [number, number, number] => {
const m = hex.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
if (!m || !m[1] || !m[2] || !m[3]) return [0, 0, 0];
return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)];
};
const [I_R, I_G, I_B] = hexToRgb(ACCENT_COLOR);
const [V_R, V_G, V_B] = hexToRgb(VERMILLION);
const [GOLD_R, GOLD_G, GOLD_B] = hexToRgb(GOLD);
export function AudioVisualizer({
bands,
@ -75,11 +95,11 @@ export function AudioVisualizer({
const x = i * (barWidth + gap);
const y = H - barH;
// Gradient from accent to vermillion based on frequency
// Gradient from accent (indigo) to vermillion based on frequency
const t = i / barCount;
const r = lerp(0x7c, 0xd4, t);
const g = lerp(0x9d, 0x63, t);
const b = lerp(0xd6, 0x4a, t);
const r = lerp(I_R, V_R, t);
const g = lerp(I_G, V_G, t);
const b = lerp(I_B, V_B, t);
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
// Rounded top rect
@ -276,21 +296,19 @@ function lerp(a: number, b: number, t: number): number {
}
function spectrogramColor(intensity: number): [number, number, number] {
// 0..0.25: black → deep blue
// 0.25..0.5: deep blue → accent
// 0.5..0.75: accent → vermillion
// 0.75..1: vermillion → gold
// Heat map: black → deep blue → indigo → vermillion → gold
// Bytes derived from token-imported hex via hexToRgb (above).
if (intensity < 0.25) {
const t = intensity / 0.25;
return [lerp(12, 40, t), lerp(12, 50, t), lerp(15, 100, t)];
} else if (intensity < 0.5) {
const t = (intensity - 0.25) / 0.25;
return [lerp(40, 0x7c, t), lerp(50, 0x9d, t), lerp(100, 0xd6, t)];
return [lerp(40, I_R, t), lerp(50, I_G, t), lerp(100, I_B, t)];
} else if (intensity < 0.75) {
const t = (intensity - 0.5) / 0.25;
return [lerp(0x7c, 0xd4, t), lerp(0x9d, 0x63, t), lerp(0xd6, 0x4a, t)];
return [lerp(I_R, V_R, t), lerp(I_G, V_G, t), lerp(I_B, V_B, t)];
} else {
const t = (intensity - 0.75) / 0.25;
return [lerp(0xd4, 0xc9, t), lerp(0x63, 0xa8, t), lerp(0x4a, 0x4c, t)];
return [lerp(V_R, GOLD_R, t), lerp(V_G, GOLD_G, t), lerp(V_B, GOLD_B, t)];
}
}

View file

@ -34,7 +34,7 @@ export function PlaybackDashboardCharts({
data={sessionsChartData}
xAxisLabel="Date"
yAxisLabel="Nombre de sessions"
color="#7c9dd6"
color="var(--sumi-viz-indigo)"
height={300}
showGrid
showDots
@ -56,7 +56,7 @@ export function PlaybackDashboardCharts({
data={playTimeChartData}
xAxisLabel="Date"
yAxisLabel="Temps moyen (secondes)"
color="#7a9e6c"
color="var(--sumi-viz-sage)"
height={300}
showGrid
showDots
@ -78,7 +78,7 @@ export function PlaybackDashboardCharts({
data={completionChartData}
xAxisLabel="Date"
yAxisLabel="Taux de complétion (%)"
color="#c9a84c"
color="var(--sumi-viz-gold)"
height={300}
showGrid
showDots

View file

@ -1,6 +1,12 @@
@import 'tailwindcss';
@import 'tw-animate-css';
/* SUMI generated tokens Single source of truth. See packages/design-system/tokens/.
This adds --color-* primitives and --sumi-* semantics (bg/text/accent/viz/feedback).
The legacy :root block below still defines additional --sumi-* vars (typography, motion,
z-index, glass) pending migration to tokens.json (Sprint 2 follow-up). */
@import '@veza/design-system/tokens.css';
@custom-variant dark (&:is([data-theme="dark"] *));
/*

View file

@ -13,7 +13,10 @@
"./tokens/spacing": "./src/tokens/spacing.ts",
"./tokens/motion": "./src/tokens/motion.ts",
"./tokens.css": "./dist/tokens.css",
"./dist/tokens.ts": "./dist/tokens.ts"
"./tokens-generated": {
"types": "./dist/tokens.d.ts",
"default": "./dist/tokens.ts"
}
},
"files": [
"src/",