32 KiB
Waypoint Sidebar — Technical Documentation
Obsidian plugin (id:
waypoint-sidebar) providing a unified sidebar with calendar, recent files, and custom bookmarks. Replaces separate Calendar and Bookmarks core plugins.
Architecture Overview
src/
├── main.ts Plugin entry — lifecycle, commands, events, data persistence
├── settings.ts Interface definitions + DEFAULT_SETTINGS / DEFAULT_DATE_SYSTEM constants
├── settings-tab.ts PluginSettingTab UI — calendar, periodic, date systems, recent, display, about
├── models/
│ └── bookmark.ts BookmarkItem + WaypointData interfaces
├── utils/
│ ├── date-utils.ts Moment.js helpers: period formatting, month grid, navigation
│ ├── date-systems.ts Pure date-system filename helpers (no `obsidian` import, unit-testable)
│ └── path-utils.ts Pure path helper: rename remapping (no `obsidian` import, unit-testable)
└── views/
└── waypoint-view.ts ItemView subclass — full sidebar rendering + interaction
Build: npm run build runs tsc --noEmit first, then esbuild bundles src/main.ts → main.js. The typecheck is a real gate — esbuild strips types without checking them, so without it type errors would ship. npm run typecheck runs the check alone; npm run dev (watch) stays ungated for speed. TypeScript strict-null + noImplicitAny, ESNext modules, ES6 target, lib floor at ES2017.
Data persistence: Single data.json via Obsidian Plugin.loadData()/saveData(). Top-level keys: settings (merged with DEFAULT_SETTINGS), waypointData (bookmark tree and recent files). Recent files are persisted under waypointData.recentFiles and reloaded on startup; writes are debounced 300ms so rapid file opens collapse into one save.
Plugin Lifecycle
onload()
- Load data — reads
data.jsonexactly once, thenapplySettings(saved)andapplyWaypointData(saved)merge its partial data over defaults. - Register view —
WAYPOINT_VIEW_TYPE = "waypoint-view"maps toWaypointViewfactory(leaf) => new WaypointView(leaf, this). - Settings tab —
WaypointSettingTabreceives the livesettingsobject +() => this.redrawAll()callback for live preview. - Register commands (see Commands section below).
- Register events —
file-open,vault:create,vault:delete,vault:rename,vault:modify. - Auto-open — On
onLayoutReady, if no existing leaves of the view type, opens one in the left sidebar. - Midnight refresh — 10-minute
setIntervalchecks ifnew Date().toDateString()changed; if so, callsredrawAll()(calendar day indicators refresh).
onunload()
Detaches all leaves of WAYPOINT_VIEW_TYPE.
Data Models
WaypointSettings
interface WaypointSettings {
calendar: CalendarSettings; // week start, indicator visibility/style, daily indicator colour
daily: PeriodNoteSettings;
weekly: PeriodNoteSettings;
monthly: PeriodNoteSettings;
quarterly: PeriodNoteSettings;
yearly: PeriodNoteSettings;
dateSystems: DateSystemSettings[]; // day-scoped systems beyond the daily note, in menu order
recentFiles: RecentFilesSettings; // maxItems, updateOn, omittedPaths[], omittedTags[], filterTags[]
display: DisplaySettings; // px sizing for bookmark rows, fonts, icons, calendar cells
}
interface PeriodNoteSettings {
folder: string; // e.g. "periodic/daily"
templateFile: string; // path without .md, e.g. "Templates/Daily note"
nameFormat: string; // moment.js format string
typeProperty: string; // frontmatter type value, e.g. "daily-note"
}
interface CalendarSettings {
firstDayOfWeek: number; // 0 = Sunday, 1 = Monday
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 {
maxItems: number; // default 50
updateOn: 'file-open' | 'file-edit'; // trigger mode
omittedPaths: string[]; // regex patterns (one per line)
omittedTags: string[]; // regex patterns for frontmatter/inline tags
filterTags: string[]; // tags shown as filter pills (empty = auto-detect from frontmatter)
}
interface DisplaySettings {
rowSize: number; // px, base height of bookmark items (18–40)
rowSpacing: number; // px, gap between items (0–12)
indentSize: number; // px, indent per depth level (8–32)
fontSize: number; // px, font size for bookmark labels (10–18)
iconSize: number; // px, icon size (12–24)
calendarCellSize: number; // px, calendar day cell height (20–48)
}
dateSystems holds the user's configured date systems in menu order — see the Date Systems section for the DateSystemSettings shape and the two systems shipped by default.
BookmarkItem (recursive tree)
interface BookmarkItem {
id: string; // "bm-{timestamp}-{random4}"
type: 'file' | 'group' | 'separator' | 'spacer';
label: string; // display text (empty for separator/spacer)
filePath: string; // vault-relative path (empty for non-file types)
icon: string; // Lucide icon name
children: BookmarkItem[]; // nested items (groups contain children)
collapsed: boolean; // group collapse state
indent: number; // depth level (0 = root)
}
interface WaypointData {
bookmarks: BookmarkItem[];
recentFiles: { path: string; basename: string }[];
}
The bookmark tree is stored under waypointData.bookmarks and rendered recursively. Groups contain children; files/separators/spacers are leaf nodes. waypointData.recentFiles holds the persisted recent-files list (mirrored into the live plugin.recentFiles array on load).
Commands
Period note navigation (with default hotkeys)
| ID | Name | Hotkey |
|---|---|---|
waypoint-go-to-daily |
Go to daily note | Mod+Shift+Alt+D |
waypoint-go-to-weekly |
Go to weekly note | Mod+Shift+Alt+W |
waypoint-go-to-monthly |
Go to monthly note | Mod+Shift+Alt+M |
waypoint-go-to-quarterly |
Go to quarterly note | Mod+Shift+Alt+Q |
waypoint-go-to-yearly |
Go to yearly note | Mod+Shift+Alt+Y |
All call openPeriodNote(period, moment()) — opens or creates the current period's note.
Next/Previous period (no default hotkeys)
10 commands generated in a loop: waypoint-go-to-{next|prev}-{daily|weekly|monthly|quarterly|yearly}.
These call navigatePeriodNote(direction) which:
- Gets the active file's basename.
- Calls
detectPeriodType(basename)— settings-driven, not hardcoded regexes. It readssettings.{daily,weekly,monthly,quarterly,yearly}.nameFormatand triesmoment(basename, nameFormat, true).isValid()(strict parsing) in order day → week → month → quarter → year, returning the first match as{ period, date }. Periods with an emptynameFormatare skipped; returnsnullif nothing matches. Changing a name format in settings therefore keeps next/prev navigation working. - Adds ±1 period (quarters add ±3 months).
- Calls
openPeriodNotefor the new date.
Other commands
| ID | Name |
|---|---|
waypoint-open-view |
Open Waypoint sidebar |
waypoint-add-bookmark |
Add current file as Waypoint bookmark |
Date Systems
A date system is a folder of notes whose filenames begin with a date. Daily notes, a journal and meeting notes are all the same shape, so they share one settings model, one discovery pass and one creation path.
DateSystemSettings
interface DateSystemSettings {
id: string; // stable across edits + reordering, so settings rows can key on it
name: string; // menu label, e.g. "Journal"
folder: string; // e.g. "periodic/journal"
nameFormat: string; // moment format string; may contain "{title}"
templateFile: string; // may omit the .md extension
typeProperty: string; // fallback frontmatter type: value
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:
| `id` | `name` | Folder | `nameFormat` | Indicator colour | Notes per date |
|---|---|---|---|---|---|
| `journal` | Journal | `periodic/journal` | `YYYY-MM-DD - [Journal]` | green (`#22c55e`) | one |
| `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
Waypoint deliberately does not evaluate Templater syntax. For an interactive Templater template, configure Templater's **Trigger Templater on new file creation** mode for the relevant folder, then leave that date system's `templateFile` blank in Waypoint. Waypoint creates a safe fallback note from `typeProperty` and the selected `YYYY-MM-DD` date; Templater's folder trigger owns the rendered content, prompts, scripts, and cursor placement.
The target filename is the contract between them. A calendar-created journal note is already named `YYYY-MM-DD - Journal`; a meeting is already `YYYY-MM-DD - {title}`. Templates must extract the date from `tp.file.title`, not `tp.date` or `tp.file.creation_date()`, because those refer to when Templater runs rather than the date selected in the calendar.
The journal template may retain a manual fallback: only prompt for a date and rename when `tp.file.title` does **not** match the date-system filename. That preserves manual note creation without a prompt or rename race on calendar creation.
### The `{title}` convention
`nameFormat` is a moment format string, so literal text needs bracket escaping — `YYYY-MM-DD - [Journal]` — the same convention the periodic formats `GGGG-[W]WW` and `YYYY-[Q]Q` already use.
When the format contains `{title}`, the system holds **many** notes per date: the text before the token is the date prefix used to discover them, and the token marks where a typed free-text title goes on creation. Without the token, a date maps to **exactly one** filename.
**Why the token is split out before moment sees the string:** `t`, `i`, `l` and `e` are all live moment format tokens — `l` on its own expands to an entire localized date. Passing `{title}` through `date.format()` would expand those letters and destroy the placeholder, with no way to recover it afterwards. `splitNameFormat()` therefore cuts the format at the token *first*, and callers format `before` and `after` separately, then join the two results around the title. It is also why discovery matches on formatted affixes instead of on a regex derived from the raw format.
### Helpers (`src/utils/date-systems.ts`)
Imports nothing from `'obsidian'` — callers make every moment call and pass the resulting strings in, which keeps the module unit-testable in plain node (same rationale as `path-utils.ts`).
| Export | Signature | Purpose |
|---|---|---|
| `TITLE_TOKEN` | `'{title}'` | The literal placeholder. |
| `splitNameFormat(nameFormat)` | `string → NameFormatParts` | `{ before, after, hasTitle }` — the format cut around the token. |
| `isInFolder(path, folder)` | `string, string → boolean` | Path sits in `folder` or any subfolder. An empty folder means the vault root, so everything matches. |
| `matchesSystemName(basename, before, after, hasTitle)` | `string, string, string, boolean → boolean` | One-per-date: the basename must equal `before + after`. Many-per-date: prefix/suffix match, since the middle is free text. |
| `titleFromBasename(basename, before, after)` | `string, string, string → string` | Strips the affixes to recover the menu label; falls back to the whole basename when they do not line up. |
| `sanitizeTitle(title)` | `string → string` | Collapses filename-illegal and link-syntax characters (`\ / : * ? " < > \| # ^ [ ]`) to `-`, squeezes runs of whitespace, trims — so a typed "Meeting w/ Mark" still yields a creatable filename. |
| `formatHasDateToken(nameFormat)` | `string → boolean` | Strips bracket-escaped literals out of `before`, then looks for any moment date token. Drives the settings warning. |
`matchesSystemName` deliberately returns `false` for a many-per-date format whose `before` is empty (a format of just `{title}`): an empty date prefix would otherwise claim every file in the folder for every date. The settings tab flags formats with no date token, and `findDateSystemNotes()` excludes them from the menu; `openDateSystemNote()` also refuses them. An unfinished or invalid row therefore cannot create `.md` or a static filename.
### Plugin API (`main.ts`)
```typescript
interface DateSystemNote {
file: TFile;
label: string; // free-text title for many-per-date systems, else the system name
}
interface DateSystemNotes {
system: DateSystemSettings;
notes: DateSystemNote[];
multiple: boolean; // nameFormat carries {title}
}
interface DateSystemIndicator {
id: string;
name: string;
color: string;
}
dateSystems(): DateSystemSettings[]
findDateSystemNotes(date: moment.Moment): DateSystemNotes[]
getDateSystemIndicators(dates: moment.Moment[]): Map<string, DateSystemIndicator[]>
openDateSystemNote(
system: DateSystemSettings,
date: moment.Moment,
opts?: { title?: string; leaf?: WorkspaceLeaf },
): 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.
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.
openDateSystemNote(system, date, opts) — the single opener for every dated note:
- Filename —
splitNameFormat(system.nameFormat), formatbeforeandafteragainstdate, and for a many-per-date system joinsanitizeTitle(opts.title)between them. Append.md; prependsystem.folderwhen it is set. - Existing note — a
vault.getFileByPath()hit is opened as-is, never overwritten. A date that already has notes can still gain another in a many-per-date system, because the title makes the filename unique. - Missing note — created via
createDatedNote, then opened. - Leaf —
opts.leafwhen supplied (middle-click and Ctrl/Cmd-click pass an explicit tab leaf), otherwiseworkspace.getLeaf(false).
Note creation (createDatedNote)
A single creation path shared by periodic notes and date systems; it replaced the period-only createPeriodNote.
- With a non-empty
system.templateFile, reads it (appending.mdwhen omitted) and creates the note with its raw contents. This is for plain, non-Templater templates. - With no template, the note is created with minimal frontmatter instead:
type: {system.typeProperty}+date: YYYY-MM-DD. Use this mode with a Templater folder trigger, which replaces that fallback content with its rendered template.
Period Note Creation (openPeriodNote)
openPeriodNote(
period: 'day' | 'week' | 'month' | 'quarter' | 'year',
date: moment.Moment,
leaf?: WorkspaceLeaf,
): Promise<void>
Single unified opener — there is no separate openPeriodNoteInLeaf, and since the date-systems refactor there is no separate creation path either. openPeriodNote synthesizes the period's PeriodNoteSettings into a DateSystemSettings (folder, nameFormat, templateFile and typeProperty carry over verbatim; a period format never contains {title}, so a period maps to exactly one filename) and delegates to openDateSystemNote(system, date, { leaf }).
Filename building, the existence check, template-or-frontmatter creation and leaf selection are therefore documented once, under Date Systems above.
Middle-click handlers in the view pass an explicit tab leaf: openPeriodNote(period, date, this.app.workspace.getLeaf('tab')).
Event Handling
file-open
Tracks the opened file into recent files only when settings.recentFiles.updateOn === 'file-open'.
vault:modify
Tracks the modified file (guarded to TFile) into recent files only when updateOn === 'file-edit'. Exactly one of the two modes is active at a time, so 'file-edit' is a working mode rather than a no-op.
vault:create / vault:delete
Both update the markdown-basename set used for calendar note indicators, then trigger broadcastRedraw() — telling all WaypointView instances to re-render (needed for the indicators and for stale recent file references).
vault:rename
- Updates the markdown-basename set (old basename out, new basename in).
- Remaps the
recentFilesentry throughremapRenamedPath(entry.path, oldPath, newPath). - Remaps every bookmark
filePaththrough the same helper.
remapRenamedPath(path, oldPath, newPath) (in src/utils/path-utils.ts) returns the updated path when path is oldPath (a plain file rename) or is nested under it (path.startsWith(oldPath + '/'), i.e. a folder rename/move), and null when the path is unaffected. This is why moving a folder no longer orphans the bookmarks inside it. The helper deliberately imports nothing from 'obsidian' so it stays pure and unit-testable in plain node.
Recent Files (persisted)
recentFiles: { path: string; basename: string }[]
Backed by waypointData.recentFiles in data.json, so the list survives vault reload. applyWaypointData(saved) restores it and re-applies the current maxItems limit (in case the setting shrank since the last save).
Update flow:
addToRecentFiles(file)— omission check, then dedupes (removes existing entry), prepends to front, truncates tomaxItems.persistRecentFiles()— mirrors the array intowaypointData.recentFilesand schedules a debounced (300ms)saveWaypointData().- Calls
broadcastRedraw().
Omitted paths/tags: Both filters are applied in addToRecentFiles. omittedPaths entries are treated as regexes tested against file.path; omittedTags entries are regexes tested against the file's tags, read via getAllTags(metadataCache.getFileCache(file)) with the leading # stripped. An invalid regex is skipped rather than throwing.
enforceRecentFilesLimit(): Called from the settings-change callback — trims the list when maxItems is lowered and persists the result.
On file not found: When clicking a recent file that no longer exists, focusFile() shows a Notice and removes the stale entry from recentFiles.
Sidebar View (WaypointView)
Extends ItemView. View type: "waypoint-view", icon: compass.
Rendering (redraw)
Called on every change. Full DOM rebuild — contentEl.empty() then three sections in order:
- Favorites (
renderFavorites) — bookmark tree - Recent Files (
renderRecentFiles) — flat list - Calendar (
renderCalendar) — month grid
The last section (waypoint-section:last-child) gets margin-top: auto, pushing it to the bottom of the sidebar.
Calendar Panel
State: currentDisplayMonth (0-indexed), currentDisplayYear — allows navigating months independently of today.
Layout:
┌────────────────────────────────────┐
│ Q2 June 2026 ◀ Today ▶ │ ← breadcrumb (clickable) + nav
├──┬───┬───┬───┬───┬───┬───┬───┤
│24│sun│mon│tue│wed│thu│fri│sat│ ← header row + week# col
├──┼───┼───┼───┼───┼───┼───┼───┤
│25│ 1 │ 2 │ 3 │... ← day cells (clickable)
└──┴───┴───┴───┴───┴───┴───┴───┘
- Breadcrumb: Q-label, month name, year — each clickable to open that period note.
- 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.
- 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-monthis muted;.todayhas an inset accent ring; hover receives a quiet fill. WithindicatorMode: 'any',.has-noteshows the existing single neutral dot through the O(1)plugin.hasNoteForDate(dateStr)lookup. WithindicatorMode: 'systems', the view callsgetDateSystemIndicators()once for the month and renders one tooltip-labelled coloured dot per matching daily/journal/meeting/custom system. - Grid generation:
getMonthGrid(year, month, firstDayOfWeek)indate-utils.tsproduces up to 6 weeks, each with 7CalendarDayobjects containingmoment,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:
- Each existing note, labelled with its
DateSystemNote.labeland the system'sicon. Click opens it in the current leaf; Ctrl/Cmd-click opens it in a new tab. - A create entry (icon
plus) labelledNew {lowercased system name} note. Many-per-date systems always show one, with a trailing ellipsis because it first opens aPromptModalfor the title (New meeting note…). A one-per-date system shows one without the ellipsis only while its note is missing (New journal note); once the note exists, the existing-note entry is all it gets.
Every label is derived from system.name, so a renamed or newly added system needs no view changes. Left-click and middle-click on the cell are untouched.
Recent Files Panel
Renders plugin.recentFiles as Obsidian-native nav-file elements using the file explorer's CSS classes (tree-item, nav-file-title, nav-file-title-content).
Per-file features:
- Active indicator:
.is-activeclass if path matchesworkspace.getActiveFile(). - Remove button: × icon, appears on hover (
.waypoint-recent-remove), drops the entry fromplugin.recentFiles, callspersistRecentFiles(), then redraws. - Drag: Uses
app.dragManager.dragFile()for native Obsidian drag. - Hover preview: Triggers
hover-linkevent for Obsidian's page preview popup. - Context menu: "Open in new tab" + Obsidian's native
file-menuevent. - Click:
Keymap.isModEvent(event)determines if opening in new leaf; otherwise same leaf. - Middle click: Opens in new tab.
Bookmarks/Favorites Panel
Empty state: Right-click on "Waypoint Bookmarks" header shows add menu:
- Add current file →
addBookmark(path, basename, 'file') - New group →
addBookmark('', 'New Group', 'group', 'folder') - Add separator →
addBookmark('', '', 'separator') - Add spacer →
addBookmark('', '', 'spacer')
Rendering: renderBookmarkList(container, items, depth) recursively renders items with paddingLeft: 8 + depth * 16px for indentation.
Item types:
| Type | Render | Behavior |
|---|---|---|
file |
icon + label | Click opens file. Tooltip = filePath. |
group |
chevron + icon + label | Click toggles collapsed. Children rendered in nested waypoint-bookmark-children div (hidden when collapsed). |
separator |
horizontal line (::after pseudo-element) | No click handler. Context menu for delete. |
spacer |
14px empty div | No click handler. Context menu for delete. |
Drag-and-drop: All bookmark items are draggable. Drop indicators show a 3px accent border above/below the target. On drop:
- Remove dragged item from its current position (recursive search).
- Insert at the target position (before or after based on cursor Y position relative to target midY).
- If target not found (edge case), push to root.
- Save + redraw.
Context menu (right-click):
- File items: "Open in new tab"
- File/Group items: "Rename" (opens
PromptModal), "Change icon" (opensIconSuggestModal) - "Move to group" submenu: Lists all available groups (excluding self + descendants) + "(Root)" for ungrouping
- Group items: "Expand/Collapse", "Add bookmark here", "New sub-group"
- All items: "Insert separator above/below", "Insert spacer above/below", "Remove"
Move to group (moveBookmarkToGroup):
- Remove item from current position.
- If targetGroupId: find group, set
item.indent = group.indent + 1, push to group's children. - If null (root): set
item.indent = 0, push towaypointData.bookmarks.
Prompt modal (PromptModal): Simple Modal with a text input plus Cancel and CTA buttons. Enter key submits. Constructed as new PromptModal(app, options, onSubmit), where options is { title, placeholder?, initialValue?, cta? } and cta (the submit button label) defaults to Save. Generalized from the old rename-only RenameModal so the calendar's day context menu can reuse it to prompt for a note title.
Icon picker (IconSuggestModal): Modal with:
- Live preview of selected icon.
- Search input with 60ms debounce.
- Grid of matching icons (max 80 shown), loaded from
https://cdn.jsdelivr.net/npm/lucide-static@0.517.0/tags.jsonwith fallback tohttps://lucide.dev/api/tagsand a hardcodedFALLBACK_ICONSobject (~300 icons). The fetch result is cached for the session, so at most one network round-trip happens no matter how often the picker is opened. - Click to select, No icon button to clear, Save/Cancel buttons.
CSS Architecture (styles.css)
All classes prefixed with waypoint-. Uses Obsidian CSS variables throughout:
--font-ui-small,--font-ui-medium,--font-semibold,--font-medium,--font-light--text-muted,--text-faint,--text-accent,--text-on-accent,--text-error--background-modifier-border,--background-modifier-active-hover,--background-modifier-hover,--background-primary,--background-primary-alt,--background-secondary--interactive-accent--cursor-link(pointer cursor, with apointerfallback)
Key layout:
.waypoint-view— flex column,overflow-y: auto, 8px padding..waypoint-section—flex-shrink: 0, 16px bottom margin. Last section getsmargin-top: auto(pins calendar to bottom)..waypoint-calendar— padded, bordered surface with separated day cells for clear scan lines.- Calendar table —
table-layout: fixed,border-collapse: separate, 2px horizontal / 3px vertical cell spacing. .waypoint-day.today— accent text with an inset accent ring, avoiding layout shifts..waypoint-day.has-note::after— neutral single-dot indicator..waypoint-day-indicators/.waypoint-day-indicator— compact ordered dot row;--waypoint-indicator-colorcarries each configured system colour..waypoint-bm-chevron—rotate(-90deg)on collapsed groups..waypoint-bm-drop-line/.waypoint-bm-drop-below— 3px accent border for drag indicators.
Date Utilities (date-utils.ts)
All use Obsidian's bundled moment (not the npm package).
| Function | Signature | Returns |
|---|---|---|
getPeriodInfo(date) |
Moment → PeriodInfo |
{day, week, month, quarter, year} strings for the date |
getQuarterString(date) |
Moment → string |
e.g. "2026-Q3" |
getTodayPeriod() |
() → PeriodInfo |
Current date's period info |
getMonthGrid(year, month, firstDayOfWeek) |
number, number, number → CalendarWeek[] |
Up to 6 weeks, each with 7 CalendarDay objects |
formatDateLabel(date) |
Moment → string |
"June 1st, 2026" |
formatMonthLabel(date) |
Moment → string |
"June 2026" |
formatQuarterLabel(date) |
Moment → string |
"Q3 2026" |
navigateDate(date, period, delta) |
Moment, period, number → Moment |
Clones + adds delta (quarters: delta×3 months) |
formatPeriodName(date, format) |
Moment, string → string |
date.format(format) wrapper |
getISOWeek(date) |
Moment → number |
date.isoWeek() |
getMonthGrid algorithm:
- Find the first day to display by subtracting
(firstOfMonth.day() - firstDayOfWeek + 7) % 7days from the 1st. - Iterate day-by-day, building 7-day weeks.
- Each
CalendarDaytracks:date(Moment),dayOfMonth,isToday,isCurrentMonth,isoWeekNumber. - Week's
weekNumber= first day's ISO week number. - Safety break after 6 weeks.
Settings Persistence
Settings and waypoint data share a single data.json via Obsidian's Plugin.loadData()/saveData():
// loadData() returns:
{
settings: { calendar: {...}, daily: {...}, ... },
waypointData: { bookmarks: [...] }
}
onload() reads data.json once, then applySettings(saved) and applyWaypointData(saved) merge partial persisted values over defaults. persistAll() writes both in-memory keys through one promise chain; it never re-reads stale disk state. Recent-file writes additionally go through the 300ms debounce in persistRecentFiles().
Redraw Mechanism
broadcastRedraw() iterates all leaves of WAYPOINT_VIEW_TYPE and calls view.redraw() on each. redraw() is a full DOM rebuild (no virtual DOM, no diffing). This is triggered by:
- File open/create/delete/rename/modify events
- Bookmark add/remove/update
- Period navigation
- Settings changes (via
onSettingsChangecallback) - Midnight detection (10-min interval)
The sidebar is re-rendered from scratch on every change. For a small sidebar this is acceptable; for larger bookmark lists it may cause flicker.
Known Limitations / Gaps
- No virtual DOM — Full rebuild on every redraw. Bookmark drag/drop, rename, and any change tears down and rebuilds the entire sidebar. For a small sidebar this is acceptable; for larger bookmark lists it may cause flicker.
- IconSuggestModal fetches from CDN — Network dependency for the Lucide tag list. The fetch happens at most once per session (the result is cached) and falls back to the hardcoded
FALLBACK_ICONSlist on failure, but a first-open with no network still degrades to that reduced list.