From a6cc17329315b91c3bff089ba1ad0e637898427f Mon Sep 17 00:00:00 2001 From: "Luca G. Oelfke" Date: Fri, 6 Feb 2026 11:18:44 +0100 Subject: [PATCH] feat: add collapsible sections within settings tabs Add reusable CollapsibleSection component with animated chevron and persisted collapse state. Applied to Sources, Templates, and Output tabs where natural content groupings exist. General and History tabs stay flat. Co-Authored-By: Claude Opus 4.6 --- src/settings.ts | 91 +++++++++++++++++++++++++++++++++++-------------- styles.css | 48 ++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 26 deletions(-) diff --git a/src/settings.ts b/src/settings.ts index 23ce77c..4728b48 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -24,6 +24,7 @@ export interface ClaudeContextSettings { primaryTargetId: string | null; targetOutputFolder: string; lastSettingsTab: string; + collapsedSections: string[]; } export const DEFAULT_SETTINGS: ClaudeContextSettings = { @@ -42,6 +43,7 @@ export const DEFAULT_SETTINGS: ClaudeContextSettings = { primaryTargetId: null, targetOutputFolder: '_claude/outputs', lastSettingsTab: 'general', + collapsedSections: [], }; export type SettingsTabId = 'general' | 'sources' | 'templates' | 'output' | 'history'; @@ -54,6 +56,39 @@ const SETTINGS_TABS: { id: SettingsTabId; label: string }[] = [ { id: 'history', label: 'History' }, ]; +class CollapsibleSection { + contentEl: HTMLElement; + + constructor( + parent: HTMLElement, + id: string, + title: string, + plugin: ClaudeContextPlugin, + ) { + const isCollapsed = plugin.settings.collapsedSections.includes(id); + + const wrapper = parent.createDiv({ cls: `cc-section${isCollapsed ? ' is-collapsed' : ''}` }); + + const header = wrapper.createDiv({ cls: 'cc-section-header' }); + header.createEl('span', { text: title, cls: 'cc-section-title' }); + header.createEl('span', { text: '\u203A', cls: 'cc-section-chevron' }); + + this.contentEl = wrapper.createDiv({ cls: 'cc-section-content' }); + + header.addEventListener('click', async () => { + const nowCollapsed = wrapper.hasClass('is-collapsed'); + if (nowCollapsed) { + wrapper.removeClass('is-collapsed'); + plugin.settings.collapsedSections = plugin.settings.collapsedSections.filter(s => s !== id); + } else { + wrapper.addClass('is-collapsed'); + plugin.settings.collapsedSections.push(id); + } + await plugin.saveSettings(); + }); + } +} + export class ClaudeContextSettingTab extends PluginSettingTab { plugin: ClaudeContextPlugin; private activeTab: SettingsTabId; @@ -171,13 +206,11 @@ export class ClaudeContextSettingTab extends PluginSettingTab { // === TAB: Sources === private renderSourcesTab(el: HTMLElement) { - const desc = el.createEl('p', { - text: 'Add additional context sources like freetext, external files, or shell command output.', - cls: 'setting-item-description' - }); - desc.style.marginBottom = '10px'; + // Section: Configured sources + const sourcesSection = new CollapsibleSection(el, 'sources-list', 'Configured sources', this.plugin); + const sc = sourcesSection.contentEl; - const buttonContainer = el.createDiv({ cls: 'sources-button-container' }); + const buttonContainer = sc.createDiv({ cls: 'sources-button-container' }); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '8px'; buttonContainer.style.marginBottom = '15px'; @@ -197,10 +230,13 @@ export class ClaudeContextSettingTab extends PluginSettingTab { new SourceModal(this.app, this.plugin, 'shell', null, () => this.display()).open(); }); - const sourcesContainer = el.createDiv({ cls: 'sources-list-container' }); + const sourcesContainer = sc.createDiv({ cls: 'sources-list-container' }); this.renderSourcesList(sourcesContainer); - new Setting(el) + // Section: Options + const optionsSection = new CollapsibleSection(el, 'sources-options', 'Options', this.plugin); + + new Setting(optionsSection.contentEl) .setName('Show source labels') .setDesc('Add position and name labels to source output') .addToggle(toggle => toggle @@ -214,13 +250,11 @@ export class ClaudeContextSettingTab extends PluginSettingTab { // === TAB: Templates === private renderTemplatesTab(el: HTMLElement) { - const desc = el.createEl('p', { - text: 'Create reusable prompt templates that wrap around your context.', - cls: 'setting-item-description' - }); - desc.style.marginBottom = '10px'; + // Section: Manage templates + const manageSection = new CollapsibleSection(el, 'templates-list', 'Manage templates', this.plugin); + const mc = manageSection.contentEl; - const buttonContainer = el.createDiv({ cls: 'templates-button-container' }); + const buttonContainer = mc.createDiv({ cls: 'templates-button-container' }); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '8px'; buttonContainer.style.marginBottom = '15px'; @@ -243,7 +277,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab { // Starter templates const hasStarterTemplates = this.plugin.settings.promptTemplates.some(t => t.isBuiltin); if (!hasStarterTemplates) { - const starterContainer = el.createDiv(); + const starterContainer = mc.createDiv(); starterContainer.style.padding = '10px'; starterContainer.style.backgroundColor = 'var(--background-secondary)'; starterContainer.style.borderRadius = '4px'; @@ -263,10 +297,13 @@ export class ClaudeContextSettingTab extends PluginSettingTab { }); } - const templatesContainer = el.createDiv({ cls: 'templates-list-container' }); + const templatesContainer = mc.createDiv({ cls: 'templates-list-container' }); this.renderTemplatesList(templatesContainer); - new Setting(el) + // Section: Defaults + const defaultsSection = new CollapsibleSection(el, 'templates-defaults', 'Defaults', this.plugin); + + new Setting(defaultsSection.contentEl) .setName('Default template') .setDesc('Template to use by default when copying context') .addDropdown(dropdown => { @@ -285,13 +322,11 @@ export class ClaudeContextSettingTab extends PluginSettingTab { // === TAB: Output === private renderOutputTab(el: HTMLElement) { - const desc = el.createEl('p', { - text: 'Configure multiple output formats for different LLMs. The primary target is copied to clipboard, secondary targets are saved as files.', - cls: 'setting-item-description' - }); - desc.style.marginBottom = '10px'; + // Section: Configured targets + const targetsSection = new CollapsibleSection(el, 'output-list', 'Configured targets', this.plugin); + const tc = targetsSection.contentEl; - const buttonContainer = el.createDiv({ cls: 'targets-button-container' }); + const buttonContainer = tc.createDiv({ cls: 'targets-button-container' }); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '8px'; buttonContainer.style.marginBottom = '15px'; @@ -316,10 +351,14 @@ export class ClaudeContextSettingTab extends PluginSettingTab { this.display(); }); - const targetsContainer = el.createDiv({ cls: 'targets-list-container' }); + const targetsContainer = tc.createDiv({ cls: 'targets-list-container' }); this.renderTargetsList(targetsContainer); - new Setting(el) + // Section: Output settings + const settingsSection = new CollapsibleSection(el, 'output-settings', 'Output settings', this.plugin); + const oc = settingsSection.contentEl; + + new Setting(oc) .setName('Primary target') .setDesc('This target\'s output is copied to clipboard') .addDropdown(dropdown => { @@ -334,7 +373,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab { }); }); - new Setting(el) + new Setting(oc) .setName('Output folder') .setDesc('Folder for secondary target files') .addText(text => text diff --git a/styles.css b/styles.css index f18c57e..0d19e2a 100644 --- a/styles.css +++ b/styles.css @@ -31,3 +31,51 @@ border-bottom-color: var(--interactive-accent); font-weight: 600; } + +/* Collapsible sections */ +.cc-section { + margin-bottom: 8px; +} + +.cc-section-header { + display: flex; + align-items: center; + padding: 8px 4px; + cursor: pointer; + border-radius: 4px; + user-select: none; +} + +.cc-section-header:hover { + background: var(--background-modifier-hover); +} + +.cc-section-title { + flex: 1; + font-weight: 600; + font-size: 14px; +} + +.cc-section-chevron { + font-size: 16px; + transition: transform 0.2s ease; + transform: rotate(90deg); +} + +.cc-section.is-collapsed .cc-section-chevron { + transform: rotate(0deg); +} + +.cc-section-content { + max-height: 2000px; + overflow: hidden; + transition: max-height 0.25s ease, opacity 0.2s ease; + opacity: 1; + padding-top: 4px; +} + +.cc-section.is-collapsed .cc-section-content { + max-height: 0; + opacity: 0; + padding-top: 0; +}