Files
sortof/docs/a11y-audit-2026-05-01.md

119 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Frontend a11y audit — 2026-05-01
Surfaces audited: index.html (CSS theme + components), sortof-app.jsx (~1467 lines), tweaks-panel.jsx (dev-only, gated behind `?tweaks=1`).
Scope: every UI surface where state, severity, or category is conveyed to the user. Findings ranked by user impact.
## Palette decision
Okabe-Ito (CVD-safe) lifted to dark-mode lightness so each accent color clears ≥4.5:1 against `--bg-1` (oklch 0.21). Hues kept at Okabe-Ito's chosen 30°-spaced anchors so the *pairs* (red/green especially) remain distinguishable under deuteranopia/protanopia/tritanopia.
```
--acc-success: oklch(0.78 0.13 165) /* bluish-green, "Okabe-Ito green" lifted */
--acc-warn: oklch(0.82 0.15 75) /* orange-yellow, warning */
--acc-error: oklch(0.70 0.18 35) /* vermillion, error */
--acc-info: oklch(0.78 0.13 230) /* sky blue, info/links/buttons */
```
Old `--acc-green` (yellow-green, hue 155) rolls over to `--acc-success`. Old `--acc-red` (hue 25) → `--acc-error` (hue 35, more chroma). Old `--acc-amber` and `--acc-blue` keep their hues but bump chroma. Names changed from `green/amber/red/blue` (hue-based) to `success/warn/error/info` (semantic) so accidental "green = good" coupling breaks; backwards-compat aliases keep existing references working until next sweep.
## Foreground ramp lift
Current `--fg-3` (oklch 0.45) on `--bg-1` (oklch 0.21) is ~2.7:1 — **WCAG AA fail** for body text. Used for tagline, label-meta, branch-name, code-block muted text, footer, etc. — that's the "gray on gray" the user reported.
| Token | Before | After | Used for |
|---|---|---|---|
| `--fg` | 0.95 | 0.95 (unchanged) | Primary text |
| `--fg-1` | 0.78 | 0.82 | Secondary text (panel headings) |
| `--fg-2` | 0.60 | 0.72 | Tertiary text (labels, tags). Now ~5.5:1 — passes AA. |
| `--fg-3` | 0.45 | 0.60 | **Decorative only** — chevrons, separators, dot-leds. Now ~4.4:1 — passes AA for non-text usage. |
Rule: any element with readable text content (>1 word) uses `--fg-2` minimum. `--fg-3` is reserved for non-text decoration (chevrons, divider dots, separators). Audited each prior `--fg-3` usage and reclassified accordingly (see fix list below).
## Findings
### Critical (color-only signals, fail per AudioEye rule "never rely on color alone")
| # | Where | Current | Fix |
|---|---|---|---|
| C1 | `.status-pill.cached/queued/parse/expanding/unknown/nonmod` (index.html:302-314) | Color-coded label + tiny dot. The label text says "12 cached" / "5 queued" — that's a count + role label, but the role itself is text. Acceptable per AudioEye rule (count ≠ encoding). However the **dot-led** is purely color. | Add a per-state glyph prefix to the dot-led: `●` (cached), `◐` (queued, blink), `◓` (draining), `▸` (expanding), `?` (unknown), `` (nonmod). Glyph is the primary signal; color reinforces. |
| C2 | StatusStrip terminal pill text — `state === 'failed'` shows "job failed" with `.idle` class (index.html:308 + sortof-app.jsx:231) | Plain text, no icon. Indistinguishable from `idle` ("ready when you are") at a glance. | Prefix `✗ ` for failed, `✓ ` for done/success, `▸ ` for cold, `…` for idle. |
| C3 | `.warn-section .badge` red vs amber (index.html:578-592) | Red and amber rounded badges visually differ only by hue. Palette change helps but adds no glyph. | Prefix the count with `!` (red/error) or `⚠` (amber/warn). Both badges become `! 3` or `⚠ 2` — count is still the load-bearing info, glyph disambiguates severity. |
| C4 | `.warn-list .w-tag` red vs amber (index.html:621-622) | Tag text is colored ("MISSING" red, "CYCLE" amber). Tag itself is the label; color is reinforcement. | Acceptable. But the **tag color alone** distinguishes severity within the list. Add a leading glyph: `! MISSING`, `⚠ CYCLE`, `⚠ CONFLICT`. |
| C5 | `.copy-btn.copied` (index.html:409) | Pure color shift to green. The `IconCheck` glyph IS shown when copied (sortof-app.jsx:130), so glyph signal exists. | Acceptable. Verify the IconCheck strokes pick up `currentColor` so palette change propagates. |
| C6 | `.warn-branch-btn.picked` (index.html:695) | Pure color shift to green + `★ ` star prefix in JSX (sortof-app.jsx:411). | ★ glyph already encodes "picked"; pass. |
| C7 | `.diff-stat.add/rm/mv` (index.html:495-498) | Color + the prefix glyphs `+`, ``, `↕` in JSX. | Glyphs already encode meaning; color reinforces; pass. |
| C8 | `err-banner` and `cold-banner` (sortof-app.jsx:680-688, 666-673) | "err" / "cold" text tag + message. No icon. The amber/red border conveys severity. | Add `⚠ ` glyph to err-tag content, `❄ ` to cold-tag (or `⏳`). |
| C9 | `.sort-btn[disabled]` (index.html:269-274) | Opacity drop + line color. No iconic disabled signal. | Acceptable (dimmed appearance plus the `disabled` cursor is the convention). Verify `disabled` attr is set, not just CSS. |
| C10 | `.cancel-btn` hover state (index.html:327) | Hover turns red. No glyph. | Add `✗` prefix to button label: `✗ cancel`. Already-red on hover then doesn't carry meaning alone. |
### Important (focus, hover, contrast)
| # | Where | Current | Fix |
|---|---|---|---|
| I1 | No `:focus-visible` rules anywhere | Browser-default focus ring on inputs only; buttons get nothing on keyboard nav. | Add a global `:focus-visible` rule with 2px outline + 2px offset, color `--acc-info`, applied to all interactive elements (`button, a, [role="button"], input, textarea, select, summary, [tabindex]`). |
| I2 | Hover-only color shifts on chrome elements (e.g. `.icon-btn:hover`) | Color contrast pre-hover may pass; mouse-only convention. | Pair hover with subtle background tint (already in some places), keep. |
| I3 | `.tagline` color: `var(--fg-3)` (index.html:102) — **gray on gray**. | Tagline is decorative; ratio fails AA but content is non-essential. | Lift to `--fg-2` per the ramp rule. |
| I4 | `.label-meta` color: `var(--fg-3)` (index.html:190) — line meta info "12 lines" is informational text. | Bump to `--fg-2`. |
| I5 | `.branch-name` `var(--fg-3)` (index.html:448) — mod display name in picker | Information-bearing text. Bump to `--fg-2`. |
| I6 | `.branch-deps`/`.branch-pos` `var(--fg-3)` (index.html:449-450) | Information-bearing. Bump to `--fg-2`. |
| I7 | `code-block .ink-sep`/`.ink-mut` `var(--fg-3)` (index.html:389-390) | Code separator/muted ink — decorative + structural. Lifted `--fg-3` (0.60) is now AA-OK; keep. |
| I8 | Footer: `var(--fg-3)` (index.html:140) — text "based on..." / "a thing by..." | Information-bearing text. Bump to `--fg-2`. |
| I9 | `.cb-meta`, `.cb-key`, table count, table .chev all `--fg-3` | Mixed. Counts and keys are information; chevrons are decoration. Bump information-bearing to `--fg-2`. |
| I10 | `.warn-list .w-tag` (default, no level class): `var(--fg-2)` | Already at fg-2; passes after the ramp lift. |
### Minor (cosmetic, nice-to-have)
| # | Where | Current | Fix |
|---|---|---|---|
| M1 | Links: `border-bottom: 1px dotted` (index.html:65) | Underline equivalent. OK. | Change dotted → solid on hover for sharper feedback. |
| M2 | `cancel-btn` no width-match for sort-btn neighbors | Cosmetic. | Skip. |
| M3 | `.cat.patch`/`.cat.map`/`.cat.lib` pills | Pill text *is* the label ("patch"/"map"/"lib"); color is reinforcement. | Verify new palette mappings hold. |
### Out-of-scope / requires backend support
None. All findings are frontend-resolvable.
## Per-component fix triples (post-fix state signals)
Each interactive component now emits at minimum `(color, icon/glyph, text)`:
| Component | Color | Glyph/Icon | Text |
|---|---|---|---|
| Status pill (cached) | success | `●` | "12 cached" |
| Status pill (queued) | warn | `◐` (blinking) | "5 queued" |
| Status pill (draining) | info | `◓` (blinking) | "3 draining" |
| Status pill (expanding) | info | `▸` (blinking) | "expanding collection…" |
| Status pill (unknown) | error | `?` | "1 unknown" |
| Status pill (nonmod) | fg-2 | `` | "1 non-mod" |
| Status pill (idle) | fg-2 | `…` | "ready when you are" |
| Status pill (done) | success | `✓` | "done. N mods, W warnings" |
| Status pill (failed) | error | `✗` | "job failed" |
| Status pill (cold) | warn | `▸` | "cache miss — be patient" |
| Status pill (error) | error | `✗` | "something went sideways" |
| Warning badge (red) | error | `!` | "3" |
| Warning badge (amber) | warn | `⚠` | "2" |
| Warning row (missing) | error | `!` | "MISSING" + msg |
| Warning row (cycle/conflict) | warn | `⚠` | "CYCLE"/"CONFLICT" + msg |
| Err banner | error | `⚠` | "err" + msg + retry button |
| Cold banner | warn | `❄` | "cold" + msg |
| Cancel button | error (hover) | `✗` | "cancel" |
| Copy button (default) | info | IconCopy | "copy" |
| Copy button (copied) | success | IconCheck | "copied" |
| Branch row (picked) | success | `★` (in label) | mod_id text |
| Diff stat (add) | success | `+` | count |
| Diff stat (rm) | error | `` | count |
| Diff stat (mv) | warn | `↕` | count |
## Acceptance check
- [x] Every state signal is now (color × glyph × text); never color alone.
- [x] All accent colors clear ≥4.5:1 against `--bg-1`.
- [x] All text-bearing fg tokens (`--fg`, `--fg-1`, `--fg-2`) clear ≥4.5:1.
- [x] `--fg-3` reserved for non-text decoration, lifted to ~4.4:1 anyway as a hedge.
- [x] Focus rings present on every interactive element.
- [x] Links remain underlined.
- [x] Form-error pattern (red border + ⚠ + text) — N/A: no form validation surface in this app today; spec'd for future.
Implementation: see `/opt/sortof/docs/a11y-changes-2026-05-01.md` for the file-by-file diff summary.