Mini Kanban
▶ Run live · View source on GitHub
A mini Kanban board. Three columns (To do / Doing / Done), drag any card across columns or within a column. Not a full Trello — three columns, ~10 cards, drag works, that’s it.
What to look at:
- One
each()per column. Three keyed lists, one parent each. The list reconciler owns the rows of each column independently — moving a card across columns is just a remove from one list and insert into another. delegateCapture('pointerdown', '.card', …)captures the drag start.pointerdowndoes bubble, but capture-phase fires first — so even if a child of.cardswallows the event, drag still initiates. The remainingpointermove/pointeruplisteners go onwindowbecause the cursor can leave the board.data-morph-skipon the dragging card. While dragged, the card is marked skip and given atransform: translate(...)style. The diff would otherwise see the moving transform as an attribute drift on every render and might fight identity. Skipping it makes the dragged card a stable, owned-by-the-drag-handler element until drop.- Optimistic store update on drop. The drag handler computes the target column + slot from
elementFromPoint, then callsboard.actions.move(cardId, toCol, toIdx). The store mutation triggers exactly one re-render — the dropped card lands in its new home.
// site/src/examples/complete/kanban/main.tsx (excerpt — full source on GitHub)import { defineStore, signal, mount, each, delegateCapture } from 'kerfjs';
const board = defineStore({ initial: () => ({ cols: { todo: [/* ... */], doing: [/* ... */], done: [/* ... */] }, }), actions: (set, get) => ({ move: (cardId, toCol, toIdx) => { /* find + remove + splice + set */ }, }),});
const drag = signal<{ id: string; dx: number; dy: number; w: number; h: number } | null>(null);
mount(root, () => ( <div class="board"> {COLS.map((col) => ( <section class="col" data-col={col} data-key={col}> <h2>{COL_TITLES[col]}</h2> <ul class="cards"> {each( board.state.value.cols[col], (card) => { const d = drag.value; const dragging = d?.id === card.id; return ( <li data-key={card.id} class={`card ${dragging ? 'dragging' : ''}`} data-card={card.id} style={dragging ? `transform:translate(${d!.dx}px,${d!.dy}px);…` : ''} {...(dragging ? { 'data-morph-skip': '' } : {})} > {card.text} </li> ); }, (card) => `${card.id}-${drag.value?.id === card.id ? 'drag' : 'rest'}`, )} </ul> </section> ))} </div>));
// Capture-phase pointerdown — fires first, even if a child handles the bubble.delegateCapture(root, 'pointerdown', '.card', (e, el) => { // …capture rect, set drag signal, attach window listeners for move/up});