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:
parent
e922523dc9
commit
de839d81c3
2 changed files with 180 additions and 111 deletions
256
src/settings.ts
256
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) {
|
||||
|
|
|
|||
35
styles.css
35
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue