From 637ed89eac75255d60701fceb409187d6138ed55 Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 7 May 2026 09:13:38 +0100 Subject: [PATCH] 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 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
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. --- src/lib/components/SettingsGroup.svelte | 14 ++- src/lib/pages/SettingsPage.svelte | 139 ++++++++++++++++++++---- 2 files changed, 131 insertions(+), 22 deletions(-) diff --git a/src/lib/components/SettingsGroup.svelte b/src/lib/components/SettingsGroup.svelte index ec238ce..5bfb0fd 100644 --- a/src/lib/components/SettingsGroup.svelte +++ b/src/lib/components/SettingsGroup.svelte @@ -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
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; + }); -
+
diff --git a/src/lib/pages/SettingsPage.svelte b/src/lib/pages/SettingsPage.svelte index 5398d32..4b9d726 100644 --- a/src/lib/pages/SettingsPage.svelte +++ b/src/lib/pages/SettingsPage.svelte @@ -17,7 +17,7 @@ import { toasts } from "$lib/stores/toasts.svelte.js"; import { clampTextLines } from "$lib/utils/textMeasure.js"; import { bodyPretextLineHeight, pretextFontShorthand } from "$lib/utils/accessibilityTypography.js"; - import { Check, ChevronRight } from "lucide-svelte"; + import { Check, ChevronRight, Search, X } from "lucide-svelte"; import { _ } from "svelte-i18n"; import { SUPPORTED_LOCALES, setLocale, currentLocale } from "$lib/i18n"; // Aliased because SettingsPage has its own local refreshLlmStatus @@ -403,9 +403,19 @@ // default (matches the prior accordion behaviour where it was the // landing section). - // Search filter (Phase 9c). Empty string = no filter; non-empty string - // pre-opens any group whose title/keywords match (case-insensitive). + // Search filter. Empty string = no filter and groups use their default + // open state. Non-empty string force-opens any SettingsGroup whose + // title, description, or hand-curated keywords contain the query + // (case-insensitive). Parent groups pass their children's keywords + // through so a match on "GPU" inside AI Assistant also opens AI & + // Processing. let settingsSearch = $state(""); + let searchActive = $derived(settingsSearch.trim().length > 0); + let searchQuery = $derived(settingsSearch.trim().toLowerCase()); + function searchMatches(...terms) { + if (!searchActive) return false; + return terms.some((t) => (t || "").toString().toLowerCase().includes(searchQuery)); + } let settingsTextFont = $derived(pretextFontShorthand(prefs.accessibility, 11)); let settingsPathLineHeight = $derived(bodyPretextLineHeight(prefs.accessibility, 11)); let outputFolderPreview = $derived.by(() => { @@ -965,10 +975,37 @@
- -

Settings

+ +
+
+

Settings

+
+
+
+
-
+
- +