import { Badge } from '@/components/badge'; import { Button } from '@/components/button'; import { Spinner } from '@/components/spinner'; import { CheckIcon, RefreshIcon, XIcon } from '@/icons'; import { useMessages } from '~/i18n'; import { IMPACTS, useA11y, useA11yMode, type Impact } from '../hooks/use-a11y'; /** * The interactive half of the accessibility story — see `use-a11y.ts` for why * it exists and what it deliberately does not duplicate. * * It is dev-only and off by default, so it renders nothing until `?a11y=on` is * in the hash. The toolbar puts a switch there in development; the parameter * itself is what makes an audited page a link. */ /* Only the tint is fixed here; the word comes from the catalogue. Minor is neutral on purpose — four shades of alarm read as one. */ const TONE: Record = { critical: 'red', serious: 'red', moderate: 'yellow', minor: 'black', }; /** * Ring the offending element and scroll it into view. * * The outline is applied imperatively rather than through React state because * the target is not in this component's tree — it is somewhere inside `
`, * rendered by a registry entry that knows nothing about the panel. The class * itself lives in `showcase.css` beside the rest of the site's own styling. */ function highlight(target: string) { const node = document.querySelector(target); if (!node) return; node.scrollIntoView({ block: 'center', behavior: 'smooth' }); node.classList.add('a11y-target'); setTimeout(() => node.classList.remove('a11y-target'), 2_000); } function Violation({ violation }: { violation: ReturnType['violations'][number] }) { const m = useMessages(); return (
  • {m.a11y.impact[violation.impact]}

    {violation.help}

    {violation.id}

    {/* The selectors are the actionable part — one button per offending element, because a rule failing on six nodes is six separate places to look and a count would not tell you where any of them are. */}
      {violation.nodes.map((node) => (
    • ))}
    {m.a11y.reference}
  • ); } export function A11yPanel() { const a11y = useA11y(); const [, setMode] = useA11yMode(); const m = useMessages(); if (!a11y.enabled) return null; const total = a11y.violations.reduce((sum, violation) => sum + violation.nodes.length, 0); /* Grouped most-severe first rather than left in axe's own order, which is by rule id — the first thing to know is whether anything critical failed. */ const groups = IMPACTS.map( (impact) => [impact, a11y.violations.filter((v) => v.impact === impact)] as const ).filter(([, list]) => list.length > 0); return ( /* Fixed rather than in the flow: it audits `
    `, and putting it inside the thing it measures would make it part of its own result. */ ); }