diff --git a/docs/a11y-audit-2026-05-01.md b/docs/a11y-audit-2026-05-01.md deleted file mode 100644 index 3c34ba3..0000000 --- a/docs/a11y-audit-2026-05-01.md +++ /dev/null @@ -1,118 +0,0 @@ -# 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. diff --git a/docs/a11y-changes-2026-05-01.md b/docs/a11y-changes-2026-05-01.md deleted file mode 100644 index 2f679b8..0000000 --- a/docs/a11y-changes-2026-05-01.md +++ /dev/null @@ -1,112 +0,0 @@ -# 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.` 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 `