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: kanbanin 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
---, keykanban-pluginwith valueboard(older files:basic, both accepted; written back as read). Any other frontmatter keys are kept. - Lane = a
## Titleheading. Optional max-items suffix## Title (5)is read asmax_items = 5and 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## Archiveheading and the archived cards. - Settings = the final
%% kanban:settingsblock; 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
canvascolour (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,overdueuses thedangertoken), double click or Enter edits in aTextarea, Esc cancels, Cmd+Enter saves. Drag handle = the whole card; drop indicator line between cards; dropping on a lane header appends. Drags useegui::DragAndDroppayloads (CardRef { lane, index }), a drop sendskanban.card.moveonce. - 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),xtoggles. - Compact layout: one lane at a time with a lane switcher (SPEC compact rules).
Acceptance criteria
- A board file written by the real plugin (fixture copied under
crates/kanban/tests/fixtures/) parses andto_markdownreturns it byte-identical (Q1 resolved first). - 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). - Unknown paragraphs between lanes, unknown settings keys and block ids survive any op.
- Archive keeps the thematic break +
## Archiveheading; archiving the first card creates them. - A 200-lane / 50 000-card file parses in < 300 ms (ignored perf test); beyond limits opens as Markdown.
- Dragging a card in the UI produces ONE
kanban.card.moveand one undo step in an open editor of the same note; undo restores the previous text. - External edit of the file (sync merge) refreshes the board within one frame of the index generation
change; a stale
expect_hashwrite givesconflict. - 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: boardexactly 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)
- Bases Kanban view (
type: kanbanin a.base, group by a property, drag changes the property): one engine callbase.row.setalready exists; highest leverage because it makes any property (status, stage) a board without a special file. Needs a Bases Kanban spec section. - 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. - New note from card (right click, "Create note"): turns
- [ ] Ideainto[[Idea]]and creates the note from a template (Templater subset, item 12). - Card metadata from linked notes: show chosen properties of
[[linked]]notes on the card (plugin "metadata keys"), using the Bases property reader. - Lane WIP limits shown (
## Doing (3)is already in the format): red header when exceeded. - Date and time triggers with a picker (
@{...},@@{...}), reusing the calendar quick-add parser, and "sort lane by date". - Tag colours and date colours settings, stored in the footer JSON like the plugin.
- Archive with date (
archive-with-date) and an "Archive done cards older than N days" command. - Board search/filter (text, tag) highlighting matches and dimming the rest.
- Per-card context menu: copy link to card (block id), duplicate, split, move to board (another board file).