Files
hihala-dashboard/src/config/chartConfig.ts
T
fahed 9138ac1098 fix: accessibility, theming, and focus-visibility improvements
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>
2026-04-26 15:46:54 +03:00

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
};