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 {
|
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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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 */
|
/* Tab navigation for settings */
|
||||||
.cc-settings-tabs {
|
.cc-settings-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue