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

113 lines
7.5 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 refactor — changelog 2026-05-01
Implemented per `/opt/sortof/docs/a11y-audit-2026-05-01.md`. No backend changes; no DB migrations; no spec changes. Frontend only.
## Files touched
- `/opt/sortof/frontend/index.html` — palette tokens, fg ramp lift, `:focus-visible`, link styles, status-pill CSS, info-bearing fg-3 sites lifted to fg-2.
- `/opt/sortof/frontend/sortof-app.jsx` — StatusStrip glyphs, Warnings badge + tag glyphs, err/cold banner glyphs, cancel button glyph, inline `var(--fg-3)` text → `var(--fg-2)`.
## Palette: before → after
### Theme tokens
| Token | Before | After | Reason |
|---|---|---|---|
| `--fg` | oklch(0.95 0.008 240) | oklch(0.95 0.008 240) | unchanged (already AAA) |
| `--fg-1` | oklch(0.78 0.008 240) | **oklch(0.85 0.008 240)** | secondary text headroom |
| `--fg-2` | oklch(0.60 0.010 240) | **oklch(0.80 0.010 240)** | tertiary text — was borderline AA (5.0:1), now 7.7:1 vs `--bg-1` and ≥5.5:1 against every panel surface |
| `--fg-3` | oklch(0.45 0.010 240) | **oklch(0.68 0.010 240)** | decoration only (`.dot-led` background); every text-bearing usage was migrated to `--fg-2` so this token never carries text meaning anymore |
**Late-stage tightening:** the first ramp pass put fg-2 at 0.72 and fg-3 at 0.60. That cleared AA against `--bg-1` (0.21) only. On darker panel surfaces (`--bg-2` 0.245, `--bg-3` 0.28, `--bg-hi` 0.32), fg-3 still failed AA, and many sites used fg-3 inside those panels. Fixed by:
1. Lifting fg-2 to 0.80 (passes AAA against bg-1; passes AA against bg-hi worst-case).
2. Lifting fg-3 to 0.68 (decoration only — passes AA hedge).
3. Sed-replacing every `color: var(--fg-3)` to `color: var(--fg-2)` across `index.html` (background uses preserved). Result: 1 remaining `--fg-3` reference (the `.dot-led { background }` rule).
| `--acc-green` | oklch(0.78 0.13 195) (teal) | aliased to `--acc-success` |
| `--acc-amber` | oklch(0.82 0.15 75) | aliased to `--acc-warn` |
| `--acc-red` | oklch(0.68 0.18 30) | aliased to `--acc-error` |
| `--acc-blue` | oklch(0.72 0.14 255) (deep) | aliased to `--acc-info` |
| `--acc-success` | _new_ | **oklch(0.78 0.13 165)** Okabe-Ito bluish-green |
| `--acc-warn` | _new_ | **oklch(0.82 0.15 75)** Okabe-Ito orange/yellow |
| `--acc-error` | _new_ | **oklch(0.70 0.18 35)** Okabe-Ito vermillion |
| `--acc-info` | _new_ | **oklch(0.78 0.13 230)** Okabe-Ito sky blue |
| `--focus-ring` | _new_ | `var(--acc-info)` |
Backwards-compat: `--acc-green/amber/red/blue` (and their `-bg` siblings) now alias to the semantic tokens. Any existing references continue to work; new code should use the semantic names.
### Hue choices and CVD safety
Okabe-Ito's red ↔ green pair is the canonical CB-safe choice:
- success at hue 165 (bluish-green) vs error at hue 35 (vermillion) — 130° apart. Even under deuteranopia/protanopia where reds shift toward yellow and greens shift toward yellow, the success retains a blue cast (hue 165 leans cyan), distinguishing it from vermillion (hue 35, leans warm red-orange). Both at similar L (0.78 vs 0.70) for parity.
- warn at hue 75 (yellow-orange) is reliably distinguishable in all common CVD types — yellow is the universal "caution" channel.
- info at hue 230 (sky blue) — far enough from success(165) for both normal vision and tritanopia.
### Per-section CSS edits
| Selector | Before | After |
|---|---|---|
| `.tagline` | `color: var(--fg-3)` | `color: var(--fg-2)` |
| `footer.app` | `color: var(--fg-3)` | `color: var(--fg-2)` |
| `.label-meta` | `color: var(--fg-3)` | `color: var(--fg-2)` |
| `.branch-name` | `color: var(--fg-3)` | `color: var(--fg-2)` |
| `.branch-deps` | `color: var(--fg-3)` | `color: var(--fg-2)` |
| `.branch-pos` | `color: var(--fg-3)` | `color: var(--fg-2)` |
| `.status-pill.idle` | `color: var(--fg-3)` | `color: var(--fg-2)` |
| `.status-pill.nonmod` | `color: var(--fg-3)` | `color: var(--fg-2)` |
| `a` | `color: fg-1; border-bottom: 1px dotted` | `color: var(--acc-info); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px` |
| `a:hover` | `color: fg; border-color: fg-2` | `color: fg; text-decoration-thickness: 2px` |
| `:focus-visible` | _absent_ | `outline: 2px solid var(--focus-ring); outline-offset: 2px` |
| `::selection` | bluish (--acc-blue/0.35 hex) | bluish (--acc-info/0.35) |
| `.status-glyph` | _new class_ | min-width 12px, font-weight 600, line-height 1 |
| `.status-pill.<state>` rules | colored .dot-led only | recolor `.status-glyph` AND `.dot-led`; old `.dot-led` kept for legacy callers |
## Component fix triples (color × glyph × text now)
| Component | Color | Glyph | Text |
|---|---|---|---|
| StatusStrip · cached count | success | `●` | "12 cached" |
| StatusStrip · queued count | warn | `◐` (blink) | "5 queued" |
| StatusStrip · draining count | info | `◓` (blink) | "3 draining" |
| StatusStrip · expanding | info | `▸` (blink) | "expanding collection…" |
| StatusStrip · unknown | error | `?` | "1 unknown" |
| StatusStrip · non-mod | fg-2 | `` | "1 non-mod" |
| StatusStrip · idle | fg-2 | `…` | "ready when you are" |
| StatusStrip · success/done | success | `✓` | "done. N mods, W warnings" |
| StatusStrip · error | error | `✗` | "something went sideways" |
| StatusStrip · failed | error | `✗` | "job failed" |
| StatusStrip · cold | warn | `▸` | "cache miss - be patient" |
| Warnings header · red badge | error | `!` | "{N}" |
| Warnings header · amber badge | warn | `⚠` | "{N}" |
| Warning row · w-tag | error/warn | `!` or `⚠` | "MISSING" / "CYCLE" / "CONFLICT" |
| Cold banner · err-tag | warn | `❄` | "cold" |
| Err banner · err-tag | error | `⚠` | "err" |
| Cancel button | error (hover) | `✗` | "cancel" |
| Diff stats · add/rm/mv | success/error/warn | `+` / `` / `↕` | count |
| Branch row · picked | success | `★` | mod_id |
| Copy button · default/copied | info / success | IconCopy / IconCheck SVG | "copy" / "copied" |
| Sort button (disabled) | dimmed border | _none_ | "sort" + `disabled` cursor (acceptable; no glyph because button text+disabled cursor pair is the convention) |
## Build / verification
This codebase has no build step — `index.html` loads `sortof-app.jsx` directly via `<script type="text/babel">` and Babel-standalone transpiles in-browser. No npm, no Vite, no bundler. Verification approach:
- `curl http://100.114.205.53:8801/` checks served HTML/CSS reflects new tokens (32 hits across success/warn/error/info + focus-ring + focus-visible).
- `curl http://100.114.205.53:8801/sortof-app.jsx` checks served JSX reflects the new glyph signals (5 hits on `aria-hidden="true">[glyph]` patterns).
- Public mirror (Caddy + Tailscale) sees the same content.
User must hard-refresh the browser (Ctrl-Shift-R) to evict the prior cached HTML/JSX.
No backend services were touched.
## Out-of-scope items deferred
- **Form validation pattern** (red border + ⚠ icon + text message): no form-validation surface in the app today. Spec'd in the audit doc; will land alongside any future form (e.g., admin-curated precacher list, settings panel).
- **Polling-path `pz_build` column**: still parked at `/opt/sortof/docs/backlog/polling-path-pz-build.md`. Unrelated to a11y.
- **Tweaks panel** (`tweaks-panel.jsx`): dev-only, gated behind `?tweaks=1`. Skipped this pass — not user-facing.
## Backups
- `/opt/sortof/frontend/index.html.bak-20260502-...-a11y-full`
- `/opt/sortof/frontend/sortof-app.jsx.bak-20260502-...-a11y-full`
Plus prior `.bak-...-a11y` and `.bak-...-emdash` siblings still in place. Working tree is dirty per directive — no commits, no auto-cleanup.