9138ac1098
Addresses critical and high-severity findings from UI audit: - C1: Define missing CSS tokens (--hover, --bg-primary/secondary/tertiary) fixing broken hover states and Slides Builder backgrounds - C2: Chart colors now read CSS custom properties at render-time via getChartTheme(), adapting tooltip, ticks, and grid to dark mode - C3: Multi-select ARIA fixed — label elements now carry role="option" and aria-selected for valid listbox semantics - H1/M1: Remove unused --gold and duplicate --primary tokens; replace all var(--primary) with var(--accent) throughout App.css - H3/H4: Focus-visible outlines added to all custom interactive elements (chips, controls, year buttons, hero button, multi-select trigger) - H5: access-badge--full hardcoded colors replaced with design tokens - H7: aria-pressed added to all chart toggle buttons - L1: Hardcoded #fff/white replaced with var(--text-inverse) - M4: index.html now preloads DM Serif Display, Outfit, and IBM Plex Sans Arabic — all fonts actually used in the app Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
148 lines
3.6 KiB
TypeScript
148 lines
3.6 KiB
TypeScript
import {
|
|
Chart as ChartJS,
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
BarElement,
|
|
ArcElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
Filler
|
|
} from 'chart.js';
|
|
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
|
import Annotation from 'chartjs-plugin-annotation';
|
|
|
|
// Register ChartJS components once
|
|
ChartJS.register(
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
BarElement,
|
|
ArcElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
Filler,
|
|
ChartDataLabels,
|
|
Annotation
|
|
);
|
|
|
|
export const chartColors = {
|
|
primary: '#2563eb',
|
|
secondary: '#7c3aed',
|
|
tertiary: '#0891b2',
|
|
success: '#059669',
|
|
danger: '#dc2626',
|
|
muted: '#94a3b8',
|
|
grid: '#e2e8f0' // fallback only; use getChartTheme().border at runtime
|
|
};
|
|
|
|
export function getChartTheme() {
|
|
const style = getComputedStyle(document.documentElement);
|
|
const get = (v: string) => style.getPropertyValue(v).trim();
|
|
return {
|
|
surface: get('--surface') || '#ffffff',
|
|
textPrimary: get('--text-primary') || '#0f172a',
|
|
textMuted: get('--text-muted') || '#64748b',
|
|
border: get('--border') || '#e2e8f0',
|
|
textInverse: get('--text-inverse') || '#ffffff',
|
|
};
|
|
}
|
|
|
|
// Extended palette for charts with many categories (events, channels)
|
|
export const chartPalette = [
|
|
'#2563eb', // blue
|
|
'#7c3aed', // purple
|
|
'#0891b2', // cyan
|
|
'#059669', // emerald
|
|
'#d97706', // amber
|
|
'#e11d48', // rose
|
|
'#4f46e5', // indigo
|
|
'#0d9488', // teal
|
|
'#c026d3', // fuchsia
|
|
'#ea580c', // orange
|
|
];
|
|
|
|
export const createDataLabelConfig = (showDataLabels: boolean, overrides?: { color?: string; backgroundColor?: string }): any => ({
|
|
display: showDataLabels,
|
|
color: overrides?.color ?? '#1e293b',
|
|
font: { size: 10, weight: 600 },
|
|
anchor: 'end',
|
|
align: 'end',
|
|
offset: 4,
|
|
padding: 4,
|
|
backgroundColor: overrides?.backgroundColor ?? 'rgba(255, 255, 255, 0.85)',
|
|
borderRadius: 3,
|
|
textDirection: 'ltr', // Force LTR for numbers - fixes RTL misalignment
|
|
formatter: (value: number | null) => {
|
|
if (value == null) return '';
|
|
if (value >= 1000000) return (value / 1000000).toFixed(2) + 'M';
|
|
if (value >= 1000) return (value / 1000).toFixed(2) + 'K';
|
|
if (value < 100 && value > 0) return value.toFixed(2);
|
|
return Math.round(value).toLocaleString();
|
|
}
|
|
});
|
|
|
|
export const createBaseOptions = (showDataLabels: boolean): any => {
|
|
const theme = getChartTheme();
|
|
return {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
locale: 'en-US', // Force LTR number formatting
|
|
layout: {
|
|
padding: {
|
|
top: showDataLabels ? 25 : 5,
|
|
right: 5,
|
|
bottom: 5,
|
|
left: 5
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
backgroundColor: theme.surface,
|
|
titleColor: theme.textPrimary,
|
|
bodyColor: theme.textMuted,
|
|
borderColor: theme.border,
|
|
borderWidth: 1,
|
|
padding: 12,
|
|
cornerRadius: 8,
|
|
titleFont: { size: 12 },
|
|
bodyFont: { size: 11 },
|
|
rtl: false,
|
|
textDirection: 'ltr'
|
|
},
|
|
datalabels: createDataLabelConfig(showDataLabels, {
|
|
color: theme.textPrimary,
|
|
backgroundColor: theme.surface + 'dd',
|
|
})
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: { display: false },
|
|
ticks: { font: { size: 10 }, color: theme.textMuted }
|
|
},
|
|
y: {
|
|
grid: { color: theme.border },
|
|
ticks: { font: { size: 10 }, color: theme.textMuted },
|
|
border: { display: false }
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
export const lineDatasetDefaults = {
|
|
borderWidth: 2,
|
|
tension: 0.4,
|
|
fill: true,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 4
|
|
};
|
|
|
|
export const barDatasetDefaults = {
|
|
borderRadius: 4
|
|
};
|