You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Epic tracking the fixes from an app-wide accessibility audit (WCAG 2.2 AA) (Fable-5, 2026-07-05, static). Full findings (file:line + WCAG SC) in memory/fable_a11y_audit_2026-07-05.md.
Verdict
LexFlow is partially operable by keyboard and only marginally usable with a screen reader. The shell fundamentals are genuinely good (skip link, a global :focus-visible ring, a real useFocusTrap used by CommandPalette/ConfirmDialog (VforVitorio#714), an exemplary SpainMap, reduced-motion respected). But the two headline surfaces fail at the core: the Explorer desktop browse table — the primary way into 12k laws — is clickable <tr>s with no tabIndex/role/key handler, so a keyboard user cannot open a single law from it; and the streaming chat reply has no live region anywhere, so a screen-reader user who sends a question is met with total silence. Around those, a pattern repeats: hand-rolled overlays that declare aria-modal but never trap/move focus (only 2 of ~10 use the focus-trap), toggle controls whose state is colour-only, status changes never announced, and a set of non-existent Tailwind classes that silently no-op (amber-200/400/600 + shadcn tokens in SplashGate) breaking dark-mode contrast and the splash progress bar.
Top 3 highest-impact fixes
Make the primary data surfaces keyboard-real (S1) — law links inside Explorer rows (P0-1), Card renders a button when clickable (P1-13), keyboard path through tag autocomplete (P1-4). One small PR unlocks the core loop.
A live-region layer (S3) — a permanently-mounted polite region + role="log" on the chat thread + announced result counts. Converts chat and every error toast from silent to usable.
A shared dialog contract on the five remaining overlays (S2) — useFocusTrap + Escape + initial focus is already written; adoption is mechanical and kills the largest focus-barrier cluster (incl. the first-run ModelWizard gate).
Sprints (findings P0-1 … P3)
Sprint 1 — Keyboard reachability (the P0 core):
S1.1 a11y(explorer): browse table rows keyboard-operable (law name as <a>, or role=link+Enter/Space)[M][P0] — <tr onClick> with no tabIndex/role/handler (ExplorerPage.tsx:571-576); search-mode rows already do it right (:444-449). WCAG 2.1.1. (P0-1)
S1.2 a11y(ui): Card renders a button/anchor when onClick/href is passed[S] — Home "Recent laws" are clickable <div>s (HomePage.tsx:180, ui/Card.tsx:8-21). WCAG 2.1.1. (P1-13)
S1.3 a11y(explorer): keyboard path for #tag autocomplete (Arrow/Enter, listbox+activedescendant)[S] — suggestions bind onMouseDown only + vanish on blur (ExplorerPage.tsx:305-325). WCAG 2.1.1/4.1.2. (P1-4)
S1.4 a11y(chat): thread drawer inert/aria-hidden when closed (mobile)[S] — off-screen via -translate-x-full but focusable → tabbing through dozens of invisible thread buttons (ChatPage.tsx:266-272). WCAG 2.4.3. (P1-2)
S1.5 a11y(shell): single-key "g x" shortcut disable/remap setting[S] — no off mechanism (lib/hotkeys.ts:49-70); WCAG 2.1.4 requires it. + Space on role=button search rows (ExplorerPage.tsx:449). (P2-5, P3)
S2.2 a11y(help): HelpDrawer focus trap (focus-move + Escape already done in #714)[S] — Tab still exits into the page (HelpDrawer.tsx:179-181,219-229). (P3)
S2.3 a11y(menus): ModelChip + ExportMenu menu semantics + Toaster Escape scoping[S] — ModelChip no aria-haspopup/aria-expanded/Escape (ModelChip.tsx:21-52); Toaster global Escape clears toasts while a dialog consumes Escape (Toaster.tsx:40-46). (P2-14, P3)
Sprint 3 — Names, states & live regions (SR semantics):
S3.1 a11y(chat): role="log" aria-live on the thread + typing/completion announcements[M][P0] — no live region anywhere (ChatPage.tsx:366-386, ChatMessage.tsx:49-77); announce whole messages on commit, not per-token. WCAG 4.1.3. (P0-2)
S3.2 a11y(toaster): mount the aria-live container permanently (only children come/go)[S][P1] — if (toasts.length===0) return null before the live region → first toast of a burst not announced; toasts are the app's primary error channel (Toaster.tsx:48-54). WCAG 4.1.3. (P1-6)
S3.3 a11y(palette): combobox wiring (label input, aria-activedescendant, live count)[S] — no accessible name/role/activedescendant; arrowing announces nothing (CommandPalette.tsx:175-207). WCAG 4.1.2. (P1-5)
S3.4 a11y(forms): name every control + aria-invalid in Input[M][P1] — unnamed icon buttons (SettingsPage.tsx:327, LawDetailPage.tsx:309-311); placeholder-only/unlabeled inputs/select/slider/textarea (P1-10 list); Input.error is colour-only, no aria-invalid/aria-describedby (ui/Input.tsx:11-17). WCAG 3.3.1/3.3.2/4.1.2. (P1-3, P1-10, P1-11)
S3.5 a11y(primitives): aria-pressed/aria-current/radiogroup/fieldset sweep + result-count live regions + <html lang> sync[M] — segmented Tabs (P2-1), Chip active (P2-3), disabled Switch/Checkbox/Radio + radiogroup (P2-4), toggles (P2-6), FilterGroup legend (P2-12), nav aria-current (P2-19), result counts (P2-7), documentElement.lang on language change (P2-8). WCAG 1.3.1/4.1.2/4.1.3/3.1.1. (P2 cluster)
S3.6 a11y(reading): law heading levels + citation sup names (file AC into #793)[S] — LawMarkdown flattens h1-h6 to <strong> (LawMarkdown.tsx:24-36), gutter "Art. N" visual-only (ArticleBlock.tsx:26-31); citation sups named only "1"/"2" (ArticleBlock.tsx:69-92). Coordinate [epic] Law reading experience (Fable 5 audit) VforVitorio/LexFlow#793 (hierarchy rendering). WCAG 1.3.1/2.4.6. (P1-12, P2-16)
Epic tracking the fixes from an app-wide accessibility audit (WCAG 2.2 AA) (Fable-5, 2026-07-05, static). Full findings (file:line + WCAG SC) in
memory/fable_a11y_audit_2026-07-05.md.Verdict
LexFlow is partially operable by keyboard and only marginally usable with a screen reader. The shell fundamentals are genuinely good (skip link, a global
:focus-visiblering, a realuseFocusTrapused by CommandPalette/ConfirmDialog (VforVitorio#714), an exemplary SpainMap, reduced-motion respected). But the two headline surfaces fail at the core: the Explorer desktop browse table — the primary way into 12k laws — is clickable<tr>s with no tabIndex/role/key handler, so a keyboard user cannot open a single law from it; and the streaming chat reply has no live region anywhere, so a screen-reader user who sends a question is met with total silence. Around those, a pattern repeats: hand-rolled overlays that declarearia-modalbut never trap/move focus (only 2 of ~10 use the focus-trap), toggle controls whose state is colour-only, status changes never announced, and a set of non-existent Tailwind classes that silently no-op (amber-200/400/600 + shadcn tokens in SplashGate) breaking dark-mode contrast and the splash progress bar.Top 3 highest-impact fixes
Cardrenders a button when clickable (P1-13), keyboard path through tag autocomplete (P1-4). One small PR unlocks the core loop.role="log"on the chat thread + announced result counts. Converts chat and every error toast from silent to usable.useFocusTrap+ Escape + initial focus is already written; adoption is mechanical and kills the largest focus-barrier cluster (incl. the first-run ModelWizard gate).Sprints (findings P0-1 … P3)
Sprint 1 — Keyboard reachability (the P0 core):
a11y(explorer): browse table rows keyboard-operable (law name as <a>, or role=link+Enter/Space)[M] [P0] —<tr onClick>with no tabIndex/role/handler (ExplorerPage.tsx:571-576); search-mode rows already do it right (:444-449). WCAG 2.1.1. (P0-1)a11y(ui): Card renders a button/anchor when onClick/href is passed[S] — Home "Recent laws" are clickable<div>s (HomePage.tsx:180,ui/Card.tsx:8-21). WCAG 2.1.1. (P1-13)a11y(explorer): keyboard path for #tag autocomplete (Arrow/Enter, listbox+activedescendant)[S] — suggestions bindonMouseDownonly + vanish on blur (ExplorerPage.tsx:305-325). WCAG 2.1.1/4.1.2. (P1-4)a11y(chat): thread drawer inert/aria-hidden when closed (mobile)[S] — off-screen via-translate-x-fullbut focusable → tabbing through dozens of invisible thread buttons (ChatPage.tsx:266-272). WCAG 2.4.3. (P1-2)a11y(shell): single-key "g x" shortcut disable/remap setting[S] — no off mechanism (lib/hotkeys.ts:49-70); WCAG 2.1.4 requires it. + Space on role=button search rows (ExplorerPage.tsx:449). (P2-5, P3)Sprint 2 — Dialog & menu focus contract:
a11y(overlays): trap + Escape + initial focus on the 5 remaining aria-modal overlays[M] [P1] — Explorer mobile filter sheet (ExplorerPage.tsx:239-287), RightRail sheet (RightRail.tsx:61-95), ModelWizard first-run gate (ModelWizard.tsx:166-203), SemanticExplainDialog (SettingsPage.tsx:645-689), MCP add-server dialog (McpServersSection.tsx:436-478).useFocusTrapalready exists + restores focus. (a11y(app): AlertDialogs, accessible names & focus management VforVitorio/LexFlow#714 did CommandPalette/ConfirmDialog; editor 4 overlays → Epic: Editor drafting flow — safe persistence, precise insertion, visible annotations (Fable-5 audit 2026-07-05) VforVitorio/LexFlow#865 S4.4.) WCAG 2.4.3/2.1.2. (P1-1)a11y(help): HelpDrawer focus trap (focus-move + Escape already done in #714)[S] — Tab still exits into the page (HelpDrawer.tsx:179-181,219-229). (P3)a11y(menus): ModelChip + ExportMenu menu semantics + Toaster Escape scoping[S] — ModelChip noaria-haspopup/aria-expanded/Escape (ModelChip.tsx:21-52); Toaster global Escape clears toasts while a dialog consumes Escape (Toaster.tsx:40-46). (P2-14, P3)Sprint 3 — Names, states & live regions (SR semantics):
a11y(chat): role="log" aria-live on the thread + typing/completion announcements[M] [P0] — no live region anywhere (ChatPage.tsx:366-386,ChatMessage.tsx:49-77); announce whole messages on commit, not per-token. WCAG 4.1.3. (P0-2)a11y(toaster): mount the aria-live container permanently (only children come/go)[S] [P1] —if (toasts.length===0) return nullbefore the live region → first toast of a burst not announced; toasts are the app's primary error channel (Toaster.tsx:48-54). WCAG 4.1.3. (P1-6)a11y(palette): combobox wiring (label input, aria-activedescendant, live count)[S] — no accessible name/role/activedescendant; arrowing announces nothing (CommandPalette.tsx:175-207). WCAG 4.1.2. (P1-5)a11y(forms): name every control + aria-invalid in Input[M] [P1] — unnamed icon buttons (SettingsPage.tsx:327,LawDetailPage.tsx:309-311); placeholder-only/unlabeled inputs/select/slider/textarea (P1-10 list);Input.erroris colour-only, noaria-invalid/aria-describedby(ui/Input.tsx:11-17). WCAG 3.3.1/3.3.2/4.1.2. (P1-3, P1-10, P1-11)a11y(primitives): aria-pressed/aria-current/radiogroup/fieldset sweep + result-count live regions + <html lang> sync[M] — segmented Tabs (P2-1), Chip active (P2-3), disabled Switch/Checkbox/Radio + radiogroup (P2-4), toggles (P2-6), FilterGroup legend (P2-12), nav aria-current (P2-19), result counts (P2-7),documentElement.langon language change (P2-8). WCAG 1.3.1/4.1.2/4.1.3/3.1.1. (P2 cluster)a11y(reading): law heading levels + citation sup names (file AC into #793)[S] — LawMarkdown flattens h1-h6 to<strong>(LawMarkdown.tsx:24-36), gutter "Art. N" visual-only (ArticleBlock.tsx:26-31); citation sups named only "1"/"2" (ArticleBlock.tsx:69-92). Coordinate [epic] Law reading experience (Fable 5 audit) VforVitorio/LexFlow#793 (hierarchy rendering). WCAG 1.3.1/2.4.6. (P1-12, P2-16)Sprint 4 — Contrast, tokens & non-text alternatives:
a11y(tokens): fix the non-existent Tailwind utilities that silently no-op + rewrite SplashGate tokens[M] [P2] —tailwind.config.ts:50-54replaces the amber scale with only 300/500/700 →amber-200/400/600no-op (call sites:CitationCard.tsx:22,ArticleBlock.tsx:28,DiffPage.tsx:69,98,LawHeader.tsx:136,CommandPalette.tsx:212,SettingsPage.tsx:1071,1083,CommentsPanel.tsx:133,ExplorerPage.tsx:665,EditorPage.tsx:324); SplashGate uses foreign shadcn tokens (bg-background/text-foreground/bg-primary…SplashGate.tsx:65-116) → progress-bar fill invisible + retry button unstyled. Verify with a build. WCAG 1.4.3/1.4.11. (P2-10)a11y(contrast): amber-500/white chips, Badge amber, tiny muted text, dark focus ring[S] — white-on-amber-500 ≈2.0:1 (ExplorerPage.tsx:389-399,FilterRail.tsx:350-351); Badge amber ≈3.5-3.8:1 (ui/Badge.tsx:8); focus ring ≈2.6:1 on dark bg (index.css:105-109). WCAG 1.4.3/1.4.11/2.4.7. (P1-7, P2-9, P2-11)a11y(dataviz): Recharts accessibilityLayer/table fallback + graph canvas name + "ver como lista"[M] — Dashboards chart values hover-only, no text equivalent (DashboardPage.tsx:166-213); graph<canvas>nameless, nodes unreachable (GraphCanvas.tsx:247-328,GraphPage.tsx:172). Coordinate [epic] Graph VISUALIZATION — design, performance & UX (Fable 5 audit, 2026-07-03) VforVitorio/LexFlow#782 (viewer) + Epic: Trust & uncertainty communication (Fable-5 audit 2026-07-05) VforVitorio/LexFlow#847 (inferido). WCAG 1.1.1/2.1.1. (P1-8, P1-9)a11y(misc): VersionTimeline colour-only state + non-interactive when no onSelect[S] — current=ring only, kind=dot colour only, dead focusable dots on LawDetail (VersionTimeline.tsx:30-43,LawDetailPage.tsx:112). WCAG 1.4.1/4.1.2. (P2-17)Cross-references (not re-filed): CommandPalette trap/ConfirmDialog/focus-visible/editor input aria/HelpDrawer+TopBar aria → VforVitorio#714; editor overlay dialog-focus → VforVitorio#865 S4.4; EN/ES + label wording → VforVitorio#852; help-content lies + dead controls → VforVitorio#832 (S1.5/misc reference two more instances); law hierarchy rendering → VforVitorio#793 (S3.6 files a11y AC there); graph viewer/inferido → VforVitorio#782/VforVitorio#847 (S4.3).
Upstream: VforVitorio#889