From 26a1a481ac29b5d0c9e55a7b3a61db698d4c20c9 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 7 Sep 2026 21:07:18 -0400 Subject: [PATCH] feat: add date systems menu to calendar days Right-clicking a calendar day now lists every configured date-prefixed note system. Journal notes are singletons; {title} formats support multiple notes per date, such as meetings.\n\nAdds default Journal and Meeting systems, configurable settings UI, safe filename/title handling, one shared dated-note creation path, and a compiled menu smoke test fixture in the test vault. --- DOCUMENTATION.md | 137 +++++++++++++++++++++++---- QA.md | 13 +++ README.md | 18 +++- main.js | 40 ++++---- src/main.ts | 186 ++++++++++++++++++++++++++++++++----- src/settings-tab.ts | 141 +++++++++++++++++++++++++++- src/settings.ts | 54 +++++++++++ src/utils/date-systems.ts | 103 ++++++++++++++++++++ src/views/waypoint-view.ts | 107 +++++++++++++++++++-- styles.css | 8 ++ 10 files changed, 731 insertions(+), 76 deletions(-) create mode 100644 src/utils/date-systems.ts diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index e98bbc9..8883735 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -9,12 +9,13 @@ ``` src/ ├── main.ts Plugin entry — lifecycle, commands, events, data persistence -├── settings.ts Interface definitions + DEFAULT_SETTINGS constant -├── settings-tab.ts PluginSettingTab UI — tabs: calendar, periodic, recent, display, about +├── settings.ts Interface definitions + DEFAULT_SETTINGS / DEFAULT_DATE_SYSTEM constants +├── settings-tab.ts PluginSettingTab UI — calendar, periodic, date systems, recent, display, about ├── models/ │ └── bookmark.ts BookmarkItem + WaypointData interfaces ├── utils/ │ ├── date-utils.ts Moment.js helpers: period formatting, month grid, navigation +│ ├── date-systems.ts Pure date-system filename helpers (no `obsidian` import, unit-testable) │ └── path-utils.ts Pure path helper: rename remapping (no `obsidian` import, unit-testable) └── views/ └── waypoint-view.ts ItemView subclass — full sidebar rendering + interaction @@ -30,7 +31,7 @@ src/ ### `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)`. 3. **Settings tab** — `WaypointSettingTab` receives the live `settings` object + `() => this.redrawAll()` callback for live preview. 4. **Register commands** (see Commands section below). @@ -56,6 +57,7 @@ interface WaypointSettings { monthly: PeriodNoteSettings; quarterly: PeriodNoteSettings; yearly: PeriodNoteSettings; + dateSystems: DateSystemSettings[]; // day-scoped systems beyond the daily note, in menu order recentFiles: RecentFilesSettings; // maxItems, updateOn, omittedPaths[], omittedTags[], filterTags[] display: DisplaySettings; // px sizing for bookmark rows, fonts, icons, calendar cells } @@ -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) ```typescript @@ -147,6 +151,99 @@ 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`) 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 | + +### 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 +``` + +**`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. Read the template at `system.templateFile` (the `.md` extension is appended when omitted). If it exists, the note is created with the template's contents. +2. With no template, the note is created with minimal frontmatter instead: `type: {system.typeProperty}` + `date: YYYY-MM-DD`. + +--- + ## Period Note Creation (`openPeriodNote`) ```typescript @@ -157,16 +254,9 @@ openPeriodNote( ): Promise ``` -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"`. -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)`. +Filename building, the existence check, template-or-frontmatter creation and leaf selection are therefore documented once, under Date Systems above. Middle-click handlers in the view pass an explicit tab leaf: `openPeriodNote(period, date, this.app.workspace.getLeaf('tab'))`. @@ -202,7 +292,7 @@ Both update the markdown-basename set used for calendar note indicators, then tr 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:** 1. `addToRecentFiles(file)` — omission check, then dedupes (removes existing entry), prepends to front, truncates to `maxItems`. @@ -250,9 +340,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. - **Nav buttons:** ◀/▶ shift month ±1. "Today" resets to current month. - **Week number column:** Clicking a week number opens the weekly note for that week's Monday; middle-clicking opens it in a new tab. -- **Day cells:** 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`. +**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 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 +389,7 @@ Renders `plugin.recentFiles` as Obsidian-native `nav-file` elements using the fi **Context menu (right-click):** - 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 - Group items: "Expand/Collapse", "Add bookmark here", "New sub-group" - All items: "Insert separator above/below", "Insert spacer above/below", "Remove" @@ -302,13 +399,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. 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: - Live preview of selected icon. - Search input with 60ms debounce. - Grid of matching icons (max 80 shown), loaded from `https://cdn.jsdelivr.net/npm/lucide-static@0.517.0/tags.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 +414,10 @@ Renders `plugin.recentFiles` as Obsidian-native `nav-file` elements using the fi All classes prefixed with `waypoint-`. Uses Obsidian CSS variables throughout: - `--font-ui-small`, `--font-ui-medium`, `--font-semibold`, `--font-medium`, `--font-light` -- `--text-muted`, `--text-faint`, `--text-accent`, `--text-on-accent` +- `--text-muted`, `--text-faint`, `--text-accent`, `--text-on-accent`, `--text-error` - `--background-modifier-border`, `--background-modifier-active-hover`, `--background-primary`, `--background-secondary` - `--interactive-accent` -- `--cursor` (for custom cursor support) +- `--cursor-link` (pointer cursor, with a `pointer` fallback) Key layout: - `.waypoint-view` — flex column, `overflow-y: auto`, 8px padding. @@ -372,7 +469,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()`. --- diff --git a/QA.md b/QA.md index 9e0bc46..fc462b3 100644 --- a/QA.md +++ b/QA.md @@ -70,6 +70,19 @@ - [ ] Note indicator dots show on days with existing .md files - [ ] 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 ## Recent Files (Regression) - [ ] Opening a file adds it to recent files diff --git a/README.md b/README.md index f33f55d..cf58bf9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Calendar, recent files, and custom bookmarks sidebar for Obsidian. ## 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 - **Favorites** — custom bookmarks with groups, icons, and rename @@ -19,6 +20,21 @@ Calendar, recent files, and custom bookmarks sidebar for Obsidian. | Go to yearly note | `Ctrl+Shift+Alt+Y` | | 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. + ## Installation ### Via BRAT diff --git a/main.js b/main.js index 551da03..d9c431d 100644 --- a/main.js +++ b/main.js @@ -2,26 +2,26 @@ THIS IS A GENERATED/BUNDLED FILE BY ESBUILD */ -var q=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var J=Object.getOwnPropertyNames;var ee=Object.prototype.hasOwnProperty;var te=(g,m)=>{for(var e in m)q(g,e,{get:m[e],enumerable:!0})},ie=(g,m,e,i)=>{if(m&&typeof m=="object"||typeof m=="function")for(let s of J(m))!ee.call(g,s)&&s!==e&&q(g,s,{get:()=>m[s],enumerable:!(i=X(m,s))||i.enumerable});return g};var ne=g=>ie(q({},"__esModule",{value:!0}),g);var le={};te(le,{default:()=>O});module.exports=ne(le);var y=require("obsidian");var P={calendar:{firstDayOfWeek:1,showNoteIndicators:!0},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:[],filterTags:[]},display:{rowSize:26,rowSpacing:2,indentSize:16,fontSize:13,iconSize:16,calendarCellSize:32}};var w=require("obsidian");var R=class extends w.PluginSettingTab{constructor(e,i,s,n){super(e,i);this.activeTab="calendar";this.plugin=i,this.settings=s,this.onSettingsChange=n}display(){let{containerEl:e}=this;e.empty();let i=e.createDiv({cls:"waypoint-settings-tabs"}),s=[{key:"calendar",label:"Calendar"},{key:"periodic",label:"Periodic Notes"},{key:"recent",label:"Recent Files"},{key:"display",label:"Display"},{key:"about",label:"About"}];for(let t of s)i.createEl("button",{cls:`waypoint-settings-tab${this.activeTab===t.key?" is-active":""}`,text:t.label}).addEventListener("click",()=>{this.activeTab=t.key,this.display()});let n=e.createDiv({cls:"waypoint-settings-content"});switch(this.activeTab){case"calendar":this.renderCalendarTab(n);break;case"periodic":this.renderPeriodicTab(n);break;case"recent":this.renderRecentTab(n);break;case"display":this.renderDisplayTab(n);break;case"about":this.renderAboutTab(n);break}}renderCalendarTab(e){new w.Setting(e).setName("First day of week").setDesc("Which day the calendar week starts on.").addDropdown(i=>{i.addOption("0","Sunday").addOption("1","Monday").setValue(String(this.settings.calendar.firstDayOfWeek)).onChange(s=>{this.settings.calendar.firstDayOfWeek=parseInt(s,10),this.saveAndRefresh()})}),new w.Setting(e).setName("Show note indicators").setDesc("Show a dot on days that have existing notes.").addToggle(i=>{i.setValue(this.settings.calendar.showNoteIndicators).onChange(s=>{this.settings.calendar.showNoteIndicators=s,this.saveAndRefresh()})})}renderPeriodicTab(e){this.addPeriodNoteSettings(e,"Daily",this.settings.daily),this.addPeriodNoteSettings(e,"Weekly",this.settings.weekly),this.addPeriodNoteSettings(e,"Monthly",this.settings.monthly),this.addPeriodNoteSettings(e,"Quarterly",this.settings.quarterly),this.addPeriodNoteSettings(e,"Yearly",this.settings.yearly)}addPeriodNoteSettings(e,i,s){new w.Setting(e).setHeading().setName(i),new w.Setting(e).setName("Folder").setDesc(`Folder path for ${i.toLowerCase()} notes.`).addText(n=>{n.setPlaceholder("periodic/daily"),n.setValue(s.folder),n.onChange(t=>{s.folder=t,this.saveAndRefresh()})}),new w.Setting(e).setName("Name format").setDesc(`Date format for ${i.toLowerCase()} note filenames (moment.js format).`).addText(n=>{n.setPlaceholder("yyyy-MM-dd"),n.setValue(s.nameFormat),n.onChange(t=>{s.nameFormat=t,this.saveAndRefresh()})}),new w.Setting(e).setName("Template file").setDesc("Path to the template file (without .md extension).").addText(n=>{n.setPlaceholder("Templates/Daily note"),n.setValue(s.templateFile),n.onChange(t=>{s.templateFile=t,this.saveAndRefresh()})}),new w.Setting(e).setName("Type property").setDesc("Value for the 'type' frontmatter property.").addText(n=>{n.setPlaceholder("daily-note"),n.setValue(s.typeProperty),n.onChange(t=>{s.typeProperty=t,this.saveAndRefresh()})})}renderRecentTab(e){new w.Setting(e).setName("Max items").setDesc("Maximum number of recent files to track.").addText(t=>{t.inputEl.setAttr("type","number"),t.inputEl.setAttr("placeholder","50"),t.setValue(String(this.settings.recentFiles.maxItems)),t.inputEl.onblur=()=>{let a=parseInt(t.getValue(),10);!isNaN(a)&&a>0&&(this.settings.recentFiles.maxItems=a,this.saveAndRefresh())}}),new w.Setting(e).setName("Update on").setDesc("When to add a file to the recent list.").addDropdown(t=>{t.addOption("file-open","File opened").addOption("file-edit","File changed").setValue(this.settings.recentFiles.updateOn).onChange(a=>{this.settings.recentFiles.updateOn=a,this.saveAndRefresh()})});let i=new DocumentFragment;i.appendText("Regex patterns for paths to exclude. One per line."),new w.Setting(e).setName("Omitted paths").setDesc(i).addTextArea(t=>{t.inputEl.setAttr("rows",4),t.setPlaceholder(`^archives/ -\\.png$`),t.setValue(this.settings.recentFiles.omittedPaths.join(` -`)),t.inputEl.onblur=()=>{this.settings.recentFiles.omittedPaths=t.getValue().split(` -`).filter(a=>a.trim()),this.saveAndRefresh()}});let s=new DocumentFragment;s.appendText("Regex patterns for frontmatter tags to exclude. One per line."),new w.Setting(e).setName("Omitted tags").setDesc(s).addTextArea(t=>{t.inputEl.setAttr("rows",4),t.setPlaceholder(`ignore -archive`),t.setValue(this.settings.recentFiles.omittedTags.join(` -`)),t.inputEl.onblur=()=>{this.settings.recentFiles.omittedTags=t.getValue().split(` -`).filter(a=>a.trim()),this.saveAndRefresh()}});let n=new DocumentFragment;n.appendText("Tags to show as filter pills above the file list. One per line. Leave empty to auto-detect from frontmatter `type` property."),new w.Setting(e).setName("Filter tags").setDesc(n).addTextArea(t=>{t.inputEl.setAttr("rows",4),t.setPlaceholder(`meeting +var _=Object.defineProperty;var re=Object.getOwnPropertyDescriptor;var le=Object.getOwnPropertyNames;var ce=Object.prototype.hasOwnProperty;var de=(p,d)=>{for(var e in d)_(p,e,{get:d[e],enumerable:!0})},pe=(p,d,e,a)=>{if(d&&typeof d=="object"||typeof d=="function")for(let i of le(d))!ce.call(p,i)&&i!==e&&_(p,i,{get:()=>d[i],enumerable:!(a=re(d,i))||a.enumerable});return p};var me=p=>pe(_({},"__esModule",{value:!0}),p);var ve={};de(ve,{default:()=>q});module.exports=me(ve);var f=require("obsidian");var R={name:"",folder:"",nameFormat:"YYYY-MM-DD - {title}",templateFile:"",typeProperty:"",icon:"file"},E={calendar:{firstDayOfWeek:1,showNoteIndicators:!0},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"},dateSystems:[{id:"journal",name:"Journal",folder:"periodic/journal",nameFormat:"YYYY-MM-DD - [Journal]",templateFile:"resources/template/journal",typeProperty:"journal-note",icon:"book-open"},{id:"meetings",name:"Meeting",folder:"periodic/meetings",nameFormat:"YYYY-MM-DD - {title}",templateFile:"resources/template/meeting",typeProperty:"meeting-note",icon:"users"}],recentFiles:{maxItems:50,updateOn:"file-open",omittedPaths:[],omittedTags:[],filterTags:[]},display:{rowSize:26,rowSpacing:2,indentSize:16,fontSize:13,iconSize:16,calendarCellSize:32}};var k=require("obsidian");var X="{title}";function W(p){let d=p.indexOf(X);return d<0?{before:p,after:"",hasTitle:!1}:{before:p.slice(0,d),after:p.slice(d+X.length),hasTitle:!0}}function J(p,d){if(!d)return!0;let e=d.endsWith("/")?d:`${d}/`;return p.startsWith(e)}function ee(p,d,e,a){return a?!d||p.length|#^[\]]/g;function ne(p){return p.replace(he,"-").replace(/\s+/g," ").trim()}function N(p){let{before:d}=W(p),e=d.replace(/\[[^\]]*\]/g,"");return/[YMDQGWEwdgeo]/.test(e)}var O=class extends k.PluginSettingTab{constructor(e,a,i,t){super(e,a);this.activeTab="calendar";this.plugin=a,this.settings=i,this.onSettingsChange=t}display(){let{containerEl:e}=this;e.empty();let a=e.createDiv({cls:"waypoint-settings-tabs"}),i=[{key:"calendar",label:"Calendar"},{key:"periodic",label:"Periodic Notes"},{key:"systems",label:"Date systems"},{key:"recent",label:"Recent Files"},{key:"display",label:"Display"},{key:"about",label:"About"}];for(let n of i)a.createEl("button",{cls:`waypoint-settings-tab${this.activeTab===n.key?" is-active":""}`,text:n.label}).addEventListener("click",()=>{this.activeTab=n.key,this.display()});let t=e.createDiv({cls:"waypoint-settings-content"});switch(this.activeTab){case"calendar":this.renderCalendarTab(t);break;case"periodic":this.renderPeriodicTab(t);break;case"systems":this.renderSystemsTab(t);break;case"recent":this.renderRecentTab(t);break;case"display":this.renderDisplayTab(t);break;case"about":this.renderAboutTab(t);break}}renderCalendarTab(e){new k.Setting(e).setName("First day of week").setDesc("Which day the calendar week starts on.").addDropdown(a=>{a.addOption("0","Sunday").addOption("1","Monday").setValue(String(this.settings.calendar.firstDayOfWeek)).onChange(i=>{this.settings.calendar.firstDayOfWeek=parseInt(i,10),this.saveAndRefresh()})}),new k.Setting(e).setName("Show note indicators").setDesc("Show a dot on days that have existing notes.").addToggle(a=>{a.setValue(this.settings.calendar.showNoteIndicators).onChange(i=>{this.settings.calendar.showNoteIndicators=i,this.saveAndRefresh()})})}renderPeriodicTab(e){this.addPeriodNoteSettings(e,"Daily",this.settings.daily),this.addPeriodNoteSettings(e,"Weekly",this.settings.weekly),this.addPeriodNoteSettings(e,"Monthly",this.settings.monthly),this.addPeriodNoteSettings(e,"Quarterly",this.settings.quarterly),this.addPeriodNoteSettings(e,"Yearly",this.settings.yearly)}addPeriodNoteSettings(e,a,i){new k.Setting(e).setHeading().setName(a),new k.Setting(e).setName("Folder").setDesc(`Folder path for ${a.toLowerCase()} notes.`).addText(t=>{t.setPlaceholder("periodic/daily"),t.setValue(i.folder),t.onChange(n=>{i.folder=n,this.saveAndRefresh()})}),new k.Setting(e).setName("Name format").setDesc(`Date format for ${a.toLowerCase()} note filenames (moment.js format).`).addText(t=>{t.setPlaceholder("yyyy-MM-dd"),t.setValue(i.nameFormat),t.onChange(n=>{i.nameFormat=n,this.saveAndRefresh()})}),new k.Setting(e).setName("Template file").setDesc("Path to the template file (without .md extension).").addText(t=>{t.setPlaceholder("Templates/Daily note"),t.setValue(i.templateFile),t.onChange(n=>{i.templateFile=n,this.saveAndRefresh()})}),new k.Setting(e).setName("Type property").setDesc("Value for the 'type' frontmatter property.").addText(t=>{t.setPlaceholder("daily-note"),t.setValue(i.typeProperty),t.onChange(n=>{i.typeProperty=n,this.saveAndRefresh()})})}renderSystemsTab(e){let a=new DocumentFragment;a.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."}),a.createDiv({text:"The daily note is configured under Periodic Notes and always comes first in that menu."}),a.createDiv({text:"Name format is a moment.js format. Literal words need bracket escaping, e.g. YYYY-MM-DD - [Journal]."}),a.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 k.Setting(e).setHeading().setName("Date systems").setDesc(a),this.settings.dateSystems.forEach((i,t,n)=>{new k.Setting(e).setHeading().setName(i.name||"Untitled system").addExtraButton(o=>{o.setIcon("arrow-up").setTooltip("Move up").setDisabled(t===0).onClick(async()=>{if(t===0)return;let r=n[t-1];n[t-1]=n[t],n[t]=r,await this.saveAndRefresh(),this.display()})}).addExtraButton(o=>{o.setIcon("arrow-down").setTooltip("Move down").setDisabled(t===n.length-1).onClick(async()=>{if(t===n.length-1)return;let r=n[t+1];n[t+1]=n[t],n[t]=r,await this.saveAndRefresh(),this.display()})}).addExtraButton(o=>{o.setIcon("trash").setTooltip("Delete this date system").onClick(async()=>{n.splice(t,1),await this.saveAndRefresh(),this.display()})}),this.addSystemTextSetting(e,"Name","Label shown in the calendar right-click menu.",i,"name","Journal"),this.addSystemTextSetting(e,"Folder","Folder these notes live in.",i,"folder","periodic/journal");let s=this.addSystemTextSetting(e,"Name format","Filename format (moment.js format). Include {title} for many notes per date.",i,"nameFormat","YYYY-MM-DD - {title}");i.nameFormat&&!N(i.nameFormat)&&s.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(e,"Template file","Path to the template file. The .md extension is optional.",i,"templateFile","resources/template/journal"),this.addSystemTextSetting(e,"Type property","Fallback value for the 'type' frontmatter property, used when no template is found.",i,"typeProperty","journal-note"),this.addSystemTextSetting(e,"Icon","Lucide icon name for the menu item. Browse names at lucide.dev.",i,"icon","book-open")}),new k.Setting(e).addButton(i=>i.setButtonText("Add date system").setCta().onClick(async()=>{this.settings.dateSystems.push(Object.assign({id:`ds-${Date.now()}-${Math.random().toString(36).slice(2,6)}`},R)),await this.saveAndRefresh(),this.display()}))}addSystemTextSetting(e,a,i,t,n,s){return new k.Setting(e).setName(a).setDesc(i).addText(o=>{o.setPlaceholder(s),o.setValue(t[n]),o.onChange(r=>{t[n]=r,this.saveAndRefresh()})})}renderRecentTab(e){new k.Setting(e).setName("Max items").setDesc("Maximum number of recent files to track.").addText(n=>{n.inputEl.setAttr("type","number"),n.inputEl.setAttr("placeholder","50"),n.setValue(String(this.settings.recentFiles.maxItems)),n.inputEl.onblur=()=>{let s=parseInt(n.getValue(),10);!isNaN(s)&&s>0&&(this.settings.recentFiles.maxItems=s,this.saveAndRefresh())}}),new k.Setting(e).setName("Update on").setDesc("When to add a file to the recent list.").addDropdown(n=>{n.addOption("file-open","File opened").addOption("file-edit","File changed").setValue(this.settings.recentFiles.updateOn).onChange(s=>{this.settings.recentFiles.updateOn=s,this.saveAndRefresh()})});let a=new DocumentFragment;a.appendText("Regex patterns for paths to exclude. One per line."),new k.Setting(e).setName("Omitted paths").setDesc(a).addTextArea(n=>{n.inputEl.setAttr("rows",4),n.setPlaceholder(`^archives/ +\\.png$`),n.setValue(this.settings.recentFiles.omittedPaths.join(` +`)),n.inputEl.onblur=()=>{this.settings.recentFiles.omittedPaths=n.getValue().split(` +`).filter(s=>s.trim()),this.saveAndRefresh()}});let i=new DocumentFragment;i.appendText("Regex patterns for frontmatter tags to exclude. One per line."),new k.Setting(e).setName("Omitted tags").setDesc(i).addTextArea(n=>{n.inputEl.setAttr("rows",4),n.setPlaceholder(`ignore +archive`),n.setValue(this.settings.recentFiles.omittedTags.join(` +`)),n.inputEl.onblur=()=>{this.settings.recentFiles.omittedTags=n.getValue().split(` +`).filter(s=>s.trim()),this.saveAndRefresh()}});let t=new DocumentFragment;t.appendText("Tags to show as filter pills above the file list. One per line. Leave empty to auto-detect from frontmatter `type` property."),new k.Setting(e).setName("Filter tags").setDesc(t).addTextArea(n=>{n.inputEl.setAttr("rows",4),n.setPlaceholder(`meeting person -project`),t.setValue(this.settings.recentFiles.filterTags.join(` -`)),t.inputEl.onblur=()=>{this.settings.recentFiles.filterTags=t.getValue().split(` -`).filter(a=>a.trim()),this.saveAndRefresh()}})}renderDisplayTab(e){new w.Setting(e).setHeading().setName("Bookmarks"),this.addSliderSetting(e,"Row size","Height of bookmark items.",this.settings.display,"rowSize",18,40,1,"px"),this.addSliderSetting(e,"Row spacing","Gap between bookmark items.",this.settings.display,"rowSpacing",0,12,1,"px"),this.addSliderSetting(e,"Indent size","Indent per nesting depth.",this.settings.display,"indentSize",8,32,2,"px"),this.addSliderSetting(e,"Font size","Label font size.",this.settings.display,"fontSize",10,18,1,"px"),this.addSliderSetting(e,"Icon size","Bookmark icon size.",this.settings.display,"iconSize",12,24,1,"px"),new w.Setting(e).setHeading().setName("Calendar"),this.addSliderSetting(e,"Cell size","Height of calendar day cells.",this.settings.display,"calendarCellSize",20,48,2,"px"),new w.Setting(e).addButton(i=>i.setButtonText("Reset to defaults").onClick(()=>{this.settings.display={...P.display},this.saveAndRefresh(),this.display()}))}addSliderSetting(e,i,s,n,t,a,o,r,c){let l=new w.Setting(e).setName(i).setDesc(`${s} (${n[t]}${c})`);l.addSlider(h=>{h.setLimits(a,o,r).setValue(n[t]).setDynamicTooltip().onChange(p=>{n[t]=p,l.setDesc(`${s} (${p}${c})`),this.saveAndRefresh()})})}async saveAndRefresh(){await this.plugin.saveSettings(),this.onSettingsChange()}renderAboutTab(e){let i=this.plugin.manifest.version,s=e.createDiv();s.style.display="flex",s.style.alignItems="center",s.style.gap="12px",s.style.marginBottom="16px";let n=s.createDiv();n.style.display="flex",n.style.alignItems="center",n.style.justifyContent="center",n.style.width="48px",n.style.height="48px",n.style.borderRadius="12px",n.style.background="var(--interactive-accent)",n.style.color="var(--text-on-accent)",n.style.fontSize="24px",(0,w.setIcon)(n,"compass");let t=s.createDiv(),a=t.createEl("h2",{text:"Waypoint Sidebar"});a.style.margin="0",a.style.lineHeight="1.2";let o=t.createDiv({text:`v${i}`});o.style.color="var(--text-muted)",o.style.fontSize="var(--font-ui-small)";let r=e.createDiv();r.style.marginBottom="20px",r.style.lineHeight="1.6",r.style.color="var(--text-normal)",r.innerHTML=["

Waypoint is a sidebar plugin that brings three essential panels into one view:

",'
    ',"
  • Calendar \u2014 a monthly grid for your periodic notes (daily, weekly, monthly, quarterly, yearly). Click any day, week, or month to open or create the corresponding note.
  • ","
  • Recent Files \u2014 a list of recently opened files with type filtering, drag-and-drop, and right-click actions.
  • ","
  • Bookmarks \u2014 custom bookmarks with icons, groups, nesting, and drag-and-drop reordering. Separate from Obsidian's native bookmarks.
  • ","
",'

Made by Olivier. Licensed under MIT.

'].join(` -`)}};var d=require("obsidian");var I=require("obsidian");function Q(g,m,e){let i=(0,I.moment)({year:g,month:m,day:1}),s=(0,I.moment)(i).endOf("month"),n=(0,I.moment)(i).subtract((i.day()-e+7)%7,"days"),t=(0,I.moment)().startOf("day"),a=[],o=(0,I.moment)(n);for(;o.isBefore(s)||o.month()===m;){let r=[];for(let c=0;c<7;c++)r.push({date:(0,I.moment)(o),dayOfMonth:o.date(),isToday:o.isSame(t,"day"),isCurrentMonth:o.month()===m,isoWeekNumber:o.isoWeek()}),o.add(1,"day");if(a.push({weekNumber:r[0].isoWeekNumber,days:r}),a.length>=6)break}return a}var T="waypoint-view";function ae(g){return g.dragManager}var L=class extends d.ItemView{constructor(e,i){super(e);this.redraw=()=>{this.contentEl.empty(),this.contentEl.addClass("waypoint-view");let e=this.plugin.settings.display;this.contentEl.style.setProperty("--wp-row-size",e.rowSize+"px"),this.contentEl.style.setProperty("--wp-row-spacing",e.rowSpacing+"px"),this.contentEl.style.setProperty("--wp-indent-size",e.indentSize+"px"),this.contentEl.style.setProperty("--wp-font-size",e.fontSize+"px"),this.contentEl.style.setProperty("--wp-icon-size",e.iconSize+"px"),this.contentEl.style.setProperty("--wp-cal-cell-size",e.calendarCellSize+"px"),this.renderFavorites(),this.renderRecentFiles(),this.renderCalendar()};this.currentDisplayMonth=(0,d.moment)().month();this.currentDisplayYear=(0,d.moment)().year();this.dragId=null;this.dropZones=new WeakMap;this.recentFilesFilter=null;this.plugin=i}getViewType(){return T}getDisplayText(){return"Waypoint"}getIcon(){return"compass"}async onOpen(){this.redraw()}async onClose(){}renderCalendar(){let e=this.contentEl.createDiv({cls:"waypoint-section"});e.createDiv({cls:"waypoint-section-header",text:"Calendar"});let i=e.createDiv({cls:"waypoint-calendar"}),s=(0,d.moment)(),n=(0,d.moment)({year:this.currentDisplayYear,month:this.currentDisplayMonth,day:1}),t=i.createDiv({cls:"waypoint-calendar-top"}),a=t.createDiv({cls:"waypoint-calendar-breadcrumb"}),o=n.format("[Q]Q"),r=a.createSpan({cls:"waypoint-clickable",text:o});r.addEventListener("click",()=>{this.plugin.openPeriodNote("quarter",n)}),r.addEventListener("mousedown",F=>{F.button===1&&(F.preventDefault(),this.plugin.openPeriodNote("quarter",n,this.app.workspace.getLeaf("tab")))});let c=n.format("MMMM"),l=a.createSpan({cls:"waypoint-clickable",text:c});l.addEventListener("click",()=>{this.plugin.openPeriodNote("month",n)}),l.addEventListener("mousedown",F=>{F.button===1&&(F.preventDefault(),this.plugin.openPeriodNote("month",n,this.app.workspace.getLeaf("tab")))});let h=n.format("YYYY"),p=a.createSpan({cls:"waypoint-clickable",text:h});p.addEventListener("click",()=>{this.plugin.openPeriodNote("year",n)}),p.addEventListener("mousedown",F=>{F.button===1&&(F.preventDefault(),this.plugin.openPeriodNote("year",n,this.app.workspace.getLeaf("tab")))});let u=t.createDiv({cls:"waypoint-calendar-today-group"}),D=u.createEl("button",{cls:"waypoint-calendar-nav-btn"});(0,d.setIcon)(D,"chevron-left"),D.addEventListener("click",()=>this.navigateMonth(-1)),u.createEl("button",{cls:"waypoint-calendar-today-btn",text:"Today"}).addEventListener("click",()=>{this.currentDisplayMonth=(0,d.moment)().month(),this.currentDisplayYear=(0,d.moment)().year(),this.redraw()});let k=u.createEl("button",{cls:"waypoint-calendar-nav-btn"});(0,d.setIcon)(k,"chevron-right"),k.addEventListener("click",()=>this.navigateMonth(1));let b=i.createEl("table"),f=b.createEl("thead").createEl("tr");f.createEl("th",{text:""});let C=["sun","mon","tue","wed","thu","fri","sat"],$=this.plugin.settings.calendar.firstDayOfWeek;for(let F=0;F<7;F++){let A=($+F)%7;f.createEl("th",{text:C[A]})}let S=b.createEl("tbody"),U=Q(this.currentDisplayYear,this.currentDisplayMonth,this.plugin.settings.calendar.firstDayOfWeek);for(let F of U){let A=S.createEl("tr"),z=A.createEl("td",{cls:"waypoint-weeknum"});z.setText(String(F.weekNumber));let K=F.days[0].date;z.addEventListener("click",()=>{this.plugin.openPeriodNote("week",K)}),z.addEventListener("mousedown",E=>{E.button===1&&(E.preventDefault(),this.plugin.openPeriodNote("week",K,this.app.workspace.getLeaf("tab")))});for(let E of F.days){let M=A.createEl("td",{cls:"waypoint-day"});if(M.setText(String(E.dayOfMonth)),E.isCurrentMonth||M.addClass("other-month"),E.isToday&&M.addClass("today"),this.plugin.settings.calendar.showNoteIndicators){let N=E.date.format("YYYY-MM-DD");this.plugin.hasNoteForDate(N)&&M.addClass("has-note")}M.addEventListener("click",()=>{this.plugin.openPeriodNote("day",E.date)}),M.addEventListener("mousedown",N=>{N.button===1&&(N.preventDefault(),this.plugin.openPeriodNote("day",E.date,this.app.workspace.getLeaf("tab")))})}}}navigateMonth(e){let i=(0,d.moment)({year:this.currentDisplayYear,month:this.currentDisplayMonth}).add(e,"month");this.currentDisplayMonth=i.month(),this.currentDisplayYear=i.year(),this.redraw()}renderRecentFiles(){var r,c;let e=this.contentEl.createDiv({cls:"waypoint-section waypoint-recent-files"});if(e.createDiv({cls:"waypoint-section-header",text:"Recent Files"}),this.plugin.recentFiles.length===0){e.createDiv({cls:"nav-file",text:"No recent files"});return}let i=this.plugin.settings.recentFiles.filterTags||[],s={};if(i.length>0){for(let l of i)s[l]=0;for(let l of this.plugin.recentFiles){let h=this.app.vault.getAbstractFileByPath(l.path);if(h instanceof d.TFile){let p=this.app.metadataCache.getFileCache(h),u=(r=p==null?void 0:p.frontmatter)==null?void 0:r.type;u&&typeof u=="string"&&s.hasOwnProperty(u)&&s[u]++}}}else for(let l of this.plugin.recentFiles){let h=this.app.vault.getAbstractFileByPath(l.path);if(h instanceof d.TFile){let p=this.app.metadataCache.getFileCache(h),u=(c=p==null?void 0:p.frontmatter)==null?void 0:c.type;u&&typeof u=="string"&&(s[u]=(s[u]||0)+1)}}if(Object.keys(s).length>0){let l=e.createDiv({cls:"waypoint-recent-filter"});l.createSpan({cls:`waypoint-recent-pill${this.recentFilesFilter?"":" is-active"}`,text:"all"}).addEventListener("click",()=>{this.recentFilesFilter=null,this.redraw()});let p=i.length>0?Object.entries(s):Object.entries(s).sort((u,D)=>D[1]-u[1]);for(let[u,D]of p){let x=l.createSpan({cls:`waypoint-recent-pill${this.recentFilesFilter===u?" is-active":""}`});x.setText(`${u} ${D}`),x.addEventListener("click",()=>{this.recentFilesFilter=this.recentFilesFilter===u?null:u,this.redraw()})}}let n=this.plugin.recentFiles;this.recentFilesFilter&&(n=this.plugin.recentFiles.filter(l=>{var p;let h=this.app.vault.getAbstractFileByPath(l.path);if(h instanceof d.TFile){let u=this.app.metadataCache.getFileCache(h);return((p=u==null?void 0:u.frontmatter)==null?void 0:p.type)===this.recentFilesFilter}return!1}));let t=this.app.workspace.getActiveFile(),a=e.createDiv({cls:"nav-folder mod-root"}),o=a.createDiv({cls:"nav-folder-children"});for(let l of n){let h=o.createDiv({cls:"tree-item nav-file"}),p=h.createDiv({cls:"tree-item-self is-clickable nav-file-title"});p.createDiv({cls:"tree-item-inner nav-file-title-content"}).setText(l.basename);let D=p.createDiv({cls:"tree-item-spacer"}),x=p.createDiv({cls:"waypoint-recent-remove"});(0,d.setIcon)(x,"x"),x.addEventListener("click",k=>{k.stopPropagation(),this.plugin.recentFiles=this.plugin.recentFiles.filter(b=>b.path!==l.path),this.plugin.persistRecentFiles(),this.redraw()}),(0,d.setTooltip)(h,l.path),t&&l.path===t.path&&p.addClass("is-active"),p.setAttr("draggable","true"),p.addEventListener("dragstart",k=>{let b=this.app.metadataCache.getFirstLinkpathDest(l.path,"");if(b){let v=ae(this.app),f=v.dragFile(k,b);v.onDragStart(k,f)}}),p.addEventListener("mouseover",k=>{this.app.workspace.trigger("hover-link",{event:k,source:T,hoverParent:a,targetEl:h,linktext:l.path})}),p.addEventListener("contextmenu",k=>{let b=new d.Menu;b.addItem(f=>f.setSection("action").setTitle("Open in new tab").setIcon("file-plus").onClick(()=>this.focusFile(l,"tab"))),b.addItem(f=>f.setSection("action").setTitle("Add to bookmarks").setIcon("bookmark").onClick(()=>{this.plugin.addBookmark(l.path,l.basename,"file"),new d.Notice(`Bookmarked: ${l.basename}`)}));let v=this.app.vault.getAbstractFileByPath(l.path);v&&this.app.workspace.trigger("file-menu",b,v,"link-context-menu"),b.showAtPosition({x:k.clientX,y:k.clientY})}),p.addEventListener("click",k=>{let b=d.Keymap.isModEvent(k);this.focusFile(l,b)}),p.addEventListener("mousedown",k=>{k.button===1&&(k.preventDefault(),this.focusFile(l,"tab"))})}}focusFile(e,i){let s=this.app.vault.getFiles().find(n=>n.path===e.path);s?this.app.workspace.getLeaf(i).openFile(s):(new d.Notice("Cannot find file"),this.plugin.recentFiles=this.plugin.recentFiles.filter(n=>n.path!==e.path),this.plugin.persistRecentFiles(),this.redraw())}renderFavorites(){let e=this.contentEl.createDiv({cls:"waypoint-section waypoint-favorites"}),i=e.createDiv({cls:"waypoint-section-header"});i.setText("Waypoint Bookmarks");let s=i.createEl("button",{cls:"waypoint-header-more"});if((0,d.setIcon)(s,"more-horizontal"),(0,d.setTooltip)(s,"Add bookmark"),s.addEventListener("click",n=>{let t=new d.Menu;t.addItem(a=>{a.setTitle("Add current file").setIcon("file-plus").onClick(()=>{let o=this.app.workspace.getActiveFile();o&&this.plugin.addBookmark(o.path,o.basename,"file")})}),t.addItem(a=>{a.setTitle("Add as parent note").setIcon("folder-plus").onClick(()=>{let o=this.app.workspace.getActiveFile();if(!o)return;let r=this.plugin.addBookmark(o.path,o.basename,"group","");r.filePath=o.path,this.plugin.saveWaypointData(),this.redraw()})}),t.addItem(a=>{a.setTitle("New group").setIcon("folder-plus").onClick(()=>{this.plugin.addBookmark("","New Group","group","")})}),t.addSeparator(),t.addItem(a=>{a.setTitle("Add separator").setIcon("minus").onClick(()=>{this.plugin.addBookmark("","","separator")})}),t.addItem(a=>{a.setTitle("Add spacer").setIcon("space").onClick(()=>{this.plugin.addBookmark("","","spacer")})}),t.showAtPosition({x:n.clientX,y:n.clientY})}),this.plugin.waypointData.bookmarks.length===0){e.createDiv({cls:"waypoint-bookmark-item",text:"No bookmarks"});return}this.renderBookmarkList(e,this.plugin.waypointData.bookmarks,0)}renderBookmarkList(e,i,s){for(let n=0;n{c.stopPropagation(),c.preventDefault(),this.showBookmarkContextMenu(c,t)});continue}if(t.type==="spacer"){let r=e.createDiv({cls:"waypoint-bookmark-item waypoint-bookmark-spacer"});r.setAttr("draggable","true"),r.setAttr("data-bm-id",t.id),r.style.paddingLeft=`${8+s*16}px`,r.style.cursor="grab",this.attachBookmarkDragHandlers(r,e,t,!1),r.addEventListener("contextmenu",c=>{c.stopPropagation(),c.preventDefault(),this.showBookmarkContextMenu(c,t)});continue}let a=t.type==="group",o=e.createDiv({cls:`waypoint-bookmark-item${a?" waypoint-bookmark-group":""}${t.collapsed?" collapsed":""}`});if(o.setAttr("draggable","true"),o.setAttr("data-bm-id",t.id),a||(o.style.paddingLeft=`${8+s*16}px`),this.attachBookmarkDragHandlers(o,e,t,!0),a){let r=o.createDiv({cls:"waypoint-bm-icon"});t.icon&&(0,d.setIcon)(r,t.icon);let c=o.createDiv({cls:"waypoint-bm-label",text:t.label}),l=o.createDiv({cls:"waypoint-bm-chevron"});(0,d.setIcon)(l,"chevron-down"),l.addEventListener("click",p=>{p.stopPropagation(),this.plugin.updateBookmark(t.id,{collapsed:!t.collapsed})}),o.addEventListener("click",p=>{if(t.filePath){let u=this.app.vault.getFileByPath(t.filePath);if(u){let D=d.Keymap.isModEvent(p);this.app.workspace.getLeaf(D).openFile(u);return}}this.plugin.updateBookmark(t.id,{collapsed:!t.collapsed})}),o.addEventListener("mousedown",p=>{if(p.button===1&&t.filePath){p.preventDefault();let u=this.app.vault.getFileByPath(t.filePath);u&&this.app.workspace.getLeaf("tab").openFile(u)}});let h=e.createDiv({cls:`waypoint-bookmark-children${t.collapsed?" collapsed":""}`});t.children&&t.children.length>0&&this.renderBookmarkList(h,t.children,s+1)}else{let r=o.createDiv({cls:"waypoint-bm-icon"});t.icon&&(0,d.setIcon)(r,t.icon);let c=o.createDiv({cls:"waypoint-bm-label",text:t.label});if(t.children&&t.children.length>0){let l=o.createDiv({cls:"waypoint-bm-chevron"});(0,d.setIcon)(l,"chevron-down"),l.addEventListener("click",h=>{h.stopPropagation(),this.plugin.updateBookmark(t.id,{collapsed:!t.collapsed})}),t.collapsed&&(o.addClass("collapsed"),l.style.transform="rotate(-90deg)")}if((0,d.setTooltip)(o,t.filePath),o.addEventListener("click",l=>{if(t.filePath){let h=this.app.vault.getFileByPath(t.filePath);if(h){let p=d.Keymap.isModEvent(l);this.app.workspace.getLeaf(p).openFile(h)}else new d.Notice("File not found"),this.plugin.removeBookmark(t.id)}}),o.addEventListener("mousedown",l=>{if(l.button===1&&t.filePath){l.preventDefault();let h=this.app.vault.getFileByPath(t.filePath);h&&this.app.workspace.getLeaf("tab").openFile(h)}}),t.children&&t.children.length>0){let l=e.createDiv({cls:`waypoint-bookmark-children${t.collapsed?" collapsed":""}`});this.renderBookmarkList(l,t.children,s+1)}}o.addEventListener("contextmenu",r=>{r.stopPropagation(),r.preventDefault(),this.showBookmarkContextMenu(r,t)})}}showBookmarkContextMenu(e,i){let s=new d.Menu;if(i.type==="separator"||i.type==="spacer"){s.addItem(n=>n.setTitle("Remove").setIcon("trash").onClick(()=>this.plugin.removeBookmark(i.id))),s.showAtPosition({x:e.clientX,y:e.clientY});return}i.type==="file"?(s.addItem(n=>n.setTitle("Open in new tab").setIcon("file-plus").onClick(()=>{let t=this.app.vault.getFileByPath(i.filePath);t&&this.app.workspace.getLeaf("tab").openFile(t)})),s.addSeparator(),s.addItem(n=>n.setTitle("Rename").setIcon("pencil").onClick(()=>this.promptRename(i))),s.addItem(n=>n.setTitle("Change icon").setIcon("image").onClick(()=>this.promptIcon(i))),s.addSeparator(),s.addItem(n=>n.setTitle("Remove").setIcon("trash").onClick(()=>this.plugin.removeBookmark(i.id)))):i.type==="group"&&(i.filePath&&(s.addItem(n=>n.setTitle("Open in new tab").setIcon("file-plus").onClick(()=>{let t=this.app.vault.getFileByPath(i.filePath);t&&this.app.workspace.getLeaf("tab").openFile(t)})),s.addSeparator()),s.addItem(n=>n.setTitle("Rename").setIcon("pencil").onClick(()=>this.promptRename(i))),s.addItem(n=>n.setTitle("Change icon").setIcon("image").onClick(()=>this.promptIcon(i))),s.addSeparator(),s.addItem(n=>n.setTitle("Add child bookmark").setIcon("file-plus").onClick(()=>{let t=this.app.workspace.getActiveFile();if(!t){new d.Notice("No active file");return}let a={id:`bm-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:"file",label:t.basename,filePath:t.path,icon:"",children:[],collapsed:!1,indent:i.indent+1};i.children.push(a),this.plugin.saveWaypointData(),this.redraw()})),s.addItem(n=>n.setTitle("Add child note").setIcon("folder-plus").onClick(()=>{let t=this.app.workspace.getActiveFile();if(!t){new d.Notice("No active file");return}let a={id:`bm-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:"group",label:t.basename,filePath:t.path,icon:"",children:[],collapsed:!1,indent:i.indent+1};i.children.push(a),this.plugin.saveWaypointData(),this.redraw()})),s.addItem(n=>n.setTitle("New sub-group").setIcon("folder-plus").onClick(()=>{let t={id:`bm-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:"group",label:"New Group",filePath:"",icon:"",children:[],collapsed:!1,indent:i.indent+1};i.children.push(t),this.plugin.saveWaypointData(),this.redraw()})),s.addSeparator(),s.addItem(n=>n.setTitle("Remove").setIcon("trash").onClick(()=>this.plugin.removeBookmark(i.id)))),s.showAtPosition({x:e.clientX,y:e.clientY})}attachBookmarkDragHandlers(e,i,s,n){let t=()=>{e.removeClass("waypoint-bm-drop-line"),e.removeClass("waypoint-bm-drop-below"),e.removeClass("waypoint-bm-drop-into")};e.addEventListener("dragstart",a=>{this.dragId=s.id,a.dataTransfer.effectAllowed="move",a.dataTransfer.setData("text/plain",s.id),e.addClass("waypoint-bm-dragging")}),e.addEventListener("dragend",()=>{this.dragId=null,i.querySelectorAll(".waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below, .waypoint-bm-drop-into").forEach(a=>{a.removeClass("waypoint-bm-dragging"),a.removeClass("waypoint-bm-drop-line"),a.removeClass("waypoint-bm-drop-below"),a.removeClass("waypoint-bm-drop-into")})}),e.addEventListener("dragenter",a=>{a.preventDefault(),!(!this.dragId||this.dragId===s.id)&&this.showDropIndicator(e,a,n)}),e.addEventListener("dragover",a=>{a.preventDefault(),!(!this.dragId||this.dragId===s.id)&&this.showDropIndicator(e,a,n)}),e.addEventListener("dragleave",t),e.addEventListener("drop",a=>{var c,l;a.preventDefault(),this.dragId=null,t();let o=(c=a.dataTransfer)==null?void 0:c.getData("text/plain");if(!o||o===s.id)return;let r=this.dropZones.get(e);n&&(r!=null&&r.into)?s.type==="group"?this.moveBookmarkToGroup(o,s.id):this.createParentNoteAndMove(o,s.id):this.moveBookmarkToPosition(o,s.id,(l=r==null?void 0:r.above)!=null?l:!1)})}showDropIndicator(e,i,s){let n=e.parentElement;n&&n.querySelectorAll(".waypoint-bm-drop-line, .waypoint-bm-drop-below, .waypoint-bm-drop-into").forEach(o=>{o.removeClass("waypoint-bm-drop-line"),o.removeClass("waypoint-bm-drop-below"),o.removeClass("waypoint-bm-drop-into")});let t=e.getBoundingClientRect(),a=i.clientY;if(s){let o=t.top+t.height*.25,r=t.top+t.height*.75;ar?(e.addClass("waypoint-bm-drop-line"),e.addClass("waypoint-bm-drop-below"),this.dropZones.set(e,{above:!1,into:!1})):(e.addClass("waypoint-bm-drop-into"),this.dropZones.set(e,{above:!1,into:!0}))}else{let o=a{let a=t.findIndex(o=>o.id===e);if(a>=0){let[o]=t.splice(a,1);return o}for(let o of t){let r=s(o.children);if(r)return r}return null},n=s(this.plugin.waypointData.bookmarks);if(n){if(i){let t=(a,o)=>a.id===o?!0:a.children.some(r=>t(r,o));if(n.id===i||t(n,i))return}if(i){let t=o=>{for(let r of o){if(r.id===i)return r;let c=t(r.children);if(c)return c}return null},a=t(this.plugin.waypointData.bookmarks);a&&(n.indent=a.indent+1,a.children.push(n))}else n.indent=0,this.plugin.waypointData.bookmarks.push(n);this.plugin.saveWaypointData(),this.redraw()}}createParentNoteAndMove(e,i){let s=o=>{let r=o.findIndex(c=>c.id===e);if(r>=0){let[c]=o.splice(r,1);return c}for(let c of o)if(c.children){let l=s(c.children);if(l)return l}return null},n=s(this.plugin.waypointData.bookmarks);if(!n)return;let t=o=>{for(let r of o){if(r.id===i)return r;if(r.children){let c=t(r.children);if(c)return c}}return null},a=t(this.plugin.waypointData.bookmarks);a&&(n.indent=a.indent+1,a.children.push(n),this.plugin.saveWaypointData(),this.redraw())}moveBookmarkToPosition(e,i,s){let n=r=>{let c=r.findIndex(l=>l.id===e);if(c>=0){let[l]=r.splice(c,1);return{item:l,parent:r}}for(let l of r)if(l.children){let h=n(l.children);if(h.item)return h}return{item:null,parent:[]}},{item:t}=n(this.plugin.waypointData.bookmarks);if(!t)return;let a=r=>{let c=r.findIndex(l=>l.id===i);if(c>=0)return{parent:r,idx:c};for(let l of r)if(l.children){let h=a(l.children);if(h)return h}return null},o=a(this.plugin.waypointData.bookmarks);if(!o)t.indent=0,this.plugin.waypointData.bookmarks.push(t);else{let r=s?o.idx:o.idx+1;o.parent.splice(r,0,t)}this.plugin.saveWaypointData(),this.redraw()}promptRename(e){new V(this.app,e.label,i=>{i&&i.trim()&&this.plugin.updateBookmark(e.id,{label:i.trim()})}).open()}promptIcon(e){new G(this.app,e.icon,i=>{this.plugin.updateBookmark(e.id,{icon:i})}).open()}},V=class extends d.Modal{constructor(m,e,i){super(m),this.currentValue=e,this.onSubmit=i}onOpen(){this.titleEl.setText("Rename bookmark");let m=this.contentEl.createEl("input",{type:"text",value:this.currentValue});m.style.width="100%",m.style.marginBottom="12px",m.focus(),m.select();let e=this.contentEl.createDiv({cls:"modal-button-container"}),i=e.createEl("button",{text:"Cancel",cls:"mod-cta"});i.style.marginRight="8px",i.addEventListener("click",()=>this.close()),e.createEl("button",{text:"Save",cls:"mod-cta"}).addEventListener("click",()=>{this.onSubmit(m.value),this.close()}),m.addEventListener("keydown",n=>{n.key==="Enter"&&(this.onSubmit(m.value),this.close())})}onClose(){this.contentEl.empty()}},W=null;function se(){return W||(W=(async()=>{try{return await(await fetch("https://cdn.jsdelivr.net/npm/lucide-static@0.517.0/tags.json")).json()}catch(g){try{return await(await fetch("https://lucide.dev/api/tags")).json()}catch(m){return W=null,oe}}})()),W}var G=class extends d.Modal{constructor(e,i,s){super(e);this.allIcons=[];this.tagsMap={};this.loaded=!1;this.selected=i,this.onSubmit=s}async onOpen(){let e=this.contentEl;e.style.display="flex",e.style.flexDirection="column",e.style.gap="10px",this.titleEl.setText("Change icon");let i=e.createDiv({cls:"waypoint-icon-preview"});i.style.display="flex",i.style.alignItems="center",i.style.gap="10px",i.style.padding="12px 16px",i.style.borderRadius="8px",i.style.background="var(--background-secondary)",i.style.minHeight="48px";let s=i.createSpan();s.style.display="flex",this.selected&&(0,d.setIcon)(s,this.selected);let n=i.createSpan();n.style.fontWeight="var(--font-medium)",n.style.fontSize="var(--font-ui-medium)",n.setText(this.selected||"No icon");let t=e.createEl("input",{type:"text",placeholder:"Type to search (e.g. arrow, chart, home)..."});Object.assign(t.style,{width:"100%",boxSizing:"border-box",padding:"8px 10px",borderRadius:"6px",border:"1px solid var(--background-modifier-border)",background:"var(--background-primary)",color:"var(--text-normal)",fontSize:"var(--font-ui-medium)"}),t.focus();let a=e.createDiv({cls:"waypoint-icon-grid"});a.style.display="grid",a.style.gridTemplateColumns="repeat(auto-fill, minmax(52px, 1fr))",a.style.gap="4px",a.style.maxHeight="320px",a.style.overflowY="auto",a.style.padding="2px 0";let o=e.createDiv();o.style.display="flex",o.style.justifyContent="space-between",o.style.alignItems="center",o.style.fontSize="var(--font-ui-smaller)",o.style.color="var(--text-muted)",o.style.padding="0 4px";let r=o.createSpan();r.setText("Loading\u2026"),this.loadIcons().then(()=>{this.loaded=!0,r.setText(this.allIcons.length+" icons"),l(t.value)});let c,l=x=>{if(a.empty(),!this.loaded){a.createDiv({text:"Loading\u2026"});return}let k=x.toLowerCase().trim(),b=k?this.allIcons.filter(v=>{if(v.includes(k))return!0;let f=this.tagsMap[v];return f?f.some(C=>C.includes(k)):!1}).slice(0,80):this.allIcons.slice(0,80);if(b.length===0){let v=a.createDiv();v.style.gridColumn="1 / -1",v.style.textAlign="center",v.style.color="var(--text-muted)",v.style.padding="20px",v.setText('No icons match "'+x+'"');return}for(let v of b){let f=a.createDiv();f.setAttr("data-icon",v),f.style.display="flex",f.style.alignItems="center",f.style.justifyContent="center",f.style.aspectRatio="1",f.style.borderRadius="6px",f.style.cursor="var(--cursor-link, pointer)",f.style.transition="background 80ms",f.setAttr("title",v),v===this.selected?(f.style.background="var(--interactive-accent)",f.style.color="var(--text-on-accent)"):f.style.color="var(--text-muted)";let C=f.createSpan();C.style.display="flex",(0,d.setIcon)(C,v),f.addEventListener("mouseenter",()=>{v!==this.selected&&(f.style.background="var(--background-modifier-hover)")}),f.addEventListener("mouseleave",()=>{v!==this.selected&&(f.style.background="")}),f.addEventListener("click",()=>{this.selected=v,l(x),s.empty(),(0,d.setIcon)(s,v),n.setText(v),a.querySelectorAll("div[data-icon]").forEach($=>{let S=$;S.getAttr("data-icon")===v?(S.style.background="var(--interactive-accent)",S.style.color="var(--text-on-accent)"):(S.style.background="",S.style.color="var(--text-muted)")})})}r.setText(b.length+" of "+this.allIcons.length+" icons")};t.addEventListener("input",()=>{window.clearTimeout(c),c=window.setTimeout(()=>l(t.value),60)}),t.addEventListener("keydown",x=>{x.key==="Escape"&&this.close()});let h=e.createDiv({cls:"modal-button-container"});h.createEl("button",{text:"No icon",cls:"waypoint-icon-clear"}).addEventListener("click",()=>{this.onSubmit(""),this.close()}),h.createEl("button",{text:"Cancel"}).addEventListener("click",()=>this.close());let D=h.createEl("button",{text:"Save",cls:"mod-cta"});D.style.marginLeft="8px",D.addEventListener("click",()=>{this.onSubmit(this.selected),this.close()})}onClose(){this.contentEl.empty()}async loadIcons(){let e=await se();this.tagsMap=e,this.allIcons=Object.keys(e).sort()}},oe={file:[],folder:[],star:[],heart:[],bookmark:[],flag:[],pin:[],tag:[],book:[],"book-open":[],library:[],calendar:[],"calendar-days":[],clock:[],home:[],inbox:[],mail:[],search:[],settings:[],cog:[],user:[],users:[],zap:[],sparkles:[],target:[],link:[],globe:[],edit:[],pencil:[],anchor:[],award:[],bell:[],"bell-ring":[],brain:[],briefcase:[],camera:[],"chart-bar":[],"chart-line":[],"chart-pie":[],check:[],"check-circle":[],"chevron-down":[],"chevron-left":[],"chevron-right":[],"chevron-up":[],circle:[],clipboard:[],code:[],command:[],compass:[],copy:[],"credit-card":[],crown:[],database:[],download:[],"external-link":[],eye:[],"eye-off":[],"file-text":[],filter:[],fingerprint:[],flashlight:[],"folder-open":[],"folder-plus":[],gift:[],"git-branch":[],"git-commit":[],"git-merge":[],"git-pull-request":[],github:[],grid:[],hash:[],headphones:[],image:[],info:[],key:[],layers:[],layout:[],"life-buoy":[],"link-2":[],list:[],loader:[],lock:[],"log-in":[],"log-out":[],map:[],"map-pin":[],maximize:[],megaphone:[],menu:[],"message-circle":[],"message-square":[],mic:[],minimize:[],moon:[],"more-horizontal":[],"more-vertical":[],"mouse-pointer":[],move:[],music:[],navigation:[],"navigation-2":[],package:[],palette:[],paperclip:[],pause:[],phone:[],play:[],plus:[],"plus-circle":[],power:[],printer:[],radio:[],"refresh-cw":[],repeat:[],"rotate-ccw":[],"rotate-cw":[],rss:[],save:[],scissors:[],screen:[],send:[],server:[],share:[],"share-2":[],shield:[],"shield-off":[],"shopping-bag":[],"shopping-cart":[],shuffle:[],sidebar:[],slack:[],slash:[],sliders:[],smartphone:[],smile:[],speaker:[],square:[],"stop-circle":[],sun:[],sunrise:[],sunset:[],swords:[],table:[],tablet:[],terminal:[],thermometer:[],"thumbs-down":[],"thumbs-up":[],"toggle-left":[],"toggle-right":[],tool:[],trash:[],"trash-2":[],trello:[],"trending-down":[],"trending-up":[],triangle:[],truck:[],tv:[],twitter:[],type:[],umbrella:[],unlock:[],upload:[],"user-check":[],"user-minus":[],"user-plus":[],"user-x":[],video:[],"video-off":[],voicemail:[],volume:[],"volume-1":[],"volume-2":[],"volume-x":[],watch:[],wifi:[],"wifi-off":[],wind:[],x:[],"x-circle":[],"x-square":[],youtube:[],"zap-off":[],"zoom-in":[],"zoom-out":[],"arrow-down":[],"arrow-left":[],"arrow-right":[],"arrow-up":[],airplay:[],"alarm-clock":[],archive:[],armchair:[],atom:[],baby:[],backpack:[],badge:[],"badge-check":[],ban:[],banknote:[],barcode:[],bath:[],battery:[],"battery-charging":[],beer:[],bike:[],bird:[],bluetooth:[],bolt:[],bone:[],"bookmark-plus":[],bot:[],box:[],bug:[],building:[],bus:[],cake:[],calculator:[],car:[],"clipboard-check":[],cloud:[],"cloud-download":[],"cloud-lightning":[],"cloud-rain":[],"cloud-sun":[],"cloud-upload":[],clover:[],coffee:[],coins:[],contact:[],cookie:[],"corner-down-left":[],"corner-down-right":[],"corner-up-left":[],"corner-up-right":[],crosshair:[],"dice-1":[],"dice-6":[],"dollar-sign":[],"door-open":[],drama:[],droplet:[],drum:[],egg:[],equal:[],euro:[],factory:[],fan:[],feather:[],film:[],fish:[],flame:[],flask:[],flower:[],frown:[],fuel:[],gamepad:[],gauge:[],gem:[],ghost:[],glasses:[],"graduation-cap":[],hammer:[],"hard-drive":[],haze:[],"help-circle":[],"ice-cream":[],infinity:[],italic:[],"japanese-yen":[],keyboard:[],knife:[],lamp:[],landmark:[],languages:[],laptop:[],laugh:[],leaf:[],lightbulb:[],"list-plus":[],magnet:[],"mail-plus":[],meh:[],microscope:[],milestone:[],"minimize-2":[],monitor:[],mountain:[],mouse:[],network:[],newspaper:[],"package-check":[],"package-search":[],"paint-bucket":[],parking:[],"party-popper":[],"pen-tool":[],percent:[],"person-standing":[],"picture-in-picture-2":[],plane:[],plug:[],podcast:[],pointer:[],"pound-sterling":[],puzzle:[],"qr-code":[],rabbit:[],radar:[],rainbow:[],rocket:[],"roller-coaster":[],route:[],ruler:[],sailboat:[],scale:[],scan:[],school:[],ship:[],shirt:[],"shopping-basket":[],shovel:[],sigma:[],siren:[],skull:[],snowflake:[],soup:[],space:[],sparkle:[],stamp:[],store:[],subscript:[],superscript:[],syringe:[],tent:[],"tent-tree":[],"test-tube":[],theater:[],timer:[],train:[],"tree-deciduous":[],"tree-pine":[],trophy:[],typing:[],utensils:[],vibrate:[],wallet:[],wand:[],warehouse:[],waves:[],webcam:[],wheat:[],wine:[],wrench:[]};function H(g,m,e){return!g||!m?null:g===m?e:g.startsWith(m+"/")?e+g.slice(m.length):null}var O=class extends y.Plugin{constructor(){super(...arguments);this.recentFiles=[];this.savePromise=Promise.resolve();this.markdownBasenames=new Set}async onload(){console.debug("Waypoint: loading plugin v"+this.manifest.version);let e=await this.loadData();this.applySettings(e),this.applyWaypointData(e),this.registerView(T,a=>new L(a,this)),this.addSettingTab(new R(this.app,this,this.settings,()=>{this.enforceRecentFilesLimit(),this.redrawAll()})),this.addCommand({id:"waypoint-open-view",name:"Open Waypoint sidebar",callback:async()=>{let a=this.app.workspace.getLeavesOfType(T);if(a.length>0)await this.app.workspace.revealLeaf(a[0]);else{let o=this.app.workspace.getLeftLeaf(!1);o&&(await o.setViewState({type:T}),await this.app.workspace.revealLeaf(o))}}}),this.addCommand({id:"waypoint-add-bookmark",name:"Add current file as Waypoint bookmark",callback:async()=>{let a=this.app.workspace.getActiveFile();if(!a){new y.Notice("No active file");return}this.addBookmark(a.path,a.basename,"file"),new y.Notice(`Bookmarked: ${a.basename}`)}}),this.addCommand({id:"waypoint-go-to-daily",name:"Go to daily note",hotkeys:[{modifiers:["Mod","Shift","Alt"],key:"d"}],callback:async()=>{await this.openPeriodNote("day",(0,y.moment)())}}),this.addCommand({id:"waypoint-go-to-weekly",name:"Go to weekly note",hotkeys:[{modifiers:["Mod","Shift","Alt"],key:"w"}],callback:async()=>{await this.openPeriodNote("week",(0,y.moment)())}}),this.addCommand({id:"waypoint-go-to-monthly",name:"Go to monthly note",hotkeys:[{modifiers:["Mod","Shift","Alt"],key:"m"}],callback:async()=>{await this.openPeriodNote("month",(0,y.moment)())}}),this.addCommand({id:"waypoint-go-to-quarterly",name:"Go to quarterly note",hotkeys:[{modifiers:["Mod","Shift","Alt"],key:"q"}],callback:async()=>{await this.openPeriodNote("quarter",(0,y.moment)())}}),this.addCommand({id:"waypoint-go-to-yearly",name:"Go to yearly note",hotkeys:[{modifiers:["Mod","Shift","Alt"],key:"y"}],callback:async()=>{await this.openPeriodNote("year",(0,y.moment)())}});let i=["next","prev"],s=["daily","weekly","monthly","quarterly","yearly"],n={next:"Next",prev:"Previous"};for(let a of s)for(let o of i){let r=`waypoint-go-to-${o}-${a}`,c=`${n[o]} ${a} note`;this.addCommand({id:r,name:c,callback:async()=>{await this.navigatePeriodNote(o)}})}this.registerEvent(this.app.workspace.on("file-open",a=>{a&&this.onFileOpen(a)})),this.registerEvent(this.app.vault.on("create",a=>this.onVaultCreate(a))),this.registerEvent(this.app.vault.on("delete",a=>this.onVaultDelete(a))),this.registerEvent(this.app.vault.on("rename",(a,o)=>this.onRename(a,o))),this.registerEvent(this.app.vault.on("modify",a=>this.onFileModify(a))),this.app.workspace.onLayoutReady(()=>{if(this.buildMarkdownIndex(),this.app.workspace.getLeavesOfType(T).length===0){let o=this.app.workspace.getLeftLeaf(!1);o&&o.setViewState({type:T})}else this.broadcastRedraw()});let t=new Date().toDateString();this.registerInterval(window.setInterval(()=>{let a=new Date().toDateString();a!==t&&(t=a,this.redrawAll())},6e5))}async onunload(){this.app.workspace.detachLeavesOfType(T)}applySettings(e){let i=(e==null?void 0:e.settings)||{};this.settings=Object.assign({},P,i),this.settings.recentFiles=Object.assign({},P.recentFiles,i.recentFiles||{}),this.settings.calendar=Object.assign({},P.calendar,i.calendar||{}),this.settings.display=Object.assign({},P.display,i.display||{});for(let s of re)this.settings[s]=Object.assign({},P[s],i[s]||{})}applyWaypointData(e){let i=(e==null?void 0:e.waypointData)||{};this.waypointData={bookmarks:Array.isArray(i.bookmarks)?i.bookmarks:[],recentFiles:Array.isArray(i.recentFiles)?i.recentFiles:[]},this.recentFiles=this.waypointData.recentFiles,this.recentFiles.length>this.settings.recentFiles.maxItems&&(this.recentFiles=this.recentFiles.slice(0,this.settings.recentFiles.maxItems),this.waypointData.recentFiles=this.recentFiles)}persistAll(){let e=this.savePromise.then(()=>(this.waypointData.recentFiles=this.recentFiles,this.saveData({settings:this.settings,waypointData:this.waypointData})));return this.savePromise=e.catch(()=>{}),e}async saveSettings(){await this.persistAll()}async saveWaypointData(){await this.persistAll()}enforceRecentFilesLimit(){this.recentFiles.length>this.settings.recentFiles.maxItems&&(this.recentFiles=this.recentFiles.slice(0,this.settings.recentFiles.maxItems),this.persistRecentFiles())}persistRecentFiles(){this.waypointData.recentFiles=this.recentFiles,window.clearTimeout(this.recentFilesSaveTimer),this.recentFilesSaveTimer=window.setTimeout(()=>{this.saveWaypointData()},300)}onFileOpen(e){this.settings.recentFiles.updateOn==="file-open"&&this.addToRecentFiles(e)}onFileModify(e){this.settings.recentFiles.updateOn==="file-edit"&&e instanceof y.TFile&&(this.recentFiles.length>0&&this.recentFiles[0].path===e.path||this.addToRecentFiles(e))}addToRecentFiles(e){this.isOmittedFromRecentFiles(e)||(this.recentFiles=this.recentFiles.filter(i=>i.path!==e.path),this.recentFiles.unshift({path:e.path,basename:e.basename}),this.recentFiles.length>this.settings.recentFiles.maxItems&&(this.recentFiles=this.recentFiles.slice(0,this.settings.recentFiles.maxItems)),this.persistRecentFiles(),this.broadcastRedraw())}isOmittedFromRecentFiles(e){for(let a of this.settings.recentFiles.omittedPaths)try{if(new RegExp(a).test(e.path))return!0}catch(o){}let i=this.settings.recentFiles.omittedTags;if(i.length===0)return!1;let s=this.app.metadataCache.getFileCache(e),n=(s?(0,y.getAllTags)(s):null)||[];if(n.length===0)return!1;let t=n.map(a=>a.replace(/^#/,""));for(let a of i)try{let o=new RegExp(a);if(t.some(r=>o.test(r)))return!0}catch(o){}return!1}onRename(e,i){let s=this.syncIndexForRename(e,i),n=!1;for(let a of this.recentFiles){let o=H(a.path,i,e.path);o!==null&&(a.path=o,a.basename=Z(o),n=!0)}let t=a=>{for(let o of a){if(o.filePath){let r=H(o.filePath,i,e.path);r!==null&&(o.filePath=r,n=!0)}o.children&&t(o.children)}};t(this.waypointData.bookmarks),n&&(this.waypointData.recentFiles=this.recentFiles,this.persistAll()),(n||s)&&this.broadcastRedraw()}onVaultCreate(e){e instanceof y.TFile&&e.extension==="md"&&this.markdownBasenames.add(e.basename),this.broadcastRedraw()}onVaultDelete(e){e instanceof y.TFile&&e.extension==="md"&&this.removeFromMarkdownIndex(e.basename,e.path),this.broadcastRedraw()}buildMarkdownIndex(){this.markdownBasenames.clear();for(let e of this.app.vault.getMarkdownFiles())this.markdownBasenames.add(e.basename)}removeFromMarkdownIndex(e,i){return!this.markdownBasenames.has(e)||this.app.vault.getMarkdownFiles().some(n=>n.basename===e&&n.path!==i)?!1:(this.markdownBasenames.delete(e),!0)}syncIndexForRename(e,i){if(!(e instanceof y.TFile))return!1;let s=!1,n=Z(i);return i.toLowerCase().endsWith(".md")&&(n!==e.basename||e.extension!=="md")&&(s=this.removeFromMarkdownIndex(n,i)),e.extension==="md"&&!this.markdownBasenames.has(e.basename)&&(this.markdownBasenames.add(e.basename),s=!0),s}hasNoteForDate(e){return this.markdownBasenames.has(e)}addBookmark(e,i,s="file",n=""){let a={id:`bm-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:s,label:i,filePath:s==="file"?e:"",icon:n,children:[],collapsed:!1,indent:0};return this.waypointData.bookmarks.push(a),this.saveWaypointData(),this.broadcastRedraw(),a}removeBookmark(e){let i=s=>{let n=s.findIndex(t=>t.id===e);if(n>=0)return s.splice(n,1),!0;for(let t of s)if(t.children&&i(t.children))return!0;return!1};i(this.waypointData.bookmarks),this.saveWaypointData(),this.broadcastRedraw()}updateBookmark(e,i){let s=t=>{for(let a of t){if(a.id===e)return a;if(a.children){let o=s(a.children);if(o)return o}}return null},n=s(this.waypointData.bookmarks);n&&(Object.assign(n,i),this.saveWaypointData(),this.broadcastRedraw())}async openPeriodNote(e,i,s){let n=Y[e],t=this.settings[n.key],a=i.format(t.nameFormat)+".md",o=t.folder?`${t.folder}/${a}`:a,r=this.app.vault.getFileByPath(o);if(!r&&(r=await this.createPeriodNote(o,t,i,n.label),!r))return;await(s||this.app.workspace.getLeaf(!1)).openFile(r)}async createPeriodNote(e,i,s,n){let t=n.toLowerCase(),a=e.lastIndexOf("/"),o=a<0?"":e.slice(0,a);try{await this.ensureFolderExists(o)}catch(p){return new y.Notice(`Waypoint: could not create the folder "${o}" for the ${t} note. -${j(p)} -Check Settings \u2192 Waypoint Sidebar \u2192 Periodic Notes.`,B),null}let r=i.templateFile,c=this.resolveTemplateFile(r),l;if(c)try{l=await this.app.vault.read(c)}catch(p){return new y.Notice(`Waypoint: could not read the template "${c.path}" for the ${t} note. -`+j(p),B),null}else l=`--- -type: ${i.typeProperty} -date: ${s.format("YYYY-MM-DD")} +project`),n.setValue(this.settings.recentFiles.filterTags.join(` +`)),n.inputEl.onblur=()=>{this.settings.recentFiles.filterTags=n.getValue().split(` +`).filter(s=>s.trim()),this.saveAndRefresh()}})}renderDisplayTab(e){new k.Setting(e).setHeading().setName("Bookmarks"),this.addSliderSetting(e,"Row size","Height of bookmark items.",this.settings.display,"rowSize",18,40,1,"px"),this.addSliderSetting(e,"Row spacing","Gap between bookmark items.",this.settings.display,"rowSpacing",0,12,1,"px"),this.addSliderSetting(e,"Indent size","Indent per nesting depth.",this.settings.display,"indentSize",8,32,2,"px"),this.addSliderSetting(e,"Font size","Label font size.",this.settings.display,"fontSize",10,18,1,"px"),this.addSliderSetting(e,"Icon size","Bookmark icon size.",this.settings.display,"iconSize",12,24,1,"px"),new k.Setting(e).setHeading().setName("Calendar"),this.addSliderSetting(e,"Cell size","Height of calendar day cells.",this.settings.display,"calendarCellSize",20,48,2,"px"),new k.Setting(e).addButton(a=>a.setButtonText("Reset to defaults").onClick(()=>{this.settings.display={...E.display},this.saveAndRefresh(),this.display()}))}addSliderSetting(e,a,i,t,n,s,o,r,c){let l=new k.Setting(e).setName(a).setDesc(`${i} (${t[n]}${c})`);l.addSlider(h=>{h.setLimits(s,o,r).setValue(t[n]).setDynamicTooltip().onChange(u=>{t[n]=u,l.setDesc(`${i} (${u}${c})`),this.saveAndRefresh()})})}async saveAndRefresh(){await this.plugin.saveSettings(),this.onSettingsChange()}renderAboutTab(e){let a=this.plugin.manifest.version,i=e.createDiv();i.style.display="flex",i.style.alignItems="center",i.style.gap="12px",i.style.marginBottom="16px";let t=i.createDiv();t.style.display="flex",t.style.alignItems="center",t.style.justifyContent="center",t.style.width="48px",t.style.height="48px",t.style.borderRadius="12px",t.style.background="var(--interactive-accent)",t.style.color="var(--text-on-accent)",t.style.fontSize="24px",(0,k.setIcon)(t,"compass");let n=i.createDiv(),s=n.createEl("h2",{text:"Waypoint Sidebar"});s.style.margin="0",s.style.lineHeight="1.2";let o=n.createDiv({text:`v${a}`});o.style.color="var(--text-muted)",o.style.fontSize="var(--font-ui-small)";let r=e.createDiv();r.style.marginBottom="20px",r.style.lineHeight="1.6",r.style.color="var(--text-normal)",r.innerHTML=["

Waypoint is a sidebar plugin that brings three essential panels into one view:

",'
    ',"
  • Calendar \u2014 a monthly grid for your periodic notes (daily, weekly, monthly, quarterly, yearly). Click any day, week, or month to open or create the corresponding note.
  • ","
  • Recent Files \u2014 a list of recently opened files with type filtering, drag-and-drop, and right-click actions.
  • ","
  • Bookmarks \u2014 custom bookmarks with icons, groups, nesting, and drag-and-drop reordering. Separate from Obsidian's native bookmarks.
  • ","
",'

Made by Olivier. Licensed under MIT.

'].join(` +`)}};var m=require("obsidian");var C=require("obsidian");function ie(p,d,e){let a=(0,C.moment)({year:p,month:d,day:1}),i=(0,C.moment)(a).endOf("month"),t=(0,C.moment)(a).subtract((a.day()-e+7)%7,"days"),n=(0,C.moment)().startOf("day"),s=[],o=(0,C.moment)(t);for(;o.isBefore(i)||o.month()===d;){let r=[];for(let c=0;c<7;c++)r.push({date:(0,C.moment)(o),dayOfMonth:o.date(),isToday:o.isSame(n,"day"),isCurrentMonth:o.month()===d,isoWeekNumber:o.isoWeek()}),o.add(1,"day");if(s.push({weekNumber:r[0].isoWeekNumber,days:r}),s.length>=6)break}return s}var T="waypoint-view";function ue(p){return p.dragManager}var B=class extends m.ItemView{constructor(e,a){super(e);this.redraw=()=>{this.contentEl.empty(),this.contentEl.addClass("waypoint-view");let e=this.plugin.settings.display;this.contentEl.style.setProperty("--wp-row-size",e.rowSize+"px"),this.contentEl.style.setProperty("--wp-row-spacing",e.rowSpacing+"px"),this.contentEl.style.setProperty("--wp-indent-size",e.indentSize+"px"),this.contentEl.style.setProperty("--wp-font-size",e.fontSize+"px"),this.contentEl.style.setProperty("--wp-icon-size",e.iconSize+"px"),this.contentEl.style.setProperty("--wp-cal-cell-size",e.calendarCellSize+"px"),this.renderFavorites(),this.renderRecentFiles(),this.renderCalendar()};this.currentDisplayMonth=(0,m.moment)().month();this.currentDisplayYear=(0,m.moment)().year();this.dragId=null;this.dropZones=new WeakMap;this.recentFilesFilter=null;this.plugin=a}getViewType(){return T}getDisplayText(){return"Waypoint"}getIcon(){return"compass"}async onOpen(){this.redraw()}async onClose(){}renderCalendar(){let e=this.contentEl.createDiv({cls:"waypoint-section"});e.createDiv({cls:"waypoint-section-header",text:"Calendar"});let a=e.createDiv({cls:"waypoint-calendar"}),i=(0,m.moment)(),t=(0,m.moment)({year:this.currentDisplayYear,month:this.currentDisplayMonth,day:1}),n=a.createDiv({cls:"waypoint-calendar-top"}),s=n.createDiv({cls:"waypoint-calendar-breadcrumb"}),o=t.format("[Q]Q"),r=s.createSpan({cls:"waypoint-clickable",text:o});r.addEventListener("click",()=>{this.plugin.openPeriodNote("quarter",t)}),r.addEventListener("mousedown",D=>{D.button===1&&(D.preventDefault(),this.plugin.openPeriodNote("quarter",t,this.app.workspace.getLeaf("tab")))});let c=t.format("MMMM"),l=s.createSpan({cls:"waypoint-clickable",text:c});l.addEventListener("click",()=>{this.plugin.openPeriodNote("month",t)}),l.addEventListener("mousedown",D=>{D.button===1&&(D.preventDefault(),this.plugin.openPeriodNote("month",t,this.app.workspace.getLeaf("tab")))});let h=t.format("YYYY"),u=s.createSpan({cls:"waypoint-clickable",text:h});u.addEventListener("click",()=>{this.plugin.openPeriodNote("year",t)}),u.addEventListener("mousedown",D=>{D.button===1&&(D.preventDefault(),this.plugin.openPeriodNote("year",t,this.app.workspace.getLeaf("tab")))});let g=n.createDiv({cls:"waypoint-calendar-today-group"}),F=g.createEl("button",{cls:"waypoint-calendar-nav-btn"});(0,m.setIcon)(F,"chevron-left"),F.addEventListener("click",()=>this.navigateMonth(-1)),g.createEl("button",{cls:"waypoint-calendar-today-btn",text:"Today"}).addEventListener("click",()=>{this.currentDisplayMonth=(0,m.moment)().month(),this.currentDisplayYear=(0,m.moment)().year(),this.redraw()});let w=g.createEl("button",{cls:"waypoint-calendar-nav-btn"});(0,m.setIcon)(w,"chevron-right"),w.addEventListener("click",()=>this.navigateMonth(1));let b=a.createEl("table"),y=b.createEl("thead").createEl("tr");y.createEl("th",{text:""});let L=["sun","mon","tue","wed","thu","fri","sat"],V=this.plugin.settings.calendar.firstDayOfWeek;for(let D=0;D<7;D++){let Y=(V+D)%7;y.createEl("th",{text:L[Y]})}let M=b.createEl("tbody"),oe=ie(this.currentDisplayYear,this.currentDisplayMonth,this.plugin.settings.calendar.firstDayOfWeek);for(let D of oe){let Y=M.createEl("tr"),G=Y.createEl("td",{cls:"waypoint-weeknum"});G.setText(String(D.weekNumber));let Z=D.days[0].date;G.addEventListener("click",()=>{this.plugin.openPeriodNote("week",Z)}),G.addEventListener("mousedown",S=>{S.button===1&&(S.preventDefault(),this.plugin.openPeriodNote("week",Z,this.app.workspace.getLeaf("tab")))});for(let S of D.days){let I=Y.createEl("td",{cls:"waypoint-day"});if(I.setText(String(S.dayOfMonth)),S.isCurrentMonth||I.addClass("other-month"),S.isToday&&I.addClass("today"),this.plugin.settings.calendar.showNoteIndicators){let P=S.date.format("YYYY-MM-DD");this.plugin.hasNoteForDate(P)&&I.addClass("has-note")}I.addEventListener("click",()=>{this.plugin.openPeriodNote("day",S.date)}),I.addEventListener("mousedown",P=>{P.button===1&&(P.preventDefault(),this.plugin.openPeriodNote("day",S.date,this.app.workspace.getLeaf("tab")))}),I.addEventListener("contextmenu",P=>{P.preventDefault(),P.stopPropagation(),this.showDayContextMenu(P,S.date)})}}}navigateMonth(e){let a=(0,m.moment)({year:this.currentDisplayYear,month:this.currentDisplayMonth}).add(e,"month");this.currentDisplayMonth=a.month(),this.currentDisplayYear=a.year(),this.redraw()}showDayContextMenu(e,a){let i=new m.Menu;i.addItem(t=>t.setTitle(a.format("dddd, MMMM D, YYYY")).setIsLabel(!0));for(let t of this.plugin.findDateSystemNotes(a)){let n=t.multiple||t.notes.length===0;if(t.notes.length+(n?1:0)===0)continue;i.addSeparator();for(let o of t.notes)i.addItem(r=>r.setTitle(o.label).setIcon(t.system.icon).onClick(c=>this.focusFile(o.file,m.Keymap.isModEvent(c))));if(!n)continue;let s=t.system.name.toLowerCase();i.addItem(o=>o.setTitle(t.multiple?`New ${s} note\u2026`:`New ${s} note`).setIcon("plus").onClick(r=>this.createDateSystemNote(t,a,m.Keymap.isModEvent(r))))}i.showAtPosition({x:e.clientX,y:e.clientY})}createDateSystemNote(e,a,i){if(!e.multiple){this.plugin.openDateSystemNote(e.system,a,{leaf:i?this.app.workspace.getLeaf(i):void 0});return}let t=e.system.name;new z(this.app,{title:`New ${t.toLowerCase()} note`,placeholder:`${t} with\u2026`,cta:"Create"},n=>{this.plugin.openDateSystemNote(e.system,a,{title:n,leaf:i?this.app.workspace.getLeaf(i):void 0})}).open()}renderRecentFiles(){var r,c;let e=this.contentEl.createDiv({cls:"waypoint-section waypoint-recent-files"});if(e.createDiv({cls:"waypoint-section-header",text:"Recent Files"}),this.plugin.recentFiles.length===0){e.createDiv({cls:"nav-file",text:"No recent files"});return}let a=this.plugin.settings.recentFiles.filterTags||[],i={};if(a.length>0){for(let l of a)i[l]=0;for(let l of this.plugin.recentFiles){let h=this.app.vault.getAbstractFileByPath(l.path);if(h instanceof m.TFile){let u=this.app.metadataCache.getFileCache(h),g=(r=u==null?void 0:u.frontmatter)==null?void 0:r.type;g&&typeof g=="string"&&i.hasOwnProperty(g)&&i[g]++}}}else for(let l of this.plugin.recentFiles){let h=this.app.vault.getAbstractFileByPath(l.path);if(h instanceof m.TFile){let u=this.app.metadataCache.getFileCache(h),g=(c=u==null?void 0:u.frontmatter)==null?void 0:c.type;g&&typeof g=="string"&&(i[g]=(i[g]||0)+1)}}if(Object.keys(i).length>0){let l=e.createDiv({cls:"waypoint-recent-filter"});l.createSpan({cls:`waypoint-recent-pill${this.recentFilesFilter?"":" is-active"}`,text:"all"}).addEventListener("click",()=>{this.recentFilesFilter=null,this.redraw()});let u=a.length>0?Object.entries(i):Object.entries(i).sort((g,F)=>F[1]-g[1]);for(let[g,F]of u){let x=l.createSpan({cls:`waypoint-recent-pill${this.recentFilesFilter===g?" is-active":""}`});x.setText(`${g} ${F}`),x.addEventListener("click",()=>{this.recentFilesFilter=this.recentFilesFilter===g?null:g,this.redraw()})}}let t=this.plugin.recentFiles;this.recentFilesFilter&&(t=this.plugin.recentFiles.filter(l=>{var u;let h=this.app.vault.getAbstractFileByPath(l.path);if(h instanceof m.TFile){let g=this.app.metadataCache.getFileCache(h);return((u=g==null?void 0:g.frontmatter)==null?void 0:u.type)===this.recentFilesFilter}return!1}));let n=this.app.workspace.getActiveFile(),s=e.createDiv({cls:"nav-folder mod-root"}),o=s.createDiv({cls:"nav-folder-children"});for(let l of t){let h=o.createDiv({cls:"tree-item nav-file"}),u=h.createDiv({cls:"tree-item-self is-clickable nav-file-title"});u.createDiv({cls:"tree-item-inner nav-file-title-content"}).setText(l.basename);let F=u.createDiv({cls:"tree-item-spacer"}),x=u.createDiv({cls:"waypoint-recent-remove"});(0,m.setIcon)(x,"x"),x.addEventListener("click",w=>{w.stopPropagation(),this.plugin.recentFiles=this.plugin.recentFiles.filter(b=>b.path!==l.path),this.plugin.persistRecentFiles(),this.redraw()}),(0,m.setTooltip)(h,l.path),n&&l.path===n.path&&u.addClass("is-active"),u.setAttr("draggable","true"),u.addEventListener("dragstart",w=>{let b=this.app.metadataCache.getFirstLinkpathDest(l.path,"");if(b){let v=ue(this.app),y=v.dragFile(w,b);v.onDragStart(w,y)}}),u.addEventListener("mouseover",w=>{this.app.workspace.trigger("hover-link",{event:w,source:T,hoverParent:s,targetEl:h,linktext:l.path})}),u.addEventListener("contextmenu",w=>{let b=new m.Menu;b.addItem(y=>y.setSection("action").setTitle("Open in new tab").setIcon("file-plus").onClick(()=>this.focusFile(l,"tab"))),b.addItem(y=>y.setSection("action").setTitle("Add to bookmarks").setIcon("bookmark").onClick(()=>{this.plugin.addBookmark(l.path,l.basename,"file"),new m.Notice(`Bookmarked: ${l.basename}`)}));let v=this.app.vault.getAbstractFileByPath(l.path);v&&this.app.workspace.trigger("file-menu",b,v,"link-context-menu"),b.showAtPosition({x:w.clientX,y:w.clientY})}),u.addEventListener("click",w=>{let b=m.Keymap.isModEvent(w);this.focusFile(l,b)}),u.addEventListener("mousedown",w=>{w.button===1&&(w.preventDefault(),this.focusFile(l,"tab"))})}}focusFile(e,a){let i=this.app.vault.getFiles().find(t=>t.path===e.path);i?this.app.workspace.getLeaf(a).openFile(i):(new m.Notice("Cannot find file"),this.plugin.recentFiles=this.plugin.recentFiles.filter(t=>t.path!==e.path),this.plugin.persistRecentFiles(),this.redraw())}renderFavorites(){let e=this.contentEl.createDiv({cls:"waypoint-section waypoint-favorites"}),a=e.createDiv({cls:"waypoint-section-header"});a.setText("Waypoint Bookmarks");let i=a.createEl("button",{cls:"waypoint-header-more"});if((0,m.setIcon)(i,"more-horizontal"),(0,m.setTooltip)(i,"Add bookmark"),i.addEventListener("click",t=>{let n=new m.Menu;n.addItem(s=>{s.setTitle("Add current file").setIcon("file-plus").onClick(()=>{let o=this.app.workspace.getActiveFile();o&&this.plugin.addBookmark(o.path,o.basename,"file")})}),n.addItem(s=>{s.setTitle("Add as parent note").setIcon("folder-plus").onClick(()=>{let o=this.app.workspace.getActiveFile();if(!o)return;let r=this.plugin.addBookmark(o.path,o.basename,"group","");r.filePath=o.path,this.plugin.saveWaypointData(),this.redraw()})}),n.addItem(s=>{s.setTitle("New group").setIcon("folder-plus").onClick(()=>{this.plugin.addBookmark("","New Group","group","")})}),n.addSeparator(),n.addItem(s=>{s.setTitle("Add separator").setIcon("minus").onClick(()=>{this.plugin.addBookmark("","","separator")})}),n.addItem(s=>{s.setTitle("Add spacer").setIcon("space").onClick(()=>{this.plugin.addBookmark("","","spacer")})}),n.showAtPosition({x:t.clientX,y:t.clientY})}),this.plugin.waypointData.bookmarks.length===0){e.createDiv({cls:"waypoint-bookmark-item",text:"No bookmarks"});return}this.renderBookmarkList(e,this.plugin.waypointData.bookmarks,0)}renderBookmarkList(e,a,i){for(let t=0;t{c.stopPropagation(),c.preventDefault(),this.showBookmarkContextMenu(c,n)});continue}if(n.type==="spacer"){let r=e.createDiv({cls:"waypoint-bookmark-item waypoint-bookmark-spacer"});r.setAttr("draggable","true"),r.setAttr("data-bm-id",n.id),r.style.paddingLeft=`${8+i*16}px`,r.style.cursor="grab",this.attachBookmarkDragHandlers(r,e,n,!1),r.addEventListener("contextmenu",c=>{c.stopPropagation(),c.preventDefault(),this.showBookmarkContextMenu(c,n)});continue}let s=n.type==="group",o=e.createDiv({cls:`waypoint-bookmark-item${s?" waypoint-bookmark-group":""}${n.collapsed?" collapsed":""}`});if(o.setAttr("draggable","true"),o.setAttr("data-bm-id",n.id),s||(o.style.paddingLeft=`${8+i*16}px`),this.attachBookmarkDragHandlers(o,e,n,!0),s){let r=o.createDiv({cls:"waypoint-bm-icon"});n.icon&&(0,m.setIcon)(r,n.icon);let c=o.createDiv({cls:"waypoint-bm-label",text:n.label}),l=o.createDiv({cls:"waypoint-bm-chevron"});(0,m.setIcon)(l,"chevron-down"),l.addEventListener("click",u=>{u.stopPropagation(),this.plugin.updateBookmark(n.id,{collapsed:!n.collapsed})}),o.addEventListener("click",u=>{if(n.filePath){let g=this.app.vault.getFileByPath(n.filePath);if(g){let F=m.Keymap.isModEvent(u);this.app.workspace.getLeaf(F).openFile(g);return}}this.plugin.updateBookmark(n.id,{collapsed:!n.collapsed})}),o.addEventListener("mousedown",u=>{if(u.button===1&&n.filePath){u.preventDefault();let g=this.app.vault.getFileByPath(n.filePath);g&&this.app.workspace.getLeaf("tab").openFile(g)}});let h=e.createDiv({cls:`waypoint-bookmark-children${n.collapsed?" collapsed":""}`});n.children&&n.children.length>0&&this.renderBookmarkList(h,n.children,i+1)}else{let r=o.createDiv({cls:"waypoint-bm-icon"});n.icon&&(0,m.setIcon)(r,n.icon);let c=o.createDiv({cls:"waypoint-bm-label",text:n.label});if(n.children&&n.children.length>0){let l=o.createDiv({cls:"waypoint-bm-chevron"});(0,m.setIcon)(l,"chevron-down"),l.addEventListener("click",h=>{h.stopPropagation(),this.plugin.updateBookmark(n.id,{collapsed:!n.collapsed})}),n.collapsed&&(o.addClass("collapsed"),l.style.transform="rotate(-90deg)")}if((0,m.setTooltip)(o,n.filePath),o.addEventListener("click",l=>{if(n.filePath){let h=this.app.vault.getFileByPath(n.filePath);if(h){let u=m.Keymap.isModEvent(l);this.app.workspace.getLeaf(u).openFile(h)}else new m.Notice("File not found"),this.plugin.removeBookmark(n.id)}}),o.addEventListener("mousedown",l=>{if(l.button===1&&n.filePath){l.preventDefault();let h=this.app.vault.getFileByPath(n.filePath);h&&this.app.workspace.getLeaf("tab").openFile(h)}}),n.children&&n.children.length>0){let l=e.createDiv({cls:`waypoint-bookmark-children${n.collapsed?" collapsed":""}`});this.renderBookmarkList(l,n.children,i+1)}}o.addEventListener("contextmenu",r=>{r.stopPropagation(),r.preventDefault(),this.showBookmarkContextMenu(r,n)})}}showBookmarkContextMenu(e,a){let i=new m.Menu;if(a.type==="separator"||a.type==="spacer"){i.addItem(t=>t.setTitle("Remove").setIcon("trash").onClick(()=>this.plugin.removeBookmark(a.id))),i.showAtPosition({x:e.clientX,y:e.clientY});return}a.type==="file"?(i.addItem(t=>t.setTitle("Open in new tab").setIcon("file-plus").onClick(()=>{let n=this.app.vault.getFileByPath(a.filePath);n&&this.app.workspace.getLeaf("tab").openFile(n)})),i.addSeparator(),i.addItem(t=>t.setTitle("Rename").setIcon("pencil").onClick(()=>this.promptRename(a))),i.addItem(t=>t.setTitle("Change icon").setIcon("image").onClick(()=>this.promptIcon(a))),i.addSeparator(),i.addItem(t=>t.setTitle("Remove").setIcon("trash").onClick(()=>this.plugin.removeBookmark(a.id)))):a.type==="group"&&(a.filePath&&(i.addItem(t=>t.setTitle("Open in new tab").setIcon("file-plus").onClick(()=>{let n=this.app.vault.getFileByPath(a.filePath);n&&this.app.workspace.getLeaf("tab").openFile(n)})),i.addSeparator()),i.addItem(t=>t.setTitle("Rename").setIcon("pencil").onClick(()=>this.promptRename(a))),i.addItem(t=>t.setTitle("Change icon").setIcon("image").onClick(()=>this.promptIcon(a))),i.addSeparator(),i.addItem(t=>t.setTitle("Add child bookmark").setIcon("file-plus").onClick(()=>{let n=this.app.workspace.getActiveFile();if(!n){new m.Notice("No active file");return}let s={id:`bm-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:"file",label:n.basename,filePath:n.path,icon:"",children:[],collapsed:!1,indent:a.indent+1};a.children.push(s),this.plugin.saveWaypointData(),this.redraw()})),i.addItem(t=>t.setTitle("Add child note").setIcon("folder-plus").onClick(()=>{let n=this.app.workspace.getActiveFile();if(!n){new m.Notice("No active file");return}let s={id:`bm-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:"group",label:n.basename,filePath:n.path,icon:"",children:[],collapsed:!1,indent:a.indent+1};a.children.push(s),this.plugin.saveWaypointData(),this.redraw()})),i.addItem(t=>t.setTitle("New sub-group").setIcon("folder-plus").onClick(()=>{let n={id:`bm-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:"group",label:"New Group",filePath:"",icon:"",children:[],collapsed:!1,indent:a.indent+1};a.children.push(n),this.plugin.saveWaypointData(),this.redraw()})),i.addSeparator(),i.addItem(t=>t.setTitle("Remove").setIcon("trash").onClick(()=>this.plugin.removeBookmark(a.id)))),i.showAtPosition({x:e.clientX,y:e.clientY})}attachBookmarkDragHandlers(e,a,i,t){let n=()=>{e.removeClass("waypoint-bm-drop-line"),e.removeClass("waypoint-bm-drop-below"),e.removeClass("waypoint-bm-drop-into")};e.addEventListener("dragstart",s=>{this.dragId=i.id,s.dataTransfer.effectAllowed="move",s.dataTransfer.setData("text/plain",i.id),e.addClass("waypoint-bm-dragging")}),e.addEventListener("dragend",()=>{this.dragId=null,a.querySelectorAll(".waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below, .waypoint-bm-drop-into").forEach(s=>{s.removeClass("waypoint-bm-dragging"),s.removeClass("waypoint-bm-drop-line"),s.removeClass("waypoint-bm-drop-below"),s.removeClass("waypoint-bm-drop-into")})}),e.addEventListener("dragenter",s=>{s.preventDefault(),!(!this.dragId||this.dragId===i.id)&&this.showDropIndicator(e,s,t)}),e.addEventListener("dragover",s=>{s.preventDefault(),!(!this.dragId||this.dragId===i.id)&&this.showDropIndicator(e,s,t)}),e.addEventListener("dragleave",n),e.addEventListener("drop",s=>{var c,l;s.preventDefault(),this.dragId=null,n();let o=(c=s.dataTransfer)==null?void 0:c.getData("text/plain");if(!o||o===i.id)return;let r=this.dropZones.get(e);t&&(r!=null&&r.into)?i.type==="group"?this.moveBookmarkToGroup(o,i.id):this.createParentNoteAndMove(o,i.id):this.moveBookmarkToPosition(o,i.id,(l=r==null?void 0:r.above)!=null?l:!1)})}showDropIndicator(e,a,i){let t=e.parentElement;t&&t.querySelectorAll(".waypoint-bm-drop-line, .waypoint-bm-drop-below, .waypoint-bm-drop-into").forEach(o=>{o.removeClass("waypoint-bm-drop-line"),o.removeClass("waypoint-bm-drop-below"),o.removeClass("waypoint-bm-drop-into")});let n=e.getBoundingClientRect(),s=a.clientY;if(i){let o=n.top+n.height*.25,r=n.top+n.height*.75;sr?(e.addClass("waypoint-bm-drop-line"),e.addClass("waypoint-bm-drop-below"),this.dropZones.set(e,{above:!1,into:!1})):(e.addClass("waypoint-bm-drop-into"),this.dropZones.set(e,{above:!1,into:!0}))}else{let o=s{let s=n.findIndex(o=>o.id===e);if(s>=0){let[o]=n.splice(s,1);return o}for(let o of n){let r=i(o.children);if(r)return r}return null},t=i(this.plugin.waypointData.bookmarks);if(t){if(a){let n=(s,o)=>s.id===o?!0:s.children.some(r=>n(r,o));if(t.id===a||n(t,a))return}if(a){let n=o=>{for(let r of o){if(r.id===a)return r;let c=n(r.children);if(c)return c}return null},s=n(this.plugin.waypointData.bookmarks);s&&(t.indent=s.indent+1,s.children.push(t))}else t.indent=0,this.plugin.waypointData.bookmarks.push(t);this.plugin.saveWaypointData(),this.redraw()}}createParentNoteAndMove(e,a){let i=o=>{let r=o.findIndex(c=>c.id===e);if(r>=0){let[c]=o.splice(r,1);return c}for(let c of o)if(c.children){let l=i(c.children);if(l)return l}return null},t=i(this.plugin.waypointData.bookmarks);if(!t)return;let n=o=>{for(let r of o){if(r.id===a)return r;if(r.children){let c=n(r.children);if(c)return c}}return null},s=n(this.plugin.waypointData.bookmarks);s&&(t.indent=s.indent+1,s.children.push(t),this.plugin.saveWaypointData(),this.redraw())}moveBookmarkToPosition(e,a,i){let t=r=>{let c=r.findIndex(l=>l.id===e);if(c>=0){let[l]=r.splice(c,1);return{item:l,parent:r}}for(let l of r)if(l.children){let h=t(l.children);if(h.item)return h}return{item:null,parent:[]}},{item:n}=t(this.plugin.waypointData.bookmarks);if(!n)return;let s=r=>{let c=r.findIndex(l=>l.id===a);if(c>=0)return{parent:r,idx:c};for(let l of r)if(l.children){let h=s(l.children);if(h)return h}return null},o=s(this.plugin.waypointData.bookmarks);if(!o)n.indent=0,this.plugin.waypointData.bookmarks.push(n);else{let r=i?o.idx:o.idx+1;o.parent.splice(r,0,n)}this.plugin.saveWaypointData(),this.redraw()}promptRename(e){new z(this.app,{title:"Rename bookmark",initialValue:e.label},a=>{a&&a.trim()&&this.plugin.updateBookmark(e.id,{label:a.trim()})}).open()}promptIcon(e){new K(this.app,e.icon,a=>{this.plugin.updateBookmark(e.id,{icon:a})}).open()}},z=class extends m.Modal{constructor(d,e,a){super(d),this.options=e,this.onSubmit=a}onOpen(){var t,n,s;this.titleEl.setText(this.options.title);let d=this.contentEl.createEl("input",{type:"text",value:(t=this.options.initialValue)!=null?t:"",placeholder:(n=this.options.placeholder)!=null?n:""});d.style.width="100%",d.style.marginBottom="12px",d.focus(),d.select();let e=this.contentEl.createDiv({cls:"modal-button-container"}),a=e.createEl("button",{text:"Cancel",cls:"mod-cta"});a.style.marginRight="8px",a.addEventListener("click",()=>this.close()),e.createEl("button",{text:(s=this.options.cta)!=null?s:"Save",cls:"mod-cta"}).addEventListener("click",()=>{this.onSubmit(d.value),this.close()}),d.addEventListener("keydown",o=>{o.key==="Enter"&&(this.onSubmit(d.value),this.close())})}onClose(){this.contentEl.empty()}},$=null;function ge(){return $||($=(async()=>{try{return await(await fetch("https://cdn.jsdelivr.net/npm/lucide-static@0.517.0/tags.json")).json()}catch(p){try{return await(await fetch("https://lucide.dev/api/tags")).json()}catch(d){return $=null,fe}}})()),$}var K=class extends m.Modal{constructor(e,a,i){super(e);this.allIcons=[];this.tagsMap={};this.loaded=!1;this.selected=a,this.onSubmit=i}async onOpen(){let e=this.contentEl;e.style.display="flex",e.style.flexDirection="column",e.style.gap="10px",this.titleEl.setText("Change icon");let a=e.createDiv({cls:"waypoint-icon-preview"});a.style.display="flex",a.style.alignItems="center",a.style.gap="10px",a.style.padding="12px 16px",a.style.borderRadius="8px",a.style.background="var(--background-secondary)",a.style.minHeight="48px";let i=a.createSpan();i.style.display="flex",this.selected&&(0,m.setIcon)(i,this.selected);let t=a.createSpan();t.style.fontWeight="var(--font-medium)",t.style.fontSize="var(--font-ui-medium)",t.setText(this.selected||"No icon");let n=e.createEl("input",{type:"text",placeholder:"Type to search (e.g. arrow, chart, home)..."});Object.assign(n.style,{width:"100%",boxSizing:"border-box",padding:"8px 10px",borderRadius:"6px",border:"1px solid var(--background-modifier-border)",background:"var(--background-primary)",color:"var(--text-normal)",fontSize:"var(--font-ui-medium)"}),n.focus();let s=e.createDiv({cls:"waypoint-icon-grid"});s.style.display="grid",s.style.gridTemplateColumns="repeat(auto-fill, minmax(52px, 1fr))",s.style.gap="4px",s.style.maxHeight="320px",s.style.overflowY="auto",s.style.padding="2px 0";let o=e.createDiv();o.style.display="flex",o.style.justifyContent="space-between",o.style.alignItems="center",o.style.fontSize="var(--font-ui-smaller)",o.style.color="var(--text-muted)",o.style.padding="0 4px";let r=o.createSpan();r.setText("Loading\u2026"),this.loadIcons().then(()=>{this.loaded=!0,r.setText(this.allIcons.length+" icons"),l(n.value)});let c,l=x=>{if(s.empty(),!this.loaded){s.createDiv({text:"Loading\u2026"});return}let w=x.toLowerCase().trim(),b=w?this.allIcons.filter(v=>{if(v.includes(w))return!0;let y=this.tagsMap[v];return y?y.some(L=>L.includes(w)):!1}).slice(0,80):this.allIcons.slice(0,80);if(b.length===0){let v=s.createDiv();v.style.gridColumn="1 / -1",v.style.textAlign="center",v.style.color="var(--text-muted)",v.style.padding="20px",v.setText('No icons match "'+x+'"');return}for(let v of b){let y=s.createDiv();y.setAttr("data-icon",v),y.style.display="flex",y.style.alignItems="center",y.style.justifyContent="center",y.style.aspectRatio="1",y.style.borderRadius="6px",y.style.cursor="var(--cursor-link, pointer)",y.style.transition="background 80ms",y.setAttr("title",v),v===this.selected?(y.style.background="var(--interactive-accent)",y.style.color="var(--text-on-accent)"):y.style.color="var(--text-muted)";let L=y.createSpan();L.style.display="flex",(0,m.setIcon)(L,v),y.addEventListener("mouseenter",()=>{v!==this.selected&&(y.style.background="var(--background-modifier-hover)")}),y.addEventListener("mouseleave",()=>{v!==this.selected&&(y.style.background="")}),y.addEventListener("click",()=>{this.selected=v,l(x),i.empty(),(0,m.setIcon)(i,v),t.setText(v),s.querySelectorAll("div[data-icon]").forEach(V=>{let M=V;M.getAttr("data-icon")===v?(M.style.background="var(--interactive-accent)",M.style.color="var(--text-on-accent)"):(M.style.background="",M.style.color="var(--text-muted)")})})}r.setText(b.length+" of "+this.allIcons.length+" icons")};n.addEventListener("input",()=>{window.clearTimeout(c),c=window.setTimeout(()=>l(n.value),60)}),n.addEventListener("keydown",x=>{x.key==="Escape"&&this.close()});let h=e.createDiv({cls:"modal-button-container"});h.createEl("button",{text:"No icon",cls:"waypoint-icon-clear"}).addEventListener("click",()=>{this.onSubmit(""),this.close()}),h.createEl("button",{text:"Cancel"}).addEventListener("click",()=>this.close());let F=h.createEl("button",{text:"Save",cls:"mod-cta"});F.style.marginLeft="8px",F.addEventListener("click",()=>{this.onSubmit(this.selected),this.close()})}onClose(){this.contentEl.empty()}async loadIcons(){let e=await ge();this.tagsMap=e,this.allIcons=Object.keys(e).sort()}},fe={file:[],folder:[],star:[],heart:[],bookmark:[],flag:[],pin:[],tag:[],book:[],"book-open":[],library:[],calendar:[],"calendar-days":[],clock:[],home:[],inbox:[],mail:[],search:[],settings:[],cog:[],user:[],users:[],zap:[],sparkles:[],target:[],link:[],globe:[],edit:[],pencil:[],anchor:[],award:[],bell:[],"bell-ring":[],brain:[],briefcase:[],camera:[],"chart-bar":[],"chart-line":[],"chart-pie":[],check:[],"check-circle":[],"chevron-down":[],"chevron-left":[],"chevron-right":[],"chevron-up":[],circle:[],clipboard:[],code:[],command:[],compass:[],copy:[],"credit-card":[],crown:[],database:[],download:[],"external-link":[],eye:[],"eye-off":[],"file-text":[],filter:[],fingerprint:[],flashlight:[],"folder-open":[],"folder-plus":[],gift:[],"git-branch":[],"git-commit":[],"git-merge":[],"git-pull-request":[],github:[],grid:[],hash:[],headphones:[],image:[],info:[],key:[],layers:[],layout:[],"life-buoy":[],"link-2":[],list:[],loader:[],lock:[],"log-in":[],"log-out":[],map:[],"map-pin":[],maximize:[],megaphone:[],menu:[],"message-circle":[],"message-square":[],mic:[],minimize:[],moon:[],"more-horizontal":[],"more-vertical":[],"mouse-pointer":[],move:[],music:[],navigation:[],"navigation-2":[],package:[],palette:[],paperclip:[],pause:[],phone:[],play:[],plus:[],"plus-circle":[],power:[],printer:[],radio:[],"refresh-cw":[],repeat:[],"rotate-ccw":[],"rotate-cw":[],rss:[],save:[],scissors:[],screen:[],send:[],server:[],share:[],"share-2":[],shield:[],"shield-off":[],"shopping-bag":[],"shopping-cart":[],shuffle:[],sidebar:[],slack:[],slash:[],sliders:[],smartphone:[],smile:[],speaker:[],square:[],"stop-circle":[],sun:[],sunrise:[],sunset:[],swords:[],table:[],tablet:[],terminal:[],thermometer:[],"thumbs-down":[],"thumbs-up":[],"toggle-left":[],"toggle-right":[],tool:[],trash:[],"trash-2":[],trello:[],"trending-down":[],"trending-up":[],triangle:[],truck:[],tv:[],twitter:[],type:[],umbrella:[],unlock:[],upload:[],"user-check":[],"user-minus":[],"user-plus":[],"user-x":[],video:[],"video-off":[],voicemail:[],volume:[],"volume-1":[],"volume-2":[],"volume-x":[],watch:[],wifi:[],"wifi-off":[],wind:[],x:[],"x-circle":[],"x-square":[],youtube:[],"zap-off":[],"zoom-in":[],"zoom-out":[],"arrow-down":[],"arrow-left":[],"arrow-right":[],"arrow-up":[],airplay:[],"alarm-clock":[],archive:[],armchair:[],atom:[],baby:[],backpack:[],badge:[],"badge-check":[],ban:[],banknote:[],barcode:[],bath:[],battery:[],"battery-charging":[],beer:[],bike:[],bird:[],bluetooth:[],bolt:[],bone:[],"bookmark-plus":[],bot:[],box:[],bug:[],building:[],bus:[],cake:[],calculator:[],car:[],"clipboard-check":[],cloud:[],"cloud-download":[],"cloud-lightning":[],"cloud-rain":[],"cloud-sun":[],"cloud-upload":[],clover:[],coffee:[],coins:[],contact:[],cookie:[],"corner-down-left":[],"corner-down-right":[],"corner-up-left":[],"corner-up-right":[],crosshair:[],"dice-1":[],"dice-6":[],"dollar-sign":[],"door-open":[],drama:[],droplet:[],drum:[],egg:[],equal:[],euro:[],factory:[],fan:[],feather:[],film:[],fish:[],flame:[],flask:[],flower:[],frown:[],fuel:[],gamepad:[],gauge:[],gem:[],ghost:[],glasses:[],"graduation-cap":[],hammer:[],"hard-drive":[],haze:[],"help-circle":[],"ice-cream":[],infinity:[],italic:[],"japanese-yen":[],keyboard:[],knife:[],lamp:[],landmark:[],languages:[],laptop:[],laugh:[],leaf:[],lightbulb:[],"list-plus":[],magnet:[],"mail-plus":[],meh:[],microscope:[],milestone:[],"minimize-2":[],monitor:[],mountain:[],mouse:[],network:[],newspaper:[],"package-check":[],"package-search":[],"paint-bucket":[],parking:[],"party-popper":[],"pen-tool":[],percent:[],"person-standing":[],"picture-in-picture-2":[],plane:[],plug:[],podcast:[],pointer:[],"pound-sterling":[],puzzle:[],"qr-code":[],rabbit:[],radar:[],rainbow:[],rocket:[],"roller-coaster":[],route:[],ruler:[],sailboat:[],scale:[],scan:[],school:[],ship:[],shirt:[],"shopping-basket":[],shovel:[],sigma:[],siren:[],skull:[],snowflake:[],soup:[],space:[],sparkle:[],stamp:[],store:[],subscript:[],superscript:[],syringe:[],tent:[],"tent-tree":[],"test-tube":[],theater:[],timer:[],train:[],"tree-deciduous":[],"tree-pine":[],trophy:[],typing:[],utensils:[],vibrate:[],wallet:[],wand:[],warehouse:[],waves:[],webcam:[],wheat:[],wine:[],wrench:[]};function Q(p,d,e){return!p||!d?null:p===d?e:p.startsWith(d+"/")?e+p.slice(d.length):null}var q=class extends f.Plugin{constructor(){super(...arguments);this.recentFiles=[];this.savePromise=Promise.resolve();this.markdownBasenames=new Set}async onload(){console.debug("Waypoint: loading plugin v"+this.manifest.version);let e=await this.loadData();this.applySettings(e),this.applyWaypointData(e),this.registerView(T,s=>new B(s,this)),this.addSettingTab(new O(this.app,this,this.settings,()=>{this.enforceRecentFilesLimit(),this.redrawAll()})),this.addCommand({id:"waypoint-open-view",name:"Open Waypoint sidebar",callback:async()=>{let s=this.app.workspace.getLeavesOfType(T);if(s.length>0)await this.app.workspace.revealLeaf(s[0]);else{let o=this.app.workspace.getLeftLeaf(!1);o&&(await o.setViewState({type:T}),await this.app.workspace.revealLeaf(o))}}}),this.addCommand({id:"waypoint-add-bookmark",name:"Add current file as Waypoint bookmark",callback:async()=>{let s=this.app.workspace.getActiveFile();if(!s){new f.Notice("No active file");return}this.addBookmark(s.path,s.basename,"file"),new f.Notice(`Bookmarked: ${s.basename}`)}}),this.addCommand({id:"waypoint-go-to-daily",name:"Go to daily note",hotkeys:[{modifiers:["Mod","Shift","Alt"],key:"d"}],callback:async()=>{await this.openPeriodNote("day",(0,f.moment)())}}),this.addCommand({id:"waypoint-go-to-weekly",name:"Go to weekly note",hotkeys:[{modifiers:["Mod","Shift","Alt"],key:"w"}],callback:async()=>{await this.openPeriodNote("week",(0,f.moment)())}}),this.addCommand({id:"waypoint-go-to-monthly",name:"Go to monthly note",hotkeys:[{modifiers:["Mod","Shift","Alt"],key:"m"}],callback:async()=>{await this.openPeriodNote("month",(0,f.moment)())}}),this.addCommand({id:"waypoint-go-to-quarterly",name:"Go to quarterly note",hotkeys:[{modifiers:["Mod","Shift","Alt"],key:"q"}],callback:async()=>{await this.openPeriodNote("quarter",(0,f.moment)())}}),this.addCommand({id:"waypoint-go-to-yearly",name:"Go to yearly note",hotkeys:[{modifiers:["Mod","Shift","Alt"],key:"y"}],callback:async()=>{await this.openPeriodNote("year",(0,f.moment)())}});let a=["next","prev"],i=["daily","weekly","monthly","quarterly","yearly"],t={next:"Next",prev:"Previous"};for(let s of i)for(let o of a){let r=`waypoint-go-to-${o}-${s}`,c=`${t[o]} ${s} note`;this.addCommand({id:r,name:c,callback:async()=>{await this.navigatePeriodNote(o)}})}this.registerEvent(this.app.workspace.on("file-open",s=>{s&&this.onFileOpen(s)})),this.registerEvent(this.app.vault.on("create",s=>this.onVaultCreate(s))),this.registerEvent(this.app.vault.on("delete",s=>this.onVaultDelete(s))),this.registerEvent(this.app.vault.on("rename",(s,o)=>this.onRename(s,o))),this.registerEvent(this.app.vault.on("modify",s=>this.onFileModify(s))),this.app.workspace.onLayoutReady(()=>{if(this.buildMarkdownIndex(),this.app.workspace.getLeavesOfType(T).length===0){let o=this.app.workspace.getLeftLeaf(!1);o&&o.setViewState({type:T})}else this.broadcastRedraw()});let n=new Date().toDateString();this.registerInterval(window.setInterval(()=>{let s=new Date().toDateString();s!==n&&(n=s,this.redrawAll())},6e5))}async onunload(){this.app.workspace.detachLeavesOfType(T)}applySettings(e){let a=(e==null?void 0:e.settings)||{};this.settings=Object.assign({},E,a),this.settings.recentFiles=Object.assign({},E.recentFiles,a.recentFiles||{}),this.settings.calendar=Object.assign({},E.calendar,a.calendar||{}),this.settings.display=Object.assign({},E.display,a.display||{});for(let i of ye)this.settings[i]=Object.assign({},E[i],a[i]||{});this.settings.dateSystems=Array.isArray(a.dateSystems)?a.dateSystems.map(i=>Object.assign({},R,i)):E.dateSystems.map(i=>Object.assign({},i))}applyWaypointData(e){let a=(e==null?void 0:e.waypointData)||{};this.waypointData={bookmarks:Array.isArray(a.bookmarks)?a.bookmarks:[],recentFiles:Array.isArray(a.recentFiles)?a.recentFiles:[]},this.recentFiles=this.waypointData.recentFiles,this.recentFiles.length>this.settings.recentFiles.maxItems&&(this.recentFiles=this.recentFiles.slice(0,this.settings.recentFiles.maxItems),this.waypointData.recentFiles=this.recentFiles)}persistAll(){let e=this.savePromise.then(()=>(this.waypointData.recentFiles=this.recentFiles,this.saveData({settings:this.settings,waypointData:this.waypointData})));return this.savePromise=e.catch(()=>{}),e}async saveSettings(){await this.persistAll()}async saveWaypointData(){await this.persistAll()}enforceRecentFilesLimit(){this.recentFiles.length>this.settings.recentFiles.maxItems&&(this.recentFiles=this.recentFiles.slice(0,this.settings.recentFiles.maxItems),this.persistRecentFiles())}persistRecentFiles(){this.waypointData.recentFiles=this.recentFiles,window.clearTimeout(this.recentFilesSaveTimer),this.recentFilesSaveTimer=window.setTimeout(()=>{this.saveWaypointData()},300)}onFileOpen(e){this.settings.recentFiles.updateOn==="file-open"&&this.addToRecentFiles(e)}onFileModify(e){this.settings.recentFiles.updateOn==="file-edit"&&e instanceof f.TFile&&(this.recentFiles.length>0&&this.recentFiles[0].path===e.path||this.addToRecentFiles(e))}addToRecentFiles(e){this.isOmittedFromRecentFiles(e)||(this.recentFiles=this.recentFiles.filter(a=>a.path!==e.path),this.recentFiles.unshift({path:e.path,basename:e.basename}),this.recentFiles.length>this.settings.recentFiles.maxItems&&(this.recentFiles=this.recentFiles.slice(0,this.settings.recentFiles.maxItems)),this.persistRecentFiles(),this.broadcastRedraw())}isOmittedFromRecentFiles(e){for(let s of this.settings.recentFiles.omittedPaths)try{if(new RegExp(s).test(e.path))return!0}catch(o){}let a=this.settings.recentFiles.omittedTags;if(a.length===0)return!1;let i=this.app.metadataCache.getFileCache(e),t=(i?(0,f.getAllTags)(i):null)||[];if(t.length===0)return!1;let n=t.map(s=>s.replace(/^#/,""));for(let s of a)try{let o=new RegExp(s);if(n.some(r=>o.test(r)))return!0}catch(o){}return!1}onRename(e,a){let i=this.syncIndexForRename(e,a),t=!1;for(let s of this.recentFiles){let o=Q(s.path,a,e.path);o!==null&&(s.path=o,s.basename=se(o),t=!0)}let n=s=>{for(let o of s){if(o.filePath){let r=Q(o.filePath,a,e.path);r!==null&&(o.filePath=r,t=!0)}o.children&&n(o.children)}};n(this.waypointData.bookmarks),t&&(this.waypointData.recentFiles=this.recentFiles,this.persistAll()),(t||i)&&this.broadcastRedraw()}onVaultCreate(e){e instanceof f.TFile&&e.extension==="md"&&this.markdownBasenames.add(e.basename),this.broadcastRedraw()}onVaultDelete(e){e instanceof f.TFile&&e.extension==="md"&&this.removeFromMarkdownIndex(e.basename,e.path),this.broadcastRedraw()}buildMarkdownIndex(){this.markdownBasenames.clear();for(let e of this.app.vault.getMarkdownFiles())this.markdownBasenames.add(e.basename)}removeFromMarkdownIndex(e,a){return!this.markdownBasenames.has(e)||this.app.vault.getMarkdownFiles().some(t=>t.basename===e&&t.path!==a)?!1:(this.markdownBasenames.delete(e),!0)}syncIndexForRename(e,a){if(!(e instanceof f.TFile))return!1;let i=!1,t=se(a);return a.toLowerCase().endsWith(".md")&&(t!==e.basename||e.extension!=="md")&&(i=this.removeFromMarkdownIndex(t,a)),e.extension==="md"&&!this.markdownBasenames.has(e.basename)&&(this.markdownBasenames.add(e.basename),i=!0),i}hasNoteForDate(e){return this.markdownBasenames.has(e)}addBookmark(e,a,i="file",t=""){let s={id:`bm-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:i,label:a,filePath:i==="file"?e:"",icon:t,children:[],collapsed:!1,indent:0};return this.waypointData.bookmarks.push(s),this.saveWaypointData(),this.broadcastRedraw(),s}removeBookmark(e){let a=i=>{let t=i.findIndex(n=>n.id===e);if(t>=0)return i.splice(t,1),!0;for(let n of i)if(n.children&&a(n.children))return!0;return!1};a(this.waypointData.bookmarks),this.saveWaypointData(),this.broadcastRedraw()}updateBookmark(e,a){let i=n=>{for(let s of n){if(s.id===e)return s;if(s.children){let o=i(s.children);if(o)return o}}return null},t=i(this.waypointData.bookmarks);t&&(Object.assign(t,a),this.saveWaypointData(),this.broadcastRedraw())}periodAsDateSystem(e){let a=j[e],i=this.settings[a.key];return{id:a.key,name:a.label,folder:i.folder,nameFormat:i.nameFormat,templateFile:i.templateFile,typeProperty:i.typeProperty,icon:"calendar"}}dateSystems(){return[this.periodAsDateSystem("day"),...this.settings.dateSystems]}findDateSystemNotes(e){let a=this.dateSystems().map(i=>{let t=W(i.nameFormat);return{result:{system:i,notes:[],multiple:t.hasTitle},before:H(e,t.before),after:H(e,t.after),hasTitle:t.hasTitle,skip:!N(i.nameFormat)}});for(let i of this.app.vault.getMarkdownFiles())for(let t of a){if(t.skip)continue;let n=t.result.system;J(i.path,n.folder)&&ee(i.basename,t.before,t.after,t.hasTitle)&&t.result.notes.push({file:i,label:t.hasTitle?te(i.basename,t.before,t.after):n.name})}for(let i of a)i.result.notes.sort((t,n)=>t.file.basename.localeCompare(n.file.basename));return a.filter(i=>!i.skip).map(i=>i.result)}async openDateSystemNote(e,a,i){if(!N(e.nameFormat)){new f.Notice(`Waypoint: ${e.name||"This"} name format needs a date placeholder.`);return}let t=W(e.nameFormat),n=H(a,t.before),s=H(a,t.after),o;if(t.hasTitle){let h=ne((i==null?void 0:i.title)||"");if(!h){new f.Notice(`Waypoint: a ${e.name.toLowerCase()} note needs a title.`);return}o=n+h+s}else o=n+s;let r=e.folder?`${e.folder}/${o}.md`:`${o}.md`,c=this.app.vault.getFileByPath(r);if(!c&&(c=await this.createDatedNote(r,e,a),!c))return;await((i==null?void 0:i.leaf)||this.app.workspace.getLeaf(!1)).openFile(c)}async openPeriodNote(e,a,i){await this.openDateSystemNote(this.periodAsDateSystem(e),a,{leaf:i})}async createDatedNote(e,a,i){let t=a.name.toLowerCase(),n=e.lastIndexOf("/"),s=n<0?"":e.slice(0,n);try{await this.ensureFolderExists(s)}catch(h){return new f.Notice(`Waypoint: could not create the folder "${s}" for the ${t} note. +${U(h)} +Check Settings \u2192 Waypoint Sidebar \u2192 Periodic Notes.`,A),null}let o=a.templateFile,r=this.resolveTemplateFile(o),c;if(r)try{c=await this.app.vault.read(r)}catch(h){return new f.Notice(`Waypoint: could not read the template "${r.path}" for the ${t} note. +`+U(h),A),null}else c=`--- +type: ${a.typeProperty} +date: ${i.format("YYYY-MM-DD")} --- -`;let h;try{h=await this.app.vault.create(e,l)}catch(p){return new y.Notice(`Waypoint: could not create the ${t} note at "${e}". -${j(p)}`,B),null}return r&&!c?new y.Notice(`Created ${t} note: ${h.basename} -Template "${r}" was not found, so a basic note was created instead.`,B):new y.Notice(`Created ${t} note: ${h.basename}`),h}async ensureFolderExists(e){if(!e||this.app.vault.getAbstractFileByPath(e)instanceof y.TFolder)return;let i="";for(let s of e.split("/"))if(s&&(i=i?`${i}/${s}`:s,!(this.app.vault.getAbstractFileByPath(i)instanceof y.TFolder)))try{await this.app.vault.createFolder(i)}catch(n){if(!(this.app.vault.getAbstractFileByPath(i)instanceof y.TFolder))throw n}}resolveTemplateFile(e){if(!e)return null;let i=e.toLowerCase().endsWith(".md")?e:`${e}.md`;return this.app.vault.getFileByPath(i)}detectPeriodType(e){for(let i of _){let s=this.settings[Y[i].key].nameFormat;if(!s)continue;let n=(0,y.moment)(e,s,!0);if(n.isValid())return{period:i,date:n}}return null}async navigatePeriodNote(e){let i=this.app.workspace.getActiveFile();if(!i){new y.Notice("No active file");return}let s=this.detectPeriodType(i.basename);if(!s){let r=_.map(c=>`${Y[c].label.toLowerCase()} "${this.settings[Y[c].key].nameFormat}"`).join(", ");new y.Notice(`Waypoint: "${i.basename}" does not match any configured periodic note format. -Expected one of: ${r}.`,B);return}let{period:n,date:t}=s,a=e==="next"?1:-1,o=n==="quarter"?t.clone().add(a*3,"months"):t.clone().add(a,`${n}s`);await this.openPeriodNote(n,o)}redrawAll(){this.broadcastRedraw()}broadcastRedraw(){let e=this.app.workspace.getLeavesOfType(T);for(let i of e)i.view instanceof L&&i.view.redraw()}},re=["daily","weekly","monthly","quarterly","yearly"],Y={day:{key:"daily",label:"Daily"},week:{key:"weekly",label:"Weekly"},month:{key:"monthly",label:"Monthly"},quarter:{key:"quarterly",label:"Quarterly"},year:{key:"yearly",label:"Yearly"}},_=["day","week","month","quarter","year"];function Z(g){return g.slice(g.lastIndexOf("/")+1).replace(/\.[^/.]+$/,"")}var B=1e4;function j(g){return g instanceof Error?g.message:String(g)} +`;let l;try{l=await this.app.vault.create(e,c)}catch(h){return new f.Notice(`Waypoint: could not create the ${t} note at "${e}". +${U(h)}`,A),null}return o&&!r?new f.Notice(`Created ${t} note: ${l.basename} +Template "${o}" was not found, so a basic note was created instead.`,A):new f.Notice(`Created ${t} note: ${l.basename}`),l}async ensureFolderExists(e){if(!e||this.app.vault.getAbstractFileByPath(e)instanceof f.TFolder)return;let a="";for(let i of e.split("/"))if(i&&(a=a?`${a}/${i}`:i,!(this.app.vault.getAbstractFileByPath(a)instanceof f.TFolder)))try{await this.app.vault.createFolder(a)}catch(t){if(!(this.app.vault.getAbstractFileByPath(a)instanceof f.TFolder))throw t}}resolveTemplateFile(e){if(!e)return null;let a=e.toLowerCase().endsWith(".md")?e:`${e}.md`;return this.app.vault.getFileByPath(a)}detectPeriodType(e){for(let a of ae){let i=this.settings[j[a].key].nameFormat;if(!i)continue;let t=(0,f.moment)(e,i,!0);if(t.isValid())return{period:a,date:t}}return null}async navigatePeriodNote(e){let a=this.app.workspace.getActiveFile();if(!a){new f.Notice("No active file");return}let i=this.detectPeriodType(a.basename);if(!i){let r=ae.map(c=>`${j[c].label.toLowerCase()} "${this.settings[j[c].key].nameFormat}"`).join(", ");new f.Notice(`Waypoint: "${a.basename}" does not match any configured periodic note format. +Expected one of: ${r}.`,A);return}let{period:t,date:n}=i,s=e==="next"?1:-1,o=t==="quarter"?n.clone().add(s*3,"months"):n.clone().add(s,`${t}s`);await this.openPeriodNote(t,o)}redrawAll(){this.broadcastRedraw()}broadcastRedraw(){let e=this.app.workspace.getLeavesOfType(T);for(let a of e)a.view instanceof B&&a.view.redraw()}},ye=["daily","weekly","monthly","quarterly","yearly"],j={day:{key:"daily",label:"Daily"},week:{key:"weekly",label:"Weekly"},month:{key:"monthly",label:"Monthly"},quarter:{key:"quarterly",label:"Quarterly"},year:{key:"yearly",label:"Yearly"}},ae=["day","week","month","quarter","year"];function se(p){return p.slice(p.lastIndexOf("/")+1).replace(/\.[^/.]+$/,"")}var A=1e4;function U(p){return p instanceof Error?p.message:String(p)}function H(p,d){return d?p.format(d):""} diff --git a/src/main.ts b/src/main.ts index 5c92096..5d3c472 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,14 +10,38 @@ import { getAllTags, moment, } 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 { WaypointView, WAYPOINT_VIEW_TYPE } from 'src/views/waypoint-view'; import { BookmarkItem, WaypointData } from 'src/models/bookmark'; 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'; +/** 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 { public settings: WaypointSettings; public waypointData: WaypointData; @@ -221,6 +245,13 @@ export default class WaypointPlugin extends Plugin { for (const key of PERIOD_SETTING_KEYS) { 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 | 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 { + + 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`. * Opens in `leaf` when given, otherwise in the active leaf. */ async openPeriodNote(period: PeriodKey, date: moment.Moment, leaf?: WorkspaceLeaf): Promise { - const config = PERIOD_CONFIGS[period]; - 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); + await this.openDateSystemNote(this.periodAsDateSystem(period), date, { leaf }); } /** - * 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 * cause is a configured folder that does not exist yet, which `vault.create` * refuses outright rather than creating. */ - private async createPeriodNote( + private async createDatedNote( fullPath: string, - periodSettings: PeriodNoteSettings, + system: DateSystemSettings, date: moment.Moment, - label: string, ): Promise { - const noun = label.toLowerCase(); + const noun = system.name.toLowerCase(); const slash = fullPath.lastIndexOf('/'); const folder = slash < 0 ? '' : fullPath.slice(0, slash); @@ -563,7 +692,7 @@ export default class WaypointPlugin extends Plugin { return null; } - const configuredTemplate = periodSettings.templateFile; + const configuredTemplate = system.templateFile; const templateFile = this.resolveTemplateFile(configuredTemplate); let content: string; @@ -579,7 +708,7 @@ export default class WaypointPlugin extends Plugin { return null; } } 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; @@ -733,3 +862,12 @@ const DIAGNOSTIC_NOTICE_MS = 10000; function describeError(err: unknown): string { 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) : ''; +} diff --git a/src/settings-tab.ts b/src/settings-tab.ts index 22cf6a5..967ed2a 100644 --- a/src/settings-tab.ts +++ b/src/settings-tab.ts @@ -1,12 +1,13 @@ import { Setting, PluginSettingTab, App, setIcon } from 'obsidian'; 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 { private plugin: WaypointPlugin; private settings: WaypointSettings; 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) { super(app, plugin); @@ -24,6 +25,7 @@ export class WaypointSettingTab extends PluginSettingTab { const tabs = [ { key: 'calendar' as const, label: 'Calendar' }, { key: 'periodic' as const, label: 'Periodic Notes' }, + { key: 'systems' as const, label: 'Date systems' }, { key: 'recent' as const, label: 'Recent Files' }, { key: 'display' as const, label: 'Display' }, { key: 'about' as const, label: 'About' }, @@ -49,6 +51,9 @@ export class WaypointSettingTab extends PluginSettingTab { case 'periodic': this.renderPeriodicTab(tabContent); break; + case 'systems': + this.renderSystemsTab(tabContent); + break; case 'recent': this.renderRecentTab(tabContent); 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( + container: HTMLElement, + name: string, + desc: string, + obj: Record, + 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 // ═══════════════════════════════ diff --git a/src/settings.ts b/src/settings.ts index 5ca7491..8a94098 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -7,6 +7,38 @@ export interface PeriodNoteSettings { 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 = { + name: '', + folder: '', + nameFormat: 'YYYY-MM-DD - {title}', + templateFile: '', + typeProperty: '', + icon: 'file', +}; + export interface CalendarSettings { firstDayOfWeek: number; // 0=Sunday, 1=Monday showNoteIndicators: boolean; @@ -36,6 +68,8 @@ export interface WaypointSettings { monthly: PeriodNoteSettings; quarterly: PeriodNoteSettings; yearly: PeriodNoteSettings; + /** Day-scoped systems beyond the daily note, in menu order. */ + dateSystems: DateSystemSettings[]; recentFiles: RecentFilesSettings; display: DisplaySettings; } @@ -75,6 +109,26 @@ export const DEFAULT_SETTINGS: WaypointSettings = { nameFormat: 'YYYY', typeProperty: 'yearly-note', }, + dateSystems: [ + { + id: 'journal', + name: 'Journal', + folder: 'periodic/journal', + nameFormat: 'YYYY-MM-DD - [Journal]', + templateFile: 'resources/template/journal', + typeProperty: 'journal-note', + icon: 'book-open', + }, + { + id: 'meetings', + name: 'Meeting', + folder: 'periodic/meetings', + nameFormat: 'YYYY-MM-DD - {title}', + templateFile: 'resources/template/meeting', + typeProperty: 'meeting-note', + icon: 'users', + }, + ], recentFiles: { maxItems: 50, updateOn: 'file-open', diff --git a/src/utils/date-systems.ts b/src/utils/date-systems.ts new file mode 100644 index 0000000..dc06edf --- /dev/null +++ b/src/utils/date-systems.ts @@ -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); +} diff --git a/src/views/waypoint-view.ts b/src/views/waypoint-view.ts index eb4540c..ead7de3 100644 --- a/src/views/waypoint-view.ts +++ b/src/views/waypoint-view.ts @@ -15,6 +15,7 @@ import { type PaneType, } from 'obsidian'; import type WaypointPlugin from 'src/main'; +import type { DateSystemNotes } from 'src/main'; import { getMonthGrid } from 'src/utils/date-utils'; import { BookmarkItem } from 'src/models/bookmark'; @@ -224,6 +225,11 @@ export class WaypointView extends ItemView { this.plugin.openPeriodNote('day', day.date, this.app.workspace.getLeaf('tab')); } }); + cell.addEventListener('contextmenu', (event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + this.showDayContextMenu(event, day.date); + }); } } @@ -236,6 +242,79 @@ export class WaypointView extends ItemView { this.redraw(); } + /** + * Right-click menu for a day cell: every date system's existing notes for + * that date, plus the actions that would create the ones it is missing. + */ + private showDayContextMenu(event: MouseEvent, date: moment.Moment): void { + const menu = new Menu(); + + menu.addItem((i) => i.setTitle(date.format('dddd, MMMM D, YYYY')).setIsLabel(true)); + + for (const bucket of this.plugin.findDateSystemNotes(date)) { + // Many-per-date systems can always take another note; one-per-date + // systems only offer creation while their single note is missing. + const canCreate = bucket.multiple || bucket.notes.length === 0; + if (bucket.notes.length + (canCreate ? 1 : 0) === 0) continue; + + menu.addSeparator(); + + for (const note of bucket.notes) { + menu.addItem((i) => + i + .setTitle(note.label) + .setIcon(bucket.system.icon) + .onClick((evt) => this.focusFile(note.file, Keymap.isModEvent(evt))), + ); + } + + if (!canCreate) continue; + + const noun = bucket.system.name.toLowerCase(); + menu.addItem((i) => + i + // The ellipsis promises the prompt that a free-text title needs. + .setTitle(bucket.multiple ? `New ${noun} note…` : `New ${noun} note`) + .setIcon('plus') + .onClick((evt) => this.createDateSystemNote(bucket, date, Keymap.isModEvent(evt))), + ); + } + + menu.showAtPosition({ x: event.clientX, y: event.clientY }); + } + + /** + * Creates a date system's note, asking for a title first when the system + * holds many notes per date. A held modifier opens the result in a new pane. + */ + private createDateSystemNote(bucket: DateSystemNotes, date: moment.Moment, newLeaf: PaneType | boolean): void { + if (!bucket.multiple) { + void this.plugin.openDateSystemNote(bucket.system, date, { + leaf: newLeaf ? this.app.workspace.getLeaf(newLeaf) : undefined, + }); + return; + } + + const name = bucket.system.name; + new PromptModal( + this.app, + { + title: `New ${name.toLowerCase()} note`, + placeholder: `${name} with…`, + cta: 'Create', + }, + (title) => { + // `getLeaf` materialises the pane immediately, so the target pane is + // resolved only after the title is confirmed — cancelling the prompt + // must not leave an empty tab behind. + void this.plugin.openDateSystemNote(bucket.system, date, { + title, + leaf: newLeaf ? this.app.workspace.getLeaf(newLeaf) : undefined, + }); + }, + ).open(); + } + // ════════════════════════════════════════ // Recent Files panel // ════════════════════════════════════════ @@ -1062,7 +1141,7 @@ export class WaypointView extends ItemView { } private promptRename(item: BookmarkItem): void { - new RenameModal(this.app, item.label, (newLabel) => { + new PromptModal(this.app, { title: 'Rename bookmark', initialValue: item.label }, (newLabel) => { if (newLabel && newLabel.trim()) { this.plugin.updateBookmark(item.id, { label: newLabel.trim() }); } @@ -1077,24 +1156,34 @@ export class WaypointView extends ItemView { } } -// ── Rename modal ── +// ── Text prompt modal ── -class RenameModal extends Modal { - private currentValue: string; +interface PromptModalOptions { + title: string; + placeholder?: string; + initialValue?: string; + /** Submit button label. Defaults to 'Save'. */ + cta?: string; +} + +/** Single-line text prompt, shared by bookmark renaming and note creation. */ +class PromptModal extends Modal { + private options: PromptModalOptions; private onSubmit: (value: string) => void; - constructor(app: App, currentValue: string, onSubmit: (value: string) => void) { + constructor(app: App, options: PromptModalOptions, onSubmit: (value: string) => void) { super(app); - this.currentValue = currentValue; + this.options = options; this.onSubmit = onSubmit; } onOpen(): void { - this.titleEl.setText('Rename bookmark'); + this.titleEl.setText(this.options.title); const input = this.contentEl.createEl('input', { type: 'text', - value: this.currentValue, + value: this.options.initialValue ?? '', + placeholder: this.options.placeholder ?? '', }); input.style.width = '100%'; input.style.marginBottom = '12px'; @@ -1107,7 +1196,7 @@ class RenameModal extends Modal { cancelBtn.style.marginRight = '8px'; cancelBtn.addEventListener('click', () => this.close()); - const saveBtn = btnContainer.createEl('button', { text: 'Save', cls: 'mod-cta' }); + const saveBtn = btnContainer.createEl('button', { text: this.options.cta ?? 'Save', cls: 'mod-cta' }); saveBtn.addEventListener('click', () => { this.onSubmit(input.value); this.close(); diff --git a/styles.css b/styles.css index 8f3ba0c..d77f856 100644 --- a/styles.css +++ b/styles.css @@ -420,3 +420,11 @@ button.waypoint-calendar-today-btn { margin-right: auto; cursor: var(--cursor-link, pointer); } + +/* ── Settings: date systems ── */ + +.waypoint-settings-warning { + margin-top: 4px; + color: var(--text-error); + font-size: var(--font-ui-smaller); +}