Skip to content

Commit 4e90137

Browse files
committed
Settings: Fix up search feature
- 200ms debounce added to search input in SettingsSidebar - Content filtering: SettingsContent now only shows sections containing matching settings, and only the individual setting rows that match the query - Search highlighting: Matching text is highlighted with <mark class="search-highlight"> in setting labels. Added --color-highlight CSS variable (yellow tint) for light/dark modes. - Updated all section components (Appearance, FileOperations, Updates, Network, McpServer, Logging, Themes, Advanced) to filter individual settings using getMatchingSettingIds() - Bug fixes: Fixed ESLint error in SettingSelect, added CSS classes to allowlist
1 parent 4242474 commit 4e90137

16 files changed

Lines changed: 443 additions & 194 deletions

apps/desktop/src-tauri/src/menu.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,8 @@ pub fn build_menu<R: Runtime>(
135135
// Add separator and Settings after license key
136136
let separator = tauri::menu::PredefinedMenuItem::separator(app)?;
137137
submenu.insert(&separator, 2)?;
138-
let settings_item = tauri::menu::MenuItem::with_id(
139-
app,
140-
SETTINGS_ID,
141-
"Settings...",
142-
true,
143-
Some("Cmd+,"),
144-
)?;
138+
let settings_item =
139+
tauri::menu::MenuItem::with_id(app, SETTINGS_ID, "Settings...", true, Some("Cmd+,"))?;
145140
submenu.insert(&settings_item, 3)?;
146141
break;
147142
}

apps/desktop/src/app.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
/* === Selection === */
5050
--color-selection-fg: #c9a227;
5151

52+
/* === Search Highlight === */
53+
--color-highlight: rgba(255, 213, 0, 0.4);
54+
5255
/* === Size tier colors (20% offset toward target color) === */
5356
--color-size-kb: color-mix(in srgb, var(--color-text-secondary) 70%, #f0c000);
5457
--color-size-mb: color-mix(in srgb, var(--color-text-secondary) 70%, #ff8c00);
@@ -104,6 +107,9 @@
104107

105108
/* === Selection === */
106109
--color-selection-fg: #d4a82a;
110+
111+
/* === Search Highlight === */
112+
--color-highlight: rgba(255, 213, 0, 0.35);
107113
}
108114
}
109115

apps/desktop/src/lib/settings/components/SettingRow.svelte

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import type { Snippet } from 'svelte'
33
import { isModified, resetSetting, onSpecificSettingChange, type SettingId } from '$lib/settings'
4+
import { getMatchIndicesForLabel, highlightMatches } from '$lib/settings/settings-search'
45
import { onMount } from 'svelte'
56
67
interface Props {
@@ -10,6 +11,7 @@
1011
disabled?: boolean
1112
disabledReason?: string
1213
requiresRestart?: boolean
14+
searchQuery?: string
1315
children: Snippet
1416
}
1517
@@ -20,9 +22,19 @@
2022
disabled = false,
2123
disabledReason,
2224
requiresRestart = false,
25+
searchQuery = '',
2326
children,
2427
}: Props = $props()
2528
29+
// Get highlighted label segments based on search query
30+
const labelSegments = $derived.by(() => {
31+
if (!searchQuery.trim()) {
32+
return [{ text: label, matched: false }]
33+
}
34+
const matchIndices = getMatchIndicesForLabel(searchQuery, id)
35+
return highlightMatches(label, matchIndices)
36+
})
37+
2638
// Track modified state reactively by subscribing to changes
2739
let modified = $state(isModified(id))
2840
@@ -44,7 +56,11 @@
4456
{#if modified}
4557
<span class="modified-indicator" title="Modified from default">●</span>
4658
{/if}
47-
<label class="setting-label" for={id}>{label}</label>
59+
<label class="setting-label" for={id}
60+
>{#each labelSegments as segment, i (i)}{#if segment.matched}<mark class="search-highlight"
61+
>{segment.text}</mark
62+
>{:else}{segment.text}{/if}{/each}</label
63+
>
4864
{#if disabled && disabledReason}
4965
<span class="disabled-badge">{disabledReason}</span>
5066
{/if}
@@ -142,4 +158,11 @@
142158
.reset-link.hidden {
143159
visibility: hidden;
144160
}
161+
162+
.search-highlight {
163+
background-color: var(--color-highlight);
164+
color: inherit;
165+
padding: 0 2px;
166+
border-radius: 2px;
167+
}
145168
</style>

apps/desktop/src/lib/settings/components/SettingSelect.svelte

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,18 @@
2525
let value = $state(getSetting(id))
2626
let showCustomInput = $state(false)
2727
let customValue = $state('')
28-
let customInputRef: HTMLInputElement | null = $state(null)
28+
let customInputRef: HTMLInputElement | undefined = $state()
2929
30-
// Auto-focus custom input when it becomes visible
30+
// Focus custom input when it becomes visible (next microtask after render)
3131
$effect(() => {
32-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Svelte $state is reactive
3332
if (showCustomInput) {
34-
// customInputRef will be populated when the input element is mounted
35-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Svelte bind:this updates this reactively
36-
customInputRef?.focus()
37-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Svelte bind:this updates this reactively
38-
customInputRef?.select()
33+
// Use setTimeout to wait for DOM to update after reactive state change
34+
setTimeout(() => {
35+
if (customInputRef) {
36+
customInputRef.focus()
37+
customInputRef.select()
38+
}
39+
}, 0)
3940
}
4041
})
4142

apps/desktop/src/lib/settings/components/SettingsContent.svelte

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import LoggingSection from '$lib/settings/sections/LoggingSection.svelte'
1010
import AdvancedSection from '$lib/settings/sections/AdvancedSection.svelte'
1111
import SectionSummary from './SectionSummary.svelte'
12+
import { getMatchingSettingIdsInSection } from '$lib/settings/settings-search'
1213
1314
interface Props {
1415
searchQuery: string
@@ -32,11 +33,18 @@
3233
onNavigate?.(path)
3334
}
3435
36+
// Check if a section has any matching settings during search
37+
function sectionHasMatchingSettings(sectionPath: string[]): boolean {
38+
if (!searchQuery.trim()) return true
39+
const matchingIds = getMatchingSettingIdsInSection(searchQuery, sectionPath)
40+
return matchingIds.size > 0
41+
}
42+
3543
// Determine which sections to show based on selection and search
3644
function shouldShowSection(sectionPath: string[]): boolean {
37-
// If searching, show all sections that have matches (handled by components)
45+
// If searching, only show sections that have matching settings
3846
if (searchQuery.trim()) {
39-
return true
47+
return sectionHasMatchingSettings(sectionPath)
4048
}
4149
// If showing summary, don't show any section content
4250
if (showSummary) {

apps/desktop/src/lib/settings/components/SettingsSidebar.svelte

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
const { searchQuery, matchingSections, selectedSection, onSearch, onSectionSelect }: Props = $props()
1414
1515
let searchInput: HTMLInputElement | null = $state(null)
16+
let debounceTimer: ReturnType<typeof setTimeout> | null = null
1617
const sectionTree = buildSectionTree()
1718
1819
// Special sections that have dedicated UI (not from registry)
@@ -54,7 +55,18 @@
5455
5556
function handleSearchInput(event: Event) {
5657
const target = event.target as HTMLInputElement
57-
onSearch(target.value)
58+
const value = target.value
59+
60+
// Clear any pending debounce timer
61+
if (debounceTimer) {
62+
clearTimeout(debounceTimer)
63+
}
64+
65+
// Debounce search by 200ms
66+
debounceTimer = setTimeout(() => {
67+
onSearch(value)
68+
debounceTimer = null
69+
}, 200)
5870
}
5971
6072
function clearSearch() {

apps/desktop/src/lib/settings/sections/AdvancedSection.svelte

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
} from '$lib/settings'
1212
import { Switch } from '@ark-ui/svelte/switch'
1313
import { NumberInput, type NumberInputValueChangeDetails } from '@ark-ui/svelte/number-input'
14-
import { searchAdvancedSettings } from '$lib/settings/settings-search'
14+
import { searchAdvancedSettings, getMatchIndicesForLabel, highlightMatches } from '$lib/settings/settings-search'
1515
1616
interface Props {
1717
searchQuery: string
@@ -49,6 +49,15 @@
4949
function handleReset(id: SettingId) {
5050
resetSetting(id)
5151
}
52+
53+
// Get highlighted label segments for a setting
54+
function getLabelSegments(label: string, settingId: string) {
55+
if (!searchQuery.trim()) {
56+
return [{ text: label, matched: false }]
57+
}
58+
const matchIndices = getMatchIndicesForLabel(searchQuery, settingId)
59+
return highlightMatches(label, matchIndices)
60+
}
5261
</script>
5362

5463
<div class="section">
@@ -75,7 +84,9 @@
7584
{#if modified}
7685
<span class="modified-dot">●</span>
7786
{/if}
78-
{setting.label}
87+
{#each getLabelSegments(setting.label, setting.id) as segment, i (i)}{#if segment.matched}<mark
88+
class="search-highlight">{segment.text}</mark
89+
>{:else}{segment.text}{/if}{/each}
7990
</div>
8091
<div class="setting-description">{setting.description}</div>
8192
<div class="setting-default">
@@ -220,6 +231,13 @@
220231
font-size: 10px;
221232
}
222233
234+
.search-highlight {
235+
background-color: var(--color-highlight);
236+
color: inherit;
237+
padding: 0 2px;
238+
border-radius: 2px;
239+
}
240+
223241
.setting-description {
224242
color: var(--color-text-secondary);
225243
font-size: var(--font-size-xs);

apps/desktop/src/lib/settings/sections/AppearanceSection.svelte

Lines changed: 91 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,23 @@
55
import SettingToggleGroup from '../components/SettingToggleGroup.svelte'
66
import SettingRadioGroup from '../components/SettingRadioGroup.svelte'
77
import { getSettingDefinition, getSetting, setSetting } from '$lib/settings'
8+
import { getMatchingSettingIds } from '$lib/settings/settings-search'
89
910
interface Props {
1011
searchQuery: string
1112
}
1213
13-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1414
const { searchQuery }: Props = $props()
1515
16+
// Get matching setting IDs for filtering
17+
const matchingIds = $derived(searchQuery.trim() ? getMatchingSettingIds(searchQuery) : null)
18+
19+
// Check if a setting should be shown
20+
function shouldShow(id: string): boolean {
21+
if (!matchingIds) return true
22+
return matchingIds.has(id)
23+
}
24+
1625
// Get definitions for rendering (with fallbacks for type safety)
1726
const uiDensityDef = getSettingDefinition('appearance.uiDensity') ?? { label: '', description: '' }
1827
const appIconsDef = getSettingDefinition('appearance.useAppIconsForDocuments') ?? { label: '', description: '' }
@@ -49,60 +58,88 @@
4958
<div class="section">
5059
<h2 class="section-title">Appearance</h2>
5160

52-
<SettingRow id="appearance.uiDensity" label={uiDensityDef.label} description={uiDensityDef.description}>
53-
<SettingToggleGroup id="appearance.uiDensity" />
54-
</SettingRow>
55-
56-
<SettingRow id="appearance.useAppIconsForDocuments" label={appIconsDef.label} description={appIconsDef.description}>
57-
<SettingSwitch id="appearance.useAppIconsForDocuments" />
58-
</SettingRow>
59-
60-
<SettingRow id="appearance.fileSizeFormat" label={fileSizeDef.label} description={fileSizeDef.description}>
61-
<SettingSelect id="appearance.fileSizeFormat" />
62-
</SettingRow>
63-
64-
<SettingRow id="appearance.dateTimeFormat" label={dateTimeDef.label} description={dateTimeDef.description}>
65-
<div class="date-time-setting">
66-
<SettingRadioGroup id="appearance.dateTimeFormat">
67-
{#snippet customContent(value)}
68-
{#if value === 'custom'}
69-
<div class="custom-format">
70-
<input
71-
type="text"
72-
class="format-input"
73-
value={customFormat}
74-
oninput={handleCustomFormatChange}
75-
placeholder="YYYY-MM-DD HH:mm"
76-
/>
77-
<div class="format-preview">
78-
Preview: <strong>{formatPreview(customFormat)}</strong>
79-
</div>
80-
<button
81-
type="button"
82-
class="help-toggle"
83-
onclick={() => (showFormatHelp = !showFormatHelp)}
84-
>
85-
{showFormatHelp ? 'Hide format help' : 'Show format help'}
86-
</button>
87-
{#if showFormatHelp}
88-
<div class="format-help">
89-
<h4>Format placeholders</h4>
90-
<ul>
91-
<li><code>YYYY</code> — 4-digit year (2025)</li>
92-
<li><code>MM</code> — 2-digit month (01-12)</li>
93-
<li><code>DD</code> — 2-digit day (01-31)</li>
94-
<li><code>HH</code> — 2-digit hour (00-23)</li>
95-
<li><code>mm</code> — 2-digit minute (00-59)</li>
96-
<li><code>ss</code> — 2-digit second (00-59)</li>
97-
</ul>
61+
{#if shouldShow('appearance.uiDensity')}
62+
<SettingRow
63+
id="appearance.uiDensity"
64+
label={uiDensityDef.label}
65+
description={uiDensityDef.description}
66+
{searchQuery}
67+
>
68+
<SettingToggleGroup id="appearance.uiDensity" />
69+
</SettingRow>
70+
{/if}
71+
72+
{#if shouldShow('appearance.useAppIconsForDocuments')}
73+
<SettingRow
74+
id="appearance.useAppIconsForDocuments"
75+
label={appIconsDef.label}
76+
description={appIconsDef.description}
77+
{searchQuery}
78+
>
79+
<SettingSwitch id="appearance.useAppIconsForDocuments" />
80+
</SettingRow>
81+
{/if}
82+
83+
{#if shouldShow('appearance.fileSizeFormat')}
84+
<SettingRow
85+
id="appearance.fileSizeFormat"
86+
label={fileSizeDef.label}
87+
description={fileSizeDef.description}
88+
{searchQuery}
89+
>
90+
<SettingSelect id="appearance.fileSizeFormat" />
91+
</SettingRow>
92+
{/if}
93+
94+
{#if shouldShow('appearance.dateTimeFormat')}
95+
<SettingRow
96+
id="appearance.dateTimeFormat"
97+
label={dateTimeDef.label}
98+
description={dateTimeDef.description}
99+
{searchQuery}
100+
>
101+
<div class="date-time-setting">
102+
<SettingRadioGroup id="appearance.dateTimeFormat">
103+
{#snippet customContent(value)}
104+
{#if value === 'custom'}
105+
<div class="custom-format">
106+
<input
107+
type="text"
108+
class="format-input"
109+
value={customFormat}
110+
oninput={handleCustomFormatChange}
111+
placeholder="YYYY-MM-DD HH:mm"
112+
/>
113+
<div class="format-preview">
114+
Preview: <strong>{formatPreview(customFormat)}</strong>
98115
</div>
99-
{/if}
100-
</div>
101-
{/if}
102-
{/snippet}
103-
</SettingRadioGroup>
104-
</div>
105-
</SettingRow>
116+
<button
117+
type="button"
118+
class="help-toggle"
119+
onclick={() => (showFormatHelp = !showFormatHelp)}
120+
>
121+
{showFormatHelp ? 'Hide format help' : 'Show format help'}
122+
</button>
123+
{#if showFormatHelp}
124+
<div class="format-help">
125+
<h4>Format placeholders</h4>
126+
<ul>
127+
<li><code>YYYY</code> — 4-digit year (2025)</li>
128+
<li><code>MM</code> — 2-digit month (01-12)</li>
129+
<li><code>DD</code> — 2-digit day (01-31)</li>
130+
<li><code>HH</code> — 2-digit hour (00-23)</li>
131+
<li><code>mm</code> — 2-digit minute (00-59)</li>
132+
<li><code>ss</code> — 2-digit second (00-59)</li>
133+
</ul>
134+
</div>
135+
{/if}
136+
</div>
137+
{/if}
138+
{/snippet}
139+
</SettingRadioGroup>
140+
</div>
141+
</SettingRow>
142+
{/if}
106143
</div>
107144

108145
<style>

0 commit comments

Comments
 (0)