Initial commit: Waypoint Obsidian plugin with calendar, recent files, and bookmarks

This commit is contained in:
2026-06-03 20:26:23 -04:00
commit fbdc42b4a3
16 changed files with 3400 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
.DS_Store
*.log
+329
View File
@@ -0,0 +1,329 @@
# Waypoint Plugin — Implementation Plan
## Overview
A sidebar plugin for Obsidian that combines three panels into one coherent view: a **periodic note calendar** (day/week/month/quarter/year), a **recent files list**, and a **custom favorites/bookmarks system** with rename, icons, and grouping.
---
## Features (from requirements)
1. **Calendar panel** — shows a month grid with every day clickable
2. **Period indicators** — day, week, month, quarter, year all displayed and clickable
3. **Note creation on click** — clicking a period creates a note at a configurable path with a configurable template
4. **Recent files** — list of recently opened/edited files
5. **Favorites/Bookmarks** — custom bookmarks (distinct from Obsidian native bookmarks) with:
- Command: "Add current file as Waypoint bookmark"
- Rename without renaming the underlying file
- Assign an icon (from Lucide icons)
- Right-click context menu: rename, change icon, remove, move
- Foldable groups and indented hierarchy
6. **Settings** — configure folder paths and templates for each period type
---
## Architecture
### Approach
- **Plain ItemView** (not React) — simpler, matches recent-files-obsidian's approach, avoids extra dependencies
- **Single view type** `waypoint-view` that contains all three panels stacked vertically
- **Plugin data** stored via `Plugin.loadData()` / `Plugin.saveData()` for bookmarks and recent files
- **Settings** via `PluginSettingTab`
### Why not React?
The daily-note-calendar plugin uses React, but that adds complexity (esbuild config for JSX, react-dom dependency). The recent-files plugin does everything with plain Obsidian DOM helpers (`createDiv`, `setIcon`, etc.) and is much simpler. We can build a clean, native-feeling UI without React. If the calendar grid becomes complex, we can reconsider.
### File Structure
```
waypoint/
├── manifest.json
├── package.json
├── tsconfig.json
├── esbuild.config.mjs
├── styles.css
├── src/
│ ├── main.ts # Plugin entry point
│ ├── settings.ts # Settings interface + defaults + SettingTab
│ ├── views/
│ │ ├── waypoint-view.ts # Main ItemView (orchestrates panels)
│ │ ├── calendar-panel.ts # Calendar month grid + period indicators
│ │ ├── recent-files-panel.ts # Recent files list
│ │ └── favorites-panel.ts # Favorites/bookmarks tree
│ ├── models/
│ │ └── bookmark.ts # Bookmark item/group interfaces
│ ├── services/
│ │ ├── calendar-service.ts # Date math, period detection, note creation
│ │ ├── recent-files-service.ts # Track and filter recent files
│ │ └── favorites-service.ts # CRUD for custom bookmarks
│ └── utils/
│ ├── date-utils.ts # Date formatting, period navigation
│ └── icon-utils.ts # Icon picker modal
└── README.md
```
---
## Component Details
### 1. Calendar Panel (`calendar-panel.ts`)
**Visual layout (top to bottom):**
```
┌─────────────────────────────────────┐
│ Q3 · June · 1st, 2026 │ ← Breadcrumb: quarter, month, day (all clickable)
├─────────────────────────────────────┤
│ ◀ June 2026 ▶ │ ← Month+Year header with nav
├─────────────────────────────────────┤
│ Mon Tue Wed Thu Fri Sat Sun │ ← Day-of-week headers
├─────────────────────────────────────┤
│ W22 1 2 3 4 5 6 │ ← Week rows (week number + 7 days)
│ W23 7 8 9 10 11 12 13 │
│ W24 14 15 16 17 18 19 20 │
│ W25 21 22 23 24 25 26 27 │
│ W26 28 29 30 │
├─────────────────────────────────────┤
│ ◀ [Today] ▶ │ ← Today navigation
└─────────────────────────────────────┘
```
**Behavior:**
- **Click a day cell** → open or create the daily note for that date
- **Click week number** → open/create the weekly note
- **Click breadcrumb quarter** → open/create the quarterly note
- **Click breadcrumb month** → open/create the monthly note
- **Click breadcrumb day** → open/create the daily note
- **Left/right arrows on month header** → navigate months
- **"Today" button** → jump to current month
- **Dot indicator** → show which days already have notes
- **Today highlight** → accent-colored border on current day
- **Week numbers** displayed as the first column, using ISO week numbering (weeks start on Monday)
**Note creation logic:**
1. Check if a note exists at `{folder}/{nameTemplate}` using the date
2. If it exists, open it
3. If not, create it using the configured template file (via Obsidian API)
4. Apply the `type` property matching the period type (daily-note, weekly-note, etc.)
### 2. Recent Files Panel (`recent-files-panel.ts`)
Based closely on the recent-files-obsidian plugin:
- Track file opens/edits via `app.workspace.on('file-open')`
- Store as ordered list of `{ path, basename }`
- Display in a `nav-folder` styled list (matches Obsidian's file explorer look)
- Each item shows:
- File name (supporting frontmatter-title plugin if available)
- Hover preview via `hover-link` event
- Drag support (wikilink insertion)
- Right-click: "Open in new tab" + Obsidian file menu
- Middle-click / Ctrl+click: open in new pane
- X button on hover to remove from list
- Respect omitted paths/tags filter (from settings)
- Max length configurable
### 3. Favorites / Bookmarks Panel (`favorites-panel.ts`)
**Data model:**
```typescript
interface BookmarkItem {
id: string; // unique ID
type: 'file' | 'group';
label: string; // display name (can differ from file name)
filePath: string; // path to the Obsidian file (empty for groups)
icon: string; // Lucide icon name (default: 'file')
children: BookmarkItem[]; // for groups
collapsed: boolean; // group fold state
indent: number; // indent level (0 = top level)
}
```
**Visual layout:**
```
── Waypoint Bookmarks ────
📁 Projects ← group (collapsible)
📄 Fabrique ← file bookmark
📄 Haro dashboard ← file bookmark
📁 References ← group
📄 CSS grid notes
📄 Inbox ← ungrouped bookmark
```
**Behavior:**
- **Click file bookmark** → opens the file
- **Click group** → toggles collapse/expand
- **Drag to reorder** (stretch goal — use Obsidian's drag system)
- **Right-click on file bookmark** → context menu:
- Rename bookmark
- Change icon (opens icon picker)
- Remove bookmark
- Move to group → submenu of existing groups
- **Right-click on group** → context menu:
- Rename group
- Change icon
- Remove group (and all children)
- Add bookmark to this group
- New sub-group
- **Right-click on empty space** → context menu:
- New group
- Add current file
**Add current file command:**
- Command ID: `waypoint:add-bookmark`
- Picks up the active file
- Shows a prompt to set the display name (defaults to file's basename)
- Shows an icon picker (default: file icon)
- Adds to root level (or active group if one is right-clicked)
### 4. Settings Tab (`settings.ts`)
```
┌── Waypoint Settings ──────────────────┐
│ │
│ 📅 Calendar │
│ ┌─ General ─────────────────────────┐ │
│ │ First day of week [Monday ▼] │ │
│ │ Week number standard [ISO ▼] │ │
│ │ Show note indicators [toggle] │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌─ Periodic Note Paths ─────────────┐ │
│ │ Daily note │ │
│ │ Folder: [periodic/daily ] │ │
│ │ Template: [templates/daily ] │ │
│ │ Name: [yyyy-MM-dd ] │ │
│ │ Type: [daily-note ] │ │
│ │ │ │
│ │ Weekly note │ │
│ │ Folder: [periodic/weekly ] │ │
│ │ Template: [templates/weekly ] │ │
│ │ Name: [GGGG-[W]WW ] │ │
│ │ Type: [weekly-note ] │ │
│ │ │ │
│ │ Monthly note │ │
│ │ Folder: [periodic/monthly ] │ │
│ │ ... │ │
│ │ │ │
│ │ Quarterly note │ │
│ │ ... │ │
│ │ │ │
│ │ Yearly note │ │
│ │ ... │ │
│ └──────────────────────────────────┘ │
│ │
│ 📄 Recent Files │
│ ┌──────────────────────────────────┐ │
│ │ Max items: [50 ] │ │
│ │ Update on: [File opened ▼] │ │
│ │ Omitted paths: [textarea ] │ │
│ │ Omitted tags: [textarea ] │ │
│ └──────────────────────────────────┘ │
│ │
│ ⭐ Favorites │
│ ┌──────────────────────────────────┐ │
│ │ (No specific settings yet) │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘
```
---
## Implementation Steps
### Step 1: Project scaffolding
- Create `manifest.json`, `package.json`, `tsconfig.json`, `esbuild.config.mjs`
- Install deps: `obsidian`, `builtin-modules`, typescript
- Create folder structure
### Step 2: Settings
- Define interfaces: `WaypointSettings`, `PeriodNoteSettings`
- Defaults matching O-vault convention:
- Daily: `periodic/daily/`, template `Templates/Daily note`, name `yyyy-MM-dd`
- Weekly: `periodic/weekly/`, template `Templates/Weekly note`, name `GGGG-[W]WW`
- Monthly: `periodic/monthly/`, template `Templates/Monthly note`, name `yyyy-MM`
- Quarterly: `periodic/quarterly/`, template `Templates/Quarterly note`, name `yyyy-[Q]Q`
- Yearly: `periodic/yearly/`, template `Templates/Yearly note`, name `yyyy`
- Implement `WaypointSettingTab` with sections for each period + recent files
### Step 3: Calendar Service
- `getMonthGrid(year, month, firstDayOfWeek)` → array of weeks with day objects
- `getPeriodFromDate(date)` → { day, week, month, quarter, year }
- `findOrCreateNote(period, settings)` → checks vault, creates if missing
- Date formatting using `moment()` (bundled in Obsidian)
### Step 4: Calendar Panel (ItemView section)
- Render the calendar grid using Obsidian DOM (`createDiv`, `createSpan`)
- Period header row (year/quarter/week)
- Month header with nav arrows
- Day-of-week header row
- Day cells (with today highlight, note indicators, other-month dimming)
- Arrow navigation for months
- Click handlers for each period type
### Step 5: Recent Files Service
- Track `file-open` events
- Maintain ordered list (max length, dedup)
- Support omitted paths/tags filtering
### Step 6: Recent Files Panel
- Render the list in `nav-folder` style
- Click, hover, drag, right-click, middle-click, delete button
### Step 7: Favorites Service
- CRUD operations on `BookmarkItem[]`
- Persist via `Plugin.loadData/saveData`
- Add current file command
- Rename, icon change, remove, grouping
### Step 8: Favorites Panel
- Render tree with collapsible groups
- Context menus for items and groups
- Icon display using `setIcon()`
- Drag support (stretch)
### Step 9: Waypoint View (main orchestrator)
- `ItemView` subclass
- On open: create three sections inside `contentEl`
- Redraw on data changes
- Register as sidebar view type `waypoint-view`
### Step 10: Main Plugin
- `onload()`: load data, register view, register commands, register events, add settings tab
- Auto-open on right sidebar on first load
- Commands:
- `waypoint:open-view` → reveal the waypoint sidebar
- `waypoint:add-bookmark` → add current file as bookmark
- `waypoint:go-to-today` → open today's daily note
- Events:
- `file-open` → update recent files + redraw
- `create`, `delete`, `rename` → update calendar note indicators + redraw
- Midnight check → refresh today highlight
---
## Key Design Decisions
1. **No React** — Using native Obsidian DOM APIs avoids build complexity and keeps the bundle small. The calendar grid is straightforward enough to render imperatively.
2. **Flat data storage** — Bookmarks and recent files stored via Plugin.loadData/saveData as JSON. This makes them portable and compatible with Obsidian sync.
3. **Separate from native bookmarks** — We never touch `app.internalPlugins.getEnabledPluginById('bookmarks')`. These are entirely custom bookmarks with their own data store.
4. **Icon picker** — We'll build a simple icon picker modal using Obsidian's `SuggestModal` or a grid of Lucide icons that are commonly used (file, folder, star, heart, bookmark, flag, pin, etc.). We won't implement a searchable full icon browser initially — a curated selection of ~40 icons plus a text input for any Lucide icon name.
5. **O-vault aware** — Default paths match Olivier's vault structure. All settings are overridable.
---
## Stretch Goals (after MVP)
- Drag-and-drop reordering in favorites panel
- Search/filter within bookmarks
- Multiple bookmark lists / profiles
- Sync bookmarks with Obsidian Sync
- Show note previews on calendar hover
- Custom CSS classes per bookmark (color coding)
- Nested groups beyond 2 levels
- Collapse/expand all in favorites
- Keyboard shortcuts for bookmark navigation
+46
View File
@@ -0,0 +1,46 @@
import esbuild from 'esbuild';
import process from 'process';
import builtins from 'builtin-modules';
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
*/
`;
const prod = process.argv[2] === 'production';
const context = await esbuild.context({
banner: { js: banner },
entryPoints: ['src/main.ts'],
bundle: true,
external: [
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins,
],
format: 'cjs',
target: 'es2018',
logLevel: 'info',
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: 'main.js',
minify: prod,
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}
+16
View File
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
{
"id": "waypoint",
"name": "Waypoint",
"version": "1.0.0",
"minAppVersion": "0.16.3",
"description": "Calendar, recent files, and custom bookmarks sidebar.",
"author": "Olivier",
"isDesktopOnly": false
}
+639
View File
@@ -0,0 +1,639 @@
{
"name": "waypoint",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "waypoint",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"builtin-modules": "4.0.0",
"esbuild": "^0.25.0",
"obsidian": "latest",
"typescript": "^5.6.0"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.38.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@types/codemirror": {
"version": "5.60.8",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/tern": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/builtin-modules": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
"integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/obsidian": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.13.0.tgz",
"integrity": "sha512-PHw5+SAPlJ0S3leFvJ0wgFg63Z3DavxL6+d1ll+8toXR2ZlYKc1rMWqdUv9LgUbTwPQUyY6yfhOMMivampRRiQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/codemirror": "5.60.8",
"moment": "2.29.4"
},
"peerDependencies": {
"@codemirror/state": "6.5.0",
"@codemirror/view": "6.38.6"
}
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"dev": true,
"license": "MIT",
"peer": true
}
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"name": "waypoint",
"version": "1.0.0",
"description": "Calendar, recent files, and custom bookmarks sidebar.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "node esbuild.config.mjs production"
},
"keywords": [],
"author": "Olivier",
"license": "MIT",
"devDependencies": {
"builtin-modules": "4.0.0",
"esbuild": "^0.25.0",
"obsidian": "latest",
"typescript": "^5.6.0"
}
}
+352
View File
@@ -0,0 +1,352 @@
// ── Main plugin entry ──
import {
Plugin,
WorkspaceLeaf,
ItemView,
Notice,
TFile,
TAbstractFile,
moment,
} from 'obsidian';
import { WaypointSettings, DEFAULT_SETTINGS, PeriodNoteSettings } from 'src/settings';
import { WaypointSettingTab } from 'src/settings-tab';
import { WaypointView, WAYPOINT_VIEW_TYPE } from 'src/views/waypoint-view';
import { BookmarkItem, WaypointData } from 'src/models/bookmark';
const DEFAULT_DATA: WaypointData = {
bookmarks: [],
};
export default class WaypointPlugin extends Plugin {
public settings: WaypointSettings;
public waypointData: WaypointData;
public recentFiles: { path: string; basename: string }[] = [];
async onload(): Promise<void> {
console.debug('Waypoint: loading plugin v' + this.manifest.version);
// Load persisted data
await this.loadSettings();
await this.loadWaypointData();
// Register the sidebar view
this.registerView(
WAYPOINT_VIEW_TYPE,
(leaf: WorkspaceLeaf) => new WaypointView(leaf, this),
);
// Register settings tab
this.addSettingTab(new WaypointSettingTab(
this.app,
this,
this.settings,
() => this.redrawAll(),
));
// ── Commands ──
this.addCommand({
id: 'waypoint-open-view',
name: 'Open Waypoint sidebar',
callback: async () => {
const leaves = this.app.workspace.getLeavesOfType(WAYPOINT_VIEW_TYPE);
if (leaves.length > 0) {
await this.app.workspace.revealLeaf(leaves[0]);
} else {
const leaf = this.app.workspace.getLeftLeaf(false);
if (leaf) {
await leaf.setViewState({ type: WAYPOINT_VIEW_TYPE });
await this.app.workspace.revealLeaf(leaf);
}
}
},
});
this.addCommand({
id: 'waypoint-add-bookmark',
name: 'Add current file as Waypoint bookmark',
callback: async () => {
const file = this.app.workspace.getActiveFile();
if (!file) {
new Notice('No active file');
return;
}
this.addBookmark(file.path, file.basename, 'file');
new Notice(`Bookmarked: ${file.basename}`);
},
});
this.addCommand({
id: 'waypoint-go-to-today',
name: 'Go to today\'s daily note',
callback: async () => {
await this.openPeriodNote('day', moment());
},
});
// ── Events ──
this.registerEvent(
this.app.workspace.on('file-open', (file: TFile | null) => {
if (file) this.onFileOpen(file);
}),
);
this.registerEvent(
this.app.vault.on('create', () => this.onVaultChange()),
);
this.registerEvent(
this.app.vault.on('delete', () => this.onVaultChange()),
);
this.registerEvent(
this.app.vault.on('rename', (file, oldPath) => this.onRename(file, oldPath)),
);
// Auto-open view on first load
this.app.workspace.onLayoutReady(() => {
const leaves = this.app.workspace.getLeavesOfType(WAYPOINT_VIEW_TYPE);
if (leaves.length === 0) {
const leaf = this.app.workspace.getLeftLeaf(false);
if (leaf) {
leaf.setViewState({ type: WAYPOINT_VIEW_TYPE });
}
}
});
// Midnight refresh (check every 10 min)
let lastDate = new Date().toDateString();
this.registerInterval(
window.setInterval(() => {
const currentDate = new Date().toDateString();
if (currentDate !== lastDate) {
lastDate = currentDate;
this.redrawAll();
}
}, 600000), // 10 min
);
}
async onunload(): Promise<void> {
this.app.workspace.detachLeavesOfType(WAYPOINT_VIEW_TYPE);
}
// ── Settings ──
async loadSettings(): Promise<void> {
const saved = await this.loadData() as Record<string, unknown> | null;
const s = (saved?.settings || {}) as Partial<WaypointSettings>;
this.settings = Object.assign({}, DEFAULT_SETTINGS, s);
}
async saveSettings(): Promise<void> {
const all = (await this.loadData()) as Record<string, unknown> || {};
all.settings = this.settings;
await this.saveData(all);
}
async loadWaypointData(): Promise<void> {
const saved = await this.loadData() as Record<string, unknown> | null;
const d = (saved?.waypointData || {}) as Partial<WaypointData>;
this.waypointData = Object.assign({}, DEFAULT_DATA, d);
}
async saveWaypointData(): Promise<void> {
const all = (await this.loadData()) as Record<string, unknown> || {};
all.waypointData = this.waypointData;
await this.saveData(all);
}
// ── Recent Files ──
private onFileOpen(file: TFile): void {
if (this.settings.recentFiles.updateOn === 'file-edit') {
// We'll handle this via quick-preview in a future refinement
return;
}
this.addToRecentFiles(file);
}
addToRecentFiles(file: TFile): void {
this.recentFiles = this.recentFiles.filter(f => f.path !== file.path);
this.recentFiles.unshift({ path: file.path, basename: file.basename });
// Prune
if (this.recentFiles.length > this.settings.recentFiles.maxItems) {
this.recentFiles = this.recentFiles.slice(0, this.settings.recentFiles.maxItems);
}
this.broadcastRedraw();
}
private onRename(file: TAbstractFile, oldPath: string): void {
const entry = this.recentFiles.find(f => f.path === oldPath);
if (entry) {
entry.path = file.path;
entry.basename = (file as TFile).basename || file.name.replace(/\.[^/.]+$/, '');
this.broadcastRedraw();
}
// Update bookmark file paths
this.updateBookmarkPath(oldPath, file.path);
}
private onVaultChange(): void {
this.broadcastRedraw();
}
// ── Bookmarks ──
addBookmark(filePath: string, label: string, type: 'file' | 'group' | 'separator' | 'spacer' = 'file', icon = 'file'): BookmarkItem {
const id = `bm-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
const item: BookmarkItem = {
id,
type,
label,
filePath: type === 'file' ? filePath : '',
icon,
children: [],
collapsed: false,
indent: 0,
};
this.waypointData.bookmarks.push(item);
this.saveWaypointData();
this.broadcastRedraw();
return item;
}
removeBookmark(id: string): void {
const removeRecursive = (items: BookmarkItem[]): boolean => {
const idx = items.findIndex(i => i.id === id);
if (idx >= 0) {
items.splice(idx, 1);
return true;
}
for (const item of items) {
if (item.children && removeRecursive(item.children)) return true;
}
return false;
};
removeRecursive(this.waypointData.bookmarks);
this.saveWaypointData();
this.broadcastRedraw();
}
updateBookmark(id: string, updates: Partial<BookmarkItem>): void {
const findRecursive = (items: BookmarkItem[]): BookmarkItem | null => {
for (const item of items) {
if (item.id === id) return item;
if (item.children) {
const found = findRecursive(item.children);
if (found) return found;
}
}
return null;
};
const item = findRecursive(this.waypointData.bookmarks);
if (item) {
Object.assign(item, updates);
this.saveWaypointData();
this.broadcastRedraw();
}
}
private updateBookmarkPath(oldPath: string, newPath: string): void {
const updateRecursive = (items: BookmarkItem[]) => {
for (const item of items) {
if (item.filePath === oldPath) {
item.filePath = newPath;
}
if (item.children) updateRecursive(item.children);
}
};
updateRecursive(this.waypointData.bookmarks);
}
// ── Period note creation/opening ──
async openPeriodNote(period: 'day' | 'week' | 'month' | 'quarter' | 'year', date: moment.Moment): Promise<void> {
// Map period to settings and date format
type PeriodConfig = {
settings: PeriodNoteSettings;
label: string;
};
const configs: Record<string, PeriodConfig> = {
day: { settings: this.settings.daily, label: 'Daily' },
week: { settings: this.settings.weekly, label: 'Weekly' },
month: { settings: this.settings.monthly, label: 'Monthly' },
quarter: { settings: this.settings.quarterly, label: 'Quarterly' },
year: { settings: this.settings.yearly, label: 'Yearly' },
};
const config = configs[period];
if (!config) return;
const { settings: periodSettings } = config;
const filename = date.format(periodSettings.nameFormat) + '.md';
const fullPath = periodSettings.folder
? `${periodSettings.folder}/${filename}`
: filename;
// Check if exists
let file = this.app.vault.getFileByPath(fullPath);
if (!file) {
// Create it from template
try {
// Try to find template file
const templatePath = periodSettings.templateFile + '.md';
const templateFile = this.app.vault.getFileByPath(templatePath);
if (templateFile) {
const templateContent = await this.app.vault.read(templateFile);
file = await this.app.vault.create(fullPath, templateContent);
} else {
// Fallback: create with minimal frontmatter
const content = `---\ntype: ${periodSettings.typeProperty}\ndate: ${date.format('YYYY-MM-DD')}\n---\n\n`;
file = await this.app.vault.create(fullPath, content);
}
} catch (err) {
new Notice(`Failed to create ${config.label.toLowerCase()} note: ${err.message}`);
return;
}
new Notice(`Created ${config.label.toLowerCase()} note: ${filename}`);
}
if (file) {
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file);
}
}
// ── Redraw ──
private redrawAll(): void {
this.broadcastRedraw();
}
private broadcastRedraw(): void {
const leaves = this.app.workspace.getLeavesOfType(WAYPOINT_VIEW_TYPE);
for (const leaf of leaves) {
if (leaf.view instanceof WaypointView) {
leaf.view.redraw();
}
}
}
/**
* Get all markdown files that exist on a specific date.
* Used to show note indicators on the calendar.
*/
async getNotesForDate(dateStr: string): Promise<TFile[]> {
return this.app.vault.getFiles().filter(f =>
f.extension === 'md' && f.basename === dateStr,
);
}
async hasNoteForDate(dateStr: string): Promise<boolean> {
return this.app.vault.getFiles().some(f =>
f.extension === 'md' && f.basename === dateStr,
);
}
}
+16
View File
@@ -0,0 +1,16 @@
// ── Bookmark data model ──
export interface BookmarkItem {
id: string;
type: 'file' | 'group' | 'separator' | 'spacer';
label: string;
filePath: string;
icon: string;
children: BookmarkItem[];
collapsed: boolean;
indent: number;
}
export interface WaypointData {
bookmarks: BookmarkItem[];
}
+178
View File
@@ -0,0 +1,178 @@
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;
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();
// ── Calendar Section ──
new Setting(containerEl).setHeading().setName('Calendar');
new Setting(containerEl)
.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(containerEl)
.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 Note Settings ──
new Setting(containerEl).setHeading().setName('Periodic Note Paths');
this.addPeriodNoteSettings('Daily', this.settings.daily);
this.addPeriodNoteSettings('Weekly', this.settings.weekly);
this.addPeriodNoteSettings('Monthly', this.settings.monthly);
this.addPeriodNoteSettings('Quarterly', this.settings.quarterly);
this.addPeriodNoteSettings('Yearly', this.settings.yearly);
// ── Recent Files Section ──
new Setting(containerEl).setHeading().setName('Recent Files');
new Setting(containerEl)
.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(containerEl)
.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(containerEl)
.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(containerEl)
.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();
};
});
}
private addPeriodNoteSettings(label: string, period: PeriodNoteSettings): void {
new Setting(this.containerEl).setHeading().setName(label);
new Setting(this.containerEl)
.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(this.containerEl)
.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(this.containerEl)
.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(this.containerEl)
.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();
});
});
}
private async saveAndRefresh(): Promise<void> {
await this.plugin.saveData(this.settings);
this.onSettingsChange();
}
}
+73
View File
@@ -0,0 +1,73 @@
// ── Waypoint Settings ──
export interface PeriodNoteSettings {
folder: string;
templateFile: string;
nameFormat: string;
typeProperty: string;
}
export interface CalendarSettings {
firstDayOfWeek: number; // 0=Sunday, 1=Monday
showNoteIndicators: boolean;
}
export interface RecentFilesSettings {
maxItems: number;
updateOn: 'file-open' | 'file-edit';
omittedPaths: string[];
omittedTags: string[];
}
export interface WaypointSettings {
calendar: CalendarSettings;
daily: PeriodNoteSettings;
weekly: PeriodNoteSettings;
monthly: PeriodNoteSettings;
quarterly: PeriodNoteSettings;
yearly: PeriodNoteSettings;
recentFiles: RecentFilesSettings;
}
export const DEFAULT_SETTINGS: WaypointSettings = {
calendar: {
firstDayOfWeek: 1, // Monday
showNoteIndicators: true,
},
daily: {
folder: 'periodic/daily',
templateFile: 'Templates/Daily note',
nameFormat: 'YYYY-MM-DD',
typeProperty: 'daily-note',
},
weekly: {
folder: 'periodic/weekly',
templateFile: 'Templates/Weekly note',
nameFormat: 'GGGG-[W]WW',
typeProperty: 'weekly-note',
},
monthly: {
folder: 'periodic/monthly',
templateFile: 'Templates/Monthly note',
nameFormat: 'YYYY-MM',
typeProperty: 'monthly-note',
},
quarterly: {
folder: 'periodic/quarterly',
templateFile: 'Templates/Quarterly note',
nameFormat: 'YYYY-[Q]Q',
typeProperty: 'quarterly-note',
},
yearly: {
folder: 'periodic/yearly',
templateFile: 'Templates/Yearly note',
nameFormat: 'YYYY',
typeProperty: 'yearly-note',
},
recentFiles: {
maxItems: 50,
updateOn: 'file-open',
omittedPaths: [],
omittedTags: [],
},
};
+149
View File
@@ -0,0 +1,149 @@
// ── Date utilities ──
// Uses moment.js which is bundled with Obsidian.
// Period format helpers following O-vault conventions.
import { moment } from 'obsidian';
export interface PeriodInfo {
day: string; // yyyy-MM-dd
week: string; // GGGG-[W]WW
month: string; // yyyy-MM
quarter: string; // yyyy-[Q]Q
year: string; // yyyy
}
export interface CalendarDay {
date: moment.Moment;
dayOfMonth: number;
isToday: boolean;
isCurrentMonth: boolean;
isoWeekNumber: number;
}
export interface CalendarWeek {
weekNumber: number;
days: CalendarDay[];
}
/**
* Get period info for a given date.
*/
export function getPeriodInfo(date: moment.Moment): PeriodInfo {
return {
day: date.format('YYYY-MM-DD'),
week: date.format('GGGG-[W]WW'),
month: date.format('YYYY-MM'),
quarter: date.format('YYYY-[Q]Q'),
year: date.format('YYYY'),
};
}
/**
* Get quarter string from a moment date (e.g. "2026-Q3").
*/
export function getQuarterString(date: moment.Moment): string {
return date.format('YYYY-[Q]Q');
}
/**
* Get the current date's period info.
*/
export function getTodayPeriod(): PeriodInfo {
return getPeriodInfo(moment());
}
/**
* Build a month grid as an array of weeks.
* Each week has 7 days, starting on the configured first day of week.
* ISO week numbers are calculated from the Monday of that week row.
*/
export function getMonthGrid(
year: number,
month: number, // 0-indexed (0 = January)
firstDayOfWeek: number, // 0 = Sunday, 1 = Monday
): CalendarWeek[] {
const firstOfMonth = moment({ year, month, day: 1 });
const lastOfMonth = moment(firstOfMonth).endOf('month');
// Find the first day to display (may go back into previous month)
const startDate = moment(firstOfMonth)
.subtract((firstOfMonth.day() - firstDayOfWeek + 7) % 7, 'days');
const today = moment().startOf('day');
const weeks: CalendarWeek[] = [];
let currentDay = moment(startDate);
while (currentDay.isBefore(lastOfMonth) || currentDay.month() === month) {
const weekDays: CalendarDay[] = [];
for (let i = 0; i < 7; i++) {
weekDays.push({
date: moment(currentDay),
dayOfMonth: currentDay.date(),
isToday: currentDay.isSame(today, 'day'),
isCurrentMonth: currentDay.month() === month,
isoWeekNumber: currentDay.isoWeek(),
});
currentDay.add(1, 'day');
}
weeks.push({
weekNumber: weekDays[0].isoWeekNumber,
days: weekDays,
});
// Safety: break after 6 weeks
if (weeks.length >= 6) break;
}
return weeks;
}
/**
* Format a date label for the breadcrumb (e.g. "June 1st, 2026").
*/
export function formatDateLabel(date: moment.Moment): string {
return date.format('MMMM Do, YYYY');
}
/**
* Format a month label (e.g. "June 2026").
*/
export function formatMonthLabel(date: moment.Moment): string {
return date.format('MMMM YYYY');
}
/**
* Get readable quarter label (e.g. "Q3 2026").
*/
export function formatQuarterLabel(date: moment.Moment): string {
return date.format('[Q]Q YYYY');
}
/**
* Navigate a date by a given period and amount.
*/
export function navigateDate(
date: moment.Moment,
period: 'day' | 'week' | 'month' | 'quarter' | 'year',
delta: number,
): moment.Moment {
if (period === 'quarter') {
return moment(date).add(delta * 3, 'months');
}
return moment(date).add(delta, period);
}
/**
* Create a note filename from a date using the given format string.
*/
export function formatPeriodName(date: moment.Moment, format: string): string {
return date.format(format);
}
/**
* Get ISO week number for a given date.
*/
export function getISOWeek(date: moment.Moment): number {
return date.isoWeek();
}
File diff suppressed because it is too large Load Diff
+294
View File
@@ -0,0 +1,294 @@
/* ── Waypoint Base ── */
.waypoint-view {
padding: 8px;
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
}
.waypoint-view .waypoint-section {
margin-bottom: 16px;
flex-shrink: 0;
}
.waypoint-view .waypoint-section:last-child {
margin-top: auto;
margin-bottom: 0;
}
.waypoint-view .waypoint-section-header {
font-size: var(--font-ui-small);
font-weight: var(--font-semibold);
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 4px;
padding-bottom: 4px;
border-bottom: 1px solid var(--background-modifier-border);
}
/* ── Calendar ── */
.waypoint-calendar {
font-size: var(--font-ui-small);
}
.waypoint-calendar-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.waypoint-calendar-breadcrumb {
display: flex;
align-items: center;
gap: 6px;
font-size: var(--font-ui-medium);
font-weight: var(--font-semibold);
padding: 2px 0;
}
.waypoint-calendar-breadcrumb .waypoint-clickable {
color: var(--text-muted);
cursor: var(--cursor);
padding: 1px 4px;
border-radius: 4px;
}
.waypoint-calendar-breadcrumb .waypoint-clickable:hover {
color: var(--text-accent);
background-color: var(--background-modifier-active-hover);
}
.waypoint-calendar .waypoint-separator {
color: var(--text-faint);
margin: 0 2px;
}
.waypoint-calendar-today-group {
display: flex;
align-items: center;
gap: 2px;
}
.waypoint-calendar-today-group button {
background: none;
border: none;
cursor: var(--cursor);
padding: 2px 6px;
border-radius: 4px;
color: var(--text-muted);
font-size: var(--font-ui-small);
}
.waypoint-calendar-today-group button:hover {
color: var(--text-accent);
background-color: var(--background-modifier-active-hover);
}
.waypoint-calendar-today-group .waypoint-calendar-today-btn {
font-weight: var(--font-medium);
}
.waypoint-calendar table {
table-layout: fixed;
width: 100%;
border-collapse: collapse;
}
.waypoint-calendar th,
.waypoint-calendar td {
text-align: center;
padding: 2px 1px;
font-size: var(--font-ui-small);
}
.waypoint-calendar th {
font-weight: var(--font-medium);
color: var(--text-faint);
font-size: calc(var(--font-ui-small) * 0.85);
}
.waypoint-calendar .waypoint-weeknum {
font-size: calc(var(--font-ui-small) * 0.75);
color: var(--text-faint);
font-weight: var(--font-light);
cursor: var(--cursor);
padding: 2px 0;
border-radius: 4px;
}
.waypoint-calendar .waypoint-weeknum:hover {
color: var(--text-accent);
background-color: var(--background-modifier-active-hover);
}
.waypoint-calendar .waypoint-day {
cursor: var(--cursor);
padding: 3px 0;
border-radius: 6px;
position: relative;
width: 100%;
}
.waypoint-calendar .waypoint-day:hover {
background-color: var(--background-modifier-active-hover);
}
.waypoint-calendar .waypoint-day.other-month {
opacity: 0.35;
}
.waypoint-calendar .waypoint-day.today {
color: var(--text-accent);
border: 1px solid var(--text-accent);
font-weight: var(--font-semibold);
}
.waypoint-calendar .waypoint-day.has-note::after {
content: '';
display: block;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: var(--text-faint);
margin: 0 auto;
}
.waypoint-calendar .waypoint-day.today.has-note::after {
background-color: var(--text-accent);
}
/* ── Recent Files ── */
.waypoint-recent-files .nav-file {
padding: 2px 0;
}
.waypoint-recent-files .nav-file-title {
padding: 2px 28px 2px 4px;
font-size: var(--font-ui-small);
position: relative;
}
.waypoint-recent-files .waypoint-recent-remove {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
opacity: 0;
cursor: var(--cursor);
color: var(--text-faint);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
transition: opacity 100ms;
}
.waypoint-recent-files .nav-file-title:hover .waypoint-recent-remove {
opacity: 1;
pointer-events: auto;
}
.waypoint-recent-files .waypoint-recent-remove:hover {
color: var(--text-accent);
}
/* ── Favorites ── */
.waypoint-favorites .waypoint-bookmark-item {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 8px 2px 4px;
font-size: var(--font-ui-small);
cursor: var(--cursor);
border-radius: 4px;
user-select: none;
position: relative;
}
.waypoint-favorites .waypoint-bookmark-item:hover {
background-color: var(--background-modifier-active-hover);
}
.waypoint-favorites .waypoint-bookmark-item .waypoint-bm-icon {
display: flex;
align-items: center;
flex-shrink: 0;
}
.waypoint-favorites .waypoint-bookmark-item .waypoint-bm-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.waypoint-favorites .waypoint-bookmark-group {
font-weight: var(--font-medium);
}
.waypoint-favorites .waypoint-bookmark-group .waypoint-bm-chevron {
display: flex;
align-items: center;
transition: transform 150ms;
}
.waypoint-favorites .waypoint-bookmark-group.collapsed .waypoint-bm-chevron {
transform: rotate(-90deg);
}
.waypoint-favorites .waypoint-bookmark-children {
padding-left: 16px;
}
.waypoint-favorites .waypoint-bookmark-children.collapsed {
display: none;
}
/* ── Drag-and-drop ── */
.waypoint-bm-dragging {
opacity: 0.3;
}
.waypoint-favorites .waypoint-bm-drop-line {
border-top: 3px solid var(--text-accent) !important;
}
.waypoint-favorites .waypoint-bm-drop-line.waypoint-bm-drop-below {
border-top: none !important;
border-bottom: 3px solid var(--text-accent) !important;
}
/* ── Separator & Spacer ── */
.waypoint-favorites .waypoint-bookmark-item.waypoint-bookmark-separator {
padding: 0;
min-height: auto;
height: auto;
border: none;
cursor: grab;
}
.waypoint-favorites .waypoint-bookmark-item.waypoint-bookmark-separator::after {
content: '';
display: block;
height: 1px;
margin: 6px 8px;
background: var(--background-modifier-border);
pointer-events: none;
}
.waypoint-favorites .waypoint-bookmark-item.waypoint-bookmark-spacer {
padding: 0;
min-height: auto;
height: 14px;
border: none;
cursor: grab;
background: transparent;
}
+5
View File
@@ -0,0 +1,5 @@
- [ ] when user middle clicks on a bookmark, it should open in a new tab
- [ ] the sidebar should never scroll. the content of recent files should wrap after 10 files or so (no sidebar visible)
- [ ] it should be possible to add a separator line and/or an empty space
- [ ] by default, the bookmark should have no icon
- [ ]
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": false,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": false,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
],
"paths": {
"src/*": ["./src/*"]
}
},
"include": [
"**/*.ts"
]
}