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:
parent
a25ad2e0b4
commit
cfbc110be6
11 changed files with 80 additions and 41 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function BarChart({
|
|||
data,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
color = '#7c9dd6',
|
||||
color = 'var(--sumi-viz-indigo)',
|
||||
showGrid = true,
|
||||
showValues = false,
|
||||
height = 300,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function LineChart({
|
|||
data,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
color = '#7c9dd6',
|
||||
color = 'var(--sumi-viz-indigo)',
|
||||
showGrid = true,
|
||||
showDots = true,
|
||||
height = 300,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"] *));
|
||||
|
||||
/* ╔══════════════════════════════════════════════════════════════════════════╗
|
||||
|
|
|
|||
|
|
@ -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/",
|
||||
|
|
|
|||
Loading…
Reference in a new issue