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>
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
<meta name="description" content="HiHala Data Dashboard — Event analytics, visitor tracking, and revenue insights" />
|
<meta name="description" content="HiHala Data Dashboard — Event analytics, visitor tracking, and revenue insights" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=IBM+Plex+Sans+Arabic:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Outfit:wght@300;400;500;600;700&family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&display=swap">
|
||||||
<title>HiHala Data</title>
|
<title>HiHala Data</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+35
-18
@@ -23,13 +23,11 @@
|
|||||||
--text-secondary: #334155;
|
--text-secondary: #334155;
|
||||||
--text-muted: #64748b;
|
--text-muted: #64748b;
|
||||||
--accent: #2563eb;
|
--accent: #2563eb;
|
||||||
--primary: #2563eb;
|
|
||||||
--accent-light: #dbeafe;
|
--accent-light: #dbeafe;
|
||||||
--success: #059669;
|
--success: #059669;
|
||||||
--success-light: #d1fae5;
|
--success-light: #d1fae5;
|
||||||
--danger: #dc2626;
|
--danger: #dc2626;
|
||||||
--danger-light: #fee2e2;
|
--danger-light: #fee2e2;
|
||||||
--gold: #b8860b;
|
|
||||||
--brand-icon: #3b82f6;
|
--brand-icon: #3b82f6;
|
||||||
--brand-text: #1e3a5f;
|
--brand-text: #1e3a5f;
|
||||||
--text-inverse: #ffffff;
|
--text-inverse: #ffffff;
|
||||||
@@ -44,6 +42,10 @@
|
|||||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
||||||
--shadow: 0 4px 12px rgba(0,0,0,0.08);
|
--shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||||
--radius: 12px;
|
--radius: 12px;
|
||||||
|
--hover: var(--muted-light);
|
||||||
|
--bg-primary: var(--surface);
|
||||||
|
--bg-secondary: var(--bg);
|
||||||
|
--bg-tertiary: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode */
|
/* Dark mode */
|
||||||
@@ -56,7 +58,6 @@
|
|||||||
--text-secondary: #cbd5e1;
|
--text-secondary: #cbd5e1;
|
||||||
--text-muted: #94a3b8;
|
--text-muted: #94a3b8;
|
||||||
--accent: #3b82f6;
|
--accent: #3b82f6;
|
||||||
--primary: #3b82f6;
|
|
||||||
--accent-light: #1e3a5f;
|
--accent-light: #1e3a5f;
|
||||||
--success: #34d399;
|
--success: #34d399;
|
||||||
--success-light: #064e3b;
|
--success-light: #064e3b;
|
||||||
@@ -88,7 +89,6 @@
|
|||||||
--text-secondary: #cbd5e1;
|
--text-secondary: #cbd5e1;
|
||||||
--text-muted: #94a3b8;
|
--text-muted: #94a3b8;
|
||||||
--accent: #3b82f6;
|
--accent: #3b82f6;
|
||||||
--primary: #3b82f6;
|
|
||||||
--accent-light: #1e3a5f;
|
--accent-light: #1e3a5f;
|
||||||
--success: #34d399;
|
--success: #34d399;
|
||||||
--success-light: #064e3b;
|
--success-light: #064e3b;
|
||||||
@@ -226,7 +226,7 @@ html[dir="rtl"] {
|
|||||||
|
|
||||||
.empty-state-action {
|
.empty-state-action {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: var(--primary);
|
background: var(--accent);
|
||||||
color: var(--text-inverse);
|
color: var(--text-inverse);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -1089,7 +1089,7 @@ table tbody tr:hover {
|
|||||||
.drp-custom-field input[type="date"]:focus {
|
.drp-custom-field input[type="date"]:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.12);
|
box-shadow: 0 0 0 2px var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.drp-custom-sep {
|
.drp-custom-sep {
|
||||||
@@ -1127,7 +1127,7 @@ table tbody tr:hover {
|
|||||||
.drp-apply {
|
.drp-apply {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
color: #fff;
|
color: var(--text-inverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
.drp-apply:hover {
|
.drp-apply:hover {
|
||||||
@@ -1301,7 +1301,7 @@ table tbody tr:hover {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: white;
|
color: var(--text-inverse);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 150ms ease;
|
transition: opacity 150ms ease;
|
||||||
}
|
}
|
||||||
@@ -1398,8 +1398,8 @@ table tbody tr:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.access-badge--full {
|
.access-badge--full {
|
||||||
background: #d1fae5;
|
background: var(--success-light);
|
||||||
color: #065f46;
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-small {
|
.btn-small {
|
||||||
@@ -1418,7 +1418,7 @@ table tbody tr:hover {
|
|||||||
|
|
||||||
.btn-small.btn-primary {
|
.btn-small.btn-primary {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: white;
|
color: var(--text-inverse);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1758,7 +1758,7 @@ tr.editing td {
|
|||||||
|
|
||||||
.chart-metric-selector button.active {
|
.chart-metric-selector button.active {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
color: var(--primary);
|
color: var(--accent);
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -1792,7 +1792,7 @@ tr.editing td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.carousel:focus-visible {
|
.carousel:focus-visible {
|
||||||
outline: 2px solid var(--primary);
|
outline: 2px solid var(--accent);
|
||||||
outline-offset: 4px;
|
outline-offset: 4px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
@@ -1877,7 +1877,7 @@ tr.editing td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.carousel-dot.active {
|
.carousel-dot.active {
|
||||||
background: var(--primary);
|
background: var(--accent);
|
||||||
width: 24px;
|
width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1903,14 +1903,14 @@ tr.editing td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.carousel-dots.labeled .carousel-dot:hover {
|
.carousel-dots.labeled .carousel-dot:hover {
|
||||||
border-color: var(--primary);
|
border-color: var(--accent);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.carousel-dots.labeled .carousel-dot.active {
|
.carousel-dots.labeled .carousel-dot.active {
|
||||||
width: auto;
|
width: auto;
|
||||||
background: var(--primary);
|
background: var(--accent);
|
||||||
border-color: var(--primary);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.carousel-dots.labeled .carousel-dot.active .dot-label {
|
.carousel-dots.labeled .carousel-dot.active .dot-label {
|
||||||
@@ -2082,7 +2082,7 @@ tr.editing td {
|
|||||||
|
|
||||||
.mobile-nav-item:hover,
|
.mobile-nav-item:hover,
|
||||||
.mobile-nav-item.active {
|
.mobile-nav-item.active {
|
||||||
color: var(--primary);
|
color: var(--accent);
|
||||||
background: rgba(37, 99, 235, 0.08);
|
background: rgba(37, 99, 235, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2833,6 +2833,23 @@ html[dir="rtl"] .exportable-chart-wrapper .chart-export-btn.visible {
|
|||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Focus Visible — Keyboard accessibility
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.drp-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.drp-year-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.drp-cancel:focus-visible,
|
||||||
|
.drp-apply:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.nav-refresh-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.nav-lang-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.multi-select-trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
|
||||||
|
.toggle-switch button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.chart-metric-selector button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.nav-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.controls-reset:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.alt-filter-reset:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
Reduced Motion
|
Reduced Motion
|
||||||
======================================== */
|
======================================== */
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ function AltMultiSelect({ value, options, onChange, allLabel, countLabel, clearL
|
|||||||
<div className="altms-dropdown" role="listbox" aria-multiselectable="true">
|
<div className="altms-dropdown" role="listbox" aria-multiselectable="true">
|
||||||
<div className="altms-list">
|
<div className="altms-list">
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
<label key={opt} className={`altms-option${value.includes(opt)?' altms-option--checked':''}`}>
|
<label key={opt} role="option" aria-selected={value.includes(opt)} className={`altms-option${value.includes(opt)?' altms-option--checked':''}`}>
|
||||||
<input type="checkbox" className="altms-check" checked={value.includes(opt)} onChange={() => toggle(opt)} aria-label={opt} />
|
<input type="checkbox" className="altms-check" checked={value.includes(opt)} onChange={() => toggle(opt)} aria-label={opt} />
|
||||||
<span className="altms-check-box">{value.includes(opt) && <svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4L3.5 6.5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>}</span>
|
<span className="altms-check-box">{value.includes(opt) && <svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4L3.5 6.5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>}</span>
|
||||||
<span className="altms-opt-label">{opt}</span>
|
<span className="altms-opt-label">{opt}</span>
|
||||||
@@ -611,6 +611,15 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM
|
|||||||
.alt-page-title { font-size:1.75rem; }
|
.alt-page-title { font-size:1.75rem; }
|
||||||
.alt-chart-header { flex-direction:column; }
|
.alt-chart-header { flex-direction:column; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── focus-visible ── */
|
||||||
|
.alt-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.alt-ctrl:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.alt-yr-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.dalt-hero-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.altms-trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.altms-clear:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.alt-cancel:focus-visible, .alt-apply:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
<Link to={L.backTo} className="alt-back">
|
<Link to={L.backTo} className="alt-back">
|
||||||
@@ -659,9 +668,9 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM
|
|||||||
<div className="alt-chart-header">
|
<div className="alt-chart-header">
|
||||||
<h3 className="alt-chart-title">{L.trendTitle}</h3>
|
<h3 className="alt-chart-title">{L.trendTitle}</h3>
|
||||||
<div className="alt-chart-controls">
|
<div className="alt-chart-controls">
|
||||||
{metricOpts.map(o => <button key={o.value} type="button" className={`alt-ctrl${metric===o.value?' alt-ctrl-on':''}`} onClick={() => setMetric(o.value)}>{o.label}</button>)}
|
{metricOpts.map(o => <button key={o.value} type="button" aria-pressed={metric===o.value} className={`alt-ctrl${metric===o.value?' alt-ctrl-on':''}`} onClick={() => setMetric(o.value)}>{o.label}</button>)}
|
||||||
<div className="alt-ctrl-sep" />
|
<div className="alt-ctrl-sep" />
|
||||||
{granOpts.map(o => <button key={o.value} type="button" className={`alt-ctrl${gran===o.value?' alt-ctrl-on':''}`} onClick={() => setGran(o.value)}>{o.label}</button>)}
|
{granOpts.map(o => <button key={o.value} type="button" aria-pressed={gran===o.value} className={`alt-ctrl${gran===o.value?' alt-ctrl-on':''}`} onClick={() => setGran(o.value)}>{o.label}</button>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="alt-chart-wrap"><Line data={trendData} options={chartOpts} /></div>
|
<div className="alt-chart-wrap"><Line data={trendData} options={chartOpts} /></div>
|
||||||
@@ -670,7 +679,7 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM
|
|||||||
<div className="alt-chart-header">
|
<div className="alt-chart-header">
|
||||||
<h3 className="alt-chart-title">{L.museumTitle}</h3>
|
<h3 className="alt-chart-title">{L.museumTitle}</h3>
|
||||||
<div className="alt-chart-controls">
|
<div className="alt-chart-controls">
|
||||||
{metricOpts.map(o => <button key={o.value} type="button" className={`alt-ctrl${metric===o.value?' alt-ctrl-on':''}`} onClick={() => setMetric(o.value)}>{o.label}</button>)}
|
{metricOpts.map(o => <button key={o.value} type="button" aria-pressed={metric===o.value} className={`alt-ctrl${metric===o.value?' alt-ctrl-on':''}`} onClick={() => setMetric(o.value)}>{o.label}</button>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="alt-chart-wrap"><Bar data={museumData} options={chartOpts} /></div>
|
<div className="alt-chart-wrap"><Bar data={museumData} options={chartOpts} /></div>
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ function AltMultiSelect({ value, options, onChange, allLabel, countLabel, clearL
|
|||||||
<div className="altms-dropdown" role="listbox" aria-multiselectable="true">
|
<div className="altms-dropdown" role="listbox" aria-multiselectable="true">
|
||||||
<div className="altms-list">
|
<div className="altms-list">
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
<label key={opt} className={`altms-option${value.includes(opt)?' altms-option--checked':''}`}>
|
<label key={opt} role="option" aria-selected={value.includes(opt)} className={`altms-option${value.includes(opt)?' altms-option--checked':''}`}>
|
||||||
<input type="checkbox" className="altms-check" checked={value.includes(opt)} onChange={() => toggle(opt)} aria-label={opt} />
|
<input type="checkbox" className="altms-check" checked={value.includes(opt)} onChange={() => toggle(opt)} aria-label={opt} />
|
||||||
<span className="altms-check-box">{value.includes(opt) && <svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4L3.5 6.5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>}</span>
|
<span className="altms-check-box">{value.includes(opt) && <svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4L3.5 6.5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>}</span>
|
||||||
<span className="altms-opt-label">{opt}</span>
|
<span className="altms-opt-label">{opt}</span>
|
||||||
@@ -617,6 +617,15 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set
|
|||||||
.alt-page-title { font-size:1.75rem; }
|
.alt-page-title { font-size:1.75rem; }
|
||||||
.altms-label { max-width:100px; }
|
.altms-label { max-width:100px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── focus-visible ── */
|
||||||
|
.alt-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.alt-ctrl:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.alt-yr-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.dalt-hero-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.altms-trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.altms-clear:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.alt-cancel:focus-visible, .alt-apply:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
<h1 className="alt-page-title">{L.pageTitle}</h1>
|
<h1 className="alt-page-title">{L.pageTitle}</h1>
|
||||||
@@ -656,9 +665,9 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set
|
|||||||
<div className="alt-chart-header">
|
<div className="alt-chart-header">
|
||||||
<h3 className="alt-chart-title">{L.trendTitle}</h3>
|
<h3 className="alt-chart-title">{L.trendTitle}</h3>
|
||||||
<div className="alt-chart-controls">
|
<div className="alt-chart-controls">
|
||||||
{metricOpts.map(o => <button key={o.value} type="button" className={`alt-ctrl${metric===o.value?' alt-ctrl-on':''}`} onClick={() => setMetric(o.value)}>{o.label}</button>)}
|
{metricOpts.map(o => <button key={o.value} type="button" aria-pressed={metric===o.value} className={`alt-ctrl${metric===o.value?' alt-ctrl-on':''}`} onClick={() => setMetric(o.value)}>{o.label}</button>)}
|
||||||
<div className="alt-ctrl-sep" />
|
<div className="alt-ctrl-sep" />
|
||||||
{granOpts.map(o => <button key={o.value} type="button" className={`alt-ctrl${gran===o.value?' alt-ctrl-on':''}`} onClick={() => setGran(o.value)}>{o.label}</button>)}
|
{granOpts.map(o => <button key={o.value} type="button" aria-pressed={gran===o.value} className={`alt-ctrl${gran===o.value?' alt-ctrl-on':''}`} onClick={() => setGran(o.value)}>{o.label}</button>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="alt-chart-wrap alt-chart-wrap--tall"><Line data={trendData} options={chartOpts} /></div>
|
<div className="alt-chart-wrap alt-chart-wrap--tall"><Line data={trendData} options={chartOpts} /></div>
|
||||||
@@ -668,13 +677,13 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set
|
|||||||
<div className="alt-chart-header">
|
<div className="alt-chart-header">
|
||||||
<h3 className="alt-chart-title">{L.museumTitle}</h3>
|
<h3 className="alt-chart-title">{L.museumTitle}</h3>
|
||||||
<div className="alt-chart-controls">
|
<div className="alt-chart-controls">
|
||||||
{metricOpts.map(o => <button key={o.value} type="button" className={`alt-ctrl${metric===o.value?' alt-ctrl-on':''}`} onClick={() => setMetric(o.value)}>{o.label}</button>)}
|
{metricOpts.map(o => <button key={o.value} type="button" aria-pressed={metric===o.value} className={`alt-ctrl${metric===o.value?' alt-ctrl-on':''}`} onClick={() => setMetric(o.value)}>{o.label}</button>)}
|
||||||
<div className="alt-ctrl-sep" />
|
<div className="alt-ctrl-sep" />
|
||||||
<button type="button" className={`alt-ctrl${museumChartType==='bar'?' alt-ctrl-on':''}`} onClick={() => setMuseumChartType('bar')}>{L.barLabel}</button>
|
<button type="button" aria-pressed={museumChartType==='bar'} className={`alt-ctrl${museumChartType==='bar'?' alt-ctrl-on':''}`} onClick={() => setMuseumChartType('bar')}>{L.barLabel}</button>
|
||||||
<button type="button" className={`alt-ctrl${museumChartType==='pie'?' alt-ctrl-on':''}`} onClick={() => setMuseumChartType('pie')}>{L.pieLabel}</button>
|
<button type="button" aria-pressed={museumChartType==='pie'} className={`alt-ctrl${museumChartType==='pie'?' alt-ctrl-on':''}`} onClick={() => setMuseumChartType('pie')}>{L.pieLabel}</button>
|
||||||
<div className="alt-ctrl-sep" />
|
<div className="alt-ctrl-sep" />
|
||||||
<button type="button" className={`alt-ctrl${museumDisplayMode==='absolute'?' alt-ctrl-on':''}`} onClick={() => setMuseumDisplayMode('absolute')}>{L.absLabel}</button>
|
<button type="button" aria-pressed={museumDisplayMode==='absolute'} className={`alt-ctrl${museumDisplayMode==='absolute'?' alt-ctrl-on':''}`} onClick={() => setMuseumDisplayMode('absolute')}>{L.absLabel}</button>
|
||||||
<button type="button" className={`alt-ctrl${museumDisplayMode==='percent'?' alt-ctrl-on':''}`} onClick={() => setMuseumDisplayMode('percent')}>{L.pctLabel}</button>
|
<button type="button" aria-pressed={museumDisplayMode==='percent'} className={`alt-ctrl${museumDisplayMode==='percent'?' alt-ctrl-on':''}`} onClick={() => setMuseumDisplayMode('percent')}>{L.pctLabel}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="alt-chart-wrap alt-chart-wrap--tall">
|
<div className="alt-chart-wrap alt-chart-wrap--tall">
|
||||||
@@ -686,13 +695,13 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set
|
|||||||
<div className="alt-chart-header">
|
<div className="alt-chart-header">
|
||||||
<h3 className="alt-chart-title">{L.channelTitle}</h3>
|
<h3 className="alt-chart-title">{L.channelTitle}</h3>
|
||||||
<div className="alt-chart-controls">
|
<div className="alt-chart-controls">
|
||||||
{metricOpts.map(o => <button key={o.value} type="button" className={`alt-ctrl${metric===o.value?' alt-ctrl-on':''}`} onClick={() => setMetric(o.value)}>{o.label}</button>)}
|
{metricOpts.map(o => <button key={o.value} type="button" aria-pressed={metric===o.value} className={`alt-ctrl${metric===o.value?' alt-ctrl-on':''}`} onClick={() => setMetric(o.value)}>{o.label}</button>)}
|
||||||
<div className="alt-ctrl-sep" />
|
<div className="alt-ctrl-sep" />
|
||||||
<button type="button" className={`alt-ctrl${channelChartType==='bar'?' alt-ctrl-on':''}`} onClick={() => setChannelChartType('bar')}>{L.barLabel}</button>
|
<button type="button" aria-pressed={channelChartType==='bar'} className={`alt-ctrl${channelChartType==='bar'?' alt-ctrl-on':''}`} onClick={() => setChannelChartType('bar')}>{L.barLabel}</button>
|
||||||
<button type="button" className={`alt-ctrl${channelChartType==='pie'?' alt-ctrl-on':''}`} onClick={() => setChannelChartType('pie')}>{L.pieLabel}</button>
|
<button type="button" aria-pressed={channelChartType==='pie'} className={`alt-ctrl${channelChartType==='pie'?' alt-ctrl-on':''}`} onClick={() => setChannelChartType('pie')}>{L.pieLabel}</button>
|
||||||
<div className="alt-ctrl-sep" />
|
<div className="alt-ctrl-sep" />
|
||||||
<button type="button" className={`alt-ctrl${channelDisplayMode==='absolute'?' alt-ctrl-on':''}`} onClick={() => setChannelDisplayMode('absolute')}>{L.absLabel}</button>
|
<button type="button" aria-pressed={channelDisplayMode==='absolute'} className={`alt-ctrl${channelDisplayMode==='absolute'?' alt-ctrl-on':''}`} onClick={() => setChannelDisplayMode('absolute')}>{L.absLabel}</button>
|
||||||
<button type="button" className={`alt-ctrl${channelDisplayMode==='percent'?' alt-ctrl-on':''}`} onClick={() => setChannelDisplayMode('percent')}>{L.pctLabel}</button>
|
<button type="button" aria-pressed={channelDisplayMode==='percent'} className={`alt-ctrl${channelDisplayMode==='percent'?' alt-ctrl-on':''}`} onClick={() => setChannelDisplayMode('percent')}>{L.pctLabel}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="alt-chart-wrap">
|
<div className="alt-chart-wrap">
|
||||||
@@ -704,13 +713,13 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set
|
|||||||
<div className="alt-chart-header">
|
<div className="alt-chart-header">
|
||||||
<h3 className="alt-chart-title">{L.districtTitle}</h3>
|
<h3 className="alt-chart-title">{L.districtTitle}</h3>
|
||||||
<div className="alt-chart-controls">
|
<div className="alt-chart-controls">
|
||||||
{metricOpts.map(o => <button key={o.value} type="button" className={`alt-ctrl${metric===o.value?' alt-ctrl-on':''}`} onClick={() => setMetric(o.value)}>{o.label}</button>)}
|
{metricOpts.map(o => <button key={o.value} type="button" aria-pressed={metric===o.value} className={`alt-ctrl${metric===o.value?' alt-ctrl-on':''}`} onClick={() => setMetric(o.value)}>{o.label}</button>)}
|
||||||
<div className="alt-ctrl-sep" />
|
<div className="alt-ctrl-sep" />
|
||||||
<button type="button" className={`alt-ctrl${districtChartType==='bar'?' alt-ctrl-on':''}`} onClick={() => setDistrictChartType('bar')}>{L.barLabel}</button>
|
<button type="button" aria-pressed={districtChartType==='bar'} className={`alt-ctrl${districtChartType==='bar'?' alt-ctrl-on':''}`} onClick={() => setDistrictChartType('bar')}>{L.barLabel}</button>
|
||||||
<button type="button" className={`alt-ctrl${districtChartType==='pie'?' alt-ctrl-on':''}`} onClick={() => setDistrictChartType('pie')}>{L.pieLabel}</button>
|
<button type="button" aria-pressed={districtChartType==='pie'} className={`alt-ctrl${districtChartType==='pie'?' alt-ctrl-on':''}`} onClick={() => setDistrictChartType('pie')}>{L.pieLabel}</button>
|
||||||
<div className="alt-ctrl-sep" />
|
<div className="alt-ctrl-sep" />
|
||||||
<button type="button" className={`alt-ctrl${districtDisplayMode==='absolute'?' alt-ctrl-on':''}`} onClick={() => setDistrictDisplayMode('absolute')}>{L.absLabel}</button>
|
<button type="button" aria-pressed={districtDisplayMode==='absolute'} className={`alt-ctrl${districtDisplayMode==='absolute'?' alt-ctrl-on':''}`} onClick={() => setDistrictDisplayMode('absolute')}>{L.absLabel}</button>
|
||||||
<button type="button" className={`alt-ctrl${districtDisplayMode==='percent'?' alt-ctrl-on':''}`} onClick={() => setDistrictDisplayMode('percent')}>{L.pctLabel}</button>
|
<button type="button" aria-pressed={districtDisplayMode==='percent'} className={`alt-ctrl${districtDisplayMode==='percent'?' alt-ctrl-on':''}`} onClick={() => setDistrictDisplayMode('percent')}>{L.pctLabel}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="alt-chart-wrap">
|
<div className="alt-chart-wrap">
|
||||||
|
|||||||
+60
-38
@@ -37,9 +37,21 @@ export const chartColors = {
|
|||||||
success: '#059669',
|
success: '#059669',
|
||||||
danger: '#dc2626',
|
danger: '#dc2626',
|
||||||
muted: '#94a3b8',
|
muted: '#94a3b8',
|
||||||
grid: '#f1f5f9'
|
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)
|
// Extended palette for charts with many categories (events, channels)
|
||||||
export const chartPalette = [
|
export const chartPalette = [
|
||||||
'#2563eb', // blue
|
'#2563eb', // blue
|
||||||
@@ -54,15 +66,15 @@ export const chartPalette = [
|
|||||||
'#ea580c', // orange
|
'#ea580c', // orange
|
||||||
];
|
];
|
||||||
|
|
||||||
export const createDataLabelConfig = (showDataLabels: boolean): any => ({
|
export const createDataLabelConfig = (showDataLabels: boolean, overrides?: { color?: string; backgroundColor?: string }): any => ({
|
||||||
display: showDataLabels,
|
display: showDataLabels,
|
||||||
color: '#1e293b',
|
color: overrides?.color ?? '#1e293b',
|
||||||
font: { size: 10, weight: 600 },
|
font: { size: 10, weight: 600 },
|
||||||
anchor: 'end',
|
anchor: 'end',
|
||||||
align: 'end',
|
align: 'end',
|
||||||
offset: 4,
|
offset: 4,
|
||||||
padding: 4,
|
padding: 4,
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.85)',
|
backgroundColor: overrides?.backgroundColor ?? 'rgba(255, 255, 255, 0.85)',
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
textDirection: 'ltr', // Force LTR for numbers - fixes RTL misalignment
|
textDirection: 'ltr', // Force LTR for numbers - fixes RTL misalignment
|
||||||
formatter: (value: number | null) => {
|
formatter: (value: number | null) => {
|
||||||
@@ -74,43 +86,53 @@ export const createDataLabelConfig = (showDataLabels: boolean): any => ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createBaseOptions = (showDataLabels: boolean): any => ({
|
export const createBaseOptions = (showDataLabels: boolean): any => {
|
||||||
responsive: true,
|
const theme = getChartTheme();
|
||||||
maintainAspectRatio: false,
|
return {
|
||||||
locale: 'en-US', // Force LTR number formatting
|
responsive: true,
|
||||||
layout: {
|
maintainAspectRatio: false,
|
||||||
padding: {
|
locale: 'en-US', // Force LTR number formatting
|
||||||
top: showDataLabels ? 25 : 5,
|
layout: {
|
||||||
right: 5,
|
padding: {
|
||||||
bottom: 5,
|
top: showDataLabels ? 25 : 5,
|
||||||
left: 5
|
right: 5,
|
||||||
}
|
bottom: 5,
|
||||||
},
|
left: 5
|
||||||
plugins: {
|
}
|
||||||
legend: { display: false },
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: '#1e293b',
|
|
||||||
padding: 12,
|
|
||||||
cornerRadius: 8,
|
|
||||||
titleFont: { size: 12 },
|
|
||||||
bodyFont: { size: 11 },
|
|
||||||
rtl: false,
|
|
||||||
textDirection: 'ltr'
|
|
||||||
},
|
},
|
||||||
datalabels: createDataLabelConfig(showDataLabels)
|
plugins: {
|
||||||
},
|
legend: { display: false },
|
||||||
scales: {
|
tooltip: {
|
||||||
x: {
|
backgroundColor: theme.surface,
|
||||||
grid: { display: false },
|
titleColor: theme.textPrimary,
|
||||||
ticks: { font: { size: 10 }, color: '#94a3b8' }
|
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',
|
||||||
|
})
|
||||||
},
|
},
|
||||||
y: {
|
scales: {
|
||||||
grid: { color: chartColors.grid },
|
x: {
|
||||||
ticks: { font: { size: 10 }, color: '#94a3b8' },
|
grid: { display: false },
|
||||||
border: { 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 = {
|
export const lineDatasetDefaults = {
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
|
|||||||
Reference in New Issue
Block a user