119 lines
9.4 KiB
Markdown
119 lines
9.4 KiB
Markdown
# 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.
|