From de839d81c39b951642190ea5f10ebb4938001333 Mon Sep 17 00:00:00 2001 From: "Luca G. Oelfke" Date: Fri, 6 Feb 2026 11:14:17 +0100 Subject: [PATCH] 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 --- src/settings.ts | 256 ++++++++++++++++++++++++++++-------------------- styles.css | 35 ++++++- 2 files changed, 180 insertions(+), 111 deletions(-) diff --git a/src/settings.ts b/src/settings.ts index d3d2480..23ce77c 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -23,6 +23,7 @@ export interface ClaudeContextSettings { targets: OutputTarget[]; primaryTargetId: string | null; targetOutputFolder: string; + lastSettingsTab: string; } export const DEFAULT_SETTINGS: ClaudeContextSettings = { @@ -40,21 +41,67 @@ export const DEFAULT_SETTINGS: ClaudeContextSettings = { targets: [], primaryTargetId: null, 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 { plugin: ClaudeContextPlugin; + private activeTab: SettingsTabId; constructor(app: App, plugin: ClaudeContextPlugin) { super(app, plugin); this.plugin = plugin; + this.activeTab = (plugin.settings.lastSettingsTab as SettingsTabId) || 'general'; } display(): void { const { containerEl } = this; 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') .setDesc('Folder containing your context files') .addText(text => text @@ -65,7 +112,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); - new Setting(containerEl) + new Setting(el) .setName('Separator') .setDesc('Text between files (e.g. "---" or "***")') .addText(text => text @@ -76,7 +123,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); - new Setting(containerEl) + new Setting(el) .setName('Include filenames') .setDesc('Add "# === filename.md ===" headers before each file') .addToggle(toggle => toggle @@ -86,7 +133,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); - new Setting(containerEl) + new Setting(el) .setName('Show preview') .setDesc('Show preview modal before copying') .addToggle(toggle => toggle @@ -96,7 +143,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); - new Setting(containerEl) + new Setting(el) .setName('Include active note') .setDesc('Append currently open note to context') .addToggle(toggle => toggle @@ -106,7 +153,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); - new Setting(containerEl) + new Setting(el) .setName('Excluded files') .setDesc('Comma-separated filenames to exclude (e.g. "examples.md, drafts.md")') .addText(text => text @@ -119,17 +166,18 @@ export class ClaudeContextSettingTab extends PluginSettingTab { .filter(s => s.length > 0); await this.plugin.saveSettings(); })); + } - // === CONTEXT SOURCES SECTION === - containerEl.createEl('h3', { text: 'Context Sources' }); + // === TAB: 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.', 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.gap = '8px'; buttonContainer.style.marginBottom = '15px'; @@ -149,11 +197,10 @@ export class ClaudeContextSettingTab extends PluginSettingTab { new SourceModal(this.app, this.plugin, 'shell', null, () => this.display()).open(); }); - // Sources list - const sourcesContainer = containerEl.createDiv({ cls: 'sources-list-container' }); + const sourcesContainer = el.createDiv({ cls: 'sources-list-container' }); this.renderSourcesList(sourcesContainer); - new Setting(containerEl) + new Setting(el) .setName('Show source labels') .setDesc('Add position and name labels to source output') .addToggle(toggle => toggle @@ -162,32 +209,33 @@ export class ClaudeContextSettingTab extends PluginSettingTab { this.plugin.settings.showSourceLabels = value; await this.plugin.saveSettings(); })); + } - // === PROMPT TEMPLATES SECTION === - containerEl.createEl('h3', { text: 'Prompt Templates' }); + // === TAB: 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.', cls: 'setting-item-description' }); - templatesDesc.style.marginBottom = '10px'; + desc.style.marginBottom = '10px'; - const templateButtonContainer = containerEl.createDiv({ cls: 'templates-button-container' }); - templateButtonContainer.style.display = 'flex'; - templateButtonContainer.style.gap = '8px'; - templateButtonContainer.style.marginBottom = '15px'; + const buttonContainer = el.createDiv({ cls: 'templates-button-container' }); + buttonContainer.style.display = 'flex'; + buttonContainer.style.gap = '8px'; + buttonContainer.style.marginBottom = '15px'; - const addTemplateBtn = templateButtonContainer.createEl('button', { text: '+ New Template' }); + const addTemplateBtn = buttonContainer.createEl('button', { text: '+ New Template' }); addTemplateBtn.addEventListener('click', () => { 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', () => { 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', () => { new TemplateImportExportModal(this.app, this.plugin, 'export', () => {}).open(); }); @@ -195,7 +243,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab { // Starter templates const hasStarterTemplates = this.plugin.settings.promptTemplates.some(t => t.isBuiltin); if (!hasStarterTemplates) { - const starterContainer = containerEl.createDiv(); + const starterContainer = el.createDiv(); starterContainer.style.padding = '10px'; starterContainer.style.backgroundColor = 'var(--background-secondary)'; starterContainer.style.borderRadius = '4px'; @@ -215,12 +263,10 @@ export class ClaudeContextSettingTab extends PluginSettingTab { }); } - // Templates list - const templatesContainer = containerEl.createDiv({ cls: 'templates-list-container' }); + const templatesContainer = el.createDiv({ cls: 'templates-list-container' }); this.renderTemplatesList(templatesContainer); - // Default template setting - new Setting(containerEl) + new Setting(el) .setName('Default template') .setDesc('Template to use by default when copying context') .addDropdown(dropdown => { @@ -234,17 +280,82 @@ export class ClaudeContextSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); }); }); + } - // === HISTORY SECTION === - containerEl.createEl('h3', { text: 'Context History' }); + // === TAB: Output === - 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.', cls: 'setting-item-description' }); - historyDesc.style.marginBottom = '10px'; + desc.style.marginBottom = '10px'; - new Setting(containerEl) + new Setting(el) .setName('Enable history') .setDesc('Save generated contexts for later review and comparison') .addToggle(toggle => toggle @@ -256,7 +367,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab { })); if (this.plugin.settings.history.enabled) { - new Setting(containerEl) + new Setting(el) .setName('Storage folder') .setDesc('Folder in your vault where history entries are stored') .addText(text => text @@ -267,7 +378,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); - new Setting(containerEl) + new Setting(el) .setName('Maximum entries') .setDesc('Oldest entries will be deleted when limit is exceeded') .addText(text => text @@ -281,7 +392,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab { } })); - new Setting(containerEl) + new Setting(el) .setName('Auto-cleanup (days)') .setDesc('Delete entries older than this many days (0 = disabled)') .addText(text => text @@ -295,8 +406,7 @@ export class ClaudeContextSettingTab extends PluginSettingTab { } })); - // History actions - const historyActions = containerEl.createDiv(); + const historyActions = el.createDiv(); historyActions.style.display = 'flex'; historyActions.style.gap = '8px'; historyActions.style.marginTop = '10px'; @@ -312,72 +422,6 @@ export class ClaudeContextSettingTab extends PluginSettingTab { 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) { diff --git a/styles.css b/styles.css index 71cc60f..f18c57e 100644 --- a/styles.css +++ b/styles.css @@ -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 -available in the app when your plugin is enabled. +.cc-settings-tab { + 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; +}