PageSourceSearch

https://rv-grid.com/assets/blog_virtualized-kanban-board-50000-tasks.md.BpMCCnRl.js

js rv-grid.com collected 2026-09-25 19:53:41 UTC 135,980 bytes, 247 lines download raw bytes

1import{n as e}from"./chunks/rolldown-runtime.hePW80VL.js";import{Et as t,Lt as n,St as r,it as i,n as a,nt as o,ot as s}from"./chunks/framework.CC4wkWh5.js";import"./chunks/assets.9Ly-vK68.js";var c=e({__pageData:()=>l,default:()=>f}),l=JSON.parse(`{"title":"Virtualized Kanban Board: How to Handle 50,000 Tasks in JavaScript","description":"Build a high-performance virtualized Kanban board for 50,000 JavaScript tasks with bounded DOM rendering, one data model, WIP rules, and server persistence.","frontmatter":{"title":"Virtualized Kanban Board: How to Handle 50,000 Tasks in JavaScript","description":"Build a high-performance virtualized Kanban board for 50,000 JavaScript tasks with bounded DOM rendering, one data model, WIP rules, and server persistence.","outline":"deep","date":"2026-08-09T00:00:00.000Z","author":"RevoGrid Team","category":"Kanban","tags":["Kanban","JavaScript","Performance","Virtualization","Enterprise","RevoGrid"],"image":"/img/kanban/workflow.webp","imageAlt":"Virtualized RevoGrid Kanban board with workflow columns and swimlanes","head":[["meta",{"name":"keywords","content":"high-performance Kanban board, virtualized Kanban, enterprise Kanban component, large Kanban board, JavaScript Kanban board, 50000 task Kanban, Kanban virtualization, Kanban performance, Kanban swimlanes, Kanban WIP limits, multi-card drag and drop, server persistence Kanban, RevoGrid Kanban"}],["meta",{"property":"og:title","content":"Virtualized Kanban Board: How to Handle 50,000 Tasks in JavaScript"}],["meta",{"property":"og:description","content":"Learn how bounded DOM rendering, a shared grid and Kanban data model, atomic multi-card movement, WIP rules, and compact persistence events keep a 50,000-task board usable."}],["meta",{"property":"og:type","content":"article"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["script",{"type":"application/ld+json"},"{\\n  \\"@context\\": \\"https://schema.org\\",\\n  \\"@type\\": \\"TechArticle\\",\\n  \\"headline\\": \\"Virtualized Kanban Board: How to Handle 50,000 Tasks in JavaScript\\",\\n  \\"description\\": \\"Build a high-performance virtualized Kanban board for 50,000 JavaScript tasks with bounded DOM rendering, one data model, WIP rules, and server persistence.\\",\\n  \\"author\\": {\\n    \\"@type\\": \\"Organization\\",\\n    \\"name\\": \\"RevoGrid\\"\\n  },\\n  \\"publisher\\": {\\n    \\"@type\\": \\"Organization\\",\\n    \\"name\\": \\"RevoGrid\\",\\n    \\"url\\": \\"https://rv-grid.com/\\"\\n  },\\n  \\"mainEntityOfPage\\": {\\n    \\"@type\\": \\"WebPage\\",\\n    \\"@id\\": \\"https://rv-grid.com/blog/virtualized-kanban-board-50000-tasks\\"\\n  },\\n  \\"datePublished\\": \\"2026-08-07\\",\\n  \\"dateModified\\": \\"2026-08-07\\",\\n  \\"proficiencyLevel\\": \\"Advanced\\"\\n}\\n"],["script",{"type":"application/ld+json"},"{\\n  \\"@context\\": \\"https://schema.org\\",\\n  \\"@type\\": \\"FAQPage\\",\\n  \\"mainEntity\\": [\\n    {\\n      \\"@type\\": \\"Question\\",\\n      \\"name\\": \\"Can a JavaScript Kanban board handle 50,000 tasks?\\",\\n      \\"acceptedA
1nswer\\": {\\n        \\"@type\\": \\"Answer\\",\\n        \\"text\\": \\"Yes, when the board virtualizes both workflow columns and card rows instead of mounting every task in the DOM. RevoGrid includes a live 50,000-task Kanban demo; practical capacity still depends on hardware, record size, card complexity, and surrounding application work.\\"\\n      }\\n    },\\n    {\\n      \\"@type\\": \\"Question\\",\\n      \\"name\\": \\"What is a virtualized Kanban board?\\",\\n      \\"acceptedAnswer\\": {\\n        \\"@type\\": \\"Answer\\",\\n        \\"text\\": \\"A virtualized Kanban board keeps the full logical dataset available while rendering only the workflow columns and card rows needed for the current viewport, plus a small buffer. This bounds DOM work as the dataset grows.\\"\\n      }\\n    },\\n    {\\n      \\"@type\\": \\"Question\\",\\n      \\"name\\": \\"Does RevoGrid use separate data for table and Kanban views?\\",\\n      \\"acceptedAnswer\\": {\\n        \\"@type\\": \\"Answer\\",\\n        \\"text\\": \\"No. Canonical task records remain in grid.source. Enabling grid.kanban projects those records into a board, and setting grid.kanban to false restores the latest source and table columns.\\"\\n      }\\n    },\\n    {\\n      \\"@type\\": \\"Question\\",\\n      \\"name\\": \\"How should a large Kanban board persist drag-and-drop changes?\\",\\n      \\"acceptedA
1nswer\\": {\\n        \\"@type\\": \\"Answer\\",\\n        \\"text\\": \\"Persist the committed changedCards batch from the kanbancardmove event instead of sending the complete dataset. The event also identifies any destination-bucket records changed by rank rebalancing.\\"\\n      }\\n    },\\n    {\\n      \\"@type\\": \\"Question\\",\\n      \\"name\\": \\"Do WIP limits work with multi-card movement?\\",\\n      \\"acceptedAnswer\\": {\\n        \\"@type\\": \\"Answer\\",\\n        \\"text\\": \\"Yes. RevoGrid validates the final proposed batch count. With blocking WIP behavior, one failed capacity or policy check rejects the complete multi-card move rather than partially moving the selection.\\"\\n      }\\n    }\\n  ]\\n}\\n"]]},"headers":[],"relativePath":"blog/virtualized-kanban-board-50000-tasks.md","filePath":"blog/virtualized-kanban-board-50000-tasks.md"}`),u={name:`blog/virtualized-kanban-board-50000-tasks.md`};function d(e,a,c,l,u,d){let f=t(`Mermaid`),p=t(`ClientOnly`);return r(),o(`div`,null,[a[0]||=i(`<h1 id="Virtualized-Kanban-Board-How-to-Handle-50-000-Tasks-in-JavaScript" tabindex="-1">Virtualized Kanban Board: How to Handle 50,000 Tasks in JavaScript <a class="header-anchor" href="#Virtualized-Kanban-Board-How-to-Handle-50-000-Tasks-in-JavaScript" aria-label="Permalink to “Virtualized Kanban Board: How to Handle 50,000 Tasks in JavaScript”">​</a></h1><p><img src="/img/kanban/workflow.webp" alt="Virtualized RevoGrid Kanban board with workflow columns and swimlanes" width="1440" height="900"></p><p>Most Kanban tutorials stop after three columns and twenty cards.</p><p>That is enough to explain drag and drop. It is not enough to build an operational product.</p><p>A <strong>large Kanban board</strong> has a different set of problems. It may need tens of thousands of tasks, several workflow stages, team swimlanes, multi-card operations, WIP policies, keyboard access, editing, table views, and reliable server persistence. A board that simply creates one DOM tree per column eventually spends more time mounting, measuring, styling, and reconciling cards than helping the user manage work.</p><p>The solution is not a faster drag-and-drop library. It is a different rendering and data architecture.</p><p><a href="/kanban">RevoGrid Kanban</a> treats the board as a virtualized projection of canonical application records. The same task objects can power a data grid and a Kanban view. Workflow columns become native RevoGrid columns, card positions become virtualized rows, and every projected cell contains at most one card.</p><p>The result is a <strong>high-performance Kanban board</strong> whose rendered DOM remains bounded even when the source contains 50,000 tasks.</p><p>You can see that architecture running in the <a href="/demo/kanban-performance">50K-task Kanban performance demo</a>: 50,000 source-backed records, ten workflow columns, and two collapsible swimlanes.</p><h2 id="The-architecture-in-one-diagram" tabindex="-1">The architecture in one diagram <a class="header-anchor" href="#The-architecture-in-one-diagram" aria-label="Permalink to “The architecture in one diagram”">​</a></h2>`,10),s(p,null,{default:n(()=>[s(f,{id:`mermaid-30`,graph:`flowchart%20LR%0A%20%20A%5BAPI%20or%20database%5D%20--%3E%20B%5BCanonical%20Task%20records%5D%0A%20%20B%20--%3E%20C%5Bgrid.source%5D%0A%20%20C%20--%3E%20D%5BTable%20view%5D%0A%20%20C%20--%3E%20E%5BKanban%20projection%5D%0A%20%20E%20--%3E%20F%5BVisible%20workflow%20columns%5D%0A%20%20E%20--%3E%20G%5BVisible%20card%20rows%5D%0A%20%20H%5BTransitions%2C%20WIP%2C%20permissions%5D%20--%3E%20E%0A%20%20E%20--%3E%20I%5BCommitted%20changedCards%20batch%5D%0A%20%20I%20--%3E%20A%0A`})]),_:1}),a[1]||=i(`<p>This separation is the key:</p><ul><li>your application owns task records, business rules, authorization, and persistence;</li><li>RevoGrid owns board projection, viewport rendering, focus, movement, and local provider updates;</li><li>the browser renders the visible board, not 50,000 complete card components.</li></ul><p>That is what makes a virtualized Kanban different from a visually similar Trello clone.</p>
1<h2 id="What-normally-breaks-when-a-Kanban-board-grows-" tabindex="-1">What normally breaks when a Kanban board grows? <a class="header-anchor" href="#What-normally-breaks-when-a-Kanban-board-grows-" aria-label="Permalink to “What normally breaks when a Kanban board grows?”">​</a></h2><p>A small board can hide architectural problems for a long time. Those problems become visible when the dataset, workflow, or interaction model expands.</p><table tabindex="0"><thead><tr><th>Failure mode</th><th>What users experience</th><th>Architectural cause</th></tr></thead><tbody><tr><td>DOM explosion</td><td>Slow startup, scroll jank, delayed clicks</td><td>Every card is mounted whether visible or not</td></tr><tr><td>Nested column scrollers</td><td>Awkward horizontal and vertical navigation</td><td>Each column owns an independent list and scroll state</td></tr><tr><td>Duplicated board state</td><td>Edits disappear or views drift apart</td><td>Table records and board cards are separate models</td></tr><tr><td>Full-dataset persistence</td><td>Large requests after every move</td><td>The app saves all tasks instead of changed records</td></tr><tr><td>Fragile reordering</td><td>Duplicate or unstable positions</td><td>Array indexes are treated as durable server order</td></tr><tr><td>Partial bulk moves</td><td>Some selected cards move and others fail</td><td>Validation runs card by card rather than on the batch</td></tr><tr><td>Expensive templates</td><td>Scrolling degrades as cards become richer</td><td>Rendering performs repeated calculations or side effects</td></tr><tr><td>Filter-based policy bugs</td><td>Hidden work bypasses WIP limits</td><td>Rules count only visible cards instead of canonical tasks</td></tr></tbody></table><p>A production <strong>enterprise Kanban component</strong> has to address all of these together. Virtualization alone is important, but virtualization without a stable data model and mutation contract still leaves the application difficult to maintain.</p><h2 id="_1-Keep-the-DOM-bounded" tabindex="-1">1. Keep the DOM bounded <a class="header-anchor" href="#_1-Keep-the-DOM-bounded" aria-label="Permalink to “1. Keep the DOM bounded”">​</a></h2><p>The browser does not need 50,000 task records represented by 50,000 mounted cards.</p><p>It needs enough DOM for the current viewport and a small rendering buffer. As the user scrolls, the virtualized view reuses the rendering surface for different logical rows and columns.</p><p>RevoGrid Kanban projects workflow data into a two-dimensional grid:</p><ul><li>workflow stages are RevoGrid columns;</li><li>cards are projected into rows inside a workflow column and optional swimlane;</li><li>one projected cell contains at most one card;</li><li>RevoGrid virtualizes both the horizontal column axis and vertical card-row axis.</li></ul><p>This avoids building a separate virtual list inside every Kanban column. Focus ownership, drop targets, row dimensions, and viewport state stay inside the same provider model.</p><h3 id="Why-one-card-per-projected-cell-matters" tabindex="-1">Why one card per projected cell matters <a class="header-anchor" href="#Why-one-card-per-projected-cell-matters" aria-label="Permalink to “Why one card per projected cell matters”">​</a></h3><p>Many boards begin as a horizontal flex container containing several independently scrolling vertical lists. That structure is intuitive, but every list then needs its own virtualization, measurement, drag targeting, overscan, and focus coordination.</p><p>RevoGrid uses the grid itself as the board layout engine. The card is not an arbitrary child inside a nested list. It has a stable projected cell, while the underlying task identity and rank remain independent of the rendered row index.</p><p>That gives the board one coordinated scrolling and rendering system instead of several competing ones.</p><h3 id="Reserve-a-predictable-card-row-height" tabindex="-1">Reserve a predictable card-row height <a class="header-anchor" href="#Reserve-a-predictable-card-row-height" aria-label="Permalink to “Reserve a predictable card-row height”">​</a></h3><p>Virtualization works best when card geometry is intentional. Set <code>cardRowHeight</code> to fit the complete card template:</p><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span>
1<pre class="shiki shiki-themes one-dark-pro github-light" style="--shiki-dark:#abb2bf;--shiki-light:#24292e;--shiki-dark-bg:#282c34;--shiki-light-bg:#fff;" tabindex="0" dir="ltr"><code><span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> kanban</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> {</span></span>
2<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  columns</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">workflowColumns</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
3<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  cardRowHeight</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">190</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
4<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  customization</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: {</span></span>
5<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    cardContent</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
6<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  },</span></span>
7<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">};</span></span></code></pre></div><p>Do not rely on a card overflowing into the next projected row. When a custom template gains another metadata band, increase the row height or simplify the content.</p><p>Uniform projected rows reduce measurement work and make drop targets deterministic.</p><h2 id="_2-Use-one-canonical-data-model-for-grid-and-Kanban" tabindex="-1">2. Use one canonical data model for grid and Kanban <a class="header-anchor" href="#_2-Use-one-canonical-data-model-for-grid-and-Kanban" aria-label="Permalink to “2. Use one canonical data model for grid and Kanban”">​</a></h2><p>A common product mistake is to create a table dataset and then transform it into a separate nested structure such as this:</p><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes one-dark-pro github-light" style="--shiki-dark:#abb2bf;--shiki-light:#24292e;--shiki-dark-bg:#282c34;--shiki-light-bg:#fff;" tabindex="0" dir="ltr"><code><span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">{</span></span>
8<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#6F42C1;">  backlog</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span><span style="--shiki-dark:#7F848E;--shiki-dark-font-style:italic;--shiki-light:#62687b;--shiki-light-font-style:inherit;">/* copied cards */</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">],</span></span>
9<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#6F42C1;">  development</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span><span style="--shiki-dark:#7F848E;--shiki-dark-font-style:italic;--shiki-light:#62687b;--shiki-light-font-style:inherit;">/* copied cards */</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">],</span></span>
10<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#6F42C1;">  done</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span><span style="--shiki-dark:#7F848E;--shiki-dark-font-style:italic;--shiki-light:#62687b;--shiki-light-font-style:inherit;">/* copied cards */</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">],</span></span>
11<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">}</span></span></code></pre></div><p>That looks convenient until a user edits a task in the table, moves it in the board, receives an update over WebSocket, or switches views. The application now has to synchronize two representations of the same records.<
11/p><p>RevoGrid avoids that duplication.</p><p>Canonical tasks stay in <code>grid.source</code>. The table schema stays in <code>grid.columns</code>. The Kanban configuration describes how those same records should be projected into workflow columns and swimlanes.</p><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes one-dark-pro github-light" style="--shiki-dark:#abb2bf;--shiki-light:#24292e;--shiki-dark-bg:#282c34;--shiki-light-bg:#fff;" tabindex="0" dir="ltr"><code><span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">import</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> type</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">ColumnRegular</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> } </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">from</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;@revolist/revogrid&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
12<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">import</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> {</span></span>
13<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  KanbanCardEditorDialogPlugin</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
14<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  KanbanPlugin</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
15<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">  type</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> KanbanConfig</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
16<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">} </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">from</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;@revolist/kanban&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
17<span class="line"></span>
18<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">type</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;"> WorkflowStatus</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span></span>
19<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;backlog&#39;</span></span>
20<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;triage&#39;</span></span>
21<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;ready&#39;</span></span>
22<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;design&#39;</span></span>
23<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;development&#39;</span></span>
24<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;testing&#39;</span></span>
25<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;review&#39;</span></span>
26<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;blocked&#39;</span></span>
27<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;release&#39;</span></span>
28<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;done&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
29<span class="line"></span>
30<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">type</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;"> Task</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> {</span></span>
31<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#c13617;">  id</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> string</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
32<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#c13617;">  title</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> string</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
33<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#c13617;">  description</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> string</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
34<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#c13617;">  status</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;"> WorkflowStatus</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
35<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#c13617;">  team</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;Product&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;"> |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;Platform&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
36<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#c13617;">  owner</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> string</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
37<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#c13617;">  priority</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;High&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;"> |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;Medium&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;"> |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;Low&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
38<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#c13617;">  points</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> number</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
39<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#c13617;">
39  order</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> number</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
40<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#c13617;">  tags</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> string</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">[];</span></span>
41<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#c13617;">  assignees</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> string</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">[];</span></span>
42<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">};</span></span>
43<span class="line"></span>
44<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> tableColumns</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;"> ColumnRegular</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">[] </span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">=</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> [</span></span>
45<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;id&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;ID&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">110</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
46<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;title&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Title&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">280</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
47<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;status&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Status&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">130</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
48<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;team&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Team&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">120</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
49<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;owner&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Owner&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">120</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
50<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;priority&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Priority&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">110</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
51<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;points&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Points&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">90</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
52<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">];</span></span>
53<span class="line"></span>
54<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> kanban</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;"> KanbanConfig</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&lt;</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;">Task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&gt; </span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">=</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> {</span></span>
55<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  columns</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span></span>
56<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;backlog&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Backlog&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">270</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
57<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;triage&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Triage&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">270</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
58<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;ready&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Ready&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">270</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
59<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;design&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Design&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">270</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
60<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;development&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Development&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">270</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
61<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;testing&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Testing&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">270</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
62<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;review&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Review&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">270</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
63<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;blocked&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Blocked&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-ligh
63t:#005CC5;">270</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
64<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;release&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Release&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">270</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
65<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;done&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Done&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">size</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">270</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
66<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  ],</span></span>
67<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  idField</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;id&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
68<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  columnField</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;status&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
69<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  orderField</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;order&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
70<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  swimlaneField</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;team&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
71<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  cardRowHeight</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">190</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
72<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  card</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: {</span></span>
73<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    titleField</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;title&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
74<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    descriptionField</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;description&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
75<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    priorityField</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;priority&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
76<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    tagsField</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;tags&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
77<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    assigneeField</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;assignees&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
78<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  },</span></span>
79<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">};
79</span></span>
80<span class="line"></span>
81<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> grid</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> document</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">querySelector</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&lt;</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;">HTMLRevoGridElement</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&gt;(</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;#work-grid&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">)</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">!</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
82<span class="line"></span>
83<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">grid</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">plugins</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> [</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">KanbanPlugin</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">KanbanCardEditorDialogPlugin</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">];</span></span>
84<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">grid</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">columns</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> tableColumns</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
85<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">grid</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">source</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> tasks</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
86<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">grid</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">kanbanCardEditorDialog</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> {};</span></span></code></pre></div><p>There is no copied <code>columns -&gt; cards[]</code> state tree. A task remains an ordinary record with stable fields.</p><h3 id="Switch-between-table-and-board-without-converting-data" tabindex="-1">Switch between table and board without converting data <a class="header-anchor" href="#Switch-between-table-and-board-without-converting-data" aria-label="Permalink to “Switch between table and board without converting data”">​</a></h3><p>The host application controls the view:</p><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes one-dark-pro github-light" style="--shiki-dark:#abb2bf;--shiki-light:#24292e;--shiki-dark-bg:#282c34;--shiki-light-bg:#fff;" tabindex="0" dir="ltr"><code><span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">function</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;"> setView</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">view</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;table&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;"> |</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;kanban&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) {</span></span>
87<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">  grid</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">kanban</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> view</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> ===</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;kanban&#39;</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> ?</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> kanban</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> :</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">
87 false</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
88<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">}</span></span></code></pre></div><p>Setting <code>grid.kanban</code> to a configuration activates the board. Setting it to <code>false</code> restores the latest canonical source and table columns.</p><p>That makes table-to-board switching a presentation decision rather than a data migration. It is particularly useful for operational products where users need both:</p><ul><li>a board for workflow movement;</li><li>a dense table for filtering, editing, comparison, and reporting.</li></ul><p>The <a href="/demo/planning">multi-view planning demo</a> applies the same idea across Grid, Kanban, Gantt, and Scheduler views.</p><h2 id="_3-Generate-50-000-stable-task-records" tabindex="-1">3. Generate 50,000 stable task records <a class="header-anchor" href="#_3-Generate-50-000-stable-task-records" aria-label="Permalink to “3. Generate 50,000 stable task records”">​</a></h2><p>The public performance demo uses ten workflow columns and two team swimlanes. Its generator gives every task a stable primitive ID and a finite numeric rank.</p><p>A simplified generator looks like this:</p><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes one-dark-pro github-light" style="--shiki-dark:#abb2bf;--shiki-light:#24292e;--shiki-dark-bg:#282c34;--shiki-light-bg:#fff;" tabindex="0" dir="ltr"><code><span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> statuses</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> [</span></span>
89<span class="line"><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">  &#39;backlog&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
90<span class="line"><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">  &#39;triage&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
91<span class="line"><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">  &#39;ready&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
92<span class="line"><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">  &#39;design&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
93<span class="line"><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">  &#39;development&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
94<span class="line"><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">  &#39;testing&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
95<span class="line"><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">  &#39;review&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
96<span class="line"><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">  &#39;blocked&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
97<span class="line"><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">  &#39;release&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
98<span class="line"><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">  &#39;done&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
99<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">] </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">as</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> const</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
100<span class="line"></span>
101<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> owners</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> [</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Maya&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Jon&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Ari&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Nora&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Theo&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Iris&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">] </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">as</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> const</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
102<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> priorities</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> [</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;High&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Medium&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Low&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">] </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">as</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> const</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
103<span class="line"></span>
104<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">function</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;"> createTasks</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">count</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;"> 50_000</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">)</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;"> Task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">[] {</span></span>
105<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">  return</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> Array</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">from</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">({ </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">length</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">count</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> }, (</span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">_</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">index</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">=&gt;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> {</span></span>
106<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">    const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> workflowIndex</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> index</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> %</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> statuses</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">length</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
107<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">    const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> cycle</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> Math</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">floor</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">index</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> /</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> statuses</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">length</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">);</span></span>
108<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">    const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> team</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;"> Task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">[</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;team&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">] </span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">=</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> cycle</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> %</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;"> 2</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> ===</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;"> 0</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> ?</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;Product&#39;</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> :</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;Platform&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
109<span class="line"></span>
110<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">    return</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> {</span></span>
111<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      id</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">\`KAN-</span><span style="--shiki-dark:#C678DD;--shiki-light:#032F62;">\${</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">index</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> +</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;"> 101</span><span style="--shiki-dark:#C678DD;--shiki-light:#032F62;">}</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">\`</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
112<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      title</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">\`Operational task </span><span style="--shiki-dark:#C678DD;--shiki-light:#032F62;">\${</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">index</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> +</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;"> 1</span><span style="--shiki-dark:#C678DD;--shiki-light:#032F62;">}</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">\`</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
113<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      description</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Source-backed work item for the virtualized Kanban board.&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
114<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      status</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">statuses</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">[</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">workflowIndex</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">],</span></span>
115<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      team</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
116<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      owner</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">owners</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">[(</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">index</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> +</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> cycle</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) </span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">%</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> owners</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">length</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">],</span></span>
117<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      priority</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">priorities</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">[</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">index</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> %</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> priorities</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">length</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">],</span></span>
118<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      points</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">2</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">3</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">5</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">8</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">][</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">index</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> %</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;"> 4</span><span style="--shiki-dark
118:#ABB2BF;--shiki-light:#24292E;">],</span></span>
119<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      order</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: (</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">Math</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">floor</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">cycle</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> /</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;"> 2</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) </span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">+</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;"> 1</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) </span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">*</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;"> 1_000</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
120<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      tags</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Kanban&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">team</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">],</span></span>
121<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      assignees</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">owners</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">[(</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">index</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> +</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> cycle</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) </span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">%</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> owners</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">length</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">]],</span></span>
122<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    };</span></span>
123<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  });</span></span>
124<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">}</span></span>
125<span class="line"></span>
126<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> tasks</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;"> createTasks</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">();</span></span>
127<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">grid</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">source</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> tasks</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span></code></pre></div><p>The important parts are not the synthetic titles. They are the identity and ordering rules:</p><ul><li>IDs are stable and unique;</li><li>workflow values match configured column props;</li><li>swimlane values are primitive and predictable;</li><li>order values are finite numbers with room between neighbors.</li></ul><p>Those properties let the board move records without treating a volatile rendered row index as durable task identity.</p><h2 id="_4-Use-fractional-ranking-instead-of-rewriting-whole-columns" tabindex="-1">4. Use fractional ranking instead of rewriting whole columns <a class="header-anchor" href="#_4-Use-fractional-ranking-instead-of-rewriting-whole-columns" aria-label="Permalink to “4. Use fractional ranking instead of rewriting whole columns”">​</a></h2><p>Dragging a card changes its logical position. The naive persistence strategy is to renumber every task in the destination column after every move.</p><p>That is expensive on a large board and creates unnecessary write conflicts.</p><p>RevoGrid uses numeric ranks. A normal move assigns a rank between neighboring cards. When the available numeric gap becomes unsafe, Kanban rebalances only the destination bucket and reports every affected record in the committed event.</p><p>This matters because a move can change more than the card the user touched. The persistence payload may include:</p><ul><li>the moved cards;</li><li>records whose status or swimlane changed;</li><li>destination records whose ranks were rebalanced.</li></ul><p>Your server should persist the complete <code>changedCards</code> batch atomically.</p><h2 id="_5-Split-the-board-with-virtualized-swimlanes" tabindex="-1">5. Split the board with virtualized swimlanes <a class="header-anchor" href="#_5-Split-the-board-with-virtualized-swimlanes" aria-label="Permalink to “5. Split the board with virtualized swimlanes”">​</a></h2><p>Swimlanes are often treated as decorative group headings. In an enterprise workflow they are part of the projection and policy model.</p><p>Use them to split every workflow stage by team, product area, release, class of service, assignee group, or another task field.</p><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span>
127<pre class="shiki shiki-themes one-dark-pro github-light" style="--shiki-dark:#abb2bf;--shiki-light:#24292e;--shiki-dark-bg:#282c34;--shiki-light-bg:#fff;" tabindex="0" dir="ltr"><code><span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> kanban</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;"> KanbanConfig</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&lt;</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;">Task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&gt; </span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">=</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> {</span></span>
128<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  ...</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">baseKanban</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
129<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  swimlaneField</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;team&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
130<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  swimlanes</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span></span>
131<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    {</span></span>
132<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      id</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Product&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
133<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      title</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Product team&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
134<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      collapsible</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">true</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
135<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    },</span></span>
136<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    {</span></span>
137<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      id</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Platform&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
138<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      title</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Platform team&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
139<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      collapsible</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">true</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
140<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      wipLimits</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: {</span></span>
141<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">        review</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">260</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
142<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">      },</span></span>
143<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    },</span></span>
144<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  ],</span></span>
145<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  swimlaneColumn</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: {</span></span>
146<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    title</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Teams&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
147<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    width</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">210</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
148<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    collapsedWidth</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">52</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
149<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    collapsible</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">true</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
150<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  },</span></span>
151<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">};
151</span></span></code></pre></div><p>A lane can be derived from first appearance or explicitly configured when title, order, height, collapse state, styling, or lane-specific WIP limits matter.</p><p>Collapsing a lane reduces its projected row area without creating a second hidden board model. The canonical tasks remain the same.</p><h2 id="_6-Move-several-cards-as-one-validated-operation" tabindex="-1">6. Move several cards as one validated operation <a class="header-anchor" href="#_6-Move-several-cards-as-one-validated-operation" aria-label="Permalink to “6. Move several cards as one validated operation”">​</a></h2><p>A large operational board needs more than one-card drag and drop.</p><p>RevoGrid supports toggle selection, range selection, and selections that span columns and swimlanes. Dragging a selected card moves the complete selection while preserving its prior board reading order.</p><p>Programmatic workflows use the same movement path:</p><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes one-dark-pro github-light" style="--shiki-dark:#abb2bf;--shiki-light:#24292e;--shiki-dark-bg:#282c34;--shiki-light-bg:#fff;" tabindex="0" dir="ltr"><code><span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> plugins</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> await</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> grid</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">getPlugins</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">();</span></span>
152<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> kanbanPlugin</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> plugins</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">find</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span></span>
153<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  (</span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">plugin</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">=&gt;</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> plugin</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> instanceof</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;"> KanbanPlugin</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
154<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">);</span></span>
155<span class="line"></span>
156<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">await</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> kanbanPlugin</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">?.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">moveCards</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span></span>
157<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  [</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;KAN-112&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;KAN-219&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;KAN-441&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">],</span></span>
158<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  {</span></span>
159<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    columnProp</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;review&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
160<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    swimlaneId</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Platform&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
161<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">    index</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">0</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
162<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  },</span></span>
163<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">reason</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;api&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
164<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">);</span></span></code></pre></div><p>Pointer, keyboard, context-menu, and API movement all pass through the same validation, ranking, provider update, history, and persistence contract.</p><p>That consistency is important. An automated bulk action should not be able to bypass the rules applied to a pointer drag.</p><h2 id="_7-Validate-WIP-and-transitions-against-the-final-batch" tabindex="-1">7. Validate WIP and transitions against the final batch <a class="header-anchor" href="#_7-Validate-WIP-and-transitions-against-the-final-batch" aria-label="Permalink to “7. Validate WIP and transitions against the final batch”">​</a></h2><p>WIP limits are not only a red number in the column header. They are part of the move transaction.</p><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span>
164<pre class="shiki shiki-themes one-dark-pro github-light" style="--shiki-dark:#abb2bf;--shiki-light:#24292e;--shiki-dark-bg:#282c34;--shiki-light-bg:#fff;" tabindex="0" dir="ltr"><code><span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> kanban</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;"> KanbanConfig</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&lt;</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;">Task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&gt; </span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">=</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> {</span></span>
165<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  ...</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">baseKanban</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
166<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  columns</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span></span>
167<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;backlog&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Backlog&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> },</span></span>
168<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    {</span></span>
169<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;development&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
170<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Development&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
171<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      wipLimit</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">40</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
172<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      allowedFrom</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;ready&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;development&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">],</span></span>
173<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    },</span></span>
174<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    {</span></span>
175<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;review&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
176<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Review&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
177<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      wipLimit</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">15</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
178<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      allowedFrom</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;development&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;review&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">],</span></span>
179<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    },</span></span>
180<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    { </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;done&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">name</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Done&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">allowedFrom</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;review&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;done&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">] },</span></span>
181<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  ],</span></span>
182<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  wipBehavior</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;block&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
183<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  rules</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: {</span></span>
184<span class="line"><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">    canDrag</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: (</span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">card</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">=&gt;</span></span>
185<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">      card</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">locked</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> ?</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;This task is locked by another user.&#39;</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> :</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;"> true</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
186<span class="line"><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">    canSelect</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: (</span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">card</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">=&gt;</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> !</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">card</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">archived</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
187<span class="line"><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">    canDrop</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: ({ </span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">cards</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">targetColumn</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> }) </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">=&gt;</span></span>
188<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">      cards</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">some</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">((</span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">card</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">=&gt;</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> card</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">requiresApproval</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) </span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">&amp;&amp;</span></span>
189<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">      targetColumn</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">prop</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> ===</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;done&#39;</span></span>
190<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">        ?</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;Approval is required before completion.&#39;</span></span>
191<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">        :</span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;"> true</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
192<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  },</span></span>
193<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">};
193</span></span></code></pre></div><p>RevoGrid supports two WIP behaviors:</p><ul><li><code>warn</code> permits the move and exposes the over-limit state;</li><li><code>block</code> rejects the complete proposed move.</li></ul><p>For a multi-card operation, validation uses the final batch count. It does not move cards one by one until the column becomes full. If one capacity, permission, or transition check fails, the whole move is rejected.</p><p>WIP counts use the canonical bucket total, not only cards currently visible after filtering. A user cannot hide tasks with a search term and then accidentally overfill the workflow stage.</p><h2 id="_8-Persist-changed-records-not-50-000-tasks" tabindex="-1">8. Persist changed records, not 50,000 tasks <a class="header-anchor" href="#_8-Persist-changed-records-not-50-000-tasks" aria-label="Permalink to “8. Persist changed records, not 50,000 tasks”">​</a></h2><p>Virtualized rendering solves the DOM problem. Efficient mutation events solve the network and database problem.</p><p>Listen to the cancelable proposal when local authorization or product policy may reject a move. Then persist the committed changed-card batch:</p><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes one-dark-pro github-light" style="--shiki-dark:#abb2bf;--shiki-light:#24292e;--shiki-dark-bg:#282c34;--shiki-light-bg:#fff;" tabindex="0" dir="ltr"><code><span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">import</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> {</span></span>
194<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  BEFORE_KANBAN_CARD_MOVE_EVENT</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
195<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  KANBAN_CARD_MOVE_EVENT</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
196<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">  type</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> KanbanBeforeCardMoveDetail</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
197<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">  type</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;"> KanbanCardMoveDetail</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
198<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">} </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">from</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;"> &#39;@revolist/kanban&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">;</span></span>
199<span class="line"></span>
200<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">grid</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">addEventListener</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span></span>
201<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;">  BEFORE_KANBAN_CARD_MOVE_EVENT</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
202<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  (</span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">event</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;"> CustomEvent</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&lt;</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;">KanbanBeforeCardMoveDetail</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&lt;</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;">Task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&gt;&gt;) </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">=&gt;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> {</span></span>
203<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">    if</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> (</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">!</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">permissions</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">canMove</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">event</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">detail</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">cardIds</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">event</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">detail</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">target</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">)) {</span></span>
204<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">      event</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">preventDefault</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">();</span></span>
205<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    }</span></span>
206<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  },</span></span>
207<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">);</span></span>
208<span class="line"></span>
209<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">grid</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">addEventListener</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span></span>
210<span class="line"><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;">  KANBAN_CARD_MOVE_EVENT</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
211<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">  async</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> (</span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">event</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">:</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;"> CustomEvent</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&lt;</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;">KanbanCardMoveDetail</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&lt;</span><span style="--shiki-dark:#E5C07B;--shiki-light:#6F42C1;">Task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">&gt;&gt;) </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">=&gt;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> {</span></span>
212<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">    const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> response</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> await</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;"> fetch</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;/api/kanban/cards/batch&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, {</span></span>
213<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      method</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;PATCH&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
214<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      headers</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: {</span></span>
215<span class="line"><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">        &#39;content-type&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;application/json&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
216<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">      },</span></span>
217<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">      body</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;">JSON</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">stringify</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">
217({</span></span>
218<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">        cards</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">event</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">detail</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">changedCards</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
219<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">        target</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">event</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">detail</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">target</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
220<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">        reason</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">event</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">detail</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">reason</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
221<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">        rebalancedCardIds</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">event</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">detail</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">rebalancedCardIds</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
222<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">      }),</span></span>
223<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    });</span></span>
224<span class="line"></span>
225<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">    if</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> (</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;">!</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">response</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">ok</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) {</span></span>
226<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">      await</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;"> reloadCanonicalTasks</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">();</span></span>
227<span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">      throw</span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;"> new</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;"> Error</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&#39;Kanban persistence failed; source was reconciled.&#39;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">);</span></span>
228<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    }</span></span>
229<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  },</span></span>
230<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">);</span></span></code></pre></div><p>The critical field is <code>event.detail.changedCards</code>. It gives the server the exact canonical records affected by movement and any local rank rebalance.</p><p>Do not send the full 50,000-record source after every drag.</p><h3 id="Keep-authorization-local-during-the-gesture" tabindex="-1">Keep authorization local during the gesture <a class="header-anchor" href="#Keep-authorization-local-during-the-gesture" aria-label="Permalink to “Keep authorization local during the gesture”">​</a></h3><p>The before-move decision is synchronous. A drag interaction should not pause while the browser asks the server whether the user may drop a card.</p><p>Resolve remote authorization into local policy state before movement begins. After a committed local move, persist the mutation asynchronously. If the server rejects because of a version conflict or a newer remote state, reload or reconcile the canonical records.<
230/p><p>For collaborative products, include your own version, ETag, or revision field in each task and let the batch endpoint enforce optimistic concurrency.</p><h3 id="A-suitable-server-contract" tabindex="-1">A suitable server contract <a class="header-anchor" href="#A-suitable-server-contract" aria-label="Permalink to “A suitable server contract”">​</a></h3><p>A practical endpoint can accept a compact transaction:</p><div class="language-json"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes one-dark-pro github-light" style="--shiki-dark:#abb2bf;--shiki-light:#24292e;--shiki-dark-bg:#282c34;--shiki-light-bg:#fff;" tabindex="0" dir="ltr"><code><span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">{</span></span>
231<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">  &quot;cards&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span></span>
232<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    {</span></span>
233<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">      &quot;id&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&quot;KAN-112&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
234<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">      &quot;status&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&quot;review&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
235<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">      &quot;team&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&quot;Platform&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
236<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">      &quot;order&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">1500</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
237<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">      &quot;version&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#D19A66;--shiki-light:#005CC5;">18</span></span>
238<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">    }</span></span>
239<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">  ],</span></span>
240<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">  &quot;rebalancedCardIds&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: [</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&quot;KAN-221&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">, </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&quot;KAN-305&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">],</span></span>
241<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#005CC5;">  &quot;clientMutationId&quot;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">&quot;f46b9fcb-8447-4fd1-b34e-c9115f478a77&quot;</span></span>
242<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">}</span></span></code></pre></div><p>The database transaction should validate versions and update all included records together. The response can return authoritative tasks, new versions, and any server-calculated fields.</p><h2 id="_9-Keep-custom-cards-cheap-to-render" tabindex="-1">9. Keep custom cards cheap to render <a class="header-anchor" href="#_9-Keep-custom-cards-cheap-to-render" aria-label="Permalink to “9. Keep custom cards cheap to render”">​</a></h2><p>Virtualization reduces the number of mounted cards. It does not make expensive card templates free.</p><p>A good card renderer should be a pure projection of task data. Avoid doing any of the following inside the template:</p><ul><li>network requests;</li><li>timers;</li><li>global event listeners;</li><li>repeated date parsing for values that can be normalized once;</li><li>expensive aggregation over the complete dataset;</li><li>creating large new configuration objects on every render;</li><li>measuring arbitrary content when a fixed row height will work.</li></ul><p>Put expensive derived fields on the task record or memoize them outside the renderer.</p><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span>
242<pre class="shiki shiki-themes one-dark-pro github-light" style="--shiki-dark:#abb2bf;--shiki-light:#24292e;--shiki-dark-bg:#282c34;--shiki-light-bg:#fff;" tabindex="0" dir="ltr"><code><span class="line"><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">const</span><span style="--shiki-dark:#E5C07B;--shiki-light:#005CC5;"> preparedTasks</span><span style="--shiki-dark:#56B6C2;--shiki-light:#c62739;"> =</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;"> rawTasks</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">map</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">((</span><span style="--shiki-dark:#E06C75;--shiki-dark-font-style:italic;--shiki-light:#c13617;--shiki-light-font-style:inherit;">task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">) </span><span style="--shiki-dark:#C678DD;--shiki-light:#c62739;">=&gt;</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;"> ({</span></span>
243<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#c62739;">  ...</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">,</span></span>
244<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  dueLabel</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">formatDueDate</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">dueDate</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">),</span></span>
245<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  isOverdue</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">isTaskOverdue</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">),</span></span>
246<span class="line"><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">  searchText</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">: </span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">\`</span><span style="--shiki-dark:#C678DD;--shiki-light:#032F62;">\${</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#032F62;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">id</span><span style="--shiki-dark:#C678DD;--shiki-light:#032F62;">}</span><span style="--shiki-dark:#C678DD;--shiki-light:#032F62;"> \${</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#032F62;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">title</span><span style="--shiki-dark:#C678DD;--shiki-light:#032F62;">}</span><span style="--shiki-dark:#C678DD;--shiki-light:#032F62;"> \${</span><span style="--shiki-dark:#E5C07B;--shiki-light:#24292E;">task</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#032F62;">.</span><span style="--shiki-dark:#E06C75;--shiki-light:#24292E;">owner</span><span style="--shiki-dark:#C678DD;--shiki-light:#032F62;">}</span><span style="--shiki-dark:#98C379;--shiki-light:#032F62;">\`</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">.</span><span style="--shiki-dark:#61AFEF;--shiki-light:#6F42C1;">toLowerCase</span><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">(),</span></span>
247<span class="line"><span style="--shiki-dark:#ABB2BF;--shiki-light:#24292E;">}));</span></span></code></pre></div><p>The card can then render <code>dueLabel</code> and <code>isOverdue</code> directly instead of recomputing them during scrolling.</p><h2 id="_10-Understand-what-virtualization-does—and-does-not—solve" tabindex="-1">10. Understand what virtualization does—and does not—solve <a class="header-anchor" href="#_10-Understand-what-virtualization-does—and-does-not—solve" aria-label="Permalink to “10. Understand what virtualization does—and does not—solve”">​</a></h2><p>Two concepts are often mixed together:</p><table tabindex="0"><thead><tr><th>
247Concern</th><th>What it controls</th></tr></thead><tbody><tr><td>UI virtualization</td><td>How many columns, rows, and cards are mounted in the DOM</td></tr><tr><td>Remote pagination or data virtualization</td><td>How many records are loaded into browser memory</td></tr></tbody></table><p>RevoGrid Kanban currently keeps the canonical Kanban dataset locally in <code>grid.source</code>. The 50,000-task demo proves that the board can project a large local collection while keeping rendered card DOM bounded.</p><p>That does not mean every conceivable dataset should be downloaded into the browser. If your workflow contains millions of records, first define the operational slice a person can reasonably act on: a workspace, team, date range, portfolio, queue, or server-side query. Then assign that result to <code>grid.source</code> and let UI virtualization control rendering inside the selected slice.</p><p>Use filtering to help users focus. Do not treat a client-side filter as a replacement for a remote loading strategy.</p><h2 id="RevoGrid-architecture-versus-a-basic-board-component" tabindex="-1">RevoGrid architecture versus a basic board component <a class="header-anchor" href="#RevoGrid-architecture-versus-a-basic-board-component" aria-label="Permalink to “RevoGrid architecture versus a basic board component”">​</a></h2><table tabindex="0"><thead><tr><th>Capability</th><th>Basic Kanban implementation</th><th>RevoGrid Kanban</th></tr></thead><tbody><tr><td>Large-board rendering</td><td>Mount every card or virtualize each list separately</td><td>Native horizontal and vertical grid virtualization</td></tr><tr><td>Card placement</td><td>Nested arrays by column</td><td>Projection from canonical <code>grid.source</code> rows</td></tr><tr><td>Table-to-board switch</td><td>Convert and synchronize separate models</td><td>Set <code>grid.kanban</code> to config or <code>false</code></td></tr><tr><td>Scroll ownership</td><td>Independent column scrollers</td><td>Coordinated RevoGrid viewport</td></tr><tr><td>Multi-card movement</td><td>Usually custom application logic</td><td>Toggle/range selection and atomic movement</td></tr><tr><td>WIP limits</td><td>Visual count or per-card rejection</td><td>Warn/block behavior against final batch count</td></tr><tr><td>Swimlanes</td><td>Additional nested lists</td><td>Explicit or derived projected lanes with collapse controls</td></tr><tr><td>Ordering</td><td>Array index or full-column renumbering</td><td>Stable numeric rank with destination-bucket rebalance</td></tr><tr><td>Persistence</td><td>Save whole board state</td><td>Persist exact <code>changedCards</code> batches</td></tr><tr><td>Interaction paths</td><td>Pointer drag only</td><td>Pointer, touch, keyboard, context menu, and API</td></tr><tr><td>Alternate product views</td><td>Separate component state</td><td>One record model for Grid, Kanban, and other views</td></tr></tbody></table><p>This is why RevoGrid is positioned as an <strong>enterprise Kanban component</strong>, not only a task-board widget.</p><h2 id="A-practical-performance-checklist" tabindex="-1">A practical performance checklist <a class="header-anchor" href="#A-practical-performance-checklist" aria-label="Permalink to “A practical performance checklist”">​</a></h2><p>Before shipping a large board, test the complete product path rather than only a synthetic scroll benchmark.</p><h3 id="Data-and-identity" tabindex="-1">Data and identity <a class="header-anchor" href="#Data-and-identity" aria-label="Permalink to “Data and identity”">​</a></h3><ul><li>Use stable primitive IDs.</li><li>Use finite numeric ranks.</li><li>Keep workflow and swimlane values predictable.</li><li>Detect duplicate IDs and invalid configuration during development.</li></ul><h3 id="Rendering" tabindex="-1">Rendering <a class="header-anchor" href="#Rendering" aria-label="Permalink to “Rendering”">​</a></h3><ul><li>Set a deliberate <code>cardRowHeight</code>.</li><li>Keep render hooks pure.</li><li>Precompute expensive derived fields.</li><li>Avoid deep reactive wrappers around a 50,000-item source when the framework does not need them.</li><li>Test production builds, not only development mode.</li></ul><h3 id="Interaction" tabindex="-1">Interaction <a class="header-anchor" href="#Interaction" aria-label="Permalink to “Interaction”">​</a></h3><ul><li>Scroll horizontally across all workflow columns.</li><li>Scroll vertically through large buckets and collapsed lanes.</li><li>Move one card, a range, and a cross-lane selection.</li><li>Verify pointer, touch, keyboard, context-menu, and API movement.</li><li>
247Check invalid drop feedback and order-preserving no-op drops.</li></ul><h3 id="Policy" tabindex="-1">Policy <a class="header-anchor" href="#Policy" aria-label="Permalink to “Policy”">​</a></h3><ul><li>Test WIP <code>warn</code> and <code>block</code> behavior.</li><li>Verify filtered cards still count toward WIP.</li><li>Test local permissions and transition restrictions.</li><li>Confirm one invalid card rejects an atomic multi-card move.</li></ul><h3 id="Persistence" tabindex="-1">Persistence <a class="header-anchor" href="#Persistence" aria-label="Permalink to “Persistence”">​</a></h3><ul><li>Inspect the committed <code>changedCards</code> payload.</li><li>Test destination-bucket rank rebalancing.</li><li>Persist the batch in one transaction.</li><li>Add optimistic concurrency for collaborative products.</li><li>Reconcile the canonical source after server rejection.</li></ul><h3 id="Accessibility" tabindex="-1">Accessibility <a class="header-anchor" href="#Accessibility" aria-label="Permalink to “Accessibility”">​</a></h3><ul><li>Move cards without a pointer.</li><li>Verify focus survives virtual scrolling.</li><li>Test at 200% and 400% zoom.</li><li>Check reduced-motion and high-contrast modes.</li><li>Test custom card controls so they do not accidentally initiate movement.</li></ul><h2 id="When-should-you-use-a-virtualized-Kanban-" tabindex="-1">When should you use a virtualized Kanban? <a class="header-anchor" href="#When-should-you-use-a-virtualized-Kanban-" aria-label="Permalink to “When should you use a virtualized Kanban?”">​</a></h2><p>A <strong>virtualized Kanban</strong> is valuable when the board is embedded in a data-heavy product rather than used as a small standalone checklist.</p><p>Typical use cases include:</p><ul><li>support and incident queues;</li><li>product delivery across many teams;</li><li>CRM and onboarding pipelines;</li><li>manufacturing and quality workflows;</li><li>content production and approval;</li><li>compliance and procurement processes;</li><li>operations platforms where users alternate between workflow and table views.</li></ul><p>The strongest fit is a product where tasks already have rich fields, permissions, reports, server state, and alternate views. In that environment, a separate board-only data model quickly becomes technical debt.</p><h2 id="Final-takeaway" tabindex="-1">Final takeaway <a class="header-anchor" href="#Final-takeaway" aria-label="Permalink to “Final takeaway”">​</a></h2><p>Handling 50,000 tasks is not mainly a drag-and-drop problem.</p><p>It is a rendering, identity, policy, ordering, and persistence problem.</p><p>A scalable architecture should:</p><ul><li>keep task records canonical;</li><li>render a bounded viewport instead of every card;</li><li>virtualize workflow columns and card rows together;</li><li>switch between table and board without duplicating state;</li><li>validate multi-card movement atomically;</li><li>enforce WIP and transition rules against canonical counts;</li><li>persist only records changed by the committed operation;</li><li>keep custom templates lightweight.</li></ul><p>That is the architecture behind RevoGrid&#39;s <a href="/demo/kanban-performance">live 50,000-task Kanban demo</a>.</p><p>Explore the <a href="/kanban">RevoGrid Kanban product page</a>, open the <a href="/demo/kanban">complete interactive Kanban demo</a>, read the <a href="https://pro.rv-grid.com/guides/kanban/" target="_blank" rel="noreferrer">Enterprise Kanban documentation</a>, or <a href="/trial">get the Pro Advanced trial</a> to validate the board with your own task model, policies, templates, and persistence layer.</p><h2 id="Frequently-asked-questions" tabindex="-1">Frequently asked questions <a class="header-anchor" href="#Frequently-asked-questions" aria-label="Permalink to “Frequently asked questions”">​</a></h2><h3 id="Can-a-JavaScript-Kanban-board-really-handle-50-000-tasks-" tabindex="-1">Can a JavaScript Kanban board really handle 50,000 tasks? <a class="header-anchor" href="#Can-a-JavaScript-Kanban-board-really-handle-50-000-tasks-" aria-label="Permalink to “Can a JavaScript Kanban board really handle 50,000 tasks?”">​</a></h3><p>Yes, provided the component does not mount all 50,000 cards. RevoGrid&#39;s performance demo loads 50,000 source-backed tasks across ten workflow columns and two swimlanes while native row and column virtualization keep the rendered board bounded. Actual capacity depends on browser, hardware, task-record size, card template complexity, and surrounding application work.</p><h3 id="Does-RevoGrid-render-every-Kanban-card-" tabindex="-1">Does RevoGrid render every Kanban card? <a class="header-anchor" href="#Does-RevoGrid-render-every-Kanban-card-" aria-label="Permalink to “Does RevoGrid render every Kanban card?”">​</a></h3><p>No. The logical records remain available in <code>grid.source</code>, but RevoGrid renders the workflow columns and card rows required for the current viewport plus its rendering buffer.</p><h3 id="Can-the-same-records-be-shown-in-a-table-and-Kanban-board-" tabindex="-1">Can the same records be shown in a table and Kanban board? <a class="header-anchor" href="#Can-the-same-records-be-shown-in-a-table-and-Kanban-board-" aria-label="Permalink to “Can the same records be shown in a table and Kanban board?”">​</a></h3><p>Yes. The table uses <code>grid.columns</code> and the board uses <code>grid.kanban</code> to project the same <code>grid.source</code> records. Set <code>grid.kanban</code> to <code>false</code> to return to the latest table source and columns.</p><h3 id="How-are-drag-and-drop-changes-saved-to-a-server-" tabindex="-1">How are drag-and-drop changes saved to a server? <a class="header-anchor" href="#How-are-drag-and-drop-changes-saved-to-a-server-" aria-label="Permalink to “How are drag-and-drop changes saved to a server?”">​</a></h3><p>Listen for <code>kanbancardmove</code> and persist <code>event.detail.changedCards</code>. That batch includes moved records and any additional records affected by destination-rank rebalancing. Avoid sending the complete board after each operation.</p><h3 id="Can-WIP-limits-reject-a-multi-card-move-" tabindex="-1">Can WIP limits reject a multi-card move? <a class="header-anchor" href="#Can-WIP-limits-reject-a-multi-card-move-" aria-label="Permalink to “Can WIP limits reject a multi-card move?”">​</a></h3><p>Yes. With <code>wipBehavior: &#39;block&#39;</code>, the board validates the final proposed batch count and rejects the entire move when the destination would exceed capacity or another policy check fails.</p><h3 id="Is-50-000-the-maximum-number-of-tasks-" tabindex="-1">Is 50,000 the maximum number of tasks? <a class="header-anchor" href="#Is-50-000-the-maximum-number-of-tasks-" aria-label="Permalink to “Is 50,000 the maximum number of tasks?”">​</a></h3><p>No fixed product limit is implied by the demo. Practical limits depend on the user&#39;s hardware, browser memory, record size, card rendering cost, and the rest of the application. The important property is that rendered DOM remains bounded as the logical dataset grows.</p><h2 id="Related-Kanban-resources" tabindex="-1">Related Kanban resources <a class="header-anchor" href="#Related-Kanban-resources" aria-label="Permalink to “Related Kanban resources”">​</a></h2><ul><li><a href="/kanban">RevoGrid Kanban overview</a></li><li><a href="/demo/kanban-performance">50K-task Kanban performance demo</a></li><li><a href="/demo/kanban">Complete Kanban walkthrough</a></li><li><a href="https://pro.rv-grid.com/guides/kanban/data-model/" target="_blank" rel="noreferrer">Kanban data model guide</a></li><li><a href="https://pro.rv-grid.com/guides/kanban/swimlanes-wip-rules/" target="_blank" rel="noreferrer">Swimlanes, WIP, and rules</a></li><li><a href="https://pro.rv-grid.com/guides/kanban/interaction/" target="_blank" rel="noreferrer">Drag, drop, and multi-selection</a></li><li><a href="https://pro.rv-grid.com/guides/kanban/events-methods/" target="_blank" rel="noreferrer">Events, methods, and persistence</a></li><li><a href="https://pro.rv-grid.com/guides/kanban/accessibility-performance/" target="_blank" rel="noreferrer">Accessibility and performance</a></li><li><a href="https://github.com/revolist/kanban" target="_blank" rel="noreferrer">Kanban showcase source on GitHub</a></li></ul>`,142)])}var f=a(u,[[`render`,d]]);
247export{l as __pageData,f as default,c as t};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.