7.5 KiB
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, inlinevar(--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:
- Lifting fg-2 to 0.80 (passes AAA against bg-1; passes AA against bg-hi worst-case).
- Lifting fg-3 to 0.68 (decoration only — passes AA hedge).
- Sed-replacing every
color: var(--fg-3)tocolor: var(--fg-2)acrossindex.html(background uses preserved). Result: 1 remaining--fg-3reference (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.jsxchecks served JSX reflects the new glyph signals (5 hits onaria-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_buildcolumn: 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.