Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12209155dc | |||
| e027ac4a9e | |||
| 26a1a481ac |
+125
-20
@@ -9,12 +9,13 @@
|
|||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── main.ts Plugin entry — lifecycle, commands, events, data persistence
|
├── main.ts Plugin entry — lifecycle, commands, events, data persistence
|
||||||
├── settings.ts Interface definitions + DEFAULT_SETTINGS constant
|
├── settings.ts Interface definitions + DEFAULT_SETTINGS / DEFAULT_DATE_SYSTEM constants
|
||||||
├── settings-tab.ts PluginSettingTab UI — tabs: calendar, periodic, recent, display, about
|
├── settings-tab.ts PluginSettingTab UI — calendar, periodic, date systems, recent, display, about
|
||||||
├── models/
|
├── models/
|
||||||
│ └── bookmark.ts BookmarkItem + WaypointData interfaces
|
│ └── bookmark.ts BookmarkItem + WaypointData interfaces
|
||||||
├── utils/
|
├── utils/
|
||||||
│ ├── date-utils.ts Moment.js helpers: period formatting, month grid, navigation
|
│ ├── 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)
|
│ └── path-utils.ts Pure path helper: rename remapping (no `obsidian` import, unit-testable)
|
||||||
└── views/
|
└── views/
|
||||||
└── waypoint-view.ts ItemView subclass — full sidebar rendering + interaction
|
└── waypoint-view.ts ItemView subclass — full sidebar rendering + interaction
|
||||||
@@ -30,7 +31,7 @@ src/
|
|||||||
|
|
||||||
### `onload()`
|
### `onload()`
|
||||||
|
|
||||||
1. **Load data** — `loadSettings()` then `loadWaypointData()`, both merge saved partials over defaults via `Object.assign({}, DEFAULT, partial)`.
|
1. **Load data** — reads `data.json` exactly once, then `applySettings(saved)` and `applyWaypointData(saved)` merge its partial data over defaults.
|
||||||
2. **Register view** — `WAYPOINT_VIEW_TYPE = "waypoint-view"` maps to `WaypointView` factory `(leaf) => new WaypointView(leaf, this)`.
|
2. **Register view** — `WAYPOINT_VIEW_TYPE = "waypoint-view"` maps to `WaypointView` factory `(leaf) => new WaypointView(leaf, this)`.
|
||||||
3. **Settings tab** — `WaypointSettingTab` receives the live `settings` object + `() => this.redrawAll()` callback for live preview.
|
3. **Settings tab** — `WaypointSettingTab` receives the live `settings` object + `() => this.redrawAll()` callback for live preview.
|
||||||
4. **Register commands** (see Commands section below).
|
4. **Register commands** (see Commands section below).
|
||||||
@@ -56,6 +57,7 @@ interface WaypointSettings {
|
|||||||
monthly: PeriodNoteSettings;
|
monthly: PeriodNoteSettings;
|
||||||
quarterly: PeriodNoteSettings;
|
quarterly: PeriodNoteSettings;
|
||||||
yearly: PeriodNoteSettings;
|
yearly: PeriodNoteSettings;
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -90,6 +92,8 @@ interface DisplaySettings {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`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)
|
### BookmarkItem (recursive tree)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -147,6 +151,107 @@ These call `navigatePeriodNote(direction)` which:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `journal` | Journal | `periodic/journal` | `YYYY-MM-DD - [Journal]` | one |
|
||||||
|
| `meetings` | Meeting | `periodic/meetings` | `YYYY-MM-DD - {title}` | many |
|
||||||
|
|
||||||
|
### 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
|
||||||
|
/** One existing note of a date system, with the label the menu should show. */
|
||||||
|
interface DateSystemNote {
|
||||||
|
file: TFile;
|
||||||
|
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 {
|
||||||
|
system: DateSystemSettings;
|
||||||
|
notes: DateSystemNote[]; // sorted by basename; empty when the date has no note
|
||||||
|
multiple: boolean; // nameFormat carries {title}, i.e. many notes per date
|
||||||
|
}
|
||||||
|
|
||||||
|
dateSystems(): DateSystemSettings[]
|
||||||
|
findDateSystemNotes(date: moment.Moment): DateSystemNotes[]
|
||||||
|
openDateSystemNote(
|
||||||
|
system: DateSystemSettings,
|
||||||
|
date: moment.Moment,
|
||||||
|
opts?: { title?: string; leaf?: WorkspaceLeaf },
|
||||||
|
): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
**`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:
|
||||||
|
|
||||||
|
1. **Filename** — `splitNameFormat(system.nameFormat)`, format `before` and `after` against `date`, and for a many-per-date system join `sanitizeTitle(opts.title)` between them. Append `.md`; prepend `system.folder` when it is set.
|
||||||
|
2. **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.
|
||||||
|
3. **Missing note** — created via `createDatedNote`, then opened.
|
||||||
|
4. **Leaf** — `opts.leaf` when supplied (middle-click and Ctrl/Cmd-click pass an explicit tab leaf), otherwise `workspace.getLeaf(false)`.
|
||||||
|
|
||||||
|
### Note creation (`createDatedNote`)
|
||||||
|
|
||||||
|
A single creation path shared by periodic notes and date systems; it replaced the period-only `createPeriodNote`.
|
||||||
|
|
||||||
|
1. With a non-empty `system.templateFile`, reads it (appending `.md` when omitted) and creates the note with its raw contents. This is for plain, non-Templater templates.
|
||||||
|
2. 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`)
|
## Period Note Creation (`openPeriodNote`)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -157,16 +262,9 @@ openPeriodNote(
|
|||||||
): Promise<void>
|
): Promise<void>
|
||||||
```
|
```
|
||||||
|
|
||||||
Single unified opener — there is no separate `openPeriodNoteInLeaf`. For a given period + moment date:
|
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 })`.
|
||||||
|
|
||||||
1. **Build filename:** `date.format(periodSettings.nameFormat) + ".md"`.
|
Filename building, the existence check, template-or-frontmatter creation and leaf selection are therefore documented once, under Date Systems above.
|
||||||
2. **Build full path:** If `periodSettings.folder` is set, prepend it; otherwise root.
|
|
||||||
3. **Check existence:** `vault.getFileByPath(fullPath)`.
|
|
||||||
4. **If not found, create:**
|
|
||||||
- Try to read template at `periodSettings.templateFile + ".md"`.
|
|
||||||
- If template exists → `vault.create(fullPath, templateContent)`.
|
|
||||||
- If no template → `vault.create(fullPath, minimalFrontmatter)` where frontmatter is `type: {typeProperty}` + `date: YYYY-MM-DD`.
|
|
||||||
5. **Open:** in `leaf` when one is supplied, otherwise in `workspace.getLeaf(false)`.
|
|
||||||
|
|
||||||
Middle-click handlers in the view pass an explicit tab leaf: `openPeriodNote(period, date, this.app.workspace.getLeaf('tab'))`.
|
Middle-click handlers in the view pass an explicit tab leaf: `openPeriodNote(period, date, this.app.workspace.getLeaf('tab'))`.
|
||||||
|
|
||||||
@@ -202,7 +300,7 @@ Both update the markdown-basename set used for calendar note indicators, then tr
|
|||||||
recentFiles: { path: string; basename: string }[]
|
recentFiles: { path: string; basename: string }[]
|
||||||
```
|
```
|
||||||
|
|
||||||
Backed by `waypointData.recentFiles` in `data.json`, so the list survives vault reload. `loadWaypointData()` restores it and re-applies the current `maxItems` limit (in case the setting shrank since the last save).
|
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:**
|
**Update flow:**
|
||||||
1. `addToRecentFiles(file)` — omission check, then dedupes (removes existing entry), prepends to front, truncates to `maxItems`.
|
1. `addToRecentFiles(file)` — omission check, then dedupes (removes existing entry), prepends to front, truncates to `maxItems`.
|
||||||
@@ -250,9 +348,16 @@ 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:** Click opens daily note, middle-click opens it in a new tab. `.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` 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).
|
||||||
- **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:
|
||||||
|
|
||||||
|
- Each existing note, labelled with its `DateSystemNote.label` and the system's `icon`. Click opens it in the current leaf; Ctrl/Cmd-click opens it in a new tab.
|
||||||
|
- A create entry (icon `plus`) labelled `New {lowercased system name} note`. Many-per-date systems always show one, with a trailing ellipsis because it first opens a `PromptModal` for 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
|
### 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`).
|
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`).
|
||||||
@@ -292,7 +397,7 @@ Renders `plugin.recentFiles` as Obsidian-native `nav-file` elements using the fi
|
|||||||
|
|
||||||
**Context menu (right-click):**
|
**Context menu (right-click):**
|
||||||
- File items: "Open in new tab"
|
- File items: "Open in new tab"
|
||||||
- File/Group items: "Rename" (opens `RenameModal`), "Change icon" (opens `IconSuggestModal`)
|
- File/Group items: "Rename" (opens `PromptModal`), "Change icon" (opens `IconSuggestModal`)
|
||||||
- "Move to group" submenu: Lists all available groups (excluding self + descendants) + "(Root)" for ungrouping
|
- "Move to group" submenu: Lists all available groups (excluding self + descendants) + "(Root)" for ungrouping
|
||||||
- Group items: "Expand/Collapse", "Add bookmark here", "New sub-group"
|
- Group items: "Expand/Collapse", "Add bookmark here", "New sub-group"
|
||||||
- All items: "Insert separator above/below", "Insert spacer above/below", "Remove"
|
- All items: "Insert separator above/below", "Insert spacer above/below", "Remove"
|
||||||
@@ -302,13 +407,13 @@ Renders `plugin.recentFiles` as Obsidian-native `nav-file` elements using the fi
|
|||||||
2. If targetGroupId: find group, set `item.indent = group.indent + 1`, push to group's children.
|
2. If targetGroupId: find group, set `item.indent = group.indent + 1`, push to group's children.
|
||||||
3. If null (root): set `item.indent = 0`, push to `waypointData.bookmarks`.
|
3. If null (root): set `item.indent = 0`, push to `waypointData.bookmarks`.
|
||||||
|
|
||||||
**Rename modal (`RenameModal`):** Simple Modal with text input + Cancel/Save buttons. Enter key submits.
|
**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:
|
**Icon picker (`IconSuggestModal`):** Modal with:
|
||||||
- Live preview of selected icon.
|
- Live preview of selected icon.
|
||||||
- Search input with 60ms debounce.
|
- 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.json` with fallback to `https://lucide.dev/api/tags` and a hardcoded `FALLBACK_ICONS` object (~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.
|
- Grid of matching icons (max 80 shown), loaded from `https://cdn.jsdelivr.net/npm/lucide-static@0.517.0/tags.json` with fallback to `https://lucide.dev/api/tags` and a hardcoded `FALLBACK_ICONS` object (~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" link to clear, Save/Cancel buttons.
|
- Click to select, **No icon** button to clear, Save/Cancel buttons.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -317,10 +422,10 @@ Renders `plugin.recentFiles` as Obsidian-native `nav-file` elements using the fi
|
|||||||
All classes prefixed with `waypoint-`. Uses Obsidian CSS variables throughout:
|
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-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-primary`, `--background-secondary`
|
||||||
- `--interactive-accent`
|
- `--interactive-accent`
|
||||||
- `--cursor` (for custom cursor support)
|
- `--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.
|
||||||
@@ -372,7 +477,7 @@ Settings and waypoint data share a single `data.json` via Obsidian's `Plugin.loa
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Both `loadSettings()` and `loadWaypointData()` read from the same file, merging partials over defaults. `saveSettings()` and `saveWaypointData()` each re-read the full data, update their key, and write back — so they are serialized through a single-writer save queue: each call chains onto the previous one's promise instead of racing it. Recent-file writes additionally go through the 300ms debounce in `persistRecentFiles()`.
|
`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()`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,24 @@
|
|||||||
- [ ] 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: Date systems
|
||||||
|
|
||||||
|
- [ ] Right-click a date with a daily note, journal note, and meeting note → full-date header, then all three systems appear in the menu
|
||||||
|
- [ ] Right-click a date with no date-system notes → each single-note system offers a create action and the many-note system offers “New …”
|
||||||
|
- [ ] Right-click a date with one meeting → the existing meeting and “New meeting note…” both appear
|
||||||
|
- [ ] Create a second meeting for the same date → it is created with that date prefix and both meetings appear on the next right-click
|
||||||
|
- [ ] Ctrl/Cmd-click an existing date-system menu entry → it opens in a new tab
|
||||||
|
- [ ] Submit an empty title for a new many-note system → a clear notice appears and no malformed file is created
|
||||||
|
- [ ] Add a date system in Settings → Date systems → it appears in the day menu
|
||||||
|
- [ ] Reorder and delete date systems → the day menu immediately follows the configured order and removes the deleted system
|
||||||
|
- [ ] Enter a non-empty name format with no date token → Settings shows the inline warning
|
||||||
|
- [ ] Left-click a day still opens/creates its daily note; middle-click still opens it in a new tab
|
||||||
|
|
||||||
|
- [ ] With Templater folder mappings for journal and meetings, create a date-system note → Templater expands `<% … %>` syntax rather than leaving it literal
|
||||||
|
- [ ] Create a journal from the calendar → no duplicate date prompt and no rename; its frontmatter `date` equals the selected calendar day
|
||||||
|
- [ ] Create a meeting from the calendar for a non-today date → its frontmatter `date` equals the selected calendar day
|
||||||
|
|
||||||
## Recent Files (Regression)
|
## Recent Files (Regression)
|
||||||
|
|
||||||
- [ ] Opening a file adds it to recent files
|
- [ ] Opening a file adds it to recent files
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ Calendar, recent files, and custom bookmarks sidebar for Obsidian.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Calendar panel** — month grid with clickable days, week numbers, period indicators (day/week/month/quarter/year)
|
- **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
|
||||||
|
- **Date systems** — browse or create daily, journal, meeting, and other date-prefixed notes from a single day menu
|
||||||
- **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
|
||||||
|
|
||||||
@@ -19,6 +20,27 @@ Calendar, recent files, and custom bookmarks sidebar for Obsidian.
|
|||||||
| Go to yearly note | `Ctrl+Shift+Alt+Y` |
|
| Go to yearly note | `Ctrl+Shift+Alt+Y` |
|
||||||
| Next/Previous daily/weekly/monthly/quarterly/yearly note | — |
|
| Next/Previous daily/weekly/monthly/quarterly/yearly note | — |
|
||||||
|
|
||||||
|
## Date systems
|
||||||
|
|
||||||
|
Right-click any calendar day to open its **date systems** menu. The daily note always appears first; left-click behaviour is unchanged. The included defaults match this vault's common layouts:
|
||||||
|
|
||||||
|
| System | Folder | Name format | Behaviour |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
Configure systems in **Settings → Waypoint Sidebar → Date systems**. Each system has a folder, filename format, template, fallback frontmatter type, and Lucide icon.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
The selected calendar date is encoded in the filename. Templater templates should derive their `date` from `tp.file.title`, not from `tp.date` or `tp.file.creation_date()`. A journal template can retain a prompt-and-rename fallback for manual note creation, but it must skip that flow when the filename already follows `YYYY-MM-DD - Journal`.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Via BRAT
|
### Via BRAT
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "waypoint-sidebar",
|
"id": "waypoint-sidebar",
|
||||||
"name": "Waypoint Sidebar",
|
"name": "Waypoint Sidebar",
|
||||||
"version": "1.5.2",
|
"version": "1.6.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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "waypoint",
|
"name": "waypoint",
|
||||||
"version": "1.5.2",
|
"version": "1.6.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": {
|
||||||
|
|||||||
+162
-24
@@ -10,14 +10,38 @@ import {
|
|||||||
getAllTags,
|
getAllTags,
|
||||||
moment,
|
moment,
|
||||||
} from 'obsidian';
|
} from 'obsidian';
|
||||||
import { WaypointSettings, DEFAULT_SETTINGS, PeriodNoteSettings } from 'src/settings';
|
import { WaypointSettings, DEFAULT_SETTINGS, DateSystemSettings, DEFAULT_DATE_SYSTEM } from 'src/settings';
|
||||||
import { WaypointSettingTab } from 'src/settings-tab';
|
import { WaypointSettingTab } from 'src/settings-tab';
|
||||||
import { WaypointView, WAYPOINT_VIEW_TYPE } from 'src/views/waypoint-view';
|
import { WaypointView, WAYPOINT_VIEW_TYPE } from 'src/views/waypoint-view';
|
||||||
import { BookmarkItem, WaypointData } from 'src/models/bookmark';
|
import { BookmarkItem, WaypointData } from 'src/models/bookmark';
|
||||||
import { remapRenamedPath } from 'src/utils/path-utils';
|
import { remapRenamedPath } from 'src/utils/path-utils';
|
||||||
|
import {
|
||||||
|
splitNameFormat,
|
||||||
|
isInFolder,
|
||||||
|
matchesSystemName,
|
||||||
|
titleFromBasename,
|
||||||
|
sanitizeTitle,
|
||||||
|
formatHasDateToken,
|
||||||
|
} from 'src/utils/date-systems';
|
||||||
|
|
||||||
export type PeriodKey = 'day' | 'week' | 'month' | 'quarter' | 'year';
|
export type PeriodKey = 'day' | 'week' | 'month' | 'quarter' | 'year';
|
||||||
|
|
||||||
|
/** One existing note of a date system, with the label the menu should show. */
|
||||||
|
export interface DateSystemNote {
|
||||||
|
file: TFile;
|
||||||
|
/** Free-text title for many-per-date systems, else the system name. */
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A day-scoped system and the notes it already holds for one date. */
|
||||||
|
export interface DateSystemNotes {
|
||||||
|
system: DateSystemSettings;
|
||||||
|
/** Sorted by basename. Empty when the date has no note in this system. */
|
||||||
|
notes: DateSystemNote[];
|
||||||
|
/** True when nameFormat carries {title}, i.e. many notes per date. */
|
||||||
|
multiple: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default class WaypointPlugin extends Plugin {
|
export default class WaypointPlugin extends Plugin {
|
||||||
public settings: WaypointSettings;
|
public settings: WaypointSettings;
|
||||||
public waypointData: WaypointData;
|
public waypointData: WaypointData;
|
||||||
@@ -221,6 +245,13 @@ export default class WaypointPlugin extends Plugin {
|
|||||||
for (const key of PERIOD_SETTING_KEYS) {
|
for (const key of PERIOD_SETTING_KEYS) {
|
||||||
this.settings[key] = Object.assign({}, DEFAULT_SETTINGS[key], s[key] || {});
|
this.settings[key] = Object.assign({}, DEFAULT_SETTINGS[key], s[key] || {});
|
||||||
}
|
}
|
||||||
|
// Cloned, not Object.assign'd in: the defaults array would otherwise be
|
||||||
|
// 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.
|
||||||
|
this.settings.dateSystems = Array.isArray(s.dateSystems)
|
||||||
|
? s.dateSystems.map(sys => Object.assign({}, DEFAULT_DATE_SYSTEM, sys))
|
||||||
|
: DEFAULT_SETTINGS.dateSystems.map(sys => Object.assign({}, sys));
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyWaypointData(saved: Record<string, unknown> | null): void {
|
private applyWaypointData(saved: Record<string, unknown> | null): void {
|
||||||
@@ -510,44 +541,142 @@ export default class WaypointPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Period note creation/opening ──
|
// ── Date systems: discovery, creation, opening ──
|
||||||
|
|
||||||
|
/** Periodic notes are date systems with a fixed one-note-per-period format. */
|
||||||
|
private periodAsDateSystem(period: PeriodKey): DateSystemSettings {
|
||||||
|
const config = PERIOD_CONFIGS[period];
|
||||||
|
const periodSettings = this.settings[config.key];
|
||||||
|
return {
|
||||||
|
id: config.key,
|
||||||
|
name: config.label,
|
||||||
|
folder: periodSettings.folder,
|
||||||
|
nameFormat: periodSettings.nameFormat,
|
||||||
|
templateFile: periodSettings.templateFile,
|
||||||
|
typeProperty: periodSettings.typeProperty,
|
||||||
|
icon: 'calendar',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All day-scoped systems: the daily periodic note first, then settings.dateSystems. */
|
||||||
|
dateSystems(): DateSystemSettings[] {
|
||||||
|
return [this.periodAsDateSystem('day'), ...this.settings.dateSystems];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bucket the notes every day-scoped system already holds for `date`.
|
||||||
|
*
|
||||||
|
* One vault scan serves all systems at once. That is affordable because
|
||||||
|
* this runs once per right-click, not once per calendar cell, and it keeps
|
||||||
|
* arbitrary user-defined name formats out of the incremental index.
|
||||||
|
*/
|
||||||
|
findDateSystemNotes(date: moment.Moment): DateSystemNotes[] {
|
||||||
|
const buckets = this.dateSystems().map(system => {
|
||||||
|
const parts = splitNameFormat(system.nameFormat);
|
||||||
|
return {
|
||||||
|
result: { system, notes: [] as DateSystemNote[], multiple: parts.hasTitle },
|
||||||
|
// Each half is formatted on its own: the raw format may still
|
||||||
|
// hold {title}, whose letters are live moment tokens.
|
||||||
|
before: formatDatePart(date, parts.before),
|
||||||
|
after: formatDatePart(date, parts.after),
|
||||||
|
hasTitle: parts.hasTitle,
|
||||||
|
// Empty/date-less formats have no date-specific filename. They stay
|
||||||
|
// editable in Settings but must not contribute a menu action.
|
||||||
|
skip: !formatHasDateToken(system.nameFormat),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const file of this.app.vault.getMarkdownFiles()) {
|
||||||
|
for (const bucket of buckets) {
|
||||||
|
if (bucket.skip) continue;
|
||||||
|
const system = bucket.result.system;
|
||||||
|
if (!isInFolder(file.path, system.folder)) continue;
|
||||||
|
if (!matchesSystemName(file.basename, bucket.before, bucket.after, bucket.hasTitle)) continue;
|
||||||
|
bucket.result.notes.push({
|
||||||
|
file,
|
||||||
|
label: bucket.hasTitle
|
||||||
|
? titleFromBasename(file.basename, bucket.before, bucket.after)
|
||||||
|
: system.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const bucket of buckets) {
|
||||||
|
bucket.result.notes.sort((a, b) => a.file.basename.localeCompare(b.file.basename));
|
||||||
|
}
|
||||||
|
return buckets
|
||||||
|
.filter(bucket => !bucket.skip)
|
||||||
|
.map(bucket => bucket.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open (creating if needed) the note `system` holds for `date`.
|
||||||
|
*
|
||||||
|
* `opts.title` fills the `{title}` token of a many-per-date system and is
|
||||||
|
* ignored by systems without one. Opens in `opts.leaf` when given,
|
||||||
|
* otherwise in the active leaf.
|
||||||
|
*/
|
||||||
|
async openDateSystemNote(
|
||||||
|
system: DateSystemSettings,
|
||||||
|
date: moment.Moment,
|
||||||
|
opts?: { title?: string; leaf?: WorkspaceLeaf },
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
if (!formatHasDateToken(system.nameFormat)) {
|
||||||
|
new Notice(`Waypoint: ${system.name || 'This'} name format needs a date placeholder.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parts = splitNameFormat(system.nameFormat);
|
||||||
|
const before = formatDatePart(date, parts.before);
|
||||||
|
const after = formatDatePart(date, parts.after);
|
||||||
|
|
||||||
|
let basename: string;
|
||||||
|
if (parts.hasTitle) {
|
||||||
|
const title = sanitizeTitle(opts?.title || '');
|
||||||
|
// Public method, so a caller can legitimately hand us nothing;
|
||||||
|
// creating "2026-09-07 - .md" would be worse than refusing.
|
||||||
|
if (!title) {
|
||||||
|
new Notice(`Waypoint: a ${system.name.toLowerCase()} note needs a title.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
basename = before + title + after;
|
||||||
|
} else {
|
||||||
|
basename = before + after;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = system.folder ? `${system.folder}/${basename}.md` : `${basename}.md`;
|
||||||
|
|
||||||
|
let file = this.app.vault.getFileByPath(fullPath);
|
||||||
|
if (!file) {
|
||||||
|
file = await this.createDatedNote(fullPath, system, date);
|
||||||
|
if (!file) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = opts?.leaf || this.app.workspace.getLeaf(false);
|
||||||
|
await target.openFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open (creating if needed) the period note for `date`.
|
* Open (creating if needed) the period note for `date`.
|
||||||
* Opens in `leaf` when given, otherwise in the active leaf.
|
* Opens in `leaf` when given, otherwise in the active leaf.
|
||||||
*/
|
*/
|
||||||
async openPeriodNote(period: PeriodKey, date: moment.Moment, leaf?: WorkspaceLeaf): Promise<void> {
|
async openPeriodNote(period: PeriodKey, date: moment.Moment, leaf?: WorkspaceLeaf): Promise<void> {
|
||||||
const config = PERIOD_CONFIGS[period];
|
await this.openDateSystemNote(this.periodAsDateSystem(period), date, { leaf });
|
||||||
const periodSettings = this.settings[config.key];
|
|
||||||
const filename = date.format(periodSettings.nameFormat) + '.md';
|
|
||||||
const fullPath = periodSettings.folder
|
|
||||||
? `${periodSettings.folder}/${filename}`
|
|
||||||
: filename;
|
|
||||||
|
|
||||||
let file = this.app.vault.getFileByPath(fullPath);
|
|
||||||
if (!file) {
|
|
||||||
file = await this.createPeriodNote(fullPath, periodSettings, date, config.label);
|
|
||||||
if (!file) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = leaf || this.app.workspace.getLeaf(false);
|
|
||||||
await target.openFile(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a period note from its template, or from minimal frontmatter.
|
* Create a date system's note from its template, or from minimal frontmatter.
|
||||||
*
|
*
|
||||||
* Every failure path names the offending path and the reason: the usual
|
* Every failure path names the offending path and the reason: the usual
|
||||||
* cause is a configured folder that does not exist yet, which `vault.create`
|
* cause is a configured folder that does not exist yet, which `vault.create`
|
||||||
* refuses outright rather than creating.
|
* refuses outright rather than creating.
|
||||||
*/
|
*/
|
||||||
private async createPeriodNote(
|
private async createDatedNote(
|
||||||
fullPath: string,
|
fullPath: string,
|
||||||
periodSettings: PeriodNoteSettings,
|
system: DateSystemSettings,
|
||||||
date: moment.Moment,
|
date: moment.Moment,
|
||||||
label: string,
|
|
||||||
): Promise<TFile | null> {
|
): Promise<TFile | null> {
|
||||||
const noun = label.toLowerCase();
|
const noun = system.name.toLowerCase();
|
||||||
const slash = fullPath.lastIndexOf('/');
|
const slash = fullPath.lastIndexOf('/');
|
||||||
const folder = slash < 0 ? '' : fullPath.slice(0, slash);
|
const folder = slash < 0 ? '' : fullPath.slice(0, slash);
|
||||||
|
|
||||||
@@ -563,7 +692,7 @@ export default class WaypointPlugin extends Plugin {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const configuredTemplate = periodSettings.templateFile;
|
const configuredTemplate = system.templateFile;
|
||||||
const templateFile = this.resolveTemplateFile(configuredTemplate);
|
const templateFile = this.resolveTemplateFile(configuredTemplate);
|
||||||
|
|
||||||
let content: string;
|
let content: string;
|
||||||
@@ -579,7 +708,7 @@ export default class WaypointPlugin extends Plugin {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
content = `---\ntype: ${periodSettings.typeProperty}\ndate: ${date.format('YYYY-MM-DD')}\n---\n\n`;
|
content = `---\ntype: ${system.typeProperty}\ndate: ${date.format('YYYY-MM-DD')}\n---\n\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let file: TFile;
|
let file: TFile;
|
||||||
@@ -733,3 +862,12 @@ const DIAGNOSTIC_NOTICE_MS = 10000;
|
|||||||
function describeError(err: unknown): string {
|
function describeError(err: unknown): string {
|
||||||
return err instanceof Error ? err.message : String(err);
|
return err instanceof Error ? err.message : String(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format one half of a split name format. Moment falls back to its default
|
||||||
|
* ISO output when handed an empty format string, so an empty half must never
|
||||||
|
* reach it.
|
||||||
|
*/
|
||||||
|
function formatDatePart(date: moment.Moment, part: string): string {
|
||||||
|
return part ? date.format(part) : '';
|
||||||
|
}
|
||||||
|
|||||||
+139
-2
@@ -1,12 +1,13 @@
|
|||||||
import { Setting, PluginSettingTab, App, setIcon } from 'obsidian';
|
import { Setting, PluginSettingTab, App, setIcon } from 'obsidian';
|
||||||
import type WaypointPlugin from 'src/main';
|
import type WaypointPlugin from 'src/main';
|
||||||
import { WaypointSettings, DEFAULT_SETTINGS, PeriodNoteSettings } from 'src/settings';
|
import { WaypointSettings, DEFAULT_SETTINGS, DEFAULT_DATE_SYSTEM, PeriodNoteSettings } from 'src/settings';
|
||||||
|
import { formatHasDateToken } from 'src/utils/date-systems';
|
||||||
|
|
||||||
export class WaypointSettingTab extends PluginSettingTab {
|
export class WaypointSettingTab extends PluginSettingTab {
|
||||||
private plugin: WaypointPlugin;
|
private plugin: WaypointPlugin;
|
||||||
private settings: WaypointSettings;
|
private settings: WaypointSettings;
|
||||||
private onSettingsChange: () => void;
|
private onSettingsChange: () => void;
|
||||||
private activeTab: 'calendar' | 'periodic' | 'recent' | 'display' | 'about' = 'calendar';
|
private activeTab: 'calendar' | 'periodic' | 'systems' | 'recent' | 'display' | 'about' = 'calendar';
|
||||||
|
|
||||||
constructor(app: App, plugin: WaypointPlugin, settings: WaypointSettings, onSettingsChange: () => void) {
|
constructor(app: App, plugin: WaypointPlugin, settings: WaypointSettings, onSettingsChange: () => void) {
|
||||||
super(app, plugin);
|
super(app, plugin);
|
||||||
@@ -24,6 +25,7 @@ export class WaypointSettingTab extends PluginSettingTab {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: 'calendar' as const, label: 'Calendar' },
|
{ key: 'calendar' as const, label: 'Calendar' },
|
||||||
{ key: 'periodic' as const, label: 'Periodic Notes' },
|
{ key: 'periodic' as const, label: 'Periodic Notes' },
|
||||||
|
{ key: 'systems' as const, label: 'Date systems' },
|
||||||
{ key: 'recent' as const, label: 'Recent Files' },
|
{ key: 'recent' as const, label: 'Recent Files' },
|
||||||
{ key: 'display' as const, label: 'Display' },
|
{ key: 'display' as const, label: 'Display' },
|
||||||
{ key: 'about' as const, label: 'About' },
|
{ key: 'about' as const, label: 'About' },
|
||||||
@@ -49,6 +51,9 @@ export class WaypointSettingTab extends PluginSettingTab {
|
|||||||
case 'periodic':
|
case 'periodic':
|
||||||
this.renderPeriodicTab(tabContent);
|
this.renderPeriodicTab(tabContent);
|
||||||
break;
|
break;
|
||||||
|
case 'systems':
|
||||||
|
this.renderSystemsTab(tabContent);
|
||||||
|
break;
|
||||||
case 'recent':
|
case 'recent':
|
||||||
this.renderRecentTab(tabContent);
|
this.renderRecentTab(tabContent);
|
||||||
break;
|
break;
|
||||||
@@ -157,6 +162,138 @@ export class WaypointSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════
|
||||||
|
// Date systems tab
|
||||||
|
// ═══════════════════════════════
|
||||||
|
|
||||||
|
private renderSystemsTab(container: HTMLElement): void {
|
||||||
|
const intro = new DocumentFragment();
|
||||||
|
intro.createDiv({ text: 'A date system is a folder of notes whose filenames start with a date. Each one appears in the calendar\'s right-click menu for that day.' });
|
||||||
|
intro.createDiv({ text: 'The daily note is configured under Periodic Notes and always comes first in that menu.' });
|
||||||
|
intro.createDiv({ text: 'Name format is a moment.js format. Literal words need bracket escaping, e.g. YYYY-MM-DD - [Journal].' });
|
||||||
|
intro.createDiv({ text: 'Include {title} for systems that hold many notes per date, such as meetings: the text before the token finds the existing notes, and the token marks where a typed title goes. Without it, a date has exactly one note.' });
|
||||||
|
|
||||||
|
new Setting(container)
|
||||||
|
.setHeading()
|
||||||
|
.setName('Date systems')
|
||||||
|
.setDesc(intro);
|
||||||
|
|
||||||
|
this.settings.dateSystems.forEach((system, i, arr) => {
|
||||||
|
new Setting(container)
|
||||||
|
.setHeading()
|
||||||
|
.setName(system.name || 'Untitled system')
|
||||||
|
.addExtraButton((btn) => {
|
||||||
|
btn
|
||||||
|
.setIcon('arrow-up')
|
||||||
|
.setTooltip('Move up')
|
||||||
|
.setDisabled(i === 0)
|
||||||
|
.onClick(async () => {
|
||||||
|
if (i === 0) return;
|
||||||
|
const above = arr[i - 1];
|
||||||
|
arr[i - 1] = arr[i];
|
||||||
|
arr[i] = above;
|
||||||
|
await this.saveAndRefresh();
|
||||||
|
this.display();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addExtraButton((btn) => {
|
||||||
|
btn
|
||||||
|
.setIcon('arrow-down')
|
||||||
|
.setTooltip('Move down')
|
||||||
|
.setDisabled(i === arr.length - 1)
|
||||||
|
.onClick(async () => {
|
||||||
|
if (i === arr.length - 1) return;
|
||||||
|
const below = arr[i + 1];
|
||||||
|
arr[i + 1] = arr[i];
|
||||||
|
arr[i] = below;
|
||||||
|
await this.saveAndRefresh();
|
||||||
|
this.display();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addExtraButton((btn) => {
|
||||||
|
btn
|
||||||
|
.setIcon('trash')
|
||||||
|
.setTooltip('Delete this date system')
|
||||||
|
.onClick(async () => {
|
||||||
|
arr.splice(i, 1);
|
||||||
|
await this.saveAndRefresh();
|
||||||
|
this.display();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addSystemTextSetting(container,
|
||||||
|
'Name', 'Label shown in the calendar right-click menu.',
|
||||||
|
system, 'name', 'Journal',
|
||||||
|
);
|
||||||
|
this.addSystemTextSetting(container,
|
||||||
|
'Folder', 'Folder these notes live in.',
|
||||||
|
system, 'folder', 'periodic/journal',
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatSetting = this.addSystemTextSetting(container,
|
||||||
|
'Name format', 'Filename format (moment.js format). Include {title} for many notes per date.',
|
||||||
|
system, 'nameFormat', 'YYYY-MM-DD - {title}',
|
||||||
|
);
|
||||||
|
// A blank row is still being filled in, so only warn once something was typed.
|
||||||
|
if (system.nameFormat && !formatHasDateToken(system.nameFormat)) {
|
||||||
|
formatSetting.descEl.createDiv({
|
||||||
|
cls: 'waypoint-settings-warning',
|
||||||
|
text: 'This name format has no date placeholder, so it will never match or create dated notes.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addSystemTextSetting(container,
|
||||||
|
'Template file', 'Path to the template file. The .md extension is optional.',
|
||||||
|
system, 'templateFile', 'resources/template/journal',
|
||||||
|
);
|
||||||
|
this.addSystemTextSetting(container,
|
||||||
|
'Type property', `Fallback value for the 'type' frontmatter property, used when no template is found.`,
|
||||||
|
system, 'typeProperty', 'journal-note',
|
||||||
|
);
|
||||||
|
this.addSystemTextSetting(container,
|
||||||
|
'Icon', 'Lucide icon name for the menu item. Browse names at lucide.dev.',
|
||||||
|
system, 'icon', 'book-open',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
new Setting(container)
|
||||||
|
.addButton((btn) =>
|
||||||
|
btn
|
||||||
|
.setButtonText('Add date system')
|
||||||
|
.setCta()
|
||||||
|
.onClick(async () => {
|
||||||
|
this.settings.dateSystems.push(Object.assign(
|
||||||
|
{ id: `ds-${Date.now()}-${Math.random().toString(36).slice(2, 6)}` },
|
||||||
|
DEFAULT_DATE_SYSTEM,
|
||||||
|
));
|
||||||
|
await this.saveAndRefresh();
|
||||||
|
this.display();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the Setting so callers can append validation notices to its description. */
|
||||||
|
private addSystemTextSetting<K extends string>(
|
||||||
|
container: HTMLElement,
|
||||||
|
name: string,
|
||||||
|
desc: string,
|
||||||
|
obj: Record<K, string>,
|
||||||
|
key: K,
|
||||||
|
placeholder: string,
|
||||||
|
): Setting {
|
||||||
|
return new Setting(container)
|
||||||
|
.setName(name)
|
||||||
|
.setDesc(desc)
|
||||||
|
.addText((text) => {
|
||||||
|
text.setPlaceholder(placeholder);
|
||||||
|
text.setValue(obj[key]);
|
||||||
|
text.onChange((value) => {
|
||||||
|
obj[key] = value;
|
||||||
|
this.saveAndRefresh();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════
|
// ═══════════════════════════════
|
||||||
// Recent Files tab
|
// Recent Files tab
|
||||||
// ═══════════════════════════════
|
// ═══════════════════════════════
|
||||||
|
|||||||
@@ -7,6 +7,38 @@ export interface PeriodNoteSettings {
|
|||||||
typeProperty: string;
|
typeProperty: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A folder of notes whose filenames begin with a date — the vault's "systems":
|
||||||
|
* daily notes, a journal, meeting notes.
|
||||||
|
*
|
||||||
|
* `nameFormat` is a moment format string, so literal text needs bracket
|
||||||
|
* escaping (`YYYY-MM-DD - [Journal]`). When it contains `{title}` the system
|
||||||
|
* holds many notes per date and the token marks where the free-text title
|
||||||
|
* goes; without it a date maps to exactly one filename.
|
||||||
|
*/
|
||||||
|
export interface DateSystemSettings {
|
||||||
|
/** Stable across edits and reordering, so the settings UI can key rows. */
|
||||||
|
id: string;
|
||||||
|
/** Shown in the calendar's right-click menu. */
|
||||||
|
name: string;
|
||||||
|
folder: string;
|
||||||
|
nameFormat: string;
|
||||||
|
templateFile: string;
|
||||||
|
typeProperty: string;
|
||||||
|
/** Lucide icon name for the menu item. */
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Field defaults for a newly added system, and the merge base for saved ones. */
|
||||||
|
export const DEFAULT_DATE_SYSTEM: Omit<DateSystemSettings, 'id'> = {
|
||||||
|
name: '',
|
||||||
|
folder: '',
|
||||||
|
nameFormat: 'YYYY-MM-DD - {title}',
|
||||||
|
templateFile: '',
|
||||||
|
typeProperty: '',
|
||||||
|
icon: 'file',
|
||||||
|
};
|
||||||
|
|
||||||
export interface CalendarSettings {
|
export interface CalendarSettings {
|
||||||
firstDayOfWeek: number; // 0=Sunday, 1=Monday
|
firstDayOfWeek: number; // 0=Sunday, 1=Monday
|
||||||
showNoteIndicators: boolean;
|
showNoteIndicators: boolean;
|
||||||
@@ -36,6 +68,8 @@ export interface WaypointSettings {
|
|||||||
monthly: PeriodNoteSettings;
|
monthly: PeriodNoteSettings;
|
||||||
quarterly: PeriodNoteSettings;
|
quarterly: PeriodNoteSettings;
|
||||||
yearly: PeriodNoteSettings;
|
yearly: PeriodNoteSettings;
|
||||||
|
/** Day-scoped systems beyond the daily note, in menu order. */
|
||||||
|
dateSystems: DateSystemSettings[];
|
||||||
recentFiles: RecentFilesSettings;
|
recentFiles: RecentFilesSettings;
|
||||||
display: DisplaySettings;
|
display: DisplaySettings;
|
||||||
}
|
}
|
||||||
@@ -75,6 +109,26 @@ export const DEFAULT_SETTINGS: WaypointSettings = {
|
|||||||
nameFormat: 'YYYY',
|
nameFormat: 'YYYY',
|
||||||
typeProperty: 'yearly-note',
|
typeProperty: 'yearly-note',
|
||||||
},
|
},
|
||||||
|
dateSystems: [
|
||||||
|
{
|
||||||
|
id: 'journal',
|
||||||
|
name: 'Journal',
|
||||||
|
folder: 'periodic/journal',
|
||||||
|
nameFormat: 'YYYY-MM-DD - [Journal]',
|
||||||
|
templateFile: '',
|
||||||
|
typeProperty: 'journal',
|
||||||
|
icon: 'book-open',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'meetings',
|
||||||
|
name: 'Meeting',
|
||||||
|
folder: 'periodic/meetings',
|
||||||
|
nameFormat: 'YYYY-MM-DD - {title}',
|
||||||
|
templateFile: '',
|
||||||
|
typeProperty: 'meeting',
|
||||||
|
icon: 'users',
|
||||||
|
},
|
||||||
|
],
|
||||||
recentFiles: {
|
recentFiles: {
|
||||||
maxItems: 50,
|
maxItems: 50,
|
||||||
updateOn: 'file-open',
|
updateOn: 'file-open',
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// ── Date system filename helpers ──
|
||||||
|
//
|
||||||
|
// A "date system" is a folder of notes whose filenames begin with a date:
|
||||||
|
//
|
||||||
|
// periodic/daily/2026-09-07.md one per date
|
||||||
|
// periodic/journal/2026-09-07 - Journal.md one per date
|
||||||
|
// periodic/meetings/2026-09-07 - Meeting w Mark.md many per date
|
||||||
|
//
|
||||||
|
// The system's `nameFormat` is a moment format string. When it contains
|
||||||
|
// `{title}` the system holds many notes per date: the part before the token is
|
||||||
|
// the date prefix used to find them, and the token marks where a free-text
|
||||||
|
// title goes when creating one.
|
||||||
|
//
|
||||||
|
// This module imports nothing from 'obsidian' so it stays unit-testable in
|
||||||
|
// plain node. Callers do the moment formatting and pass the results in.
|
||||||
|
|
||||||
|
export const TITLE_TOKEN = '{title}';
|
||||||
|
|
||||||
|
export interface NameFormatParts {
|
||||||
|
/** Moment format for the text before the title. */
|
||||||
|
before: string;
|
||||||
|
/** Moment format for the text after the title. Empty for most systems. */
|
||||||
|
after: string;
|
||||||
|
/** True when the format carries a title token, i.e. many notes per date. */
|
||||||
|
hasTitle: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a name format around its title token.
|
||||||
|
*
|
||||||
|
* The split has to happen before moment sees the string: `t`, `i`, `l` and `e`
|
||||||
|
* are all live moment tokens, so formatting `{title}` directly would mangle it.
|
||||||
|
*/
|
||||||
|
export function splitNameFormat(nameFormat: string): NameFormatParts {
|
||||||
|
const idx = nameFormat.indexOf(TITLE_TOKEN);
|
||||||
|
if (idx < 0) return { before: nameFormat, after: '', hasTitle: false };
|
||||||
|
return {
|
||||||
|
before: nameFormat.slice(0, idx),
|
||||||
|
after: nameFormat.slice(idx + TITLE_TOKEN.length),
|
||||||
|
hasTitle: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether `path` sits in `folder` or any subfolder. An empty folder is the vault root. */
|
||||||
|
export function isInFolder(path: string, folder: string): boolean {
|
||||||
|
if (!folder) return true;
|
||||||
|
const prefix = folder.endsWith('/') ? folder : `${folder}/`;
|
||||||
|
return path.startsWith(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether `basename` is a note of a system whose formatted name parts are
|
||||||
|
* `before` and `after`.
|
||||||
|
*
|
||||||
|
* One-per-date systems must match the whole basename. Many-per-date systems
|
||||||
|
* match on the date prefix, since the middle is a free-text title.
|
||||||
|
*/
|
||||||
|
export function matchesSystemName(
|
||||||
|
basename: string,
|
||||||
|
before: string,
|
||||||
|
after: string,
|
||||||
|
hasTitle: boolean,
|
||||||
|
): boolean {
|
||||||
|
if (!hasTitle) return basename === before + after;
|
||||||
|
// A format of just `{title}` formats to an empty prefix, which would claim
|
||||||
|
// every file in the folder for every date. Treat it as matching nothing;
|
||||||
|
// the settings tab flags the format as missing a date instead.
|
||||||
|
if (!before) return false;
|
||||||
|
if (basename.length < before.length + after.length) return false;
|
||||||
|
return basename.startsWith(before) && basename.endsWith(after);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The free-text part of a many-per-date basename, for use as a menu label.
|
||||||
|
* Falls back to the whole basename when the affixes do not line up.
|
||||||
|
*/
|
||||||
|
export function titleFromBasename(basename: string, before: string, after: string): string {
|
||||||
|
const start = basename.startsWith(before) ? before.length : 0;
|
||||||
|
const end = after && basename.endsWith(after)
|
||||||
|
? basename.length - after.length
|
||||||
|
: basename.length;
|
||||||
|
if (end <= start) return basename;
|
||||||
|
return basename.slice(start, end).trim() || basename;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Characters Obsidian rejects in filenames, plus the link-syntax characters
|
||||||
|
* (`#^[]|`) it refuses to index cleanly. Collapsed to a dash so a typed title
|
||||||
|
* like "Meeting w/ Mark" still produces a creatable filename.
|
||||||
|
*/
|
||||||
|
const ILLEGAL_FILENAME_CHARS = /[\\/:*?"<>|#^[\]]/g;
|
||||||
|
|
||||||
|
export function sanitizeTitle(title: string): string {
|
||||||
|
return title.replace(ILLEGAL_FILENAME_CHARS, '-').replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether a name format resolves to a usable date prefix at all. */
|
||||||
|
export function formatHasDateToken(nameFormat: string): boolean {
|
||||||
|
const { before } = splitNameFormat(nameFormat);
|
||||||
|
// Strip bracket-escaped literals, then look for any moment date token.
|
||||||
|
const unescaped = before.replace(/\[[^\]]*\]/g, '');
|
||||||
|
return /[YMDQGWEwdgeo]/.test(unescaped);
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
type PaneType,
|
type PaneType,
|
||||||
} from 'obsidian';
|
} from 'obsidian';
|
||||||
import type WaypointPlugin from 'src/main';
|
import type WaypointPlugin from 'src/main';
|
||||||
|
import type { DateSystemNotes } from 'src/main';
|
||||||
import { getMonthGrid } from 'src/utils/date-utils';
|
import { getMonthGrid } from 'src/utils/date-utils';
|
||||||
import { BookmarkItem } from 'src/models/bookmark';
|
import { BookmarkItem } from 'src/models/bookmark';
|
||||||
|
|
||||||
@@ -224,6 +225,11 @@ export class WaypointView extends ItemView {
|
|||||||
this.plugin.openPeriodNote('day', day.date, this.app.workspace.getLeaf('tab'));
|
this.plugin.openPeriodNote('day', day.date, this.app.workspace.getLeaf('tab'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
cell.addEventListener('contextmenu', (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.showDayContextMenu(event, day.date);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +242,79 @@ export class WaypointView extends ItemView {
|
|||||||
this.redraw();
|
this.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Right-click menu for a day cell: every date system's existing notes for
|
||||||
|
* that date, plus the actions that would create the ones it is missing.
|
||||||
|
*/
|
||||||
|
private showDayContextMenu(event: MouseEvent, date: moment.Moment): void {
|
||||||
|
const menu = new Menu();
|
||||||
|
|
||||||
|
menu.addItem((i) => i.setTitle(date.format('dddd, MMMM D, YYYY')).setIsLabel(true));
|
||||||
|
|
||||||
|
for (const bucket of this.plugin.findDateSystemNotes(date)) {
|
||||||
|
// Many-per-date systems can always take another note; one-per-date
|
||||||
|
// systems only offer creation while their single note is missing.
|
||||||
|
const canCreate = bucket.multiple || bucket.notes.length === 0;
|
||||||
|
if (bucket.notes.length + (canCreate ? 1 : 0) === 0) continue;
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
|
for (const note of bucket.notes) {
|
||||||
|
menu.addItem((i) =>
|
||||||
|
i
|
||||||
|
.setTitle(note.label)
|
||||||
|
.setIcon(bucket.system.icon)
|
||||||
|
.onClick((evt) => this.focusFile(note.file, Keymap.isModEvent(evt))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canCreate) continue;
|
||||||
|
|
||||||
|
const noun = bucket.system.name.toLowerCase();
|
||||||
|
menu.addItem((i) =>
|
||||||
|
i
|
||||||
|
// The ellipsis promises the prompt that a free-text title needs.
|
||||||
|
.setTitle(bucket.multiple ? `New ${noun} note…` : `New ${noun} note`)
|
||||||
|
.setIcon('plus')
|
||||||
|
.onClick((evt) => this.createDateSystemNote(bucket, date, Keymap.isModEvent(evt))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.showAtPosition({ x: event.clientX, y: event.clientY });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a date system's note, asking for a title first when the system
|
||||||
|
* holds many notes per date. A held modifier opens the result in a new pane.
|
||||||
|
*/
|
||||||
|
private createDateSystemNote(bucket: DateSystemNotes, date: moment.Moment, newLeaf: PaneType | boolean): void {
|
||||||
|
if (!bucket.multiple) {
|
||||||
|
void this.plugin.openDateSystemNote(bucket.system, date, {
|
||||||
|
leaf: newLeaf ? this.app.workspace.getLeaf(newLeaf) : undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = bucket.system.name;
|
||||||
|
new PromptModal(
|
||||||
|
this.app,
|
||||||
|
{
|
||||||
|
title: `New ${name.toLowerCase()} note`,
|
||||||
|
placeholder: `${name} with…`,
|
||||||
|
cta: 'Create',
|
||||||
|
},
|
||||||
|
(title) => {
|
||||||
|
// `getLeaf` materialises the pane immediately, so the target pane is
|
||||||
|
// resolved only after the title is confirmed — cancelling the prompt
|
||||||
|
// must not leave an empty tab behind.
|
||||||
|
void this.plugin.openDateSystemNote(bucket.system, date, {
|
||||||
|
title,
|
||||||
|
leaf: newLeaf ? this.app.workspace.getLeaf(newLeaf) : undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
).open();
|
||||||
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
// Recent Files panel
|
// Recent Files panel
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
@@ -1062,7 +1141,7 @@ export class WaypointView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private promptRename(item: BookmarkItem): void {
|
private promptRename(item: BookmarkItem): void {
|
||||||
new RenameModal(this.app, item.label, (newLabel) => {
|
new PromptModal(this.app, { title: 'Rename bookmark', initialValue: item.label }, (newLabel) => {
|
||||||
if (newLabel && newLabel.trim()) {
|
if (newLabel && newLabel.trim()) {
|
||||||
this.plugin.updateBookmark(item.id, { label: newLabel.trim() });
|
this.plugin.updateBookmark(item.id, { label: newLabel.trim() });
|
||||||
}
|
}
|
||||||
@@ -1077,24 +1156,34 @@ export class WaypointView extends ItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Rename modal ──
|
// ── Text prompt modal ──
|
||||||
|
|
||||||
class RenameModal extends Modal {
|
interface PromptModalOptions {
|
||||||
private currentValue: string;
|
title: string;
|
||||||
|
placeholder?: string;
|
||||||
|
initialValue?: string;
|
||||||
|
/** Submit button label. Defaults to 'Save'. */
|
||||||
|
cta?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Single-line text prompt, shared by bookmark renaming and note creation. */
|
||||||
|
class PromptModal extends Modal {
|
||||||
|
private options: PromptModalOptions;
|
||||||
private onSubmit: (value: string) => void;
|
private onSubmit: (value: string) => void;
|
||||||
|
|
||||||
constructor(app: App, currentValue: string, onSubmit: (value: string) => void) {
|
constructor(app: App, options: PromptModalOptions, onSubmit: (value: string) => void) {
|
||||||
super(app);
|
super(app);
|
||||||
this.currentValue = currentValue;
|
this.options = options;
|
||||||
this.onSubmit = onSubmit;
|
this.onSubmit = onSubmit;
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen(): void {
|
onOpen(): void {
|
||||||
this.titleEl.setText('Rename bookmark');
|
this.titleEl.setText(this.options.title);
|
||||||
|
|
||||||
const input = this.contentEl.createEl('input', {
|
const input = this.contentEl.createEl('input', {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
value: this.currentValue,
|
value: this.options.initialValue ?? '',
|
||||||
|
placeholder: this.options.placeholder ?? '',
|
||||||
});
|
});
|
||||||
input.style.width = '100%';
|
input.style.width = '100%';
|
||||||
input.style.marginBottom = '12px';
|
input.style.marginBottom = '12px';
|
||||||
@@ -1107,7 +1196,7 @@ class RenameModal extends Modal {
|
|||||||
cancelBtn.style.marginRight = '8px';
|
cancelBtn.style.marginRight = '8px';
|
||||||
cancelBtn.addEventListener('click', () => this.close());
|
cancelBtn.addEventListener('click', () => this.close());
|
||||||
|
|
||||||
const saveBtn = btnContainer.createEl('button', { text: 'Save', cls: 'mod-cta' });
|
const saveBtn = btnContainer.createEl('button', { text: this.options.cta ?? 'Save', cls: 'mod-cta' });
|
||||||
saveBtn.addEventListener('click', () => {
|
saveBtn.addEventListener('click', () => {
|
||||||
this.onSubmit(input.value);
|
this.onSubmit(input.value);
|
||||||
this.close();
|
this.close();
|
||||||
|
|||||||
@@ -420,3 +420,11 @@ button.waypoint-calendar-today-btn {
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
cursor: var(--cursor-link, pointer);
|
cursor: var(--cursor-link, pointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Settings: date systems ── */
|
||||||
|
|
||||||
|
.waypoint-settings-warning {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--text-error);
|
||||||
|
font-size: var(--font-ui-smaller);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user