3.2.5 Per-column lazy load-more + virtualization (finding #57) — consume cursor/count
Estimate: 20m · Depends on: 3.2.3, 3.2.2, 3.1.6
Make each column bounded at the UI, honouring the 3.1 paged projection (finding #57): the board never renders every card. 3.1 already returns, per column, a bounded first page + a total count + a cursor; this subtask consumes that.
Load-more. A column showing fewer than its total renders the "Load more" affordance from design/boards/ (button and/or scroll-to-load via an IntersectionObserver sentinel); triggering it calls GET /api/projects/[key]/board/columns/[columnId]/cards?cursor=&limit= (Story 3.1.6) and appends the returned page to that column's card list in the board state (the per-column-mutable shape from 3.2.2), advancing the cursor. Done/terminal columns are already windowed server-side — the UI just shows their count + load-more like any column.
Virtualization. A tall column virtualizes its card stack so the DOM row count stays bounded regardless of how many cards are loaded — reusing the windowing primitive Story 2.5.15 establishes for the issue tree (do NOT add a second virtualization library). The virtualized list must coexist with the 3.2.4 dnd-kit sortable (keep the dragged item + its neighbours mounted; this is a known dnd-kit + virtualization integration — follow the documented pattern).
Out of scope here: the move behaviour itself (3.2.4); swimlanes (3.3). This subtask is the count display, the load-more paging, and the windowing.
Acceptance criteria
- Each column shows its
totalcount and, when more cards exist, a "Load more" affordance (button and/or scroll-sentinel) that fetchesGET …/columns/[id]/cards?cursor=and appends the page, advancing the cursor — never a full-board refetch, never a load-all. - A tall column virtualizes (the rendered DOM row count stays bounded as more pages load), reusing the 2.5.15 windowing primitive (no second virtualization dep).
- Virtualization coexists with drag-drop (a card can be dragged within/out of a virtualized column without detaching mid-drag).
- Against
pnpm db:seed:large, a hundreds-of-cards column loads bounded pages and the DOM stays bounded (the finding-#57 proof). - Vitest covers the page-append logic (cursor advance, no duplicate cards, count vs loaded reconciliation).
Context refs
- Story 3.1.4 / 3.1.6 — the per-column
cursor/total+loadColumnCards/GET …/columns/[id]/cardsthis consumes - Story 2.5.15 — the virtualization/windowing primitive to reuse;
design/work-items/tree-scale.mock.html— the load-more affordance language design/boards/board.mock.html+design-notes.md(3.2.1) — the column count + load-more design; finding #57 — bounded, no load-all- dnd-kit + virtualization integration notes;
motir-core/CLAUDE.md— token rules