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:
parent
de839d81c3
commit
a6cc173293
2 changed files with 113 additions and 26 deletions
|
|
@ -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
|
||||
|
|
|
|||
48
styles.css
48
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue