feat: add search/filter field to settings page

Search input above tabs filters all settings by name and description
in real-time. When a query is active, all tabs render simultaneously
with group headings and non-matching settings are hidden via DOM
toggling. Empty sections and tab groups are hidden entirely. Escape
clears search and restores normal tabbed view.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca G. Oelfke 2026-02-06 11:48:21 +01:00
parent 2f3574c63f
commit 94c2822340
No known key found for this signature in database
GPG key ID: E22BABF67200F864
2 changed files with 170 additions and 24 deletions

View file

@ -94,6 +94,7 @@ class CollapsibleSection {
export class ClaudeContextSettingTab extends PluginSettingTab { export class ClaudeContextSettingTab extends PluginSettingTab {
plugin: ClaudeContextPlugin; plugin: ClaudeContextPlugin;
private activeTab: SettingsTabId; private activeTab: SettingsTabId;
private searchQuery = '';
constructor(app: App, plugin: ClaudeContextPlugin) { constructor(app: App, plugin: ClaudeContextPlugin) {
super(app, plugin); super(app, plugin);
@ -105,33 +106,135 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
const { containerEl } = this; const { containerEl } = this;
containerEl.empty(); containerEl.empty();
// Tab navigation // Search input
const nav = containerEl.createEl('nav', { cls: 'cc-settings-tabs' }); const searchInput = containerEl.createEl('input', {
for (const tab of SETTINGS_TABS) { type: 'text',
const btn = nav.createEl('button', { placeholder: 'Search settings...',
text: tab.label, cls: 'cc-settings-search-input',
cls: 'cc-settings-tab', });
}); searchInput.value = this.searchQuery;
if (tab.id === this.activeTab) {
btn.addClass('is-active'); searchInput.addEventListener('input', () => {
} const wasSearching = this.searchQuery.length > 0;
btn.addEventListener('click', async () => { this.searchQuery = searchInput.value;
this.activeTab = tab.id; const nowSearching = this.searchQuery.length > 0;
this.plugin.settings.lastSettingsTab = tab.id;
await this.plugin.saveSettings(); if (wasSearching !== nowSearching) {
this.display(); // Mode transition (tabs ↔ all): re-render and refocus
}); this.display();
} const refocused = containerEl.querySelector('.cc-settings-search-input') as HTMLInputElement;
if (refocused) {
refocused.focus();
refocused.selectionStart = refocused.selectionEnd = refocused.value.length;
}
} else if (nowSearching) {
this.filterSettings(containerEl);
}
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.searchQuery) {
e.preventDefault();
e.stopPropagation();
this.searchQuery = '';
this.display();
}
});
// Tab content
const content = containerEl.createDiv({ cls: 'cc-settings-tab-content' }); const content = containerEl.createDiv({ cls: 'cc-settings-tab-content' });
switch (this.activeTab) { if (this.searchQuery) {
case 'general': this.renderGeneralTab(content); break; // Search mode: render all tabs with group headings
case 'sources': this.renderSourcesTab(content); break; const tabRenderers: { label: string; render: (el: HTMLElement) => void }[] = [
case 'templates': this.renderTemplatesTab(content); break; { label: 'General', render: el => this.renderGeneralTab(el) },
case 'output': this.renderOutputTab(content); break; { label: 'Sources', render: el => this.renderSourcesTab(el) },
case 'history': this.renderHistoryTab(content); break; { label: 'Templates', render: el => this.renderTemplatesTab(el) },
{ label: 'Output', render: el => this.renderOutputTab(el) },
{ label: 'History', render: el => this.renderHistoryTab(el) },
];
for (const tab of tabRenderers) {
const group = content.createDiv({ cls: 'cc-settings-search-group' });
group.createEl('h4', { text: tab.label, cls: 'cc-settings-search-heading' });
tab.render(group);
}
this.filterSettings(content);
} else {
// Normal mode: tab navigation
const nav = containerEl.createEl('nav', { cls: 'cc-settings-tabs' });
containerEl.insertBefore(nav, content);
for (const tab of SETTINGS_TABS) {
const btn = nav.createEl('button', {
text: tab.label,
cls: 'cc-settings-tab',
});
if (tab.id === this.activeTab) {
btn.addClass('is-active');
}
btn.addEventListener('click', async () => {
this.activeTab = tab.id;
this.plugin.settings.lastSettingsTab = tab.id;
await this.plugin.saveSettings();
this.display();
});
}
switch (this.activeTab) {
case 'general': this.renderGeneralTab(content); break;
case 'sources': this.renderSourcesTab(content); break;
case 'templates': this.renderTemplatesTab(content); break;
case 'output': this.renderOutputTab(content); break;
case 'history': this.renderHistoryTab(content); break;
}
}
}
private filterSettings(content: HTMLElement) {
const query = this.searchQuery.toLowerCase();
// Filter individual setting items by name and description
content.querySelectorAll('.setting-item').forEach(el => {
const item = el as HTMLElement;
const name = (item.querySelector('.setting-item-name')?.textContent ?? '').toLowerCase();
const desc = (item.querySelector('.setting-item-description')?.textContent ?? '').toLowerCase();
item.style.display = (name.includes(query) || desc.includes(query)) ? '' : 'none';
});
// Force-expand all collapsible sections, hide ones with no visible settings
content.querySelectorAll('.cc-section').forEach(el => {
const section = el as HTMLElement;
section.classList.remove('is-collapsed');
const hasVisible = Array.from(section.querySelectorAll('.setting-item'))
.some(s => (s as HTMLElement).style.display !== 'none');
section.style.display = hasVisible ? '' : 'none';
});
// Hide tab groups with no visible settings
let totalVisible = 0;
content.querySelectorAll('.cc-settings-search-group').forEach(el => {
const group = el as HTMLElement;
const visibleCount = Array.from(group.querySelectorAll('.setting-item'))
.filter(s => (s as HTMLElement).style.display !== 'none').length;
group.style.display = visibleCount > 0 ? '' : 'none';
totalVisible += visibleCount;
});
// Show "no results" message
let noResults = content.querySelector('.cc-settings-no-results') as HTMLElement;
if (totalVisible === 0) {
if (!noResults) {
noResults = content.createEl('p', {
text: `No settings matching "${this.searchQuery}"`,
cls: 'cc-settings-no-results',
});
} else {
noResults.textContent = `No settings matching "${this.searchQuery}"`;
}
noResults.style.display = '';
} else if (noResults) {
noResults.style.display = 'none';
} }
} }

View file

@ -1,3 +1,46 @@
/* Settings search */
.cc-settings-search-input {
width: 100%;
padding: 8px 12px;
margin-bottom: 8px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background: var(--background-primary);
color: var(--text-normal);
font-size: 14px;
}
.cc-settings-search-input:focus {
border-color: var(--interactive-accent);
outline: none;
}
.cc-settings-search-input::placeholder {
color: var(--text-faint);
}
.cc-settings-search-heading {
font-size: 13px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 16px 0 8px 0;
padding-bottom: 4px;
border-bottom: 1px solid var(--background-modifier-border);
}
.cc-settings-search-group:first-child .cc-settings-search-heading {
margin-top: 0;
}
.cc-settings-no-results {
color: var(--text-muted);
font-style: italic;
text-align: center;
padding: 20px 0;
}
/* Tab navigation for settings */ /* Tab navigation for settings */
.cc-settings-tabs { .cc-settings-tabs {
display: flex; display: flex;