Files
Marcos Paulo de3ec375ae
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
docs: specs and briefs for Tasks, Templater, Kanban and icons/banners
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 06:06:54 -03:00

12 KiB

Kanban board note (kanban-plugin: board) (roadmap item 13)

Part of specs/plugins-parity/. Replaces the community plugin Kanban (2.77M downloads, by mgmeyers). Research (2026-10-21): the plugin's own parser source (src/parsers/formats/list.ts on GitHub: frontmatter, ## Lane headings, - [ ] cards, the Complete marker, the Archive section after a thematic break, the %% kanban:settings footer), the community plugin listings of compatible forks (Boardwalk, Kanban Pro Boards, Studio Kanban: all read and write the same kanban-plugin: board format), and the project README (markdown-backed boards). The Obsidian docs site for the plugin was not reachable; literal values of the marker constants (**Complete**, ***) are from memory of the plugin and MUST be checked against a board file written by the real plugin (Open question Q1) before task 1 is accepted.

Problem

Kanban boards are the second most used way to see task state in Obsidian. A user's existing boards are plain Markdown files that today open as a wall of headings. NotesCraft has no board view and Bases Kanban (Obsidian 1.14) is a non-goal of the Bases spec.

Goal

A note whose frontmatter has kanban-plugin: board opens as a board: lanes side by side, cards that can be dragged between lanes and reordered, added, edited, completed, archived. The file stays a normal Markdown file that Obsidian's Kanban plugin opens unchanged, and editing the file by hand (or sync merging it) never corrupts the board. Every action is an engine command (SPEC 10).

Non-Goals (v1)

  • Date/time pickers and calendar integration of cards (@{date} is parsed and shown, edited as text).
  • Card metadata pulled from linked notes (properties on cards), "new note from card".
  • Table and list layouts of the plugin, per-lane limits UI, tag colours, date colours. The settings JSON is kept byte-for-byte.
  • Bases Kanban view (type: kanban in a .base): follow-up, listed in Feature ideas.
  • Multi-user live editing of one board (the live co-editing layer treats the file as text anyway).

Design decisions

D1 - File format

---

kanban-plugin: board

---

## To do

- [ ] Write spec @{2026-10-21} #work
- [ ] Review [[Design notes]]
	continuation line of the card (tab- or space-indented)

## Doing

- [ ] Build parser

## Done

**Complete**
- [x] Research


***

## Archive

- [x] Old card

%% kanban:settings

{"kanban-plugin":"board","list-collapse":[false,false,false]}

%%
  • Frontmatter: YAML between ---, key kanban-plugin with value board (older files: basic, both accepted; written back as read). Any other frontmatter keys are kept.
  • Lane = a ## Title heading. Optional max-items suffix ## Title (5) is read as max_items = 5 and written back as read. The list items directly below are the cards.
  • **Complete** as the first paragraph under a lane marks it a "complete lane" (cards moved in become - [x]; moved out - [ ]).
  • Card = a list item - [ ] text / - [x] text. Continuation lines indented under it belong to the card (multi-line cards). Block ids (^abc123) at the end of the first line are kept verbatim.
  • Archive = after a thematic break ***, a ## Archive heading and the archived cards.
  • Settings = the final %% kanban:settings block; the JSON is kept as an opaque string except the keys in D4. Missing block means defaults.
  • Anything else (paragraphs between lanes, headings of other levels) is kept in Lane.extra (raw lines) and written back where it was. Unknown structure is NEVER dropped.

D2 - Crate and layering

New crate crates/kanban, package notescraft-kanban, layer L1 (depends only on notescraft-markdown (L0) and thiserror). Pure: parse(&str) -> Result<Board>, to_markdown(&Board) -> String, ops on a Board. No I/O, no egui. Registration in xtask/src/layers.rs and the root [workspace.dependencies] is lead work (step 0).

pub struct Board {
    pub front: String,                 // raw frontmatter block text including the '---' lines
    pub lanes: Vec<Lane>,
    pub archive: Vec<Card>,
    pub archive_extra: Vec<String>,    // raw lines of the archive section outside cards
    pub settings_json: Option<String>, // raw JSON inside the footer fence
    pub tail: Vec<String>,             // text after the footer, kept
}
pub struct Lane {
    pub title: String, pub max_items: Option<u32>, pub complete: bool,
    pub cards: Vec<Card>, pub extra: Vec<String>,
}
pub struct Card { pub checked: bool, pub text: String /* may contain '\n' continuation, no indent */, pub block_id: Option<String> }

Limits (hostile input): file 4 MiB, 200 lanes, 5 000 cards per lane, 50 000 cards in all, card 64 KiB, title 1 KiB, settings JSON 64 KiB. Over a limit: Error::TooLarge; the file then opens as plain Markdown (D6), never truncated.

D3 - Fidelity rule (the one that matters)

to_markdown(parse(x)) equals x byte for byte for every board the real plugin writes, and equals x after normalising only: list marker to -, trailing spaces, final newline. Property test: parse then serialise, then parse again, boards are equal. Edits touch only the lanes/cards that changed; a card the user never touched is serialised from its original text.

D4 - Settings keys read

From the footer JSON: list-collapse: [bool] (per-lane collapse state, written back when the user folds a lane), max-archive-size, new-card-insertion-method (prepend|prepend-compact|append), archive-with-date, date-trigger (@), time-trigger (@@), date-format, time-format, show-checkboxes. Unknown keys preserved. Reading/writing is serde_json-free: the crate has no JSON dependency, so a tiny flat-object JSON scanner (settings.rs, string/number/bool/array of bool only, everything else treated as opaque and the whole footer rewritten ONLY when a known key changes).

D5 - Card metadata

card_meta(&Card, &Settings) -> Meta { title, date: Option<String>, time: Option<String>, tags, links, done }. @{2026-10-21} and @@{13:00} with the configured triggers; tags via the markdown tag scanner; wiki links via the link scanner. title = first line without those tokens' decoration for display only.

D6 - Engine and tab

Engine crates/engine/src/kanban/ (mod.rs, commands.rs, apply.rs):

id params result writes
kanban.read {path} board JSON: lanes, cards with meta, archive count, settings no
kanban.is_board {path} {board: bool} (index property check, cheap) no
kanban.create {path, lanes?: [..]} {path} (default lanes To do / Doing / Done) yes
kanban.card.add {path, lane, text, position?: "top"|"bottom"} board yes
kanban.card.edit {path, lane, index, text} board yes
kanban.card.move {path, from_lane, from_index, to_lane, to_index} board yes
kanban.card.toggle {path, lane, index} board yes
kanban.card.archive {path, lane, index} board yes
kanban.card.delete {path, lane, index} board yes
kanban.lane.add / rename / move / delete / archive_cards / collapse lane params board yes

Every write command takes an optional expect_hash (hash of the file text the UI rendered); mismatch is conflict and the UI reloads. Writes go through the open buffer as one undoable edit when the note is open, else Session::write_note (same as note.property_set). Add all ids to ai/assistant_gate.rs (reads READ, others WRITE) and CORE_IDS.

A file that fails parse (limits, no kanban-plugin) opens as a normal note. A board note shows a header button "Open as Markdown" (per-tab state, not persisted) so the source is always one click away.

D7 - UI

crates/ui-egui/src/views/kanban_view/ (mod.rs, state.rs queues engine calls and pumps after the frame like base_view/state.rs, lane.rs, card.rs, drag.rs, add.rs). Dispatch: in shell/app/window/views.rs::show, the TabKind::Note arm first asks the cached kanban.is_board for the path (cache invalidated with the index generation); a board renders kanban_view. Tab icon: the board icon from design/icons.rs (icon!("kanban"), add the Lucide kanban.svg under assets/icons/).

  • Lanes are rounded cards on the canvas colour (tone, not borders), fixed width 280 px (scaled), vertical scroll inside a lane, horizontal scroll of the board.
  • Card: checkbox (when show-checkboxes), rendered inline Markdown (links clickable, tags as chips, date chip, overdue uses the danger token), double click or Enter edits in a Textarea, Esc cancels, Cmd+Enter saves. Drag handle = the whole card; drop indicator line between cards; dropping on a lane header appends. Drags use egui::DragAndDrop payloads (CardRef { lane, index }), a drop sends kanban.card.move once.
  • Add card at the bottom of a lane ("+ Add a card") and "+ Add a list". Lane menu: rename, move left/right, archive all cards, delete, collapse.
  • Keyboard: arrows move focus between cards, Alt+arrows move the card, Enter edit, Delete confirms (ConfirmModal), x toggles.
  • Compact layout: one lane at a time with a lane switcher (SPEC compact rules).

Acceptance criteria

  1. A board file written by the real plugin (fixture copied under crates/kanban/tests/fixtures/) parses and to_markdown returns it byte-identical (Q1 resolved first).
  2. Moving a card to a Complete lane writes - [x]; back out writes - [ ]; a card in the middle of a lane moved by 1 changes exactly two list lines (byte diff test).
  3. Unknown paragraphs between lanes, unknown settings keys and block ids survive any op.
  4. Archive keeps the thematic break + ## Archive heading; archiving the first card creates them.
  5. A 200-lane / 50 000-card file parses in < 300 ms (ignored perf test); beyond limits opens as Markdown.
  6. Dragging a card in the UI produces ONE kanban.card.move and one undo step in an open editor of the same note; undo restores the previous text.
  7. External edit of the file (sync merge) refreshes the board within one frame of the index generation change; a stale expect_hash write gives conflict.
  8. Compact layout shows one lane at a time; a screenshot test at 1440x900 and 390x844.

Open questions

  • Q1: obtain a board file saved by the real plugin (owner has Obsidian) and commit it as the golden fixture BEFORE task 1 is accepted; confirm **Complete**, ***, the settings fence and the (N) max-items form.
  • Q2: new boards: write frontmatter kanban-plugin: board exactly like the plugin (yes, for round trip).
  • Q3: should boards appear in the Explorer with the board icon without opening? Needs an index property read (cheap): yes, in task 10.

Feature ideas (researched, ranked by value)

  1. Bases Kanban view (type: kanban in a .base, group by a property, drag changes the property): one engine call base.row.set already exists; highest leverage because it makes any property (status, stage) a board without a special file. Needs a Bases Kanban spec section.
  2. Card progress and checklist counts: a card whose body has sub-tasks shows 2/5; reads task lines. Pairs with the Tasks index (item 11): a card is a task, a lane is a status.
  3. New note from card (right click, "Create note"): turns - [ ] Idea into [[Idea]] and creates the note from a template (Templater subset, item 12).
  4. Card metadata from linked notes: show chosen properties of [[linked]] notes on the card (plugin "metadata keys"), using the Bases property reader.
  5. Lane WIP limits shown (## Doing (3) is already in the format): red header when exceeded.
  6. Date and time triggers with a picker (@{...}, @@{...}), reusing the calendar quick-add parser, and "sort lane by date".
  7. Tag colours and date colours settings, stored in the footer JSON like the plugin.
  8. Archive with date (archive-with-date) and an "Archive done cards older than N days" command.
  9. Board search/filter (text, tag) highlighting matches and dimming the rest.
  10. Per-card context menu: copy link to card (block id), duplicate, split, move to board (another board file).