605 lines
15 KiB
Markdown
605 lines
15 KiB
Markdown
# Plugin Settings UI Guide
|
|
|
|
This guide explains how to structure and format plugin settings UI in Obsidian, with examples for building multiple plugins with distinct features.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Core Concepts](#core-concepts)
|
|
2. [Setting Types](#setting-types)
|
|
3. [UI Organization Patterns](#ui-organization-patterns)
|
|
4. [Multi-Plugin Architecture](#multi-plugin-architecture)
|
|
5. [Plugin Examples](#plugin-examples)
|
|
|
|
## Core Concepts
|
|
|
|
### Settings Interface & Settings Tab
|
|
|
|
Every plugin has:
|
|
|
|
1. **Settings Interface** - TypeScript interface defining your data structure
|
|
2. **Settings Tab Class** - Extends `PluginSettingTab` and defines the UI
|
|
|
|
```typescript
|
|
// Define the data structure
|
|
export interface MyPluginSettings {
|
|
settingName: string;
|
|
settingValue: boolean;
|
|
}
|
|
|
|
// Default values
|
|
export const DEFAULT_SETTINGS: MyPluginSettings = {
|
|
settingName: 'default',
|
|
settingValue: false,
|
|
};
|
|
|
|
// UI definition
|
|
export class MySettingTab extends PluginSettingTab {
|
|
plugin: MyPlugin;
|
|
|
|
constructor(app: App, plugin: MyPlugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
display(): void {
|
|
const { containerEl } = this;
|
|
containerEl.empty(); // Clear previous UI
|
|
|
|
// Add UI elements here
|
|
}
|
|
}
|
|
```
|
|
|
|
### Container Structure
|
|
|
|
The `containerEl` is where you build your UI. Always start with `containerEl.empty()` to clear old content when the settings tab reopens.
|
|
|
|
```typescript
|
|
display(): void {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
|
|
// Add heading
|
|
containerEl.createEl('h2', { text: 'Plugin Name' });
|
|
|
|
// Add settings
|
|
new Setting(containerEl)
|
|
.setName('Setting Name')
|
|
.setDesc('Description of what this does')
|
|
.addText((text) => { /* ... */ });
|
|
}
|
|
```
|
|
|
|
## Setting Types
|
|
|
|
### Text Input
|
|
|
|
Single-line text input:
|
|
|
|
```typescript
|
|
new Setting(containerEl)
|
|
.setName('Plugin Name')
|
|
.setDesc('The name of your plugin')
|
|
.addText((text) =>
|
|
text
|
|
.setPlaceholder('Enter name')
|
|
.setValue(this.plugin.settings.pluginName)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.pluginName = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
```
|
|
|
|
### Text Area
|
|
|
|
Multi-line text input:
|
|
|
|
```typescript
|
|
new Setting(containerEl)
|
|
.setName('Configuration')
|
|
.setDesc('Multi-line configuration')
|
|
.addTextArea((text) =>
|
|
text
|
|
.setPlaceholder('Enter configuration')
|
|
.setValue(this.plugin.settings.config)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.config = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
```
|
|
|
|
### Toggle
|
|
|
|
Boolean switch:
|
|
|
|
```typescript
|
|
new Setting(containerEl)
|
|
.setName('Enable Feature')
|
|
.setDesc('Turn this feature on or off')
|
|
.addToggle((toggle) =>
|
|
toggle
|
|
.setValue(this.plugin.settings.enabled)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.enabled = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
```
|
|
|
|
### Dropdown
|
|
|
|
Select from predefined options:
|
|
|
|
```typescript
|
|
new Setting(containerEl)
|
|
.setName('Theme')
|
|
.setDesc('Choose a color theme')
|
|
.addDropdown((dropdown) =>
|
|
dropdown
|
|
.addOption('light', 'Light')
|
|
.addOption('dark', 'Dark')
|
|
.addOption('auto', 'Auto')
|
|
.setValue(this.plugin.settings.theme)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.theme = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
```
|
|
|
|
### Slider
|
|
|
|
Numeric input with range:
|
|
|
|
```typescript
|
|
new Setting(containerEl)
|
|
.setName('Search Depth')
|
|
.setDesc('How deep to search in folders (1-10)')
|
|
.addSlider((slider) =>
|
|
slider
|
|
.setMin(1)
|
|
.setMax(10)
|
|
.setStep(1)
|
|
.setValue(this.plugin.settings.searchDepth)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.searchDepth = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
```
|
|
|
|
### Button
|
|
|
|
Trigger an action:
|
|
|
|
```typescript
|
|
new Setting(containerEl)
|
|
.setName('Process Files')
|
|
.setDesc('Start processing all files')
|
|
.addButton((button) =>
|
|
button
|
|
.setButtonText('Process Now')
|
|
.onClick(async () => {
|
|
await this.plugin.processAllFiles();
|
|
new Notice('Processing complete!');
|
|
})
|
|
);
|
|
```
|
|
|
|
### Color Picker
|
|
|
|
Select a color:
|
|
|
|
```typescript
|
|
new Setting(containerEl)
|
|
.setName('Tag Color')
|
|
.setDesc('Color for tags')
|
|
.addColorPicker((color) =>
|
|
color
|
|
.setValue(this.plugin.settings.tagColor)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.tagColor = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
```
|
|
|
|
## UI Organization Patterns
|
|
|
|
### Section Headers
|
|
|
|
Organize settings into logical sections:
|
|
|
|
```typescript
|
|
display(): void {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
|
|
// Main heading
|
|
containerEl.createEl('h2', { text: 'Plugin Settings' });
|
|
|
|
// Section 1
|
|
containerEl.createEl('h3', { text: 'Basic Configuration' });
|
|
new Setting(containerEl).setName('Setting 1')...
|
|
new Setting(containerEl).setName('Setting 2')...
|
|
|
|
// Section 2
|
|
containerEl.createEl('h3', { text: 'Advanced Options' });
|
|
new Setting(containerEl).setName('Setting 3')...
|
|
new Setting(containerEl).setName('Setting 4')...
|
|
}
|
|
```
|
|
|
|
### Conditional Display
|
|
|
|
Show/hide settings based on other settings:
|
|
|
|
```typescript
|
|
display(): void {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
|
|
new Setting(containerEl)
|
|
.setName('Enable Advanced')
|
|
.addToggle((toggle) =>
|
|
toggle
|
|
.setValue(this.plugin.settings.advancedMode)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.advancedMode = value;
|
|
await this.plugin.saveSettings();
|
|
this.display(); // Redraw to show/hide advanced settings
|
|
})
|
|
);
|
|
|
|
if (this.plugin.settings.advancedMode) {
|
|
new Setting(containerEl)
|
|
.setName('Advanced Option')
|
|
.setDesc('This only shows when advanced mode is on')
|
|
.addText((text) => { /* ... */ });
|
|
}
|
|
}
|
|
```
|
|
|
|
### Grouped Controls
|
|
|
|
Multiple controls in one setting:
|
|
|
|
```typescript
|
|
new Setting(containerEl)
|
|
.setName('Color')
|
|
.setDesc('Choose color and opacity')
|
|
.addColorPicker((color) =>
|
|
color
|
|
.setValue(this.plugin.settings.color)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.color = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
.addSlider((slider) =>
|
|
slider
|
|
.setMin(0)
|
|
.setMax(100)
|
|
.setValue(this.plugin.settings.opacity)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.opacity = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
```
|
|
|
|
## Multi-Plugin Architecture
|
|
|
|
When building multiple plugins within one codebase, consider this structure:
|
|
|
|
### Folder Structure
|
|
|
|
```
|
|
src/
|
|
main.ts (Plugin entry point)
|
|
settings.ts (Shared settings)
|
|
plugins/
|
|
folder/
|
|
folderPlugin.ts
|
|
folderSettings.ts
|
|
markdown/
|
|
markdownPlugin.ts
|
|
markdownSettings.ts
|
|
kanban/
|
|
kanbanPlugin.ts
|
|
kanbanSettings.ts
|
|
```
|
|
|
|
### Unified Settings Interface
|
|
|
|
```typescript
|
|
// settings.ts
|
|
export interface PluginConfig {
|
|
// Folder Plugin settings
|
|
folderPlugin: {
|
|
enabled: boolean;
|
|
organizationHierarchy: string;
|
|
};
|
|
|
|
// Markdown Plugin settings
|
|
markdownPlugin: {
|
|
enabled: boolean;
|
|
autoCreatePath: string;
|
|
template: string;
|
|
};
|
|
|
|
// Kanban Plugin settings
|
|
kanbanPlugin: {
|
|
enabled: boolean;
|
|
boardTemplate: string;
|
|
defaultLayout: string;
|
|
};
|
|
}
|
|
|
|
export const DEFAULT_SETTINGS: PluginConfig = {
|
|
folderPlugin: {
|
|
enabled: true,
|
|
organizationHierarchy: 'Category, Status',
|
|
},
|
|
markdownPlugin: {
|
|
enabled: true,
|
|
autoCreatePath: 'Notes',
|
|
template: 'default',
|
|
},
|
|
kanbanPlugin: {
|
|
enabled: false,
|
|
boardTemplate: 'default',
|
|
defaultLayout: 'columns',
|
|
},
|
|
};
|
|
```
|
|
|
|
### Unified Settings Tab
|
|
|
|
```typescript
|
|
// settings.ts
|
|
export class UnifiedSettingTab extends PluginSettingTab {
|
|
plugin: MultiPluginBase;
|
|
|
|
display(): void {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
|
|
containerEl.createEl('h2', { text: 'Plugin Suite Settings' });
|
|
|
|
// Folder Plugin Section
|
|
this.displayFolderPluginSettings(containerEl);
|
|
|
|
// Markdown Plugin Section
|
|
this.displayMarkdownPluginSettings(containerEl);
|
|
|
|
// Kanban Plugin Section
|
|
this.displayKanbanPluginSettings(containerEl);
|
|
}
|
|
|
|
displayFolderPluginSettings(containerEl: HTMLElement): void {
|
|
containerEl.createEl('h3', { text: 'Folder Organization Plugin' });
|
|
|
|
new Setting(containerEl)
|
|
.setName('Enable Folder Plugin')
|
|
.addToggle((toggle) =>
|
|
toggle
|
|
.setValue(this.plugin.settings.folderPlugin.enabled)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.folderPlugin.enabled = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
|
|
if (this.plugin.settings.folderPlugin.enabled) {
|
|
new Setting(containerEl)
|
|
.setName('Organization Hierarchy')
|
|
.setDesc('Comma-separated frontmatter fields')
|
|
.addTextArea((text) =>
|
|
text
|
|
.setValue(this.plugin.settings.folderPlugin.organizationHierarchy)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.folderPlugin.organizationHierarchy = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
displayMarkdownPluginSettings(containerEl: HTMLElement): void {
|
|
containerEl.createEl('h3', { text: 'Markdown Auto-Create Plugin' });
|
|
|
|
new Setting(containerEl)
|
|
.setName('Enable Markdown Plugin')
|
|
.addToggle((toggle) =>
|
|
toggle
|
|
.setValue(this.plugin.settings.markdownPlugin.enabled)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.markdownPlugin.enabled = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
|
|
if (this.plugin.settings.markdownPlugin.enabled) {
|
|
new Setting(containerEl)
|
|
.setName('Auto-Create Path')
|
|
.setDesc('Default folder for auto-created files')
|
|
.addText((text) =>
|
|
text
|
|
.setPlaceholder('Notes')
|
|
.setValue(this.plugin.settings.markdownPlugin.autoCreatePath)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.markdownPlugin.autoCreatePath = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
|
|
new Setting(containerEl)
|
|
.setName('Template')
|
|
.setDesc('Template to use for new files')
|
|
.addDropdown((dropdown) =>
|
|
dropdown
|
|
.addOption('default', 'Default')
|
|
.addOption('minimal', 'Minimal')
|
|
.addOption('detailed', 'Detailed')
|
|
.setValue(this.plugin.settings.markdownPlugin.template)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.markdownPlugin.template = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
displayKanbanPluginSettings(containerEl: HTMLElement): void {
|
|
containerEl.createEl('h3', { text: 'Kanban Board Plugin' });
|
|
|
|
new Setting(containerEl)
|
|
.setName('Enable Kanban Plugin')
|
|
.addToggle((toggle) =>
|
|
toggle
|
|
.setValue(this.plugin.settings.kanbanPlugin.enabled)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.kanbanPlugin.enabled = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
|
|
if (this.plugin.settings.kanbanPlugin.enabled) {
|
|
new Setting(containerEl)
|
|
.setName('Board Template')
|
|
.setDesc('Default board template')
|
|
.addDropdown((dropdown) =>
|
|
dropdown
|
|
.addOption('default', 'Default')
|
|
.addOption('agile', 'Agile')
|
|
.addOption('gtd', 'Getting Things Done')
|
|
.setValue(this.plugin.settings.kanbanPlugin.boardTemplate)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.kanbanPlugin.boardTemplate = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
|
|
new Setting(containerEl)
|
|
.setName('Default Layout')
|
|
.setDesc('How boards are displayed by default')
|
|
.addDropdown((dropdown) =>
|
|
dropdown
|
|
.addOption('columns', 'Columns')
|
|
.addOption('rows', 'Rows')
|
|
.addOption('grid', 'Grid')
|
|
.setValue(this.plugin.settings.kanbanPlugin.defaultLayout)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.kanbanPlugin.defaultLayout = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Plugin Examples
|
|
|
|
### Example 1: Folder Organization Plugin
|
|
|
|
**Settings Interface:**
|
|
```typescript
|
|
export interface FolderOrganizerSettings {
|
|
enabled: boolean;
|
|
organizationHierarchy: string;
|
|
createMissingFolders: boolean;
|
|
overwriteExisting: boolean;
|
|
excludeFolders: string[];
|
|
}
|
|
```
|
|
|
|
**Key UI Elements:**
|
|
- Toggle to enable/disable
|
|
- Text area for hierarchy configuration
|
|
- Toggle for auto-create folders
|
|
- Toggle for overwrite confirmation
|
|
- Multi-select or list for excluded folders
|
|
- Action button to organize files
|
|
|
|
### Example 2: Markdown Auto-Create Plugin
|
|
|
|
**Settings Interface:**
|
|
```typescript
|
|
export interface MarkdownAutoCreateSettings {
|
|
enabled: boolean;
|
|
defaultPath: string;
|
|
template: 'default' | 'minimal' | 'detailed';
|
|
autoOpenNewFile: boolean;
|
|
defaultFrontmatter: string;
|
|
hotkey: string;
|
|
}
|
|
```
|
|
|
|
**Key UI Elements:**
|
|
- Toggle to enable/disable
|
|
- Text input for default path
|
|
- Dropdown for template selection
|
|
- Toggle for auto-open
|
|
- Text area for frontmatter template
|
|
- Display current hotkey (or hotkey picker if available)
|
|
|
|
### Example 3: Kanban Board Plugin
|
|
|
|
**Settings Interface:**
|
|
```typescript
|
|
export interface KanbanBoardSettings {
|
|
enabled: boolean;
|
|
boardTemplate: 'default' | 'agile' | 'gtd';
|
|
defaultLayout: 'columns' | 'rows' | 'grid';
|
|
cardWidth: number;
|
|
showDates: boolean;
|
|
showTags: boolean;
|
|
defaultColumns: string[];
|
|
darkMode: boolean;
|
|
}
|
|
```
|
|
|
|
**Key UI Elements:**
|
|
- Toggle to enable/disable
|
|
- Dropdown for board templates
|
|
- Dropdown for default layout
|
|
- Slider for card width
|
|
- Toggles for feature flags
|
|
- Text area for default columns (comma-separated)
|
|
- Color picker for theme customization
|
|
|
|
## Best Practices
|
|
|
|
1. **Always save settings** - Call `await this.plugin.saveSettings()` after any change
|
|
2. **Use descriptive names** - Users should understand what each setting does
|
|
3. **Group related settings** - Use section headers (h3) to organize
|
|
4. **Provide defaults** - Always have sensible defaults in `DEFAULT_SETTINGS`
|
|
5. **Validate input** - Check user input before saving (trim, validate paths, etc.)
|
|
6. **Use conditionals wisely** - Show/hide advanced options based on basic settings
|
|
7. **Provide feedback** - Use `new Notice()` to confirm actions
|
|
8. **Redraw on enable/disable** - Call `this.display()` to update UI when features toggle
|
|
9. **Document complex settings** - Use `.setDesc()` liberally
|
|
10. **Test multi-step workflows** - Ensure settings changes work with all plugins enabled
|
|
|
|
## Testing Your Settings UI
|
|
|
|
```typescript
|
|
// In your test file
|
|
const plugin = new MyPlugin(app, manifest);
|
|
plugin.loadSettings();
|
|
plugin.registerSettingTab(new UnifiedSettingTab(app, plugin));
|
|
|
|
// Settings should load without errors
|
|
// Check that all toggles/inputs work
|
|
// Verify settings persist after reload
|
|
// Test conditional displays
|
|
```
|
|
|
|
---
|
|
|
|
This guide provides a foundation for building clean, organized, and user-friendly plugin settings. Adapt these patterns to your specific needs!
|