refactor(report): full UX audit + accessibility pass
Deploy HiHala Dashboard / deploy (push) Successful in 11s
Deploy HiHala Dashboard / deploy (push) Successful in 11s
UI/UX redesign: - Module cards with master toggle + badge state for all report sections - BreakdownModule with indeterminate checkbox and metric pill sub-toggles - PillGroup replaces all text toggles and <select> (Language, VAT, Confidentiality, Trend metric, Orientation) for full visual consistency - Visual orientation picker (portrait/landscape card buttons) - Comparison period in accent-tinted block, revealed contextually - Footer meta strip: section count, date range, orientation, comparison flag - Removed generic subtitle copy Accessibility (audit findings C1–C3, H2, H6, L1–L2): - aria-pressed on all PillGroup and orientation buttons - role="group" + aria-label on every pill group and orientation row - aria-hidden on decorative module badges and footer separator dots - :focus-visible on rf-metric-pill, rf-orient-btn, rf-upload-btn, rf-remove-btn - aria-label on upload/remove logo buttons - Semantic <h2> elements replace <div> group labels - alert() replaced with inline role="alert" error messages in footer + logo field - aria-live="polite" sr-only region for PDF generation status - aria-busy on generate button during PDF creation Dark mode & theming (H1): - All rgba(37,99,235,...) hard-codes replaced with color-mix(in srgb, var(--accent) N%, transparent) so tints follow the accent token in dark mode - rf-module-header:hover uses var(--hover) instead of rgba(0,0,0,0.02) Performance (H8): - getUniqueMuseums/getUniqueChannels wrapped in useMemo([data]) PDF fixes: - ▲/▼ Unicode glyphs (outside Helvetica Latin-1 range) replaced with +/- prefix - Chart width adapts to orientation via CHART_W constant - Y-axis labels added to trend chart (padL 38pt) Responsive (H4–H5): - rf-metric-pill touch target increased to 8px/14px on mobile - Mobile footer shows section count only; period/orientation details hide Cleanup (M3): - Removed dead CSS: rf-toggle, rf-toggle-opt, rf-section-title, rf-check-h-group, rf-inline-row (7 rules) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Svg, Line, Polyline, Rect, Text as SvgText, G } from '@react-pdf/renderer';
|
||||
|
||||
export const CHART_PALETTE = [
|
||||
'#2563eb', '#0891b2', '#7c3aed', '#059669',
|
||||
'#d97706', '#dc2626', '#db2777', '#f59e0b',
|
||||
'#10b981', '#6366f1', '#0284c7', '#65a30d',
|
||||
];
|
||||
|
||||
function fmtAxis(v: number): string {
|
||||
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
|
||||
if (v >= 10_000) return `${Math.round(v / 1_000)}K`;
|
||||
if (v >= 1_000) return `${(v / 1_000).toFixed(1)}K`;
|
||||
return String(Math.round(v));
|
||||
}
|
||||
|
||||
interface TrendChartProps {
|
||||
labels: string[];
|
||||
current: number[];
|
||||
@@ -10,10 +23,11 @@ interface TrendChartProps {
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function PdfTrendChart({ labels, current, previous, color, width = 460, height = 140 }: TrendChartProps) {
|
||||
export function PdfTrendChart({ labels, current, previous, color, width = 470, height = 155 }: TrendChartProps) {
|
||||
const allValues = [...current, ...(previous ?? [])].filter(v => v > 0);
|
||||
const max = allValues.length > 0 ? Math.max(...allValues) : 1;
|
||||
const padL = 8, padR = 8, padT = 8, padB = 18; // padB=18 leaves room for x-axis labels
|
||||
// padL wide enough for y-axis labels like "1.2M"
|
||||
const padL = 38, padR = 8, padT = 10, padB = 20;
|
||||
const w = width - padL - padR;
|
||||
const h = height - padT - padB;
|
||||
|
||||
@@ -27,20 +41,38 @@ export function PdfTrendChart({ labels, current, previous, color, width = 460, h
|
||||
|
||||
return (
|
||||
<Svg width={width} height={height}>
|
||||
{gridLines.map(f => (
|
||||
<Line key={f}
|
||||
x1={padL} y1={sy(max * f).toFixed(1)}
|
||||
x2={width - padR} y2={sy(max * f).toFixed(1)}
|
||||
stroke="#e2e8f0" strokeWidth={0.5} />
|
||||
))}
|
||||
{/* Baseline */}
|
||||
<Line x1={padL} y1={(padT + h).toFixed(1)} x2={width - padR} y2={(padT + h).toFixed(1)}
|
||||
stroke="#cbd5e1" strokeWidth={0.75} />
|
||||
|
||||
{/* Grid lines + Y-axis labels */}
|
||||
{gridLines.map(f => {
|
||||
const yPos = sy(max * f);
|
||||
return (
|
||||
<G key={f}>
|
||||
<Line x1={padL} y1={yPos.toFixed(1)} x2={width - padR} y2={yPos.toFixed(1)}
|
||||
stroke="#e2e8f0" strokeWidth={0.5} />
|
||||
<SvgText x={(padL - 5).toFixed(1)} y={(yPos + 2.5).toFixed(1)}
|
||||
fill="#94a3b8" style={{ fontSize: 6.5, textAnchor: 'end' }}>
|
||||
{fmtAxis(max * f)}
|
||||
</SvgText>
|
||||
</G>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Comparison line (dashed) */}
|
||||
{previous && previous.some(v => v > 0) && (
|
||||
<Polyline points={toPoints(previous)}
|
||||
stroke="#94a3b8" strokeWidth={1.5} strokeDasharray="4 3" fill="none" />
|
||||
)}
|
||||
|
||||
{/* Current period line */}
|
||||
{current.some(v => v > 0) && (
|
||||
<Polyline points={toPoints(current)}
|
||||
stroke={color} strokeWidth={2.5} fill="none" />
|
||||
)}
|
||||
|
||||
{/* X-axis week labels */}
|
||||
{labels
|
||||
.filter((_, i) => labels.length <= 8 || i % Math.ceil(labels.length / 8) === 0)
|
||||
.map((label) => {
|
||||
@@ -60,15 +92,15 @@ export function PdfTrendChart({ labels, current, previous, color, width = 460, h
|
||||
interface HBarChartProps {
|
||||
items: Array<{ name: string; value: number }>;
|
||||
color: string;
|
||||
usepalette?: boolean;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export function PdfHBarChart({ items, color, width = 460 }: HBarChartProps) {
|
||||
const barH = 16;
|
||||
export function PdfHBarChart({ items, color, usepalette = false, width = 470 }: HBarChartProps) {
|
||||
const barH = 17;
|
||||
const gap = 10;
|
||||
const labelW = 150;
|
||||
const valueW = 70;
|
||||
const barAreaW = width - labelW - valueW - 8;
|
||||
const labelW = 160;
|
||||
const barAreaW = width - labelW - 20;
|
||||
const max = Math.max(...items.map(i => i.value), 1);
|
||||
const totalH = items.length * (barH + gap);
|
||||
|
||||
@@ -77,13 +109,21 @@ export function PdfHBarChart({ items, color, width = 460 }: HBarChartProps) {
|
||||
{items.map((item, i) => {
|
||||
const y = i * (barH + gap);
|
||||
const bw = Math.max((item.value / max) * barAreaW, 2);
|
||||
const shortName = item.name.length > 22 ? item.name.slice(0, 22) + '…' : item.name;
|
||||
const shortName = item.name.length > 26 ? item.name.slice(0, 26) + '…' : item.name;
|
||||
const valueStr = item.value.toLocaleString('en-SA', { maximumFractionDigits: 0 });
|
||||
const barColor = usepalette ? CHART_PALETTE[i % CHART_PALETTE.length] : color;
|
||||
const isShort = bw < 48;
|
||||
return (
|
||||
<G key={item.name}>
|
||||
<SvgText x={0} y={y + barH - 4} fill="#334155" style={{ fontSize: 8 }}>{shortName}</SvgText>
|
||||
<Rect x={labelW} y={y} width={bw} height={barH} fill={color} rx={3} />
|
||||
<SvgText x={labelW + bw + 4} y={y + barH - 4} fill="#64748b" style={{ fontSize: 8 }}>{valueStr}</SvgText>
|
||||
<G key={item.name + i}>
|
||||
<SvgText x={0} y={y + barH - 4} fill="#334155" style={{ fontSize: 8.5 }}>{shortName}</SvgText>
|
||||
<Rect x={labelW} y={y} width={bw} height={barH} fill={barColor} rx={3} />
|
||||
{isShort ? (
|
||||
<SvgText x={labelW + bw + 6} y={y + barH - 4} fill="#334155"
|
||||
style={{ fontSize: 8.5 }}>{valueStr}</SvgText>
|
||||
) : (
|
||||
<SvgText x={labelW + bw - 6} y={y + barH - 4} fill="#ffffff"
|
||||
style={{ fontSize: 8.5, textAnchor: 'end' }}>{valueStr}</SvgText>
|
||||
)}
|
||||
</G>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user