feat: replace scrollable settings page with tabbed layout

Split settings into 5 tabs (General, Sources, Templates, Output,
History) with a sticky tab bar. Persists the last active tab in
plugin settings so it reopens where the user left off.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca G. Oelfke 2026-02-06 11:14:17 +01:00
parent e922523dc9
commit de839d81c3
No known key found for this signature in database
GPG key ID: E22BABF67200F864
2 changed files with 180 additions and 111 deletions

View file

@ -23,6 +23,7 @@ export interface ClaudeContextSettings {
targets: OutputTarget[]; targets: OutputTarget[];
primaryTargetId: string | null; primaryTargetId: string | null;
targetOutputFolder: string; targetOutputFolder: string;
lastSettingsTab: string;
} }
export const DEFAULT_SETTINGS: ClaudeContextSettings = { export const DEFAULT_SETTINGS: ClaudeContextSettings = {
@ -40,21 +41,67 @@ export const DEFAULT_SETTINGS: ClaudeContextSettings = {
targets: [], targets: [],
primaryTargetId: null, primaryTargetId: null,
targetOutputFolder: '_claude/outputs', targetOutputFolder: '_claude/outputs',
lastSettingsTab: 'general',
}; };
export type SettingsTabId = 'general' | 'sources' | 'templates' | 'output' | 'history';
const SETTINGS_TABS: { id: SettingsTabId; label: string }[] = [
{ id: 'general', label: 'General' },
{ id: 'sources', label: 'Sources' },
{ id: 'templates', label: 'Templates' },
{ id: 'output', label: 'Output' },
{ id: 'history', label: 'History' },
];
export class ClaudeContextSettingTab extends PluginSettingTab { export class ClaudeContextSettingTab extends PluginSettingTab {
plugin: ClaudeContextPlugin; plugin: ClaudeContextPlugin;
private activeTab: SettingsTabId;
constructor(app: App, plugin: ClaudeContextPlugin) { constructor(app: App, plugin: ClaudeContextPlugin) {
super(app, plugin); super(app, plugin);
this.plugin = plugin; this.plugin = plugin;
this.activeTab = (plugin.settings.lastSettingsTab as SettingsTabId) || 'general';
} }
display(): void { display(): void {
const { containerEl } = this; const { containerEl } = this;
containerEl.empty(); containerEl.empty();
new Setting(containerEl) // Tab navigation
const nav = containerEl.createEl('nav', { cls: 'cc-settings-tabs' });
for (const tab of SETTINGS_TABS) {
const btn = nav.createEl('button', {
text: tab.label,
cls: 'cc-settings-tab',
});
if (tab.id === this.activeTab) {
btn.addClass('is-active');
}
btn.addEventListener('click', async () => {
this.activeTab = tab.id;
this.plugin.settings.lastSettingsTab = tab.id;
await this.plugin.saveSettings();
this.display();
});
}
// Tab content
const content = containerEl.createDiv({ cls: 'cc-settings-tab-content' });
switch (this.activeTab) {
case 'general': this.renderGeneralTab(content); break;
case 'sources': this.renderSourcesTab(content); break;
case 'templates': this.renderTemplatesTab(content); break;
case 'output': this.renderOutputTab(content); break;
case 'history': this.renderHistoryTab(content); break;
}
}
// === TAB: General ===
private renderGeneralTab(el: HTMLElement) {
new Setting(el)
.setName('Context folder') .setName('Context folder')
.setDesc('Folder containing your context files') .setDesc('Folder containing your context files')
.addText(text => text .addText(text => text
@ -65,7 +112,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
})); }));
new Setting(containerEl) new Setting(el)
.setName('Separator') .setName('Separator')
.setDesc('Text between files (e.g. "---" or "***")') .setDesc('Text between files (e.g. "---" or "***")')
.addText(text => text .addText(text => text
@ -76,7 +123,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
})); }));
new Setting(containerEl) new Setting(el)
.setName('Include filenames') .setName('Include filenames')
.setDesc('Add "# === filename.md ===" headers before each file') .setDesc('Add "# === filename.md ===" headers before each file')
.addToggle(toggle => toggle .addToggle(toggle => toggle
@ -86,7 +133,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
})); }));
new Setting(containerEl) new Setting(el)
.setName('Show preview') .setName('Show preview')
.setDesc('Show preview modal before copying') .setDesc('Show preview modal before copying')
.addToggle(toggle => toggle .addToggle(toggle => toggle
@ -96,7 +143,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
})); }));
new Setting(containerEl) new Setting(el)
.setName('Include active note') .setName('Include active note')
.setDesc('Append currently open note to context') .setDesc('Append currently open note to context')
.addToggle(toggle => toggle .addToggle(toggle => toggle
@ -106,7 +153,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
})); }));
new Setting(containerEl) new Setting(el)
.setName('Excluded files') .setName('Excluded files')
.setDesc('Comma-separated filenames to exclude (e.g. "examples.md, drafts.md")') .setDesc('Comma-separated filenames to exclude (e.g. "examples.md, drafts.md")')
.addText(text => text .addText(text => text
@ -119,17 +166,18 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
.filter(s => s.length > 0); .filter(s => s.length > 0);
await this.plugin.saveSettings(); await this.plugin.saveSettings();
})); }));
}
// === CONTEXT SOURCES SECTION === // === TAB: Sources ===
containerEl.createEl('h3', { text: 'Context Sources' });
const sourcesDesc = containerEl.createEl('p', { private renderSourcesTab(el: HTMLElement) {
const desc = el.createEl('p', {
text: 'Add additional context sources like freetext, external files, or shell command output.', text: 'Add additional context sources like freetext, external files, or shell command output.',
cls: 'setting-item-description' cls: 'setting-item-description'
}); });
sourcesDesc.style.marginBottom = '10px'; desc.style.marginBottom = '10px';
const buttonContainer = containerEl.createDiv({ cls: 'sources-button-container' }); const buttonContainer = el.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';
@ -149,11 +197,10 @@ 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();
}); });
// Sources list const sourcesContainer = el.createDiv({ cls: 'sources-list-container' });
const sourcesContainer = containerEl.createDiv({ cls: 'sources-list-container' });
this.renderSourcesList(sourcesContainer); this.renderSourcesList(sourcesContainer);
new Setting(containerEl) new Setting(el)
.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
@ -162,32 +209,33 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
this.plugin.settings.showSourceLabels = value; this.plugin.settings.showSourceLabels = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
})); }));
}
// === PROMPT TEMPLATES SECTION === // === TAB: Templates ===
containerEl.createEl('h3', { text: 'Prompt Templates' });
const templatesDesc = containerEl.createEl('p', { private renderTemplatesTab(el: HTMLElement) {
const desc = el.createEl('p', {
text: 'Create reusable prompt templates that wrap around your context.', text: 'Create reusable prompt templates that wrap around your context.',
cls: 'setting-item-description' cls: 'setting-item-description'
}); });
templatesDesc.style.marginBottom = '10px'; desc.style.marginBottom = '10px';
const templateButtonContainer = containerEl.createDiv({ cls: 'templates-button-container' }); const buttonContainer = el.createDiv({ cls: 'templates-button-container' });
templateButtonContainer.style.display = 'flex'; buttonContainer.style.display = 'flex';
templateButtonContainer.style.gap = '8px'; buttonContainer.style.gap = '8px';
templateButtonContainer.style.marginBottom = '15px'; buttonContainer.style.marginBottom = '15px';
const addTemplateBtn = templateButtonContainer.createEl('button', { text: '+ New Template' }); const addTemplateBtn = buttonContainer.createEl('button', { text: '+ New Template' });
addTemplateBtn.addEventListener('click', () => { addTemplateBtn.addEventListener('click', () => {
new TemplateModal(this.app, this.plugin, null, () => this.display()).open(); new TemplateModal(this.app, this.plugin, null, () => this.display()).open();
}); });
const importBtn = templateButtonContainer.createEl('button', { text: 'Import' }); const importBtn = buttonContainer.createEl('button', { text: 'Import' });
importBtn.addEventListener('click', () => { importBtn.addEventListener('click', () => {
new TemplateImportExportModal(this.app, this.plugin, 'import', () => this.display()).open(); new TemplateImportExportModal(this.app, this.plugin, 'import', () => this.display()).open();
}); });
const exportBtn = templateButtonContainer.createEl('button', { text: 'Export' }); const exportBtn = buttonContainer.createEl('button', { text: 'Export' });
exportBtn.addEventListener('click', () => { exportBtn.addEventListener('click', () => {
new TemplateImportExportModal(this.app, this.plugin, 'export', () => {}).open(); new TemplateImportExportModal(this.app, this.plugin, 'export', () => {}).open();
}); });
@ -195,7 +243,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 = containerEl.createDiv(); const starterContainer = el.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';
@ -215,12 +263,10 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
}); });
} }
// Templates list const templatesContainer = el.createDiv({ cls: 'templates-list-container' });
const templatesContainer = containerEl.createDiv({ cls: 'templates-list-container' });
this.renderTemplatesList(templatesContainer); this.renderTemplatesList(templatesContainer);
// Default template setting new Setting(el)
new Setting(containerEl)
.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 => {
@ -234,17 +280,82 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}); });
}); });
}
// === HISTORY SECTION === // === TAB: Output ===
containerEl.createEl('h3', { text: 'Context History' });
const historyDesc = containerEl.createEl('p', { 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';
const buttonContainer = el.createDiv({ cls: 'targets-button-container' });
buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '8px';
buttonContainer.style.marginBottom = '15px';
const addTargetBtn = buttonContainer.createEl('button', { text: '+ New Target' });
addTargetBtn.addEventListener('click', () => {
new TargetModal(this.app, this.plugin, null, () => this.display()).open();
});
const addBuiltinsBtn = buttonContainer.createEl('button', { text: 'Add Built-in Targets' });
addBuiltinsBtn.addEventListener('click', async () => {
const { BUILTIN_TARGETS } = await import('./targets');
const existingIds = this.plugin.settings.targets.map(t => t.id);
const newTargets = BUILTIN_TARGETS.filter(t => !existingIds.includes(t.id));
if (newTargets.length === 0) {
new Notice('All built-in targets already added');
return;
}
this.plugin.settings.targets.push(...newTargets);
await this.plugin.saveSettings();
new Notice(`Added ${newTargets.length} built-in target(s)`);
this.display();
});
const targetsContainer = el.createDiv({ cls: 'targets-list-container' });
this.renderTargetsList(targetsContainer);
new Setting(el)
.setName('Primary target')
.setDesc('This target\'s output is copied to clipboard')
.addDropdown(dropdown => {
dropdown.addOption('', 'First enabled target');
for (const target of this.plugin.settings.targets) {
dropdown.addOption(target.id, target.name);
}
dropdown.setValue(this.plugin.settings.primaryTargetId || '');
dropdown.onChange(async (value) => {
this.plugin.settings.primaryTargetId = value || null;
await this.plugin.saveSettings();
});
});
new Setting(el)
.setName('Output folder')
.setDesc('Folder for secondary target files')
.addText(text => text
.setPlaceholder('_claude/outputs')
.setValue(this.plugin.settings.targetOutputFolder)
.onChange(async (value) => {
this.plugin.settings.targetOutputFolder = value || '_claude/outputs';
await this.plugin.saveSettings();
}));
}
// === TAB: History ===
private renderHistoryTab(el: HTMLElement) {
const desc = el.createEl('p', {
text: 'Track and compare previously generated contexts. Useful for iterative LLM workflows.', text: 'Track and compare previously generated contexts. Useful for iterative LLM workflows.',
cls: 'setting-item-description' cls: 'setting-item-description'
}); });
historyDesc.style.marginBottom = '10px'; desc.style.marginBottom = '10px';
new Setting(containerEl) new Setting(el)
.setName('Enable history') .setName('Enable history')
.setDesc('Save generated contexts for later review and comparison') .setDesc('Save generated contexts for later review and comparison')
.addToggle(toggle => toggle .addToggle(toggle => toggle
@ -256,7 +367,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
})); }));
if (this.plugin.settings.history.enabled) { if (this.plugin.settings.history.enabled) {
new Setting(containerEl) new Setting(el)
.setName('Storage folder') .setName('Storage folder')
.setDesc('Folder in your vault where history entries are stored') .setDesc('Folder in your vault where history entries are stored')
.addText(text => text .addText(text => text
@ -267,7 +378,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
})); }));
new Setting(containerEl) new Setting(el)
.setName('Maximum entries') .setName('Maximum entries')
.setDesc('Oldest entries will be deleted when limit is exceeded') .setDesc('Oldest entries will be deleted when limit is exceeded')
.addText(text => text .addText(text => text
@ -281,7 +392,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
} }
})); }));
new Setting(containerEl) new Setting(el)
.setName('Auto-cleanup (days)') .setName('Auto-cleanup (days)')
.setDesc('Delete entries older than this many days (0 = disabled)') .setDesc('Delete entries older than this many days (0 = disabled)')
.addText(text => text .addText(text => text
@ -295,8 +406,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
} }
})); }));
// History actions const historyActions = el.createDiv();
const historyActions = containerEl.createDiv();
historyActions.style.display = 'flex'; historyActions.style.display = 'flex';
historyActions.style.gap = '8px'; historyActions.style.gap = '8px';
historyActions.style.marginTop = '10px'; historyActions.style.marginTop = '10px';
@ -312,72 +422,6 @@ export class ClaudeContextSettingTab extends PluginSettingTab {
new Notice(`Cleaned up ${deleted} old entries`); new Notice(`Cleaned up ${deleted} old entries`);
}); });
} }
// === OUTPUT TARGETS SECTION ===
containerEl.createEl('h3', { text: 'Output Targets' });
const targetsDesc = containerEl.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'
});
targetsDesc.style.marginBottom = '10px';
const targetButtonContainer = containerEl.createDiv({ cls: 'targets-button-container' });
targetButtonContainer.style.display = 'flex';
targetButtonContainer.style.gap = '8px';
targetButtonContainer.style.marginBottom = '15px';
const addTargetBtn = targetButtonContainer.createEl('button', { text: '+ New Target' });
addTargetBtn.addEventListener('click', () => {
new TargetModal(this.app, this.plugin, null, () => this.display()).open();
});
const addBuiltinsBtn = targetButtonContainer.createEl('button', { text: 'Add Built-in Targets' });
addBuiltinsBtn.addEventListener('click', async () => {
const { BUILTIN_TARGETS } = await import('./targets');
const existingIds = this.plugin.settings.targets.map(t => t.id);
const newTargets = BUILTIN_TARGETS.filter(t => !existingIds.includes(t.id));
if (newTargets.length === 0) {
new Notice('All built-in targets already added');
return;
}
this.plugin.settings.targets.push(...newTargets);
await this.plugin.saveSettings();
new Notice(`Added ${newTargets.length} built-in target(s)`);
this.display();
});
// Targets list
const targetsContainer = containerEl.createDiv({ cls: 'targets-list-container' });
this.renderTargetsList(targetsContainer);
// Primary target setting
new Setting(containerEl)
.setName('Primary target')
.setDesc('This target\'s output is copied to clipboard')
.addDropdown(dropdown => {
dropdown.addOption('', 'First enabled target');
for (const target of this.plugin.settings.targets) {
dropdown.addOption(target.id, target.name);
}
dropdown.setValue(this.plugin.settings.primaryTargetId || '');
dropdown.onChange(async (value) => {
this.plugin.settings.primaryTargetId = value || null;
await this.plugin.saveSettings();
});
});
// Output folder for secondary targets
new Setting(containerEl)
.setName('Output folder')
.setDesc('Folder for secondary target files')
.addText(text => text
.setPlaceholder('_claude/outputs')
.setValue(this.plugin.settings.targetOutputFolder)
.onChange(async (value) => {
this.plugin.settings.targetOutputFolder = value || '_claude/outputs';
await this.plugin.saveSettings();
}));
} }
private renderSourcesList(container: HTMLElement) { private renderSourcesList(container: HTMLElement) {

View file

@ -1,8 +1,33 @@
/* /* Tab navigation for settings */
.cc-settings-tabs {
display: flex;
gap: 0;
border-bottom: 1px solid var(--background-modifier-border);
margin-bottom: 16px;
position: sticky;
top: 0;
background: var(--background-primary);
z-index: 1;
padding-top: 4px;
}
This CSS file will be included with your plugin, and .cc-settings-tab {
available in the app when your plugin is enabled. padding: 8px 16px;
border: none;
background: none;
color: var(--text-muted);
cursor: pointer;
font-size: 14px;
border-bottom: 2px solid transparent;
transition: color 0.15s ease, border-color 0.15s ease;
}
If your plugin does not need CSS, delete this file. .cc-settings-tab:hover {
color: var(--text-normal);
}
*/ .cc-settings-tab.is-active {
color: var(--text-normal);
border-bottom-color: var(--interactive-accent);
font-weight: 600;
}