api reference
Kanban API Reference
createKanban: columns from any status property, drag or keyboard moves that write back, swimlanes, sprints, epics and the child grid.
API reference › The board (kanban) view
All 29 pages Everything on one page → Developer guide →
The board (kanban) view
modules/kanban is an opt-in view of grid rows as cards, grouped into columns by a configurable property - a status, a stage, a state GUID, whatever the host's schema calls it. It is a separate bundle in the same shape as charts and the data router: it adds no weight to a page that does not load it, changes nothing in grid core, and pulls in no dependency. A board is just another dataset viewer: it consumes data through the same keyed-diff contract a grid exposes, board.rows.apply({ add, update, remove }), so a Data Router can attach(value, board) and drive a kanban beside a grid and a chart off one feed. (Distinct from the grid-core board presentation mode, config.board in, which lays a single grid's rows out as lanes; this is a standalone view module.)
import { createKanban } from '@toclocoinc/lattice-grid/modules/kanban';
const board = createKanban(document.querySelector('#board'), {
rows, // or { grid } to bind to a live grid; reuses its column formatters
rowKey: 'id',
columnProperty: 'status', // the group-by property (configurable)
columns: [ // shown even when empty; this order is honoured
{ id: 'todo', title: 'To do' },
{ id: 'doing', title: 'In progress', wipLimit: 3 },
{ id: 'done', title: 'Done', color: '#2e7d32' },
],
pointsProperty: 'points', // configurable; drives the header points sum
showPoints: true,
card: { title: 'title', subtitle: 'assignee', labels: 'tags' },
readonly: { columns: { done: true } }, // granular: whole board / per column / per card
onCardClick: ({ card }) => open(card.row),
});
Every structural property is named in config - columnProperty, pointsProperty, orderProperty, swimlaneProperty, sprintProperty, epicProperty - so the same board maps any customer schema (a status field, points, sprint, epic, a swimlane) without code change. Configured columns show even when empty; a data value outside them gets its own column rather than being dropped. Each column header carries its card count and, with showPoints, its points sum, and is flagged when the count exceeds a column's wipLimit. Accessibility is built in: the board is a labelled group, each column a labelled list, each card a list item in a roving-tabindex focus ring with arrow-key navigation, and a polite live region is present for the move announcements a later cycle adds.
| Member | Description |
|---|---|
| createKanban(el, config) | Create a board. Pass a DOM element to render into, or null for a headless board that computes the same column/card model without a DOM. |
| columns() / column(id) | The columns with their cards and aggregates, or one column by id (id, title, count, points, wipLimit, over, cards). |
| count(id) / points(id) | A column's card count, and its points sum from pointsProperty. |
| cards() / card(key) | Every card model, or one by its key. |
| rows.apply({ add, update, remove }) | The keyed-diff consumer contract a grid shares, so the board is a drop-in Data Router target. Also rows.forEach and rows.count. |
| move(keys, toColumn, toIndex?) | Move one or more cards to a column (and, with orderProperty, to a position within it) - the entry point behind drag-and-drop and keyboard move. Runs the transitions/require workflow rules first (a disallowed or missing-field move fails with a named [lattice] error), then onBeforeMove, then writes through the grid's shipped write-back path. |
| select / selection / isSelected / clearSelection | Card selection: select(keys, 'set'|'add'|'toggle'|'remove'), the selected keys, a membership test, and a clear. Click selects; Ctrl/Cmd toggles; Shift extends within a column. |
| collapseColumn(id) / collapseLane(id) | Collapse, expand or toggle a column or swimlane; emits column:collapse / swimlane:collapse. State survives a keyed-diff update. |
| reorderColumns(order) / moveColumn(id, before) | Reorder the columns (also done by dragging a column header); emits column:reorder. |
| setColumns(defs) | Replace the whole column set after construction - a tenant renaming, adding or removing a column, in one call, without rebuilding the board. Id-keyed like the grid's own columns.apply(state): a column that keeps its id keeps its cards, its collapsed state and its place in a pinned reorderColumns order; the quick filter and the selection are untouched. A column dropped from defs is not specially handled - a card whose value has nowhere configured to go re-derives a plain, humanised ad hoc column rather than being dropped, the same rule an always-unconfigured value already gets. |
| setQuickFilter(text) / setFilter(fn) / facets(property) | Quick text search across card fields, a predicate filter, and distinct-value counts for a facet control. |
| filters.where(name, fn) / filters.where(name, null) / filters.where() / filters.reapply(name?) | Named predicates, composed with AND - the grid's own filters.where convention. Register or replace one under name; where(name, null) removes only that one, leaving the others in force; where() lists the registered names; reapply(name?) re-runs and re-renders. The quick filter is untouched by any of this. setFilter(fn) is unchanged sugar for where(filters.DEFAULT, fn), so it composes with any other named predicate instead of replacing it. |
| setSprint(id) / showBacklog() / sprints() | Sprint view and switcher: show one sprint, the backlog (board.BACKLOG - cards with no sprint), or all; and the distinct sprint values. Emits sprint:changed. |
| setEpic(id) / epics() / epicRollup() / rollup(property) | Epic view and rollup: filter to an epic, list epics, and roll rows up by epic (or any property) into count, points, and progress toward the done columns. |
| expand(key) / closeDetail() / canExpand(card) | Pop a card's children out as a nested child grid (or board) in a drawer/modal/inline container; emits card:expand and card:drill. |
| editCard(key, field) / applyEdit(key, field, value) | Inline-edit a card field opted in with card: { title: { field, edit: true } }: grid-bound it commits through the grid's own field editor path (grid.edit.setCells); standalone it uses a host editor factory or a default input, reverting when onCardEdit rejects. Double-click a card to edit; emits card:edit. |
| addCard(columnId, seed?) | Add a card to a column (with config.addCard's per-column affordance) and open it in inline edit; a host onAddCard(columnId) supplies the row, or one is generated (grid-bound via grid.edit.addRow). Emits card:add. |
| getState() / setState(snapshot) | Serialise and restore the board state - collapsed columns/lanes, column order, quick filter, sprint/epic selection and selection. Also accepted as config.state at construction. |
| sla | The card-aging / SLA monitor, present only when a sla config is supplied. Read sla.states(), sla.breaches()/sla.warnings() and sla.stateFor(cardOrKey) for each card's age and level; sla.evaluate() re-checks and fires crossings. See the card-aging note below. |
| setLoading(bool) / setError(message) | A loading state and a host-supplied error banner; empty columns already render their placeholder. |
| fields | Extra columns of the bound grid to project onto the rows a tile filter sees, beyond the fields the tiles declare. A read of a bound column outside the projection still resolves, and warns once naming the tile and the column; a field naming no column at all is refused by name at first read, and that tile reports unknown rather than an aggregation identity. |
| setRows(rows) / refresh() | Replace the source rows, or recompute and re-render. What refresh() re-reads depends on where the rows come from: a grid-bound board re-reads the bound grid; a board still on its config.rows re-reads that array (a host that mutated it in place sees the change); a routed board - any row has arrived through rows.apply, typically from a Data Router attach - re-reads nothing: it regroups from the rows it holds and shows exactly what it showed before, re-rendered. setRows puts a routed board back on its configured rows. |
| on(name, fn) / off(name, fn) | Events: card:click, card:dblclick, card:contextmenu, card:move, card:reverted, card:confirmed, card:sla, selection:changed, drag:start, drag:end (plus the vocabulary the later cycles emit). On a grid-bound board a move fires card:move optimistically; the grid's write-back then settles it with card:confirmed or, if the server rejects, card:reverted (the card re-reads and the flow transition log rolls the optimistic move back). |
| readonly(scope) | Whether a scope is readonly - the whole board, a { column } or a { card }. A readonly card is not draggable; a move into a readonly column is refused. |
| destroy() | Empty the element and drop the model. The host still owns any bound grid. |
Drag-and-drop and keyboard move (write-back). Cards drag between columns (writing columnProperty) and within a column into a position (writing orderProperty with fractional ranking, so only the moved cards' order is written). The same move is available from the keyboard: focus a card, press Space to grab it, use the arrows to choose a target column and position (announced on a live region), Space/Enter to drop, Escape to cancel. Multi-select drags every selected card. A move calls onBeforeMove(card, from, to, index) first - return false (or a promise of it) to veto - then persists: grid-bound, through grid.edit.setCells (the same public edit-commit path inline editing and the write-back adapter use, so the grid's own pipeline owns the optimistic apply, the confirm and the revert); standalone, optimistically with a revert when onCardMove returns false/rejects. A configurable per-card contextMenu (an array or fn(card, selected)) replaces the card:contextmenu event when present.
Touch. The drag above is a pointer gesture, so cards, column headers and swimlane headers drag on phones and tablets as well as with a mouse. A mouse drag starts on movement; a touch or pen drag starts after a short press-and-hold - moving before the hold scrolls the board instead, so a quick swipe never accidentally moves a card. While a drag is held, the pointer auto-scrolls a board edge it sits within 24 px of, and Escape or a second finger cancels. A completed move still fires the same beforeMove/card:move events and the htmx lattice:kanban-change post, once per move.
Swimlanes, collapse, reorder and search. Set swimlanes: true to render a 2D lane×column grid grouped by swimlaneProperty: one band per lane with its own count/points, columns aligned across every lane, the board scrolling vertically through lanes and horizontally through columns inside its own box. A drag across lanes writes the swimlane property too. Columns and lanes collapse (their state survives a keyed-diff update); columns reorder by dragging their header (reorderColumns/moveColumn). setQuickFilter(text) searches across card fields, setFilter(fn) applies a predicate (sugar for filters.where(filters.DEFAULT, fn) - filters.where(name, fn) registers any number of independent named predicates, ANDed together), and facets(property) returns distinct-value counts to build a facet control. Naming the swimlaneProperty is separate from turning on the lane view, so a board can carry it for a cross-lane move without switching layout.
Sprint, epic and card pop-out. setSprint(id) shows one sprint, showBacklog() the cards with no sprint, and sprints() feeds a switcher; setEpic(id) narrows to an epic and epicRollup() (or rollup(property)) returns per-epic count, points and progress toward the done columns. A card can pop out a nested grid of its children - an epic's stories, a story's tasks, recursively. The child relationship is a childrenProperty (parent-id within the dataset) and/or a loadChildren(card) (per-card dataset or async fetch), and the child is a full composed createGrid (sort/filter/edit/write-back) - supplied as children.factory - opened in a drawer (default), modal or inline. With children.asBoard the child is itself a board, so it can pop its own children. This reuses the grid by composition and adds no grid-core coupling. expand(key) and the per-card drill affordance emit card:expand; a deeper open emits card:drill.
Live updates. Because the board consumes data through the same keyed-diff contract a grid does, a Data Router drives it directly - router.attach(board, predicate) - and one feed fans out to a grid, a kanban, a chart and a KPI tile at once. A live rows.apply({ add, update, remove }) is applied as a keyed diff (an unchanged card keeps its model) and re-rendered preserving scroll, focus, selection, collapsed columns/lanes and any open pop-out, so a card can appear, move or update under the user without losing their place.
Scale, state and accessibility. virtualize renders only a scroll window of a tall column (with true-height spacers so the scrollbar stays honest), for boards of thousands of cards. getState()/setState() (and config.state) save and restore the collapsed columns and lanes, the column order, the quick filter and the sprint/epic selection, so a reopened board comes back as it was; setLoading/setError add loading and error states. Accessibility runs throughout: the board is a labelled group of labelled column lists, cards are a roving-tabindex focus ring (arrows to move focus, Enter to activate), the move is fully keyboard-driven (Space grab, arrows for column/position, Alt+↑/↓ across swimlanes, Space/Enter drop, Escape cancel) with live-region announcements, and every affordance carries a name.
Column width. A board used to take a fixed --lattice-board-column-width (default 280px) on every column, so a three-column board left a wide host mostly empty and a page that computed the token from the viewport overflowed its host with a horizontal scrollbar. The columnWidth option fixes that: 'fill' (the default) shares the board width equally between the columns, never below minColumnWidth (240px), so a board that fits fills its host edge-to-edge and one that does not scrolls at the minimum; a number fixes every column at that many pixels; 'auto' keeps the natural token width. Collapsed columns stay narrow strips and the remaining columns take the freed space, and the board follows its host's resize. Setting --lattice-board-column-width on the host element still overrides. The resolved values are read back at board.columnWidth and board.minColumnWidth.
Card aging / SLA. A sla config ages each card and highlights the ones sitting too long. Thresholds are a raw millisecond count or a { days, hours, … } spec, set globally as { warn, breach }, per column (either sla.columns[id], or a column def's own sla/slaWarn/slaBreach) and per swimlane (sla.lanes[id]); the most specific wins, lane → column → global. basis chooses whether the clock is time-in-current-column (default) or age-on-the-board, resolved from the flow transition log, an enteredProperty/createdProperty timestamp, or arrival. The view puts an age chip on aged cards (showAge: 'always' shows it on every card) and a highlight on breached ones. A rising crossing (ok→warn, warn→breach) fires the card:sla event and the onWarn/onBreach(level, rows) callbacks - the same (signal, rows) shape a Data Router alert route uses, so one handler serves both. It is reached at runtime as board.sla; done-column cards are exempt by default (ignoreDone: false opts them in), and an optional tick re-checks so a card that breaches by simply sitting still still lights up. Example: createKanban(el, { …, sla: { warn: { days: 2 }, breach: { days: 4 }, onBreach: notify } }).
Inline edit and add-card. A field is opted into inline edit with the object mapping form - card: { title: { field: 'title', edit: true } }. Double-clicking a card (or editCard(key, field)) edits it in place: grid-bound, through the grid's own field editor for that column via its public edit path, so the column's parse, validate and optimistic/confirm/revert all run; standalone, through a host editor factory (or a default input), reverting when onCardEdit rejects. A per-column add-card affordance (config.addCard) creates a card carrying the column's group value - from a host onAddCard(columnId), or generated, or appended through grid.edit.addRow when bound - and opens it straight in inline edit on its title, so the user just types. The module imports nothing from the grid's DOM package: grid-bound edits ride the grid's public edit API, standalone edits use the host's editor, so a board-only page never pulls the grid in.
Blocked cards and dependencies. A card can be blocked outright, or blocked by other cards. blockedProperty names a row field that blocks a card - a boolean flag, or a reason string whose non-empty value is the reason the blocked badge shows (with an icon and a warning-tone chip, and an accessible name, so it is never colour alone). blockedByProperty names the field holding the keys of the cards that block this one (a comma-separated string or an array); the card then shows a "blocked by N" list of focusable chips, each one focusing (opening) its blocking card, and flips to a ready state once every blocker is in a done column. A blocked filter - setBlockedFilter(true), or blockedFilter: true at construction - shows only blocked cards (a card with an unresolved blocker counts as blocked). Blocking and unblocking run through setBlocked(key, blocked, reason?) (or toggleBlocked(key)), which raise card:edit with field: 'blocked' like any other edit, and a blocked card is refused any forward move (a move into a column to its right). The flow analytics record blocked intervals and report board.flow.blockedTime(), board.flow.flowEfficiency() (active time / cycle time) and board.flow.blockedTimeByColumn(), so a cycle time that was really time waiting on someone else stops looking like active work.
Blocked cards and dependencies, executed
An explicit block with a reason, a dependency on another card, the blocked filter, and a blocker
resolving to ready - headless, run on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const board = createKanban(null, {
rows: [
{ id: 'a', status: 'todo', title: 'A' },
{ id: 'b', status: 'todo', title: 'B', blockedBy: 'a' }, // blocked by card a
{ id: 'c', status: 'doing', title: 'C', blocked: 'Waiting on design' }, // explicit reason
],
rowKey: 'id',
columnProperty: 'status',
columns: [{ id: 'todo' }, { id: 'doing' }, { id: 'done', done: true }],
blockedProperty: 'blocked',
blockedByProperty: 'blockedBy',
blockedFilter: true, // start filtered to blocked cards
card: { title: 'title' },
});
board.setBlockedFilter(false); // clear it (only b and c were shown)
const c = board.card('c');
const badge = c.blocked + ' ' + c.blockedReason; // true Waiting on design
const b = board.card('b');
const deps = b.blockedBy.join(',') + ' ' + b.blocked; // a true (a is not done yet)
board.setBlockedFilter(true);
const filtered = board.model.cardsByKey.size; // 2 (b and c)
board.setBlockedFilter(false);
await board.setBlocked('c', false); // unblock C
const cleared = board.card('c').explicitBlocked; // false
await board.toggleBlocked('c', 'oops'); // re-block C
const toggled = board.card('c').blocked; // true
await board.move('a', 'done'); // resolve B's blocker
const ready = board.card('b').ready; // true
board.destroy();
return [badge, deps, filtered, cleared, toggled, ready].join(' | ');
Richer card parts. Beyond title/subtitle/labels, the card mapping takes four built-in parts, each optional and each folded into the card's accessible name: assignee - one or several people (a name, a comma-separated string, or an array of { name, avatar } objects) drawn with the avatar component the Gantt has used since 1.88, image with an initials fallback coloured from the name, and a +N overflow beyond max (default 2); due - a date field shown in the grid's date format when bound, with a due-soon/overdue state and icon from a dueSoon day threshold (card: { due: { field: 'due', dueSoon: 3 } }); progress - a 0..1 fraction or 0..100 value as a thin bar; and checklist - a { done, total } object or two named fields, shown as 3/5. A bound grid's own formatters are reused, and board search matches assignee names. density: 'compact' tightens each card (less padding, no subtitle) so a column fits more cards; the default 'comfortable' keeps the roomier card.
Undo and redo. Every user move (a drag, a reorder, a lane change), add and inline edit is one undo step - a multi-card drag is one step, because it reaches move() once with every key it carries. board.undo() re-runs the inverse through the board's own write path (the workflow rules, the onBeforeMove/onBeforeEdit veto, the WIP rules and the confirm/revert pipeline still run, so a refused undo leaves the card and the history unchanged and logs a catalogued warning); board.redo() re-applies, and canUndo()/canRedo() report the stacks, with a history event ({ canUndo, canRedo, label }) on every change. Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z undo and redo while the board has focus (the shortcut stands aside inside a text field). historyDepth caps the steps kept (default 100, 0 turns undo off). On a grid-bound board the board shares the grid's timeline, so one Ctrl+Z order covers a grid edit and a board move; host loads through rows.apply are never undoable.
A board, grouped and aggregated, executed
A customer-shaped set - statuses as columns, points, an empty configured column,
a WIP limit, and one status outside the configured set - grouped headless, then a keyed diff applied
through the same rows.apply contract the Data Router drives. Run on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const board = createKanban(null, {
rows: [
{ id: 'a', status: 'todo', points: 3, title: 'Login form' },
{ id: 'b', status: 'doing', points: 5, title: 'OAuth' },
{ id: 'c', status: 'doing', points: 2, title: 'Reset flow' },
{ id: 'd', status: 'done', points: 8, title: 'Audit log' },
{ id: 'z', status: 'archived', points: 1, title: 'Old ticket' }, // outside the configured columns
],
rowKey: 'id',
columnProperty: 'status',
columns: [
{ id: 'todo', title: 'To do' },
{ id: 'doing', title: 'In progress', wipLimit: 1 },
{ id: 'done', title: 'Done' },
{ id: 'blocked', title: 'Blocked' }, // configured but empty; still shown
],
pointsProperty: 'points',
card: { title: 'title' },
});
const todo = board.count('todo') + '/' + board.points('todo'); // 1/3
const doing = board.count('doing') + '/' + board.points('doing'); // 2/7
const over = board.column('doing').over ? 'over' : 'ok'; // over the WIP limit of 1
const extra = board.column('archived') ? 'archived' : 'dropped'; // an out-of-set value is kept
// The keyed-diff consumer contract a Data Router drives: move b to done, drop c.
board.rows.apply({ update: [{ id: 'b', status: 'done', points: 5, title: 'OAuth' }], remove: ['c'] });
const done = board.count('done') + '/' + board.points('done'); // 2/13
board.destroy();
return [todo, doing, over, extra, done].join(' | ');
Move, reorder and veto, executed
The move path drag-and-drop and keyboard move share: a reorder writes the order
property with fractional ranking, a cross-column move writes the column property, and
onBeforeMove can veto. Run headless on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const board = createKanban(null, {
rows: [
{ id: 'a', status: 'todo', ord: 1, title: 'A' },
{ id: 'b', status: 'todo', ord: 2, title: 'B' },
{ id: 'c', status: 'todo', ord: 3, title: 'C' },
],
rowKey: 'id',
columnProperty: 'status',
columns: [{ id: 'todo' }, { id: 'doing' }, { id: 'done' }],
orderProperty: 'ord',
// Veto any move into 'doing'; allow the rest.
onBeforeMove: (card, from, to) => to !== 'doing',
});
await board.move('c', 'todo', 0); // reorder C to the top of todo
const order = board.column('todo').cards.map((x) => x.key).join(','); // c,a,b
await board.move('a', 'done'); // cross-column move writes the column
const moved = board.card('a').columnId; // done
await board.move('b', 'doing'); // vetoed by onBeforeMove
const vetoed = board.card('b').columnId; // still todo
return [order, moved, vetoed].join(' | ');
Workflow rules. Two optional config maps make the board enforce a workflow
before a move persists, and both run first - before onBeforeMove, before the
write-back, before any confirm or revert. transitions maps a source column id to the
destination column ids it may move to (a list, or '*' for any); a source absent from the map
may move anywhere, so a board that never configures rules stays exactly as permissive as it was.
require maps a destination column id to the row properties a card must carry to enter
it ({ field, label?, editor? }). A drag over a disallowed column shows it not droppable
(dropEffect none, aria-dropeffect="none") and the keyboard move skips it; a drop into
a column whose required fields are missing opens a small form, and the move commits only once the values
are filled (cancel reverts). A programmatic board.move honours the same rules and fails
with a catalogued [lattice] error naming the rule. Either map may also be declared on a lane
(config.lanes[i].transitions / .require), overriding the board per key.
Workflow rules, executed
A transition map that gates movement between columns and a required field on
done - the refused programmatic moves name their rule, and a card that already carries the
required value moves through. Run headless on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const board = createKanban(null, {
rows: [
{ id: 1, status: 'todo', title: 'A' },
{ id: 2, status: 'doing', title: 'B', deployed: '2026-01-01' },
],
rowKey: 'id',
columnProperty: 'status',
columns: [{ id: 'todo' }, { id: 'doing' }, { id: 'done' }],
card: { title: 'title' },
transitions: { todo: ['doing'], doing: ['done'] }, // todo may only reach doing
require: { done: [{ field: 'deployed', label: 'Deployed' }] },
});
let transition = 'no';
try { await board.move(1, 'done'); } // todo → done is not in todo's map
catch (err) { transition = /kanban\.transition/.test(err.message) ? 'transition' : err.message; }
let required = 'no';
await board.move(1, 'doing'); // todo → doing is allowed
try { await board.move(1, 'done'); } // but done requires `deployed`
catch (err) { required = /kanban\.require/.test(err.message) ? 'require' : err.message; }
const moved = (await board.move(2, 'done')).moved.length; // 1 - card 2 already carries it
board.destroy();
return [transition, required, moved].join(' | ');
Swimlanes, collapse and search, executed
Lanes with per-lane aggregates, a collapsed column, a reorder, and a quick-filter - all headless, run on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const board = createKanban(null, {
rows: [
{ id: 1, status: 'todo', assignee: 'Ann', points: 3, title: 'Login' },
{ id: 2, status: 'doing', assignee: 'Ann', points: 5, title: 'OAuth' },
{ id: 3, status: 'doing', assignee: 'Bob', points: 2, title: 'Reset' },
{ id: 4, status: 'done', assignee: 'Bob', points: 8, title: 'Audit' },
],
rowKey: 'id',
columnProperty: 'status',
columns: [{ id: 'todo' }, { id: 'doing' }, { id: 'done' }],
swimlaneProperty: 'assignee',
swimlanes: true,
pointsProperty: 'points',
card: { title: 'title' },
});
const ann = board.model.lanes.find((l) => l.id === 'Ann');
const lane = 'Ann ' + ann.count + '/' + ann.points; // Ann 2/8
board.reorderColumns(['done', 'todo', 'doing']);
const order = board.columns().map((c) => c.id).join(','); // done,todo,doing
board.setQuickFilter('reset');
const shown = board.model.cardsByKey.size; // 1 (only "Reset" matches)
return [lane, order, shown].join(' | ');
Richer card parts, executed
Assignee avatars, a progress bar, a checklist count, an overdue due date, the compact density and a search that matches an assignee's name - resolved headless, run on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const board = createKanban(null, {
rows: [
{ id: 1, status: 'todo', title: 'Ship', assignee: [{ name: 'Ann' }, 'Bo Chen'], pct: 0.4, ck: { done: 3, total: 5 } },
{ id: 2, status: 'todo', title: 'Late', assignee: ['Cy'], due: new Date(Date.now() - 86400000).toISOString() },
],
rowKey: 'id',
columnProperty: 'status',
columns: [{ id: 'todo' }],
density: 'compact',
card: { title: 'title', assignee: 'assignee', progress: 'pct', checklist: 'ck', due: { field: 'due', dueSoon: 3 } },
});
const card = board.card(1);
const overdue = board.card(2).due.state; // overdue
board.setQuickFilter('Ann'); // search matches an assignee name
const shown = board.model.cardsByKey.size; // 1 (only "Ship")
board.destroy();
return [card.assignees.length, card.progressPct, card.checklist.done + '/' + card.checklist.total, overdue, shown].join(' | ');
Label and badge tones, and a due format, executed
A label or badge tone (a map from value to accent, info,
success, warning, danger or neutral, or a function of the row) draws the chip
in the theme's variant roles; a due format (a date pattern or a function) sets the chip text. Unmapped values stay
neutral and raw - resolved headless, run on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const board = createKanban(null, {
rows: [
{ id: 1, status: 'todo', title: 'Ship', tags: 'bug, ok, misc', due: '2026-03-05T12:00:00' },
{ id: 2, status: 'todo', title: 'Fix', tags: 'bug', due: '2026-03-05T12:00:00' },
],
rowKey: 'id',
columnProperty: 'status',
columns: [{ id: 'todo' }],
card: {
title: 'title',
labels: { field: 'tags', tone: { bug: 'danger', ok: 'success' } },
due: { field: 'due', format: 'MMM d', locale: 'en-US' },
},
});
const tones = board.card(1).labelTones;
const text = board.card(1).due.text;
board.destroy();
const plain = createKanban(null, {
rows: [{ id: 1, status: 'todo', due: '2026-03-05T12:00:00' }],
rowKey: 'id', columnProperty: 'status', columns: [{ id: 'todo' }],
card: { due: { field: 'due', format: (d) => 'due ' + d.getFullYear() } },
});
const fn = plain.card(1).due.text;
plain.destroy();
return [tones.bug, tones.ok, tones.misc || 'none', text, fn].join(' | ');
Column width, executed
The columnWidth and minColumnWidth options resolve to fill (default),
a fixed pixel width, auto, and a raised minimum - headless, run on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const base = createKanban(null, {
rows: [], rowKey: 'id', columnProperty: 'status',
columns: [{ id: 'todo' }, { id: 'doing' }, { id: 'done' }],
});
const fixed = createKanban(null, {
rows: [], rowKey: 'id', columnProperty: 'status',
columns: [{ id: 'todo' }], columnWidth: 300,
});
const auto = createKanban(null, {
rows: [], rowKey: 'id', columnProperty: 'status',
columns: [{ id: 'todo' }], columnWidth: 'auto',
});
const wide = createKanban(null, {
rows: [], rowKey: 'id', columnProperty: 'status',
columns: [{ id: 'todo' }], minColumnWidth: 200,
});
return [base.columnWidth, base.minColumnWidth, fixed.columnWidth, auto.columnWidth, wide.minColumnWidth].join(' | ');
Sprint, backlog and epic rollup, executed
A sprint view, the backlog, and an epic rollup with progress toward the done column - headless, run on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const board = createKanban(null, {
rows: [
{ id: 's1', status: 'todo', epic: 'E', sprint: 'S1', points: 3, title: 'Story 1' },
{ id: 's2', status: 'done', epic: 'E', sprint: 'S1', points: 5, title: 'Story 2' },
{ id: 's3', status: 'todo', epic: 'E', sprint: null, points: 2, title: 'Story 3' },
],
rowKey: 'id',
columnProperty: 'status',
columns: [{ id: 'todo' }, { id: 'done', done: true }],
pointsProperty: 'points',
sprintProperty: 'sprint',
epicProperty: 'epic',
});
board.setSprint('S1');
const inSprint = board.model.cardsByKey.size; // 2 (s1, s2 are in S1; s3 has no sprint)
board.showBacklog();
const backlog = board.model.cardsByKey.size; // 1 (s3)
board.setSprint(undefined);
const e = board.epicRollup()[0];
const rollup = e.points + ' ' + e.progress; // 10 0.5
return [inSprint, backlog, rollup].join(' | ');
Live, driven by a Data Router, executed
One feed, routed to a board through the same keyed-diff contract a grid uses: a snapshot hydrates it and a delta moves a card. Run headless on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const { createDataRouter } = await import('../packages/modules/data-router/index.js');
const board = createKanban(null, {
rows: [], rowKey: 'id', columnProperty: 'status',
columns: [{ id: 'todo' }, { id: 'done' }], card: { title: 'title' },
});
const router = createDataRouter({ key: 'kind', rowKey: 'id' });
router.attach(board, 'task'); // the board is a drop-in router target
router.load([
{ id: 't1', kind: 'task', status: 'todo', title: 'T1' },
{ id: 't2', kind: 'task', status: 'done', title: 'T2' },
]);
const hydrated = board.count('todo') + ',' + board.count('done'); // 1,1
router.apply([{ op: 'upsert', row: { id: 't1', kind: 'task', status: 'done', title: 'T1' } }]);
const afterDelta = board.count('todo') + ',' + board.count('done'); // 0,2
router.destroy();
return [hydrated, afterDelta].join(' | ');
Re-bound to a replacement grid, executed
A grid-bound board, pointed at a replacement grid through rebind(grid) - the same call the htmx module's re-bind pass makes after a swap. Run headless on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
rowKey: 'id', columns: [{ field: 'title' }, { field: 'status' }],
rows: [{ id: 1, title: 'A', status: 'todo' }],
});
const board = createKanban(null, {
grid, columnProperty: 'status', columns: [{ id: 'todo' }, { id: 'done' }], card: { title: 'title' },
});
const before = board.cards().map((c) => c.fields.title).join(','); // A
const replacement = createHeadlessGrid({
rowKey: 'id', columns: [{ field: 'title' }, { field: 'status' }],
rows: [{ id: 2, title: 'X', status: 'todo' }],
});
board.rebind(replacement);
const after = board.cards().map((c) => c.fields.title).join(','); // X
return before + ' | ' + after;
Undo and redo, executed
A move is one undo step: undo() returns the card to its column and position,
redo() re-applies it, and canUndo()/canRedo() report the stacks, capped by
historyDepth. Run headless on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const board = createKanban(null, {
rows: [
{ id: 'a', status: 'todo', ord: 1, title: 'A' },
{ id: 'b', status: 'todo', ord: 2, title: 'B' },
],
rowKey: 'id',
columnProperty: 'status',
columns: [{ id: 'todo' }, { id: 'done' }],
orderProperty: 'ord',
historyDepth: 5, // keep five undo steps (default 100)
});
await board.move('a', 'done'); // one undo step
const moved = board.card('a').columnId; // done
const can = (board.canUndo() ? 'yes' : 'no') + '/' + (board.canRedo() ? 'yes' : 'no'); // yes/no
await board.undo(); // a returns to todo, its position restored
const undone = board.card('a').columnId; // todo
await board.redo(); // re-apply the move
const redone = board.card('a').columnId; // done
board.destroy();
return [moved, can, undone, redone].join(' | ');
A server-side board, executed
A board bound to the same pushdown source a grid takes: the column count and
points sum come from an engine GROUP BY, cards load one pageSize at a time
(loadMore fetches the next page), and a move writes through the source's update path.
Run headless on every build.
const { createKanban } = await import('../packages/modules/kanban/index.js');
const { createPushdownSource } = await import('../packages/core/src/index.js');
const rows = [
{ id: 1, status: 'todo', points: 1, title: 'A' },
{ id: 2, status: 'todo', points: 2, title: 'B' },
{ id: 3, status: 'todo', points: 3, title: 'C' },
{ id: 4, status: 'todo', points: 4, title: 'D' },
{ id: 5, status: 'todo', points: 5, title: 'E' },
{ id: 6, status: 'done', points: 6, title: 'F' },
{ id: 7, status: 'done', points: 7, title: 'G' },
];
// A mock pushdown adapter over rows: pages the fetch, groups the totals.
const adapter = {
capabilities: {
filter: true, operators: ['eq', 'and'], quick: true, range: true, total: true, group: true,
mutate: { update: true, returning: 'row' },
},
async execute(query) {
const tree = query.filters;
const eq = tree && Array.isArray(tree.conditions) ? tree.conditions.find((c) => c.op === 'eq') : tree;
const col = eq && eq.value;
const matched = rows.filter((r) => col === undefined || r.status === col);
const start = query.range ? query.range.start : 0;
const end = query.range ? Math.min(query.range.end, matched.length) : matched.length;
return { rows: matched.slice(start, end), total: matched.length };
},
async executeGroupedAggregates(query, groupBy, aggs) {
const by = new Map();
for (const r of rows) {
const keys = groupBy.map((g) => r[g]);
const k = JSON.stringify(keys);
if (!by.has(k)) by.set(k, { keys, rows: [] });
by.get(k).rows.push(r);
}
return [...by.values()].map(({ keys, rows: bucket }) => ({
keys,
level: groupBy.length,
values: Object.fromEntries(aggs.map((a) => [a.id,
a.fn === 'count' ? bucket.length : bucket.reduce((t, r) => t + r[a.col], 0)])),
}));
},
async mutate(op) {
const r = rows.find((x) => String(x.id) === op.key);
if (r) Object.assign(r, op.patch);
return { keys: [op.key], rows: [r] };
},
};
const source = createPushdownSource({ adapter, pageSize: 2, aggregates: { default: 'engine' }, edit: true });
const board = createKanban(null, {
source,
rowKey: 'id',
columnProperty: 'status',
columns: [{ id: 'todo', title: 'To do', wipLimit: 2 }, { id: 'done', title: 'Done' }],
pointsProperty: 'points',
card: { title: 'title' },
pageSize: 2,
transitionLog: [], // the source-supplied transition log enables flow analytics
});
await new Promise((r) => setTimeout(r, 20)); // the construction load settles
const count = board.count('todo'); // 5 - the engine GROUP BY, not the 2 loaded
const cards = board.column('todo').cards.length; // 2 - the first page
const over = board.column('todo').over; // true - 5 > the WIP limit of 2
const more = await board.loadMore('todo'); // true - another page was fetched
const cardsAfter = board.column('todo').cards.length; // 4
const reloaded = await board.reload(); // reload re-queries the engine totals
const isBoard = reloaded === board;
const flow = board.flowAvailable; // true - the transitionLog is supplied
board.destroy();
return [count, cards, over, more, cardsAfter, isBoard, flow].join(' | ');
Type reference
Generated from the type declarations, so it always matches the release. Each surface lists its properties, its methods and the events it raises as three tables; an option or value type lists its members once.
The kanban module
KanbanCard
A card model - one row as it appears on the board. `fields` holds the resolved display text for each mapped card field; `columnId` is the column the card sits in; `points` is the numeric points value (0 when absent). `swimlane`/`sprint`/`epic`/`order` are read from their configured properties and carried for the later cycles that render them.
| Property | Type | Description |
|---|---|---|
| key | unknown | The card's identity, from the board's `rowKey`. |
| row | KanbanRow | The row behind the card. On a grid-bound board this is the materialised object the board read off the grid, not the grid's own row. |
| columnId | string | null | Which column the card sits in - the stringified group value - or null when the row's group value is null or undefined, which leaves the card unplaced. |
| points | number | The card's estimate, from `pointsProperty`. Zero when there is no points property or the value is not a finite number. |
| hasPoints | boolean | Whether the points value was a finite number. A card without points contributes nothing to a column's sum. |
| order | unknown | The raw value of `orderProperty`, which ranks the card inside its column. Undefined when no order property is configured. (optional) |
| swimlane | unknown | The raw value of `swimlaneProperty`, the lane the card belongs to. Undefined when no swimlane property is configured. (optional) |
| sprint | unknown | The raw value of `sprintProperty`, used by the sprint selection. Undefined when no sprint property is configured. (optional) |
| epic | unknown | The raw value of `epicProperty`, used by the epic selection. Undefined when no epic property is configured. (optional) |
| fields | Record<string, string> | The card template's text, one entry per `card` mapping. A mapping that names a grid column is read through that column's own formatter, so it reads exactly as the cell does; anything missing is ''. |
| blocked | boolean | Whether the card is blocked: its `blockedProperty` is set, or it still waits on a `blockedByProperty` blocker that is not in a done column. The flag the blocked filter and the forward-move guard read. |
| explicitBlocked | boolean | Whether the card's own `blockedProperty` marks it blocked - the flag behind the blocked badge and the flow analytics' blocked interval, independent of any blockers. |
| blockedReason | string | The reason the card is blocked: the `blockedProperty` value itself when it is a non-empty string, '' when the flag was a bare boolean or no reason was written. |
| blockedBy | unknown[] | The keys of the cards blocking this one, from `blockedByProperty`. Empty when the card has no blockers. |
| ready | boolean | Whether the card is ready: it has blockers and every one of them is now in a done column, and it is not explicitly blocked itself. |
| assignees | { name: string; avatar?: string | null }[] | The assignees resolved from the `assignee` mapping: one entry per person, `{ name, avatar }`. Empty when the mapping is absent or the row has none; the names also feed the quick filter. (optional) |
| due | { text: string; ts: number | null; state: 'overdue' | 'soon' | null } | null | The due part: the display text (the grid's date format when bound), the parsed timestamp, and the `'overdue'`/`'soon'` state (null when the value is not a parseable date). Null when the `due` mapping is absent. (optional) |
| progressPct | number | null | The progress percent resolved from the `progress` mapping, clamped to 0..100; null when the value is not a number. (optional) |
| checklist | { done: number; total: number } | null | The checklist count resolved from the `checklist` mapping, or null when it is unmapped or neither side is numeric. (optional) |
KanbanColumn
A column with its cards and aggregates. `over` is true when `count` exceeds `wipLimit`.
| Property | Type | Description |
|---|---|---|
| id | string | The column's identity - a configured column's `id`, or the stringified group value a data-driven column came from. |
| title | string | The heading shown above the column. Defaults to the id, humanised. |
| color | string | null | The accent colour drawn as a dot in the header, as configured. Null when the column def set none. |
| wipLimit | number | null | The work-in-progress limit for this column, or null when it has none. Advisory unless `enforceWip` is on. |
| collapsed | boolean | Whether the column is currently collapsed to a strip - either configured so, or collapsed by the user. |
| cards | KanbanCard[] | The column's cards that survive the board's filters, in arrival order, re-sorted by `orderProperty` when one is configured. |
| count | number | How many cards the column holds - the length of `cards`, after filtering. |
| points | number | The sum of the column's card points; cards without a finite points value add nothing. |
| over | boolean | True when the column has a WIP limit and its count exceeds it. The header shows this whether or not `enforceWip` refuses the move. |
KanbanEditor
A card field editor handle returned by a host editor factory.
| Property | Type | Description |
|---|---|---|
| el | HTMLElement | The editor's element. The board appends it to the card being edited. |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| focus | () => void | - | => void | Called straight after the editor is appended, to put the caret where it belongs. Without it the board focuses `el` itself. (optional) |
| destroy | () => void | - | => void | Called when the edit commits or is cancelled, so the editor can release what it holds. Optional. (optional) |
KanbanRequiredField
A required-field rule: the row property a card must carry before it may enter a column, shown in the fill-in form when missing.
| Property | Type | Description |
|---|---|---|
| field | string | The row property path to fill. |
| label | string | The form field's human label (host-localised); defaults to the property name. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| editor | (ctx: { card: KanbanCard; field: string; value: string; commit: (value: unknown) => void; cancel: () => void }) => KanbanEditor | ctx: { card: KanbanCard; field: string; value: string; commit: (value: unknown) => void; cancel: () => void } | => KanbanEditor | A custom editor factory for the form field; defaults to a text input. Same shape as a card field editor. (optional) |
KanbanCardMap
The field-to-property mapping that drives the card template.
| Property | Type | Description |
|---|---|---|
| title | KanbanFieldMap | The card's headline text. (optional) |
| subtitle | KanbanFieldMap | A second line under the title. (optional) |
| labels | KanbanToneFieldMap | A comma-separated string rendered as one chip per label; blanks are skipped. (optional) |
| assignee | KanbanFieldMap | { field: string; max?: number } | One or several people, drawn as avatars: the row property holds a single {@link KanbanAssignee}, an array of them, or a comma-separated string of names. Up to `max` (default 2) are drawn, the rest as a `+N` overflow. (optional) |
| due | KanbanDueMap | The due date, with a due-soon/overdue state and icon. (optional) |
| cover | KanbanFieldMap | An image URL drawn as a cover band across the top of the card. (optional) |
| progress | KanbanFieldMap | A progress bar over the card. The value is a 0..1 fraction or a 0..100 percent (a trailing `%` is tolerated); a value that is not a number draws no bar. (optional) |
| checklist | KanbanChecklistMap | A checklist count on the card, rendered as `3/5`. See {@link KanbanChecklistMap}. (optional) |
| badges | KanbanToneFieldMap | A single badge chip at the end of the meta row. (optional) |
| accent | KanbanFieldMap | A colour for the card's left border. Any CSS colour; an empty value leaves the border plain. (optional) |
| [field: string] | KanbanFieldMap | undefined | Any other card slot the board's theme declares, mapped the same way as the named ones above. `undefined` is the honest value for a slot that is left unmapped. |
KanbanEvent
The payload of the three card pointer events - `card:click`, `card:dblclick` and `card:contextmenu`. The board's other events carry their own shapes; {@link KanbanEventPayloads} names one per event.
| Property | Type | Description |
|---|---|---|
| card | KanbanCard | The card the event is about. |
| column | string | null | The id of the column the card is in, or null when it is unplaced. |
| el | unknown | The card's element, for a host that wants to anchor a popover to it. Absent on a headless board. (optional) |
| originalEvent | unknown | The DOM event that caused this one, so a host can read modifier keys or call `preventDefault`. (optional) |
KanbanSlaConfig
Card-aging / SLA configuration. A card is measured against a `warn` and a `breach` threshold; the view puts an age chip on aged cards and a highlight on breached ones, and a rising crossing fires the `card:sla` event and the matching `onWarn`/`onBreach` callback (signature `(level, rows)`, the Data Router alert handler's). Thresholds resolve most-specific-first: lane → column → global. Reached at runtime as {@link Kanban#sla}.
| Property | Type | Description |
|---|---|---|
| warn | KanbanSlaThreshold | The global warn threshold. (optional) |
| breach | KanbanSlaThreshold | The global breach threshold. (optional) |
| columns | Record<string, KanbanSlaThreshold | { warn?: KanbanSlaThreshold; breach?: KanbanSlaThreshold }> | Per-column overrides by column id (each a threshold or a `{ warn, breach }` pair). (optional) |
| lanes | Record<string, KanbanSlaThreshold | { warn?: KanbanSlaThreshold; breach?: KanbanSlaThreshold }> | Per-swimlane overrides by lane id (each a threshold or a `{ warn, breach }` pair). (optional) |
| basis | SlaAgeingBasis 'column' | 'board' | Where the ageing clock starts: `'column'` (default) measures time in the card's current column; `'board'` measures age since the card arrived/was created. (optional) |
| enteredProperty | string | A row property holding the wall-clock time the card entered its column. (optional) |
| createdProperty | string | A row property holding the wall-clock time the card was created. (optional) |
| ignoreDone | boolean | Whether cards in a done column are exempt from ageing (default true). (optional) |
| useTransitionLog | boolean | Whether the flow transition log drives the ageing basis when present (default true). (optional) |
| showAge | SlaAgeChipVisibility 'always' | 'threshold' | Show the age chip on every aged card (`'always'`), or only on warn/breach (`'threshold'`, default). (optional) |
| tick | number | A re-check interval in ms so a card breaching by sitting still still lights up (0 = off). (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| now | () => number | - | => number | A wall-clock epoch clock, injectable for deterministic tests (default `Date.now`). (optional) |
| onWarn | (level: 'warn' | 'breach', rows: KanbanRow[]) => void | level: 'warn' | 'breach'rows: KanbanRow[] | => void | Called on a rising crossing to warn level, `(level, rows)` - the router alert handler's shape. (optional) |
| onBreach | (level: 'warn' | 'breach', rows: KanbanRow[]) => void | level: 'warn' | 'breach'rows: KanbanRow[] | => void | Called on a rising crossing to breach level, `(level, rows)` - the router alert handler's shape. (optional) |
KanbanSlaState
The computed SLA state of one card.
| Property | Type | Description |
|---|---|---|
| key | unknown | The card this ageing state belongs to. |
| columnId | string | null | The column the card was in when it was aged - the thresholds are looked up per column. |
| lane | unknown | The card's swimlane value, for a per-lane threshold. Undefined on a board without swimlanes. (optional) |
| start | number | null | The ageing-clock start epoch (ms), or null when no time source could be resolved. |
| ageMs | number | null | The card's age in ms, or null when unknown. |
| ageText | string | A short human age label (`2d`, `5h`, …), '' when unknown. |
| warnMs | number | null | The resolved warn threshold in ms, or null. |
| breachMs | number | null | The resolved breach threshold in ms, or null. |
| level | ThresholdLevel | null | The classified level, or null when the card cannot be aged. |
| breached | boolean | True when `level` is `'breach'`. |
KanbanSla
The card-aging / SLA monitor, reached as {@link Kanban#sla} when a `sla` config is supplied. Pure and DOM-free: it computes each card's ageing state from the board's card model and the flow transition log, and the view paints it.
| Property | Type | Description |
|---|---|---|
| config | object | The normalised SLA config (read-only). (read-only) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| sync | (): KanbanSla | - | KanbanSla | Recompute every card's SLA state without emitting anything. |
| evaluate | (opts?: { emit?: boolean }): KanbanSlaState[] | opts?: { emit?: boolean } | KanbanSlaState[] | Recompute and fire `card:sla`/`onWarn`/`onBreach` on each rising crossing. |
| start | (): KanbanSla | - | KanbanSla | Establish the baseline, notify on the current state, and start the optional tick. |
| stateFor | (cardOrKey: KanbanCard | unknown): KanbanSlaState | null | cardOrKey: KanbanCard | unknown | KanbanSlaState | null | The SLA state of one card (by card model or key), or null when unknown. |
| states | (): KanbanSlaState[] | - | KanbanSlaState[] | Every card's current SLA state. |
| breaches | (): KanbanSlaState[] | - | KanbanSlaState[] | The cards currently at breach level. |
| warnings | (): KanbanSlaState[] | - | KanbanSlaState[] | The cards currently at warn level (not yet breached). |
| destroy | (): void | - | void | Stop the tick and drop the board subscriptions. |
KanbanFlowDurationStats
Distribution stats over a set of durations, in milliseconds - the shape {@link KanbanFlow.cycleTime} and {@link KanbanFlow.leadTime} return. `null` throughout when there is nothing to summarise (`count` is then 0).
| Property | Type | Description |
|---|---|---|
| count | number | How many durations were summarised. |
| mean | number | null | The mean duration. |
| median | number | null | The median duration; identical to `p50`. |
| p50 | number | null | The 50th percentile (R-7). |
| p85 | number | null | The 85th percentile (R-7). |
| p95 | number | null | The 95th percentile (R-7). |
| min | number | null | The shortest duration. |
| max | number | null | The longest duration. |
| stdev | number | null | The sample standard deviation. |
| total | number | The sum of every duration. |
KanbanFlowCfd
The cumulative-flow series {@link KanbanFlow.cfd} returns: at each bucket boundary, how many cards sat in each column, plus the WIP and done totals.
| Property | Type | Description |
|---|---|---|
| bucket | number | The bucket size actually used, in milliseconds. |
| columns | string[] | The board's columns, in display order. |
| points | { /** The bucket boundary's timestamp (ms epoch). */ at: number; /** Card count per column id at this moment. */ counts: Record<string, number>; /** Cards in a WIP column at this moment. */ wip: number; /** Cards in a done column at this moment. */ done: number; /** Cards in any column at this moment. */ total: number; }[] | One point per bucket boundary. |
KanbanFlow
Flow analytics over a board's transition log: how long a card takes from start to finish (cycle time), from arrival to finish (lead time), and how the shape of the board has changed over time (the cumulative-flow diagram). Reached as {@link Kanban#flow}.
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| cycleTime | (): KanbanFlowDurationStats | - | KanbanFlowDurationStats | Active-work-time summary: first-done minus first-start, over cards that finished and passed through a start column. |
| leadTime | (): KanbanFlowDurationStats | - | KanbanFlowDurationStats | Arrival-to-done-time summary: first-done minus arrival, over cards that finished. |
| cfd | (opts?: { bucket?: string | number; from?: number; to?: number }): KanbanFlowCfd | opts?: { bucket?: string | number; from?: number; to?: number } | KanbanFlowCfd | The cumulative-flow series a stacked-area chart draws - one band per column, growing over time. |
| blockedTime | (): KanbanFlowDurationStats | - | KanbanFlowDurationStats | The blocked-time summary: how long each card has spent blocked, over cards with any transition. See {@link KanbanFlowDurationStats}. |
| flowEfficiency | (): KanbanFlowDurationStats | - | KanbanFlowDurationStats | The flow-efficiency summary: active time / cycle time per card, as 0..1 fractions over finished cards. See {@link KanbanFlowDurationStats}. |
| blockedTimeByColumn | (): { column: string; blockedMs: number; flowEfficiency: number | null }[] | - | { column: string; blockedMs: number; flowEfficiency: number | null }[] | Blocked time and flow efficiency, aggregated per column. Each card's blocked time is charged to the column it now sits in. |
KanbanConfig
Kanban configuration. Every structural property is named here so the same board maps any customer schema (a status field, `points`, `sprint`, `epic`, a swimlane property) without code change. Theme: no `theme` of its own - it inherits. Its colours are the grid's `--lattice-*` tokens, so `data-theme="dark"` on any ancestor (a layout's `theme: 'dark'`, or `<html>`) darkens it with everything else, and the nearest `data-theme` wins. Load the grid stylesheet, which carries the tokens.
| Property | Type | Description |
|---|---|---|
| rows | KanbanRow[] | The source rows, one per card. Use this or `grid`, not both - `rows` wins. (optional) |
| grid | unknown | A Lattice grid to bind to instead of `rows`: the board reads the grid's displayed rows through its own value pipeline and writes moves back through it. On a bound board `rows.apply` and `setRows` are ignored with a warning. (optional) |
| source | SourceConfig | A pushdown source the board binds to instead of `rows` or `grid`: the result of `createPushdownSource`. The board asks the source's `fetch` for each column's cards one page at a time (loading the next as the column is scrolled), its `aggregate` for each column's count and points sum as an engine GROUP BY, and its `edit.commit` to persist a move. A `source` wins over `rows` and `grid`; use only one. (optional) |
| pageSize | number | Cards fetched per column page on a source-bound board. Default 50. Only meaningful with `source`. (optional) |
| transitionLog | unknown[] | A persisted transition log the board hands to flow analytics on a source-bound board. Without it `flowAvailable` is false and flow features report themselves unavailable rather than deriving from the loaded page. Only meaningful with `source`. (optional) |
| rowKey | string | ((row: KanbanRow) => string | number) | Card identity (a field or fn, returning a string or number); default 'id'. Composite (`string[]`) keys are core-grid-only: a board keys its own card identity from one value, so there is nothing for an array to join into here. (optional) |
| columnProperty | string | The row property whose value puts a card in a column. Without it (and without `grid` or `columns`) the board warns and shows nothing. (optional) |
| columns | KanbanColumnDef[] | The columns to show, as ids or `{ id, title, color, wipLimit, collapsed, done }`. Configured columns appear even when empty; a group value outside them still gets a column of its own, appended, so no card is dropped. (optional) |
| columnOrder | string[] | Pins the leading column order by id; anything not named keeps its natural position after the pinned ones. A user reorder replaces this. (optional) |
| pointsProperty | string | The row property summed into each column header's points figure. (optional) |
| showPoints | boolean | Show the points sum in each column and lane header (default false). It needs `pointsProperty` too. (optional) |
| orderProperty | string | The row property that ranks cards within a column. Set it to make dropping a card at a position meaningful: a move then writes a new order value. Without it cards stay in arrival order. (optional) |
| swimlaneProperty | string | The row property that puts a card in a swimlane. Naming it does not switch on the lane layout - set `swimlanes` for that - but it does let a cross-lane drop write the lane back. (optional) |
| swimlanes | boolean | Render the 2D swimlane layout using `swimlaneProperty` (default false). (optional) |
| lanes | (string | KanbanLaneDef)[] | Explicit lane definitions; otherwise lanes come from the distinct swimlane values. (optional) |
| laneOrder | string[] | An explicit lane order by id (also set by a lane-header-drag reorder). (optional) |
| enforceWip | boolean | Enforce `wipLimit` as a hard gate: a move that would exceed it is refused (default false). (optional) |
| sprintProperty | string | The row property holding a card's sprint, which `setSprint` and the sprint selection filter on. (optional) |
| epicProperty | string | The row property holding a card's epic, which `setEpic` and the epic selection filter on. (optional) |
| blockedProperty | string | The row property that blocks a card: a boolean flag, or a reason string whose non-empty value is the reason shown on the blocked badge. Truthy means blocked; empty/false means clear. (optional) |
| blockedByProperty | string | The row property naming the keys of the cards that block this one - a comma-separated string or an array. Each key must be another card's `rowKey`; the card shows a "blocked by N" list of them, and is ready once they are all done. (optional) |
| blockedFilter | boolean | Initially filter to blocked cards only. The same flag `setBlockedFilter(on)` toggles at runtime. (optional) |
| sprints | (string | { id: unknown; title?: string })[] | A configurable sprint dataset: the canonical sprint list (order + titles), shown even when empty. (optional) |
| sprint | unknown | The initially selected sprint id, `Kanban.BACKLOG`, or undefined for all. (optional) |
| epic | unknown | The initially selected epic id, or undefined for all. (optional) |
| doneColumns | string[] | Column ids that count as "done" for a rollup's progress (also a column def's `done: true`). (optional) |
| transitions | Record<string, string[] | '*'> | Workflow rules: the moves each source column allows, as a list of destination column ids, or `'*'` for any. A source column absent from the map may move anywhere. A move against the map is refused - a drag shows the target as not droppable and a keyboard move skips it, while a programmatic move fails with a named `[lattice]` error. May also be declared per lane; a lane's entry overrides the board's per key. (optional) |
| require | Record<string, KanbanRequiredField[]> | Workflow rules: the row properties a card must carry before it may enter each destination column. A missing value opens the small fill-in form on a drag, and fails a programmatic move with a named `[lattice]` error. May also be declared per lane; a lane's entry overrides the board's per key. (optional) |
| children | KanbanChildren | Card pop-out: a nested child grid or board (master-detail by composition). (optional) |
| virtualize | boolean | { rowHeight?: number; overscan?: number; threshold?: number; viewport?: number } | Card virtualization for tall columns: true, or `{ rowHeight, overscan, threshold, viewport }`. (optional) |
| sla | KanbanSlaConfig | Card aging / SLA highlighting: warn/breach thresholds (globally, per column and/or per lane) that age each card and fire `card:sla` on a rising crossing. Opt-in; reached at runtime as {@link Kanban#sla}. See {@link KanbanSlaConfig}. (optional) |
| state | object | A saved board state (from `getState`) to restore on construction. (optional) |
| addCard | boolean | Show a per-column add-card affordance. (optional) |
| quickFilter | string | Quick-filter text matched case-insensitively across card fields. (optional) |
| card | KanbanCardMap | Maps the card template's slots - title, subtitle, labels, assignee, due, cover, progress, badges, accent - to row properties, and opts a slot into inline edit. Any other name is carried as an extra field on `KanbanCard.fields`. (optional) |
| readonly | KanbanReadonly | Blocks edits and moves: `true` for the whole board, or a map singling out columns and cards by id and key. Default false. (optional) |
| historyDepth | number | How many undo steps the board keeps: every move, add and inline edit is one step, a multi-card drag included. Default 100; 0 turns undo off. A grid-bound board ignores this and shares the bound grid's history, so one Ctrl+Z order covers both views. (optional) |
| ariaLabel | string | The board's accessible name. Defaults to `Board`. (optional) |
| emptyText | string | The placeholder shown in a column with no cards. Empty by default, so the module ships no English of its own. (optional) |
| selectable | boolean | Whether card selection is enabled (default true). (optional) |
| density | KanbanDensity 'compact' | 'comfortable' | The card density: `'compact'` tightens each card's padding and drops the subtitle; `'comfortable'` (the default) keeps the roomier card. (optional) |
| columnWidth | KanbanColumnWidth number | 'fill' | 'auto' | How wide each column is. A number fixes the width in pixels; `'fill'` (the default) shares the board width equally between the columns, never below `minColumnWidth` - so a board whose columns fit fills its host, and one whose columns do not fit keeps them at the minimum and scrolls; `'auto'` keeps the natural fixed width from the `--lattice-board-column-width` token (default 280px). The CSS token, when set, still overrides the option. Collapsed columns stay narrow strips and the rest take the space; the fill follows host resizes. (optional) |
| minColumnWidth | number | The pixel floor `'fill'` never drops a column below (default 240). (optional) |
| labels | Record<string, string> | Host-localised words for the move announcements (grabbed/moved/dropped/reverted/cancelled). (optional) |
| contextMenu | KanbanMenuItem[] | ((card: KanbanCard, selected: KanbanCard[]) => KanbanMenuItem[]) | A per-card context menu: items, or `fn(card, selectedCards)` returning items. Suppresses `card:contextmenu`. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| cardRenderer | (card: KanbanCard, ctx: { column: KanbanColumn; readonly: boolean; el: HTMLElement; doc: Document }) => string | Node | void | card: KanbanCardctx: { column: KanbanColumn; readonly: boolean; el: HTMLElement; doc: Document } | => string | Node | void | A custom card template: return an HTML string or a DOM node to own the whole card body. (optional) |
| onCardEdit | (event: { card: KanbanCard; key: unknown; field: string; fieldPath: string; value: unknown }) => boolean | void | Promise<boolean | void> | event: { card: KanbanCard; key: unknown; field: string; fieldPath: string; value: unknown } | => boolean | void | Promise<boolean | void> | Persist a standalone inline edit; return false or a rejected promise to revert. (optional) |
| onAddCard | (columnId: string) => KanbanRow | Promise<KanbanRow> | void | columnId: string | => KanbanRow | Promise<KanbanRow> | void | Create a card for a column on add-card; return the row to create (with its key), a Promise of that row, or nothing to auto-generate. A rejected Promise creates no card and leaves the board unchanged. (optional) |
| filter | (row: KanbanRow, card: KanbanCard) => boolean | row: KanbanRowcard: KanbanCard | => boolean | A predicate filter over cards; only matching cards are shown. (optional) |
| onBeforeMove | (card: KanbanCard, from: string | null, to: string, index: number | null) => boolean | Promise<boolean> | card: KanbanCardfrom: string | nullto: stringindex: number | null | => boolean | Promise<boolean> | Veto/confirm a move before any write. Return `false` (or a promise of it) to refuse; `from`/`to` are column ids, `index` the target position. (optional) |
| onCardMove | (event: KanbanMoveEvent) => boolean | void | Promise<boolean | void> | event: KanbanMoveEvent | => boolean | void | Promise<boolean | void> | Persist a move on a standalone (non-grid) board. Return `false` or a rejected promise to revert the optimistic move. On a grid-bound board the grid's write-back pipeline persists instead and this is not called. (optional) |
| onCardClick | (event: KanbanEvent) => void | event: KanbanEvent | => void | Called when a card is clicked, with the same payload as the `card:click` event. Both fire: this does not replace a registered handler. (optional) |
| onCardDblClick | (event: KanbanEvent) => void | event: KanbanEvent | => void | Called when a card is double-clicked, alongside the `card:dblclick` event. (optional) |
| onCardContextMenu | (event: KanbanEvent) => void | event: KanbanEvent | => void | Called on a card's context-menu gesture, alongside the `card:contextmenu` event. A configured `contextMenu` suppresses both. (optional) |
KanbanChildren
| Property | Type | Description |
|---|---|---|
| property | string | Parent-id property linking child rows to a card within the same dataset. (optional) |
| present | Presentation 'drawer' | 'modal' | 'inline' | Where the pop-out appears (default `drawer`). (optional) |
| asBoard | boolean | Make the child a nested board (recursive) instead of a grid. (optional) |
| gridOptions | object | ((card: KanbanCard) => object) | Options for the child grid/board - an object or `fn(card)`. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| load | (card: KanbanCard) => KanbanRow[] | Promise<KanbanRow[]> | card: KanbanCard | => KanbanRow[] | Promise<KanbanRow[]> | Per-card child rows, sync or async - an alternative (or addition) to `property`. (optional) |
| hasChildren | (card: KanbanCard) => boolean | card: KanbanCard | => boolean | Whether a card can be expanded, overriding the property/load inference. (optional) |
| factory | (container: HTMLElement, options: object) => { destroy?: () => void } | container: HTMLElementoptions: object | => { destroy?: () => void } | The grid factory (a `createGrid`) that builds the child grid. (optional) |
| render | (container: HTMLElement, ctx: { card: KanbanCard; rows: KanbanRow[]; board: Kanban; depth: number }) => (void | (() => void)) | container: HTMLElementctx: { card: KanbanCard; rows: KanbanRow[]; board: Kanban; depth: number } | => (void | (() => void)) | Fully custom child render; returns a cleanup function. (optional) |
| title | (card: KanbanCard) => string | card: KanbanCard | => string | The pop-out title (default the card title). (optional) |
KanbanMenuItem
One context-menu item. `action` receives the card, the selected cards, and the board.
| Property | Type | Description |
|---|---|---|
| label | string | The item's text in the menu. |
| disabled | boolean | Greys the item out and ignores clicks on it. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| action | (ctx: { card: KanbanCard; cards: KanbanCard[]; board: Kanban }) => void | ctx: { card: KanbanCard; cards: KanbanCard[]; board: Kanban } | => void | Runs when the item is chosen, with the clicked card, the selected cards (the clicked one when nothing is selected) and the board. Not called on a disabled item. (optional) |
KanbanMoveEvent
The payload of a `card:move` (and `card:reverted`) event.
| Property | Type | Description |
|---|---|---|
| keys | unknown[] | The keys of the cards that actually moved - the ones no `beforeMove` veto refused. |
| cards | KanbanCard[] | The moved cards, in the same order as `keys`. |
| from | (string | null)[] | Where each card came from, one column id (or null for unplaced) per moved card. |
| to | string | The column the cards landed in. |
| index | number | null | The position asked for within the target column, or null for the end. |
| orders | number[] | null | The new order values written to `orderProperty`, one per moved card - computed to sit between the neighbours at the drop point. Null when no order property is configured. |
| origin | EventOrigin 'api' | 'user' | 'init' | 'ai' | Where the move came from: `'user'` for a drag or keyboard move, `'ai'` for an approved AI proposal, `'api'` for `board.move`. |
| lane | unknown | The swimlane the cards were moved to, when the gesture named one and a swimlane property is configured. (optional) |
KanbanHistoryStep
The outcome of one board undo/redo step.
| Property | Type | Description |
|---|---|---|
| applied | boolean | Whether the step actually applied; false is a vetoed step that left the stacks untouched. |
| label | string | The step's label: `'move'`, `'edit'` or `'add'`. |
KanbanHistoryEvent
`history`: the undo/redo stacks moved.
| Property | Type | Description |
|---|---|---|
| canUndo | boolean | Whether there is a step to undo. |
| canRedo | boolean | Whether there is a step to redo. |
| label | string | null | The label of the next undo step, or null when there is none. |
KanbanBeforeEvent
The members every cancellable board before-event carries.
| Property | Type | Description |
|---|---|---|
| type | string | The event's own name. |
| origin | EventOrigin 'api' | 'user' | 'init' | 'ai' | Where the action came from. |
| defaultPrevented | boolean | True once any handler has cancelled it. (read-only) |
| reason | string | null | The first reason given to `preventDefault`, or null. (read-only) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| preventDefault | (reason?: string): void | reason?: string | void | Cancel the pending action; the reason is surfaced on the matching `<action>:cancelled`. |
KanbanCardEditEvent
`card:edit` and `beforeEdit`: one field of one card.
| Property | Type | Description |
|---|---|---|
| card | KanbanCard | The card being edited. |
| key | unknown | That card's key. |
| field | string | The card-spec name of the field. |
| fieldPath | string | The row property the field writes to. |
| value | unknown | The value being written. |
| origin | EventOrigin 'api' | 'user' | 'init' | 'ai' | Where the edit came from. |
KanbanBeforeEditEvent
`beforeEdit`: an inline card edit is about to be written.
| Property | Type | Description |
|---|---|---|
| card | KanbanCard | The card being edited. |
| key | unknown | That card's key. |
| field | string | The card-spec name of the field. |
| fieldPath | string | The row property the field writes to. |
| value | unknown | The value that would be written. |
KanbanEditCancelledEvent
`edit:cancelled`: a `beforeEdit` handler refused the write.
| Property | Type | Description |
|---|---|---|
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
KanbanCardAddEvent
`card:add`: a card was appended to a column.
| Property | Type | Description |
|---|---|---|
| column | string | The column it was added to. |
| key | unknown | The new card's key - the grid's temporary key on a grid-bound board until the server confirms it. |
KanbanBeforeAddEvent
`beforeAdd`: a card is about to be appended.
| Property | Type | Description |
|---|---|---|
| column | string | The column it would be added to. |
| seed | Record<string, unknown> | The seed values the new row would be built from. |
KanbanAddCancelledEvent
`add:cancelled`: a `beforeAdd` handler refused the append.
| Property | Type | Description |
|---|---|---|
| column | string | The column the card was not added to. |
| seed | Record<string, unknown> | The seed values that were not written. |
| origin | EventOrigin 'api' | 'user' | 'init' | 'ai' | Where the append came from. |
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
KanbanBeforeMoveEvent
`beforeMove`: one card of a move is about to be applied; raised once per card.
| Property | Type | Description |
|---|---|---|
| card | KanbanCard | The card being moved. |
| key | unknown | That card's key. |
| from | string | null | The column it is in, or null when it is unplaced. |
| to | string | The column it would land in. |
| index | number | null | The position asked for within that column, or null for the end. |
KanbanMoveCancelledEvent
`move:cancelled`: a `beforeMove` handler refused one card of a move.
| Property | Type | Description |
|---|---|---|
| keys | unknown[] | The one key that did not move. |
| cards | KanbanCard[] | That one card. |
| from | (string | null)[] | The column it is still in. |
| to | string | The column it would have landed in. |
| index | number | null | The position that was asked for, or null. |
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
KanbanCardRevertedEvent
`card:reverted`: a move did not stick - the bound grid refused the write, an `onCardMove` callback returned false, or the server reverted the cell.
| Property | Type | Description |
|---|---|---|
| keys | unknown[] | The keys that went back. |
| cards | KanbanCard[] | Those cards. |
| from | (string | null)[] | Where each came from. |
| to | string | null | The column they are in again, or null when it is not known. |
| index | number | null | The position that had been asked for, or null. |
| orders | number[] | null | The order values that had been computed, or null. |
| origin | EventOrigin 'api' | 'user' | 'init' | 'ai' | Where the move came from, on the two board-side reverts. (optional) |
| reverted | boolean | True on the grid-side revert, which reports it explicitly. (optional) |
| reason | string | Why the grid reverted the cell, on the grid-side revert. (optional) |
KanbanCardConfirmedEvent
`card:confirmed`: the grid confirmed the column write behind an optimistic move.
| Property | Type | Description |
|---|---|---|
| keys | unknown[] | The key that was confirmed. |
| cards | KanbanCard[] | That card, or an empty array when it has since gone. |
| to | string | null | The column it is in, or null when the card has gone. |
KanbanSelectionEvent
`selection:changed`: the selected cards changed.
| Property | Type | Description |
|---|---|---|
| keys | unknown[] | Every selected card key, in selection order. |
KanbanColumnCollapseEvent
`column:collapse`: a column was collapsed or expanded.
| Property | Type | Description |
|---|---|---|
| column | string | The column that moved. |
| collapsed | boolean | True when it is now collapsed. |
KanbanBeforeColumnChangeEvent
`beforeColumnChange`: a column is about to be collapsed or expanded.
| Property | Type | Description |
|---|---|---|
| column | string | The column that would move. |
| collapsed | boolean | True when it would become collapsed. |
KanbanColumnChangeCancelledEvent
`columnChange:cancelled`: a `beforeColumnChange` handler refused it.
| Property | Type | Description |
|---|---|---|
| origin | EventOrigin 'api' | 'user' | 'init' | 'ai' | Where the change came from. |
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
KanbanSwimlaneCollapseEvent
`swimlane:collapse`: a swimlane was collapsed or expanded.
| Property | Type | Description |
|---|---|---|
| swimlane | string | The lane that moved. |
| collapsed | boolean | True when it is now collapsed. |
KanbanOrderEvent
`column:reorder` and `swimlane:reorder`: the order the board draws them in.
| Property | Type | Description |
|---|---|---|
| order | string[] | The ids in their new order, as the rebuilt model holds them. |
KanbanBeforeOrderEvent
`beforeColumnReorder` and `beforeLaneReorder`: an order is about to be applied.
| Property | Type | Description |
|---|---|---|
| order | string[] | The ids in the order that was asked for. |
KanbanOrderCancelledEvent
`columnReorder:cancelled` and `laneReorder:cancelled`: a handler refused the order.
| Property | Type | Description |
|---|---|---|
| origin | EventOrigin 'api' | 'user' | 'init' | 'ai' | Where the reorder came from. |
| reason | string | The reason given to `preventDefault`, or `'prevented'`. |
KanbanFilterChangedEvent
`filter:changed`: the quick filter or a named predicate changed.
| Property | Type | Description |
|---|---|---|
| quickFilter | string | The quick-filter text now in force, or undefined when there is none. (optional) |
| hasFilter | boolean | True when at least one named predicate is registered. |
| filters | string[] | The names of the registered predicates. |
KanbanSprintChangedEvent
`sprint:changed`: the shown sprint changed.
| Property | Type | Description |
|---|---|---|
| sprint | unknown | The sprint now shown: a sprint id, `board.BACKLOG`, or undefined for all of them. |
KanbanEpicChangedEvent
`epic:changed`: the shown epic changed.
| Property | Type | Description |
|---|---|---|
| epic | unknown | The epic now shown, or undefined for all of them. |
KanbanCardExpandEvent
`card:expand`: a card's children were opened.
| Property | Type | Description |
|---|---|---|
| card | KanbanCard | The card that was expanded. |
| rows | Record<string, unknown>[] | Its child rows, as the children loader returned them. |
| present | Presentation 'drawer' | 'modal' | 'inline' | How they are being presented. |
KanbanCardDrillEvent
`card:drill`: a card was expanded from inside an already-open detail.
| Property | Type | Description |
|---|---|---|
| card | KanbanCard | The card that was expanded. |
| rows | Record<string, unknown>[] | Its child rows. |
| depth | number | How many levels down this is; 1 is the first drill. |
KanbanDragStartEvent
`drag:start`: a card drag began.
| Property | Type | Description |
|---|---|---|
| keys | unknown[] | Every key the drag carries - the selection when the dragged card is in it. |
| card | KanbanCard | The card under the pointer. |
| originalEvent | unknown | The DOM `dragstart` event. |
KanbanDragEndEvent
`drag:end`: a card drag ended, whether or not it dropped on a column.
| Property | Type | Description |
|---|---|---|
| keys | unknown[] | The keys the drag carried. |
| originalEvent | unknown | The DOM `drop` event; absent when the browser ended the drag without one. (optional) |
KanbanSlaEvent
`card:sla`: a card crossed an ageing threshold.
| Property | Type | Description |
|---|---|---|
| key | unknown | The card's key. |
| card | KanbanCard | null | The card, or null when it is no longer on the board. |
| level | ThresholdLevel | null | The level it has just reached. |
| previous | ThresholdLevel | null | The level it was at before this crossing. |
| ageMs | number | null | Its age in ms, or null when unknown. |
| ageText | string | A short human age label (`2d`, `5h`, …). |
| columnId | string | null | The column it was aged in. |
| lane | unknown | Its swimlane value, on a board with swimlanes. (optional) |
| warnMs | number | null | The resolved warn threshold in ms, or null. |
| breachMs | number | null | The resolved breach threshold in ms, or null. |
| at | number | The epoch (ms) of the crossing. |
KanbanCardsLoadedEvent
The payload of the `cards:loaded` event: a page of cards has been fetched from the bound pushdown source for one column (and, in swimlane mode, one lane).
| Property | Type | Description |
|---|---|---|
| column | string | The column the page loaded into. |
| lane | string | The lane the page loaded into, present only in swimlane mode. (optional) |
| keys | unknown[] | The keys of the rows admitted by this page. |
| more | boolean | True when another page may still follow for this column (and lane). |
KanbanEventPayloads
What a handler receives, per board event.
| Property | Type | Description |
|---|---|---|
| card:click | KanbanEvent | The card, its column, its element and the DOM event. |
| card:dblclick | KanbanEvent | The card, its column, its element and the DOM event. |
| card:contextmenu | KanbanEvent | The card, its column, its element and the DOM event. |
| card:move | KanbanMoveEvent | Which cards moved, where from and to, and the order values written. |
| card:reverted | KanbanCardRevertedEvent | Which cards went back, and why when the grid said so. |
| card:confirmed | KanbanCardConfirmedEvent | The card the grid confirmed, and the column it is in. |
| selection:changed | KanbanSelectionEvent | Every selected card key. |
| column:collapse | KanbanColumnCollapseEvent | The column, and whether it is now collapsed. |
| card:add | KanbanCardAddEvent | The column added to, and the new card's key. |
| drag:start | KanbanDragStartEvent | The keys the drag carries, the card under the pointer, and the DOM event. |
| drag:end | KanbanDragEndEvent | The keys the drag carried, and the DOM event where there was one. |
| swimlane:collapse | KanbanSwimlaneCollapseEvent | The lane, and whether it is now collapsed. |
| swimlane:reorder | KanbanOrderEvent | The lane ids in their new order. |
| column:reorder | KanbanOrderEvent | The column ids in their new order. |
| filter:changed | KanbanFilterChangedEvent | The quick-filter text, and the named predicates in force. |
| sprint:changed | KanbanSprintChangedEvent | The sprint now shown. |
| epic:changed | KanbanEpicChangedEvent | The epic now shown. |
| card:expand | KanbanCardExpandEvent | The card, its child rows, and how they are presented. |
| card:drill | KanbanCardDrillEvent | The card, its child rows, and how deep the drill is. |
| card:edit | KanbanCardEditEvent | The card, the field, and the value written. |
| card:sla | KanbanSlaEvent | The crossing: the level reached, the one before it, and the age behind it. |
| cards:loaded | KanbanCardsLoadedEvent | The column (and lane) a page of cards loaded into, the admitted keys, and whether another page follows. |
| beforeMove | KanbanBeforeMoveEvent | The card about to move, with `preventDefault` to stop it. |
| beforeAdd | KanbanBeforeAddEvent | The column and seed about to be appended, with `preventDefault` to stop it. |
| beforeEdit | KanbanBeforeEditEvent | The field about to be written, with `preventDefault` to stop it. |
| beforeLaneReorder | KanbanBeforeOrderEvent | The lane order about to be applied, with `preventDefault` to stop it. |
| beforeColumnReorder | KanbanBeforeOrderEvent | The column order about to be applied, with `preventDefault` to stop it. |
| beforeColumnChange | KanbanBeforeColumnChangeEvent | The collapse about to be applied, with `preventDefault` to stop it. |
| move:cancelled | KanbanMoveCancelledEvent | The one card that did not move, and why. |
| add:cancelled | KanbanAddCancelledEvent | The card that was not added, and why. |
| edit:cancelled | KanbanEditCancelledEvent | The write that was not made, and why. |
| laneReorder:cancelled | KanbanOrderCancelledEvent | The lane order that was not applied, and why. |
| columnReorder:cancelled | KanbanOrderCancelledEvent | The column order that was not applied, and why. |
| columnChange:cancelled | KanbanColumnChangeCancelledEvent | The collapse that was not applied, and why. |
| history | KanbanHistoryEvent | The undo/redo stacks moved. |
KanbanRows
The keyed-diff consumer surface a board shares with a grid, so a Data Router routes to it directly.
| Property | Type | Description |
|---|---|---|
| count | number | How many rows the board holds, before filtering. (read-only) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| apply | (change: { add?: KanbanRow[]; update?: KanbanRow[]; remove?: unknown[] }): void | change: { add?: KanbanRow[]; update?: KanbanRow[]; remove?: unknown[] } | void | Apply a keyed diff: `add` replaces whatever row each key names, `update` merges its fields into the stored row (so a patch need only carry what changed), `remove` drops the keys. The board regroups and repaints, keeping scroll, focus, selection, collapse and any open pop-out. Ignored with a warning on a grid-bound board. |
| forEach | (fn: (row: KanbanRow, key: unknown) => void): void | fn: (row: KanbanRow, key: unknown) => void | void | Visit every row the board holds, with its key. |
KanbanFilters
Named card predicates, composed with AND, following the grid's `filters.where` convention. Several may be registered under different names at once; each can be replaced or removed without touching the others. `setFilter(fn)` is unchanged sugar for `where(DEFAULT, fn)` / `where(DEFAULT, null)`.
| Property | Type | Description |
|---|---|---|
| DEFAULT | string | The reserved name `board.setFilter` registers/removes under. (read-only) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| where | (): string[] | - | string[] | The registered names, in registration order. |
| where | (name: string, predicate: (row: KanbanRow, card: KanbanCard) => boolean): Kanban | name: stringpredicate: (row: KanbanRow, card: KanbanCard) => boolean | Kanban | Register or replace the predicate under `name`. |
| where | (name: string, predicate: null): Kanban | name: stringpredicate: null | Kanban | Remove whatever is registered under `name`; a no-op if nothing was. |
| engine | (name: string, condition: Condition | FilterGroup): Kanban | name: stringcondition: Condition | FilterGroup | Kanban | Register, replace, remove (`null`) or list (no arguments) a named **engine** filter: a pushable condition tree the board sends to a bound pushdown source as a WHERE predicate, so the engine narrows both the column counts and the loaded cards. The tree is never evaluated client-side; on a board with no `source` it is stored but does not narrow cards. |
| engine | (name: string, condition: null): Kanban | name: stringcondition: null | Kanban | Remove the engine filter registered under `name`; a no-op if nothing was. |
| reapply | (name?: string): boolean | name?: string | boolean | Re-run every named predicate (or one, by name) and re-render. |
Kanban
A board instance: a kanban view of grid rows as cards grouped into columns. It consumes data through the same keyed-diff `rows.apply` contract a grid exposes, so `dataRouter.attach(value, board)` drives it like any other viewer.
Properties
| Property | Type | Description |
|---|---|---|
| el | unknown | null | The element the board renders into, or null for a headless board. (read-only) |
| rowKey | string | ((row: KanbanRow) => string | number) | The resolved card identity; see `KanbanConfig.rowKey`. (read-only) |
| rows | KanbanRows | The keyed-diff consumer surface, the same shape a grid exposes - this is what makes a board a Data Router target. |
| sla | KanbanSla | The card-aging / SLA monitor, present only when a `sla` config was supplied. (optional) |
| flow | KanbanFlow | Flow analytics over the board's transition log: cycle time, lead time and the cumulative-flow diagram. Built automatically unless the config says `flow: false`. (optional) |
| flowAvailable | boolean | Whether flow analytics can answer on this board. False on a source-bound board with no source `transitionLog`, where the flow features report themselves unavailable rather than deriving from the loaded page. (read-only) |
| sourceBound | boolean | Whether the board is bound to a pushdown source and owns its own paged data path: counts/sums from an engine GROUP BY, cards fetched per column page, moves written through the source. (read-only) |
| columnWidth | KanbanColumnWidth number | 'fill' | 'auto' | The resolved column-width mode; see {@link KanbanConfig.columnWidth}. (read-only) |
| minColumnWidth | number | The resolved minimum column width in px; see {@link KanbanConfig.minColumnWidth}. (read-only) |
| filters | KanbanFilters | Named card predicates, composed with AND. See {@link KanbanFilters}. |
| BACKLOG | unknown | The sentinel `setSprint` value that selects the backlog (cards with no sprint). (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| columns | (): KanbanColumn[] | - | KanbanColumn[] | The current columns in display order, each with its cards and aggregates. |
| column | (id: string): KanbanColumn | undefined | id: string | KanbanColumn | undefined | One column by id, or undefined when the board has no such column. |
| count | (id: string): number | id: string | number | How many cards a column holds after filtering; 0 for an unknown column. |
| points | (id: string): number | id: string | number | A column's points sum; 0 for an unknown column. |
| cards | (): KanbanCard[] | - | KanbanCard[] | Every card on the board, unplaced ones included, in no particular column order. |
| card | (key: unknown): KanbanCard | undefined | key: unknown | KanbanCard | undefined | One card by key, or undefined when no card has that key (it may have been filtered out). |
| on | (name: KanbanEventName, fn: (event: KanbanEventPayloads[KanbanEventName]) => void): () => void | name: KanbanEventNamefn: (event: KanbanEventPayloads[KanbanEventName]) => void | () => void | Register an event handler; returns a function that removes it. An unrecognised event name is warned about once, because it names a binding that could never fire. What each event carries is {@link KanbanEventPayloads}; the handler is declared with the widest of them, so narrow on the name inside it. |
| off | (name: KanbanEventName, fn: (event: KanbanEventPayloads[KanbanEventName]) => void): void | name: KanbanEventNamefn: (event: KanbanEventPayloads[KanbanEventName]) => void | void | Remove a handler registered with `on`. |
| readonly | (scope?: { column?: string; card?: unknown }): boolean | scope?: { column?: string; card?: unknown } | boolean | Whether editing is blocked - for the whole board, or for the column or card named in the scope. |
| move | (keys: unknown | unknown[], toColumn: string, toIndex?: number | null, toLane?: string, opts?: { origin?: 'user' | 'ai' }): Promise<{ moved: unknown[]; reverted: boolean }> | keys: unknown | unknown[]toColumn: stringtoIndex?: number | nulltoLane?: stringopts?: { origin?: 'user' | 'ai' } | Promise<{ moved: unknown[]; reverted: boolean }> | Move one or more cards to a column (and, with an order property, to a position within it), through the workflow rules, the `onBeforeMove` veto and the grid's shipped write-back path. The single entry point behind drag-and-drop and keyboard move. A move the workflow rules refuse rejects with a named `[lattice]` error rather than silently skipping - the interactive paths pre-check the same rules, so only a programmatic call can raise it. |
| undo | (): Promise<KanbanHistoryStep | null> | - | Promise<KanbanHistoryStep | null> | Undo the last user change, re-running its inverse through the board's write path (so a `beforeMove`/`beforeEdit` veto still applies). A refused undo leaves the card and the history unchanged, with a catalogued warning. On a grid-bound board this delegates to the grid's history, so one undo order covers a grid edit and a board move. |
| redo | (): Promise<KanbanHistoryStep | null> | - | Promise<KanbanHistoryStep | null> | Redo the last undone change, re-running it through the board's write path. |
| canUndo | (): boolean | - | boolean | Whether there is a step to undo. |
| canRedo | (): boolean | - | boolean | Whether there is a step to redo. |
| selection | (): unknown[] | - | unknown[] | The selected card keys. |
| isSelected | (key: unknown): boolean | key: unknown | boolean | Whether a card is selected. |
| select | (keys: unknown | unknown[], mode?: 'set' | 'add' | 'toggle' | 'remove'): Kanban | keys: unknown | unknown[]mode?: 'set' | 'add' | 'toggle' | 'remove' | Kanban | Change the selection: `set` (replace), `add`, `toggle` or `remove`. |
| clearSelection | (): Kanban | - | Kanban | Clear the selection. |
| collapseColumn | (id: string, collapsed?: boolean): Kanban | id: stringcollapsed?: boolean | Kanban | Collapse, expand or toggle a column (emits `column:collapse`). |
| collapseLane | (id: string, collapsed?: boolean): Kanban | id: stringcollapsed?: boolean | Kanban | Collapse, expand or toggle a swimlane (emits `swimlane:collapse`). |
| reorderColumns | (order: string[]): Kanban | order: string[] | Kanban | Reorder the columns to the given id order (emits `column:reorder`). |
| moveColumn | (id: string, beforeId: string | null): Kanban | id: stringbeforeId: string | null | Kanban | Move one column before another (or to the end); emits `column:reorder`. |
| reorderLanes | (order: string[]): Kanban | order: string[] | Kanban | Reorder the swimlanes to the given id order (emits `swimlane:reorder`). |
| moveLane | (id: string, beforeId: string | null): Kanban | id: stringbeforeId: string | null | Kanban | Move one swimlane before another (or to the end); emits `swimlane:reorder`. |
| setFilter | (fn: ((row: KanbanRow, card: KanbanCard) => boolean) | null): Kanban | fn: ((row: KanbanRow, card: KanbanCard) => boolean) | null | Kanban | Set a predicate filter over cards, or clear it with null. Sugar for `filters.where(filters.DEFAULT, fn)`. |
| setQuickFilter | (text: string): Kanban | text: string | Kanban | Set the quick-filter text matched across card fields. Independent of every `filters.where` predicate. |
| facets | (property: string): { value: unknown; count: number }[] | property: string | { value: unknown; count: number }[] | Distinct values of a property with card counts - the raw material for a facet control. |
| setSprint | (sprint: unknown): Kanban | sprint: unknown | Kanban | Select the shown sprint (`BACKLOG` for the backlog, undefined for all); emits `sprint:changed`. |
| showBacklog | (): Kanban | - | Kanban | Show only the backlog (cards with no sprint). |
| setEpic | (epic: unknown): Kanban | epic: unknown | Kanban | Select the shown epic (undefined for all); emits `epic:changed`. |
| sprints | (): unknown[] | - | unknown[] | The distinct sprint values (the switcher's options); a configured `sprints` dataset pins the order. |
| sprintDefs | (): { id: unknown; title: string }[] | - | { id: unknown; title: string }[] | The sprint dataset as `{ id, title }` descriptors - the configured list plus any data-only sprint. |
| epics | (): unknown[] | - | unknown[] | The distinct epic values. |
| rollup | (property: string): { value: unknown; count: number; points: number; doneCount: number; donePoints: number; progress: number }[] | property: string | { value: unknown; count: number; points: number; doneCount: number; donePoints: number; progress: number }[] | Roll rows up by a property: per-bucket count, points, done and progress. |
| epicRollup | (): { value: unknown; count: number; points: number; doneCount: number; donePoints: number; progress: number }[] | - | { value: unknown; count: number; points: number; doneCount: number; donePoints: number; progress: number }[] | The epic rollup (empty when no epic property is configured). |
| canExpand | (card: KanbanCard): boolean | card: KanbanCard | boolean | Whether a card can be expanded to a child pop-out. |
| expand | (key: unknown): Promise<object | null> | key: unknown | Promise<object | null> | Open a card's children in a pop-out (drawer/modal/inline); emits `card:expand`/`card:drill`. |
| closeDetail | (): Kanban | - | Kanban | Close any open card pop-out. |
| isFieldEditable | (name: string): boolean | name: string | boolean | Whether a mapped card field is opted into inline edit and writable. |
| editCard | (key: unknown, name?: string): object | null | key: unknownname?: string | object | null | Start inline editing a card's field (the grid's own field editor when bound); no-op headless. |
| applyEdit | (key: unknown, name: string, value: unknown): Promise<boolean> | key: unknownname: stringvalue: unknown | Promise<boolean> | Commit an inline edit through the write-back path (grid.edit.setCells when bound); emits `card:edit`. |
| setBlocked | (key: unknown, blocked: boolean, reason?: string): Promise<boolean> | key: unknownblocked: booleanreason?: string | Promise<boolean> | Block or unblock a card: write its `blockedProperty` - a reason string or `true` to block, `false` to clear - and emit `card:edit` with `field: 'blocked'`, like any edit. A `beforeEdit` veto or an `onCardEdit` returning false refuses/reverts the write. |
| toggleBlocked | (key: unknown, reason?: string): Promise<boolean> | key: unknownreason?: string | Promise<boolean> | Toggle a card's blocked flag; sugar for {@link Kanban#setBlocked} reading the card's current state. |
| setBlockedFilter | (on: boolean): Kanban | on: boolean | Kanban | Show only blocked cards (`true`) or every card (`false`); emits `filter:changed`. |
| addCard | (columnId: string, seed?: KanbanRow): unknown | Promise<unknown> | columnId: stringseed?: KanbanRow | unknown | Promise<unknown> | Add a card to a column and open it in inline edit; emits `card:add`. Returns the new key directly, or a Promise of it when `onAddCard` returns a Promise or a `beforeAdd` handler defers; a rejected `onAddCard` Promise resolves this to `null` with no card added. |
| getState | (): object | - | object | Serialise the restorable state: collapsed columns/lanes, order, filter, sprint/epic, selection. |
| setState | (snapshot: object): Kanban | snapshot: object | Kanban | Restore a state snapshot from {@link Kanban#getState}. |
| setLoading | (loading: boolean): Kanban | loading: boolean | Kanban | Mark the board loading (renders a host-localised loading state). |
| setError | (message: string | null): Kanban | message: string | null | Kanban | Set (or clear with null) an error state, rendered as a host-supplied message. |
| setRows | (rows: KanbanRow[]): Kanban | rows: KanbanRow[] | Kanban | Replace the source rows and re-render, and make that array the board's source again so a later `refresh()` re-reads it. Ignored with a warning on a grid-bound board. |
| setColumns | (defs: KanbanColumnDef[]): Kanban | defs: KanbanColumnDef[] | Kanban | Replace the board's configured column set. Keeps card placement and interaction state (collapsed columns, column order, quick filter, selection) for every column id that survives; a dropped id is not specially handled - a card whose value has nowhere configured to go re-derives an ad hoc column rather than becoming `unplaced` (the same "never silently drop a card" rule an unconfigured value already gets). |
| refresh | (): Kanban | - | Kanban | Re-read the source and re-render: a bound grid's rows, or the configured array. Once rows have arrived through `rows.apply` nothing is re-read - the board regroups what it holds, so a routed feed is never thrown away. |
| reload | (): Promise<Kanban> | - | Promise<Kanban> | Reload a source-bound board from its pushdown source: drop the loaded pages and re-query the engine's column counts/sums, then fetch the first page of every column. On a client-side board this is `refresh()`. Called automatically at construction and after a search or filter change. |
| loadMore | (columnId: string, laneId?: string): Promise<boolean> | columnId: stringlaneId?: string | Promise<boolean> | Fetch the next page of cards for a column (and, in swimlane mode, a lane) on a source-bound board. The column scroll hook calls this as the user nears the bottom; a no-op resolving `false` when the board is not source-bound, the column is exhausted, or a page is in flight. |
| rebind | (grid: Grid): Kanban | grid: Grid | Kanban | Point this board at a replacement grid. The one call a page framework makes after its swap destroyed the grid the board was bound to: the board keeps its element, columns and interaction state, drops its subscriptions to the dead grid, subscribes to the new one and re-reads its rows. A no-op on a board not built over a grid, or already following the grid it is handed. |
| destroy | (): void | - | void | Empty the element, remove only the class the board added, and stop the flow and SLA monitors. A bound grid is left alone - the host owns it. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| card:click | A card was clicked, or Enter was pressed on a focused card. | KanbanEvent | no |
| card:dblclick | A card was double-clicked. | KanbanEvent | no |
| card:contextmenu | A context menu was requested on a card. | KanbanEvent | no |
| card:move | One or more cards were moved to a column, after every `beforeMove` gate passed and the write-back was applied. | KanbanMoveEvent | no |
| card:reverted | A move did not stick: the bound grid refused the write, an `onCardMove` callback returned false, or the server reverted the cell. | KanbanCardRevertedEvent | no |
| card:confirmed | The bound grid confirmed the column write behind an optimistic move. | KanbanCardConfirmedEvent | no |
| selection:changed | The set of selected cards changed. | KanbanSelectionEvent | no |
| column:collapse | A column was collapsed or expanded. | KanbanColumnCollapseEvent | no |
| card:add | A card was appended to a column. | KanbanCardAddEvent | no |
| drag:start | A card drag began. | KanbanDragStartEvent | no |
| drag:end | A card drag ended, whether or not it dropped on a column. | KanbanDragEndEvent | no |
| swimlane:collapse | A swimlane was collapsed or expanded. | KanbanSwimlaneCollapseEvent | no |
| swimlane:reorder | The swimlane order changed. | KanbanOrderEvent | no |
| column:reorder | The column order changed. | KanbanOrderEvent | no |
| filter:changed | The quick filter or a named card predicate changed. | KanbanFilterChangedEvent | no |
| sprint:changed | The shown sprint changed. | KanbanSprintChangedEvent | no |
| epic:changed | The shown epic changed. | KanbanEpicChangedEvent | no |
| card:expand | A card's children were opened. | KanbanCardExpandEvent | no |
| card:drill | A card was expanded from inside an already-open detail. | KanbanCardDrillEvent | no |
| card:edit | An inline card edit was written. | KanbanCardEditEvent | no |
| card:sla | A card crossed an ageing threshold - ok to warn, or ok/warn to breach. | KanbanSlaEvent | no |
| cards:loaded | A page of cards was fetched for a column (and lane), carrying the admitted keys and whether another page follows. | KanbanCardsLoadedEvent | no |
| beforeMove | One card of a move is about to be applied; raised once per card, and cancellable. | KanbanBeforeMoveEvent | yes |
| beforeAdd | A card is about to be appended; cancellable. | KanbanBeforeAddEvent | yes |
| beforeEdit | An inline card edit is about to be written; cancellable. | KanbanBeforeEditEvent | yes |
| beforeLaneReorder | A swimlane reorder is about to be applied; cancellable. | KanbanBeforeOrderEvent | yes |
| beforeColumnReorder | A column reorder is about to be applied; cancellable. | KanbanBeforeOrderEvent | yes |
| beforeColumnChange | A column collapse or expand is about to be applied; cancellable. | KanbanBeforeColumnChangeEvent | yes |
| move:cancelled | A `beforeMove` handler refused one card; raised once per refused card. | KanbanMoveCancelledEvent | no |
| add:cancelled | A `beforeAdd` handler refused the append. | KanbanAddCancelledEvent | no |
| edit:cancelled | A `beforeEdit` handler refused the write. | KanbanEditCancelledEvent | no |
| laneReorder:cancelled | A `beforeLaneReorder` handler refused the order. | KanbanOrderCancelledEvent | no |
| columnReorder:cancelled | A `beforeColumnReorder` handler refused the order. | KanbanOrderCancelledEvent | no |
| columnChange:cancelled | A `beforeColumnChange` handler refused the collapse or expand. | KanbanColumnChangeCancelledEvent | no |
| history | The undo/redo stacks moved. | KanbanHistoryEvent | no |