Files
waypoint/src/settings-tab.ts
T
olivier 765c6f3b47 feat: chevron on right + tabbed settings + display customization
View:
- Chevron moved from left side to right side of bookmark rows
- Display settings applied as CSS variables on the view element

Settings:
- Tabbed UI with 4 tabs: Calendar, Periodic Notes, Recent Files, Display
- Display tab: row size, row spacing, indent size, font size, icon size, calendar cell size
- All settings use sliders with live preview and dynamic tooltips
- Reset to defaults button

CSS:
- Bookmark items use CSS variables (--wp-row-size, --wp-row-spacing, etc.)
- Calendar cells use --wp-cal-cell-size for configurable height
- Settings tab styling with active indicator
2026-06-12 12:56:27 -04:00

301 lines
9.2 KiB
TypeScript

import { Setting, PluginSettingTab, App, Plugin } from 'obsidian';
import { WaypointSettings, DEFAULT_SETTINGS, PeriodNoteSettings } from 'src/settings';
export class WaypointSettingTab extends PluginSettingTab {
private plugin: Plugin;
private settings: WaypointSettings;
private onSettingsChange: () => void;
private activeTab: 'calendar' | 'periodic' | 'recent' | 'display' = 'calendar';
constructor(app: App, plugin: Plugin, settings: WaypointSettings, onSettingsChange: () => void) {
super(app, plugin);
this.plugin = plugin;
this.settings = settings;
this.onSettingsChange = onSettingsChange;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
// ── Tab bar ──
const tabBar = containerEl.createDiv({ cls: 'waypoint-settings-tabs' });
const tabs = [
{ key: 'calendar' as const, label: 'Calendar' },
{ key: 'periodic' as const, label: 'Periodic Notes' },
{ key: 'recent' as const, label: 'Recent Files' },
{ key: 'display' as const, label: 'Display' },
];
for (const tab of tabs) {
const btn = tabBar.createEl('button', {
cls: `waypoint-settings-tab${this.activeTab === tab.key ? ' is-active' : ''}`,
text: tab.label,
});
btn.addEventListener('click', () => {
this.activeTab = tab.key;
this.display();
});
}
const tabContent = containerEl.createDiv({ cls: 'waypoint-settings-content' });
switch (this.activeTab) {
case 'calendar':
this.renderCalendarTab(tabContent);
break;
case 'periodic':
this.renderPeriodicTab(tabContent);
break;
case 'recent':
this.renderRecentTab(tabContent);
break;
case 'display':
this.renderDisplayTab(tabContent);
break;
}
}
// ═══════════════════════════════
// Calendar tab
// ═══════════════════════════════
private renderCalendarTab(container: HTMLElement): void {
new Setting(container)
.setName('First day of week')
.setDesc('Which day the calendar week starts on.')
.addDropdown((dropdown) => {
dropdown
.addOption('0', 'Sunday')
.addOption('1', 'Monday')
.setValue(String(this.settings.calendar.firstDayOfWeek))
.onChange((value) => {
this.settings.calendar.firstDayOfWeek = parseInt(value, 10);
this.saveAndRefresh();
});
});
new Setting(container)
.setName('Show note indicators')
.setDesc('Show a dot on days that have existing notes.')
.addToggle((toggle) => {
toggle
.setValue(this.settings.calendar.showNoteIndicators)
.onChange((value) => {
this.settings.calendar.showNoteIndicators = value;
this.saveAndRefresh();
});
});
}
// ═══════════════════════════════
// Periodic Notes tab
// ═══════════════════════════════
private renderPeriodicTab(container: HTMLElement): void {
this.addPeriodNoteSettings(container, 'Daily', this.settings.daily);
this.addPeriodNoteSettings(container, 'Weekly', this.settings.weekly);
this.addPeriodNoteSettings(container, 'Monthly', this.settings.monthly);
this.addPeriodNoteSettings(container, 'Quarterly', this.settings.quarterly);
this.addPeriodNoteSettings(container, 'Yearly', this.settings.yearly);
}
private addPeriodNoteSettings(container: HTMLElement, label: string, period: PeriodNoteSettings): void {
new Setting(container).setHeading().setName(label);
new Setting(container)
.setName('Folder')
.setDesc(`Folder path for ${label.toLowerCase()} notes.`)
.addText((text) => {
text.setPlaceholder('periodic/daily');
text.setValue(period.folder);
text.onChange((value) => {
period.folder = value;
this.saveAndRefresh();
});
});
new Setting(container)
.setName('Name format')
.setDesc(`Date format for ${label.toLowerCase()} note filenames (moment.js format).`)
.addText((text) => {
text.setPlaceholder('yyyy-MM-dd');
text.setValue(period.nameFormat);
text.onChange((value) => {
period.nameFormat = value;
this.saveAndRefresh();
});
});
new Setting(container)
.setName('Template file')
.setDesc(`Path to the template file (without .md extension).`)
.addText((text) => {
text.setPlaceholder('Templates/Daily note');
text.setValue(period.templateFile);
text.onChange((value) => {
period.templateFile = value;
this.saveAndRefresh();
});
});
new Setting(container)
.setName('Type property')
.setDesc(`Value for the 'type' frontmatter property.`)
.addText((text) => {
text.setPlaceholder('daily-note');
text.setValue(period.typeProperty);
text.onChange((value) => {
period.typeProperty = value;
this.saveAndRefresh();
});
});
}
// ═══════════════════════════════
// Recent Files tab
// ═══════════════════════════════
private renderRecentTab(container: HTMLElement): void {
new Setting(container)
.setName('Max items')
.setDesc('Maximum number of recent files to track.')
.addText((text) => {
text.inputEl.setAttr('type', 'number');
text.inputEl.setAttr('placeholder', '50');
text.setValue(String(this.settings.recentFiles.maxItems));
text.inputEl.onblur = () => {
const val = parseInt(text.getValue(), 10);
if (!isNaN(val) && val > 0) {
this.settings.recentFiles.maxItems = val;
this.saveAndRefresh();
}
};
});
new Setting(container)
.setName('Update on')
.setDesc('When to add a file to the recent list.')
.addDropdown((dropdown) => {
dropdown
.addOption('file-open', 'File opened')
.addOption('file-edit', 'File changed')
.setValue(this.settings.recentFiles.updateOn)
.onChange((value: 'file-open' | 'file-edit') => {
this.settings.recentFiles.updateOn = value;
this.saveAndRefresh();
});
});
const pathDesc = new DocumentFragment();
pathDesc.appendText('Regex patterns for paths to exclude. One per line.');
new Setting(container)
.setName('Omitted paths')
.setDesc(pathDesc)
.addTextArea((text) => {
text.inputEl.setAttr('rows', 4);
text.setPlaceholder('^archives/\n\\.png$');
text.setValue(this.settings.recentFiles.omittedPaths.join('\n'));
text.inputEl.onblur = () => {
this.settings.recentFiles.omittedPaths = text.getValue().split('\n').filter(p => p.trim());
this.saveAndRefresh();
};
});
const tagDesc = new DocumentFragment();
tagDesc.appendText('Regex patterns for frontmatter tags to exclude. One per line.');
new Setting(container)
.setName('Omitted tags')
.setDesc(tagDesc)
.addTextArea((text) => {
text.inputEl.setAttr('rows', 4);
text.setPlaceholder('ignore\narchive');
text.setValue(this.settings.recentFiles.omittedTags.join('\n'));
text.inputEl.onblur = () => {
this.settings.recentFiles.omittedTags = text.getValue().split('\n').filter(t => t.trim());
this.saveAndRefresh();
};
});
}
// ═══════════════════════════════
// Display tab
// ═══════════════════════════════
private renderDisplayTab(container: HTMLElement): void {
new Setting(container).setHeading().setName('Bookmarks');
this.addSliderSetting(container,
'Row size', 'Height of bookmark items.',
this.settings.display, 'rowSize', 18, 40, 1, 'px',
);
this.addSliderSetting(container,
'Row spacing', 'Gap between bookmark items.',
this.settings.display, 'rowSpacing', 0, 12, 1, 'px',
);
this.addSliderSetting(container,
'Indent size', 'Indent per nesting depth.',
this.settings.display, 'indentSize', 8, 32, 2, 'px',
);
this.addSliderSetting(container,
'Font size', 'Label font size.',
this.settings.display, 'fontSize', 10, 18, 1, 'px',
);
this.addSliderSetting(container,
'Icon size', 'Bookmark icon size.',
this.settings.display, 'iconSize', 12, 24, 1, 'px',
);
new Setting(container).setHeading().setName('Calendar');
this.addSliderSetting(container,
'Cell size', 'Height of calendar day cells.',
this.settings.display, 'calendarCellSize', 20, 48, 2, 'px',
);
new Setting(container)
.addButton((btn) =>
btn
.setButtonText('Reset to defaults')
.onClick(() => {
this.settings.display = { ...DEFAULT_SETTINGS.display };
this.saveAndRefresh();
this.display();
}),
);
}
private addSliderSetting(
container: HTMLElement,
name: string,
desc: string,
obj: any,
key: string,
min: number,
max: number,
step: number,
unit: string,
): void {
const setting = new Setting(container)
.setName(name)
.setDesc(`${desc} (${obj[key]}${unit})`);
setting.addSlider((slider) => {
slider
.setLimits(min, max, step)
.setValue(obj[key])
.setDynamicTooltip()
.onChange((value) => {
obj[key] = value;
setting.setDesc(`${desc} (${value}${unit})`);
this.saveAndRefresh();
});
});
}
private async saveAndRefresh(): Promise<void> {
await this.plugin.saveData(this.settings);
this.onSettingsChange();
}
}