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 <noreply@anthropic.com>
This commit is contained in:
Luca G. Oelfke 2026-02-06 11:18:44 +01:00
parent de839d81c3
commit a6cc173293
No known key found for this signature in database
GPG key ID: E22BABF67200F864
2 changed files with 113 additions and 26 deletions

View file

@ -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

View file

@ -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;
}