diff --git a/src/settings.ts b/src/settings.ts index 001f001..b648ae3 100644 --- a/src/settings.ts +++ b/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'; } } diff --git a/styles.css b/styles.css index 55b3cb7..6bdd10e 100644 --- a/styles.css +++ b/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;