feat(settings): ship search filter that opens matching groups

The settingsSearch state was declared in Phase 9c with a comment
describing the intended behaviour but no <input> bound to it. With 21
SettingsGroup instances and a 2,261-line page, finding any specific
control required scrolling and opening every group. For the
neurodivergent audience this is exactly the cognitive-load failure
the brand commits to avoiding.

Added a sticky search input at the top of the page (paired with the
"Settings" title in a single sticky header that bg-bg-matches the
page so groups don't bleed under it). Each SettingsGroup now passes
through searchMatches against its title, description, and a curated
keyword string. Parent groups inherit their children's keywords so a
search for "GPU" expands AI & Processing and the AI Assistant inner
group together.

Updated SettingsGroup to push prop changes to the DOM via $effect:
the native <details> element's open attribute is mutated by user
clicks outside Svelte's reactive graph, so a parent prop change
needs to force-sync. User-driven toggles where the prop value didn't
change keep their local state because $effect only re-runs on diff.

Verified live: baseline shows 3 groups open (Transcription, Terms &
profiles, Capture & export), "GPU" opens AI & Processing + AI
Assistant only (other 19 closed), "ritual" opens Tasks & Rituals +
Rituals only, X-button clear restores baseline.
This commit is contained in:
2026-05-07 09:13:38 +01:00
parent 7a21d0c7fc
commit 637ed89eac
2 changed files with 131 additions and 22 deletions

View File

@@ -21,6 +21,8 @@
// 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;
let detailsEl: HTMLDetailsElement | null = $state(null);
function handleToggle(event: Event) {
const el = event.currentTarget as HTMLDetailsElement | null;
if (el?.open && !hasOpened) {
@@ -28,9 +30,19 @@
onopen?.();
}
}
// Push prop changes to the DOM. The native <details> open attribute
// is mutated by user interaction outside Svelte's reactive graph;
// when the parent later changes the open prop (for example because a
// search filter expanded a different group), $effect force-syncs.
// User-driven toggles where the prop value did not change keep their
// local state because $effect only re-runs on prop diff.
$effect(() => {
if (detailsEl && detailsEl.open !== open) detailsEl.open = open;
});
</script>
<details {open} ontoggle={handleToggle} class="settings-group border-t border-border-subtle">
<details bind:this={detailsEl} {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"
>