113 lines
7.5 KiB
Markdown
113 lines
7.5 KiB
Markdown
# 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.
|