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;
|
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
|
||||||
|
|
|
||||||
48
styles.css
48
styles.css
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue