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
@@ -335,6 +335,30 @@ erase a user-selected accent or a theme switch to leave the border behind.
| `ChoiceRow { setting }` | An enum, as a segmented control |
| `ActionRow` | A button: opens a GNOME panel, runs a one-shot |
| `TextRow` | A genuinely read-only fact |
| `SettingsNote` | An inset paragraph: the explanation too long to be a `detail` |
`SettingsNote` is deliberately quiet and must stay that way. It explains
absences and boundaries — what this session cannot do, and why — and a
warning-coloured box would turn "not offered yet, here is the reason" into
"something is wrong here".
### The rows are keyboard-reachable
Every row primitive carries `Accessible.role`, an `Accessible.name` taken from
its label, and an `Accessible.description` taken from its detail; the
interactive element sets `activeFocusOnTab` and draws a two-pixel
`Theme.accentSecondary` ring **only** while it holds focus. Tab walks the page,
Space and Enter flip a switch or press a button, and Left/Right (and Up/Down)
step a slider by one schema step or move a segmented choice by one option,
never wrapping.
Two rules hold this together. An inert row is not a Tab stop: `SettingRow`
becomes focusable only when `activatable` is true, because Tab landing on
static text is how keyboard navigation stops being usable. And a keyboard
change commits through exactly the same path a pointer change does —
`SliderRow`'s arrows go through the same 140 ms debounce as a drag, so a held
arrow behaves like a drag rather than a burst of writes the compositor spends
the whole time rejecting.
`TextRow` is for facts, not for settings that were merely expensive to wire.
Before Stage 3 more than half of all rows were static text standing in for