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; primaryTargetId: string | null;
targetOutputFolder: string; targetOutputFolder: string;
lastSettingsTab: string; lastSettingsTab: string;
collapsedSections: string[];
} }
export const DEFAULT_SETTINGS: ClaudeContextSettings = { export const DEFAULT_SETTINGS: ClaudeContextSettings = {
@ -42,6 +43,7 @@ export const DEFAULT_SETTINGS: ClaudeContextSettings = {
primaryTargetId: null, primaryTargetId: null,
targetOutputFolder: '_claude/outputs', targetOutputFolder: '_claude/outputs',
lastSettingsTab: 'general', lastSettingsTab: 'general',
collapsedSections: [],
}; };
export type SettingsTabId = 'general' | 'sources' | 'templates' | 'output' | 'history'; export type SettingsTabId = 'general' | 'sources' | 'templates' | 'output' | 'history';
@ -54,6 +56,39 @@ const SETTINGS_TABS: { id: SettingsTabId; label: string }[] = [
{ id: 'history', label: 'History' }, { 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 { export class ClaudeContextSettingTab extends PluginSettingTab {
plugin: ClaudeContextPlugin; plugin: ClaudeContextPlugin;
private activeTab: SettingsTabId; private activeTab: SettingsTabId;
@ -171,13 +206,11 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
// === TAB: Sources === // === TAB: Sources ===
private renderSourcesTab(el: HTMLElement) { private renderSourcesTab(el: HTMLElement) {
const desc = el.createEl('p', { // Section: Configured sources
text: 'Add additional context sources like freetext, external files, or shell command output.', const sourcesSection = new CollapsibleSection(el, 'sources-list', 'Configured sources', this.plugin);
cls: 'setting-item-description' const sc = sourcesSection.contentEl;
});
desc.style.marginBottom = '10px';
const buttonContainer = el.createDiv({ cls: 'sources-button-container' }); const buttonContainer = sc.createDiv({ cls: 'sources-button-container' });
buttonContainer.style.display = 'flex'; buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '8px'; buttonContainer.style.gap = '8px';
buttonContainer.style.marginBottom = '15px'; buttonContainer.style.marginBottom = '15px';
@ -197,10 +230,13 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
new SourceModal(this.app, this.plugin, 'shell', null, () => this.display()).open(); 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); 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') .setName('Show source labels')
.setDesc('Add position and name labels to source output') .setDesc('Add position and name labels to source output')
.addToggle(toggle => toggle .addToggle(toggle => toggle
@ -214,13 +250,11 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
// === TAB: Templates === // === TAB: Templates ===
private renderTemplatesTab(el: HTMLElement) { private renderTemplatesTab(el: HTMLElement) {
const desc = el.createEl('p', { // Section: Manage templates
text: 'Create reusable prompt templates that wrap around your context.', const manageSection = new CollapsibleSection(el, 'templates-list', 'Manage templates', this.plugin);
cls: 'setting-item-description' const mc = manageSection.contentEl;
});
desc.style.marginBottom = '10px';
const buttonContainer = el.createDiv({ cls: 'templates-button-container' }); const buttonContainer = mc.createDiv({ cls: 'templates-button-container' });
buttonContainer.style.display = 'flex'; buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '8px'; buttonContainer.style.gap = '8px';
buttonContainer.style.marginBottom = '15px'; buttonContainer.style.marginBottom = '15px';
@ -243,7 +277,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
// Starter templates // Starter templates
const hasStarterTemplates = this.plugin.settings.promptTemplates.some(t => t.isBuiltin); const hasStarterTemplates = this.plugin.settings.promptTemplates.some(t => t.isBuiltin);
if (!hasStarterTemplates) { if (!hasStarterTemplates) {
const starterContainer = el.createDiv(); const starterContainer = mc.createDiv();
starterContainer.style.padding = '10px'; starterContainer.style.padding = '10px';
starterContainer.style.backgroundColor = 'var(--background-secondary)'; starterContainer.style.backgroundColor = 'var(--background-secondary)';
starterContainer.style.borderRadius = '4px'; 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); 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') .setName('Default template')
.setDesc('Template to use by default when copying context') .setDesc('Template to use by default when copying context')
.addDropdown(dropdown => { .addDropdown(dropdown => {
@ -285,13 +322,11 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
// === TAB: Output === // === TAB: Output ===
private renderOutputTab(el: HTMLElement) { private renderOutputTab(el: HTMLElement) {
const desc = el.createEl('p', { // Section: Configured targets
text: 'Configure multiple output formats for different LLMs. The primary target is copied to clipboard, secondary targets are saved as files.', const targetsSection = new CollapsibleSection(el, 'output-list', 'Configured targets', this.plugin);
cls: 'setting-item-description' const tc = targetsSection.contentEl;
});
desc.style.marginBottom = '10px';
const buttonContainer = el.createDiv({ cls: 'targets-button-container' }); const buttonContainer = tc.createDiv({ cls: 'targets-button-container' });
buttonContainer.style.display = 'flex'; buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '8px'; buttonContainer.style.gap = '8px';
buttonContainer.style.marginBottom = '15px'; buttonContainer.style.marginBottom = '15px';
@ -316,10 +351,14 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
this.display(); this.display();
}); });
const targetsContainer = el.createDiv({ cls: 'targets-list-container' }); const targetsContainer = tc.createDiv({ cls: 'targets-list-container' });
this.renderTargetsList(targetsContainer); 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') .setName('Primary target')
.setDesc('This target\'s output is copied to clipboard') .setDesc('This target\'s output is copied to clipboard')
.addDropdown(dropdown => { .addDropdown(dropdown => {
@ -334,7 +373,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
}); });
}); });
new Setting(el) new Setting(oc)
.setName('Output folder') .setName('Output folder')
.setDesc('Folder for secondary target files') .setDesc('Folder for secondary target files')
.addText(text => text .addText(text => text

View file

@ -31,3 +31,51 @@
border-bottom-color: var(--interactive-accent); border-bottom-color: var(--interactive-accent);
font-weight: 600; 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;
}