refactor(settings): regroup SettingsPage into 7 progressive-disclosure groups

Phase 9c follow-up. The 2309-line hand-rolled accordion is replaced with
seven SettingsGroup-wrapped top-level groups, each containing the
relevant existing sub-sections as nested SettingsGroups:

1. Audio — microphone (input device).
2. Vocabulary — terms & profiles, profiles & templates (legacy manager
   moved in here so all profile / vocabulary state lives together).
3. Transcription — engine, format mode, model management, compute device,
   language. Defaults to open to preserve the prior accordion's landing.
4. AI & Processing — post-processing toggles, AI Assistant tier and
   model management, if-then implementation rules. AI Assistant and
   if-then rules moved out of their original positions to sit alongside
   the deterministic post-processing toggles.
5. Tasks & Rituals — rituals (incl. launch-at-login, kept bundled with
   the existing Rituals UI block to avoid splitting that block), tasks
   page (sparkline), nudges.
6. Output & Capture — read aloud (TTS, lazy-loaded on first open via the
   new SettingsGroup `onopen` hook), capture & export.
7. Appearance & System — global hotkey, appearance (theme/zone/font/
   locale), accessibility, about (engine status + diagnostics).

Deviations from Codex's 7-group spec:
- Launch-at-login stays inside the Rituals sub-group (was bundled there
  in the existing markup; relocating would require splitting the
  Rituals UI block, which is out of scope for this pass).
- Profiles & Templates legacy manager pulled into the Vocabulary group
  rather than appearance/system.

Implementation notes:
- SettingsGroup gains an optional `onopen` callback prop, fired once on
  the first closed→open transition. Used by Read aloud to lazy-load TTS
  voices and by AI & Processing to refresh LLM status. Replaces the
  former toggleAiSection / toggleReadAloudSection imperative handlers.
- The centralised openSection state is removed; each <details> manages
  its own disclosure.
- Tokens are inherited from app.css; the prior global token darkening
  (commit 2da0a5b) covers all section labels via the existing utility
  classes — no per-component label-class swaps were added.

Search box deferred to a follow-up commit. Forcing `open=true` on
SettingsGroup based on a filter input would require either a controlled
`open` prop or a {#key} re-mount strategy, both of which add risk to
this restructure pass.

Tauri-bridged commands cannot be exercised in browser-only `npm run
dev`; structural verification done via npm build, npm check, and a
live dev server fetch of /settings. Real persistence smoke needs a
tauri dev session.

Verification:
- cargo fmt --check: pre-existing whitespace diffs in src-tauri/src/
  live.rs and src-tauri/src/lib.rs only (untouched by this commit).
- cargo clippy --all-targets -- -D warnings: clean.
- cargo test: 283 tests pass.
- npm run check: 1 pre-existing error in vite.config.js:5; 0 new.
- npm run build: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 12:50:28 +01:00
parent 6469663c25
commit eb3d2f90ab
2 changed files with 465 additions and 531 deletions

View File

@@ -10,13 +10,27 @@
title: string;
description?: string;
open?: boolean;
onopen?: () => void;
children?: import("svelte").Snippet;
}
let { title, description = "", open = false, children }: Props = $props();
let { title, description = "", open = false, onopen, children }: Props = $props();
// Fires once on the first transition from closed → open. Used by
// sections that lazy-load expensive data (e.g. TTS voice enumeration).
// Plain let (not $state) — the flag is mutated only by the toggle
// handler and never has to be reactive in the template.
let hasOpened = false;
function handleToggle(event: Event) {
const el = event.currentTarget as HTMLDetailsElement | null;
if (el?.open && !hasOpened) {
hasOpened = true;
onopen?.();
}
}
</script>
<details {open} class="settings-group border-t border-border-subtle">
<details {open} ontoggle={handleToggle} class="settings-group border-t border-border-subtle">
<summary
class="flex items-start gap-2 cursor-pointer list-none py-3 px-1 rounded hover:bg-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
>

File diff suppressed because it is too large Load Diff