2026-07-28 12:11:09 -04:00

494 lines
40 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# GTD Calendar — Project Specification
*Status: living document, current through v1.0.0. Sections 15 describe the shipped design; §9 tracks what is still ahead.*
A fork of [WeMakeMachines/joplin-plugin-event-calendar](https://github.com/WeMakeMachines/joplin-plugin-event-calendar) (MIT) that inverts the plugin's data model: instead of events living as YAML inside the calendar note, the plugin's views are **populated by real Joplin notes and todos** in the surrounding folder tree, with clickable drilldown to the source note. What started as a single calendar view has grown into four composable views—calendar, kanban, priorities matrix, and Gantt—sharing one data layer.
**Package name:** `joplin-plugin-gtd-calendar` (npm)
**Manifest id:** `com.victorwiebe.joplin.plugin.gtd-calendar`
---
## 1. Concept
A note anywhere in a folder contains a ` ```gtd-calendar ` fenced block. That block renders as a Day/Week/Month calendar populated by notes and todos in scope. Items opt in (by default) via a ` ```gtd ` fenced block in their body. Clicking a calendar event opens the underlying note.
Dateless items are not lost — they collect in an **Unscheduled** section below the calendar grid, functioning as a GTD next-actions / someday-maybe bucket.
---
## 2. Schema Reference
### 2.1 The `gtd-calendar` block (lives in the calendar note)
```
```gtd-calendar
view: month # day | week | month | month-grid (default: day)
title: Editorial Schedule
scope: this-folder # this-folder | children | <integer depth> | all (default: this-folder)
notebook: Work/Ideas # notebook name | Parent/Child path | folder id (default: this note's folder)
notes: gtd-only # gtd-only | all | none (default: gtd-only)
todos: gtd-only # gtd-only | all | none (default: gtd-only)
sort: asc # asc | desc (default: asc)
sort-type: title # title | modified_date (default: title)
```
| Key | Values | Default | Notes |
|---|---|---|---|
| `view` | `day`, `week`, `month` (also `d`/`w`/`m`), `month-grid` (also `grid`) | `day` | Grouping granularity. `day`/`week`/`month` render the compact tile strip spanning all events; `month-grid` renders a classic 7-column grid of the entire *current* calendar month with weekday headers, dimmed adjacent-month days, today highlighted, and clickable event chips in each day cell. Events outside the displayed month are counted in a footnote. |
| `title` | string | none | Rendered as a heading above the calendar. |
| `scope` | `this-folder`, `children`, an integer, or `all` | `this-folder` | `children` = unlimited recursion into subfolders. An integer *n* = this folder plus *n* levels of descendants (`0``this-folder`). `all` = **every notebook in the profile**, ignoring the root folder and tree position entirely — an explicit, deliberately expensive opt-in for a global dashboard. Guardrails: the stats footnote reports scanned notebooks/notes and is marked `scope: all`; a soft ⚠ warning appears when more than ~2000 notes are scanned (advisory, never blocks). `notebook:` is ignored under `scope: all` (with a warning). Documentation cautions that broad scopes scan every note body in the tree. |
| `notebook` | notebook title, `Parent/Child` title path, or 32-char folder id | this note's folder | Roots the folder scan at a chosen notebook, possibly outside the host note's tree; `scope` then applies relative to it. Resolution order: (1) a raw folder id; (2) a `Parent/Child/…` case-insensitive title path; (3) a bare title, used only if unique. Any miss — unknown id, unresolvable path, ambiguous or absent title — appends a ⚠ warning and falls back to the host note's own folder (warn-don't-fail). |
| `notes` | `gtd-only`, `all`, `none` | `gtd-only` | Which plain notes participate. `all` includes notes without a `gtd` block (they receive default styling and are Unscheduled unless... see §3.2). |
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Same semantics for todos. |
| `sort` | `asc`, `desc` | `asc` | Order of events **within** each day/week/month grouping and within Unscheduled. Groupings themselves always run chronologically. |
| `sort-type` | `title`, `modified_date` | `title` | Field used by `sort`. `modified_date` maps to Joplin's `updated_time`. |
| `unscheduled-todos` | `yes`/`no` (also `true`/`false`, `show`/`hide`) | `yes` | Whether the Unscheduled **to-dos** sub-section is shown. |
| `unscheduled-notes` | `yes`/`no` (also `true`/`false`, `show`/`hide`) | `yes` | Whether the Unscheduled **notes** sub-section is shown. Independent of `unscheduled-todos` — lets a dashboard split the two lists across stacked calendar blocks instead of duplicating both. |
Unscheduled items (no resolvable date) are split into two labelled sub-sections — to-dos first, then notes — each hidden when empty or toggled off.
Unknown keys are ignored with a warning. A malformed block renders an inline warning above the calendar rather than failing silently.
### 2.2 The `gtd` block (lives in participating notes/todos)
```
```gtd
date: 2026-07-04 # required for notes to be scheduled; optional override for todos
bg-colour: teal # any CSS color value
fg-colour: white
icon: 🦴
title: Override title # optional; defaults to the note's own title
text: Hover text # optional; tooltip detail, as upstream
```
| Key | Applies to | Behavior |
|---|---|---|
| `date` | notes | Required for the item to appear **in the grid**; without it the note goes to Unscheduled. Accepts `yyyy-mm-dd` (and `mm-dd-yyyy` for upstream parity). |
| `date` | todos | Optional. If present, **overrides** `todo_due` (explicit beats implicit). |
| `bg-colour` / `bgColor` | both | Event tile background. Both spellings accepted; `bg-colour` is canonical for this fork. |
| `fg-colour` | both | Event tile text color (new vs. upstream). |
| `icon` | both | Emoji/short string, as upstream. Defaults: type indicator (see §4). |
| `title` | both | Overrides the note title on the tile. |
| `text` | both | Tooltip body. |
An **empty** ` ```gtd ` block is valid and meaningful: it is the opt-in signal with all defaults.
### 2.3 The `gtd-kanban` block (lives in the board note)
```
```gtd-kanban
title: Editorial Board
scope: this-folder # this-folder | children | N | all — same semantics as gtd-calendar
notebook: Work/Ideas # same semantics as gtd-calendar (default: this note's folder)
notes: all # all | none (default: all; gtd notes only)
todos: gtd-only # gtd-only | all | none (default: gtd-only)
sort-type: due-date # due-date | title | modified-date (default: due-date)
sort: asc # asc | desc (default: asc)
in-progress-tag: in-progress # default: "in-progress"
done-tag: done # ordinary-note completion; default: "done"
card-detail: hover # hover | always | none (default: hover)
page-size: 10 # positive integer (default: 10)
done-window: 7 # days, or "all" (default: 7)
```
Three columns are always shown: **Backlog**, **In Progress**, and **Done**.
Ordinary notes participate only when their body contains a `gtd` block; empty
and malformed blocks both opt in, with malformed YAML producing a warning and
fallback card values. `notes: all` admits those opted-in ordinary notes while
`notes: none` excludes them; it never admits a note without a `gtd` block. The
`notes` and `todos` options apply independently.
Bucketing: completion wins regardless of progress tags. Native to-dos use
`todo_completed`; opted-in ordinary notes use `done-tag`. Incomplete items
carrying `in-progress-tag` enter In Progress; everything else enters Backlog.
The done tag neither completes a native to-do nor opts in a plain note.
`done-window` filters native completed to-dos only; completed notes remain in
Done because no completion timestamp is inferred from `updated_time`. Sorting
is per-column across the combined note/to-do array.
Under `due-date`, dateless cards sort after dated cards regardless of direction.
`page-size` must be a positive integer; invalid values warn and fall back to
10. Each column initially renders at most that many cards and reveals another
batch per **List more** click. Expansion is independent per column and resets
when the rendered note reloads; headings and statistics retain complete totals.
Read-only in the current version.
### 2.4 The `gtd-matrix` block (lives in the matrix note)
```
```gtd-matrix
mode: skeleton # skeleton | eisenhower (default: skeleton)
title: Priorities
scope: this-folder # this-folder | children | N | all — same semantics as gtd-calendar
notebook: Work/Ideas # same semantics as gtd-calendar (default: this note's folder)
notes: all # all | none; gtd notes only (default: all)
todos: gtd-only
sort-type: due-date
sort: asc
urgent-tag: urgent # default: "urgent"
important-tag: important # Eisenhower mode only; default: "important"
in-progress-tag: in-progress # Skeleton mode only; default: "in-progress"
done-tag: done # note completion in both modes; default: "done"
urgent-window: 3 # Skeleton mode only, days; default: 3
card-detail: hover
page-size: 10 # positive integer; per quadrant (default: 10)
```
A 2×2 grid with labelled axes. It includes incomplete to-dos admitted by
`todos:` plus unfinished ordinary notes admitted by `notes:`. `notes: all`
still requires an explicit `gtd` block; `notes: none` excludes all notes. Completed
to-dos and notes carrying `done-tag` are excluded before either mode buckets
them, so done wins over active, urgent, and important tags. Native to-dos ignore
`done-tag` and remain governed by `todo_completed`. The board is internally
positional; each mode supplies its own axis and quadrant labels.
**`mode: skeleton`** (default) — rows = active (`in-progress-tag`, the same tag the kanban uses); columns = due soon (resolved card date within `urgent-window` days, overdue included, or the `urgent-tag` as a manual override for dateless items). Quadrants: **Do Next** (active + due soon), **Scheduled** (active, not due soon — includes dateless in-progress work), **On Deck** (not active + due soon — the early-warning quadrant), **Backlog** (neither). Designed to complement the calendar and kanban directly: same in-progress tag, dates drive urgency instead of a second opinion-tag, and nothing is ever condemned as "Eliminate" — a Backlog item is simply low priority. In-progress + `urgent-tag` + no date resolves to Do Next (the urgent tag is an explicit override, so it wins over the absence of a date).
**`mode: eisenhower`** — the classic: rows = `important-tag`; columns = `urgent-tag`. Quadrants: Do First (both), Schedule (important only), Delegate (urgent only), Eliminate (neither).
A warning is raised if `urgent-tag` and `important-tag` resolve to the same tag (Eisenhower mode only — the axes would collapse). `page-size` has the same validation and transient behavior as the kanban, applied independently to each quadrant after sorting. Read-only.
### 2.5 The `gtd-gantt` block (dual-role: chart **and** item declaration)
Unlike the other views, gantt items opt in through their **own** `gtd-gantt` block rather than the shared `gtd` block, and every item carries a `project:` that groups items into swimlanes. The same fence name therefore serves two roles, disambiguated by a single rule:
> **A `gtd-gantt` block containing a non-empty `project:` key is an ITEM declaration; one without is the CHART config block.**
The markdown-it content script applies this rule with a cheap line scan (no YAML lib): an item block renders in place as a small inline badge (`gantt: <project>`) and is never drawn as a chart; a chart block emits the usual webview placeholder. `collectGantt` applies the identical rule when scanning (empty/whitespace project ⇒ treated as a chart block, i.e. not an item).
**Item block** (lives in any participating note/todo):
```
```gtd-gantt
project: bluesky # REQUIRED — the swimlane; its presence marks this an item
begin-date: 2026-07-01 # notes only — bar span start (yyyy-mm-dd or mm-dd-yyyy)
end-date: 2026-07-14 # notes only — bar span end
bg-colour: teal # optional styling, mirroring the gtd block
fg-colour: white
title: Design phase # optional; defaults to the note title
text: Hover detail # optional; tooltip
```
```
| Key | Applies to | Behavior |
|---|---|---|
| `project` | both | Required. Groups the item into a swimlane (case-insensitive; first-seen casing is the display name). Empty/absent ⇒ this is a chart block, not an item. |
| `begin-date` / `end-date` | notes → bars | Both required and valid, `end-date` ≥ `begin-date`, else the item is **excluded with a warning**. |
| (native due date) | todos → milestones | A to-do becomes a ◆ **milestone** on its own `todo_due`. `begin-date`/`end-date` on a to-do are ignored (warning). No due date ⇒ excluded with a warning. |
| `bg-colour`/`fg-colour`/`title`/`text` | both | As the gtd block. Bars default to the stable pastel hash when no colour is set. |
Completed to-do milestones render dimmed and struck through; recurring to-dos get the ↻ mark. There is **no Unscheduled section** — an item with no usable date is simply excluded (the warning explains why).
**Chart block** (lives where the timeline is drawn):
```
```gtd-gantt
title: Roadmap
scope: children # this-folder | children | N | all — same semantics as gtd-calendar
notebook: Work/Ideas # same semantics as gtd-calendar; ignored under scope: all
filter-project: bluesky # optional — limit to a single project (case-insensitive)
sort-type: begin-date # begin-date | title | modified-date (default: begin-date)
sort: asc # asc | desc (default: asc)
card-detail: hover # hover | always | none (default: hover)
```
```
There is deliberately **no `notes`/`todos` inclusion option**: items opt in solely via their `project:` block, so those keys are unknown here (and warn as such). Projects render alphabetically; within each project, rows sort by `sort-type`/`sort`. The renderer draws a month tick header, a "today" line when today falls within the chart's date envelope, and scrolls horizontally for wide ranges. The host note is always excluded from its own scan (as with every view). Bars and milestones are clickable (open the source note); hover detail uses a native tooltip, suppressed by `card-detail: none`. Read-only.
---
## 3. Data Model Rules
### 3.1 Date resolution
1. **Todos:** `todo_due` from the Joplin API. A `date:` in the `gtd` block overrides it.
2. **Notes:** `date:` from the `gtd` block only. No fallback to created/updated time — those are not event dates and would produce misleading calendars.
3. **No resolvable date** → the calendar renders the item in **Unscheduled**; kanban and matrix retain it in the appropriate dateless bucket.
### 3.2 Inclusion matrix
An item appears on the calendar iff its type's mode admits it:
| Mode | Has `gtd` block | No `gtd` block |
|---|---|---|
| `gtd-only` (default) | ✅ included | ❌ excluded |
| `all` | ✅ included | ✅ included (default styling) |
| `none` | ❌ excluded | ❌ excluded |
The calendar note itself is always excluded from results, as is any note whose only `gtd-calendar` block makes it a calendar (a note may, however, be both an event and contain a calendar if it has both block types — edge case, supported, documented).
Kanban and matrix use a deliberately asymmetric inclusion contract:
| Item | Has `gtd` block | No `gtd` block |
|---|---|---|
| Ordinary note with `notes: all` (default) | Included | Excluded |
| Ordinary note with `notes: none` | Excluded | Excluded |
| To-do with `todos: gtd-only` | Included | Excluded |
| To-do with `todos: all` | Included | Included |
| To-do with `todos: none` | Excluded | Excluded |
An empty or malformed `gtd` block counts as found. Malformed admitted blocks
warn but do not disappear. The independent `notes` and `todos` filters run
before warnings and tag lookup. Each kanban/matrix host note is excluded before
eligibility and tag lookup.
### 3.3 Completed todos
**Calendar:** completed to-dos render with strikethrough and a checked indicator.
**Kanban:** completed to-dos enter Done when inside the configured done window;
opted-in ordinary notes carrying `done-tag` also enter Done without timestamp
filtering. **Matrix:** completed to-dos and done-tag notes are excluded. Ordinary
notes never become recurring.
---
## 4. Rendering
- **Calendar** reuses upstream's grouping/renderer architecture (`DayGrouping`/`WeekGrouping`/`MonthGrouping` + DOM renderers + CSS asset), including the empty-grouping placeholders and current-date highlight, plus the fork's own `month-grid` (a 7-column grid of the entire current month).
- **Fixed upstream bug:** `Calendar/index.ts` imported `WeekGrouping` from `Month/MonthGrouping`, so the week view silently grouped by month. Corrected in the fork.
- **Type distinction:** to-dos render with a checkbox glyph (☐ / ☑ when completed); ordinary notes render with 📄. A custom `icon` takes precedence. Recurring to-dos (per the `recurring` tag) get a ↻ suffix; ordinary notes never do, even if tagged `recurring`.
- **Unscheduled section:** below the calendar grid, split into two labelled sub-sections — to-dos first, then notes — each hidden when empty or switched off (`unscheduled-todos` / `unscheduled-notes`).
- **Kanban** renders eligible notes and to-dos in three fixed columns (Backlog / In Progress / Done) of compact cards; a "hover" mode shows date and hover text only on mouseover (`card-detail`). Each column independently renders cards in `page-size` batches.
- **Matrix** renders eligible notes and incomplete to-dos in a 2×2 grid with axis and quadrant labels supplied per mode; cards are visually and behaviourally identical to kanban cards. Each quadrant owns an independent batch counter.
- **Incremental card lists:** collectors always return complete sorted arrays and full statistics. The main process sends normalized `pageSize`; the webview owns transient `visibleCount` state per column/quadrant and appends one batch per **List more** click. A linked tooltip (`aria-describedby`) reports the exact remaining count on pointer hover and keyboard focus. No expansion state is persisted; rerender/reload resets it.
- **Drilldown:** every tile, card, hover-card row, and chip across all four view types is clickable and opens the source note.
---
## 5. Architecture
Upstream is a single synchronous markdown-it content script with no data API access. The fork is a three-part plugin serving four view fence types (`gtd-calendar`, `gtd-kanban`, `gtd-matrix`, and `gtd-gantt`) through the same pipeline:
```
┌─────────────────────────────┐
│ 1. Markdown-it content │ Intercepts the four view fence types
│ script (renderer) │ and parses their YAML configuration,
│ │ emits a placeholder <div> tagged with
│ │ data-block-type and the config as a
│ │ data attribute.
└──────────────┬──────────────┘
│ (rendered HTML)
┌──────────────▼──────────────┐
│ 2. Webview asset script │ Finds placeholders, calls
│ (runs in rendered note) │ webviewApi.postMessage(config) with
│ │ getEvents / getKanban / getMatrix / getGantt
│ │ depending on block type. Builds the
│ │ calendar / board / matrix / Gantt DOM from
│ │ returned payload. Click → postMessage
│ │ ({type: "openNote", noteId}).
└──────────────┬──────────────┘
│ postMessage / onMessage
┌──────────────▼──────────────┐
│ 3. Main plugin process │ joplin.contentScripts.onMessage routes
│ (index.ts) │ by message type to a handler per block
│ │ type, each of which:
│ │ • resolves current note's folder
│ │ (joplin.workspace.selectedNote)
│ │ • builds folder tree, applies scope
│ │ (src/Gtd/folderScope.ts)
│ │ • fetches notes/todos, paginated
│ │ • parses ```gtd blocks from bodies
│ │ (src/Gtd/gtdBlock.ts)
│ │ • fetches tag titles per eligible item
│ │ (getNoteTagTitles) for recurrence /
│ │ in-progress / urgent / important
│ │ detection
│ │ • applies the inclusion matrix, date
│ │ resolution, and view-specific
│ │ bucketing (collectEvents.ts /
│ │ collectKanban.ts / collectMatrix.ts /
│ │ collectGantt.ts)
│ │ • handles openNote via
│ │ joplin.commands.execute
└─────────────────────────────┘
```
The calendar, kanban, and matrix collectors share `folderScope.ts`,
`gtdBlock.ts`, and `resolveDate.ts`. Kanban and matrix also share
`buildKanbanCard.ts`, which normalizes the explicit `isTodo` discriminator,
source ID, title/style overrides, resolved date, completion state, recurrence
state, and modification time.
Their collection flow is: resolve scoped folders → exclude the host note →
evaluate item-kind eligibility → parse and warn on the `gtd` block → fetch
tags only for eligible items → build a normalized card → bucket → sort the
combined note/to-do arrays. Complete arrays and totals cross the plugin boundary;
SLICE8 batching remains transient webview state.
### Performance notes
- Folder tree and note list fetched per render request; paginated API calls (`page`/`has_more`).
- Body scanning is limited to items in scope. Calendar may omit bodies when both
item inclusion modes are `none`. Kanban and matrix always request bodies,
including when `todos: none`, because ordinary notes opt in through a `gtd`
block and `todos:` governs only to-dos. This is an intentional performance
tradeoff: block discovery requires the body, while tag-title requests remain
limited to items that pass host, type, completion, and shortcode eligibility.
For large scopes, pre-filter with Joplin's search API before fetching full
bodies; cache by `updated_time` if needed — not yet required at observed
real-world scales (largest tested: ~285 notes, single folder scope), but worth
revisiting if a `children`-scope board on a large tree feels slow, especially
with Persistent Layout keeping several boards rendered at once.
- Refresh model: re-render occurs on note switch (Joplin behavior); no live refresh on background data changes. Fine for a read-only plugin; becomes a sharper question once the authoring axis (§9) lands, where a write on one rendered board should ideally refresh others.
### Platform caveats
- Desktop is the target. Mobile plugin support is partial and this plugin's core mechanisms (custom fence rendering, webview↔plugin messaging) are not expected to work there; documented explicitly in the README. Underlying to-dos still sync and appear in Joplin's native mobile views. The "Embed Search" plugin pairing remains a documented pattern for cross-scope aggregation generally, not specifically a mobile workaround.
---
## 6. Implementation Phases
1. **Phase 0 — Scaffolding.** Fork, rename (manifest id, plugin name, file names), build pipeline verified (`npm install && npm run dist`), upstream week-view bug fixed, upstream tests green.
2. **Phase 1 — Plumbing.** Placeholder rendering for `gtd-calendar` fences; webview ⇄ plugin message round-trip proven with a hardcoded payload.
3. **Phase 2 — Data layer.** Folder tree + scope resolution; note/todo fetching; `gtd` block parsing; inclusion matrix; date resolution; unit tests for all of it (the upstream Jest setup carries over).
4. **Phase 3 — Rendering.** Wire real data into the reused renderers; Unscheduled section; type glyphs; fg-colour support; sort options.
5. **Phase 4 — Drilldown & polish.** Click-to-open; error boxes for malformed YAML; docs/README; scope-performance warnings.
---
## 7. Decision Log / Open Items
| # | Decision | Status |
|---|---|---|
| D1 | Defaults: `notes: gtd-only`, `todos: gtd-only` | ⚠️ **Confirm** — earlier discussion said todos are *always* included (i.e. `todos: all` default); the final message specified `gtd-only` as default for both. Spec follows the final message. |
| D2 | `gtd` `date:` overrides `todo_due` (vs. emitting a warning) | Adopted; revisit if confusing in practice. |
| D3 | Completed todos: shown struck-through in v1; `completed:` option reserved | Proposed, unconfirmed. |
| D4 | `sort`/`sort-type` act within groupings only; grid is always chronological | Confirmed. |
| D5 | Colour key spelling: `bg-colour`/`fg-colour` canonical, `bgColor` accepted for upstream parity | Proposed. |
| D6 | Future options parked: `completed:`, tag-based filtering, week-start day, live refresh | Backlog. |
---
## 8. Licensing & Attribution
Upstream is MIT (© Franco Speziali / WeMakeMachines). The fork retains the MIT license, preserves the original copyright notice, and credits upstream in the README per standard practice.
---
## 9. Roadmap (post-1.0 / post-launch)
Recorded direction, not commitments. Each gets a focused design pass before work begins; real-world use of the shipped plugin may reorder this.
### v0.2.0 — Repeating to-dos (visualisation) — SHIPPED · strip horizon — NOT YET BUILT
Recurrence itself is delegated to an existing community plugin (open-environment approach; we don't reimplement it). Candidates evaluated: BeatLink's original *Repeating To-Dos* (unmaintained), the *[Revived]* fork (maintained, GitHub issues only), and *Repeating To-dos v2* by TheScriptingGuy (`com.github.TheScriptingGuy.joplin-plugin-repeating-todos`, best-documented). All share the same mechanic: on completion the to-do's alarm date is reset to the next occurrence and the to-do is unmarked — so at any moment a recurring to-do is a **single** to-do at its next due date, with the recurrence rule stored in the note body.
- **Option A (shipped in 0.2.0):** detect recurrence and mark those events with a ↻ glyph. *Implementation note:* v2 of the plugin stores recurrence in Joplin's `userData` (not the note body) and maintains a `recurring` **tag** as its query index — so detection is by tag presence on to-dos (constant `RECURRING_TAG`), not body-scanning. Cleaner than the originally-planned body parse, and reuses the tag-fetching the kanban work will need. No series projection — the to-do shows at its next due date.
- **Option B (in pocket):** parse the recurrence rule and project all future occurrences across the visible window. Deferred because the body format has proven to be a moving target across forks; promote to a 0.2.x only if Option A's single-occurrence view proves insufficient, and target a specific fork's format at that time.
- **README:** recommend the maintained fork (v2 first, Revived as fallback); warn about the known "recurrence block injected into all notes" failure mode; note coexistence (their plugin moves the anchor, ours draws the marker).
**Configurable strip horizon.** `day`/`week`/`month` strip views currently span first-event to last-event (plus today). Add a horizon option to `gtd-calendar` (working name `weeks:` or `horizon:`, default ~8 weeks forward) controlling how far ahead the strip extends — relevant both generally and for how many future occurrences Option B would draw. **`month-grid` is explicitly excluded**: it always shows the entire current calendar month, by design, and that is not configurable.
*Note: multiple `gtd-calendar` blocks in one note already work as of 0.1.0 (each fence renders independently) — e.g. a "week ahead" strip plus a "month grid" in a single dashboard note. No work required; document as a recommended pattern.*
### v0.3.0 — Kanban board (read-only) — SHIPPED
Decision: a **second block type within this plugin**, not a separate plugin. Rationale: a kanban needs the entire existing data layer (`folderScope`, `collectEvents` inclusion matrix, `getNoteTagTitles`, config parsing, recurrence detection, webview messaging); a separate plugin would duplicate all of it or require a shared-library split whose overhead isn't justified for "three tag-driven columns." One plugin also serves the intended use — a calendar and a kanban in one dashboard note — with a single install and a unified config/tag vocabulary.
**The `gtd-kanban` block.** Parallel to `gtd-calendar`; lives in its own note or stacked beneath a calendar (multiple blocks already render independently).
```
```gtd-kanban
title: Editorial Board
scope: this-folder # this-folder | children | N — same semantics as gtd-calendar
todos: gtd-only # gtd-only (default) | all | none
sort-type: due-date # due-date (default) | title | modified-date
sort: asc # asc | desc
in-progress-tag: in-progress # configurable; default "in-progress"
card-detail: hover # hover (default) | always | none
done-window: 7 # days; integer or "all"; default 7
```
**Columns (always three, always shown even when empty):** Backlog · In Progress · Done.
**Bucketing rules:**
- **Done wins.** Any to-do with `todo_completed > 0` → *Done*, regardless of tags.
- Else, carrying the in-progress tag → *In Progress*.
- Else → *Backlog*.
- **Backlog shows everything** uncompleted and untagged — dated or not, near or far. (Acknowledged: can grow long; compact cards mitigate.)
**Sorting (within each column):**
- `sort-type: due-date | title | modified-date`, with `sort: asc | desc`.
- Under `due-date`, to-dos **without** a due date sort *after* all dated ones.
**Done column window:** shows only to-dos completed within `done-window` days (default 7); `all` shows the full history. Prevents unbounded growth.
**Cards:** compact by default—title, resolved date, optional recurrence mark for to-dos, and click-to-open. `card-detail` controls the hover panel: `hover` (default), `always` (details inline), `none` (no panel). Ordinary notes containing a `gtd` block now participate under the same placement and styling rules.
**Read-only in 0.3.0:** reflects state, never mutates it. Proves columns/bucketing cheaply before write-back.
**Architecture:** reuses the calendar's pipeline. New `gtd-kanban` markdown-it fence → placeholder → webview requests data → plugin runs scope + inclusion + tag fetch (already built) + bucketing → returns three columns → webview renders. `IN_PROGRESS_TAG` constant mirrors `RECURRING_TAG`, exposed via `in-progress-tag:` config from the start.
**`sort-type` vocabulary reconciliation:** the calendar block's `sort-type` (currently `title | modified_date`) is unified toward `due-date | title | modified-date` across both blocks so they read consistently. For the calendar, `due-date` is accepted but falls back to chronological where less meaningful. (Note the spelling shift `modified_date` → `modified-date` for cross-block consistency; the old spelling stays accepted as an alias to avoid breaking existing calendars.)
### v0.4.0 — Eisenhower Matrix (`gtd-matrix` block) — SHIPPED
### v0.5.0 — Skeleton Matrix (`mode:` on gtd-matrix) — SHIPPED
Real-world feedback: Eisenhower's opinion axes (and "Eliminate" in particular) fit triage, not a vault of already-committed work. The **Skeleton Matrix** replaces them with two derived axes the system already maintains: rows = active (`in-progress-tag`, shared with the kanban); columns = due soon (`urgent-window` days, default 3, overdue included; `urgent` tag as manual override — in-progress + urgent + dateless is Do Next by design). Quadrants: Do Next / Scheduled / On Deck / Backlog; dateless active work → Scheduled; nothing is ever "Eliminate". Implemented as `mode: skeleton | eisenhower` on the same block — positional MatrixBoard (topLeft…bottomRight), labels supplied per mode via payload, shared rendering. Skeleton is the default (it matches the plugin's own tag/date vocabulary); classic Eisenhower preserved via `mode: eisenhower`.
A fourth display block: a 2×2 prioritisation matrix, tag-driven like the kanban. Chosen ahead of drag-and-drop deliberately: it is on the **views axis** (another rendering of tagged to-dos, reusing the whole data layer, read-only) rather than the **authoring axis** (vault mutation), so it deepens what the plugin already is without committing to write-back.
- **Quadrants** from two configurable tags (`urgent-tag:` default `urgent`; `important-tag:` default `important`):
- urgent + important → **Do First**
- important only → **Schedule**
- urgent only → **Delegate**
- neither → **Eliminate** (the default bucket for untagged to-dos — a deliberately pointed default)
- **Completed to-dos are excluded entirely** — the matrix is a prioritisation view, not a tracking view; kanban's Done column is where completions live.
- Incomplete to-dos plus ordinary notes containing a `gtd` block; same `scope` / `todos` / `sort-type` / `sort` / `card-detail` options as kanban and the same compact clickable cards.
- Own block (`gtd-matrix`), not a kanban mode — different layout, different tag semantics.
### v0.6.0 — Notebook targeting & profile-wide scope — SHIPPED
Two scope-axis additions shared across all four view blocks (see §2.1 / §2.5 for the normative schema):
- **`notebook:`** roots the folder scan at a chosen notebook rather than the host note's own folder, so a dashboard note can live anywhere and aggregate a different tree. Resolution order: raw 32-char folder id → `Parent/Child` case-insensitive title path → unique bare title. Any miss (unknown id, unresolvable path, ambiguous or absent title) warns and falls back to the host folder (warn-don't-fail). `scope` applies relative to the resolved notebook. Implemented as a pure `resolveNotebook.ts` (folders + spec → id + warning) plus a shared `parseNotebookOption`; resolution lives in `index.ts` (needs the folder tree), collectors stay unchanged.
- **`scope: all`** scans every notebook in the profile, ignoring root and depth — an explicit, deliberately expensive opt-in ("very dangerous, yes"). Represented as a `scopeAll` flag (not a `scopeDepth` sentinel); `folderScope` returns all folder ids when set. Guardrails: the stats footnote reports scan size and is marked `scope: all`; a soft ⚠ appears past ~2000 scanned notes (advisory, never blocks). `notebook:` + `scope: all` → `all` wins, with a warning.
- **Body-fetch contract:** calendar retains its safe inclusion-mode optimization and Gantt retains its item-block behavior. Kanban and matrix now always request bodies because ordinary-note opt-in must be discovered even under `todos: none`; tag requests remain eligibility-gated.
### v0.7.0 — Gantt chart (`gtd-gantt` block) — SHIPPED
A fifth view: a project-grouped timeline, on the **views axis** like the matrix (another read-only rendering of existing notes/to-dos), not the authoring axis. Unlike the other blocks, gantt items opt in through their **own** `gtd-gantt` block carrying a `project:`, and the same fence name is disambiguated by that key (item vs chart — see §2.5). Notes become bars (`begin-date`→`end-date`), to-dos become ◆ milestones on their due date; grouped into per-project swimlanes, no Unscheduled section (dateless/invalid items are excluded with a warning). Data layer is pure and unit-tested (`ganttBlock.ts`, `parseGanttConfig.ts`, `collectGantt.ts`); rendering is a CSS-grid timeline (month ticks, today line, horizontal scroll, click-to-open). Item blocks render in place as a small `gantt: <project>` badge. Read-only. Open follow-ups (deferred): richer hover cards (currently native tooltips), `card-detail: always` inline text on bars, an optional Unscheduled strip.
### Unreleased — Incremental kanban/matrix card limits — COMPLETE
Kanban columns and matrix quadrants now render in configurable `page-size`
batches (default 10). Parsing is shared and rejects zero, negative, fractional,
non-numeric, or non-finite values with a warning and fallback to 10. Collectors
remain complete and unchanged; normalized `pageSize` crosses the plugin payload
boundary, while the webview owns independent transient expansion state for every
bucket. **List more** reveals one batch, reports the exact remaining count through
an accessible hover/focus tooltip, preserves focus between intermediate clicks,
and disappears at completion. Reload resets all expansion. Automated validation
passed at 164 tests and manual Joplin acceptance passed 2026-07-27.
### Unreleased — Opted-in note cards for kanban/matrix — COMPLETE
Ordinary notes containing a `gtd` block now participate in kanban and matrix
views. Empty and malformed blocks opt in; malformed YAML warns and falls back to
source-note values. Notes use `gtd` dates only, share title/colour/icon/text
overrides, render with 📄 when no icon is supplied, and never become completed or
recurring. `todos:` continues to govern only to-dos. Kanban places notes in
Backlog or In Progress; both matrix modes reuse their existing tag/date axes.
Body inspection is always enabled for these two collectors, while tag lookup
remains eligibility-gated. Automated validation passed at 185 tests, the
production JPL built successfully, and manual Joplin acceptance passed
2026-07-27.
### v0.5.0+ — The authoring axis (drag-and-drop + create-from-view) — one deliberate epic
Clustered because they share write-back machinery, and adopting any of them commits the plugin to mutating the vault:
**Drag-and-drop kanban (write-back).** The first feature making the plugin a **controller** of data rather than a viewer — warrants its own design pass.
- Dragging a card writes back to Joplin via the existing webview→plugin channel:
- → *In Progress*: add the `in-progress` tag (create the tag if absent; resolve its id; associate).
- → *Done*: set `todo_completed`.
- → *Backlog*: remove the tag / clear completion.
- **Open design questions for that pass:** optimistic UI vs. wait-for-confirmation; behaviour on write failure; whether drops need undo; re-query/refresh of kanban and any affected calendars after a successful write; the trust implications of a rendered view mutating other notes.
- Feasibility: confirmed possible (we already execute `openNote` from the webview; data writes use the same channel with more responsibility). Complexity lives in tag management and the write/refresh cycle, not the dragging.
- **Multi-board refresh:** with Persistent Layout keeping several dashboards rendered, a write from one board leaves others stale until re-render — the refresh question is sharper in the fractal-dashboard architecture than for single-board use.
**Create-to-do from kanban.** A "+" affordance per column creates a to-do with that column's state (In Progress column → in-progress tag; Backlog → no tag). Easier than create-from-calendar: the column *is* the metadata, no date geometry. Open: target notebook (leaf dashboards obvious, cockpit murky), trigger reliability in the rendered viewer.
**Create-to-do from calendar.** Click/affordance on a day cell creates a to-do due that day. Open questions: trigger mechanism in the read-only viewer (double-click/right-click may be claimed by Joplin; per-cell "+" affordance likelier), target notebook rule, whether created to-dos get an empty gtd block so they appear under `todos: gtd-only`. Prototype the trigger before designing the rest.