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:
parent
2f3574c63f
commit
94c2822340
2 changed files with 170 additions and 24 deletions
151
src/settings.ts
151
src/settings.ts
|
|
@ -94,6 +94,7 @@ class CollapsibleSection {
|
|||
export class ClaudeContextSettingTab extends PluginSettingTab {
|
||||
plugin: ClaudeContextPlugin;
|
||||
private activeTab: SettingsTabId;
|
||||
private searchQuery = '';
|
||||
|
||||
constructor(app: App, plugin: ClaudeContextPlugin) {
|
||||
super(app, plugin);
|
||||
|
|
@ -105,33 +106,135 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
|
|||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
// Tab navigation
|
||||
const nav = containerEl.createEl('nav', { cls: 'cc-settings-tabs' });
|
||||
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();
|
||||
});
|
||||
}
|
||||
// Search input
|
||||
const searchInput = containerEl.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Search settings...',
|
||||
cls: 'cc-settings-search-input',
|
||||
});
|
||||
searchInput.value = this.searchQuery;
|
||||
|
||||
searchInput.addEventListener('input', () => {
|
||||
const wasSearching = this.searchQuery.length > 0;
|
||||
this.searchQuery = searchInput.value;
|
||||
const nowSearching = this.searchQuery.length > 0;
|
||||
|
||||
if (wasSearching !== nowSearching) {
|
||||
// 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' });
|
||||
|
||||
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;
|
||||
if (this.searchQuery) {
|
||||
// Search mode: render all tabs with group headings
|
||||
const tabRenderers: { label: string; render: (el: HTMLElement) => void }[] = [
|
||||
{ label: 'General', render: el => this.renderGeneralTab(el) },
|
||||
{ label: 'Sources', render: el => this.renderSourcesTab(el) },
|
||||
{ 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';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
43
styles.css
43
styles.css
|
|
@ -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 */
|
||||
.cc-settings-tabs {
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Reference in a new issue