commit fbdc42b4a35d452fee7de3075983fb4e168587ae Author: Olivier Legendre Date: Wed Jun 3 20:26:23 2026 -0400 Initial commit: Waypoint Obsidian plugin with calendar, recent files, and bookmarks diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9451024 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.DS_Store +*.log diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..fb3c40f --- /dev/null +++ b/PLAN.md @@ -0,0 +1,329 @@ +# Waypoint Plugin — Implementation Plan + +## Overview + +A sidebar plugin for Obsidian that combines three panels into one coherent view: a **periodic note calendar** (day/week/month/quarter/year), a **recent files list**, and a **custom favorites/bookmarks system** with rename, icons, and grouping. + +--- + +## Features (from requirements) + +1. **Calendar panel** — shows a month grid with every day clickable +2. **Period indicators** — day, week, month, quarter, year all displayed and clickable +3. **Note creation on click** — clicking a period creates a note at a configurable path with a configurable template +4. **Recent files** — list of recently opened/edited files +5. **Favorites/Bookmarks** — custom bookmarks (distinct from Obsidian native bookmarks) with: + - Command: "Add current file as Waypoint bookmark" + - Rename without renaming the underlying file + - Assign an icon (from Lucide icons) + - Right-click context menu: rename, change icon, remove, move + - Foldable groups and indented hierarchy +6. **Settings** — configure folder paths and templates for each period type + +--- + +## Architecture + +### Approach + +- **Plain ItemView** (not React) — simpler, matches recent-files-obsidian's approach, avoids extra dependencies +- **Single view type** `waypoint-view` that contains all three panels stacked vertically +- **Plugin data** stored via `Plugin.loadData()` / `Plugin.saveData()` for bookmarks and recent files +- **Settings** via `PluginSettingTab` + +### Why not React? + +The daily-note-calendar plugin uses React, but that adds complexity (esbuild config for JSX, react-dom dependency). The recent-files plugin does everything with plain Obsidian DOM helpers (`createDiv`, `setIcon`, etc.) and is much simpler. We can build a clean, native-feeling UI without React. If the calendar grid becomes complex, we can reconsider. + +### File Structure + +``` +waypoint/ +├── manifest.json +├── package.json +├── tsconfig.json +├── esbuild.config.mjs +├── styles.css +├── src/ +│ ├── main.ts # Plugin entry point +│ ├── settings.ts # Settings interface + defaults + SettingTab +│ ├── views/ +│ │ ├── waypoint-view.ts # Main ItemView (orchestrates panels) +│ │ ├── calendar-panel.ts # Calendar month grid + period indicators +│ │ ├── recent-files-panel.ts # Recent files list +│ │ └── favorites-panel.ts # Favorites/bookmarks tree +│ ├── models/ +│ │ └── bookmark.ts # Bookmark item/group interfaces +│ ├── services/ +│ │ ├── calendar-service.ts # Date math, period detection, note creation +│ │ ├── recent-files-service.ts # Track and filter recent files +│ │ └── favorites-service.ts # CRUD for custom bookmarks +│ └── utils/ +│ ├── date-utils.ts # Date formatting, period navigation +│ └── icon-utils.ts # Icon picker modal +└── README.md +``` + +--- + +## Component Details + +### 1. Calendar Panel (`calendar-panel.ts`) + +**Visual layout (top to bottom):** +``` +┌─────────────────────────────────────┐ +│ Q3 · June · 1st, 2026 │ ← Breadcrumb: quarter, month, day (all clickable) +├─────────────────────────────────────┤ +│ ◀ June 2026 ▶ │ ← Month+Year header with nav +├─────────────────────────────────────┤ +│ Mon Tue Wed Thu Fri Sat Sun │ ← Day-of-week headers +├─────────────────────────────────────┤ +│ W22 1 2 3 4 5 6 │ ← Week rows (week number + 7 days) +│ W23 7 8 9 10 11 12 13 │ +│ W24 14 15 16 17 18 19 20 │ +│ W25 21 22 23 24 25 26 27 │ +│ W26 28 29 30 │ +├─────────────────────────────────────┤ +│ ◀ [Today] ▶ │ ← Today navigation +└─────────────────────────────────────┘ +``` + +**Behavior:** +- **Click a day cell** → open or create the daily note for that date +- **Click week number** → open/create the weekly note +- **Click breadcrumb quarter** → open/create the quarterly note +- **Click breadcrumb month** → open/create the monthly note +- **Click breadcrumb day** → open/create the daily note +- **Left/right arrows on month header** → navigate months +- **"Today" button** → jump to current month +- **Dot indicator** → show which days already have notes +- **Today highlight** → accent-colored border on current day +- **Week numbers** displayed as the first column, using ISO week numbering (weeks start on Monday) + +**Note creation logic:** +1. Check if a note exists at `{folder}/{nameTemplate}` using the date +2. If it exists, open it +3. If not, create it using the configured template file (via Obsidian API) +4. Apply the `type` property matching the period type (daily-note, weekly-note, etc.) + +### 2. Recent Files Panel (`recent-files-panel.ts`) + +Based closely on the recent-files-obsidian plugin: +- Track file opens/edits via `app.workspace.on('file-open')` +- Store as ordered list of `{ path, basename }` +- Display in a `nav-folder` styled list (matches Obsidian's file explorer look) +- Each item shows: + - File name (supporting frontmatter-title plugin if available) + - Hover preview via `hover-link` event + - Drag support (wikilink insertion) + - Right-click: "Open in new tab" + Obsidian file menu + - Middle-click / Ctrl+click: open in new pane + - X button on hover to remove from list +- Respect omitted paths/tags filter (from settings) +- Max length configurable + +### 3. Favorites / Bookmarks Panel (`favorites-panel.ts`) + +**Data model:** +```typescript +interface BookmarkItem { + id: string; // unique ID + type: 'file' | 'group'; + label: string; // display name (can differ from file name) + filePath: string; // path to the Obsidian file (empty for groups) + icon: string; // Lucide icon name (default: 'file') + children: BookmarkItem[]; // for groups + collapsed: boolean; // group fold state + indent: number; // indent level (0 = top level) +} +``` + +**Visual layout:** +``` +── Waypoint Bookmarks ──── +📁 Projects ← group (collapsible) + 📄 Fabrique ← file bookmark + 📄 Haro dashboard ← file bookmark +📁 References ← group + 📄 CSS grid notes +📄 Inbox ← ungrouped bookmark +``` + +**Behavior:** +- **Click file bookmark** → opens the file +- **Click group** → toggles collapse/expand +- **Drag to reorder** (stretch goal — use Obsidian's drag system) +- **Right-click on file bookmark** → context menu: + - Rename bookmark + - Change icon (opens icon picker) + - Remove bookmark + - Move to group → submenu of existing groups +- **Right-click on group** → context menu: + - Rename group + - Change icon + - Remove group (and all children) + - Add bookmark to this group + - New sub-group +- **Right-click on empty space** → context menu: + - New group + - Add current file + +**Add current file command:** +- Command ID: `waypoint:add-bookmark` +- Picks up the active file +- Shows a prompt to set the display name (defaults to file's basename) +- Shows an icon picker (default: file icon) +- Adds to root level (or active group if one is right-clicked) + +### 4. Settings Tab (`settings.ts`) + +``` +┌── Waypoint Settings ──────────────────┐ +│ │ +│ 📅 Calendar │ +│ ┌─ General ─────────────────────────┐ │ +│ │ First day of week [Monday ▼] │ │ +│ │ Week number standard [ISO ▼] │ │ +│ │ Show note indicators [toggle] │ │ +│ └──────────────────────────────────┘ │ +│ │ +│ ┌─ Periodic Note Paths ─────────────┐ │ +│ │ Daily note │ │ +│ │ Folder: [periodic/daily ] │ │ +│ │ Template: [templates/daily ] │ │ +│ │ Name: [yyyy-MM-dd ] │ │ +│ │ Type: [daily-note ] │ │ +│ │ │ │ +│ │ Weekly note │ │ +│ │ Folder: [periodic/weekly ] │ │ +│ │ Template: [templates/weekly ] │ │ +│ │ Name: [GGGG-[W]WW ] │ │ +│ │ Type: [weekly-note ] │ │ +│ │ │ │ +│ │ Monthly note │ │ +│ │ Folder: [periodic/monthly ] │ │ +│ │ ... │ │ +│ │ │ │ +│ │ Quarterly note │ │ +│ │ ... │ │ +│ │ │ │ +│ │ Yearly note │ │ +│ │ ... │ │ +│ └──────────────────────────────────┘ │ +│ │ +│ 📄 Recent Files │ +│ ┌──────────────────────────────────┐ │ +│ │ Max items: [50 ] │ │ +│ │ Update on: [File opened ▼] │ │ +│ │ Omitted paths: [textarea ] │ │ +│ │ Omitted tags: [textarea ] │ │ +│ └──────────────────────────────────┘ │ +│ │ +│ ⭐ Favorites │ +│ ┌──────────────────────────────────┐ │ +│ │ (No specific settings yet) │ │ +│ └──────────────────────────────────┘ │ +└────────────────────────────────────────┘ +``` + +--- + +## Implementation Steps + +### Step 1: Project scaffolding +- Create `manifest.json`, `package.json`, `tsconfig.json`, `esbuild.config.mjs` +- Install deps: `obsidian`, `builtin-modules`, typescript +- Create folder structure + +### Step 2: Settings +- Define interfaces: `WaypointSettings`, `PeriodNoteSettings` +- Defaults matching O-vault convention: + - Daily: `periodic/daily/`, template `Templates/Daily note`, name `yyyy-MM-dd` + - Weekly: `periodic/weekly/`, template `Templates/Weekly note`, name `GGGG-[W]WW` + - Monthly: `periodic/monthly/`, template `Templates/Monthly note`, name `yyyy-MM` + - Quarterly: `periodic/quarterly/`, template `Templates/Quarterly note`, name `yyyy-[Q]Q` + - Yearly: `periodic/yearly/`, template `Templates/Yearly note`, name `yyyy` +- Implement `WaypointSettingTab` with sections for each period + recent files + +### Step 3: Calendar Service +- `getMonthGrid(year, month, firstDayOfWeek)` → array of weeks with day objects +- `getPeriodFromDate(date)` → { day, week, month, quarter, year } +- `findOrCreateNote(period, settings)` → checks vault, creates if missing +- Date formatting using `moment()` (bundled in Obsidian) + +### Step 4: Calendar Panel (ItemView section) +- Render the calendar grid using Obsidian DOM (`createDiv`, `createSpan`) +- Period header row (year/quarter/week) +- Month header with nav arrows +- Day-of-week header row +- Day cells (with today highlight, note indicators, other-month dimming) +- Arrow navigation for months +- Click handlers for each period type + +### Step 5: Recent Files Service +- Track `file-open` events +- Maintain ordered list (max length, dedup) +- Support omitted paths/tags filtering + +### Step 6: Recent Files Panel +- Render the list in `nav-folder` style +- Click, hover, drag, right-click, middle-click, delete button + +### Step 7: Favorites Service +- CRUD operations on `BookmarkItem[]` +- Persist via `Plugin.loadData/saveData` +- Add current file command +- Rename, icon change, remove, grouping + +### Step 8: Favorites Panel +- Render tree with collapsible groups +- Context menus for items and groups +- Icon display using `setIcon()` +- Drag support (stretch) + +### Step 9: Waypoint View (main orchestrator) +- `ItemView` subclass +- On open: create three sections inside `contentEl` +- Redraw on data changes +- Register as sidebar view type `waypoint-view` + +### Step 10: Main Plugin +- `onload()`: load data, register view, register commands, register events, add settings tab +- Auto-open on right sidebar on first load +- Commands: + - `waypoint:open-view` → reveal the waypoint sidebar + - `waypoint:add-bookmark` → add current file as bookmark + - `waypoint:go-to-today` → open today's daily note +- Events: + - `file-open` → update recent files + redraw + - `create`, `delete`, `rename` → update calendar note indicators + redraw + - Midnight check → refresh today highlight + +--- + +## Key Design Decisions + +1. **No React** — Using native Obsidian DOM APIs avoids build complexity and keeps the bundle small. The calendar grid is straightforward enough to render imperatively. + +2. **Flat data storage** — Bookmarks and recent files stored via Plugin.loadData/saveData as JSON. This makes them portable and compatible with Obsidian sync. + +3. **Separate from native bookmarks** — We never touch `app.internalPlugins.getEnabledPluginById('bookmarks')`. These are entirely custom bookmarks with their own data store. + +4. **Icon picker** — We'll build a simple icon picker modal using Obsidian's `SuggestModal` or a grid of Lucide icons that are commonly used (file, folder, star, heart, bookmark, flag, pin, etc.). We won't implement a searchable full icon browser initially — a curated selection of ~40 icons plus a text input for any Lucide icon name. + +5. **O-vault aware** — Default paths match Olivier's vault structure. All settings are overridable. + +--- + +## Stretch Goals (after MVP) + +- Drag-and-drop reordering in favorites panel +- Search/filter within bookmarks +- Multiple bookmark lists / profiles +- Sync bookmarks with Obsidian Sync +- Show note previews on calendar hover +- Custom CSS classes per bookmark (color coding) +- Nested groups beyond 2 levels +- Collapse/expand all in favorites +- Keyboard shortcuts for bookmark navigation diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..40f7e53 --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,46 @@ +import esbuild from 'esbuild'; +import process from 'process'; +import builtins from 'builtin-modules'; + +const banner = `/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +*/ +`; + +const prod = process.argv[2] === 'production'; + +const context = await esbuild.context({ + banner: { js: banner }, + entryPoints: ['src/main.ts'], + bundle: true, + external: [ + 'obsidian', + 'electron', + '@codemirror/autocomplete', + '@codemirror/collab', + '@codemirror/commands', + '@codemirror/language', + '@codemirror/lint', + '@codemirror/search', + '@codemirror/state', + '@codemirror/view', + '@lezer/common', + '@lezer/highlight', + '@lezer/lr', + ...builtins, + ], + format: 'cjs', + target: 'es2018', + logLevel: 'info', + sourcemap: prod ? false : 'inline', + treeShaking: true, + outfile: 'main.js', + minify: prod, +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..e447d46 --- /dev/null +++ b/main.js @@ -0,0 +1,16 @@ +/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +*/ + +var V=Object.defineProperty;var Q=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var U=(f,d)=>{for(var t in d)V(f,t,{get:d[t],enumerable:!0})},K=(f,d,t,e)=>{if(d&&typeof d=="object"||typeof d=="function")for(let a of H(d))!_.call(f,a)&&a!==t&&V(f,a,{get:()=>d[a],enumerable:!(e=Q(d,a))||e.enumerable});return f};var X=f=>K(V({},"__esModule",{value:!0}),f);var ee={};U(ee,{default:()=>R});module.exports=X(ee);var b=require("obsidian");var j={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:[]}};var u=require("obsidian"),W=class extends u.PluginSettingTab{constructor(d,t,e,a){super(d,t),this.plugin=t,this.settings=e,this.onSettingsChange=a}display(){let{containerEl:d}=this;d.empty(),new u.Setting(d).setHeading().setName("Calendar"),new u.Setting(d).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(s=>{this.settings.calendar.firstDayOfWeek=parseInt(s,10),this.saveAndRefresh()})}),new u.Setting(d).setName("Show note indicators").setDesc("Show a dot on days that have existing notes.").addToggle(a=>{a.setValue(this.settings.calendar.showNoteIndicators).onChange(s=>{this.settings.calendar.showNoteIndicators=s,this.saveAndRefresh()})}),new u.Setting(d).setHeading().setName("Periodic Note Paths"),this.addPeriodNoteSettings("Daily",this.settings.daily),this.addPeriodNoteSettings("Weekly",this.settings.weekly),this.addPeriodNoteSettings("Monthly",this.settings.monthly),this.addPeriodNoteSettings("Quarterly",this.settings.quarterly),this.addPeriodNoteSettings("Yearly",this.settings.yearly),new u.Setting(d).setHeading().setName("Recent Files"),new u.Setting(d).setName("Max items").setDesc("Maximum number of recent files to track.").addText(a=>{a.inputEl.setAttr("type","number"),a.inputEl.setAttr("placeholder","50"),a.setValue(String(this.settings.recentFiles.maxItems)),a.inputEl.onblur=()=>{let s=parseInt(a.getValue(),10);!isNaN(s)&&s>0&&(this.settings.recentFiles.maxItems=s,this.saveAndRefresh())}}),new u.Setting(d).setName("Update on").setDesc("When to add a file to the recent list.").addDropdown(a=>{a.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 t=new DocumentFragment;t.appendText("Regex patterns for paths to exclude. One per line."),new u.Setting(d).setName("Omitted paths").setDesc(t).addTextArea(a=>{a.inputEl.setAttr("rows",4),a.setPlaceholder(`^archives/ +\\.png$`),a.setValue(this.settings.recentFiles.omittedPaths.join(` +`)),a.inputEl.onblur=()=>{this.settings.recentFiles.omittedPaths=a.getValue().split(` +`).filter(s=>s.trim()),this.saveAndRefresh()}});let e=new DocumentFragment;e.appendText("Regex patterns for frontmatter tags to exclude. One per line."),new u.Setting(d).setName("Omitted tags").setDesc(e).addTextArea(a=>{a.inputEl.setAttr("rows",4),a.setPlaceholder(`ignore +archive`),a.setValue(this.settings.recentFiles.omittedTags.join(` +`)),a.inputEl.onblur=()=>{this.settings.recentFiles.omittedTags=a.getValue().split(` +`).filter(s=>s.trim()),this.saveAndRefresh()}})}addPeriodNoteSettings(d,t){new u.Setting(this.containerEl).setHeading().setName(d),new u.Setting(this.containerEl).setName("Folder").setDesc(`Folder path for ${d.toLowerCase()} notes.`).addText(e=>{e.setPlaceholder("periodic/daily"),e.setValue(t.folder),e.onChange(a=>{t.folder=a,this.saveAndRefresh()})}),new u.Setting(this.containerEl).setName("Name format").setDesc(`Date format for ${d.toLowerCase()} note filenames (moment.js format).`).addText(e=>{e.setPlaceholder("yyyy-MM-dd"),e.setValue(t.nameFormat),e.onChange(a=>{t.nameFormat=a,this.saveAndRefresh()})}),new u.Setting(this.containerEl).setName("Template file").setDesc("Path to the template file (without .md extension).").addText(e=>{e.setPlaceholder("Templates/Daily note"),e.setValue(t.templateFile),e.onChange(a=>{t.templateFile=a,this.saveAndRefresh()})}),new u.Setting(this.containerEl).setName("Type property").setDesc("Value for the 'type' frontmatter property.").addText(e=>{e.setPlaceholder("daily-note"),e.setValue(t.typeProperty),e.onChange(a=>{t.typeProperty=a,this.saveAndRefresh()})})}async saveAndRefresh(){await this.plugin.saveData(this.settings),this.onSettingsChange()}};var h=require("obsidian");var F=require("obsidian");function z(f,d,t){let e=(0,F.moment)({year:f,month:d,day:1}),a=(0,F.moment)(e).endOf("month"),s=(0,F.moment)(e).subtract((e.day()-t+7)%7,"days"),o=(0,F.moment)().startOf("day"),l=[],n=(0,F.moment)(s);for(;n.isBefore(a)||n.month()===d;){let i=[];for(let r=0;r<7;r++)i.push({date:(0,F.moment)(n),dayOfMonth:n.date(),isToday:n.isSame(o,"day"),isCurrentMonth:n.month()===d,isoWeekNumber:n.isoWeek()}),n.add(1,"day");if(l.push({weekNumber:i[0].isoWeekNumber,days:i}),l.length>=6)break}return l}var w="waypoint-view",A=class extends h.ItemView{constructor(t,e){super(t);this.redraw=()=>{this.contentEl.empty(),this.contentEl.addClass("waypoint-view"),this.renderFavorites(),this.renderRecentFiles(),this.renderCalendar()};this.currentDisplayMonth=(0,h.moment)().month();this.currentDisplayYear=(0,h.moment)().year();this.dragId=null;this.plugin=e}getViewType(){return w}getDisplayText(){return"Waypoint"}getIcon(){return"compass"}async onOpen(){this.redraw()}async onClose(){}renderCalendar(){let t=this.contentEl.createDiv({cls:"waypoint-section"});t.createDiv({cls:"waypoint-section-header",text:"Calendar"});let e=t.createDiv({cls:"waypoint-calendar"}),a=(0,h.moment)(),s=(0,h.moment)({year:this.currentDisplayYear,month:this.currentDisplayMonth,day:1}),o=e.createDiv({cls:"waypoint-calendar-top"}),l=o.createDiv({cls:"waypoint-calendar-breadcrumb"}),n=s.format("[Q]Q");l.createSpan({cls:"waypoint-clickable",text:n}).addEventListener("click",()=>{this.plugin.openPeriodNote("quarter",s)});let r=s.format("MMMM");l.createSpan({cls:"waypoint-clickable",text:r}).addEventListener("click",()=>{this.plugin.openPeriodNote("month",s)});let c=s.format("YYYY");l.createSpan({cls:"waypoint-clickable",text:c}).addEventListener("click",()=>{this.plugin.openPeriodNote("year",s)});let D=o.createDiv({cls:"waypoint-calendar-today-group"}),x=D.createEl("button",{cls:"waypoint-calendar-nav-btn"});(0,h.setIcon)(x,"chevron-left"),x.addEventListener("click",()=>this.navigateMonth(-1)),D.createEl("button",{cls:"waypoint-calendar-today-btn",text:"Today"}).addEventListener("click",()=>{this.currentDisplayMonth=(0,h.moment)().month(),this.currentDisplayYear=(0,h.moment)().year(),this.redraw()});let M=D.createEl("button",{cls:"waypoint-calendar-nav-btn"});(0,h.setIcon)(M,"chevron-right"),M.addEventListener("click",()=>this.navigateMonth(1));let E=e.createEl("table"),I=E.createEl("thead").createEl("tr");I.createEl("th",{text:""});let g=["sun","mon","tue","wed","thu","fri","sat"],Y=this.plugin.settings.calendar.firstDayOfWeek;for(let y=0;y<7;y++){let S=(Y+y)%7;I.createEl("th",{text:g[S]})}let v=E.createEl("tbody"),T=z(this.currentDisplayYear,this.currentDisplayMonth,this.plugin.settings.calendar.firstDayOfWeek);for(let y of T){let S=v.createEl("tr"),N=S.createEl("td",{cls:"waypoint-weeknum"});N.setText(String(y.weekNumber)),N.addEventListener("click",()=>{let k=y.days[0].date;this.plugin.openPeriodNote("week",k)});for(let k of y.days){let C=S.createEl("td",{cls:"waypoint-day"});if(C.setText(String(k.dayOfMonth)),k.isCurrentMonth||C.addClass("other-month"),k.isToday&&C.addClass("today"),this.plugin.settings.calendar.showNoteIndicators){let O=k.date.format("YYYY-MM-DD");this.plugin.app.vault.getFiles().some(q=>q.extension==="md"&&q.basename===O)&&C.addClass("has-note")}C.addEventListener("click",()=>{this.plugin.openPeriodNote("day",k.date)})}}}navigateMonth(t){let e=(0,h.moment)({year:this.currentDisplayYear,month:this.currentDisplayMonth}).add(t,"month");this.currentDisplayMonth=e.month(),this.currentDisplayYear=e.year(),this.redraw()}renderRecentFiles(){let t=this.contentEl.createDiv({cls:"waypoint-section waypoint-recent-files"});if(t.createDiv({cls:"waypoint-section-header",text:"Recent Files"}),this.plugin.recentFiles.length===0){t.createDiv({cls:"nav-file",text:"No recent files"});return}let e=this.app.workspace.getActiveFile(),a=t.createDiv({cls:"nav-folder mod-root"}),s=a.createDiv({cls:"nav-folder-children"});for(let o of this.plugin.recentFiles){let l=s.createDiv({cls:"tree-item nav-file"}),n=l.createDiv({cls:"tree-item-self is-clickable nav-file-title"});n.createDiv({cls:"tree-item-inner nav-file-title-content"}).setText(o.basename);let r=n.createDiv({cls:"tree-item-spacer"}),p=n.createDiv({cls:"waypoint-recent-remove"});(0,h.setIcon)(p,"x"),p.addEventListener("click",c=>{c.stopPropagation(),this.plugin.recentFiles=this.plugin.recentFiles.filter(m=>m.path!==o.path),this.plugin.saveSettings(),this.redraw()}),(0,h.setTooltip)(l,o.path),e&&o.path===e.path&&n.addClass("is-active"),n.setAttr("draggable","true"),n.addEventListener("dragstart",c=>{let m=this.app.metadataCache.getFirstLinkpathDest(o.path,"");m&&(this.app.dragManager.dragFile(c,m),this.app.dragManager.onDragStart(c,this.app.dragManager.dragFile(c,m)))}),n.addEventListener("mouseover",c=>{this.app.workspace.trigger("hover-link",{event:c,source:w,hoverParent:a,targetEl:l,linktext:o.path})}),n.addEventListener("contextmenu",c=>{let m=new h.Menu;m.addItem(x=>x.setSection("action").setTitle("Open in new tab").setIcon("file-plus").onClick(()=>this.focusFile(o,"tab")));let D=this.app.vault.getAbstractFileByPath(o.path);D&&this.app.workspace.trigger("file-menu",m,D,"link-context-menu"),m.showAtPosition({x:c.clientX,y:c.clientY})}),n.addEventListener("click",c=>{let m=h.Keymap.isModEvent(c);this.focusFile(o,m)}),n.addEventListener("mousedown",c=>{c.button===1&&(c.preventDefault(),this.focusFile(o,"tab"))})}}focusFile(t,e){let a=this.app.vault.getFiles().find(s=>s.path===t.path);a?this.app.workspace.getLeaf(e).openFile(a):(new h.Notice("Cannot find file"),this.plugin.recentFiles=this.plugin.recentFiles.filter(s=>s.path!==t.path),this.plugin.saveSettings(),this.redraw())}renderFavorites(){let t=this.contentEl.createDiv({cls:"waypoint-section waypoint-favorites"}),e=t.createDiv({cls:"waypoint-section-header"});if(e.setText("Waypoint Bookmarks"),e.addEventListener("contextmenu",a=>{let s=new h.Menu;s.addItem(o=>{o.setTitle("Add current file").setIcon("file-plus").onClick(()=>{let l=this.app.workspace.getActiveFile();l&&this.plugin.addBookmark(l.path,l.basename,"file")})}),s.addItem(o=>{o.setTitle("New group").setIcon("folder-plus").onClick(()=>{this.plugin.addBookmark("","New Group","group","folder")})}),s.addItem(o=>{o.setTitle("Add separator").setIcon("minus").onClick(()=>{this.plugin.addBookmark("","","separator")})}),s.addItem(o=>{o.setTitle("Add spacer").setIcon("space").onClick(()=>{this.plugin.addBookmark("","","spacer")})}),s.showAtPosition({x:a.clientX,y:a.clientY})}),this.plugin.waypointData.bookmarks.length===0){t.createDiv({cls:"waypoint-bookmark-item",text:"No bookmarks"});return}this.renderBookmarkList(t,this.plugin.waypointData.bookmarks,0)}renderBookmarkList(t,e,a){for(let s=0;s{this.dragId=o.id,r.dataTransfer.effectAllowed="move",r.dataTransfer.setData("text/plain",o.id),i.addClass("waypoint-bm-dragging")}),i.addEventListener("dragend",()=>{this.dragId=null,t.querySelectorAll(".waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below").forEach(r=>{r.removeClass("waypoint-bm-dragging"),r.removeClass("waypoint-bm-drop-line"),r.removeClass("waypoint-bm-drop-below")})}),i.addEventListener("dragenter",r=>{r.preventDefault(),!(!this.dragId||this.dragId===o.id)&&this.showDropIndicator(i,r)}),i.addEventListener("dragover",r=>{r.preventDefault(),!(!this.dragId||this.dragId===o.id)&&this.showDropIndicator(i,r)}),i.addEventListener("dragleave",()=>{i.removeClass("waypoint-bm-drop-line"),i.removeClass("waypoint-bm-drop-below")}),i.addEventListener("drop",r=>{var m;r.preventDefault(),this.dragId=null,i.removeClass("waypoint-bm-drop-line"),i.removeClass("waypoint-bm-drop-below");let p=(m=r.dataTransfer)==null?void 0:m.getData("text/plain");if(!p||p===o.id)return;let c=i.__dropAbove;this.moveBookmarkToPosition(p,o.id,c)}),i.addEventListener("contextmenu",r=>{r.stopPropagation(),r.preventDefault(),this.showBookmarkContextMenu(r,o)});continue}if(o.type==="spacer"){let i=t.createDiv({cls:"waypoint-bookmark-item waypoint-bookmark-spacer"});i.setAttr("draggable","true"),i.setAttr("data-bm-id",o.id),i.style.paddingLeft=`${8+a*16}px`,i.style.cursor="grab",i.addEventListener("dragstart",r=>{this.dragId=o.id,r.dataTransfer.effectAllowed="move",r.dataTransfer.setData("text/plain",o.id),i.addClass("waypoint-bm-dragging")}),i.addEventListener("dragend",()=>{this.dragId=null,t.querySelectorAll(".waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below").forEach(r=>{r.removeClass("waypoint-bm-dragging"),r.removeClass("waypoint-bm-drop-line"),r.removeClass("waypoint-bm-drop-below")})}),i.addEventListener("dragenter",r=>{r.preventDefault(),!(!this.dragId||this.dragId===o.id)&&this.showDropIndicator(i,r)}),i.addEventListener("dragover",r=>{r.preventDefault(),!(!this.dragId||this.dragId===o.id)&&this.showDropIndicator(i,r)}),i.addEventListener("dragleave",()=>{i.removeClass("waypoint-bm-drop-line"),i.removeClass("waypoint-bm-drop-below")}),i.addEventListener("drop",r=>{var m;r.preventDefault(),this.dragId=null,i.removeClass("waypoint-bm-drop-line"),i.removeClass("waypoint-bm-drop-below");let p=(m=r.dataTransfer)==null?void 0:m.getData("text/plain");if(!p||p===o.id)return;let c=i.__dropAbove;this.moveBookmarkToPosition(p,o.id,c)}),i.addEventListener("contextmenu",r=>{r.stopPropagation(),r.preventDefault(),this.showBookmarkContextMenu(r,o)});continue}let l=o.type==="group",n=t.createDiv({cls:`waypoint-bookmark-item${l?" waypoint-bookmark-group":""}${o.collapsed?" collapsed":""}`});if(n.setAttr("draggable","true"),n.setAttr("data-bm-id",o.id),l||(n.style.paddingLeft=`${8+a*16}px`),n.addEventListener("dragstart",i=>{this.dragId=o.id,i.dataTransfer.effectAllowed="move",i.dataTransfer.setData("text/plain",o.id),n.addClass("waypoint-bm-dragging")}),n.addEventListener("dragend",()=>{this.dragId=null,t.querySelectorAll(".waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below").forEach(i=>{i.removeClass("waypoint-bm-dragging"),i.removeClass("waypoint-bm-drop-line"),i.removeClass("waypoint-bm-drop-below")})}),n.addEventListener("dragenter",i=>{i.preventDefault(),!(!this.dragId||this.dragId===o.id)&&this.showDropIndicator(n,i)}),n.addEventListener("dragover",i=>{i.preventDefault(),!(!this.dragId||this.dragId===o.id)&&this.showDropIndicator(n,i)}),n.addEventListener("dragleave",()=>{n.removeClass("waypoint-bm-drop-line"),n.removeClass("waypoint-bm-drop-below")}),n.addEventListener("drop",i=>{var c;i.preventDefault(),this.dragId=null,n.removeClass("waypoint-bm-drop-line"),n.removeClass("waypoint-bm-drop-below");let r=(c=i.dataTransfer)==null?void 0:c.getData("text/plain");if(!r||r===o.id)return;let p=n.__dropAbove;this.moveBookmarkToPosition(r,o.id,p)}),l){let i=n.createDiv({cls:"waypoint-bm-chevron"});(0,h.setIcon)(i,"chevron-down");let r=n.createDiv({cls:"waypoint-bm-icon"});(0,h.setIcon)(r,o.icon||"folder");let p=n.createDiv({cls:"waypoint-bm-label",text:o.label});n.addEventListener("click",()=>{this.plugin.updateBookmark(o.id,{collapsed:!o.collapsed})});let c=t.createDiv({cls:`waypoint-bookmark-children${o.collapsed?" collapsed":""}`});o.children&&o.children.length>0&&this.renderBookmarkList(c,o.children,a+1)}else{let i=n.createDiv({cls:"waypoint-bm-icon"});(0,h.setIcon)(i,o.icon||"file");let r=n.createDiv({cls:"waypoint-bm-label",text:o.label});(0,h.setTooltip)(n,o.filePath),n.addEventListener("click",()=>{if(o.filePath){let p=this.app.vault.getFileByPath(o.filePath);p?this.app.workspace.getLeaf(!1).openFile(p):(new h.Notice("File not found"),this.plugin.removeBookmark(o.id))}})}n.addEventListener("contextmenu",i=>{i.stopPropagation(),i.preventDefault(),this.showBookmarkContextMenu(i,o)})}}showBookmarkContextMenu(t,e){let a=new h.Menu;e.type==="file"&&(a.addItem(n=>n.setTitle("Open in new tab").setIcon("file-plus").onClick(()=>{let i=this.app.vault.getFileByPath(e.filePath);i&&this.app.workspace.getLeaf("tab").openFile(i)})),a.addSeparator()),e.type!=="separator"&&e.type!=="spacer"&&(a.addItem(n=>n.setTitle("Rename").setIcon("pencil").onClick(()=>this.promptRename(e))),a.addItem(n=>n.setTitle("Change icon").setIcon("image").onClick(()=>this.promptIcon(e))));let s=this.collectGroups(this.plugin.waypointData.bookmarks),o=this.collectDescendantIds(e),l=s.filter(n=>!o.includes(n.id)&&n.id!==e.id);if((l.length>0||e.type==="file"||e.type==="group")&&e.type!=="separator"&&e.type!=="spacer"){if(a.addSeparator(),l.length>0)for(let n of l)a.addItem(i=>i.setTitle(` ${n.label}`).setIcon(n.icon||"folder").onClick(()=>{this.moveBookmarkToGroup(e.id,n.id)}));a.addItem(n=>n.setTitle(" (Root)").setIcon("layout").onClick(()=>{this.moveBookmarkToGroup(e.id,null)}))}e.type==="group"&&(a.addItem(n=>n.setTitle(e.collapsed?"Expand":"Collapse").setIcon(e.collapsed?"chevron-down":"chevron-up").onClick(()=>{this.plugin.updateBookmark(e.id,{collapsed:!e.collapsed})})),a.addSeparator(),a.addItem(n=>n.setTitle("Add bookmark here").setIcon("plus").onClick(()=>{let i=this.app.workspace.getActiveFile();if(!i){new h.Notice("No active file");return}let r={id:`bm-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:"file",label:i.basename,filePath:i.path,icon:"file",children:[],collapsed:!1,indent:e.indent+1};e.children.push(r),this.plugin.saveWaypointData(),this.redraw()})),a.addItem(n=>n.setTitle("New sub-group").setIcon("folder-plus").onClick(()=>{let i={id:`bm-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:"group",label:"New Group",filePath:"",icon:"folder",children:[],collapsed:!1,indent:e.indent+1};e.children.push(i),this.plugin.saveWaypointData(),this.redraw()}))),a.addSeparator(),a.addItem(n=>n.setTitle("Insert separator above").setIcon("separator-vertical").onClick(()=>this.insertAdjacent(e.id,"separator","above"))),a.addItem(n=>n.setTitle("Insert separator below").setIcon("separator-vertical").onClick(()=>this.insertAdjacent(e.id,"separator","below"))),a.addItem(n=>n.setTitle("Insert spacer above").setIcon("space").onClick(()=>this.insertAdjacent(e.id,"spacer","above"))),a.addItem(n=>n.setTitle("Insert spacer below").setIcon("space").onClick(()=>this.insertAdjacent(e.id,"spacer","below"))),a.addSeparator(),a.addItem(n=>n.setTitle("Remove").setIcon("trash").onClick(()=>{this.plugin.removeBookmark(e.id)})),a.showAtPosition({x:t.clientX,y:t.clientY})}insertAdjacent(t,e,a){let s={id:`bm-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:e,label:"",filePath:"",icon:"",children:[],collapsed:!1,indent:0},o=l=>{let n=l.findIndex(i=>i.id===t);if(n>=0){let i=a==="above"?n:n+1;return l.splice(i,0,s),!0}for(let i of l)if(i.children&&o(i.children))return!0;return!1};o(this.plugin.waypointData.bookmarks),this.plugin.saveWaypointData(),this.redraw()}collectGroups(t){let e=[];for(let a of t)a.type==="group"&&(e.push(a),e.push(...this.collectGroups(a.children)));return e}collectDescendantIds(t){let e=[];for(let a of t.children)e.push(a.id),e.push(...this.collectDescendantIds(a));return e}showDropIndicator(t,e){let a=t.parentElement;a&&a.querySelectorAll(".waypoint-bm-drop-line, .waypoint-bm-drop-below").forEach(n=>{n.removeClass("waypoint-bm-drop-line"),n.removeClass("waypoint-bm-drop-below")});let s=t.getBoundingClientRect(),o=s.top+s.height/2,l=e.clientY{let l=o.findIndex(n=>n.id===t);if(l>=0){let[n]=o.splice(l,1);return n}for(let n of o){let i=a(n.children);if(i)return i}return null},s=a(this.plugin.waypointData.bookmarks);if(s){if(e){let o=n=>{for(let i of n){if(i.id===e)return i;let r=o(i.children);if(r)return r}return null},l=o(this.plugin.waypointData.bookmarks);l&&(s.indent=l.indent+1,l.children.push(s))}else s.indent=0,this.plugin.waypointData.bookmarks.push(s);this.plugin.saveWaypointData(),this.redraw()}}moveBookmarkToPosition(t,e,a){let s=i=>{let r=i.findIndex(p=>p.id===t);if(r>=0){let[p]=i.splice(r,1);return{item:p,parent:i}}for(let p of i)if(p.children){let c=s(p.children);if(c.item)return c}return{item:null,parent:[]}},{item:o}=s(this.plugin.waypointData.bookmarks);if(!o)return;let l=i=>{let r=i.findIndex(p=>p.id===e);if(r>=0)return{parent:i,idx:r};for(let p of i)if(p.children){let c=l(p.children);if(c)return c}return null},n=l(this.plugin.waypointData.bookmarks);if(!n)o.indent=0,this.plugin.waypointData.bookmarks.push(o);else{let i=a?n.idx:n.idx+1;n.parent.splice(i,0,o)}this.plugin.saveWaypointData(),this.redraw()}promptRename(t){new $(this.app,t.label,e=>{e&&e.trim()&&this.plugin.updateBookmark(t.id,{label:e.trim()})}).open()}promptIcon(t){new G(this.app,t.icon,e=>{e&&this.plugin.updateBookmark(t.id,{icon:e})}).open()}},$=class extends h.Modal{constructor(d,t,e){super(d),this.currentValue=t,this.onSubmit=e}onOpen(){this.titleEl.setText("Rename bookmark");let d=this.contentEl.createEl("input",{type:"text",value:this.currentValue});d.style.width="100%",d.style.marginBottom="12px",d.focus(),d.select();let t=this.contentEl.createDiv({cls:"modal-button-container"}),e=t.createEl("button",{text:"Cancel",cls:"mod-cta"});e.style.marginRight="8px",e.addEventListener("click",()=>this.close()),t.createEl("button",{text:"Save",cls:"mod-cta"}).addEventListener("click",()=>{this.onSubmit(d.value),this.close()}),d.addEventListener("keydown",s=>{s.key==="Enter"&&(this.onSubmit(d.value),this.close())})}onClose(){this.contentEl.empty()}},G=class extends h.Modal{constructor(t,e,a){super(t);this.allIcons=[];this.loaded=!1;this.selected=e,this.onSubmit=a}async onOpen(){let t=this.contentEl;t.style.display="flex",t.style.flexDirection="column",t.style.gap="10px",this.titleEl.setText("Change icon");let e=t.createDiv({cls:"waypoint-icon-preview"});e.style.display="flex",e.style.alignItems="center",e.style.gap="10px",e.style.padding="12px 16px",e.style.borderRadius="8px",e.style.background="var(--background-secondary)",e.style.minHeight="48px";let a=e.createSpan();a.style.display="flex",this.selected&&(0,h.setIcon)(a,this.selected);let s=e.createSpan();s.style.fontWeight="var(--font-medium)",s.style.fontSize="var(--font-ui-medium)",s.setText(this.selected||"No icon");let o=t.createEl("input",{type:"text",placeholder:"Type to search (e.g. arrow, chart, home)..."});Object.assign(o.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)"}),o.focus();let l=t.createDiv({cls:"waypoint-icon-grid"});l.style.display="grid",l.style.gridTemplateColumns="repeat(auto-fill, minmax(52px, 1fr))",l.style.gap="4px",l.style.maxHeight="320px",l.style.overflowY="auto",l.style.padding="2px 0";let n=t.createDiv();n.style.display="flex",n.style.justifyContent="space-between",n.style.alignItems="center",n.style.fontSize="var(--font-ui-smaller)",n.style.color="var(--text-muted)",n.style.padding="0 4px";let i=n.createSpan();i.setText("Loading\u2026");let r=n.createSpan();r.style.cursor="var(--cursor)",r.style.color="var(--text-accent)",r.setText("No icon"),r.addEventListener("click",()=>{this.onSubmit(""),this.close()}),this.loadIcons().then(()=>{this.loaded=!0,i.setText(this.allIcons.length+" icons"),c(o.value)});let p=null,c=P=>{if(l.empty(),!this.loaded){l.createDiv({text:"Loading\u2026"});return}let M=P.toLowerCase().trim(),E=M?this.allIcons.filter(function(v){return v.includes(M)}).slice(0,80):this.allIcons.slice(0,80);if(E.length===0){let v=l.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 "'+P+'"');return}for(var B=0;Bnew A(e,this)),this.addSettingTab(new W(this.app,this,this.settings,()=>this.redrawAll())),this.addCommand({id:"waypoint-open-view",name:"Open Waypoint sidebar",callback:async()=>{let e=this.app.workspace.getLeavesOfType(w);if(e.length>0)await this.app.workspace.revealLeaf(e[0]);else{let a=this.app.workspace.getLeftLeaf(!1);a&&(await a.setViewState({type:w}),await this.app.workspace.revealLeaf(a))}}}),this.addCommand({id:"waypoint-add-bookmark",name:"Add current file as Waypoint bookmark",callback:async()=>{let e=this.app.workspace.getActiveFile();if(!e){new b.Notice("No active file");return}this.addBookmark(e.path,e.basename,"file"),new b.Notice(`Bookmarked: ${e.basename}`)}}),this.addCommand({id:"waypoint-go-to-today",name:"Go to today's daily note",callback:async()=>{await this.openPeriodNote("day",(0,b.moment)())}}),this.registerEvent(this.app.workspace.on("file-open",e=>{e&&this.onFileOpen(e)})),this.registerEvent(this.app.vault.on("create",()=>this.onVaultChange())),this.registerEvent(this.app.vault.on("delete",()=>this.onVaultChange())),this.registerEvent(this.app.vault.on("rename",(e,a)=>this.onRename(e,a))),this.app.workspace.onLayoutReady(()=>{if(this.app.workspace.getLeavesOfType(w).length===0){let a=this.app.workspace.getLeftLeaf(!1);a&&a.setViewState({type:w})}});let t=new Date().toDateString();this.registerInterval(window.setInterval(()=>{let e=new Date().toDateString();e!==t&&(t=e,this.redrawAll())},6e5))}async onunload(){this.app.workspace.detachLeavesOfType(w)}async loadSettings(){let t=await this.loadData(),e=(t==null?void 0:t.settings)||{};this.settings=Object.assign({},j,e)}async saveSettings(){let t=await this.loadData()||{};t.settings=this.settings,await this.saveData(t)}async loadWaypointData(){let t=await this.loadData(),e=(t==null?void 0:t.waypointData)||{};this.waypointData=Object.assign({},Z,e)}async saveWaypointData(){let t=await this.loadData()||{};t.waypointData=this.waypointData,await this.saveData(t)}onFileOpen(t){this.settings.recentFiles.updateOn!=="file-edit"&&this.addToRecentFiles(t)}addToRecentFiles(t){this.recentFiles=this.recentFiles.filter(e=>e.path!==t.path),this.recentFiles.unshift({path:t.path,basename:t.basename}),this.recentFiles.length>this.settings.recentFiles.maxItems&&(this.recentFiles=this.recentFiles.slice(0,this.settings.recentFiles.maxItems)),this.broadcastRedraw()}onRename(t,e){let a=this.recentFiles.find(s=>s.path===e);a&&(a.path=t.path,a.basename=t.basename||t.name.replace(/\.[^/.]+$/,""),this.broadcastRedraw()),this.updateBookmarkPath(e,t.path)}onVaultChange(){this.broadcastRedraw()}addBookmark(t,e,a="file",s="file"){let l={id:`bm-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:a,label:e,filePath:a==="file"?t:"",icon:s,children:[],collapsed:!1,indent:0};return this.waypointData.bookmarks.push(l),this.saveWaypointData(),this.broadcastRedraw(),l}removeBookmark(t){let e=a=>{let s=a.findIndex(o=>o.id===t);if(s>=0)return a.splice(s,1),!0;for(let o of a)if(o.children&&e(o.children))return!0;return!1};e(this.waypointData.bookmarks),this.saveWaypointData(),this.broadcastRedraw()}updateBookmark(t,e){let a=o=>{for(let l of o){if(l.id===t)return l;if(l.children){let n=a(l.children);if(n)return n}}return null},s=a(this.waypointData.bookmarks);s&&(Object.assign(s,e),this.saveWaypointData(),this.broadcastRedraw())}updateBookmarkPath(t,e){let a=s=>{for(let o of s)o.filePath===t&&(o.filePath=e),o.children&&a(o.children)};a(this.waypointData.bookmarks)}async openPeriodNote(t,e){let s={day:{settings:this.settings.daily,label:"Daily"},week:{settings:this.settings.weekly,label:"Weekly"},month:{settings:this.settings.monthly,label:"Monthly"},quarter:{settings:this.settings.quarterly,label:"Quarterly"},year:{settings:this.settings.yearly,label:"Yearly"}}[t];if(!s)return;let{settings:o}=s,l=e.format(o.nameFormat)+".md",n=o.folder?`${o.folder}/${l}`:l,i=this.app.vault.getFileByPath(n);if(!i){try{let r=o.templateFile+".md",p=this.app.vault.getFileByPath(r);if(p){let c=await this.app.vault.read(p);i=await this.app.vault.create(n,c)}else{let c=`--- +type: ${o.typeProperty} +date: ${e.format("YYYY-MM-DD")} +--- + +`;i=await this.app.vault.create(n,c)}}catch(r){new b.Notice(`Failed to create ${s.label.toLowerCase()} note: ${r.message}`);return}new b.Notice(`Created ${s.label.toLowerCase()} note: ${l}`)}i&&await this.app.workspace.getLeaf(!1).openFile(i)}redrawAll(){this.broadcastRedraw()}broadcastRedraw(){let t=this.app.workspace.getLeavesOfType(w);for(let e of t)e.view instanceof A&&e.view.redraw()}async getNotesForDate(t){return this.app.vault.getFiles().filter(e=>e.extension==="md"&&e.basename===t)}async hasNoteForDate(t){return this.app.vault.getFiles().some(e=>e.extension==="md"&&e.basename===t)}}; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..26891ec --- /dev/null +++ b/manifest.json @@ -0,0 +1,9 @@ +{ + "id": "waypoint", + "name": "Waypoint", + "version": "1.0.0", + "minAppVersion": "0.16.3", + "description": "Calendar, recent files, and custom bookmarks sidebar.", + "author": "Olivier", + "isDesktopOnly": false +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5e6b924 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,639 @@ +{ + "name": "waypoint", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "waypoint", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "builtin-modules": "4.0.0", + "esbuild": "^0.25.0", + "obsidian": "latest", + "typescript": "^5.6.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", + "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/builtin-modules": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz", + "integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/obsidian": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.13.0.tgz", + "integrity": "sha512-PHw5+SAPlJ0S3leFvJ0wgFg63Z3DavxL6+d1ll+8toXR2ZlYKc1rMWqdUv9LgUbTwPQUyY6yfhOMMivampRRiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.6" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, + "license": "MIT", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2c66075 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "waypoint", + "version": "1.0.0", + "description": "Calendar, recent files, and custom bookmarks sidebar.", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "node esbuild.config.mjs production" + }, + "keywords": [], + "author": "Olivier", + "license": "MIT", + "devDependencies": { + "builtin-modules": "4.0.0", + "esbuild": "^0.25.0", + "obsidian": "latest", + "typescript": "^5.6.0" + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..0c8caa2 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,352 @@ +// ── Main plugin entry ── + +import { + Plugin, + WorkspaceLeaf, + ItemView, + Notice, + TFile, + TAbstractFile, + moment, +} from 'obsidian'; +import { WaypointSettings, DEFAULT_SETTINGS, PeriodNoteSettings } from 'src/settings'; +import { WaypointSettingTab } from 'src/settings-tab'; +import { WaypointView, WAYPOINT_VIEW_TYPE } from 'src/views/waypoint-view'; +import { BookmarkItem, WaypointData } from 'src/models/bookmark'; + +const DEFAULT_DATA: WaypointData = { + bookmarks: [], +}; + +export default class WaypointPlugin extends Plugin { + public settings: WaypointSettings; + public waypointData: WaypointData; + public recentFiles: { path: string; basename: string }[] = []; + + async onload(): Promise { + console.debug('Waypoint: loading plugin v' + this.manifest.version); + + // Load persisted data + await this.loadSettings(); + await this.loadWaypointData(); + + // Register the sidebar view + this.registerView( + WAYPOINT_VIEW_TYPE, + (leaf: WorkspaceLeaf) => new WaypointView(leaf, this), + ); + + // Register settings tab + this.addSettingTab(new WaypointSettingTab( + this.app, + this, + this.settings, + () => this.redrawAll(), + )); + + // ── Commands ── + + this.addCommand({ + id: 'waypoint-open-view', + name: 'Open Waypoint sidebar', + callback: async () => { + const leaves = this.app.workspace.getLeavesOfType(WAYPOINT_VIEW_TYPE); + if (leaves.length > 0) { + await this.app.workspace.revealLeaf(leaves[0]); + } else { + const leaf = this.app.workspace.getLeftLeaf(false); + if (leaf) { + await leaf.setViewState({ type: WAYPOINT_VIEW_TYPE }); + await this.app.workspace.revealLeaf(leaf); + } + } + }, + }); + + this.addCommand({ + id: 'waypoint-add-bookmark', + name: 'Add current file as Waypoint bookmark', + callback: async () => { + const file = this.app.workspace.getActiveFile(); + if (!file) { + new Notice('No active file'); + return; + } + this.addBookmark(file.path, file.basename, 'file'); + new Notice(`Bookmarked: ${file.basename}`); + }, + }); + + this.addCommand({ + id: 'waypoint-go-to-today', + name: 'Go to today\'s daily note', + callback: async () => { + await this.openPeriodNote('day', moment()); + }, + }); + + // ── Events ── + + this.registerEvent( + this.app.workspace.on('file-open', (file: TFile | null) => { + if (file) this.onFileOpen(file); + }), + ); + + this.registerEvent( + this.app.vault.on('create', () => this.onVaultChange()), + ); + this.registerEvent( + this.app.vault.on('delete', () => this.onVaultChange()), + ); + this.registerEvent( + this.app.vault.on('rename', (file, oldPath) => this.onRename(file, oldPath)), + ); + + // Auto-open view on first load + this.app.workspace.onLayoutReady(() => { + const leaves = this.app.workspace.getLeavesOfType(WAYPOINT_VIEW_TYPE); + if (leaves.length === 0) { + const leaf = this.app.workspace.getLeftLeaf(false); + if (leaf) { + leaf.setViewState({ type: WAYPOINT_VIEW_TYPE }); + } + } + }); + + // Midnight refresh (check every 10 min) + let lastDate = new Date().toDateString(); + this.registerInterval( + window.setInterval(() => { + const currentDate = new Date().toDateString(); + if (currentDate !== lastDate) { + lastDate = currentDate; + this.redrawAll(); + } + }, 600000), // 10 min + ); + } + + async onunload(): Promise { + this.app.workspace.detachLeavesOfType(WAYPOINT_VIEW_TYPE); + } + + // ── Settings ── + + async loadSettings(): Promise { + const saved = await this.loadData() as Record | null; + const s = (saved?.settings || {}) as Partial; + this.settings = Object.assign({}, DEFAULT_SETTINGS, s); + } + + async saveSettings(): Promise { + const all = (await this.loadData()) as Record || {}; + all.settings = this.settings; + await this.saveData(all); + } + + async loadWaypointData(): Promise { + const saved = await this.loadData() as Record | null; + const d = (saved?.waypointData || {}) as Partial; + this.waypointData = Object.assign({}, DEFAULT_DATA, d); + } + + async saveWaypointData(): Promise { + const all = (await this.loadData()) as Record || {}; + all.waypointData = this.waypointData; + await this.saveData(all); + } + + // ── Recent Files ── + + private onFileOpen(file: TFile): void { + if (this.settings.recentFiles.updateOn === 'file-edit') { + // We'll handle this via quick-preview in a future refinement + return; + } + this.addToRecentFiles(file); + } + + addToRecentFiles(file: TFile): void { + this.recentFiles = this.recentFiles.filter(f => f.path !== file.path); + this.recentFiles.unshift({ path: file.path, basename: file.basename }); + + // Prune + if (this.recentFiles.length > this.settings.recentFiles.maxItems) { + this.recentFiles = this.recentFiles.slice(0, this.settings.recentFiles.maxItems); + } + + this.broadcastRedraw(); + } + + private onRename(file: TAbstractFile, oldPath: string): void { + const entry = this.recentFiles.find(f => f.path === oldPath); + if (entry) { + entry.path = file.path; + entry.basename = (file as TFile).basename || file.name.replace(/\.[^/.]+$/, ''); + this.broadcastRedraw(); + } + + // Update bookmark file paths + this.updateBookmarkPath(oldPath, file.path); + } + + private onVaultChange(): void { + this.broadcastRedraw(); + } + + // ── Bookmarks ── + + addBookmark(filePath: string, label: string, type: 'file' | 'group' | 'separator' | 'spacer' = 'file', icon = 'file'): BookmarkItem { + const id = `bm-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const item: BookmarkItem = { + id, + type, + label, + filePath: type === 'file' ? filePath : '', + icon, + children: [], + collapsed: false, + indent: 0, + }; + this.waypointData.bookmarks.push(item); + this.saveWaypointData(); + this.broadcastRedraw(); + return item; + } + + removeBookmark(id: string): void { + const removeRecursive = (items: BookmarkItem[]): boolean => { + const idx = items.findIndex(i => i.id === id); + if (idx >= 0) { + items.splice(idx, 1); + return true; + } + for (const item of items) { + if (item.children && removeRecursive(item.children)) return true; + } + return false; + }; + removeRecursive(this.waypointData.bookmarks); + this.saveWaypointData(); + this.broadcastRedraw(); + } + + updateBookmark(id: string, updates: Partial): void { + const findRecursive = (items: BookmarkItem[]): BookmarkItem | null => { + for (const item of items) { + if (item.id === id) return item; + if (item.children) { + const found = findRecursive(item.children); + if (found) return found; + } + } + return null; + }; + const item = findRecursive(this.waypointData.bookmarks); + if (item) { + Object.assign(item, updates); + this.saveWaypointData(); + this.broadcastRedraw(); + } + } + + private updateBookmarkPath(oldPath: string, newPath: string): void { + const updateRecursive = (items: BookmarkItem[]) => { + for (const item of items) { + if (item.filePath === oldPath) { + item.filePath = newPath; + } + if (item.children) updateRecursive(item.children); + } + }; + updateRecursive(this.waypointData.bookmarks); + } + + // ── Period note creation/opening ── + + async openPeriodNote(period: 'day' | 'week' | 'month' | 'quarter' | 'year', date: moment.Moment): Promise { + // Map period to settings and date format + type PeriodConfig = { + settings: PeriodNoteSettings; + label: string; + }; + + const configs: Record = { + day: { settings: this.settings.daily, label: 'Daily' }, + week: { settings: this.settings.weekly, label: 'Weekly' }, + month: { settings: this.settings.monthly, label: 'Monthly' }, + quarter: { settings: this.settings.quarterly, label: 'Quarterly' }, + year: { settings: this.settings.yearly, label: 'Yearly' }, + }; + + const config = configs[period]; + if (!config) return; + + const { settings: periodSettings } = config; + const filename = date.format(periodSettings.nameFormat) + '.md'; + const fullPath = periodSettings.folder + ? `${periodSettings.folder}/${filename}` + : filename; + + // Check if exists + let file = this.app.vault.getFileByPath(fullPath); + if (!file) { + // Create it from template + try { + // Try to find template file + const templatePath = periodSettings.templateFile + '.md'; + const templateFile = this.app.vault.getFileByPath(templatePath); + + if (templateFile) { + const templateContent = await this.app.vault.read(templateFile); + file = await this.app.vault.create(fullPath, templateContent); + } else { + // Fallback: create with minimal frontmatter + const content = `---\ntype: ${periodSettings.typeProperty}\ndate: ${date.format('YYYY-MM-DD')}\n---\n\n`; + file = await this.app.vault.create(fullPath, content); + } + } catch (err) { + new Notice(`Failed to create ${config.label.toLowerCase()} note: ${err.message}`); + return; + } + new Notice(`Created ${config.label.toLowerCase()} note: ${filename}`); + } + + if (file) { + const leaf = this.app.workspace.getLeaf(false); + await leaf.openFile(file); + } + } + + // ── Redraw ── + + private redrawAll(): void { + this.broadcastRedraw(); + } + + private broadcastRedraw(): void { + const leaves = this.app.workspace.getLeavesOfType(WAYPOINT_VIEW_TYPE); + for (const leaf of leaves) { + if (leaf.view instanceof WaypointView) { + leaf.view.redraw(); + } + } + } + + /** + * Get all markdown files that exist on a specific date. + * Used to show note indicators on the calendar. + */ + async getNotesForDate(dateStr: string): Promise { + return this.app.vault.getFiles().filter(f => + f.extension === 'md' && f.basename === dateStr, + ); + } + + async hasNoteForDate(dateStr: string): Promise { + return this.app.vault.getFiles().some(f => + f.extension === 'md' && f.basename === dateStr, + ); + } +} diff --git a/src/models/bookmark.ts b/src/models/bookmark.ts new file mode 100644 index 0000000..852f2ef --- /dev/null +++ b/src/models/bookmark.ts @@ -0,0 +1,16 @@ +// ── Bookmark data model ── + +export interface BookmarkItem { + id: string; + type: 'file' | 'group' | 'separator' | 'spacer'; + label: string; + filePath: string; + icon: string; + children: BookmarkItem[]; + collapsed: boolean; + indent: number; +} + +export interface WaypointData { + bookmarks: BookmarkItem[]; +} diff --git a/src/settings-tab.ts b/src/settings-tab.ts new file mode 100644 index 0000000..841a3af --- /dev/null +++ b/src/settings-tab.ts @@ -0,0 +1,178 @@ +import { Setting, PluginSettingTab, App, Plugin } from 'obsidian'; +import { WaypointSettings, DEFAULT_SETTINGS, PeriodNoteSettings } from 'src/settings'; + +export class WaypointSettingTab extends PluginSettingTab { + private plugin: Plugin; + private settings: WaypointSettings; + private onSettingsChange: () => void; + + constructor(app: App, plugin: Plugin, settings: WaypointSettings, onSettingsChange: () => void) { + super(app, plugin); + this.plugin = plugin; + this.settings = settings; + this.onSettingsChange = onSettingsChange; + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + + // ── Calendar Section ── + new Setting(containerEl).setHeading().setName('Calendar'); + + new Setting(containerEl) + .setName('First day of week') + .setDesc('Which day the calendar week starts on.') + .addDropdown((dropdown) => { + dropdown + .addOption('0', 'Sunday') + .addOption('1', 'Monday') + .setValue(String(this.settings.calendar.firstDayOfWeek)) + .onChange((value) => { + this.settings.calendar.firstDayOfWeek = parseInt(value, 10); + this.saveAndRefresh(); + }); + }); + + new Setting(containerEl) + .setName('Show note indicators') + .setDesc('Show a dot on days that have existing notes.') + .addToggle((toggle) => { + toggle + .setValue(this.settings.calendar.showNoteIndicators) + .onChange((value) => { + this.settings.calendar.showNoteIndicators = value; + this.saveAndRefresh(); + }); + }); + + // ── Periodic Note Settings ── + new Setting(containerEl).setHeading().setName('Periodic Note Paths'); + + this.addPeriodNoteSettings('Daily', this.settings.daily); + this.addPeriodNoteSettings('Weekly', this.settings.weekly); + this.addPeriodNoteSettings('Monthly', this.settings.monthly); + this.addPeriodNoteSettings('Quarterly', this.settings.quarterly); + this.addPeriodNoteSettings('Yearly', this.settings.yearly); + + // ── Recent Files Section ── + new Setting(containerEl).setHeading().setName('Recent Files'); + + new Setting(containerEl) + .setName('Max items') + .setDesc('Maximum number of recent files to track.') + .addText((text) => { + text.inputEl.setAttr('type', 'number'); + text.inputEl.setAttr('placeholder', '50'); + text.setValue(String(this.settings.recentFiles.maxItems)); + text.inputEl.onblur = () => { + const val = parseInt(text.getValue(), 10); + if (!isNaN(val) && val > 0) { + this.settings.recentFiles.maxItems = val; + this.saveAndRefresh(); + } + }; + }); + + new Setting(containerEl) + .setName('Update on') + .setDesc('When to add a file to the recent list.') + .addDropdown((dropdown) => { + dropdown + .addOption('file-open', 'File opened') + .addOption('file-edit', 'File changed') + .setValue(this.settings.recentFiles.updateOn) + .onChange((value: 'file-open' | 'file-edit') => { + this.settings.recentFiles.updateOn = value; + this.saveAndRefresh(); + }); + }); + + const pathDesc = new DocumentFragment(); + pathDesc.appendText('Regex patterns for paths to exclude. One per line.'); + new Setting(containerEl) + .setName('Omitted paths') + .setDesc(pathDesc) + .addTextArea((text) => { + text.inputEl.setAttr('rows', 4); + text.setPlaceholder('^archives/\n\\.png$'); + text.setValue(this.settings.recentFiles.omittedPaths.join('\n')); + text.inputEl.onblur = () => { + this.settings.recentFiles.omittedPaths = text.getValue().split('\n').filter(p => p.trim()); + this.saveAndRefresh(); + }; + }); + + const tagDesc = new DocumentFragment(); + tagDesc.appendText('Regex patterns for frontmatter tags to exclude. One per line.'); + new Setting(containerEl) + .setName('Omitted tags') + .setDesc(tagDesc) + .addTextArea((text) => { + text.inputEl.setAttr('rows', 4); + text.setPlaceholder('ignore\narchive'); + text.setValue(this.settings.recentFiles.omittedTags.join('\n')); + text.inputEl.onblur = () => { + this.settings.recentFiles.omittedTags = text.getValue().split('\n').filter(t => t.trim()); + this.saveAndRefresh(); + }; + }); + } + + private addPeriodNoteSettings(label: string, period: PeriodNoteSettings): void { + new Setting(this.containerEl).setHeading().setName(label); + + new Setting(this.containerEl) + .setName('Folder') + .setDesc(`Folder path for ${label.toLowerCase()} notes.`) + .addText((text) => { + text.setPlaceholder('periodic/daily'); + text.setValue(period.folder); + text.onChange((value) => { + period.folder = value; + this.saveAndRefresh(); + }); + }); + + new Setting(this.containerEl) + .setName('Name format') + .setDesc(`Date format for ${label.toLowerCase()} note filenames (moment.js format).`) + .addText((text) => { + text.setPlaceholder('yyyy-MM-dd'); + text.setValue(period.nameFormat); + text.onChange((value) => { + period.nameFormat = value; + this.saveAndRefresh(); + }); + }); + + new Setting(this.containerEl) + .setName('Template file') + .setDesc(`Path to the template file (without .md extension).`) + .addText((text) => { + text.setPlaceholder('Templates/Daily note'); + text.setValue(period.templateFile); + text.onChange((value) => { + period.templateFile = value; + this.saveAndRefresh(); + }); + }); + + new Setting(this.containerEl) + .setName('Type property') + .setDesc(`Value for the 'type' frontmatter property.`) + .addText((text) => { + text.setPlaceholder('daily-note'); + text.setValue(period.typeProperty); + text.onChange((value) => { + period.typeProperty = value; + this.saveAndRefresh(); + }); + }); + } + + private async saveAndRefresh(): Promise { + await this.plugin.saveData(this.settings); + this.onSettingsChange(); + } +} diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..ff3ec28 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,73 @@ +// ── Waypoint Settings ── + +export interface PeriodNoteSettings { + folder: string; + templateFile: string; + nameFormat: string; + typeProperty: string; +} + +export interface CalendarSettings { + firstDayOfWeek: number; // 0=Sunday, 1=Monday + showNoteIndicators: boolean; +} + +export interface RecentFilesSettings { + maxItems: number; + updateOn: 'file-open' | 'file-edit'; + omittedPaths: string[]; + omittedTags: string[]; +} + +export interface WaypointSettings { + calendar: CalendarSettings; + daily: PeriodNoteSettings; + weekly: PeriodNoteSettings; + monthly: PeriodNoteSettings; + quarterly: PeriodNoteSettings; + yearly: PeriodNoteSettings; + recentFiles: RecentFilesSettings; +} + +export const DEFAULT_SETTINGS: WaypointSettings = { + calendar: { + firstDayOfWeek: 1, // Monday + showNoteIndicators: true, + }, + daily: { + folder: 'periodic/daily', + templateFile: 'Templates/Daily note', + nameFormat: 'YYYY-MM-DD', + typeProperty: 'daily-note', + }, + weekly: { + folder: 'periodic/weekly', + templateFile: 'Templates/Weekly note', + nameFormat: 'GGGG-[W]WW', + typeProperty: 'weekly-note', + }, + monthly: { + folder: 'periodic/monthly', + templateFile: 'Templates/Monthly note', + nameFormat: 'YYYY-MM', + typeProperty: 'monthly-note', + }, + quarterly: { + folder: 'periodic/quarterly', + templateFile: 'Templates/Quarterly note', + nameFormat: 'YYYY-[Q]Q', + typeProperty: 'quarterly-note', + }, + yearly: { + folder: 'periodic/yearly', + templateFile: 'Templates/Yearly note', + nameFormat: 'YYYY', + typeProperty: 'yearly-note', + }, + recentFiles: { + maxItems: 50, + updateOn: 'file-open', + omittedPaths: [], + omittedTags: [], + }, +}; diff --git a/src/utils/date-utils.ts b/src/utils/date-utils.ts new file mode 100644 index 0000000..fcff07d --- /dev/null +++ b/src/utils/date-utils.ts @@ -0,0 +1,149 @@ +// ── Date utilities ── +// Uses moment.js which is bundled with Obsidian. +// Period format helpers following O-vault conventions. + +import { moment } from 'obsidian'; + +export interface PeriodInfo { + day: string; // yyyy-MM-dd + week: string; // GGGG-[W]WW + month: string; // yyyy-MM + quarter: string; // yyyy-[Q]Q + year: string; // yyyy +} + +export interface CalendarDay { + date: moment.Moment; + dayOfMonth: number; + isToday: boolean; + isCurrentMonth: boolean; + isoWeekNumber: number; +} + +export interface CalendarWeek { + weekNumber: number; + days: CalendarDay[]; +} + +/** + * Get period info for a given date. + */ +export function getPeriodInfo(date: moment.Moment): PeriodInfo { + return { + day: date.format('YYYY-MM-DD'), + week: date.format('GGGG-[W]WW'), + month: date.format('YYYY-MM'), + quarter: date.format('YYYY-[Q]Q'), + year: date.format('YYYY'), + }; +} + +/** + * Get quarter string from a moment date (e.g. "2026-Q3"). + */ +export function getQuarterString(date: moment.Moment): string { + return date.format('YYYY-[Q]Q'); +} + +/** + * Get the current date's period info. + */ +export function getTodayPeriod(): PeriodInfo { + return getPeriodInfo(moment()); +} + +/** + * Build a month grid as an array of weeks. + * Each week has 7 days, starting on the configured first day of week. + * ISO week numbers are calculated from the Monday of that week row. + */ +export function getMonthGrid( + year: number, + month: number, // 0-indexed (0 = January) + firstDayOfWeek: number, // 0 = Sunday, 1 = Monday +): CalendarWeek[] { + const firstOfMonth = moment({ year, month, day: 1 }); + const lastOfMonth = moment(firstOfMonth).endOf('month'); + + // Find the first day to display (may go back into previous month) + const startDate = moment(firstOfMonth) + .subtract((firstOfMonth.day() - firstDayOfWeek + 7) % 7, 'days'); + + const today = moment().startOf('day'); + const weeks: CalendarWeek[] = []; + let currentDay = moment(startDate); + + while (currentDay.isBefore(lastOfMonth) || currentDay.month() === month) { + const weekDays: CalendarDay[] = []; + + for (let i = 0; i < 7; i++) { + weekDays.push({ + date: moment(currentDay), + dayOfMonth: currentDay.date(), + isToday: currentDay.isSame(today, 'day'), + isCurrentMonth: currentDay.month() === month, + isoWeekNumber: currentDay.isoWeek(), + }); + currentDay.add(1, 'day'); + } + + weeks.push({ + weekNumber: weekDays[0].isoWeekNumber, + days: weekDays, + }); + + // Safety: break after 6 weeks + if (weeks.length >= 6) break; + } + + return weeks; +} + +/** + * Format a date label for the breadcrumb (e.g. "June 1st, 2026"). + */ +export function formatDateLabel(date: moment.Moment): string { + return date.format('MMMM Do, YYYY'); +} + +/** + * Format a month label (e.g. "June 2026"). + */ +export function formatMonthLabel(date: moment.Moment): string { + return date.format('MMMM YYYY'); +} + +/** + * Get readable quarter label (e.g. "Q3 2026"). + */ +export function formatQuarterLabel(date: moment.Moment): string { + return date.format('[Q]Q YYYY'); +} + +/** + * Navigate a date by a given period and amount. + */ +export function navigateDate( + date: moment.Moment, + period: 'day' | 'week' | 'month' | 'quarter' | 'year', + delta: number, +): moment.Moment { + if (period === 'quarter') { + return moment(date).add(delta * 3, 'months'); + } + return moment(date).add(delta, period); +} + +/** + * Create a note filename from a date using the given format string. + */ +export function formatPeriodName(date: moment.Moment, format: string): string { + return date.format(format); +} + +/** + * Get ISO week number for a given date. + */ +export function getISOWeek(date: moment.Moment): number { + return date.isoWeek(); +} diff --git a/src/views/waypoint-view.ts b/src/views/waypoint-view.ts new file mode 100644 index 0000000..d19a4cc --- /dev/null +++ b/src/views/waypoint-view.ts @@ -0,0 +1,1244 @@ +// ── Waypoint Sidebar View ── + +import { + ItemView, + WorkspaceLeaf, + Menu, + Keymap, + App, + Modal, + setIcon, + setTooltip, + Notice, + TFile, + moment, +} from 'obsidian'; +import type WaypointPlugin from 'src/main'; +import { getMonthGrid } from 'src/utils/date-utils'; +import { BookmarkItem } from 'src/models/bookmark'; + +export const WAYPOINT_VIEW_TYPE = 'waypoint-view'; + +export class WaypointView extends ItemView { + private plugin: WaypointPlugin; + + constructor(leaf: WorkspaceLeaf, plugin: WaypointPlugin) { + super(leaf); + this.plugin = plugin; + } + + getViewType(): string { + return WAYPOINT_VIEW_TYPE; + } + + getDisplayText(): string { + return 'Waypoint'; + } + + getIcon(): string { + return 'compass'; + } + + async onOpen(): Promise { + this.redraw(); + } + + async onClose(): Promise { + // Nothing to clean up + } + + readonly redraw = (): void => { + this.contentEl.empty(); + this.contentEl.addClass('waypoint-view'); + + this.renderFavorites(); + this.renderRecentFiles(); + this.renderCalendar(); + }; + + // ════════════════════════════════════════ + // Calendar panel + // ════════════════════════════════════════ + + private currentDisplayMonth: number = moment().month(); // 0-indexed + private currentDisplayYear: number = moment().year(); + private dragId: string | null = null; + + private renderCalendar(): void { + const section = this.contentEl.createDiv({ cls: 'waypoint-section' }); + section.createDiv({ cls: 'waypoint-section-header', text: 'Calendar' }); + + const cal = section.createDiv({ cls: 'waypoint-calendar' }); + + const now = moment(); + const displayDate = moment({ year: this.currentDisplayYear, month: this.currentDisplayMonth, day: 1 }); + + // ── Top row: breadcrumb + today nav ── + const topRow = cal.createDiv({ cls: 'waypoint-calendar-top' }); + + // Left: Q2 · June · 2026 + const breadcrumb = topRow.createDiv({ cls: 'waypoint-calendar-breadcrumb' }); + + const qLabel = displayDate.format('[Q]Q'); + const qEl = breadcrumb.createSpan({ cls: 'waypoint-clickable', text: qLabel }); + qEl.addEventListener('click', () => { + this.plugin.openPeriodNote('quarter', displayDate); + }); + + const mLabel = displayDate.format('MMMM'); + const mEl = breadcrumb.createSpan({ cls: 'waypoint-clickable', text: mLabel }); + mEl.addEventListener('click', () => { + this.plugin.openPeriodNote('month', displayDate); + }); + + const yLabel = displayDate.format('YYYY'); + const yEl = breadcrumb.createSpan({ cls: 'waypoint-clickable', text: yLabel }); + yEl.addEventListener('click', () => { + this.plugin.openPeriodNote('year', displayDate); + }); + + // Right: ◀ Today ▶ + const todayGroup = topRow.createDiv({ cls: 'waypoint-calendar-today-group' }); + + const prevBtn = todayGroup.createEl('button', { cls: 'waypoint-calendar-nav-btn' }); + setIcon(prevBtn, 'chevron-left'); + prevBtn.addEventListener('click', () => this.navigateMonth(-1)); + + const todayBtn = todayGroup.createEl('button', { cls: 'waypoint-calendar-today-btn', text: 'Today' }); + todayBtn.addEventListener('click', () => { + this.currentDisplayMonth = moment().month(); + this.currentDisplayYear = moment().year(); + this.redraw(); + }); + + const nextBtn = todayGroup.createEl('button', { cls: 'waypoint-calendar-nav-btn' }); + setIcon(nextBtn, 'chevron-right'); + nextBtn.addEventListener('click', () => this.navigateMonth(1)); + + // ── Calendar grid ── + const table = cal.createEl('table'); + const thead = table.createEl('thead'); + const headerRow = thead.createEl('tr'); + + // Week number column header (blank) + headerRow.createEl('th', { text: '' }); + + const dayNames = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; + const firstDay = this.plugin.settings.calendar.firstDayOfWeek; + for (let i = 0; i < 7; i++) { + const idx = (firstDay + i) % 7; + headerRow.createEl('th', { text: dayNames[idx] }); + } + + // ── Day rows ── + const tbody = table.createEl('tbody'); + const weeks = getMonthGrid( + this.currentDisplayYear, + this.currentDisplayMonth, + this.plugin.settings.calendar.firstDayOfWeek, + ); + + for (const week of weeks) { + const row = tbody.createEl('tr'); + + // Week number cell + const wnCell = row.createEl('td', { cls: 'waypoint-weeknum' }); + wnCell.setText(String(week.weekNumber)); + wnCell.addEventListener('click', () => { + const monday = week.days[0].date; + this.plugin.openPeriodNote('week', monday); + }); + + for (const day of week.days) { + const cell = row.createEl('td', { cls: 'waypoint-day' }); + cell.setText(String(day.dayOfMonth)); + + if (!day.isCurrentMonth) { + cell.addClass('other-month'); + } + if (day.isToday) { + cell.addClass('today'); + } + + if (this.plugin.settings.calendar.showNoteIndicators) { + const dateStr = day.date.format('YYYY-MM-DD'); + const hasNote = this.plugin.app.vault.getFiles().some( + f => f.extension === 'md' && f.basename === dateStr, + ); + if (hasNote) { + cell.addClass('has-note'); + } + } + + cell.addEventListener('click', () => { + this.plugin.openPeriodNote('day', day.date); + }); + } + } + + } + + private navigateMonth(delta: number): void { + const d = moment({ year: this.currentDisplayYear, month: this.currentDisplayMonth }).add(delta, 'month'); + this.currentDisplayMonth = d.month(); + this.currentDisplayYear = d.year(); + this.redraw(); + } + + // ════════════════════════════════════════ + // Recent Files panel + // ════════════════════════════════════════ + + private renderRecentFiles(): void { + const section = this.contentEl.createDiv({ cls: 'waypoint-section waypoint-recent-files' }); + section.createDiv({ cls: 'waypoint-section-header', text: 'Recent Files' }); + + if (this.plugin.recentFiles.length === 0) { + section.createDiv({ cls: 'nav-file', text: 'No recent files' }); + return; + } + + const openFile = this.app.workspace.getActiveFile(); + + const rootEl = section.createDiv({ cls: 'nav-folder mod-root' }); + const childrenEl = rootEl.createDiv({ cls: 'nav-folder-children' }); + + for (const file of this.plugin.recentFiles) { + const navFile = childrenEl.createDiv({ cls: 'tree-item nav-file' }); + const navFileTitle = navFile.createDiv({ cls: 'tree-item-self is-clickable nav-file-title' }); + const navFileTitleContent = navFileTitle.createDiv({ cls: 'tree-item-inner nav-file-title-content' }); + navFileTitleContent.setText(file.basename); + + const spacer = navFileTitle.createDiv({ cls: 'tree-item-spacer' }); + + // Remove button + const removeBtn = navFileTitle.createDiv({ cls: 'waypoint-recent-remove' }); + setIcon(removeBtn, 'x'); + removeBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.plugin.recentFiles = this.plugin.recentFiles.filter(f => f.path !== file.path); + this.plugin.saveSettings(); + this.redraw(); + }); + + setTooltip(navFile, file.path); + + if (openFile && file.path === openFile.path) { + navFileTitle.addClass('is-active'); + } + + // Drag + navFileTitle.setAttr('draggable', 'true'); + navFileTitle.addEventListener('dragstart', (event: DragEvent) => { + const tfile = this.app.metadataCache.getFirstLinkpathDest(file.path, ''); + if (tfile) { + (this.app as any).dragManager.dragFile(event, tfile); + (this.app as any).dragManager.onDragStart(event, (this.app as any).dragManager.dragFile(event, tfile)); + } + }); + + // Hover preview + navFileTitle.addEventListener('mouseover', (event: MouseEvent) => { + this.app.workspace.trigger('hover-link', { + event, + source: WAYPOINT_VIEW_TYPE, + hoverParent: rootEl, + targetEl: navFile, + linktext: file.path, + }); + }); + + // Right-click + navFileTitle.addEventListener('contextmenu', (event: MouseEvent) => { + const menu = new Menu(); + menu.addItem((item) => + item + .setSection('action') + .setTitle('Open in new tab') + .setIcon('file-plus') + .onClick(() => this.focusFile(file, 'tab')), + ); + const tfile = this.app.vault.getAbstractFileByPath(file.path); + if (tfile) { + this.app.workspace.trigger('file-menu', menu, tfile, 'link-context-menu'); + } + menu.showAtPosition({ x: event.clientX, y: event.clientY }); + }); + + // Left click + navFileTitle.addEventListener('click', (event: MouseEvent) => { + const newLeaf = Keymap.isModEvent(event); + this.focusFile(file, newLeaf); + }); + + // Middle click + navFileTitle.addEventListener('mousedown', (event: MouseEvent) => { + if (event.button === 1) { + event.preventDefault(); + this.focusFile(file, 'tab'); + } + }); + } + } + + private focusFile(file: { path: string; basename: string }, newLeaf: boolean | string | 'split'): void { + const targetFile = this.app.vault.getFiles().find(f => f.path === file.path); + if (targetFile) { + const leaf = this.app.workspace.getLeaf(newLeaf as any); + leaf.openFile(targetFile); + } else { + new Notice('Cannot find file'); + this.plugin.recentFiles = this.plugin.recentFiles.filter(f => f.path !== file.path); + this.plugin.saveSettings(); + this.redraw(); + } + } + + // ════════════════════════════════════════ + // Favorites panel + // ════════════════════════════════════════ + + private renderFavorites(): void { + const section = this.contentEl.createDiv({ cls: 'waypoint-section waypoint-favorites' }); + const headerEl = section.createDiv({ cls: 'waypoint-section-header' }); + headerEl.setText('Waypoint Bookmarks'); + + // Right-click on header area → add bookmark / group + headerEl.addEventListener('contextmenu', (event: MouseEvent) => { + const menu = new Menu(); + menu.addItem((item) => { + item + .setTitle('Add current file') + .setIcon('file-plus') + .onClick(() => { + const file = this.app.workspace.getActiveFile(); + if (!file) return; + this.plugin.addBookmark(file.path, file.basename, 'file'); + }); + }); + menu.addItem((item) => { + item + .setTitle('New group') + .setIcon('folder-plus') + .onClick(() => { + this.plugin.addBookmark('', 'New Group', 'group', 'folder'); + }); + }); + menu.addItem((item) => { + item + .setTitle('Add separator') + .setIcon('minus') + .onClick(() => { + this.plugin.addBookmark('', '', 'separator'); + }); + }); + menu.addItem((item) => { + item + .setTitle('Add spacer') + .setIcon('space') + .onClick(() => { + this.plugin.addBookmark('', '', 'spacer'); + }); + }); + + menu.showAtPosition({ x: event.clientX, y: event.clientY }); + }); + + if (this.plugin.waypointData.bookmarks.length === 0) { + section.createDiv({ cls: 'waypoint-bookmark-item', text: 'No bookmarks' }); + return; + } + + this.renderBookmarkList(section, this.plugin.waypointData.bookmarks, 0); + } + + private renderBookmarkList(container: HTMLElement, items: BookmarkItem[], depth: number): void { + for (let idx = 0; idx < items.length; idx++) { + const item = items[idx]; + + if (item.type === 'separator') { + const rowEl = container.createDiv({ + cls: 'waypoint-bookmark-item waypoint-bookmark-separator', + }); + rowEl.setAttr('draggable', 'true'); + rowEl.setAttr('data-bm-id', item.id); + rowEl.style.paddingLeft = `${8 + depth * 16}px`; + rowEl.style.cursor = 'grab'; + + // Drag events + rowEl.addEventListener('dragstart', (e) => { + this.dragId = item.id; + e.dataTransfer!.effectAllowed = 'move'; + e.dataTransfer!.setData('text/plain', item.id); + rowEl.addClass('waypoint-bm-dragging'); + }); + rowEl.addEventListener('dragend', () => { + this.dragId = null; + container.querySelectorAll('.waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below').forEach(el => { + el.removeClass('waypoint-bm-dragging'); + el.removeClass('waypoint-bm-drop-line'); + el.removeClass('waypoint-bm-drop-below'); + }); + }); + rowEl.addEventListener('dragenter', (e) => { + e.preventDefault(); + if (!this.dragId || this.dragId === item.id) return; + this.showDropIndicator(rowEl, e); + }); + rowEl.addEventListener('dragover', (e) => { + e.preventDefault(); + if (!this.dragId || this.dragId === item.id) return; + this.showDropIndicator(rowEl, e); + }); + rowEl.addEventListener('dragleave', () => { + rowEl.removeClass('waypoint-bm-drop-line'); + rowEl.removeClass('waypoint-bm-drop-below'); + }); + rowEl.addEventListener('drop', (e) => { + e.preventDefault(); + this.dragId = null; + rowEl.removeClass('waypoint-bm-drop-line'); + rowEl.removeClass('waypoint-bm-drop-below'); + const draggedId = e.dataTransfer?.getData('text/plain'); + if (!draggedId || draggedId === item.id) return; + const dropAbove = (rowEl as any).__dropAbove; + this.moveBookmarkToPosition(draggedId, item.id, dropAbove); + }); + + // Context menu + rowEl.addEventListener('contextmenu', (event: MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + this.showBookmarkContextMenu(event, item); + }); + + continue; + } + if (item.type === 'spacer') { + const rowEl = container.createDiv({ + cls: 'waypoint-bookmark-item waypoint-bookmark-spacer', + }); + rowEl.setAttr('draggable', 'true'); + rowEl.setAttr('data-bm-id', item.id); + rowEl.style.paddingLeft = `${8 + depth * 16}px`; + rowEl.style.cursor = 'grab'; + + // Drag events + rowEl.addEventListener('dragstart', (e) => { + this.dragId = item.id; + e.dataTransfer!.effectAllowed = 'move'; + e.dataTransfer!.setData('text/plain', item.id); + rowEl.addClass('waypoint-bm-dragging'); + }); + rowEl.addEventListener('dragend', () => { + this.dragId = null; + container.querySelectorAll('.waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below').forEach(el => { + el.removeClass('waypoint-bm-dragging'); + el.removeClass('waypoint-bm-drop-line'); + el.removeClass('waypoint-bm-drop-below'); + }); + }); + rowEl.addEventListener('dragenter', (e) => { + e.preventDefault(); + if (!this.dragId || this.dragId === item.id) return; + this.showDropIndicator(rowEl, e); + }); + rowEl.addEventListener('dragover', (e) => { + e.preventDefault(); + if (!this.dragId || this.dragId === item.id) return; + this.showDropIndicator(rowEl, e); + }); + rowEl.addEventListener('dragleave', () => { + rowEl.removeClass('waypoint-bm-drop-line'); + rowEl.removeClass('waypoint-bm-drop-below'); + }); + rowEl.addEventListener('drop', (e) => { + e.preventDefault(); + this.dragId = null; + rowEl.removeClass('waypoint-bm-drop-line'); + rowEl.removeClass('waypoint-bm-drop-below'); + const draggedId = e.dataTransfer?.getData('text/plain'); + if (!draggedId || draggedId === item.id) return; + const dropAbove = (rowEl as any).__dropAbove; + this.moveBookmarkToPosition(draggedId, item.id, dropAbove); + }); + + // Context menu + rowEl.addEventListener('contextmenu', (event: MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + this.showBookmarkContextMenu(event, item); + }); + + continue; + } + + const isGroup = item.type === 'group'; + const rowEl = container.createDiv({ + cls: `waypoint-bookmark-item${isGroup ? ' waypoint-bookmark-group' : ''}${item.collapsed ? ' collapsed' : ''}`, + }); + rowEl.setAttr('draggable', 'true'); + rowEl.setAttr('data-bm-id', item.id); + if (!isGroup) { + rowEl.style.paddingLeft = `${8 + depth * 16}px`; + } + + // ── Drag events ── + rowEl.addEventListener('dragstart', (e) => { + this.dragId = item.id; + e.dataTransfer!.effectAllowed = 'move'; + e.dataTransfer!.setData('text/plain', item.id); + rowEl.addClass('waypoint-bm-dragging'); + }); + + rowEl.addEventListener('dragend', () => { + this.dragId = null; + container.querySelectorAll('.waypoint-bm-dragging, .waypoint-bm-drop-line, .waypoint-bm-drop-below').forEach(el => { + el.removeClass('waypoint-bm-dragging'); + el.removeClass('waypoint-bm-drop-line'); + el.removeClass('waypoint-bm-drop-below'); + }); + }); + + rowEl.addEventListener('dragenter', (e) => { + e.preventDefault(); + if (!this.dragId || this.dragId === item.id) return; + this.showDropIndicator(rowEl, e); + }); + + rowEl.addEventListener('dragover', (e) => { + e.preventDefault(); + if (!this.dragId || this.dragId === item.id) return; + this.showDropIndicator(rowEl, e); + }); + + rowEl.addEventListener('dragleave', () => { + rowEl.removeClass('waypoint-bm-drop-line'); + rowEl.removeClass('waypoint-bm-drop-below'); + }); + + rowEl.addEventListener('drop', (e) => { + e.preventDefault(); + this.dragId = null; + rowEl.removeClass('waypoint-bm-drop-line'); + rowEl.removeClass('waypoint-bm-drop-below'); + + const draggedId = e.dataTransfer?.getData('text/plain'); + if (!draggedId || draggedId === item.id) return; + + const dropAbove = (rowEl as any).__dropAbove; + this.moveBookmarkToPosition(draggedId, item.id, dropAbove); + }); + + // ── Group: chevron + icon + label ── + if (isGroup) { + const chevron = rowEl.createDiv({ cls: 'waypoint-bm-chevron' }); + setIcon(chevron, 'chevron-down'); + + const iconEl = rowEl.createDiv({ cls: 'waypoint-bm-icon' }); + setIcon(iconEl, item.icon || 'folder'); + + const labelEl = rowEl.createDiv({ cls: 'waypoint-bm-label', text: item.label }); + + rowEl.addEventListener('click', () => { + this.plugin.updateBookmark(item.id, { collapsed: !item.collapsed }); + }); + + const childrenContainer = container.createDiv({ + cls: `waypoint-bookmark-children${item.collapsed ? ' collapsed' : ''}`, + }); + if (item.children && item.children.length > 0) { + this.renderBookmarkList(childrenContainer, item.children, depth + 1); + } + + // ── File: icon + label ── + } else { + const iconEl = rowEl.createDiv({ cls: 'waypoint-bm-icon' }); + setIcon(iconEl, item.icon || 'file'); + + const labelEl = rowEl.createDiv({ cls: 'waypoint-bm-label', text: item.label }); + setTooltip(rowEl, item.filePath); + + rowEl.addEventListener('click', () => { + if (item.filePath) { + const tfile = this.app.vault.getFileByPath(item.filePath); + if (tfile) { + const leaf = this.app.workspace.getLeaf(false); + leaf.openFile(tfile); + } else { + new Notice('File not found'); + this.plugin.removeBookmark(item.id); + } + } + }); + } + + // ── Right-click context menu (both types) ── + rowEl.addEventListener('contextmenu', (event: MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + this.showBookmarkContextMenu(event, item); + }); + } + } + + private showBookmarkContextMenu(event: MouseEvent, item: BookmarkItem): void { + const menu = new Menu(); + + if (item.type === 'file') { + menu.addItem((i) => + i + .setTitle('Open in new tab') + .setIcon('file-plus') + .onClick(() => { + const tfile = this.app.vault.getFileByPath(item.filePath); + if (tfile) { + this.app.workspace.getLeaf('tab').openFile(tfile); + } + }), + ); + menu.addSeparator(); + } + + if (item.type !== 'separator' && item.type !== 'spacer') { + menu.addItem((i) => + i + .setTitle('Rename') + .setIcon('pencil') + .onClick(() => this.promptRename(item)), + ); + + menu.addItem((i) => + i + .setTitle('Change icon') + .setIcon('image') + .onClick(() => this.promptIcon(item)), + ); + } + + // ── Move to group options ── + const allGroups = this.collectGroups(this.plugin.waypointData.bookmarks); + const ownIds = this.collectDescendantIds(item); + const availableGroups = allGroups.filter(g => !ownIds.includes(g.id) && g.id !== item.id); + + if ((availableGroups.length > 0 || item.type === 'file' || item.type === 'group') && item.type !== 'separator' && item.type !== 'spacer') { + menu.addSeparator(); + if (availableGroups.length > 0) { + for (const group of availableGroups) { + menu.addItem((mi) => + mi + .setTitle(` ${group.label}`) + .setIcon(group.icon || 'folder') + .onClick(() => { + this.moveBookmarkToGroup(item.id, group.id); + }), + ); + } + } + // Add "Root" option for ungrouping + menu.addItem((mi) => + mi + .setTitle(' (Root)') + .setIcon('layout') + .onClick(() => { + this.moveBookmarkToGroup(item.id, null); + }), + ); + } + + if (item.type === 'group') { + menu.addItem((i) => + i + .setTitle(item.collapsed ? 'Expand' : 'Collapse') + .setIcon(item.collapsed ? 'chevron-down' : 'chevron-up') + .onClick(() => { + this.plugin.updateBookmark(item.id, { collapsed: !item.collapsed }); + }), + ); + menu.addSeparator(); + menu.addItem((i) => + i + .setTitle('Add bookmark here') + .setIcon('plus') + .onClick(() => { + const file = this.app.workspace.getActiveFile(); + if (!file) { + new Notice('No active file'); + return; + } + const child: BookmarkItem = { + id: `bm-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, + type: 'file', + label: file.basename, + filePath: file.path, + icon: 'file', + children: [], + collapsed: false, + indent: item.indent + 1, + }; + item.children.push(child); + this.plugin.saveWaypointData(); + this.redraw(); + }), + ); + menu.addItem((i) => + i + .setTitle('New sub-group') + .setIcon('folder-plus') + .onClick(() => { + const child: BookmarkItem = { + id: `bm-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, + type: 'group', + label: 'New Group', + filePath: '', + icon: 'folder', + children: [], + collapsed: false, + indent: item.indent + 1, + }; + item.children.push(child); + this.plugin.saveWaypointData(); + this.redraw(); + }), + ); + } + + menu.addSeparator(); + + // ── Insert separator / spacer ── + menu.addItem((i) => + i + .setTitle('Insert separator above') + .setIcon('separator-vertical') + .onClick(() => this.insertAdjacent(item.id, 'separator', 'above')), + ); + menu.addItem((i) => + i + .setTitle('Insert separator below') + .setIcon('separator-vertical') + .onClick(() => this.insertAdjacent(item.id, 'separator', 'below')), + ); + menu.addItem((i) => + i + .setTitle('Insert spacer above') + .setIcon('space') + .onClick(() => this.insertAdjacent(item.id, 'spacer', 'above')), + ); + menu.addItem((i) => + i + .setTitle('Insert spacer below') + .setIcon('space') + .onClick(() => this.insertAdjacent(item.id, 'spacer', 'below')), + ); + + menu.addSeparator(); + + menu.addItem((i) => + i + .setTitle('Remove') + .setIcon('trash') + .onClick(() => { + this.plugin.removeBookmark(item.id); + }), + ); + + menu.showAtPosition({ x: event.clientX, y: event.clientY }); + } + + private insertAdjacent(targetId: string, type: 'separator' | 'spacer', position: 'above' | 'below'): void { + const newItem: BookmarkItem = { + id: `bm-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, + type, + label: '', + filePath: '', + icon: '', + children: [], + collapsed: false, + indent: 0, + }; + + const insert = (items: BookmarkItem[]): boolean => { + const idx = items.findIndex(i => i.id === targetId); + if (idx >= 0) { + const insertIdx = position === 'above' ? idx : idx + 1; + items.splice(insertIdx, 0, newItem); + return true; + } + for (const item of items) { + if (item.children && insert(item.children)) return true; + } + return false; + }; + + insert(this.plugin.waypointData.bookmarks); + this.plugin.saveWaypointData(); + this.redraw(); + } + + private collectGroups(items: BookmarkItem[]): BookmarkItem[] { + const groups: BookmarkItem[] = []; + for (const item of items) { + if (item.type === 'group') { + groups.push(item); + groups.push(...this.collectGroups(item.children)); + } + } + return groups; + } + + private collectDescendantIds(item: BookmarkItem): string[] { + const ids: string[] = []; + for (const child of item.children) { + ids.push(child.id); + ids.push(...this.collectDescendantIds(child)); + } + return ids; + } + + private showDropIndicator(el: HTMLElement, e: MouseEvent): void { + // Clear indicator from all siblings first + const parent = el.parentElement; + if (parent) { + parent.querySelectorAll('.waypoint-bm-drop-line, .waypoint-bm-drop-below').forEach(el2 => { + el2.removeClass('waypoint-bm-drop-line'); + el2.removeClass('waypoint-bm-drop-below'); + }); + } + + const rect = el.getBoundingClientRect(); + const midY = rect.top + rect.height / 2; + const above = e.clientY < midY; + + el.addClass('waypoint-bm-drop-line'); + if (!above) { + el.addClass('waypoint-bm-drop-below'); + } + (el as any).__dropAbove = above; + } + + private moveBookmarkToGroup(itemId: string, targetGroupId: string | null): void { + const removeRecursive = (items: BookmarkItem[]): BookmarkItem | null => { + const idx = items.findIndex(i => i.id === itemId); + if (idx >= 0) { + const [removed] = items.splice(idx, 1); + return removed; + } + for (const item of items) { + const found = removeRecursive(item.children); + if (found) return found; + } + return null; + }; + + const item = removeRecursive(this.plugin.waypointData.bookmarks); + if (!item) return; + + if (targetGroupId) { + const findGroup = (items: BookmarkItem[]): BookmarkItem | null => { + for (const i of items) { + if (i.id === targetGroupId) return i; + const found = findGroup(i.children); + if (found) return found; + } + return null; + }; + const group = findGroup(this.plugin.waypointData.bookmarks); + if (group) { + item.indent = group.indent + 1; + group.children.push(item); + } + } else { + item.indent = 0; + this.plugin.waypointData.bookmarks.push(item); + } + + this.plugin.saveWaypointData(); + this.redraw(); + } + + private moveBookmarkToPosition(draggedId: string, targetId: string, before: boolean): void { + // Find and remove the dragged item from wherever it is + const removeById = (items: BookmarkItem[]): { item: BookmarkItem | null; parent: BookmarkItem[] } => { + const idx = items.findIndex(i => i.id === draggedId); + if (idx >= 0) { + const [removed] = items.splice(idx, 1); + return { item: removed, parent: items }; + } + for (const child of items) { + if (child.children) { + const result = removeById(child.children); + if (result.item) return result; + } + } + return { item: null, parent: [] }; + }; + + const { item: dragged } = removeById(this.plugin.waypointData.bookmarks); + if (!dragged) return; + + // Find the target's parent array and index + const findTargetParent = (items: BookmarkItem[]): { parent: BookmarkItem[]; idx: number } | null => { + const idx = items.findIndex(i => i.id === targetId); + if (idx >= 0) return { parent: items, idx }; + for (const child of items) { + if (child.children) { + const result = findTargetParent(child.children); + if (result) return result; + } + } + return null; + }; + + const targetInfo = findTargetParent(this.plugin.waypointData.bookmarks); + if (!targetInfo) { + // Fallback: push to root + dragged.indent = 0; + this.plugin.waypointData.bookmarks.push(dragged); + } else { + const insertIdx = before ? targetInfo.idx : targetInfo.idx + 1; + targetInfo.parent.splice(insertIdx, 0, dragged); + } + + this.plugin.saveWaypointData(); + this.redraw(); + } + + private promptRename(item: BookmarkItem): void { + new RenameModal(this.app, item.label, (newLabel) => { + if (newLabel && newLabel.trim()) { + this.plugin.updateBookmark(item.id, { label: newLabel.trim() }); + } + }).open(); + } + + private promptIcon(item: BookmarkItem): void { + new IconSuggestModal(this.app, item.icon, (iconName) => { + if (iconName) { + this.plugin.updateBookmark(item.id, { icon: iconName }); + } + }).open(); + } +} + +// ── Rename modal ── + +class RenameModal extends Modal { + private currentValue: string; + private onSubmit: (value: string) => void; + + constructor(app: App, currentValue: string, onSubmit: (value: string) => void) { + super(app); + this.currentValue = currentValue; + this.onSubmit = onSubmit; + } + + onOpen(): void { + this.titleEl.setText('Rename bookmark'); + + const input = this.contentEl.createEl('input', { + type: 'text', + value: this.currentValue, + }); + input.style.width = '100%'; + input.style.marginBottom = '12px'; + input.focus(); + input.select(); + + const btnContainer = this.contentEl.createDiv({ cls: 'modal-button-container' }); + + const cancelBtn = btnContainer.createEl('button', { text: 'Cancel', cls: 'mod-cta' }); + cancelBtn.style.marginRight = '8px'; + cancelBtn.addEventListener('click', () => this.close()); + + const saveBtn = btnContainer.createEl('button', { text: 'Save', cls: 'mod-cta' }); + saveBtn.addEventListener('click', () => { + this.onSubmit(input.value); + this.close(); + }); + + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + this.onSubmit(input.value); + this.close(); + } + }); + } + + onClose(): void { + this.contentEl.empty(); + } +} + +// ── Icon picker modal (full Lucide icon set, grid layout) ── + +class IconSuggestModal extends Modal { + private onSubmit: (icon: string) => void; + private selected: string; + private allIcons: string[] = []; + private loaded = false; + + constructor(app: App, currentIcon: string, onSubmit: (icon: string) => void) { + super(app); + this.selected = currentIcon; + this.onSubmit = onSubmit; + } + + async onOpen(): Promise { + const modal = this.contentEl; + modal.style.display = 'flex'; + modal.style.flexDirection = 'column'; + modal.style.gap = '10px'; + + this.titleEl.setText('Change icon'); + + // ── Preview ── + const preview = modal.createDiv({ cls: 'waypoint-icon-preview' }); + preview.style.display = 'flex'; + preview.style.alignItems = 'center'; + preview.style.gap = '10px'; + preview.style.padding = '12px 16px'; + preview.style.borderRadius = '8px'; + preview.style.background = 'var(--background-secondary)'; + preview.style.minHeight = '48px'; + + const previewIcon = preview.createSpan(); + previewIcon.style.display = 'flex'; + if (this.selected) setIcon(previewIcon, this.selected); + + const previewLabel = preview.createSpan(); + previewLabel.style.fontWeight = 'var(--font-medium)'; + previewLabel.style.fontSize = 'var(--font-ui-medium)'; + previewLabel.setText(this.selected || 'No icon'); + + // ── Search ── + const input = modal.createEl('input', { + type: 'text', + placeholder: 'Type to search (e.g. arrow, chart, home)...', + }); + Object.assign(input.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)', + }); + input.focus(); + + // ── Icon grid ── + const grid = modal.createDiv({ cls: 'waypoint-icon-grid' }); + grid.style.display = 'grid'; + grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(52px, 1fr))'; + grid.style.gap = '4px'; + grid.style.maxHeight = '320px'; + grid.style.overflowY = 'auto'; + grid.style.padding = '2px 0'; + + const statusRow = modal.createDiv(); + statusRow.style.display = 'flex'; + statusRow.style.justifyContent = 'space-between'; + statusRow.style.alignItems = 'center'; + statusRow.style.fontSize = 'var(--font-ui-smaller)'; + statusRow.style.color = 'var(--text-muted)'; + statusRow.style.padding = '0 4px'; + + const statusEl = statusRow.createSpan(); + statusEl.setText('Loading\u2026'); + + const clearEl = statusRow.createSpan(); + clearEl.style.cursor = 'var(--cursor)'; + clearEl.style.color = 'var(--text-accent)'; + clearEl.setText('No icon'); + clearEl.addEventListener('click', () => { + this.onSubmit(''); + this.close(); + }); + + // ── Load icons ── + this.loadIcons().then(() => { + this.loaded = true; + statusEl.setText(this.allIcons.length + ' icons'); + renderGrid(input.value); + }); + + // ── Render grid ── + let debounce: any = null; + + const renderGrid = (query: string) => { + grid.empty(); + if (!this.loaded) { grid.createDiv({ text: 'Loading\u2026' }); return; } + + const q = query.toLowerCase().trim(); + const matches = q + ? this.allIcons.filter(function(n) { return n.includes(q); }).slice(0, 80) + : this.allIcons.slice(0, 80); + + if (matches.length === 0) { + const empty = grid.createDiv(); + empty.style.gridColumn = '1 / -1'; + empty.style.textAlign = 'center'; + empty.style.color = 'var(--text-muted)'; + empty.style.padding = '20px'; + empty.setText('No icons match "' + query + '"'); + return; + } + + for (var i = 0; i < matches.length; i++) { + var name = matches[i]; + var tile = grid.createDiv(); + tile.setAttr('data-icon', name); + tile.style.display = 'flex'; + tile.style.alignItems = 'center'; + tile.style.justifyContent = 'center'; + tile.style.aspectRatio = '1'; + tile.style.borderRadius = '6px'; + tile.style.cursor = 'var(--cursor)'; + tile.style.transition = 'background 80ms'; + tile.setAttr('title', name); + + if (name === this.selected) { + tile.style.background = 'var(--interactive-accent)'; + tile.style.color = 'var(--text-on-accent)'; + } else { + tile.style.color = 'var(--text-muted)'; + } + + var svg = tile.createSpan(); + svg.style.display = 'flex'; + setIcon(svg, name); + + ;(function(_self, _tile, _name, _query, _grid, _previewIcon, _previewLabel) { + _tile.addEventListener('mouseenter', function() { + if (_name !== _self.selected) _tile.style.background = 'var(--background-modifier-hover)'; + }); + _tile.addEventListener('mouseleave', function() { + if (_name !== _self.selected) _tile.style.background = ''; + }); + + _tile.addEventListener('click', function() { + _self.selected = _name; + renderGrid(_query); + _previewIcon.empty(); + setIcon(_previewIcon, _name); + _previewLabel.setText(_name); + _grid.querySelectorAll('div[data-icon]').forEach(function(_el) { + var el = _el as HTMLElement; + if (el.getAttr('data-icon') === _name) { + el.style.background = 'var(--interactive-accent)'; + el.style.color = 'var(--text-on-accent)'; + } else { + el.style.background = ''; + el.style.color = 'var(--text-muted)'; + } + }); + }); + })(this, tile, name, query, grid, previewIcon, previewLabel); + } + + statusEl.setText(matches.length + ' of ' + this.allIcons.length + ' icons'); + }; + + input.addEventListener('input', function() { + if (debounce !== null) clearTimeout(debounce); + debounce = setTimeout(function() { renderGrid(input.value); }, 60); + }); + + input.addEventListener('keydown', function(e: KeyboardEvent) { + if (e.key === 'Escape') this.close(); + }.bind(this)); + + // ── Buttons ── + var btns = modal.createDiv({ cls: 'modal-button-container' }); + var cancel = btns.createEl('button', { text: 'Cancel' }); + cancel.addEventListener('click', function() { this.close(); }.bind(this)); + var saveBtn = btns.createEl('button', { text: 'Save', cls: 'mod-cta' }); + saveBtn.style.marginLeft = '8px'; + saveBtn.addEventListener('click', function() { + this.onSubmit(this.selected); + this.close(); + }.bind(this)); + } + + onClose(): void { + this.contentEl.empty(); + } + + private async loadIcons(): Promise { + try { + var r = await fetch('https://cdn.jsdelivr.net/npm/lucide-static@0.517.0/tags.json'); + var d = await r.json(); + this.allIcons = Object.keys(d).sort(); + } catch (_e) { + try { + var r2 = await fetch('https://lucide.dev/api/tags'); + var d2 = await r2.json(); + this.allIcons = Object.keys(d2).sort(); + } catch (_e2) { + this.allIcons = Object.keys(FALLBACK_ICONS).sort(); + } + } + } +} + +const FALLBACK_ICONS: Record = { + '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':[], + +}; diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..6b41f16 --- /dev/null +++ b/styles.css @@ -0,0 +1,294 @@ +/* ── Waypoint Base ── */ + +.waypoint-view { + padding: 8px; + display: flex; + flex-direction: column; + height: 100%; + overflow-y: auto; +} + +.waypoint-view .waypoint-section { + margin-bottom: 16px; + flex-shrink: 0; +} + +.waypoint-view .waypoint-section:last-child { + margin-top: auto; + margin-bottom: 0; +} + +.waypoint-view .waypoint-section-header { + font-size: var(--font-ui-small); + font-weight: var(--font-semibold); + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 4px; + padding-bottom: 4px; + border-bottom: 1px solid var(--background-modifier-border); +} + +/* ── Calendar ── */ + +.waypoint-calendar { + font-size: var(--font-ui-small); +} + +.waypoint-calendar-top { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; +} + +.waypoint-calendar-breadcrumb { + display: flex; + align-items: center; + gap: 6px; + font-size: var(--font-ui-medium); + font-weight: var(--font-semibold); + padding: 2px 0; +} + +.waypoint-calendar-breadcrumb .waypoint-clickable { + color: var(--text-muted); + cursor: var(--cursor); + padding: 1px 4px; + border-radius: 4px; +} + +.waypoint-calendar-breadcrumb .waypoint-clickable:hover { + color: var(--text-accent); + background-color: var(--background-modifier-active-hover); +} + +.waypoint-calendar .waypoint-separator { + color: var(--text-faint); + margin: 0 2px; +} + +.waypoint-calendar-today-group { + display: flex; + align-items: center; + gap: 2px; +} + +.waypoint-calendar-today-group button { + background: none; + border: none; + cursor: var(--cursor); + padding: 2px 6px; + border-radius: 4px; + color: var(--text-muted); + font-size: var(--font-ui-small); +} + +.waypoint-calendar-today-group button:hover { + color: var(--text-accent); + background-color: var(--background-modifier-active-hover); +} + +.waypoint-calendar-today-group .waypoint-calendar-today-btn { + font-weight: var(--font-medium); +} + +.waypoint-calendar table { + table-layout: fixed; + width: 100%; + border-collapse: collapse; +} + +.waypoint-calendar th, +.waypoint-calendar td { + text-align: center; + padding: 2px 1px; + font-size: var(--font-ui-small); +} + +.waypoint-calendar th { + font-weight: var(--font-medium); + color: var(--text-faint); + font-size: calc(var(--font-ui-small) * 0.85); +} + +.waypoint-calendar .waypoint-weeknum { + font-size: calc(var(--font-ui-small) * 0.75); + color: var(--text-faint); + font-weight: var(--font-light); + cursor: var(--cursor); + padding: 2px 0; + border-radius: 4px; +} + +.waypoint-calendar .waypoint-weeknum:hover { + color: var(--text-accent); + background-color: var(--background-modifier-active-hover); +} + +.waypoint-calendar .waypoint-day { + cursor: var(--cursor); + padding: 3px 0; + border-radius: 6px; + position: relative; + width: 100%; +} + +.waypoint-calendar .waypoint-day:hover { + background-color: var(--background-modifier-active-hover); +} + +.waypoint-calendar .waypoint-day.other-month { + opacity: 0.35; +} + +.waypoint-calendar .waypoint-day.today { + color: var(--text-accent); + border: 1px solid var(--text-accent); + font-weight: var(--font-semibold); +} + +.waypoint-calendar .waypoint-day.has-note::after { + content: ''; + display: block; + width: 4px; + height: 4px; + border-radius: 50%; + background-color: var(--text-faint); + margin: 0 auto; +} + +.waypoint-calendar .waypoint-day.today.has-note::after { + background-color: var(--text-accent); +} +/* ── Recent Files ── */ + +.waypoint-recent-files .nav-file { + padding: 2px 0; +} + +.waypoint-recent-files .nav-file-title { + padding: 2px 28px 2px 4px; + font-size: var(--font-ui-small); + position: relative; +} + +.waypoint-recent-files .waypoint-recent-remove { + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + opacity: 0; + cursor: var(--cursor); + color: var(--text-faint); + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + transition: opacity 100ms; +} + +.waypoint-recent-files .nav-file-title:hover .waypoint-recent-remove { + opacity: 1; + pointer-events: auto; +} + +.waypoint-recent-files .waypoint-recent-remove:hover { + color: var(--text-accent); +} + +/* ── Favorites ── */ + +.waypoint-favorites .waypoint-bookmark-item { + display: flex; + align-items: center; + gap: 4px; + padding: 2px 8px 2px 4px; + font-size: var(--font-ui-small); + cursor: var(--cursor); + border-radius: 4px; + user-select: none; + position: relative; +} + +.waypoint-favorites .waypoint-bookmark-item:hover { + background-color: var(--background-modifier-active-hover); +} + +.waypoint-favorites .waypoint-bookmark-item .waypoint-bm-icon { + display: flex; + align-items: center; + flex-shrink: 0; +} + +.waypoint-favorites .waypoint-bookmark-item .waypoint-bm-label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.waypoint-favorites .waypoint-bookmark-group { + font-weight: var(--font-medium); +} + +.waypoint-favorites .waypoint-bookmark-group .waypoint-bm-chevron { + display: flex; + align-items: center; + transition: transform 150ms; +} + +.waypoint-favorites .waypoint-bookmark-group.collapsed .waypoint-bm-chevron { + transform: rotate(-90deg); +} + +.waypoint-favorites .waypoint-bookmark-children { + padding-left: 16px; +} + +.waypoint-favorites .waypoint-bookmark-children.collapsed { + display: none; +} + +/* ── Drag-and-drop ── */ + +.waypoint-bm-dragging { + opacity: 0.3; +} + +.waypoint-favorites .waypoint-bm-drop-line { + border-top: 3px solid var(--text-accent) !important; +} + +.waypoint-favorites .waypoint-bm-drop-line.waypoint-bm-drop-below { + border-top: none !important; + border-bottom: 3px solid var(--text-accent) !important; +} + +/* ── Separator & Spacer ── */ + +.waypoint-favorites .waypoint-bookmark-item.waypoint-bookmark-separator { + padding: 0; + min-height: auto; + height: auto; + border: none; + cursor: grab; +} + +.waypoint-favorites .waypoint-bookmark-item.waypoint-bookmark-separator::after { + content: ''; + display: block; + height: 1px; + margin: 6px 8px; + background: var(--background-modifier-border); + pointer-events: none; +} + +.waypoint-favorites .waypoint-bookmark-item.waypoint-bookmark-spacer { + padding: 0; + min-height: auto; + height: 14px; + border: none; + cursor: grab; + background: transparent; +} \ No newline at end of file diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..413176d --- /dev/null +++ b/todo.md @@ -0,0 +1,5 @@ +- [ ] when user middle clicks on a bookmark, it should open in a new tab +- [ ] the sidebar should never scroll. the content of recent files should wrap after 10 files or so (no sidebar visible) +- [ ] it should be possible to add a separator line and/or an empty space +- [ ] by default, the bookmark should have no icon +- [ ] \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0692ec5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES6", + "allowJs": false, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": false, + "isolatedModules": true, + "strictNullChecks": true, + "lib": [ + "DOM", + "ES5", + "ES6", + "ES7" + ], + "paths": { + "src/*": ["./src/*"] + } + }, + "include": [ + "**/*.ts" + ] +} \ No newline at end of file