Fold thirty-one settings pages into fifteen categories with tabs

The sidebar was a flat scan of thirty-one rows; now it reads like a
settings app. Multi-subject categories (Input, Network & Sharing,
Applications, Users & Accounts, Privacy & Security, System) carry an
Appearance-style tab strip above the page, drawn by the shell so the
leaf pages themselves are untouched. The taxonomy lives in one new
file, services/SettingsRoutes.qml; the sidebar, the strip, route
validation, search breadcrumbs, and both generators derive from it.

ShellState.settingsPage still holds leaf ids, so every deep link, IPC
call, and search result keeps working — and now lands on the exact
tab. Dictation moves out of Sound onto its own page under Input, with
a handoff back to Sound for the microphone. The strip scrolls when
System's nine tabs outgrow a tiled window. All 161 contracts pass.

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-23 20:21:31 -04:00
parent 50077a0c31
commit 5490fd285d
35 changed files with 816 additions and 288 deletions
@@ -0,0 +1,69 @@
# Settings navigation redesign — 31 flat pages become 15 categories
Approved 2026-08-23. The sidebar's 31 entries collapse into 15 categories. Multi-subject
categories render an Appearance-style tab strip above the page; single-subject categories
render no strip. Every old page id keeps working forever.
## The taxonomy
| Category (sidebar) | Tabs (leaf page ids) |
|---|---|
| Home | — (`home`) |
| Appearance | — (`appearance`, keeps its internal tabs) |
| Desktop & Dock | — (`desktop`) |
| Displays | — (`displays`) |
| Sound | — (`sound`, Dictation card moves out) |
| Notifications & Focus | — (`notifications`) |
| Input | Keyboard (`shortcuts`), Mouse & Touchpad (`mouse`), Dictation (`dictation`, new) |
| Network & Sharing | Connections (`connectivity`), Firewall (`firewall`), Sharing (`sharing`), Printers (`printers`) |
| Home & Phone | — (`home-phone`) |
| Applications | Applications (`applications`), Gaming (`gaming`), Screen Intelligence (`screen-intelligence`) |
| Users & Accounts | Users (`users`), Online Accounts (`accounts`) |
| Privacy & Security | Privacy (`privacy`), SSH Keys (`ssh-keys`) |
| Power & Lock | — (`power`) |
| Accessibility | — (`accessibility`) |
| System | About (`about`), Software Update (`updates`), System Health (`services`), Storage (`storage`), Snapshots (`snapshots`), Containers (`containers`), Date & Time (`datetime`), Region & Language (`region`), Manual (`manual`) |
## Architecture
`ShellState.settingsPage` keeps holding the **leaf** id, exactly as today. The page loader's
switch, the ownership contracts, HomePage's direct jumps, and every external caller are
untouched by the grouping itself. What changes:
- **`services/SettingsRoutes.qml` (new singleton)** — the one source of truth for the
taxonomy: `categories` (page, label, icon, tabs), `resolve(id)` (category id → first
available leaf; leaf → itself; unknown → home), `categoryOf(leaf)`, `tabsFor(leaf)`
(availability-filtered), `breadcrumb(leaf)`, `pageAvailable(leaf)` (the podman/snapper
gating, moved from the sidebar).
- **`SettingsSidebar.qml`** — destinations come from `SettingsRoutes.categories`; a row
highlights when it is the category owning the current leaf; clicking requests the
category id and `ShellState` resolves it.
- **`ShellState.showSettings`** — the literal allow-list becomes `SettingsRoutes.resolve()`.
- **`SettingsShell.qml`** — renders a `SettingsTabs` strip between the titlebar and the
page loader whenever the current leaf's category has more than one available tab.
Tab click is just `openSettings(leafId)`. Adds the `dictation` case.
- **`SettingsTabs.qml`** — the strip becomes horizontally scrollable when it overflows
(System has nine tabs; the window tiles down to 900px).
- **`SoundPage.qml` / `DictationPage.qml` (new)** — the Dictation card moves to its own
page under Input, with a labeled handoff noting the microphone is chosen in Sound.
- **`SettingsSearch.qml`** — dictation `extraEntries` re-route to `dictation`; the search
result subtitle becomes the `SettingsRoutes` breadcrumb ("System Storage"). Because
results carry leaf ids, search now lands on the exact tab with no further work.
## Blast radius owned by this change
- `scripts/panama-settings-commands` regex-parses the sidebar's destinations array; it
moves to parsing `SettingsRoutes.qml` and regenerates the Vicinae commands (one per
leaf; `home` still skipped; `dictation` gained). `scripts/panama-settings-docs` checked
the same way.
- Test contracts updated in the same change, not after: `settings-nav-contract`,
`settings-jump-contract`, `search-routing-contract`, `gnome-handoff-contract`,
`manual-contract`, `sound-page-contract`, plus any contract found grepping page ids.
- `modules/settings/README.md` gains the navigation model; manual chapter
`05-making-it-yours.md` updated where it describes the sidebar.
## Non-goals
- No per-tab deep links into Appearance's internal tabs from search (unchanged today).
- No persistence of last-selected tab per category (Appearance already resets; same rule).
- No renaming of leaf ids, files, or schema groups.