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

+
+
+
+
-
+
- +