4 Commits

Author SHA1 Message Date
olivier e8ba4cd2ad release v1.7.0: redesigned calendar with coloured system indicators 2026-09-08 10:18:43 -04:00
olivier 5a9b7bfff8 docs: describe calendar indicator defaults and autocomplete 2026-09-07 21:53:03 -04:00
olivier 72a61e49ee fix: default calendar to system indicators
Use coloured system markers by default, preserve configured colours through settings migration, expose all template fields through native vault-path autocomplete, and remove the redundant Calendar heading.
2026-09-07 21:52:34 -04:00
olivier 069456799e feat: add coloured calendar system indicators
Add a selectable single-dot or per-system indicator mode. Date systems now carry configurable colours; results are cached per displayed month and invalidated when paths/settings change. Refine calendar hierarchy, spacing, hover, and today states.
2026-09-07 21:43:21 -04:00
11 changed files with 347 additions and 85 deletions
+39 -27
View File
@@ -51,14 +51,14 @@ Detaches all leaves of `WAYPOINT_VIEW_TYPE`.
```typescript ```typescript
interface WaypointSettings { interface WaypointSettings {
calendar: CalendarSettings; // firstDayOfWeek (0=Sun,1=Mon), showNoteIndicators calendar: CalendarSettings; // week start, indicator visibility/style, daily indicator colour
daily: PeriodNoteSettings; daily: PeriodNoteSettings;
weekly: PeriodNoteSettings; weekly: PeriodNoteSettings;
monthly: PeriodNoteSettings; monthly: PeriodNoteSettings;
quarterly: PeriodNoteSettings; quarterly: PeriodNoteSettings;
yearly: PeriodNoteSettings; yearly: PeriodNoteSettings;
dateSystems: DateSystemSettings[]; // day-scoped systems beyond the daily note, in menu order dateSystems: DateSystemSettings[]; // day-scoped systems beyond the daily note, in menu order
recentFiles: RecentFilesSettings; // maxItems, updateOn, omittedPaths[], omittedTags[], filterTags[] recentFiles: RecentFilesSettings; // maxItems, updateOn, omittedPaths[], omittedTags[], filterTags[]
display: DisplaySettings; // px sizing for bookmark rows, fonts, icons, calendar cells display: DisplaySettings; // px sizing for bookmark rows, fonts, icons, calendar cells
} }
@@ -70,8 +70,10 @@ interface PeriodNoteSettings {
} }
interface CalendarSettings { interface CalendarSettings {
firstDayOfWeek: number; // 0 = Sunday, 1 = Monday firstDayOfWeek: number; // 0 = Sunday, 1 = Monday
showNoteIndicators: boolean; // dot on days with existing .md files showNoteIndicators: boolean; // show or hide all calendar markers
indicatorMode: 'any' | 'systems'; // defaults to systems; one neutral dot or one per date system
dailyIndicatorColor: string; // daily's colour in systems mode
} }
interface RecentFilesSettings { interface RecentFilesSettings {
@@ -157,24 +159,26 @@ A **date system** is a folder of notes whose filenames begin with a date. Daily
### `DateSystemSettings` ### `DateSystemSettings`
```typescript
interface DateSystemSettings { interface DateSystemSettings {
id: string; // stable across edits + reordering, so settings rows can key on it id: string; // stable across edits + reordering, so settings rows can key on it
name: string; // menu label, e.g. "Journal" name: string; // menu label, e.g. "Journal"
folder: string; // e.g. "periodic/journal" folder: string; // e.g. "periodic/journal"
nameFormat: string; // moment format string; may contain "{title}" nameFormat: string; // moment format string; may contain "{title}"
templateFile: string; // may omit the .md extension templateFile: string; // may omit the .md extension
typeProperty: string; // fallback frontmatter `type:` value typeProperty: string; // fallback frontmatter `type:` value
icon: string; // Lucide icon name for the menu item icon: string; // Lucide icon name for the menu item
indicatorColor: string; // calendar dot colour in systems mode
} }
``` ```
Configured systems live in `settings.dateSystems`, where array order is menu order. `DEFAULT_DATE_SYSTEM` (in `settings.ts`, typed `Omit<DateSystemSettings, 'id'>`) supplies the field defaults for a newly added row **and** the merge base for saved ones, so a system persisted before a field existed still loads with that field defined. `DEFAULT_SETTINGS.dateSystems` ships two: Configured systems live in `settings.dateSystems`, where array order is menu order. `DEFAULT_DATE_SYSTEM` (in `settings.ts`, typed `Omit<DateSystemSettings, 'id'>`) supplies the field defaults for a newly added row **and** the merge base for saved ones, so a system persisted before a field existed still loads with that field defined. `DEFAULT_SETTINGS.dateSystems` ships two:
| `id` | `name` | Folder | `nameFormat` | Notes per date | | `id` | `name` | Folder | `nameFormat` | Indicator colour | Notes per date |
|---|---|---|---|---| |---|---|---|---|---|---|
| `journal` | Journal | `periodic/journal` | `YYYY-MM-DD - [Journal]` | one | | `journal` | Journal | `periodic/journal` | `YYYY-MM-DD - [Journal]` | green (`#22c55e`) | one |
| `meetings` | Meeting | `periodic/meetings` | `YYYY-MM-DD - {title}` | many | | `meetings` | Meeting | `periodic/meetings` | `YYYY-MM-DD - {title}` | violet (`#a855f7`) | many |
The default calendar mode is `systems`, while `any` preserves the older one-neutral-dot behaviour. Template file fields in both Periodic Notes and Date systems use a native `datalist` populated from every Markdown path in the vault (without the optional `.md` extension), so templates stored outside a conventional `Templates/` folder remain discoverable.
### Templater folder triggers ### Templater folder triggers
@@ -210,21 +214,26 @@ Imports nothing from `'obsidian'` — callers make every moment call and pass th
### Plugin API (`main.ts`) ### Plugin API (`main.ts`)
```typescript ```typescript
/** One existing note of a date system, with the label the menu should show. */
interface DateSystemNote { interface DateSystemNote {
file: TFile; file: TFile;
label: string; // free-text title for many-per-date systems, else the system name label: string; // free-text title for many-per-date systems, else the system name
} }
/** A day-scoped system and the notes it already holds for one date. */
interface DateSystemNotes { interface DateSystemNotes {
system: DateSystemSettings; system: DateSystemSettings;
notes: DateSystemNote[]; // sorted by basename; empty when the date has no note notes: DateSystemNote[];
multiple: boolean; // nameFormat carries {title}, i.e. many notes per date multiple: boolean; // nameFormat carries {title}
}
interface DateSystemIndicator {
id: string;
name: string;
color: string;
} }
dateSystems(): DateSystemSettings[] dateSystems(): DateSystemSettings[]
findDateSystemNotes(date: moment.Moment): DateSystemNotes[] findDateSystemNotes(date: moment.Moment): DateSystemNotes[]
getDateSystemIndicators(dates: moment.Moment[]): Map<string, DateSystemIndicator[]>
openDateSystemNote( openDateSystemNote(
system: DateSystemSettings, system: DateSystemSettings,
date: moment.Moment, date: moment.Moment,
@@ -232,6 +241,8 @@ openDateSystemNote(
): Promise<void> ): Promise<void>
``` ```
**`getDateSystemIndicators(dates)`** — returns one `{ id, name, color }` marker per configured system with one or more notes on a requested day. The calendar calls it once for the visible month only in `indicatorMode: 'systems'`. It makes one vault pass, caches the result by displayed dates and date-system settings, and invalidates it on markdown create/delete/rename or any Settings change. It does not run on ordinary markdown edits: content edits cannot change a filename-derived indicator.
**`dateSystems()`** — every day-scoped system in menu order: the daily periodic note, synthesized into a `DateSystemSettings` from `settings.daily`, followed by `settings.dateSystems`. The daily note is therefore not a special case in any consumer. **`dateSystems()`** — every day-scoped system in menu order: the daily periodic note, synthesized into a `DateSystemSettings` from `settings.daily`, followed by `settings.dateSystems`. The daily note is therefore not a special case in any consumer.
**`findDateSystemNotes(date)`** — **one** vault scan per call, not one per system. It formats each usable system's `before`/`after` affixes for `date` once, then walks the markdown file list a single time, bucketing each file into the system that claims it (`isInFolder` + `matchesSystemName`). Systems whose formats have no date token are omitted rather than offering an unsafe create action. It returns the remaining `DateSystemNotes` in `dateSystems()` order, each bucket sorted by basename. The calendar's day context menu is built from exactly one of these calls per right-click. **`findDateSystemNotes(date)`** — **one** vault scan per call, not one per system. It formats each usable system's `before`/`after` affixes for `date` once, then walks the markdown file list a single time, bucketing each file into the system that claims it (`isInFolder` + `matchesSystemName`). Systems whose formats have no date token are omitted rather than offering an unsafe create action. It returns the remaining `DateSystemNotes` in `dateSystems()` order, each bucket sorted by basename. The calendar's day context menu is built from exactly one of these calls per right-click.
@@ -348,7 +359,7 @@ The last section (`waypoint-section:last-child`) gets `margin-top: auto`, pushin
- **Breadcrumb:** Q-label, month name, year — each clickable to open that period note. - **Breadcrumb:** Q-label, month name, year — each clickable to open that period note.
- **Nav buttons:** ◀/▶ shift month ±1. "Today" resets to current month. - **Nav buttons:** ◀/▶ shift month ±1. "Today" resets to current month.
- **Week number column:** Clicking a week number opens the weekly note for that week's Monday; middle-clicking opens it in a new tab. - **Week number column:** Clicking a week number opens the weekly note for that week's Monday; middle-clicking opens it in a new tab.
- **Day cells:** Left-click opens the daily note, middle-click opens it in a new tab, right-click opens the day context menu (below). `.other-month` dimmed. `.today` has accent border. `.has-note` gets a dot indicator, decided by the synchronous O(1) `plugin.hasNoteForDate(dateStr)` (a `Set` lookup — no per-cell vault scan). - **Day cells:** Left-click opens the daily note, middle-click opens it in a new tab, right-click opens the day context menu (below). `.other-month` is muted; `.today` has an inset accent ring; hover receives a quiet fill. With `indicatorMode: 'any'`, `.has-note` shows the existing single neutral dot through the O(1) `plugin.hasNoteForDate(dateStr)` lookup. With `indicatorMode: 'systems'`, the view calls `getDateSystemIndicators()` once for the month and renders one tooltip-labelled coloured dot per matching daily/journal/meeting/custom system.
- **Grid generation:** `getMonthGrid(year, month, firstDayOfWeek)` in `date-utils.ts` produces up to 6 weeks, each with 7 `CalendarDay` objects containing `moment`, `dayOfMonth`, `isToday`, `isCurrentMonth`, `isoWeekNumber`. - **Grid generation:** `getMonthGrid(year, month, firstDayOfWeek)` in `date-utils.ts` produces up to 6 weeks, each with 7 `CalendarDay` objects containing `moment`, `dayOfMonth`, `isToday`, `isCurrentMonth`, `isoWeekNumber`.
**Day context menu (right-click):** Built from a single `plugin.findDateSystemNotes(day.date)` call. The first entry is a non-clickable header showing the full date. Then, per system in `dateSystems()` order: **Day context menu (right-click):** Built from a single `plugin.findDateSystemNotes(day.date)` call. The first entry is a non-clickable header showing the full date. Then, per system in `dateSystems()` order:
@@ -423,17 +434,18 @@ All classes prefixed with `waypoint-`. Uses Obsidian CSS variables throughout:
- `--font-ui-small`, `--font-ui-medium`, `--font-semibold`, `--font-medium`, `--font-light` - `--font-ui-small`, `--font-ui-medium`, `--font-semibold`, `--font-medium`, `--font-light`
- `--text-muted`, `--text-faint`, `--text-accent`, `--text-on-accent`, `--text-error` - `--text-muted`, `--text-faint`, `--text-accent`, `--text-on-accent`, `--text-error`
- `--background-modifier-border`, `--background-modifier-active-hover`, `--background-primary`, `--background-secondary` - `--background-modifier-border`, `--background-modifier-active-hover`, `--background-modifier-hover`, `--background-primary`, `--background-primary-alt`, `--background-secondary`
- `--interactive-accent` - `--interactive-accent`
- `--cursor-link` (pointer cursor, with a `pointer` fallback) - `--cursor-link` (pointer cursor, with a `pointer` fallback)
Key layout: Key layout:
- `.waypoint-view` — flex column, `overflow-y: auto`, 8px padding. - `.waypoint-view` — flex column, `overflow-y: auto`, 8px padding.
- `.waypoint-section``flex-shrink: 0`, 16px bottom margin. Last section gets `margin-top: auto` (pins calendar to bottom). - `.waypoint-section``flex-shrink: 0`, 16px bottom margin. Last section gets `margin-top: auto` (pins calendar to bottom).
- `.waypoint-section-header` — uppercase, muted, with bottom border. - `.waypoint-calendar` — padded, bordered surface with separated day cells for clear scan lines.
- Calendar table — `table-layout: fixed`, `border-collapse: collapse`. - Calendar table — `table-layout: fixed`, `border-collapse: separate`, 2px horizontal / 3px vertical cell spacing.
- `.waypoint-day.today` — accent color text + 1px accent border. - `.waypoint-day.today` — accent text with an inset accent ring, avoiding layout shifts.
- `.waypoint-day.has-note::after`4px dot indicator. - `.waypoint-day.has-note::after`neutral single-dot indicator.
- `.waypoint-day-indicators` / `.waypoint-day-indicator` — compact ordered dot row; `--waypoint-indicator-color` carries each configured system colour.
- `.waypoint-bm-chevron``rotate(-90deg)` on collapsed groups. - `.waypoint-bm-chevron``rotate(-90deg)` on collapsed groups.
- `.waypoint-bm-drop-line` / `.waypoint-bm-drop-below` — 3px accent border for drag indicators. - `.waypoint-bm-drop-line` / `.waypoint-bm-drop-below` — 3px accent border for drag indicators.
+12
View File
@@ -70,6 +70,18 @@
- [ ] Note indicator dots show on days with existing .md files - [ ] Note indicator dots show on days with existing .md files
- [ ] Today is highlighted with accent border - [ ] Today is highlighted with accent border
## Calendar: System-coloured indicators
- [ ] Calendar → Indicator style → **Single dot** keeps one neutral dot for an existing date-named note
- [ ] Calendar → Indicator style → **Colour by note system** shows a blue dot for Daily, green for Journal, and violet for Meeting by default
- [ ] A day holding multiple meeting notes still shows exactly one Meeting dot
- [ ] A day holding Daily, Journal, and Meeting notes shows three compact dots in that order
- [ ] Hover each coloured dot → its date-system name is identified by a tooltip
- [ ] Change Daily/Journal/Meeting indicator colours in Settings → the visible calendar refreshes immediately
- [ ] Add a custom date system with its own colour and a matching note → its dot appears in system order
- [ ] Create, delete, or rename a date-system note → coloured dots refresh without changing months
- [ ] Start typing in any Template file field → matching vault Markdown paths appear as native autocomplete suggestions
## Calendar: Date systems ## Calendar: Date systems
+12 -3
View File
@@ -4,8 +4,8 @@ Calendar, recent files, and custom bookmarks sidebar for Obsidian.
## Features ## Features
- **Calendar panel** — month grid; left-click a day opens its daily note, middle-click opens it in a tab, and right-click reveals every configured note system for that date - **Calendar panel** — refined month grid; left-click a day opens its daily note, middle-click opens it in a tab, and right-click reveals every configured note system for that date
- **Date systems** — browse or create daily, journal, meeting, and other date-prefixed notes from a single day menu - **Calendar indicators** — retain a single neutral note dot or show a coloured dot per daily, journal, meeting, or custom date system
- **Recent files** — track recently opened/edited files - **Recent files** — track recently opened/edited files
- **Favorites** — custom bookmarks with groups, icons, and rename - **Favorites** — custom bookmarks with groups, icons, and rename
@@ -29,12 +29,21 @@ Right-click any calendar day to open its **date systems** menu. The daily note a
| Journal | `periodic/journal` | `YYYY-MM-DD - [Journal]` | Opens or creates one journal note for the date | | Journal | `periodic/journal` | `YYYY-MM-DD - [Journal]` | Opens or creates one journal note for the date |
| Meeting | `periodic/meetings` | `YYYY-MM-DD - {title}` | Lists every meeting for the date and can create another | | Meeting | `periodic/meetings` | `YYYY-MM-DD - {title}` | Lists every meeting for the date and can create another |
Configure systems in **Settings → Waypoint Sidebar → Date systems**. Each system has a folder, filename format, template, fallback frontmatter type, and Lucide icon. Configure systems in **Settings → Waypoint Sidebar → Date systems**. Each system has a folder, filename format, template, fallback frontmatter type, Lucide icon, and indicator colour. Template file fields autocomplete every Markdown path in the vault.
`Name format` uses moment.js tokens. Bracket-escape literal text: `YYYY-MM-DD - [Journal]`. Add `{title}` when a date can have multiple notes; it is replaced with the title requested when creating a note. Without `{title}`, the system has exactly one note per date. `Name format` uses moment.js tokens. Bracket-escape literal text: `YYYY-MM-DD - [Journal]`. Add `{title}` when a date can have multiple notes; it is replaced with the title requested when creating a note. Without `{title}`, the system has exactly one note per date.
Ctrl/Cmd-click a menu entry opens it in a new tab. Ctrl/Cmd-click a menu entry opens it in a new tab.
### Calendar indicators
Under **Settings → Waypoint Sidebar → Calendar**, choose either:
- **Colour by note system** — the default; one compact coloured dot per configured date system that has a note on that day. Hover a dot to identify its system.
- **Single dot** — a neutral marker for any note whose filename is exactly the date.
Daily uses the Calendar tabs colour picker. Journal, Meeting, and every custom system have their own **Indicator colour** picker under **Date systems**. The defaults are blue for Daily, green for Journal, and violet for Meeting.
### Templater folder templates ### Templater folder templates
If a date system uses a Templater folder mapping, leave its Waypoint **Template file** field blank. Waypoint then creates a safe fallback note while Templater owns prompts, scripts, cursors, and rendered content. If a date system uses a Templater folder mapping, leave its Waypoint **Template file** field blank. Waypoint then creates a safe fallback note while Templater owns prompts, scripts, cursors, and rendered content.
+19 -19
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "waypoint-sidebar", "id": "waypoint-sidebar",
"name": "Waypoint Sidebar", "name": "Waypoint Sidebar",
"version": "1.6.0", "version": "1.7.0",
"minAppVersion": "1.4.4", "minAppVersion": "1.4.4",
"description": "Calendar, recent files, and custom bookmarks sidebar.", "description": "Calendar, recent files, and custom bookmarks sidebar.",
"author": "Olivier", "author": "Olivier",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "waypoint", "name": "waypoint",
"version": "1.6.0", "version": "1.7.0",
"description": "Calendar, recent files, and custom bookmarks sidebar.", "description": "Calendar, recent files, and custom bookmarks sidebar.",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
+97 -3
View File
@@ -42,6 +42,13 @@ export interface DateSystemNotes {
multiple: boolean; multiple: boolean;
} }
/** One coloured calendar marker for a date system that has a note on a day. */
export interface DateSystemIndicator {
id: string;
name: string;
color: string;
}
export default class WaypointPlugin extends Plugin { export default class WaypointPlugin extends Plugin {
public settings: WaypointSettings; public settings: WaypointSettings;
public waypointData: WaypointData; public waypointData: WaypointData;
@@ -51,6 +58,9 @@ export default class WaypointPlugin extends Plugin {
private savePromise: Promise<void> = Promise.resolve(); private savePromise: Promise<void> = Promise.resolve();
/** Basenames of every markdown file in the vault, for O(1) calendar lookups. */ /** Basenames of every markdown file in the vault, for O(1) calendar lookups. */
private markdownBasenames: Set<string> = new Set(); private markdownBasenames: Set<string> = new Set();
/** Results for the displayed month; invalidated by markdown-file changes. */
private dateSystemIndicators: Map<string, DateSystemIndicator[]> | null = null;
private dateSystemIndicatorKey = '';
async onload(): Promise<void> { async onload(): Promise<void> {
console.debug('Waypoint: loading plugin v' + this.manifest.version); console.debug('Waypoint: loading plugin v' + this.manifest.version);
@@ -73,6 +83,7 @@ export default class WaypointPlugin extends Plugin {
this.settings, this.settings,
() => { () => {
this.enforceRecentFilesLimit(); this.enforceRecentFilesLimit();
this.invalidateDateSystemIndicators();
this.redrawAll(); this.redrawAll();
}, },
)); ));
@@ -247,10 +258,14 @@ export default class WaypointPlugin extends Plugin {
} }
// Cloned, not Object.assign'd in: the defaults array would otherwise be // Cloned, not Object.assign'd in: the defaults array would otherwise be
// aliased into the live settings and the settings UI would edit // aliased into the live settings and the settings UI would edit
// DEFAULT_SETTINGS itself. Saved entries merge over // DEFAULT_DATE_SYSTEM so older configs pick up fields added since. Known
// DEFAULT_DATE_SYSTEM so older configs pick up fields added since. // built-ins use their own defaults too, preserving Journal/Meeting colours
// when this field is first introduced.
this.settings.dateSystems = Array.isArray(s.dateSystems) this.settings.dateSystems = Array.isArray(s.dateSystems)
? s.dateSystems.map(sys => Object.assign({}, DEFAULT_DATE_SYSTEM, sys)) ? s.dateSystems.map(sys => {
const builtIn = DEFAULT_SETTINGS.dateSystems.find(defaultSystem => defaultSystem.id === sys.id);
return Object.assign({}, builtIn || DEFAULT_DATE_SYSTEM, sys);
})
: DEFAULT_SETTINGS.dateSystems.map(sys => Object.assign({}, sys)); : DEFAULT_SETTINGS.dateSystems.map(sys => Object.assign({}, sys));
} }
@@ -392,6 +407,7 @@ export default class WaypointPlugin extends Plugin {
* inside a renamed folder. * inside a renamed folder.
*/ */
private onRename(file: TAbstractFile, oldPath: string): void { private onRename(file: TAbstractFile, oldPath: string): void {
this.invalidateDateSystemIndicators();
const indexChanged = this.syncIndexForRename(file, oldPath); const indexChanged = this.syncIndexForRename(file, oldPath);
let dataChanged = false; let dataChanged = false;
@@ -427,6 +443,7 @@ export default class WaypointPlugin extends Plugin {
private onVaultCreate(file: TAbstractFile): void { private onVaultCreate(file: TAbstractFile): void {
if (file instanceof TFile && file.extension === 'md') { if (file instanceof TFile && file.extension === 'md') {
this.markdownBasenames.add(file.basename); this.markdownBasenames.add(file.basename);
this.invalidateDateSystemIndicators();
} }
this.broadcastRedraw(); this.broadcastRedraw();
} }
@@ -434,6 +451,7 @@ export default class WaypointPlugin extends Plugin {
private onVaultDelete(file: TAbstractFile): void { private onVaultDelete(file: TAbstractFile): void {
if (file instanceof TFile && file.extension === 'md') { if (file instanceof TFile && file.extension === 'md') {
this.removeFromMarkdownIndex(file.basename, file.path); this.removeFromMarkdownIndex(file.basename, file.path);
this.invalidateDateSystemIndicators();
} }
this.broadcastRedraw(); this.broadcastRedraw();
} }
@@ -447,6 +465,12 @@ export default class WaypointPlugin extends Plugin {
} }
} }
/** Forget a month result when markdown paths or date-system settings change. */
private invalidateDateSystemIndicators(): void {
this.dateSystemIndicators = null;
this.dateSystemIndicatorKey = '';
}
/** /**
* Drop a basename from the index, unless another markdown file still * Drop a basename from the index, unless another markdown file still
* carries it. `path` is excluded from that check because the vault may not * carries it. `path` is excluded from that check because the vault may not
@@ -555,6 +579,7 @@ export default class WaypointPlugin extends Plugin {
templateFile: periodSettings.templateFile, templateFile: periodSettings.templateFile,
typeProperty: periodSettings.typeProperty, typeProperty: periodSettings.typeProperty,
icon: 'calendar', icon: 'calendar',
indicatorColor: this.settings.calendar.dailyIndicatorColor,
}; };
} }
@@ -609,6 +634,75 @@ export default class WaypointPlugin extends Plugin {
.map(bucket => bucket.result); .map(bucket => bucket.result);
} }
/**
* Return one coloured indicator per system with a note on each requested day.
*
* The day grid calls this once per render. Its result is cached by the
* displayed dates and system settings, then invalidated by file changes, so
* a 42-cell calendar never repeats the vault scan per cell or per redraw.
*/
getDateSystemIndicators(dates: moment.Moment[]): Map<string, DateSystemIndicator[]> {
const dateStrings = dates.map(date => date.format('YYYY-MM-DD'));
const systems = this.dateSystems()
.filter(system => formatHasDateToken(system.nameFormat))
.map(system => {
const parts = splitNameFormat(system.nameFormat);
return {
system,
hasTitle: parts.hasTitle,
dates: dates.map((date, index) => ({
dateStr: dateStrings[index],
before: formatDatePart(date, parts.before),
after: formatDatePart(date, parts.after),
})),
};
});
const key = JSON.stringify({
dates: dateStrings,
systems: systems.map(({ system }) => [
system.id,
system.name,
system.folder,
system.nameFormat,
system.indicatorColor,
]),
});
if (this.dateSystemIndicators && this.dateSystemIndicatorKey === key) {
return this.dateSystemIndicators;
}
const matchingIds = new Map<string, Set<string>>();
for (const dateStr of dateStrings) matchingIds.set(dateStr, new Set());
// The only vault traversal: filter a file by system folder first, then
// test it against the month's at-most-42 formatted day patterns.
for (const file of this.app.vault.getMarkdownFiles()) {
for (const entry of systems) {
if (!isInFolder(file.path, entry.system.folder)) continue;
for (const match of entry.dates) {
if (matchesSystemName(file.basename, match.before, match.after, entry.hasTitle)) {
matchingIds.get(match.dateStr)?.add(entry.system.id);
}
}
}
}
const result = new Map<string, DateSystemIndicator[]>();
for (const dateStr of dateStrings) {
const ids = matchingIds.get(dateStr);
result.set(dateStr, systems
.filter(({ system }) => ids?.has(system.id))
.map(({ system }) => ({
id: system.id,
name: system.name,
color: system.indicatorColor,
})));
}
this.dateSystemIndicatorKey = key;
this.dateSystemIndicators = result;
return result;
}
/** /**
* Open (creating if needed) the note `system` holds for `date`. * Open (creating if needed) the note `system` holds for `date`.
* *
+73 -8
View File
@@ -96,6 +96,32 @@ export class WaypointSettingTab extends PluginSettingTab {
this.saveAndRefresh(); this.saveAndRefresh();
}); });
}); });
new Setting(container)
.setName('Indicator style')
.setDesc('Show one neutral dot for any dated note, or one coloured dot for each configured date system.')
.addDropdown((dropdown) => {
dropdown
.addOption('any', 'Single dot')
.addOption('systems', 'Colour by note system')
.setValue(this.settings.calendar.indicatorMode)
.onChange((value: 'any' | 'systems') => {
this.settings.calendar.indicatorMode = value;
this.saveAndRefresh();
});
});
new Setting(container)
.setName('Daily indicator colour')
.setDesc('Colour for daily-note dots when using “Colour by note system”.')
.addColorPicker((color) => {
color
.setValue(this.settings.calendar.dailyIndicatorColor)
.onChange((value) => {
this.settings.calendar.dailyIndicatorColor = value;
this.saveAndRefresh();
});
});
} }
// ═══════════════════════════════ // ═══════════════════════════════
@@ -103,14 +129,20 @@ export class WaypointSettingTab extends PluginSettingTab {
// ═══════════════════════════════ // ═══════════════════════════════
private renderPeriodicTab(container: HTMLElement): void { private renderPeriodicTab(container: HTMLElement): void {
this.addPeriodNoteSettings(container, 'Daily', this.settings.daily); const templateSuggestionsId = this.createTemplateSuggestions(container);
this.addPeriodNoteSettings(container, 'Weekly', this.settings.weekly); this.addPeriodNoteSettings(container, 'Daily', this.settings.daily, templateSuggestionsId);
this.addPeriodNoteSettings(container, 'Monthly', this.settings.monthly); this.addPeriodNoteSettings(container, 'Weekly', this.settings.weekly, templateSuggestionsId);
this.addPeriodNoteSettings(container, 'Quarterly', this.settings.quarterly); this.addPeriodNoteSettings(container, 'Monthly', this.settings.monthly, templateSuggestionsId);
this.addPeriodNoteSettings(container, 'Yearly', this.settings.yearly); this.addPeriodNoteSettings(container, 'Quarterly', this.settings.quarterly, templateSuggestionsId);
this.addPeriodNoteSettings(container, 'Yearly', this.settings.yearly, templateSuggestionsId);
} }
private addPeriodNoteSettings(container: HTMLElement, label: string, period: PeriodNoteSettings): void { private addPeriodNoteSettings(
container: HTMLElement,
label: string,
period: PeriodNoteSettings,
templateSuggestionsId: string,
): void {
new Setting(container).setHeading().setName(label); new Setting(container).setHeading().setName(label);
new Setting(container) new Setting(container)
@@ -139,10 +171,11 @@ export class WaypointSettingTab extends PluginSettingTab {
new Setting(container) new Setting(container)
.setName('Template file') .setName('Template file')
.setDesc(`Path to the template file (without .md extension).`) .setDesc('Path to the template file. The .md extension is optional.')
.addText((text) => { .addText((text) => {
text.setPlaceholder('Templates/Daily note'); text.setPlaceholder('Templates/Daily note');
text.setValue(period.templateFile); text.setValue(period.templateFile);
text.inputEl.setAttr('list', templateSuggestionsId);
text.onChange((value) => { text.onChange((value) => {
period.templateFile = value; period.templateFile = value;
this.saveAndRefresh(); this.saveAndRefresh();
@@ -178,6 +211,8 @@ export class WaypointSettingTab extends PluginSettingTab {
.setName('Date systems') .setName('Date systems')
.setDesc(intro); .setDesc(intro);
const templateSuggestionsId = this.createTemplateSuggestions(container);
this.settings.dateSystems.forEach((system, i, arr) => { this.settings.dateSystems.forEach((system, i, arr) => {
new Setting(container) new Setting(container)
.setHeading() .setHeading()
@@ -244,7 +279,7 @@ export class WaypointSettingTab extends PluginSettingTab {
this.addSystemTextSetting(container, this.addSystemTextSetting(container,
'Template file', 'Path to the template file. The .md extension is optional.', 'Template file', 'Path to the template file. The .md extension is optional.',
system, 'templateFile', 'resources/template/journal', system, 'templateFile', 'resources/template/journal', templateSuggestionsId,
); );
this.addSystemTextSetting(container, this.addSystemTextSetting(container,
'Type property', `Fallback value for the 'type' frontmatter property, used when no template is found.`, 'Type property', `Fallback value for the 'type' frontmatter property, used when no template is found.`,
@@ -254,6 +289,18 @@ export class WaypointSettingTab extends PluginSettingTab {
'Icon', 'Lucide icon name for the menu item. Browse names at lucide.dev.', 'Icon', 'Lucide icon name for the menu item. Browse names at lucide.dev.',
system, 'icon', 'book-open', system, 'icon', 'book-open',
); );
new Setting(container)
.setName('Indicator colour')
.setDesc('Colour for this systems dot when using “Colour by note system”.')
.addColorPicker((color) => {
color
.setValue(system.indicatorColor)
.onChange((value) => {
system.indicatorColor = value;
this.saveAndRefresh();
});
});
}); });
new Setting(container) new Setting(container)
@@ -280,6 +327,7 @@ export class WaypointSettingTab extends PluginSettingTab {
obj: Record<K, string>, obj: Record<K, string>,
key: K, key: K,
placeholder: string, placeholder: string,
templateSuggestionsId?: string,
): Setting { ): Setting {
return new Setting(container) return new Setting(container)
.setName(name) .setName(name)
@@ -287,6 +335,7 @@ export class WaypointSettingTab extends PluginSettingTab {
.addText((text) => { .addText((text) => {
text.setPlaceholder(placeholder); text.setPlaceholder(placeholder);
text.setValue(obj[key]); text.setValue(obj[key]);
if (templateSuggestionsId) text.inputEl.setAttr('list', templateSuggestionsId);
text.onChange((value) => { text.onChange((value) => {
obj[key] = value; obj[key] = value;
this.saveAndRefresh(); this.saveAndRefresh();
@@ -294,6 +343,22 @@ export class WaypointSettingTab extends PluginSettingTab {
}); });
} }
/**
* Native datalist keeps template paths searchable without another custom
* modal, and accepts templates stored anywhere in the vault.
*/
private createTemplateSuggestions(container: HTMLElement): string {
const id = 'waypoint-template-suggestions';
const list = container.createEl('datalist', { attr: { id } });
const paths = this.app.vault.getMarkdownFiles()
.map(file => file.path.replace(/\.md$/, ''))
.sort((a, b) => a.localeCompare(b));
for (const path of paths) {
list.createEl('option', { value: path });
}
return id;
}
// ═══════════════════════════════ // ═══════════════════════════════
// Recent Files tab // Recent Files tab
// ═══════════════════════════════ // ═══════════════════════════════
+11
View File
@@ -27,6 +27,8 @@ export interface DateSystemSettings {
typeProperty: string; typeProperty: string;
/** Lucide icon name for the menu item. */ /** Lucide icon name for the menu item. */
icon: string; icon: string;
/** CSS colour for this system's calendar indicator. */
indicatorColor: string;
} }
/** Field defaults for a newly added system, and the merge base for saved ones. */ /** Field defaults for a newly added system, and the merge base for saved ones. */
@@ -37,11 +39,16 @@ export const DEFAULT_DATE_SYSTEM: Omit<DateSystemSettings, 'id'> = {
templateFile: '', templateFile: '',
typeProperty: '', typeProperty: '',
icon: 'file', icon: 'file',
indicatorColor: '#64748b',
}; };
export interface CalendarSettings { export interface CalendarSettings {
firstDayOfWeek: number; // 0=Sunday, 1=Monday firstDayOfWeek: number; // 0=Sunday, 1=Monday
showNoteIndicators: boolean; showNoteIndicators: boolean;
/** Single neutral dot, or a dot per day-scoped date system. */
indicatorMode: 'any' | 'systems';
/** Colour used by the synthesized daily system in `systems` mode. */
dailyIndicatorColor: string;
} }
export interface RecentFilesSettings { export interface RecentFilesSettings {
@@ -78,6 +85,8 @@ export const DEFAULT_SETTINGS: WaypointSettings = {
calendar: { calendar: {
firstDayOfWeek: 1, // Monday firstDayOfWeek: 1, // Monday
showNoteIndicators: true, showNoteIndicators: true,
indicatorMode: 'systems',
dailyIndicatorColor: '#3b82f6',
}, },
daily: { daily: {
folder: 'periodic/daily', folder: 'periodic/daily',
@@ -118,6 +127,7 @@ export const DEFAULT_SETTINGS: WaypointSettings = {
templateFile: '', templateFile: '',
typeProperty: 'journal', typeProperty: 'journal',
icon: 'book-open', icon: 'book-open',
indicatorColor: '#22c55e',
}, },
{ {
id: 'meetings', id: 'meetings',
@@ -127,6 +137,7 @@ export const DEFAULT_SETTINGS: WaypointSettings = {
templateFile: '', templateFile: '',
typeProperty: 'meeting', typeProperty: 'meeting',
icon: 'users', icon: 'users',
indicatorColor: '#a855f7',
}, },
], ],
recentFiles: { recentFiles: {
+22 -3
View File
@@ -91,7 +91,6 @@ export class WaypointView extends ItemView {
private renderCalendar(): void { private renderCalendar(): void {
const section = this.contentEl.createDiv({ cls: 'waypoint-section' }); const section = this.contentEl.createDiv({ cls: 'waypoint-section' });
section.createDiv({ cls: 'waypoint-section-header', text: 'Calendar' });
const cal = section.createDiv({ cls: 'waypoint-calendar' }); const cal = section.createDiv({ cls: 'waypoint-calendar' });
@@ -181,6 +180,14 @@ export class WaypointView extends ItemView {
this.plugin.settings.calendar.firstDayOfWeek, this.plugin.settings.calendar.firstDayOfWeek,
); );
const systemIndicators = this.plugin.settings.calendar.showNoteIndicators
&& this.plugin.settings.calendar.indicatorMode === 'systems'
? this.plugin.getDateSystemIndicators(weeks.reduce<moment.Moment[]>(
(dates, week) => dates.concat(week.days.map(day => day.date)),
[],
))
: null;
for (const week of weeks) { for (const week of weeks) {
const row = tbody.createEl('tr'); const row = tbody.createEl('tr');
@@ -211,8 +218,20 @@ export class WaypointView extends ItemView {
if (this.plugin.settings.calendar.showNoteIndicators) { if (this.plugin.settings.calendar.showNoteIndicators) {
const dateStr = day.date.format('YYYY-MM-DD'); const dateStr = day.date.format('YYYY-MM-DD');
if (this.plugin.hasNoteForDate(dateStr)) { if (this.plugin.settings.calendar.indicatorMode === 'any') {
cell.addClass('has-note'); if (this.plugin.hasNoteForDate(dateStr)) {
cell.addClass('has-note');
}
} else {
const indicators = systemIndicators?.get(dateStr) || [];
if (indicators.length > 0) {
const dots = cell.createDiv({ cls: 'waypoint-day-indicators' });
for (const indicator of indicators) {
const dot = dots.createSpan({ cls: 'waypoint-day-indicator' });
dot.style.setProperty('--waypoint-indicator-color', indicator.color);
setTooltip(dot, indicator.name);
}
}
} }
} }
+60 -20
View File
@@ -58,29 +58,34 @@
.waypoint-calendar { .waypoint-calendar {
font-size: var(--font-ui-small); font-size: var(--font-ui-small);
padding: 8px;
border: 1px solid var(--background-modifier-border);
border-radius: 10px;
background: var(--background-primary-alt);
} }
.waypoint-calendar-top { .waypoint-calendar-top {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 10px; margin-bottom: 8px;
} }
.waypoint-calendar-breadcrumb { .waypoint-calendar-breadcrumb {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 4px;
font-size: var(--font-ui-medium); font-size: var(--font-ui-medium);
font-weight: var(--font-semibold); font-weight: var(--font-semibold);
padding: 4px 0; padding: 2px 0;
} }
.waypoint-calendar-breadcrumb .waypoint-clickable { .waypoint-calendar-breadcrumb .waypoint-clickable {
color: var(--text-muted); color: var(--text-muted);
cursor: var(--cursor-link, pointer); cursor: var(--cursor-link, pointer);
padding: 1px 4px; padding: 2px 4px;
border-radius: 4px; border-radius: 4px;
transition: color 80ms, background-color 80ms;
} }
.waypoint-calendar-breadcrumb .waypoint-clickable:hover { .waypoint-calendar-breadcrumb .waypoint-clickable:hover {
@@ -90,23 +95,25 @@
.waypoint-calendar .waypoint-separator { .waypoint-calendar .waypoint-separator {
color: var(--text-faint); color: var(--text-faint);
margin: 0 2px; margin: 0;
} }
.waypoint-calendar-today-group { .waypoint-calendar-today-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 1px;
} }
.waypoint-calendar-today-group button { .waypoint-calendar-today-group button {
background: none; background: none;
border: none; border: none;
cursor: var(--cursor-link, pointer); cursor: var(--cursor-link, pointer);
padding: 2px 6px; padding: 2px 5px;
border-radius: 4px; border-radius: 4px;
color: var(--text-muted); color: var(--text-muted);
font-size: var(--font-ui-small); font-size: var(--font-ui-small);
transition: color 80ms, background-color 80ms;
box-shadow: none;
} }
.waypoint-calendar-today-group button:hover { .waypoint-calendar-today-group button:hover {
@@ -121,30 +128,34 @@
.waypoint-calendar table { .waypoint-calendar table {
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: separate;
border-spacing: 2px 3px;
} }
.waypoint-calendar th, .waypoint-calendar th,
.waypoint-calendar td { .waypoint-calendar td {
text-align: center; text-align: center;
padding: 4px 2px; padding: 0;
font-size: var(--font-ui-small); font-size: var(--font-ui-small);
line-height: 1.6;
} }
.waypoint-calendar th { .waypoint-calendar th {
padding-bottom: 2px;
font-weight: var(--font-medium); font-weight: var(--font-medium);
color: var(--text-faint); color: var(--text-faint);
font-size: calc(var(--font-ui-small) * 0.85); font-size: calc(var(--font-ui-small) * 0.78);
letter-spacing: 0.04em;
text-transform: uppercase;
} }
.waypoint-calendar .waypoint-weeknum { .waypoint-calendar .waypoint-weeknum {
font-size: calc(var(--font-ui-small) * 0.75); width: 18px;
font-size: calc(var(--font-ui-small) * 0.72);
color: var(--text-faint); color: var(--text-faint);
font-weight: var(--font-light); font-weight: var(--font-light);
cursor: var(--cursor-link, pointer); cursor: var(--cursor-link, pointer);
padding: 2px 0;
border-radius: 4px; border-radius: 4px;
transition: color 80ms, background-color 80ms;
} }
.waypoint-calendar .waypoint-weeknum:hover { .waypoint-calendar .waypoint-weeknum:hover {
@@ -154,42 +165,71 @@
.waypoint-calendar .waypoint-day { .waypoint-calendar .waypoint-day {
cursor: var(--cursor-link, pointer); cursor: var(--cursor-link, pointer);
min-height: var(--wp-cal-cell-size, 32px); height: var(--wp-cal-cell-size, 32px);
padding: 2px 0; box-sizing: border-box;
padding: 2px 0 7px;
border-radius: 6px; border-radius: 6px;
position: relative; position: relative;
width: 100%; width: 100%;
vertical-align: middle; vertical-align: middle;
transition: color 80ms, background-color 80ms, box-shadow 80ms;
} }
.waypoint-calendar .waypoint-day:hover { .waypoint-calendar .waypoint-day:hover {
background-color: var(--background-modifier-active-hover); background-color: var(--background-modifier-hover);
} }
.waypoint-calendar .waypoint-day.other-month { .waypoint-calendar .waypoint-day.other-month {
opacity: 0.35; color: var(--text-faint);
opacity: 0.5;
} }
.waypoint-calendar .waypoint-day.today { .waypoint-calendar .waypoint-day.today {
color: var(--text-accent); color: var(--text-accent);
border: 1px solid var(--text-accent);
font-weight: var(--font-semibold); font-weight: var(--font-semibold);
box-shadow: inset 0 0 0 1px var(--text-accent);
} }
.waypoint-calendar .waypoint-day.today:hover {
background-color: var(--background-modifier-active-hover);
}
/* Single-dot mode preserves the pre-date-systems indicator. */
.waypoint-calendar .waypoint-day.has-note::after { .waypoint-calendar .waypoint-day.has-note::after {
content: ''; content: '';
display: block; position: absolute;
left: 50%;
bottom: 3px;
width: 4px; width: 4px;
height: 4px; height: 4px;
border-radius: 50%; border-radius: 50%;
background-color: var(--text-faint); background-color: var(--text-faint);
margin: 0 auto; transform: translateX(-50%);
} }
.waypoint-calendar .waypoint-day.today.has-note::after { .waypoint-calendar .waypoint-day.today.has-note::after {
background-color: var(--text-accent); background-color: var(--text-accent);
} }
/* Colour-by-system mode: one dot per matching system, in Settings order. */
.waypoint-day-indicators {
position: absolute;
right: 0;
bottom: 3px;
left: 0;
display: flex;
justify-content: center;
gap: 3px;
}
.waypoint-day-indicator {
width: 5px;
height: 5px;
border-radius: 50%;
background-color: var(--waypoint-indicator-color);
box-shadow: 0 0 0 1px var(--background-primary);
}
/* ── Recent Files ── */ /* ── Recent Files ── */
.waypoint-recent-filter { .waypoint-recent-filter {