feat: add date systems menu to calendar days

Right-clicking a calendar day now lists every configured date-prefixed note system. Journal notes are singletons; {title} formats support multiple notes per date, such as meetings.\n\nAdds default Journal and Meeting systems, configurable settings UI, safe filename/title handling, one shared dated-note creation path, and a compiled menu smoke test fixture in the test vault.
This commit is contained in:
2026-09-07 21:07:18 -04:00
parent b617c275eb
commit 26a1a481ac
10 changed files with 731 additions and 76 deletions
+98 -9
View File
@@ -15,6 +15,7 @@ import {
type PaneType,
} from 'obsidian';
import type WaypointPlugin from 'src/main';
import type { DateSystemNotes } from 'src/main';
import { getMonthGrid } from 'src/utils/date-utils';
import { BookmarkItem } from 'src/models/bookmark';
@@ -224,6 +225,11 @@ export class WaypointView extends ItemView {
this.plugin.openPeriodNote('day', day.date, this.app.workspace.getLeaf('tab'));
}
});
cell.addEventListener('contextmenu', (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
this.showDayContextMenu(event, day.date);
});
}
}
@@ -236,6 +242,79 @@ export class WaypointView extends ItemView {
this.redraw();
}
/**
* Right-click menu for a day cell: every date system's existing notes for
* that date, plus the actions that would create the ones it is missing.
*/
private showDayContextMenu(event: MouseEvent, date: moment.Moment): void {
const menu = new Menu();
menu.addItem((i) => i.setTitle(date.format('dddd, MMMM D, YYYY')).setIsLabel(true));
for (const bucket of this.plugin.findDateSystemNotes(date)) {
// Many-per-date systems can always take another note; one-per-date
// systems only offer creation while their single note is missing.
const canCreate = bucket.multiple || bucket.notes.length === 0;
if (bucket.notes.length + (canCreate ? 1 : 0) === 0) continue;
menu.addSeparator();
for (const note of bucket.notes) {
menu.addItem((i) =>
i
.setTitle(note.label)
.setIcon(bucket.system.icon)
.onClick((evt) => this.focusFile(note.file, Keymap.isModEvent(evt))),
);
}
if (!canCreate) continue;
const noun = bucket.system.name.toLowerCase();
menu.addItem((i) =>
i
// The ellipsis promises the prompt that a free-text title needs.
.setTitle(bucket.multiple ? `New ${noun} note…` : `New ${noun} note`)
.setIcon('plus')
.onClick((evt) => this.createDateSystemNote(bucket, date, Keymap.isModEvent(evt))),
);
}
menu.showAtPosition({ x: event.clientX, y: event.clientY });
}
/**
* Creates a date system's note, asking for a title first when the system
* holds many notes per date. A held modifier opens the result in a new pane.
*/
private createDateSystemNote(bucket: DateSystemNotes, date: moment.Moment, newLeaf: PaneType | boolean): void {
if (!bucket.multiple) {
void this.plugin.openDateSystemNote(bucket.system, date, {
leaf: newLeaf ? this.app.workspace.getLeaf(newLeaf) : undefined,
});
return;
}
const name = bucket.system.name;
new PromptModal(
this.app,
{
title: `New ${name.toLowerCase()} note`,
placeholder: `${name} with…`,
cta: 'Create',
},
(title) => {
// `getLeaf` materialises the pane immediately, so the target pane is
// resolved only after the title is confirmed — cancelling the prompt
// must not leave an empty tab behind.
void this.plugin.openDateSystemNote(bucket.system, date, {
title,
leaf: newLeaf ? this.app.workspace.getLeaf(newLeaf) : undefined,
});
},
).open();
}
// ════════════════════════════════════════
// Recent Files panel
// ════════════════════════════════════════
@@ -1062,7 +1141,7 @@ export class WaypointView extends ItemView {
}
private promptRename(item: BookmarkItem): void {
new RenameModal(this.app, item.label, (newLabel) => {
new PromptModal(this.app, { title: 'Rename bookmark', initialValue: item.label }, (newLabel) => {
if (newLabel && newLabel.trim()) {
this.plugin.updateBookmark(item.id, { label: newLabel.trim() });
}
@@ -1077,24 +1156,34 @@ export class WaypointView extends ItemView {
}
}
// ── Rename modal ──
// ── Text prompt modal ──
class RenameModal extends Modal {
private currentValue: string;
interface PromptModalOptions {
title: string;
placeholder?: string;
initialValue?: string;
/** Submit button label. Defaults to 'Save'. */
cta?: string;
}
/** Single-line text prompt, shared by bookmark renaming and note creation. */
class PromptModal extends Modal {
private options: PromptModalOptions;
private onSubmit: (value: string) => void;
constructor(app: App, currentValue: string, onSubmit: (value: string) => void) {
constructor(app: App, options: PromptModalOptions, onSubmit: (value: string) => void) {
super(app);
this.currentValue = currentValue;
this.options = options;
this.onSubmit = onSubmit;
}
onOpen(): void {
this.titleEl.setText('Rename bookmark');
this.titleEl.setText(this.options.title);
const input = this.contentEl.createEl('input', {
type: 'text',
value: this.currentValue,
value: this.options.initialValue ?? '',
placeholder: this.options.placeholder ?? '',
});
input.style.width = '100%';
input.style.marginBottom = '12px';
@@ -1107,7 +1196,7 @@ class RenameModal extends Modal {
cancelBtn.style.marginRight = '8px';
cancelBtn.addEventListener('click', () => this.close());
const saveBtn = btnContainer.createEl('button', { text: 'Save', cls: 'mod-cta' });
const saveBtn = btnContainer.createEl('button', { text: this.options.cta ?? 'Save', cls: 'mod-cta' });
saveBtn.addEventListener('click', () => {
this.onSubmit(input.value);
this.close();