Make every settings row reachable, and every accessibility switch honest

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 21:03:36 -04:00
parent e1ff25fc66
commit 9ffaf45a4d
33 changed files with 2384 additions and 76 deletions
@@ -0,0 +1,70 @@
# Accessibility redesign — everything that's real
Approved mock: `home-mocks/accessibility.html` (scratchpad, :8642). Spec wins over mock on
conflict. Tabless leaf stays.
## Goals
1. **Magnifier keybinds**: Super+= / Super+- / Super+0 step the zoom from anywhere, through the
shell so the stored preference stays truthful and the OSD shows the level. Mechanism: the
binds call `qs ipc call accessibility zoom in|out|reset`; a new IPC target routes to
`Accessibility.stepZoom()`, which computes (×1.25 steps, clamp to the schema's 1.05.0),
commits through the normal verified-preference path, and posts the OSD. No new script; a
dead shell means dead zoom keys, which is honest (everything else in the shell is dead too).
2. **Reduce motion becomes true**: `Theme.qml`'s `dur*` tokens gate on
`Settings.animationsEnabled` (0 when off) so the bar/dock/OSD/panels genuinely still.
3. **Visual alerts**: new schema key `visualAlerts` (bool, def false, group `accessibility`);
when on, a one-shot screen-edge flash (per-screen overlay, single animation per event,
never looping) fires wherever `Notifs.playBell` decides a notification sounds (including
when the audible bell is skipped for `sound: false` apps? No — the flash follows the same
eligibility as the bell EXCEPT the eventSounds gate: visual alerts are for people who can't
hear the bell, so the flash fires on bell-eligible notifications even when event sounds are
off. Pin that.)
4. **Honest Orca control**: running state (pgrep poll on page open + after actions),
Start/Stop; the a11y-bus readiness line. No gsettings screen-reader toggle (gsd-owned,
inert — refused).
5. **The primitives get accessible**: `SliderRow`, `ToggleRow`, `SwitchRow`, `ActionRow`,
`ChoiceRow`, `SegmentRow`, `SettingRow` (activatable), `OptionPickerRow`, `PickerRow`
gain `Accessible.role`/`Accessible.name` (from label; detail as description),
`activeFocusOnTab` on the interactive element, key handling (Space/Enter activates,
Left/Right steps sliders and segments), and a visible focus indicator (the titlebar
close-button border pattern). No API or visual-at-rest changes.
6. **Page rebuilt** per mock: Vision / Motion / Hearing / Keyboard & pointer / Screen reader;
`dimStrength` gated on `dimInactive`; magnifier "1× is off" honesty (schema gains
`unit: "×"`-style display via the page detail — fix the dead `zeroLabel`); the honesty box
for sticky/slow/bounce with the probed-not-assumed copy; mono-audio NOT YET row; "Open
Keyboard" jump.
7. Cleanups: `DESKTOP-PARITY.md`'s stale AccessX claim corrected; search entries for
magnifier/zoom/orca/screen reader/high contrast/reduce motion/visual alerts.
Non-goals: mono audio (deferred with the on-page honesty), sticky/slow/bounce/hover/mouse
keys (refused with receipts), gsettings `enable-animations`/`screen-reader-enabled` writes
(inert), shell text scaling.
## Ownership
- **A**: `config/PreferenceSchema.qml` (visualAlerts; magnifierFactor detail/unit fix),
`config/dot/hypr/keybinds.lua` (three zoom binds — CHECK for collisions with existing
Super+=/-/0 binds first; pick free chords and update the mock copy via B if needed),
`shell.qml` (accessibility IPC target), `services/Accessibility.qml` (stepZoom, orcaRunning
+ start/stop, poll discipline), `services/Notifs.qml` (flash trigger signal per goal 3),
NEW `modules/notifications/VisualBell.qml` (+ per-screen wiring in shell.qml),
`config/dot/hypr/DESKTOP-PARITY.md` (stale claim), `services/OsdState.qml`/`OsdModel.js`
only if the zoom OSD needs a new kind (coordinate with C on osd-model-contract).
- **B**: the nine row primitives (accessibility additions only — zero API/visual-at-rest
change), `modules/settings/AccessibilityPage.qml` rebuild (+ any new components + qmldir).
Reuse KeycapChord for the chord display.
- **C**: `services/SettingsSearch.qml`, NEW `tests/quickshell/accessibility-contract` (page
structure; zoom IPC path commits through the verified-preference path and never calls
hyprctl keyword; visual bell one-shot — no `loops: Animation.Infinite`, no Timer-driven
repeat; flash follows bell eligibility but not the eventSounds gate; primitives carry
Accessible.name + activeFocusOnTab — sweep all nine files; Orca control uses process state,
never gsettings), `osd-model-contract` (zoom kind if added), reconcile any contract pinning
Theme durations or the primitives' file contents, backlog Phase 14, README count
(174 → 175 expected), docs/commands regen at the end (schema changed).
Hard rules: no live mutations (no zoom application, no orca start/stop, no gsettings writes,
no hyprctl keyword/reload); hermetic stubs only; valid QML at every save (primitives are used
by EVERY page — a broken SliderRow takes the whole settings app down, so B edits them one at
a time with a reload-check between each). B programs against A's pinned APIs; A updates this
spec before changing them.