@toclocoinc/lattice-grid 1.59.0 → 1.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +3 -3
  2. package/docs/API.html +1623 -91
  3. package/docs/api-detail.html +270 -5
  4. package/lattice-grid.d.ts +100 -2880
  5. package/lattice-grid.esm.min.js +449 -66
  6. package/lattice-grid.min.cjs +449 -66
  7. package/lattice-grid.min.js +449 -66
  8. package/modules/ai.d.ts +401 -0
  9. package/modules/ai.esm.min.js +43 -6
  10. package/modules/ai.min.cjs +43 -6
  11. package/modules/ai.min.js +43 -6
  12. package/modules/angular.d.ts +31 -0
  13. package/modules/angular.esm.min.js +3 -3
  14. package/modules/angular.min.cjs +3 -3
  15. package/modules/angular.min.js +3 -3
  16. package/modules/chart-alluvial.d.ts +18 -0
  17. package/modules/chart-alluvial.esm.min.js +1 -1
  18. package/modules/chart-arc.d.ts +18 -0
  19. package/modules/chart-arc.esm.min.js +1 -1
  20. package/modules/chart-bubblemap.d.ts +18 -0
  21. package/modules/chart-bubblemap.esm.min.js +1 -1
  22. package/modules/chart-bump.d.ts +12 -0
  23. package/modules/chart-bump.esm.min.js +1 -1
  24. package/modules/chart-calendar.d.ts +12 -0
  25. package/modules/chart-calendar.esm.min.js +1 -1
  26. package/modules/chart-decomposition.d.ts +20 -0
  27. package/modules/chart-decomposition.esm.min.js +1 -1
  28. package/modules/chart-diverging.d.ts +12 -0
  29. package/modules/chart-diverging.esm.min.js +1 -1
  30. package/modules/chart-dumbbell.d.ts +18 -0
  31. package/modules/chart-dumbbell.esm.min.js +1 -1
  32. package/modules/chart-fan.d.ts +18 -0
  33. package/modules/chart-fan.esm.min.js +1 -1
  34. package/modules/chart-hexbin.d.ts +18 -0
  35. package/modules/chart-hexbin.esm.min.js +1 -1
  36. package/modules/chart-hexmap.d.ts +18 -0
  37. package/modules/chart-hexmap.esm.min.js +1 -1
  38. package/modules/chart-icicle.d.ts +12 -0
  39. package/modules/chart-icicle.esm.min.js +1 -1
  40. package/modules/chart-parallel.d.ts +19 -0
  41. package/modules/chart-parallel.esm.min.js +1 -1
  42. package/modules/chart-ridgeline.d.ts +14 -0
  43. package/modules/chart-ridgeline.esm.min.js +1 -1
  44. package/modules/chart-roc.d.ts +20 -0
  45. package/modules/chart-roc.esm.min.js +1 -1
  46. package/modules/chart-slope.d.ts +12 -0
  47. package/modules/chart-slope.esm.min.js +1 -1
  48. package/modules/chart-splom.d.ts +19 -0
  49. package/modules/chart-splom.esm.min.js +1 -1
  50. package/modules/chart-waffle.d.ts +12 -0
  51. package/modules/chart-waffle.esm.min.js +1 -1
  52. package/modules/charts.d.ts +122 -0
  53. package/modules/charts.esm.min.js +35 -8
  54. package/modules/charts.min.cjs +35 -8
  55. package/modules/charts.min.js +35 -8
  56. package/modules/data-router.d.ts +91 -0
  57. package/modules/data-router.esm.min.js +109 -17
  58. package/modules/data-router.min.cjs +109 -17
  59. package/modules/data-router.min.js +109 -17
  60. package/modules/devtools.d.ts +28 -0
  61. package/modules/devtools.esm.min.js +2 -2
  62. package/modules/devtools.min.cjs +2 -2
  63. package/modules/devtools.min.js +2 -2
  64. package/modules/dhtmlx-compat.d.ts +19 -0
  65. package/modules/dhtmlx-compat.esm.min.js +4 -4
  66. package/modules/dhtmlx-compat.min.cjs +4 -4
  67. package/modules/dhtmlx-compat.min.js +4 -4
  68. package/modules/gantt.d.ts +647 -0
  69. package/modules/gantt.esm.min.js +1070 -201
  70. package/modules/gantt.min.cjs +1070 -201
  71. package/modules/gantt.min.js +1070 -201
  72. package/modules/htmx.d.ts +176 -0
  73. package/modules/htmx.esm.min.js +449 -66
  74. package/modules/htmx.min.cjs +449 -66
  75. package/modules/htmx.min.js +449 -66
  76. package/modules/kanban.d.ts +492 -0
  77. package/modules/kanban.esm.min.js +4 -4
  78. package/modules/kanban.min.cjs +4 -4
  79. package/modules/kanban.min.js +4 -4
  80. package/modules/kpi.d.ts +255 -0
  81. package/modules/kpi.esm.min.js +40 -7
  82. package/modules/kpi.min.cjs +40 -7
  83. package/modules/kpi.min.js +40 -7
  84. package/modules/layout.d.ts +332 -0
  85. package/modules/layout.esm.min.js +4 -4
  86. package/modules/layout.min.cjs +4 -4
  87. package/modules/layout.min.js +4 -4
  88. package/modules/mock-socket.d.ts +114 -0
  89. package/modules/mock-socket.esm.min.js +2 -2
  90. package/modules/mock-socket.min.cjs +2 -2
  91. package/modules/mock-socket.min.js +2 -2
  92. package/modules/react.d.ts +25 -0
  93. package/modules/react.esm.min.js +3 -3
  94. package/modules/react.min.cjs +3 -3
  95. package/modules/react.min.js +3 -3
  96. package/modules/svelte.d.ts +26 -0
  97. package/modules/svelte.esm.min.js +3 -3
  98. package/modules/svelte.min.cjs +3 -3
  99. package/modules/svelte.min.js +3 -3
  100. package/modules/tabs.d.ts +133 -0
  101. package/modules/tabs.esm.min.js +11 -4
  102. package/modules/tabs.min.cjs +11 -4
  103. package/modules/tabs.min.js +11 -4
  104. package/modules/vue.d.ts +24 -0
  105. package/modules/vue.esm.min.js +3 -3
  106. package/modules/vue.min.cjs +3 -3
  107. package/modules/vue.min.js +3 -3
  108. package/modules/webcomponent.d.ts +47 -0
  109. package/modules/webcomponent.esm.min.js +449 -66
  110. package/modules/webcomponent.min.cjs +449 -66
  111. package/modules/webcomponent.min.js +449 -66
  112. package/package.json +2 -2
package/docs/API.html CHANGED
@@ -360,7 +360,7 @@
360
360
  <div class="shell">
361
361
  <aside class="rail">
362
362
  <p class="rail__brand">Lattice Grid</p>
363
- <p class="rail__sub">API reference · v1.59.0</p>
363
+ <p class="rail__sub">API reference · v1.61.0</p>
364
364
  <nav>
365
365
  <div class="rail__group">
366
366
  <span class="rail__label">Start</span>
@@ -443,7 +443,7 @@
443
443
  </header>
444
444
 
445
445
  <p class="chips">
446
- <span class="chip">Version 1.59.0</span>
446
+ <span class="chip">Version 1.61.0</span>
447
447
  <span class="chip">Zero dependencies</span>
448
448
  <span class="chip"><a href="api-detail.html">Developer guide &rarr;</a></span>
449
449
  </p>
@@ -1218,7 +1218,7 @@ grid.destroy();
1218
1218
  <table>
1219
1219
  <thead><tr><th>Member</th><th>Returns</th><th>Description</th></tr></thead>
1220
1220
  <tbody>
1221
- <tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.59.0'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
1221
+ <tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.61.0'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
1222
1222
  <tr><td class="sig">get(key)</td><td class="type">unknown</td><td class="desc">Read any configuration key.</td></tr>
1223
1223
  <tr><td class="sig">set(key, value)</td><td class="type">void</td><td class="desc">Write one key. Every key is live; nothing needs a rebuild.</td></tr>
1224
1224
  <tr><td class="sig">setAll(values)</td><td class="type">void</td><td class="desc">Write several in one pass. Emits one <code>config:changed</code> for the batch, not one per key.</td></tr>
@@ -4060,6 +4060,58 @@ app.get('/api/orders', async (req, res) =&gt; {
4060
4060
  <span class="kw">const</span> block = <span class="kw">await</span> lax.fetch(req);
4061
4061
  <span class="kw">return</span> refused ? `refused; opted in: ${block.rows.length} rows` : 'not refused';</code></pre>
4062
4062
 
4063
+ <h4 id="pushdown-whererowlimit">Running a host predicate: <code>whereRowLimit</code></h4>
4064
+ <p class="section-note">
4065
+ A <a href="#where-predicates"><code>where</code></a> predicate is a host function &mdash; whether
4066
+ this user may see the row, whether you hold a rate for its currency. No engine can evaluate one,
4067
+ so the only way a pushdown source can honour it is to fetch <em>every</em> matching row and filter
4068
+ here. That is a real answer, and it is also a windowed grid quietly turning into a whole-dataset
4069
+ download &mdash; the one thing a pushdown source exists to avoid.
4070
+ </p>
4071
+ <p class="section-note">
4072
+ So it is gated rather than done on your behalf. Under <code>whereRowLimit</code> (default
4073
+ <code>50_000</code>, the same anchor as the grid's <code>workerThreshold</code>) the predicate
4074
+ runs and the counts are whole-dataset counts. At or past it &mdash; or when the adapter reports
4075
+ <strong>no row total</strong>, since the only way to learn the size from such an adapter is to
4076
+ fetch the set &mdash; the source <strong>refuses</strong>: the predicate is not applied, the rows
4077
+ it would exclude stay on screen, and one warning names the adapter, the size, the limit and the
4078
+ way out. Raise the limit when you want the download.
4079
+ </p>
4080
+ <div class="note">
4081
+ <p><strong>The <code>{ condition }</code> twin is the route that works at any size.</strong> It is
4082
+ pushed to the engine, which narrows the fetch itself, so no limit applies and nothing is held here.
4083
+ Reach for the limit only when the predicate genuinely cannot be expressed as a condition.</p>
4084
+ </div>
4085
+ <div class="table-wrap">
4086
+ <table>
4087
+ <thead><tr><th>Key</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
4088
+ <tbody>
4089
+ <tr><td class="name">whereRowLimit</td><td class="type">number</td><td class="desc"><code>50_000</code></td><td class="desc">The most rows the source will fetch and hold in order to run a twinless <code>where</code> predicate. At or past this many matching rows the predicate is refused and warned about rather than the whole set downloaded. An adapter reporting no row total counts as over the limit. <code>0</code> refuses every predicate.</td></tr>
4090
+ </tbody>
4091
+ </table>
4092
+ </div>
4093
+ <pre data-run="js" data-expect="applied: 2, refused: 3" data-covers="config:whereRowLimit"><code><span class="cmt">// Three rows, two of them ana's. The predicate is a host function with no twin,</span>
4094
+ <span class="cmt">// so the engine cannot narrow the fetch and the source must hold the set to run it.</span>
4095
+ <span class="kw">const</span> { createPushdownSource } = <span class="kw">await</span> import('../packages/core/src/source/pushdown.js');
4096
+ <span class="kw">const</span> all = [{ id: 1, owner: 'ana' }, { id: 2, owner: 'bo' }, { id: 3, owner: 'ana' }];
4097
+ <span class="kw">const</span> adapter = {
4098
+ name: 'demo',
4099
+ capabilities: { range: <span class="kw">true</span>, total: <span class="kw">true</span> },
4100
+ execute: <span class="kw">async</span> (q) =&gt; ({ rows: q.range ? all.slice(q.range.start, q.range.end) : all, total: all.length }),
4101
+ };
4102
+ <span class="kw">const</span> where = { active: <span class="kw">true</span>, names: ['mine'], version: 1, passes: (row) =&gt; row.owner === 'ana' };
4103
+ <span class="kw">const</span> req = { range: { start: 0, end: 10 }, filters: <span class="kw">null</span>, sort: [], quick: '', where };
4104
+
4105
+ <span class="cmt">// Under the limit: the whole matching set is fetched and the predicate runs.</span>
4106
+ <span class="kw">const</span> under = createPushdownSource({ adapter, whereRowLimit: 1000 });
4107
+ <span class="kw">const</span> applied = <span class="kw">await</span> under.fetch(req);
4108
+
4109
+ <span class="cmt">// At or past it: refused and warned about, and every row stays on screen.</span>
4110
+ <span class="kw">const</span> over = createPushdownSource({ adapter, whereRowLimit: 2 });
4111
+ <span class="kw">const</span> refused = <span class="kw">await</span> over.fetch(req);
4112
+
4113
+ <span class="kw">return</span> `applied: ${applied.rows.length}, refused: ${refused.rows.length}`;</code></pre>
4114
+
4063
4115
  <h4 id="pushdown-aggregates">Pushing statistics down: <code>aggregates</code></h4>
4064
4116
  <p class="section-note">
4065
4117
  A DuckDB-class engine can compute a median or a standard deviation over the whole matching set
@@ -4348,6 +4400,8 @@ off(); <span class="cmt">// on() returns i
4348
4400
  <tr><td class="name">cell:dblclicked</td><td class="type">{ ...as cell:clicked }</td><td class="desc"></td></tr>
4349
4401
  <tr><td class="name">cell:mouseover</td><td class="type">{ ...as cell:clicked, target }</td><td class="desc">The pointer entered a cell, once per cell. <code>target</code> is the cell element. Moving between children of one cell fires nothing; moving straight to the next cell fires <code>cell:mouseout</code> then <code>cell:mouseover</code>. Delegated on the viewport, so it is correct over pooled rows — a row re-used after a scroll reports the row it shows now. Announcement only, and nothing in the grid is gated on hover.</td></tr>
4350
4402
  <tr><td class="name">cell:mouseout</td><td class="type">{ ...as cell:mouseover }</td><td class="desc">The pointer left a cell, once per cell — including when it left the grid entirely.</td></tr>
4403
+ <tr><td class="name">cell:mousedown</td><td class="type">{ ...as cell:clicked, target }</td><td class="desc">A pointer button went down on a cell. <code>target</code> is the cell element. Delegated on the viewport, so it is correct over pooled rows — a row re-used after a scroll between the press and the release reports the row it shows now. Announcement only: nothing is consumed, so the existing focus and click behaviour is unchanged.</td></tr>
4404
+ <tr><td class="name">cell:mouseup</td><td class="type">{ ...as cell:mousedown }</td><td class="desc">A pointer button was released over a cell.</td></tr>
4351
4405
  <tr><td class="name">row:clicked</td><td class="type">{ row, key, index, event }</td><td class="desc">Emitted alongside the cell event, cell first.</td></tr>
4352
4406
  <tr><td class="name">row:dblclicked</td><td class="type">{ row, key, index, event }</td><td class="desc"></td></tr>
4353
4407
  <tr><td class="name">row:edit:start</td><td class="type">{ row, key, colId, column }</td><td class="desc">Replaces the cell pair when <code>edit.mode</code> is <code>'row'</code>.</td></tr>
@@ -5170,7 +5224,7 @@ grid.destroy();
5170
5224
  ].join(' | ');</code></pre>
5171
5225
 
5172
5226
  <h2 id="datarouter">The data router</h2>
5173
- <p><code>modules/data-router</code> is a host-layer demultiplexer: it takes <strong>one</strong> arriving stream or dataset, splits it by what each record <em>is</em>, and routes each partition to its own grid &mdash; or to a headless grid driving a chart. One round-trip, or one live feed, hydrates a whole screen of grids that each see only their slice. It is optional, imports nothing from the grid, and adds no core hook: every grid is driven through the <strong>public</strong> incremental path, <code>grid.rows.apply({ add, update, remove })</code>.</p>
5227
+ <p><code>modules/data-router</code> is a host-layer demultiplexer: it takes <strong>one</strong> arriving stream or dataset, splits it by what each record <em>is</em>, and routes each partition to its own grid &mdash; or to a headless grid driving a chart. One round-trip, or one live feed, hydrates a whole screen of grids that each see only their slice. It is optional, imports nothing from the grid, and adds no core hook: every grid is driven through the <strong>public</strong> incremental path, <code>grid.rows.apply({ add, update, remove })</code>. <strong>The router never opens a connection itself &mdash; the host owns the connection (a <code>WebSocket</code>, SSE, CDC, a message bus, a plain fetch), and the router owns everything once a message has arrived</strong>; see <a href="#datarouter-websocket-example">a live WebSocket feed</a> for the worked, runnable integration.</p>
5174
5228
  <pre><code>import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
5175
5229
 
5176
5230
  const router = createDataRouter({
@@ -5365,7 +5419,7 @@ customers.selection.set(['c1']); <span class="cmt">// emea; no selection in orde
5365
5419
  customers.destroy(); orders.destroy(); lines.destroy(); router.destroy();
5366
5420
  <span class="kw">return</span> out.join(' | ');</code></pre>
5367
5421
 
5368
- <p><strong>Stream hygiene (v3, BACKLOG-0000887).</strong> A production feed arrives out of order, gets replayed, and comes faster than a grid should repaint. Configure a <code>seq</code> (a version field or <code>fn(row)</code>) and the router orders each batch by it and <strong>drops</strong> any delta not newer than the one it already applied for that record (counted in <code>router.dropped</code>) &mdash; an out-of-order or replayed feed converges to the newest state. <code>push(delta)</code> with a <code>batch</code> interval or <code>coalesce: true</code> buffers a high-frequency feed and coalesces rapid updates to one key into a single apply (flush a deterministic point with <code>flushStream()</code>). After a dropped socket, resume precisely: <code>load</code> a fresh snapshot (a keyed diff that preserves grid state) and replay from <code>lastSeq()</code>/<code>checkpoint()</code> &mdash; the deltas the router already saw are dropped by the same gate. <code>seenThrough(mark)</code> primes the checkpoint from a persisted one.</p>
5422
+ <p><strong>Stream hygiene (v3, BACKLOG-0000887).</strong> A production feed arrives out of order, gets replayed, and comes faster than a grid should repaint. Configure a <code>seq</code> (a version field or <code>fn(row)</code>) and the router orders each batch by it and <strong>drops</strong> any delta not newer than the one it already applied for that record (counted in <code>router.dropped</code>) &mdash; an out-of-order or replayed feed converges to the newest state. <code>push(delta)</code> with a <code>batch</code> interval or <code>coalesce: true</code> buffers a high-frequency feed and coalesces rapid updates to one key into a single apply (flush a deterministic point with <code>flushStream()</code>). When the host's own connection drops and it reconnects, resume precisely: <code>load</code> a fresh snapshot (a keyed diff that preserves grid state) and replay from <code>lastSeq()</code>/<code>checkpoint()</code> &mdash; the deltas the router already saw are dropped by the same gate. The router does not detect or recover from the drop itself; see <a href="#datarouter-websocket-example">a live WebSocket feed</a> for the worked reconnect example. <code>seenThrough(mark)</code> primes the checkpoint from a persisted one.</p>
5369
5423
  <pre data-run="js" data-expect="30 | 1 | 4" data-covers="export:createDataRouter"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
5370
5424
  <span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
5371
5425
 
@@ -5384,6 +5438,65 @@ router.apply([{ op: 'upsert', row: { id: 'a', n: 99, v: 4 } }]); <span class="cm
5384
5438
  g.destroy(); router.destroy();
5385
5439
  <span class="kw">return</span> [n, dropped, last].join(' | ');</code></pre>
5386
5440
 
5441
+ <p><strong>A live WebSocket feed (BACKLOG-0001259).</strong> The router never opens a connection itself — there is no <code>new WebSocket</code> anywhere in <code>modules/data-router</code>. <strong>The host owns the connection; the router owns everything once a message has arrived.</strong> Wire a socket's <code>onmessage</code> to the two entry points above: a <code>snapshot</code> message's rows go to <code>load()</code>, a <code>delta</code> message's changes go to <code>apply()</code> (or <code>push()</code>, batched, for a fast feed). Nothing else changes for a real <code>WebSocket</code>, an <code>EventSource</code>, a CDC feed or a message bus — the router takes rows, never a URL or a socket, so the transport is always the host's choice. On a drop, the reconnect pattern is the same snapshot-plus-replay shown above: capture <code>lastSeq()</code>/<code>checkpoint()</code> before the drop, <code>load()</code> a fresh snapshot on the new connection, and let the feed replay from around the last point — anything already applied is dropped by the same <code>seq</code> gate, not re-applied.</p>
5442
+ <h3 id="datarouter-websocket-example">A live WebSocket feed, with reconnect, executed</h3>
5443
+ <p class="section-note">A socket-shaped feed (<code>modules/mock-socket</code>, which frames messages exactly as a real
5444
+ <code>WebSocket</code> does — the same <code>onmessage</code>, the same JSON-framed <code>event.data</code>)
5445
+ drives the router; the connection then drops and reconnects, and a replayed delta already applied is
5446
+ dropped by the <code>seq</code> checkpoint while a genuinely new one lands. Swapping in a real
5447
+ <code>WebSocket</code> is a one-line constructor change — see <a href="#mocksocket">the mock socket</a>.
5448
+ Run headless on every build.</p>
5449
+ <pre data-run="js" data-expect="A@3 | 3 | A@4 | 2" data-covers="export:createDataRouter export:MockWebSocket"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
5450
+ <span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
5451
+ <span class="kw">const</span> { MockWebSocket } = <span class="kw">await</span> import('../packages/modules/mock-socket/index.js');
5452
+
5453
+ <span class="kw">const</span> g = createHeadlessGrid({ rowKey: 'id', columns: [{ id: 'id', field: 'id' }, { id: 'label', field: 'label' }] });
5454
+ <span class="kw">const</span> router = createDataRouter({ rowKey: 'id', seq: 'v' });
5455
+ router.attach(g, () =&gt; <span class="kw">true</span>);
5456
+
5457
+ <span class="cmt">// THE INTEGRATION: a snapshot message loads, a delta message applies. This is</span>
5458
+ <span class="cmt">// exactly what a page writes against a real `new WebSocket(url)`.</span>
5459
+ <span class="kw">function</span> wireRouter(r, socket) {
5460
+ socket.onmessage = (event) =&gt; {
5461
+ <span class="kw">const</span> msg = JSON.parse(event.data);
5462
+ <span class="kw">if</span> (msg.kind === 'snapshot') r.load(msg.rows);
5463
+ <span class="kw">else if</span> (msg.kind === 'delta') r.apply(msg.changes);
5464
+ };
5465
+ }
5466
+ <span class="kw">const</span> wait = (ms) =&gt; <span class="kw">new</span> Promise((resolve) =&gt; setTimeout(resolve, ms));
5467
+
5468
+ <span class="cmt">// First connection: live through v:3, then the socket drops.</span>
5469
+ <span class="kw">function</span>* feedA() {
5470
+ <span class="kw">yield</span> { kind: 'snapshot', rows: [] };
5471
+ <span class="kw">yield</span> { kind: 'delta', changes: [{ op: 'upsert', row: { id: 'a', label: 'A@1', v: 1 } }] };
5472
+ <span class="kw">yield</span> { kind: 'delta', changes: [{ op: 'upsert', row: { id: 'a', label: 'A@3', v: 3 } }] };
5473
+ }
5474
+ <span class="kw">const</span> socketA = <span class="kw">new</span> MockWebSocket({ feed: feedA(), rate: 5, jitter: 0, snapshotDelay: 5 });
5475
+ wireRouter(router, socketA);
5476
+ <span class="kw">await</span> wait(30);
5477
+ <span class="kw">const</span> beforeDrop = g.rows.value('a', 'label'); <span class="cmt">// A@3</span>
5478
+ <span class="kw">const</span> resumeFrom = router.lastSeq(); <span class="cmt">// 3 — the resume cursor</span>
5479
+ socketA.close();
5480
+
5481
+ <span class="cmt">// Reconnect: the server answers with a fresh snapshot plus a replay that</span>
5482
+ <span class="cmt">// includes two deltas already applied (v:1, v:3) and one truly new one (v:4).</span>
5483
+ <span class="kw">function</span>* feedB() {
5484
+ <span class="kw">yield</span> { kind: 'snapshot', rows: [{ id: 'a', label: 'A@3', v: 3 }] };
5485
+ <span class="kw">yield</span> { kind: 'delta', changes: [{ op: 'upsert', row: { id: 'a', label: 'A@1', v: 1 } }] }; <span class="cmt">// replayed</span>
5486
+ <span class="kw">yield</span> { kind: 'delta', changes: [{ op: 'upsert', row: { id: 'a', label: 'A@3', v: 3 } }] }; <span class="cmt">// replayed</span>
5487
+ <span class="kw">yield</span> { kind: 'delta', changes: [{ op: 'upsert', row: { id: 'a', label: 'A@4', v: 4 } }] }; <span class="cmt">// new</span>
5488
+ }
5489
+ <span class="kw">const</span> droppedBefore = router.dropped;
5490
+ <span class="kw">const</span> socketB = <span class="kw">new</span> MockWebSocket({ feed: feedB(), rate: 5, jitter: 0, snapshotDelay: 5 });
5491
+ wireRouter(router, socketB);
5492
+ <span class="kw">await</span> wait(30);
5493
+ <span class="kw">const</span> afterReconnect = g.rows.value('a', 'label'); <span class="cmt">// A@4 — only the new delta advanced it</span>
5494
+ <span class="kw">const</span> replaysDropped = router.dropped - droppedBefore; <span class="cmt">// 2 — both replays dropped</span>
5495
+ socketB.close();
5496
+
5497
+ g.destroy(); router.destroy();
5498
+ <span class="kw">return</span> [beforeDrop, resumeFrom, afterReconnect, replaysDropped].join(' | ');</code></pre>
5499
+
5387
5500
  <h3 id="datarouter-example">One feed, three grids, executed</h3>
5388
5501
  <p class="section-note">A single snapshot fanned to an orders grid, an invoices grid and a "rest" sink, then a
5389
5502
  delta that changes a row's partition &mdash; proving the fan-out, the sink, and that a moved row
@@ -5697,7 +5810,8 @@ gantt.mount(document.querySelector('#plan'), { editable: true });
5697
5810
  <p>Hovering a bar shows a tooltip with its dates, duration, % complete and slack. Tasks are flagged when they slip: <strong>overdue</strong> (incomplete and finishing before <code>today</code>) and <strong>at-risk</strong> (negative total float). Negative float needs a target: pass a <code>deadline</code> (a day-number) to <code>createGantt</code> and any task that cannot meet it gets negative slack and is drawn at-risk.</p>
5698
5811
  <p>Export: <code>gantt.toCSV()</code> writes the scheduled tasks as CSV (<code>{ dates: true }</code> for ISO dates); when a grid is bound, the grid's own Excel/CSV export works too. <code>gantt.view.toSVG()</code> serialises the drawn chart to a standalone SVG string — the handoff for turning it into an image or PDF.</p>
5699
5812
  <p>Accessibility: bars are focusable and carry an <code>aria-label</code> describing the task (name, dates, progress, slack, critical). With the keyboard, arrows move a focused task, Shift+arrows resize it, and <kbd>L</kbd> links two tasks (press it on the source, then on the successor) with a finish-to-start dependency; every edit is announced in a polite live region and focus follows the edited task. Set <code>keyboard: false</code> to opt out.</p>
5700
- <p><strong>Split view.</strong> The Gantt does not build a grid or the two-pane layout — you create and place a normal Lattice grid over the same task rows, and the Gantt <em>consumes</em> it. Binding is two-way: a drag on the timeline writes back through the grid (cycle 3), and an edit in the grid pane (any editor) reflects on the timeline. v1 assumes both panes share the same row height; <code>view.linkVerticalScroll(el)</code> mirrors vertical scroll so the rows stay aligned.</p>
5813
+ <p><strong>Two panes, two ways.</strong> There are two arrangements, and which one you want depends on whether the left pane is <em>your</em> grid or the Gantt's own. <code>gantt.mountSplit(container, options)</code> — the joined split view, whose options are listed with <code>GanttController</code> in the type reference — draws both panes itself as one row-aligned surface, and is what to reach for unless you specifically need your own grid beside the timeline. The arrangement described here is the other one: you create and place a normal Lattice grid over the same task rows, mount the timeline beside it with <code>gantt.mount()</code>, and the Gantt <em>consumes</em> the grid rather than building it.</p>
5814
+ <p><strong>Binding a grid you built yourself.</strong> It is two-way. A drag on the timeline writes the new dates back through the grid's public edit surface (<code>grid.edit.setCells</code>) whenever <code>createGantt</code> was given a <code>grid</code> and a <code>columns</code> map — without both, the drag moves the bar and writes nothing — and an edit made in the grid pane, from any editor, reflects on the timeline. Both panes must be given the same row height for the rows to line up, and <code>view.linkVerticalScroll(el)</code> mirrors vertical scroll between them. Neither is needed with <code>mountSplit</code>, which shares one scroll and one row height by construction.</p>
5701
5815
  <pre><code>&lt;div class="split" style="display:grid;grid-template-columns:360px 1fr"&gt;
5702
5816
  &lt;div id="tasks"&gt;&lt;/div&gt; &lt;!-- the grid pane --&gt;
5703
5817
  &lt;div id="plan"&gt;&lt;/div&gt; &lt;!-- the timeline pane --&gt;
@@ -6229,7 +6343,7 @@ const tabs = createTabs(document.querySelector('#tabs'), {
6229
6343
  </div>
6230
6344
  <h3 id="tabs-live-example">All / Open / Breached, a two-deep derivation chain, executed</h3>
6231
6345
  <p class="section-note"><code>createTabs</code> deliberately has no headless mode &mdash; it requires a real host element, the same way <code>createGrid</code> itself does &mdash; so this executed example reaches for the same in-tree DOM test double the suite itself runs the renderer against headlessly (<code>packages/dom/src/renderer/testdom.js</code>), rather than a real browser. <code>demo/tabs.html</code> is the browser version of the same chain, with buttons that edit All directly and let Open and Breached follow.</p>
6232
- <pre data-run="js" data-expect="4,3,2" data-covers="export:createTabs"><code><span class="kw">const</span> { createTestDom } = <span class="kw">await</span> import('../packages/dom/src/renderer/testdom.js');
6346
+ <pre data-run="js" data-expect="4,3,2" data-covers="export:createTabs config:createGrid config:tabs"><code><span class="kw">const</span> { createTestDom } = <span class="kw">await</span> import('../packages/dom/src/renderer/testdom.js');
6233
6347
  <span class="kw">const</span> { root } = createTestDom();
6234
6348
  <span class="kw">const</span> { createGrid } = <span class="kw">await</span> import('../packages/dom/src/index.js');
6235
6349
  <span class="kw">const</span> { createTabs } = <span class="kw">await</span> import('../packages/modules/tabs/index.js');
@@ -6257,6 +6371,73 @@ tabs.activate('breached'); <span class="cmt">// materialises
6257
6371
  tabs.destroy();
6258
6372
  <span class="kw">return</span> counts.join(','); <span class="cmt">// 4,3,2</span></code></pre>
6259
6373
 
6374
+ <h3 id="tabs-governed-example">Opening on a chosen tab, and vetoing a switch, executed</h3>
6375
+ <p class="section-note"><code>active</code> picks the tab the strip opens on rather than the first; the three config callbacks are sugar for the same events <code>on()</code> exposes, so <code>onBeforeTabChange</code> can veto a switch with <code>preventDefault(reason)</code> and <code>onTabChangeCancelled</code> is told why. <code>onTabChange</code> fires only for a switch that actually happened &mdash; note it does not fire for the initial tab.</p>
6376
+ <pre data-run="js" data-expect="review | changed:draft; cancelled:locked(sealed) | draft" data-covers="config:active config:onTabChange config:onBeforeTabChange config:onTabChangeCancelled"><code><span class="kw">const</span> { createTestDom } = <span class="kw">await</span> import('../packages/dom/src/renderer/testdom.js');
6377
+ <span class="kw">const</span> { root } = createTestDom();
6378
+ <span class="kw">const</span> { createGrid } = <span class="kw">await</span> import('../packages/dom/src/index.js');
6379
+ <span class="kw">const</span> { createTabs } = <span class="kw">await</span> import('../packages/modules/tabs/index.js');
6380
+
6381
+ <span class="kw">const</span> rows = [{ id: 1 }];
6382
+ <span class="kw">const</span> columns = [{ field: 'id' }];
6383
+ <span class="kw">const</span> log = [];
6384
+
6385
+ <span class="kw">const</span> tabs = createTabs(root, {
6386
+ createGrid,
6387
+ active: 'review', <span class="cmt">// open here, not on the first tab</span>
6388
+ tabs: [
6389
+ { id: 'draft', label: 'Draft', config: { rowKey: 'id', rows, columns } },
6390
+ { id: 'review', label: 'Review', config: { rowKey: 'id', rows, columns } },
6391
+ { id: 'locked', label: 'Locked', config: { rowKey: 'id', rows, columns } },
6392
+ ],
6393
+ onTabChange: (e) =&gt; log.push(`changed:${e.id}`),
6394
+ onBeforeTabChange: (e) =&gt; { <span class="kw">if</span> (e.id === 'locked') e.preventDefault('sealed'); },
6395
+ onTabChangeCancelled: (e) =&gt; log.push(`cancelled:${e.id}(${e.reason})`),
6396
+ });
6397
+
6398
+ <span class="kw">const</span> opened = tabs.activeId; <span class="cmt">// 'review' -- config.active won</span>
6399
+ tabs.activate('draft'); <span class="cmt">// allowed, so onTabChange fires</span>
6400
+ tabs.activate('locked'); <span class="cmt">// vetoed, so onTabChangeCancelled fires</span>
6401
+ <span class="kw">const</span> ended = tabs.activeId; <span class="cmt">// still 'draft'</span>
6402
+ tabs.destroy();
6403
+ <span class="kw">return</span> `${opened} | ${log.join('; ')} | ${ended}`;</code></pre>
6404
+
6405
+ <h3 id="tabs-headless-example">A tab nobody has clicked, carrying a live count, executed</h3>
6406
+ <p class="section-note">A badge needs rows, and rows normally need a mounted grid &mdash; so a tab that has never been activated would have nothing to count. Injecting <code>createHeadlessGrid</code> alongside <code>createGrid</code> gives that tab a real, derived count with no DOM and no mount. Without it the tab below shows no badge at all until its first activation, and the module says so once.</p>
6407
+ <pre data-run="js" data-expect="unmounted | 2 rows" data-covers="config:createHeadlessGrid"><code><span class="kw">const</span> { createTestDom } = <span class="kw">await</span> import('../packages/dom/src/renderer/testdom.js');
6408
+ <span class="kw">const</span> { root } = createTestDom();
6409
+ <span class="kw">const</span> { createGrid } = <span class="kw">await</span> import('../packages/dom/src/index.js');
6410
+ <span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
6411
+ <span class="kw">const</span> { createTabs } = <span class="kw">await</span> import('../packages/modules/tabs/index.js');
6412
+
6413
+ <span class="kw">const</span> rows = [
6414
+ { id: 1, stage: 'Open', daysOverdue: 0 },
6415
+ { id: 2, stage: 'Open', daysOverdue: 5 },
6416
+ { id: 3, stage: 'Won', daysOverdue: 0 },
6417
+ { id: 4, stage: 'Open', daysOverdue: 2 },
6418
+ ];
6419
+ <span class="kw">const</span> columns = [{ field: 'id' }, { field: 'stage' }, { field: 'daysOverdue', type: 'number' }];
6420
+
6421
+ <span class="kw">const</span> tabs = createTabs(root, {
6422
+ createGrid,
6423
+ createHeadlessGrid, <span class="cmt">// what gives an unmounted tab a count</span>
6424
+ tabs: [
6425
+ { id: 'all', label: 'All', badge: <span class="kw">true</span>, config: { rowKey: 'id', rows, columns } },
6426
+ { id: 'breached', label: 'Breached', from: 'all', where: (r) =&gt; r.daysOverdue &gt; 0,
6427
+ follow: 'filtered', refresh: 'live', badge: <span class="kw">true</span>, config: { columns } },
6428
+ ],
6429
+ });
6430
+
6431
+ <span class="cmt">// 'breached' has never been activated, so it has no grid of its own.</span>
6432
+ <span class="kw">const</span> button = root.querySelectorAll('[role=tab]')
6433
+ .find((b) =&gt; b.getAttribute('data-tab-id') === 'breached');
6434
+ <span class="kw">const</span> badge = button.children
6435
+ .find((c) =&gt; String(c.className || '').includes('lat-tabs__badge'));
6436
+ <span class="kw">const</span> state = tabs.isMounted('breached') ? 'mounted' : 'unmounted';
6437
+ <span class="kw">const</span> text = String(badge.textContent).trim().replace(/\s+/g, ' ');
6438
+ tabs.destroy();
6439
+ <span class="kw">return</span> `${state} | ${text}`; <span class="cmt">// unmounted | 2 rows</span></code></pre>
6440
+
6260
6441
  <h2 id="layout">The dashboard layout</h2>
6261
6442
  <p><code>modules/layout</code> is an opt-in <strong>reconfigurable dashboard surface</strong>: a cell grid inside an element, and a set of windows placed on it that a user can move, resize and close &mdash; by drag <em>or</em> by keyboard. It is the thing a customer would otherwise reach for GridStack or react-grid-layout to get, which means a second dependency, a second sizing model, and a seam where the viewers in it do not resize properly.</p>
6262
6443
  <p><strong>It is payload-agnostic, and that is the whole design.</strong> A window body is a <code>div</code> with an <code>id</code>. The module creates it, sizes it, and never reads or writes its contents &mdash; it does not import <code>createGrid</code>, does not know what a payload is, and never calls into one. What it does instead is emit <code>window:resized</code> with the measured content box, which is the contract. That rule is what keeps its own code to <strong>12,890 bytes gzipped</strong> (measured: a 77,190-byte bundle over a 62,206-byte fixed floor, of which 2,094 bytes are the two shared module helpers) and what makes it usable for a payload we have not written yet.</p>
@@ -6759,7 +6940,9 @@ grid.filters.reapply(); <span class="cmt">// re-run all</span
6759
6940
  </div>
6760
6941
 
6761
6942
  <div class="note">
6762
- <p><strong>On a pushdown source the counts are page-relative, and the grid says so.</strong> A predicate is a function: no engine can evaluate it, so it always runs client-side, over the rows that came back. The grid warns once that match counts and totals are therefore relative to the fetched set, and names the fix &mdash; give the predicate a <code>condition</code> twin so the engine narrows the fetch itself. The paged and remote sources receive the twin but do <strong>not</strong> apply the predicate to their window: their rows are held in a block cache indexed by the server's own ranges and totals, so filtering a block client-side would leave <code>count()</code> disagreeing with what is painted.</p>
6943
+ <p><strong>A predicate runs where the whole dataset is.</strong> Memory, stream and derived sources hold every row, so the function runs across all of them and the counts it produces are whole-dataset counts. The paged and remote sources hold only what they fetched, and what reaches them is the condition tree rather than the function: a predicate on either of those narrows nothing by itself, and the grid warns once when you register it, naming the predicate and the source kind.</p>
6944
+ <p><strong>A pushdown source is the exception, up to a point.</strong> It can fetch the whole matching set and run the function over it, so it does &mdash; while that set is under <code><a href="#pushdown-whererowlimit">whereRowLimit</a></code> (default 50,000 rows). At or past the limit, or when the adapter reports no row total, it <em>refuses</em>: the predicate is not applied, the rows it would exclude stay on screen, and a warning names the adapter and the way out. Honouring it past that point would silently turn a windowed grid into a whole-dataset download, which is the thing a pushdown source exists to avoid.</p>
6945
+ <p><strong>The twin is the route that always works.</strong> Give the predicate a <code>condition</code> twin &mdash; it is ANDed into the tree the source is sent, so the engine narrows the fetch itself, at any size, and the grid stays silent because that case genuinely works. That is the supported route on a server-delegated or pushdown source.</p>
6763
6946
  </div>
6764
6947
 
6765
6948
  <div class="note">
@@ -7282,7 +7465,7 @@ grid.destroy();
7282
7465
  <p class="section-note">Each documented event is subscribed to and unsubscribed on every build. A consumer
7283
7466
  wiring a handler to a renamed event gets silence, which is indistinguishable from an event that
7284
7467
  has not fired yet — so the name is checked rather than left to be discovered.</p>
7285
- <pre data-run="js" data-expect="114" data-covers="event:cell:mouseover event:cell:mouseout event:rowDrag:started event:rowDrag:moved event:rowDrag:left event:rowDrag:ended event:find:changed event:cell:changed event:cell:clicked event:cell:confirmed event:cell:conflict event:cell:contextmenu event:cell:dblclicked event:cell:edit:end event:cell:edit:start event:cell:pending event:cell:reverted event:clipboard:copy event:column:filter:open event:column:profile:open event:column:grouped event:column:menu:open event:column:pivoted event:column:resized event:columns:changed event:columns:tagged event:comment:added event:comment:deleted event:comment:edited event:comment:failed event:comment:indexLoaded event:comment:resolved event:comment:threadClosed event:comment:threadOpened event:comment:unresolved event:destroy event:detail:toggled event:diff:changed event:diff:swapped event:export:progress event:facet:computed event:facet:expanded event:facet:failed event:facet:filtered event:form:closed event:form:error event:form:opened event:form:saved event:formatting:changed event:group:toggled event:header:contextmenu event:highlight:changed event:history:applied event:history:changed event:licence:changed event:page:changed event:permissions:changed event:presence:failed event:presence:joined event:presence:left event:presence:lockRefused event:presence:published event:presence:updated event:presentation:captured event:presentation:changed event:presentation:ended event:presentation:scale event:presentation:spotlight event:presentation:started event:presentation:view event:range:changed event:ready event:redaction:changed event:render:done event:render:first event:row:clicked event:row:copied event:row:dblclicked event:row:edit:end event:row:edit:start event:row:moved event:row:received event:row:sent event:rows:deferred event:rows:paused event:rows:queued event:rows:resumed event:scroll event:scroll:end event:selection:changed event:size:changed event:source:error event:stream:chunk event:stream:end event:stream:evicted event:timeline:attached event:timeline:detached event:timeline:seek event:timeline:seeking event:toolpanel:focus event:tree:loadAborted event:tree:loadFailed event:tree:loaded event:tree:loading event:view:applied event:view:default event:view:removed event:view:renamed event:view:saved event:views:changed event:row:pending event:row:confirmed event:row:reverted event:row:conflict"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
7468
+ <pre data-run="js" data-expect="116" data-covers="event:cell:mouseover event:cell:mouseout event:cell:mousedown event:cell:mouseup event:rowDrag:started event:rowDrag:moved event:rowDrag:left event:rowDrag:ended event:find:changed event:cell:changed event:cell:clicked event:cell:confirmed event:cell:conflict event:cell:contextmenu event:cell:dblclicked event:cell:edit:end event:cell:edit:start event:cell:pending event:cell:reverted event:clipboard:copy event:column:filter:open event:column:profile:open event:column:grouped event:column:menu:open event:column:pivoted event:column:resized event:columns:changed event:columns:tagged event:comment:added event:comment:deleted event:comment:edited event:comment:failed event:comment:indexLoaded event:comment:resolved event:comment:threadClosed event:comment:threadOpened event:comment:unresolved event:destroy event:detail:toggled event:diff:changed event:diff:swapped event:export:progress event:facet:computed event:facet:expanded event:facet:failed event:facet:filtered event:form:closed event:form:error event:form:opened event:form:saved event:formatting:changed event:group:toggled event:header:contextmenu event:highlight:changed event:history:applied event:history:changed event:licence:changed event:page:changed event:permissions:changed event:presence:failed event:presence:joined event:presence:left event:presence:lockRefused event:presence:published event:presence:updated event:presentation:captured event:presentation:changed event:presentation:ended event:presentation:scale event:presentation:spotlight event:presentation:started event:presentation:view event:range:changed event:ready event:redaction:changed event:render:done event:render:first event:row:clicked event:row:copied event:row:dblclicked event:row:edit:end event:row:edit:start event:row:moved event:row:received event:row:sent event:rows:deferred event:rows:paused event:rows:queued event:rows:resumed event:scroll event:scroll:end event:selection:changed event:size:changed event:source:error event:stream:chunk event:stream:end event:stream:evicted event:timeline:attached event:timeline:detached event:timeline:seek event:timeline:seeking event:toolpanel:focus event:tree:loadAborted event:tree:loadFailed event:tree:loaded event:tree:loading event:view:applied event:view:default event:view:removed event:view:renamed event:view:saved event:views:changed event:row:pending event:row:confirmed event:row:reverted event:row:conflict"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
7286
7469
 
7287
7470
  <span class="cmt">// Every documented event name, checked against the bus that would carry it.</span>
7288
7471
  <span class="cmt">// Subscribing to a name the grid does not know is the failure this catches:</span>
@@ -7291,7 +7474,7 @@ grid.destroy();
7291
7474
  <span class="kw">const</span> documented = [
7292
7475
  'cell:changed', 'cell:clicked', 'cell:confirmed', 'cell:conflict',
7293
7476
  'cell:contextmenu', 'cell:dblclicked', 'cell:edit:end', 'cell:edit:start',
7294
- 'cell:mouseover', 'cell:mouseout',
7477
+ 'cell:mouseover', 'cell:mouseout', 'cell:mousedown', 'cell:mouseup',
7295
7478
  'cell:pending', 'cell:reverted', 'clipboard:copy', 'column:filter:open', 'column:profile:open', 'column:grouped',
7296
7479
  'column:menu:open', 'column:pivoted', 'column:resized', 'columns:changed',
7297
7480
  'columns:tagged', 'comment:added', 'comment:deleted', 'comment:edited',
@@ -7648,6 +7831,32 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
7648
7831
  </tbody>
7649
7832
  </table>
7650
7833
  </div>
7834
+ <h3 id="type-AI">AI</h3>
7835
+ <p class="section-note">An AI controller over a live grid. It explains the grid's computed figures (Play A), answers questions with validated read-only query specs (Play B), and PROPOSES governed edits a human approves and the grid's own gate applies (Play C). `grid.ai` (in core) is the complementary intent/plan skill layer this consumes.</p>
7836
+ <div class="table-wrap">
7837
+ <table>
7838
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7839
+ <tbody>
7840
+ <tr><td class="name">el</td><td class="type">HTMLElement | null</td><td class="desc">The mounted insights panel element, or null. <small>(read-only)</small></td></tr>
7841
+ <tr><td class="name">ready</td><td class="type">boolean</td><td class="desc">Whether a usable `ask()` is configured. <small>(read-only)</small></td></tr>
7842
+ <tr><td class="name">explain</td><td class="type">(target?: AITarget, opts?: object): Promise&lt;AINarrative&gt;</td><td class="desc">Produce a grounded, reconciled narrative for a target.</td></tr>
7843
+ <tr><td class="name">narrate</td><td class="type">(target?: AITarget, opts?: object): Promise&lt;AINarrative&gt;</td><td class="desc">An alias for {@link AI.explain}.</td></tr>
7844
+ <tr><td class="name">riskSummary</td><td class="type">(sources?: {</td><td class="desc">Produce a grounded, reconciled board / Gantt RISK SUMMARY (BACKLOG-0000979): a plain-language reading like "3 tasks at risk on the critical path, SPI 0.67, 2 SLA breaches". A convenience over `explain({ kind: 'risk', ... })`; the module sources go in `sources` (`gantt`, `board`/`sla`, or precomputed outputs). Every figure runs through the same reconciliation guard as {@link AI.explain}.</td></tr>
7845
+ <tr><td class="name">insights</td><td class="type">(el?: HTMLElement, opts?: object): AI</td><td class="desc">Mount (or re-target) the insights panel into an element.</td></tr>
7846
+ <tr><td class="name">attachExplain</td><td class="type">(target: AITarget, opts?: object): HTMLElement | null</td><td class="desc">Build an "Explain" button bound to a target.</td></tr>
7847
+ <tr><td class="name">facts</td><td class="type">(target?: AITarget, opts?: object): AIFactsPacket</td><td class="desc">Build the facts packet for a target without calling `ask()`.</td></tr>
7848
+ <tr><td class="name">query</td><td class="type">(question: string, opts?: {</td><td class="desc">Ask-your-data: turn a question into a validated, read-only query spec, run it in the engine, and (on apply) fan the answer to router-attached viewers. Returns a result the host reviews; `autoApply` applies a safe read for you.</td></tr>
7849
+ <tr><td class="name">applyQuery</td><td class="type">(result: AIQueryResult, opts?: { router?: unknown; onResult?: (rows: object[]) =&gt; void }): AIApplyReport</td><td class="desc">Apply a reviewed query result (the confirm path); re-gated at the seam.</td></tr>
7850
+ <tr><td class="name">askBar</td><td class="type">(el?: HTMLElement, opts?: object): AI</td><td class="desc">Mount the ask-your-data bar (input, Ask, auto-apply toggle, preview, Apply/Discard).</td></tr>
7851
+ <tr><td class="name">propose</td><td class="type">(instruction: string, opts?: {</td><td class="desc">Governed actor (Play C): ask the model for structured edit PROPOSALS over the current view, validate and resolve them (label -&gt; stored value, locate a named row, reject unknown columns/labels/out-of-range), and return a reviewable {@link AIProposal} with a before/after diff. NOTHING is written — the model proposes; a human approves.</td></tr>
7852
+ <tr><td class="name">applyProposal</td><td class="type">(result: AIProposal, opts?: { board?: unknown }): Promise&lt;AIProposalReport&gt;</td><td class="desc">Apply an approved proposal — the human-approval step. Writes ONLY through the gate: a grid cell edit via `grid.edit.setCells({ origin: 'ai' })` (the `beforeEdit` veto), a kanban move via `board.move({ origin: 'ai' })` (the `beforeMove` veto). A vetoing host handler stops the write.</td></tr>
7853
+ <tr><td class="name">actorBar</td><td class="type">(el?: HTMLElement, opts?: object): AI</td><td class="desc">Mount the governed-actor bar: an instruction input, Propose, a before/after diff preview stating the scope, and Approve/Discard. Approve applies through the gate.</td></tr>
7854
+ <tr><td class="name">on</td><td class="type">(name: 'narrative' | 'query' | 'proposal' | 'error' | string, fn: (payload: object) =&gt; void): () =&gt; void</td><td class="desc"></td></tr>
7855
+ <tr><td class="name">off</td><td class="type">(name: string, fn: (payload: object) =&gt; void): void</td><td class="desc"></td></tr>
7856
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc"></td></tr>
7857
+ </tbody>
7858
+ </table>
7859
+ </div>
7651
7860
  <h3 id="type-AiApi">AiApi</h3>
7652
7861
  <div class="table-wrap">
7653
7862
  <table>
@@ -7662,6 +7871,200 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
7662
7871
  </tbody>
7663
7872
  </table>
7664
7873
  </div>
7874
+ <h3 id="type-AIApplyReport">AIApplyReport</h3>
7875
+ <p class="section-note">The report from applying an ask-your-data query.</p>
7876
+ <div class="table-wrap">
7877
+ <table>
7878
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7879
+ <tbody>
7880
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc"></td></tr>
7881
+ <tr><td class="name">applied</td><td class="type">string[]</td><td class="desc">The action types that were applied.</td></tr>
7882
+ <tr><td class="name">failed</td><td class="type">Array&lt;{ type: string; reason: string }&gt;</td><td class="desc">Actions that threw while applying.</td></tr>
7883
+ <tr><td class="name">refused</td><td class="type">Array&lt;{ type: string; reason: string }&gt;</td><td class="desc">Actions refused by the read-only gate — a mutation is never applied.</td></tr>
7884
+ <tr><td class="name">fannedOut</td><td class="type">number</td><td class="desc">How many answer rows were fanned to a router's viewers.</td></tr>
7885
+ </tbody>
7886
+ </table>
7887
+ </div>
7888
+ <h3 id="type-AIConfig">AIConfig</h3>
7889
+ <p class="section-note">AI module configuration.</p>
7890
+ <div class="table-wrap">
7891
+ <table>
7892
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7893
+ <tbody>
7894
+ <tr><td class="name">ask</td><td class="type">AIAsk</td><td class="desc">The host's model callback. Falls back to the grid's `ai.ask` when omitted. <small>(optional)</small></td></tr>
7895
+ <tr><td class="name">enable</td><td class="type">string[]</td><td class="desc">Opt into specific features: `'narrative'`, `'insights'`, `'query'`/`'ask'`. All on when omitted. <small>(optional)</small></td></tr>
7896
+ <tr><td class="name">autoApply</td><td class="type">boolean</td><td class="desc">Ask-your-data: apply a safe (read-only) query result without a confirm step. Off by default — the resolved query is shown and waits for Apply. <small>(optional)</small></td></tr>
7897
+ <tr><td class="name">router</td><td class="type">unknown</td><td class="desc">A Data Router instance; on applying a query the answer rows are fanned to its attached viewers (grid + chart + KPI together) via `load()`. <small>(optional)</small></td></tr>
7898
+ <tr><td class="name">schemaOptions</td><td class="type">object</td><td class="desc">Budgets passed to the schema builder for ask-your-data. <small>(optional)</small></td></tr>
7899
+ <tr><td class="name">context</td><td class="type">unknown</td><td class="desc">Extra context passed through to `ask()`. <small>(optional)</small></td></tr>
7900
+ <tr><td class="name">onQuery</td><td class="type">(result: AIQueryResult) =&gt; void</td><td class="desc">Called with each ask-your-data result. <small>(optional)</small></td></tr>
7901
+ <tr><td class="name">onProposal</td><td class="type">(result: AIProposal) =&gt; void</td><td class="desc">Called with each governed-actor proposal (Play C), before any approval. <small>(optional)</small></td></tr>
7902
+ <tr><td class="name">board</td><td class="type">unknown</td><td class="desc">A Kanban board (from `createKanban`) the governed actor writes moves through: an NL card move applies via the board's own `beforeMove` gate (BACKLOG-0000967), never a kanban-specific write bypass. <small>(optional)</small></td></tr>
7903
+ <tr><td class="name">maxRows</td><td class="type">number</td><td class="desc">Cap on rows any tool result carries to `ask()`. <small>(optional)</small></td></tr>
7904
+ <tr><td class="name">redact</td><td class="type">string | string[] | ((colId: string) =&gt; boolean)</td><td class="desc">Columns whose values must never leave the browser. <small>(optional)</small></td></tr>
7905
+ <tr><td class="name">tools</td><td class="type">boolean</td><td class="desc">Force tool-use on or off; auto-detected from how `ask` was supplied otherwise. <small>(optional)</small></td></tr>
7906
+ <tr><td class="name">locale</td><td class="type">string</td><td class="desc">Locale for figure formatting. <small>(optional)</small></td></tr>
7907
+ <tr><td class="name">maxColumns</td><td class="type">number</td><td class="desc">Column cap for a view summary. <small>(optional)</small></td></tr>
7908
+ <tr><td class="name">reconcile</td><td class="type">'strip' | 'flag'</td><td class="desc">What to do with an ungrounded figure: `'strip'` (default) or `'flag'`. <small>(optional)</small></td></tr>
7909
+ <tr><td class="name">element</td><td class="type">HTMLElement</td><td class="desc">An element to mount the insights panel into. <small>(optional)</small></td></tr>
7910
+ <tr><td class="name">onNarrative</td><td class="type">(result: AINarrative) =&gt; void</td><td class="desc">Called when a narrative is produced. <small>(optional)</small></td></tr>
7911
+ <tr><td class="name">onError</td><td class="type">(error: { error: unknown; target: AITarget }) =&gt; void</td><td class="desc">Called when `ask()` errors; the grid stays usable. <small>(optional)</small></td></tr>
7912
+ </tbody>
7913
+ </table>
7914
+ </div>
7915
+ <h3 id="type-AIDiffEntry">AIDiffEntry</h3>
7916
+ <p class="section-note">One before/after change in a governed-actor proposal (BACKLOG-0000967).</p>
7917
+ <div class="table-wrap">
7918
+ <table>
7919
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7920
+ <tbody>
7921
+ <tr><td class="name">key</td><td class="type">string</td><td class="desc">The target row key.</td></tr>
7922
+ <tr><td class="name">rowLabel</td><td class="type">string</td><td class="desc">A human label identifying the row (a name-like column, else the key).</td></tr>
7923
+ <tr><td class="name">colId</td><td class="type">string</td><td class="desc">The target column id.</td></tr>
7924
+ <tr><td class="name">colTitle</td><td class="type">string</td><td class="desc">The column's title, for the diff header.</td></tr>
7925
+ <tr><td class="name">oldValue</td><td class="type">unknown</td><td class="desc">The current stored value.</td></tr>
7926
+ <tr><td class="name">oldDisplay</td><td class="type">string</td><td class="desc">The current value as shown (a lookup id mapped to its label).</td></tr>
7927
+ <tr><td class="name">newValue</td><td class="type">unknown</td><td class="desc">The proposed stored value (a label resolved to its option id).</td></tr>
7928
+ <tr><td class="name">newDisplay</td><td class="type">string</td><td class="desc">The proposed value as shown.</td></tr>
7929
+ </tbody>
7930
+ </table>
7931
+ </div>
7932
+ <h3 id="type-AIFact">AIFact</h3>
7933
+ <p class="section-note">A single computed figure a narrative is grounded on.</p>
7934
+ <div class="table-wrap">
7935
+ <table>
7936
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7937
+ <tbody>
7938
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
7939
+ <tr><td class="name">label</td><td class="type">string</td><td class="desc"></td></tr>
7940
+ <tr><td class="name">value</td><td class="type">number | null</td><td class="desc">The raw numeric value, or null for a context-only fact.</td></tr>
7941
+ <tr><td class="name">display</td><td class="type">string</td><td class="desc">The pre-formatted display string the model is told to use verbatim.</td></tr>
7942
+ <tr><td class="name">kind</td><td class="type">string</td><td class="desc"></td></tr>
7943
+ <tr><td class="name">colId</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
7944
+ </tbody>
7945
+ </table>
7946
+ </div>
7947
+ <h3 id="type-AIFactsPacket">AIFactsPacket</h3>
7948
+ <p class="section-note">The facts packet a narrative grounds on.</p>
7949
+ <div class="table-wrap">
7950
+ <table>
7951
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7952
+ <tbody>
7953
+ <tr><td class="name">target</td><td class="type">AITarget</td><td class="desc"></td></tr>
7954
+ <tr><td class="name">facts</td><td class="type">AIFact[]</td><td class="desc"></td></tr>
7955
+ <tr><td class="name">groundedValues</td><td class="type">number[]</td><td class="desc">The numeric values seeding the reconciliation registry.</td></tr>
7956
+ <tr><td class="name">meta</td><td class="type">{</td><td class="desc"></td></tr>
7957
+ </tbody>
7958
+ </table>
7959
+ </div>
7960
+ <h3 id="type-AINarrative">AINarrative</h3>
7961
+ <p class="section-note">The result of a narrative: reconciled prose plus what grounded and what did not.</p>
7962
+ <div class="table-wrap">
7963
+ <table>
7964
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7965
+ <tbody>
7966
+ <tr><td class="name">text</td><td class="type">string</td><td class="desc">The narrative, with every ungrounded figure stripped (or flagged).</td></tr>
7967
+ <tr><td class="name">facts</td><td class="type">AIFact[]</td><td class="desc"></td></tr>
7968
+ <tr><td class="name">grounded</td><td class="type">string[]</td><td class="desc">The figures that reconciled against a computed value.</td></tr>
7969
+ <tr><td class="name">flagged</td><td class="type">string[]</td><td class="desc">The figures removed as ungrounded.</td></tr>
7970
+ <tr><td class="name">packet</td><td class="type">AIFactsPacket</td><td class="desc"></td></tr>
7971
+ <tr><td class="name">rounds</td><td class="type">number</td><td class="desc">How many ask() rounds ran (&gt;1 only on the tool-use path).</td></tr>
7972
+ <tr><td class="name">mode</td><td class="type">'tools' | 'packet'</td><td class="desc"></td></tr>
7973
+ </tbody>
7974
+ </table>
7975
+ </div>
7976
+ <h3 id="type-AIProposal">AIProposal</h3>
7977
+ <p class="section-note">A governed-actor proposal (Play C, BACKLOG-0000967): the model's structured edits, VALIDATED and resolved against the current view — never written until a human approves. `apply()` writes ONLY through the grid's own gate.</p>
7978
+ <div class="table-wrap">
7979
+ <table>
7980
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7981
+ <tbody>
7982
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc">True when there is at least one applicable change and nothing needs a pick first.</td></tr>
7983
+ <tr><td class="name">instruction</td><td class="type">string</td><td class="desc">The user's instruction.</td></tr>
7984
+ <tr><td class="name">scope</td><td class="type">'view' | 'all'</td><td class="desc">`'view'` (the filtered set, the default) or `'all'` (an opted-in widen).</td></tr>
7985
+ <tr><td class="name">scopeCount</td><td class="type">number</td><td class="desc">How many rows the scope covers.</td></tr>
7986
+ <tr><td class="name">scopeText</td><td class="type">string</td><td class="desc">The scope in words, always stated in the confirm/diff.</td></tr>
7987
+ <tr><td class="name">bulk</td><td class="type">boolean</td><td class="desc">Whether any proposal was a bulk (`scope:'view'`) edit.</td></tr>
7988
+ <tr><td class="name">diff</td><td class="type">AIDiffEntry[]</td><td class="desc">The before/after diff — exactly what would change. Nothing is written yet.</td></tr>
7989
+ <tr><td class="name">rejected</td><td class="type">Array&lt;{ reason: string; [k: string]: unknown }&gt;</td><td class="desc">Proposals refused before apply (unknown column, unknown label, bad type/range, no match).</td></tr>
7990
+ <tr><td class="name">ambiguous</td><td class="type">Array&lt;{ reason: string; candidates: Array&lt;{ key: string; label: string }&gt;; [k: string]: unknown }&gt;</td><td class="desc">Matches needing a human pick (&gt;1 row for one phrase), with candidates.</td></tr>
7991
+ <tr><td class="name">outOfView</td><td class="type">Array&lt;{ reason: string; candidates: Array&lt;{ key: string; label: string }&gt;; [k: string]: unknown }&gt;</td><td class="desc">Named targets found only outside the view, offered for an opt-in widen.</td></tr>
7992
+ <tr><td class="name">noops</td><td class="type">Array&lt;{ reason: string; [k: string]: unknown }&gt;</td><td class="desc">Matches whose value already equals the ask (nothing to change).</td></tr>
7993
+ <tr><td class="name">applied</td><td class="type">AIProposalReport | null</td><td class="desc">The apply report once applied, or null.</td></tr>
7994
+ <tr><td class="name">describe</td><td class="type">(): string</td><td class="desc">The proposal in one human sentence, always stating the scope.</td></tr>
7995
+ <tr><td class="name">apply</td><td class="type">(opts?: { board?: unknown }): Promise&lt;AIProposalReport&gt;</td><td class="desc">Apply the approved diff through the gate (`beforeEdit`, or `beforeMove` for a board).</td></tr>
7996
+ </tbody>
7997
+ </table>
7998
+ </div>
7999
+ <h3 id="type-AIProposalReport">AIProposalReport</h3>
8000
+ <p class="section-note">The report from applying a governed-actor proposal.</p>
8001
+ <div class="table-wrap">
8002
+ <table>
8003
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8004
+ <tbody>
8005
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc">True when at least one edit landed.</td></tr>
8006
+ <tr><td class="name">applied</td><td class="type">number</td><td class="desc">How many edits landed through the gate.</td></tr>
8007
+ <tr><td class="name">requested</td><td class="type">number</td><td class="desc">How many edits were attempted.</td></tr>
8008
+ <tr><td class="name">vetoed</td><td class="type">number</td><td class="desc">How many were stopped by a before-handler veto.</td></tr>
8009
+ <tr><td class="name">via</td><td class="type">string</td><td class="desc">Which gated path applied them: `'setCells'`, `'board.move'`, or `'none'`.</td></tr>
8010
+ </tbody>
8011
+ </table>
8012
+ </div>
8013
+ <h3 id="type-AIQueryResult">AIQueryResult</h3>
8014
+ <p class="section-note">The result of an ask-your-data question (BACKLOG-0000966): a validated, READ-ONLY query spec — never rows — that the host reviews before applying.</p>
8015
+ <div class="table-wrap">
8016
+ <table>
8017
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8018
+ <tbody>
8019
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc">True when the spec is safe to apply: at least one read, nothing unsafe.</td></tr>
8020
+ <tr><td class="name">question</td><td class="type">string</td><td class="desc">The user's question.</td></tr>
8021
+ <tr><td class="name">plan</td><td class="type">Record&lt;string, unknown&gt;</td><td class="desc">The core plan (from `grid.ai.plan`).</td></tr>
8022
+ <tr><td class="name">actions</td><td class="type">object[]</td><td class="desc">The read-only actions that will run — the validated query spec.</td></tr>
8023
+ <tr><td class="name">unsafe</td><td class="type">Array&lt;{ type: string; reason: string }&gt;</td><td class="desc">Actions refused as not read-only (a mutation the model asked for).</td></tr>
8024
+ <tr><td class="name">rejected</td><td class="type">Array&lt;{ at: string; what: string; reason: string }&gt;</td><td class="desc">Parts the core validator dropped (unknown column, bad operator, …).</td></tr>
8025
+ <tr><td class="name">explain</td><td class="type">string</td><td class="desc">The model's own one-line summary, if any.</td></tr>
8026
+ <tr><td class="name">spec</td><td class="type">{ actions: object[] }</td><td class="desc">The validated query spec as data.</td></tr>
8027
+ <tr><td class="name">applied</td><td class="type">AIApplyReport | null</td><td class="desc">The apply report once applied, or null.</td></tr>
8028
+ <tr><td class="name">describe</td><td class="type">(): string</td><td class="desc">The resolved query in one human sentence, from the validated spec.</td></tr>
8029
+ <tr><td class="name">apply</td><td class="type">(opts?: { router?: unknown; onResult?: (rows: object[]) =&gt; void }): AIApplyReport</td><td class="desc">Apply the query (re-gated), fanning the answer to a router if configured.</td></tr>
8030
+ </tbody>
8031
+ </table>
8032
+ </div>
8033
+ <h3 id="type-AIRiskFacts">AIRiskFacts</h3>
8034
+ <p class="section-note">The risk facts a board / Gantt risk summary grounds on (BACKLOG-0000979), from {@link buildRiskFacts}: the facts plus which module sources resolved and which opt-in exposures (task names, cost) were honoured.</p>
8035
+ <div class="table-wrap">
8036
+ <table>
8037
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8038
+ <tbody>
8039
+ <tr><td class="name">facts</td><td class="type">AIFact[]</td><td class="desc"></td></tr>
8040
+ <tr><td class="name">meta</td><td class="type">{</td><td class="desc"></td></tr>
8041
+ </tbody>
8042
+ </table>
8043
+ </div>
8044
+ <h3 id="type-AITarget">AITarget</h3>
8045
+ <p class="section-note">A narrative target. `view` narrates the current filtered view; `column` narrates one column's profile; `forecast` adds its projection; `kpi`/`chart` narrate figures the caller passes through in `facts`; `risk` assembles a project RISK SUMMARY from the separate Gantt / Kanban modules' public outputs (BACKLOG-0000979).</p>
8046
+ <div class="table-wrap">
8047
+ <table>
8048
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8049
+ <tbody>
8050
+ <tr><td class="name">kind</td><td class="type">'view' | 'column' | 'forecast' | 'kpi' | 'chart' | 'risk'</td><td class="desc"><small>(optional)</small></td></tr>
8051
+ <tr><td class="name">colId</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
8052
+ <tr><td class="name">options</td><td class="type">object</td><td class="desc">Forecast options, for `kind: 'forecast'`. <small>(optional)</small></td></tr>
8053
+ <tr><td class="name">facts</td><td class="type">Array&lt;{ id?: string; label: string; value: unknown; display?: string; kind?: string; colId?: string }&gt;</td><td class="desc">Caller-supplied figures for a KPI/chart Explain, grounded like the rest. <small>(optional)</small></td></tr>
8054
+ <tr><td class="name">gantt</td><td class="type">unknown</td><td class="desc">For `kind: 'risk'`: a Gantt instance (from `createGantt`). Read duck-typed for `earnedValue()` (SPI/CPI/variances) and `schedule` (critical path, float). The AI bundle never imports the Gantt module. <small>(optional)</small></td></tr>
8055
+ <tr><td class="name">board</td><td class="type">unknown</td><td class="desc">For `kind: 'risk'`: a Kanban board (from `createKanban`). Read for its `board.sla` monitor (breach / warning counts). The AI bundle never imports the Kanban module. <small>(optional)</small></td></tr>
8056
+ <tr><td class="name">sla</td><td class="type">unknown</td><td class="desc">For `kind: 'risk'`: an SLA monitor, if not reached through `board`. <small>(optional)</small></td></tr>
8057
+ <tr><td class="name">earnedValue</td><td class="type">object</td><td class="desc">For `kind: 'risk'`: a precomputed `gantt.earnedValue()` result. <small>(optional)</small></td></tr>
8058
+ <tr><td class="name">schedule</td><td class="type">object</td><td class="desc">For `kind: 'risk'`: a precomputed `gantt.schedule` result. <small>(optional)</small></td></tr>
8059
+ <tr><td class="name">breaches</td><td class="type">object[]</td><td class="desc">For `kind: 'risk'`: precomputed SLA breach states. <small>(optional)</small></td></tr>
8060
+ <tr><td class="name">warnings</td><td class="type">object[]</td><td class="desc">For `kind: 'risk'`: precomputed SLA warning states. <small>(optional)</small></td></tr>
8061
+ <tr><td class="name">evmOptions</td><td class="type">object</td><td class="desc">For `kind: 'risk'`: options passed to `gantt.earnedValue()`. <small>(optional)</small></td></tr>
8062
+ <tr><td class="name">includeTaskNames</td><td class="type">boolean</td><td class="desc">For `kind: 'risk'`: expose the at-risk task NAMES (off by default — a risk summary carries aggregates only unless the host opts in). <small>(optional)</small></td></tr>
8063
+ <tr><td class="name">includeCost</td><td class="type">boolean</td><td class="desc">For `kind: 'risk'`: expose the money figures BAC/PV/EV/AC (off by default). <small>(optional)</small></td></tr>
8064
+ <tr><td class="name">maxTasks</td><td class="type">number</td><td class="desc">For `kind: 'risk'`: cap on named at-risk tasks (default 10). <small>(optional)</small></td></tr>
8065
+ </tbody>
8066
+ </table>
8067
+ </div>
7665
8068
  <h3 id="type-AnnotationApi">AnnotationApi</h3>
7666
8069
  <div class="table-wrap">
7667
8070
  <table>
@@ -8049,6 +8452,19 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
8049
8452
  </tbody>
8050
8453
  </table>
8051
8454
  </div>
8455
+ <h3 id="type-ChartTypeDefinition">ChartTypeDefinition</h3>
8456
+ <p class="section-note">The definition an extension chart type registers (BACKLOG-0000886). `draw` receives the base drawing context — `plot`, `bound`, `groups`, `scheme`, `typography`, `fontSize`, `labels`, `grid`, `spec`, `doc` — plus `ctx.helpers`, the base's own toolkit of primitives (element factory, scales, axes, mark pool, distribution kernels), and appends its marks to the layer groups. `bind` optionally supplies the bound data (default: the by-series binder); `freeform` lays the chart out without axis gutters; `labelled` declares that `labels` applies.</p>
8457
+ <div class="table-wrap">
8458
+ <table>
8459
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8460
+ <tbody>
8461
+ <tr><td class="name">draw</td><td class="type">(ctx: object) =&gt; object</td><td class="desc"></td></tr>
8462
+ <tr><td class="name">bind</td><td class="type">(grid: Grid, spec: ChartSpec) =&gt; object</td><td class="desc"><small>(optional)</small></td></tr>
8463
+ <tr><td class="name">freeform</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
8464
+ <tr><td class="name">labelled</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
8465
+ </tbody>
8466
+ </table>
8467
+ </div>
8052
8468
  <h3 id="type-Chunk">Chunk</h3>
8053
8469
  <div class="table-wrap">
8054
8470
  <table>
@@ -8103,6 +8519,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
8103
8519
  <tr><td class="name">contextMenu</td><td class="type">boolean | MenuItem[] | ((p: CellMenuParams, defaults: MenuItem[]) =&gt; MenuItem[] | void)</td><td class="desc">The cell right-click menu for this column alone (BACKLOG-0001068), in the same shapes the grid-level `contextMenu` takes plus a bare array for the common "just these items here" case. Declared where the column is declared rather than as another branch inside one grid-level callback: the menu logic for a column belongs beside the column it belongs to. It does not replace the grid-level menu — the three levels compose as a chain, built-in defaults then grid-level then this one, each handed the previous result as its `defaults`, so a column adding one item does not have to restate Paste, Clear and Fill down. `false` suppresses the menu on this column and leaves every other column alone: what a sensitive or read-only column wants. The more specific level wins, so a column may also declare a menu on a grid whose `contextMenu` is `false`. <small>(optional)</small></td></tr>
8104
8520
  <tr><td class="name">headerControls</td><td class="type">'hover' | 'always' | 'hidden'</td><td class="desc">When this column's header controls — its sort arrow, filter funnel and menu button — are shown, overriding the grid-level `headerControls` default for this column alone (BACKLOG-0000982). `'hover'` reveals them on hover or focus, `'always'` keeps them visible, `'hidden'` draws none of them and leaves them out of the tab order. Omitted, the column follows the grid default, which is itself `'hover'`. <small>(optional)</small></td></tr>
8105
8521
  <tr><td class="name">verticalAlign</td><td class="type">VAlign</td><td class="desc">Vertical alignment of this column's cell content within the row (BACKLOG-0000989). Overrides the grid-level `verticalAlign` for this column alone; `top`, `middle` or `bottom`. Also accepted as `cell.verticalAlign`, the way `align` is. Omitted, the column follows the grid default. <small>(optional)</small></td></tr>
8522
+ <tr><td class="name">showWhen</td><td class="type">'open' | 'closed' | 'always'</td><td class="desc">When this leaf column is shown, the same union `ColumnGroup` declares (BACKLOG-0001279). A leaf reads its own `showWhen` exactly as a group reads its own — `open`/`closed` tie the leaf to an ancestor group's collapsed state, `always` (the default) shows it regardless — so tying a leaf's visibility to a group's open/closed state does not require wrapping it in a `ColumnGroup` of its own just to hold this setting; a wrapper is for grouping columns, not for this. <small>(optional)</small></td></tr>
8106
8523
  <tr><td class="name">export</td><td class="type">ColumnExportSpec</td><td class="desc">How the column leaves the grid, where that differs from how it is shown. <small>(optional)</small></td></tr>
8107
8524
  <tr><td class="name">allowGroup</td><td class="type">boolean</td><td class="desc">Whether the user may group by this column from the interface. <small>(optional)</small></td></tr>
8108
8525
  <tr><td class="name">allowPivot</td><td class="type">boolean</td><td class="desc">Whether the user may pivot on it. <small>(optional)</small></td></tr>
@@ -8641,6 +9058,24 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
8641
9058
  </tbody>
8642
9059
  </table>
8643
9060
  </div>
9061
+ <h3 id="type-DataRouter">DataRouter</h3>
9062
+ <p class="section-note">A data router: one arriving stream, partitioned by a property (or composite predicate), fanned out to a grid per partition (BACKLOG-0000879). Each grid sees only its slice, updated by keyed diff through the public `grid.rows.apply` path — no grid-core change, no cross-references between grids. Snapshots apply keyed diffs (unchanged rows never repaint); deltas add, update or remove in place by `rowKey`, preserving selection and scroll.</p>
9063
+ <div class="table-wrap">
9064
+ <table>
9065
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9066
+ <tbody>
9067
+ <tr><td class="name">attach</td><td class="type">(grid: unknown, predicate: RoutePredicate, opts?: RouteOptions): DataRouter</td><td class="desc">Attach a grid behind a predicate; `opts` may reshape/filter/sort the route (v3).</td></tr>
9068
+ <tr><td class="name">attachDefault</td><td class="type">(grid: unknown, opts?: RouteOptions): DataRouter</td><td class="desc">Attach the "rest" sink for records no explicit route matched.</td></tr>
9069
+ <tr><td class="name">detach</td><td class="type">(grid: unknown): DataRouter</td><td class="desc">Detach a grid; the host still owns and destroys it.</td></tr>
9070
+ <tr><td class="name">load</td><td class="type">(snapshot: RouterRecord[]): RouteDiff[]</td><td class="desc">Apply a full snapshot as a keyed diff per grid; returns per-route counts.</td></tr>
9071
+ <tr><td class="name">apply</td><td class="type">(deltas: { op: 'upsert' | 'delete'; row: RouterRecord }[]): void</td><td class="desc">Apply incremental deltas, routed and applied in place by `rowKey`.</td></tr>
9072
+ <tr><td class="name">link</td><td class="type">(source: unknown, target: unknown, relation: SelectionRelation): DataRouter</td><td class="desc">Link a source grid's selection to what a target grid receives (v2, BACKLOG-0000880): the target shows the subset of its partition the `relation` admits, re-pushed through the keyed-diff path. No selection shows the full partition; changes are debounced.</td></tr>
9073
+ <tr><td class="name">flush</td><td class="type">(): DataRouter</td><td class="desc">Apply any debounced selection refilter synchronously (for tests/determinism).</td></tr>
9074
+ <tr><td class="name">unrouted</td><td class="type">number</td><td class="desc">How many records matched no route. <small>(read-only)</small></td></tr>
9075
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc">Detach every grid and drop every link (the host destroys the grids themselves).</td></tr>
9076
+ </tbody>
9077
+ </table>
9078
+ </div>
8644
9079
  <h3 id="type-DatasetColumnDifference">DatasetColumnDifference</h3>
8645
9080
  <div class="table-wrap">
8646
9081
  <table>
@@ -9141,6 +9576,18 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9141
9576
  </tbody>
9142
9577
  </table>
9143
9578
  </div>
9579
+ <h3 id="type-FeedMessage">FeedMessage</h3>
9580
+ <p class="section-note">A message on the wire. A snapshot carries the full opening set; a delta carries the changes since. The reader parses `event.data` and switches on `kind`, exactly as against a real feed that framed its messages the same way.</p>
9581
+ <div class="table-wrap">
9582
+ <table>
9583
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9584
+ <tbody>
9585
+ <tr><td class="name">kind</td><td class="type">'snapshot' | 'delta'</td><td class="desc"></td></tr>
9586
+ <tr><td class="name">rows</td><td class="type">FeedRow[]</td><td class="desc">Present on a snapshot: the full opening set of rows. <small>(optional)</small></td></tr>
9587
+ <tr><td class="name">changes</td><td class="type">FeedChange[]</td><td class="desc">Present on a delta: the changes to apply. <small>(optional)</small></td></tr>
9588
+ </tbody>
9589
+ </table>
9590
+ </div>
9144
9591
  <h3 id="type-Filter">Filter</h3>
9145
9592
  <div class="table-wrap">
9146
9593
  <table>
@@ -9396,94 +9843,362 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9396
9843
  </tbody>
9397
9844
  </table>
9398
9845
  </div>
9399
- <h3 id="type-Grid">Grid</h3>
9846
+ <h3 id="type-Gantt">Gantt</h3>
9847
+ <p class="section-note">A headless Gantt controller: holds the model, recomputes on edits, emits changes.</p>
9848
+ <div class="table-wrap">
9849
+ <table>
9850
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9851
+ <tbody>
9852
+ <tr><td class="name">tasks</td><td class="type">GanttTask[]</td><td class="desc"><small>(read-only)</small></td></tr>
9853
+ <tr><td class="name">dependencies</td><td class="type">GanttDependency[]</td><td class="desc"><small>(read-only)</small></td></tr>
9854
+ <tr><td class="name">schedule</td><td class="type">GanttSchedule | null</td><td class="desc"><small>(read-only)</small></td></tr>
9855
+ <tr><td class="name">critical</td><td class="type">string[]</td><td class="desc"><small>(read-only)</small></td></tr>
9856
+ <tr><td class="name">conflicts</td><td class="type">GanttConflict[]</td><td class="desc">Constraints the latest schedule could not honour (empty when all are satisfied). <small>(read-only)</small></td></tr>
9857
+ <tr><td class="name">autoSchedule</td><td class="type">boolean</td><td class="desc"><small>(read-only)</small></td></tr>
9858
+ <tr><td class="name">grid</td><td class="type">unknown</td><td class="desc"><small>(read-only)</small></td></tr>
9859
+ <tr><td class="name">overAllocations</td><td class="type">GanttOverAllocation[]</td><td class="desc">The over-allocations from the latest schedule (BACKLOG-0000948). <small>(read-only)</small></td></tr>
9860
+ <tr><td class="name">resourceLoad</td><td class="type">GanttResourceLoad | null</td><td class="desc">The latest resource-load report, or null before a successful schedule (BACKLOG-0000948). <small>(read-only)</small></td></tr>
9861
+ <tr><td class="name">setTasks</td><td class="type">(tasks: GanttTask[]): GanttSchedule</td><td class="desc"></td></tr>
9862
+ <tr><td class="name">setDependencies</td><td class="type">(deps: GanttDependency[]): GanttSchedule</td><td class="desc"></td></tr>
9863
+ <tr><td class="name">applyEdit</td><td class="type">(patch: { id: string | number; start?: number; end?: number; duration?: number; percentComplete?: number; work?: number | Array&lt;{ date: number | string | Date; hours: number }&gt; }, editOpts?: { writeBack?: boolean }): GanttSchedule</td><td class="desc">Apply one task edit and recompute — the single gated choke point every drag, keypress, table cell and workload cell commits through. A `work` ARRAY is the task's per-day contour (BACKLOG-0001282). Given without an explicit `start`/`end`/`duration` it SETS the span: the task starts on the contour's first day and runs through its last, so booking hours beyond the bar extends it and clearing an edge bucket pulls it back. Conversely, a `start` or `duration` in the patch re-times an existing contour rather than discarding it — a move keeps its shape, a resize stretches it across the new span at the same daily levels.</td></tr>
9864
+ <tr><td class="name">compute</td><td class="type">(): GanttSchedule</td><td class="desc"></td></tr>
9865
+ <tr><td class="name">findViolations</td><td class="type">(): GanttViolation[]</td><td class="desc"></td></tr>
9866
+ <tr><td class="name">resources</td><td class="type">(loadOpts?: { resources?: GanttResourceSpec; defaultCapacity?: number }): GanttResourceLoad</td><td class="desc">Compute the resource load and over-allocations on demand (BACKLOG-0000948), optionally overriding the capacities for this call.</td></tr>
9867
+ <tr><td class="name">level</td><td class="type">(levelOpts?: {</td><td class="desc">Resolve resource over-allocation by shifting tasks later — resource leveling (BACKLOG-0000948). Honours the CPM dependencies and the working-time calendar. Mutates the model unless `{ dryRun: true }`; with `{ writeBack: true }` and a bound grid the moved tasks are pushed through the grid's edit surface.</td></tr>
9868
+ <tr><td class="name">toCSV</td><td class="type">(csvOpts?: { dates?: boolean }): string</td><td class="desc">Export the scheduled tasks as CSV; `{ dates: true }` writes ISO dates.</td></tr>
9869
+ <tr><td class="name">toMSPDI</td><td class="type">(xmlOpts?: { hoursPerDay?: number; projectName?: string }): string</td><td class="desc">Export the current plan as Microsoft Project (MSPDI) XML (BACKLOG-0000950): tasks, dependencies, constraints, baseline, resources and assignments, plus the working-time calendar, serialised with the computed schedule.</td></tr>
9870
+ <tr><td class="name">rows</td><td class="type">{</td><td class="desc">The live consumer surface, mirroring `grid.rows.apply`, so a Data Router can drive the Gantt like any other view. Keyed by the controller's rowKey. <small>(read-only)</small></td></tr>
9871
+ <tr><td class="name">on</td><td class="type">(event: 'schedule' | 'error', fn: (payload: unknown) =&gt; void): () =&gt; void</td><td class="desc"></td></tr>
9872
+ <tr><td class="name">off</td><td class="type">(event: 'schedule' | 'error', fn: (payload: unknown) =&gt; void): void</td><td class="desc"></td></tr>
9873
+ <tr><td class="name">mount</td><td class="type">(container: unknown, options?: {</td><td class="desc">Render the plan into a container as an SVG timeline (bars, dependency arrows, critical-path highlight, today line, non-working shading, milestones, progress). The view redraws when the schedule recomputes.</td></tr>
9874
+ <tr><td class="name">mountSplit</td><td class="type">(container: unknown, options?: {</td><td class="desc">Mount the JOINED split view (BACKLOG-0000938): one continuous, row-aligned surface with a left task-grid panel — by default the Task Name tree with expand/collapse, start, finish, duration, assignee avatars and a circular % ring (BACKLOG-0001285), plus any host columns — and the right timeline, sharing a single vertical scroll so every grid row lines up exactly with its bar row. The timeline scrolls horizontally on its own. Composes the controller's schedule; makes no change to grid core. The plan is editable from BOTH panes (BACKLOG-0001280): every gesture `mount` has — pointer drag to move, drag on the right edge to resize, arrow-key move, Shift+arrow resize, `l` to link, Delete — works on the timeline here, and a `start`/`end`/`duration`/`progress`/`name` column in the left panel is inline-editable on a double-click. Both routes commit through the same `applyEdit` choke point, so `beforeTaskMove`, `beforeTaskResize`, `beforeProgressChange` and `beforeTaskEdit` stay the single veto whichever pane the edit came from. The three switches that govern it carry the same meaning and the same defaults as `mount`'s: `editable` (default true) turns every edit on or off, both panes at once; `keyboard` (default true) turns off the focusable bars, the arrow-key gestures and the ARIA announcements while leaving pointer editing alone; and `resizeZone` (default 6) is how many pixels in from a bar's right edge begin a resize rather than a move. `workload` adds the resource band beneath the plan (BACKLOG-0001281), which is display-only — it reports hours, it does not accept them.</td></tr>
9875
+ <tr><td class="name">captureBaseline</td><td class="type">(): Array&lt;{ id: string; baselineStart: number; baselineEnd: number; baselineDuration: number }&gt;</td><td class="desc">Capture a baseline (planned) snapshot of the current schedule as HOST data (this does not mutate the tasks). Store it and feed it back as `baselineStart`/`baselineEnd` task fields to get variance and ghost bars.</td></tr>
9876
+ <tr><td class="name">earnedValue</td><td class="type">(evmOpts?: { statusDate?: number | string | Date; costField?: string; actualCostField?: string }): GanttEarnedValue</td><td class="desc">Compute earned-value (EVM) metrics for the current plan at a status date (BACKLOG-0000958): PV/EV/AC and the derived SV/CV/SPI/CPI per task, rolled up to summaries and the project. Budget (BAC) is the task's `cost`, or its duration when no cost is given; AC comes from `actualCost`.</td></tr>
9877
+ <tr><td class="name">unmount</td><td class="type">(): void</td><td class="desc">Detach the mounted view, if any. The host still owns the container.</td></tr>
9878
+ <tr><td class="name">view</td><td class="type">unknown</td><td class="desc">The mounted view, or null. <small>(read-only)</small></td></tr>
9879
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc"></td></tr>
9880
+ </tbody>
9881
+ </table>
9882
+ </div>
9883
+ <h3 id="type-GanttConflict">GanttConflict</h3>
9884
+ <p class="section-note">An unhonourable scheduling constraint, reported rather than obeyed.</p>
9400
9885
  <div class="table-wrap">
9401
9886
  <table>
9402
9887
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9403
9888
  <tbody>
9404
- <tr><td class="name">rows</td><td class="type">RowsApi</td><td class="desc">The data: reading it, changing it, walking it. <small>(read-only)</small></td></tr>
9405
- <tr><td class="name">columns</td><td class="type">ColumnsApi</td><td class="desc">The columns: order, width, visibility, grouping and pivoting. <small>(read-only)</small></td></tr>
9406
- <tr><td class="name">selection</td><td class="type">SelectionApi</td><td class="desc">What is selected, and the range the user has marked. <small>(read-only)</small></td></tr>
9407
- <tr><td class="name">filters</td><td class="type">FiltersApi</td><td class="desc">The filter tree, however it was set. <small>(read-only)</small></td></tr>
9408
- <tr><td class="name">sort</td><td class="type">SortApi</td><td class="desc">The sort, in priority order. <small>(read-only)</small></td></tr>
9409
- <tr><td class="name">edit</td><td class="type">EditApi</td><td class="desc">Editing sessions: starting, committing and cancelling them. <small>(read-only)</small></td></tr>
9410
- <tr><td class="name">scroll</td><td class="type">ScrollApi</td><td class="desc">Where the viewport is, and moving it. <small>(read-only)</small></td></tr>
9411
- <tr><td class="name">export</td><td class="type">ExportApi</td><td class="desc">CSV, Excel and clipboard. <small>(read-only)</small></td></tr>
9412
- <tr><td class="name">import</td><td class="type">ImportApi</td><td class="desc">Bringing rows in from CSV/TSV text, a file, the clipboard or a drop. <small>(read-only)</small></td></tr>
9413
- <tr><td class="name">state</td><td class="type">StateApi</td><td class="desc">Everything the user arranged, as a serialisable object. <small>(read-only)</small></td></tr>
9414
- <tr><td class="name">overlay</td><td class="type">OverlayApi</td><td class="desc">The loading, empty and error surfaces drawn over the grid. <small>(read-only)</small></td></tr>
9415
- <tr><td class="name">history</td><td class="type">HistoryApi</td><td class="desc">Undo and redo over edits and structural changes. <small>(read-only)</small></td></tr>
9416
- <tr><td class="name">views</td><td class="type">ViewsApi</td><td class="desc">Saved arrangements the user can switch between. <small>(read-only)</small></td></tr>
9417
- <tr><td class="name">diff</td><td class="type">DiffApi</td><td class="desc">What changed against a baseline, cell by cell. <small>(read-only)</small></td></tr>
9418
- <tr><td class="name">permissions</td><td class="type">PermissionsApi</td><td class="desc">Who may see, edit and export what. <small>(read-only)</small></td></tr>
9419
- <tr><td class="name">ai</td><td class="type">AiApi</td><td class="desc">A machine-readable description of the grid, for a model to read. <small>(read-only)</small></td></tr>
9420
- <tr><td class="name">messages</td><td class="type">MessagesApi</td><td class="desc">Translation: the catalogue and the active locale. <small>(read-only)</small></td></tr>
9421
- <tr><td class="name">licence</td><td class="type">LicenceApi</td><td class="desc">Licence state, and setting a key after construction. <small>(read-only)</small></td></tr>
9422
- <tr><td class="name">pagination</td><td class="type">PaginationApi</td><td class="desc">Pages, where the grid is paged rather than scrolled. <small>(read-only)</small></td></tr>
9423
- <tr><td class="name">highlight</td><td class="type">HighlightApi</td><td class="desc">Transient emphasis on a row, column or cell. <small>(read-only)</small></td></tr>
9424
- <tr><td class="name">find</td><td class="type">FindApi</td><td class="desc">In-grid find: locate text without filtering, and step through the matches. <small>(read-only)</small></td></tr>
9425
- <tr><td class="name">redaction</td><td class="type">RedactionApi</td><td class="desc">Values hidden from view and from export. <small>(read-only)</small></td></tr>
9426
- <tr><td class="name">capture</td><td class="type">(opts?: CaptureOptions): Promise&lt;Blob&gt;</td><td class="desc">An image of the grid as drawn, where the module is installed. <small>(optional)</small></td></tr>
9427
- <tr><td class="name">annotate</td><td class="type">AnnotationApi</td><td class="desc">Drawing over the grid, where the module is installed. <small>(optional)</small></td></tr>
9428
- <tr><td class="name">presentation</td><td class="type">PresentationApi</td><td class="desc">Full screen, scaling and chrome suppression. <small>(read-only)</small></td></tr>
9429
- <tr><td class="name">pivotView</td><td class="type">PivotViewApi</td><td class="desc">Expand and collapse the pivot presentation's axes; the state a view carries. <small>(read-only)</small></td></tr>
9430
- <tr><td class="name">updates</td><td class="type">UpdatesApi</td><td class="desc">The live feed: pausing it, flushing it, and what it has done. <small>(read-only)</small></td></tr>
9431
- <tr><td class="name">timeline</td><td class="type">TimelineApi</td><td class="desc">Replaying the changes the grid has seen. <small>(read-only)</small></td></tr>
9432
- <tr><td class="name">crossFilter</td><td class="type">CrossFilter</td><td class="desc">Cross-filtering, a derived grid filtering the grid it derives from. <small>(read-only)</small></td></tr>
9433
- <tr><td class="name">facets</td><td class="type">FacetsApi</td><td class="desc">Header distributions, and the filters clicking one creates. <small>(read-only)</small></td></tr>
9434
- <tr><td class="name">detail</td><td class="type">DetailApi</td><td class="desc">The expandable panel beneath a row. <small>(read-only)</small></td></tr>
9435
- <tr><td class="name">comments</td><td class="type">CommentsApi</td><td class="desc">Threads attached to rows and cells. <small>(read-only)</small></td></tr>
9436
- <tr><td class="name">presence</td><td class="type">PresenceApi</td><td class="desc">Who else is looking, and where. <small>(read-only)</small></td></tr>
9437
- <tr><td class="name">diagnostics</td><td class="type">DiagnosticsApi</td><td class="desc">What the grid is doing, for when it is doing it slowly. <small>(read-only)</small></td></tr>
9438
- <tr><td class="name">statistics</td><td class="type">StatisticsApi</td><td class="desc">Reductions, profiles, correlations, capability and intervals. <small>(read-only)</small></td></tr>
9439
- <tr><td class="name">formatting</td><td class="type">FormattingApi</td><td class="desc">Formatting a value as the grid would, outside a cell. <small>(read-only)</small></td></tr>
9440
- <tr><td class="name">validation</td><td class="type">ValidationApi</td><td class="desc">Declarative column validation: why a write was refused, and clearing marks. <small>(read-only)</small></td></tr>
9441
- <tr><td class="name">maximise</td><td class="type">MaximiseApi</td><td class="desc">Full-screen control, where it is enabled. <small>(read-only, optional)</small></td></tr>
9442
- <tr><td class="name">element</td><td class="type">HTMLElement | null</td><td class="desc">The element you passed to `createGrid`, not the grid's own root. The grid builds its `.lattice` root *inside* that element, so `el.closest('.lattice')` never matches this, and a theme attribute set on it has no effect, the theme is read from the root within. Use `element.querySelector('.lattice')` for the grid's own root. <small>(read-only)</small></td></tr>
9443
- <tr><td class="name">destroyed</td><td class="type">boolean</td><td class="desc">Whether `destroy` has run. Every other member is inert afterwards. <small>(read-only)</small></td></tr>
9444
- <tr><td class="name">ready</td><td class="type">boolean</td><td class="desc">False until the first render has been laid out. <small>(read-only)</small></td></tr>
9445
- <tr><td class="name">config</td><td class="type">(): GridConfig</td><td class="desc">The resolved configuration, as one object.</td></tr>
9446
- <tr><td class="name">setAll</td><td class="type">(values: Partial&lt;GridConfig&gt;): void</td><td class="desc">Apply several configuration changes as one update rather than several.</td></tr>
9447
- <tr><td class="name">on</td><td class="type">(event: EventName, handler: EventHandler): Unsubscribe</td><td class="desc">Listen. Returns the function that stops listening.</td></tr>
9448
- <tr><td class="name">once</td><td class="type">(event: EventName, handler: EventHandler): Unsubscribe</td><td class="desc">Listen until it fires once.</td></tr>
9449
- <tr><td class="name">off</td><td class="type">(event: EventName, handler: EventHandler): void</td><td class="desc">Stop listening.</td></tr>
9450
- <tr><td class="name">emit</td><td class="type">(event: string, payload?: Record&lt;string, unknown&gt;): void</td><td class="desc">Raise an event of your own on the grid's bus.</td></tr>
9451
- <tr><td class="name">setPinnedRows</td><td class="type">(rows: unknown[], opts?: { edge?: 'top' | 'bottom' }): void</td><td class="desc">Pin rows above or below the scrolling body. The rows render through the ordinary column pipeline but are not part of the data: not counted, sorted, filtered, grouped, selectable or exported. Pass a new array rather than mutating the one you passed before: array identity is how the grid knows the pinned rows have changed.</td></tr>
9452
- <tr><td class="name">getPinnedRows</td><td class="type">(opts?: { edge?: 'top' | 'bottom' }): unknown[]</td><td class="desc">The objects currently pinned at one edge, as a copy.</td></tr>
9453
- <tr><td class="name">form</td><td class="type">RowFormApi</td><td class="desc">The row form. Declines when `rowForm` is not configured. <small>(read-only)</small></td></tr>
9454
- <tr><td class="name">getVersion</td><td class="type">(): string</td><td class="desc">The library version.</td></tr>
9455
- <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc">Release everything: listeners, timers, workers and the DOM the grid made.</td></tr>
9889
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
9890
+ <tr><td class="name">type</td><td class="type">string</td><td class="desc"></td></tr>
9891
+ <tr><td class="name">at</td><td class="type">number | null</td><td class="desc"></td></tr>
9892
+ <tr><td class="name">earliestFeasible</td><td class="type">number</td><td class="desc"></td></tr>
9456
9893
  </tbody>
9457
9894
  </table>
9458
9895
  </div>
9459
- <h3 id="type-GridConfig">GridConfig</h3>
9896
+ <h3 id="type-GanttDependency">GanttDependency</h3>
9897
+ <p class="section-note">A typed dependency between two tasks (by id), with optional lag/lead. `type` defaults to `'FS'`; either endpoint may be a leaf or a summary. `type` also accepts the MS Project string shorthand — `'FS+2'`, `'SS-1'` (BACKLOG-0001072). It is normalised to the structured form on the way in, so `gantt.dependencies` always reads back `{ type, lag }` and there is no second internal representation. Giving both a shorthand lag and a conflicting `lag` field warns; the explicit field wins.</p>
9460
9898
  <div class="table-wrap">
9461
9899
  <table>
9462
9900
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9463
9901
  <tbody>
9464
- <tr><td class="name">columns</td><td class="type">(Column | ColumnGroup)[]</td><td class="desc">The columns, in order. A group nests columns under one heading. <small>(optional)</small></td></tr>
9465
- <tr><td class="name">columnGroups</td><td class="type">ColumnGroup[]</td><td class="desc">Header groups declared separately from the columns they contain. <small>(optional)</small></td></tr>
9466
- <tr><td class="name">rows</td><td class="type">unknown[]</td><td class="desc">The data, for a memory grid. Use `source` for anything fetched. <small>(optional)</small></td></tr>
9467
- <tr><td class="name">rowKey</td><td class="type">string | string[] | ((row: unknown) =&gt; string | string[])</td><td class="desc">What identifies a row. Everything that survives a refresh (selection, expansion, and edits in flight) is keyed on it, so it must be stable and unique. A derived grid defaults to its own derived key. Three shapes: a field name (`'id'`, dot paths allowed); an array of field names, joined into one composite key (`['tenantId', 'circuitId']`); or a function of the row (`row =&gt; \`${row.tenantId}#${row.circuitId}\``), itself allowed to return an array to the same effect. <small>(optional)</small></td></tr>
9468
- <tr><td class="name">source</td><td class="type">SourceConfig</td><td class="desc">Where rows come from: memory, paged, remote, stream or derived. <small>(optional)</small></td></tr>
9469
- <tr><td class="name">ingest</td><td class="type">IngestConfig</td><td class="desc">How rows are ingested into the column store. <small>(optional)</small></td></tr>
9470
- <tr><td class="name">columnDefaults</td><td class="type">Column</td><td class="desc">Applied to every column before its own settings. <small>(optional)</small></td></tr>
9471
- <tr><td class="name">columnPresets</td><td class="type">Record&lt;string, Column&gt;</td><td class="desc">Named bundles of column settings, referenced by a column's `preset`. <small>(optional)</small></td></tr>
9472
- <tr><td class="name">dataTypes</td><td class="type">Record&lt;string, DataType&gt;</td><td class="desc">Your own data types, alongside the built-in catalogue. <small>(optional)</small></td></tr>
9473
- <tr><td class="name">sampleSize</td><td class="type">number</td><td class="desc">Values sampled per undeclared column when inferring its type. Default 100. <small>(optional)</small></td></tr>
9474
- <tr><td class="name">targetSize</td><td class="type">'default' | 'large'</td><td class="desc">Raise every interactive target to a comfortable size for touch, without changing the type. `'large'` asks for it; `'default'` opts out of the coarse-pointer rule that would otherwise apply it. <small>(optional)</small></td></tr>
9475
- <tr><td class="name">components</td><td class="type">Record&lt;string, RendererCtor | EditorCtor | FilterCtor&gt;</td><td class="desc">Your own renderers, editors and filters, registered by name. <small>(optional)</small></td></tr>
9476
- <tr><td class="name">pipes</td><td class="type">Record&lt;string, (value: unknown, ...args: string[]) =&gt; string&gt;</td><td class="desc">Named text transforms usable from a format mask or a template. <small>(optional)</small></td></tr>
9477
- <tr><td class="name">totalFns</td><td class="type">Record&lt;string, TotalFn&gt;</td><td class="desc">Your own reductions, alongside the built-in ones. <small>(optional)</small></td></tr>
9478
- <tr><td class="name">variants</td><td class="type">Record&lt;string, VariantDefinition&gt;</td><td class="desc">Named appearance variants a row or cell can be switched into by a rule. <small>(optional)</small></td></tr>
9479
- <tr><td class="name">tree</td><td class="type">TreeConfig</td><td class="desc">Hierarchical rows: where the parent link or the path lives. <small>(optional)</small></td></tr>
9480
- <tr><td class="name">detail</td><td class="type">DetailConfig</td><td class="desc">The expandable panel beneath a row. <small>(optional)</small></td></tr>
9481
- <tr><td class="name">selection</td><td class="type">SelectionConfig | 'single' | 'multiple' | 'none'</td><td class="desc">What the user may select, and how selection behaves across groups. The `'none'` shorthand is `{ mode: 'none' }` and behaves identically: no row selection, and no cell ranges or fill handle either. <small>(optional)</small></td></tr>
9482
- <tr><td class="name">edit</td><td class="type">EditConfig | boolean</td><td class="desc">Editing, and how a change is committed and validated. <small>(optional)</small></td></tr>
9483
- <tr><td class="name">pagination</td><td class="type">PaginationConfig | boolean</td><td class="desc">Page the rows rather than scrolling them. <small>(optional)</small></td></tr>
9484
- <tr><td class="name">locale</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
9485
- <tr><td class="name">direction</td><td class="type">'ltr' | 'rtl' | 'auto'</td><td class="desc">Writing direction. Omit it, or say `'auto'`, to settle it from the element's own computed `dir` and then from `locale`: `ar`, `he`, `fa` and the rest resolve to `rtl`. In a right-to-left grid the logical alignments `start`/`end` mirror while the physical `left`/`right` do not (see {@link Align}). <small>(optional)</small></td></tr>
9486
- <tr><td class="name">timeZone</td><td class="type">string</td><td class="desc">IANA zone every date column formats in, e.g. 'Europe/London' or 'UTC'. Omit to use each viewer's own zone. A column's own `format.timeZone` wins. <small>(optional)</small></td></tr>
9902
+ <tr><td class="name">from</td><td class="type">string | number</td><td class="desc"></td></tr>
9903
+ <tr><td class="name">to</td><td class="type">string | number</td><td class="desc"></td></tr>
9904
+ <tr><td class="name">type</td><td class="type">GanttLinkType | `${GanttLinkType}${'+' | '-'}${number}`</td><td class="desc"><small>(optional)</small></td></tr>
9905
+ <tr><td class="name">lag</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
9906
+ </tbody>
9907
+ </table>
9908
+ </div>
9909
+ <h3 id="type-GanttEarnedValue">GanttEarnedValue</h3>
9910
+ <p class="section-note">The earned-value result at a status date (BACKLOG-0000958).</p>
9911
+ <div class="table-wrap">
9912
+ <table>
9913
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9914
+ <tbody>
9915
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc"></td></tr>
9916
+ <tr><td class="name">error</td><td class="type">{ code: string; message: string }</td><td class="desc"><small>(optional)</small></td></tr>
9917
+ <tr><td class="name">statusDate</td><td class="type">number</td><td class="desc">The status date the metrics were evaluated at (day-number). <small>(optional)</small></td></tr>
9918
+ <tr><td class="name">byTask</td><td class="type">Map&lt;string, GanttEarnedValueRow&gt;</td><td class="desc">Every task keyed by id (leaf, summary and derived). <small>(optional)</small></td></tr>
9919
+ <tr><td class="name">rows</td><td class="type">GanttEarnedValueRow[]</td><td class="desc">The same rows in schedule order. <small>(optional)</small></td></tr>
9920
+ <tr><td class="name">project</td><td class="type">GanttEarnedValueRow</td><td class="desc">The project total, rolled up as money sums of the leaves. <small>(optional)</small></td></tr>
9921
+ </tbody>
9922
+ </table>
9923
+ </div>
9924
+ <h3 id="type-GanttEarnedValueRow">GanttEarnedValueRow</h3>
9925
+ <p class="section-note">Earned-value metrics for one task or the whole project (BACKLOG-0000958).</p>
9926
+ <div class="table-wrap">
9927
+ <table>
9928
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9929
+ <tbody>
9930
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
9931
+ <tr><td class="name">name</td><td class="type">string</td><td class="desc"></td></tr>
9932
+ <tr><td class="name">isSummary</td><td class="type">boolean</td><td class="desc"></td></tr>
9933
+ <tr><td class="name">isMilestone</td><td class="type">boolean</td><td class="desc"></td></tr>
9934
+ <tr><td class="name">percentComplete</td><td class="type">number | null</td><td class="desc"></td></tr>
9935
+ <tr><td class="name">hasBaseline</td><td class="type">boolean</td><td class="desc">Whether a baseline (not the fallback scheduled window) drove PV.</td></tr>
9936
+ <tr><td class="name">hasActualCost</td><td class="type">boolean</td><td class="desc">Whether any actual cost fed AC (else AC/CV/CPI are null).</td></tr>
9937
+ <tr><td class="name">bac</td><td class="type">number</td><td class="desc">Budget at completion (the task's cost, or its duration when no cost).</td></tr>
9938
+ <tr><td class="name">pv</td><td class="type">number</td><td class="desc">Planned Value (BCWS): budgeted cost of the work scheduled by the status date.</td></tr>
9939
+ <tr><td class="name">ev</td><td class="type">number</td><td class="desc">Earned Value (BCWP): budgeted cost of the work performed (BAC × %complete).</td></tr>
9940
+ <tr><td class="name">ac</td><td class="type">number | null</td><td class="desc">Actual Cost (ACWP): what the work performed actually cost, or null.</td></tr>
9941
+ <tr><td class="name">sv</td><td class="type">number</td><td class="desc">Schedule Variance (EV − PV); positive is ahead of schedule.</td></tr>
9942
+ <tr><td class="name">cv</td><td class="type">number | null</td><td class="desc">Cost Variance (EV − AC); positive is under budget; null without AC.</td></tr>
9943
+ <tr><td class="name">spi</td><td class="type">number | null</td><td class="desc">Schedule Performance Index (EV / PV); null when PV is zero.</td></tr>
9944
+ <tr><td class="name">cpi</td><td class="type">number | null</td><td class="desc">Cost Performance Index (EV / AC); null without AC or when AC is zero.</td></tr>
9945
+ </tbody>
9946
+ </table>
9947
+ </div>
9948
+ <h3 id="type-GanttLevelResult">GanttLevelResult</h3>
9949
+ <p class="section-note">The result of resource leveling: the shifted tasks and what moved (BACKLOG-0000948).</p>
9950
+ <div class="table-wrap">
9951
+ <table>
9952
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9953
+ <tbody>
9954
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc"></td></tr>
9955
+ <tr><td class="name">resolved</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
9956
+ <tr><td class="name">tasks</td><td class="type">GanttTask[]</td><td class="desc"><small>(optional)</small></td></tr>
9957
+ <tr><td class="name">schedule</td><td class="type">GanttSchedule</td><td class="desc"><small>(optional)</small></td></tr>
9958
+ <tr><td class="name">moves</td><td class="type">Array&lt;{ id: string; from: number; to: number; delay: number }&gt;</td><td class="desc"><small>(optional)</small></td></tr>
9959
+ <tr><td class="name">remaining</td><td class="type">GanttOverAllocation[]</td><td class="desc"><small>(optional)</small></td></tr>
9960
+ <tr><td class="name">error</td><td class="type">{ code: string; message: string }</td><td class="desc"><small>(optional)</small></td></tr>
9961
+ </tbody>
9962
+ </table>
9963
+ </div>
9964
+ <h3 id="type-GanttMSPDIModel">GanttMSPDIModel</h3>
9965
+ <p class="section-note">The model {@link importMSPDI} returns and {@link exportMSPDI} takes.</p>
9966
+ <div class="table-wrap">
9967
+ <table>
9968
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9969
+ <tbody>
9970
+ <tr><td class="name">tasks</td><td class="type">GanttTask[]</td><td class="desc"></td></tr>
9971
+ <tr><td class="name">dependencies</td><td class="type">GanttDependency[]</td><td class="desc"><small>(optional)</small></td></tr>
9972
+ <tr><td class="name">resources</td><td class="type">GanttResourceSpec</td><td class="desc"><small>(optional)</small></td></tr>
9973
+ <tr><td class="name">projectStart</td><td class="type">number | string | Date</td><td class="desc"><small>(optional)</small></td></tr>
9974
+ <tr><td class="name">calendar</td><td class="type">GanttCalendar | null</td><td class="desc"><small>(optional)</small></td></tr>
9975
+ <tr><td class="name">schedule</td><td class="type">GanttSchedule</td><td class="desc"><small>(optional)</small></td></tr>
9976
+ </tbody>
9977
+ </table>
9978
+ </div>
9979
+ <h3 id="type-GanttOverAllocation">GanttOverAllocation</h3>
9980
+ <p class="section-note">A resource booked beyond its capacity across concurrent tasks (BACKLOG-0000948).</p>
9981
+ <div class="table-wrap">
9982
+ <table>
9983
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9984
+ <tbody>
9985
+ <tr><td class="name">resource</td><td class="type">string</td><td class="desc"></td></tr>
9986
+ <tr><td class="name">capacity</td><td class="type">number</td><td class="desc"></td></tr>
9987
+ <tr><td class="name">start</td><td class="type">number</td><td class="desc"></td></tr>
9988
+ <tr><td class="name">end</td><td class="type">number</td><td class="desc"></td></tr>
9989
+ <tr><td class="name">load</td><td class="type">number</td><td class="desc"></td></tr>
9990
+ <tr><td class="name">taskIds</td><td class="type">string[]</td><td class="desc"></td></tr>
9991
+ </tbody>
9992
+ </table>
9993
+ </div>
9994
+ <h3 id="type-GanttResourceLoad">GanttResourceLoad</h3>
9995
+ <p class="section-note">The per-resource load and the over-allocations across a schedule (BACKLOG-0000948).</p>
9996
+ <div class="table-wrap">
9997
+ <table>
9998
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9999
+ <tbody>
10000
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc"></td></tr>
10001
+ <tr><td class="name">resources</td><td class="type">Array&lt;{ resource: string; capacity: number; peak: number; segments: GanttResourceSegment[] }&gt;</td><td class="desc"></td></tr>
10002
+ <tr><td class="name">overAllocations</td><td class="type">GanttOverAllocation[]</td><td class="desc"></td></tr>
10003
+ <tr><td class="name">byResource</td><td class="type">Map&lt;string, { capacity: number; peak: number; segments: GanttResourceSegment[] }&gt;</td><td class="desc"></td></tr>
10004
+ </tbody>
10005
+ </table>
10006
+ </div>
10007
+ <h3 id="type-GanttResourceSegment">GanttResourceSegment</h3>
10008
+ <p class="section-note">One contiguous load segment for a resource: how many units are booked over a span.</p>
10009
+ <div class="table-wrap">
10010
+ <table>
10011
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10012
+ <tbody>
10013
+ <tr><td class="name">start</td><td class="type">number</td><td class="desc"></td></tr>
10014
+ <tr><td class="name">end</td><td class="type">number</td><td class="desc"></td></tr>
10015
+ <tr><td class="name">load</td><td class="type">number</td><td class="desc"></td></tr>
10016
+ <tr><td class="name">taskIds</td><td class="type">string[]</td><td class="desc"></td></tr>
10017
+ </tbody>
10018
+ </table>
10019
+ </div>
10020
+ <h3 id="type-GanttSchedule">GanttSchedule</h3>
10021
+ <p class="section-note">A CPM schedule result: per-task dates/float and the critical path, or an error.</p>
10022
+ <div class="table-wrap">
10023
+ <table>
10024
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10025
+ <tbody>
10026
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc"></td></tr>
10027
+ <tr><td class="name">error</td><td class="type">{ code: string; message: string; cycle?: string[] }</td><td class="desc"><small>(optional)</small></td></tr>
10028
+ <tr><td class="name">tasks</td><td class="type">Map&lt;string, GanttScheduledTask&gt;</td><td class="desc"><small>(optional)</small></td></tr>
10029
+ <tr><td class="name">order</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
10030
+ <tr><td class="name">critical</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
10031
+ <tr><td class="name">criticalPaths</td><td class="type">string[][]</td><td class="desc"><small>(optional)</small></td></tr>
10032
+ <tr><td class="name">projectStart</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10033
+ <tr><td class="name">projectFinish</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10034
+ <tr><td class="name">projectDuration</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10035
+ <tr><td class="name">conflicts</td><td class="type">GanttConflict[]</td><td class="desc">Constraints a predecessor made infeasible (empty when all are satisfied). <small>(optional)</small></td></tr>
10036
+ <tr><td class="name">calendar</td><td class="type">boolean</td><td class="desc">Whether a working-time calendar was applied. <small>(optional)</small></td></tr>
10037
+ <tr><td class="name">overAllocations</td><td class="type">GanttOverAllocation[]</td><td class="desc">The resource over-allocations for this schedule (BACKLOG-0000948). <small>(optional)</small></td></tr>
10038
+ <tr><td class="name">resourceLoad</td><td class="type">GanttResourceLoad</td><td class="desc">The full resource-load report for this schedule (BACKLOG-0000948). <small>(optional)</small></td></tr>
10039
+ </tbody>
10040
+ </table>
10041
+ </div>
10042
+ <h3 id="type-GanttScheduledTask">GanttScheduledTask</h3>
10043
+ <p class="section-note">The computed CPM values for one task (a leaf is scheduled, a summary derived).</p>
10044
+ <div class="table-wrap">
10045
+ <table>
10046
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10047
+ <tbody>
10048
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
10049
+ <tr><td class="name">name</td><td class="type">string</td><td class="desc"></td></tr>
10050
+ <tr><td class="name">duration</td><td class="type">number</td><td class="desc"></td></tr>
10051
+ <tr><td class="name">es</td><td class="type">number</td><td class="desc"></td></tr>
10052
+ <tr><td class="name">ef</td><td class="type">number</td><td class="desc"></td></tr>
10053
+ <tr><td class="name">ls</td><td class="type">number</td><td class="desc"></td></tr>
10054
+ <tr><td class="name">lf</td><td class="type">number</td><td class="desc"></td></tr>
10055
+ <tr><td class="name">totalFloat</td><td class="type">number</td><td class="desc"></td></tr>
10056
+ <tr><td class="name">critical</td><td class="type">boolean</td><td class="desc"></td></tr>
10057
+ <tr><td class="name">percentComplete</td><td class="type">number | null</td><td class="desc"></td></tr>
10058
+ <tr><td class="name">parent</td><td class="type">string | null</td><td class="desc"></td></tr>
10059
+ <tr><td class="name">isSummary</td><td class="type">boolean</td><td class="desc"></td></tr>
10060
+ <tr><td class="name">isMilestone</td><td class="type">boolean</td><td class="desc"></td></tr>
10061
+ <tr><td class="name">children</td><td class="type">string[]</td><td class="desc"></td></tr>
10062
+ <tr><td class="name">baselineStart</td><td class="type">number | null</td><td class="desc">The planned (baseline) window, present only when the task carries a baseline. <small>(optional)</small></td></tr>
10063
+ <tr><td class="name">baselineEnd</td><td class="type">number | null</td><td class="desc"><small>(optional)</small></td></tr>
10064
+ <tr><td class="name">startVariance</td><td class="type">number | null</td><td class="desc">Variance vs the baseline (actual − planned, day-numbers); a positive value is a slip. <small>(optional)</small></td></tr>
10065
+ <tr><td class="name">finishVariance</td><td class="type">number | null</td><td class="desc"><small>(optional)</small></td></tr>
10066
+ <tr><td class="name">durationVariance</td><td class="type">number | null</td><td class="desc"><small>(optional)</small></td></tr>
10067
+ </tbody>
10068
+ </table>
10069
+ </div>
10070
+ <h3 id="type-GanttTask">GanttTask</h3>
10071
+ <p class="section-note">A task in a Gantt plan. Give a `duration` or a `start`+`end` (a day-number, ISO date string or `Date`; one is derived from the other). `milestone: true` (or `duration: 0`) is a zero-duration point. `parent` nests a task under a summary, whose window and progress are DERIVED from its children. `baselineStart`/`baselineEnd` (host-stored) drive planned-vs-actual variance; `constraint` pins or pulls the task; `assignee` and `height` feed the split view's grid panel.</p>
10072
+ <div class="table-wrap">
10073
+ <table>
10074
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10075
+ <tbody>
10076
+ <tr><td class="name">id</td><td class="type">string | number</td><td class="desc"></td></tr>
10077
+ <tr><td class="name">name</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10078
+ <tr><td class="name">start</td><td class="type">number | string | Date</td><td class="desc"><small>(optional)</small></td></tr>
10079
+ <tr><td class="name">end</td><td class="type">number | string | Date</td><td class="desc"><small>(optional)</small></td></tr>
10080
+ <tr><td class="name">duration</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10081
+ <tr><td class="name">percentComplete</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10082
+ <tr><td class="name">milestone</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
10083
+ <tr><td class="name">parent</td><td class="type">string | number</td><td class="desc"><small>(optional)</small></td></tr>
10084
+ <tr><td class="name">baselineStart</td><td class="type">number | string | Date</td><td class="desc"><small>(optional)</small></td></tr>
10085
+ <tr><td class="name">baselineEnd</td><td class="type">number | string | Date</td><td class="desc"><small>(optional)</small></td></tr>
10086
+ <tr><td class="name">baseline</td><td class="type">{ start?: number | string | Date; end?: number | string | Date }</td><td class="desc"><small>(optional)</small></td></tr>
10087
+ <tr><td class="name">constraint</td><td class="type">GanttConstraintType</td><td class="desc"><small>(optional)</small></td></tr>
10088
+ <tr><td class="name">constraintDate</td><td class="type">number | string | Date</td><td class="desc"><small>(optional)</small></td></tr>
10089
+ <tr><td class="name">assignee</td><td class="type">string | string[]</td><td class="desc"><small>(optional)</small></td></tr>
10090
+ <tr><td class="name">assignees</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
10091
+ <tr><td class="name">owner</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10092
+ <tr><td class="name">assignments</td><td class="type">Array&lt;{ resource?: string; name?: string; id?: string; units?: number }&gt;</td><td class="desc">Explicit resource assignments with fractional units (BACKLOG-0000948): `units` is a multiplier where 1 is a full-time booking. Use this when a task books a resource at less (or more) than 100%; a bare `assignee` is `units: 1`. <small>(optional)</small></td></tr>
10093
+ <tr><td class="name">work</td><td class="type">number | Array&lt;{ date: number | string | Date; hours: number }&gt;</td><td class="desc">The task's effort, in one of two forms (BACKLOG-0001281/1282). A **number** is the task's TOTAL hours; the workload band divides it between the assignments in proportion to their units and spreads each share evenly over the working days the task spans. (`hours` is accepted as the same field under its other common name.) An **array** is an explicit per-day contour — what a planner types into a workload cell — and states each day's hours itself: the task's total is the sum of the entries, nothing is spread, and the contour is authoritative for the span, so `applyEdit` derives the task's `start` and `duration` from its first and last day. An EMPTY array means "no hours booked", which is how clearing every bucket is expressed without reviving the even spread. A bar move re-times the contour onto the new days unchanged; a resize stretches it across the new span at the same daily levels. `date` is an ISO date, a `Date` or a plan day-number; the module writes ISO dates back. <small>(optional)</small></td></tr>
10094
+ <tr><td class="name">priority</td><td class="type">number</td><td class="desc">Leveling priority: a higher value is delayed last (default 0). <small>(optional)</small></td></tr>
10095
+ <tr><td class="name">height</td><td class="type">number</td><td class="desc">An explicit row height (px) for the split view; applied to both panels. <small>(optional)</small></td></tr>
10096
+ <tr><td class="name">cost</td><td class="type">number</td><td class="desc">The budgeted cost (BAC) for earned-value analysis (BACKLOG-0000958). When omitted the task's duration is used as the budget, giving schedule-only EVM. <small>(optional)</small></td></tr>
10097
+ <tr><td class="name">actualCost</td><td class="type">number</td><td class="desc">The actual cost incurred (ACWP) for earned-value analysis (BACKLOG-0000958). Left out, the task's cost variance/CPI are `null`. <small>(optional)</small></td></tr>
10098
+ </tbody>
10099
+ </table>
10100
+ </div>
10101
+ <h3 id="type-GanttViolation">GanttViolation</h3>
10102
+ <p class="section-note">A placement violation flagged by `findViolations`.</p>
10103
+ <div class="table-wrap">
10104
+ <table>
10105
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10106
+ <tbody>
10107
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
10108
+ <tr><td class="name">placedStart</td><td class="type">number</td><td class="desc"></td></tr>
10109
+ <tr><td class="name">earliestStart</td><td class="type">number</td><td class="desc"></td></tr>
10110
+ <tr><td class="name">by</td><td class="type">number</td><td class="desc"></td></tr>
10111
+ </tbody>
10112
+ </table>
10113
+ </div>
10114
+ <h3 id="type-Grid">Grid</h3>
10115
+ <div class="table-wrap">
10116
+ <table>
10117
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10118
+ <tbody>
10119
+ <tr><td class="name">rows</td><td class="type">RowsApi</td><td class="desc">The data: reading it, changing it, walking it. <small>(read-only)</small></td></tr>
10120
+ <tr><td class="name">columns</td><td class="type">ColumnsApi</td><td class="desc">The columns: order, width, visibility, grouping and pivoting. <small>(read-only)</small></td></tr>
10121
+ <tr><td class="name">selection</td><td class="type">SelectionApi</td><td class="desc">What is selected, and the range the user has marked. <small>(read-only)</small></td></tr>
10122
+ <tr><td class="name">filters</td><td class="type">FiltersApi</td><td class="desc">The filter tree, however it was set. <small>(read-only)</small></td></tr>
10123
+ <tr><td class="name">sort</td><td class="type">SortApi</td><td class="desc">The sort, in priority order. <small>(read-only)</small></td></tr>
10124
+ <tr><td class="name">edit</td><td class="type">EditApi</td><td class="desc">Editing sessions: starting, committing and cancelling them. <small>(read-only)</small></td></tr>
10125
+ <tr><td class="name">scroll</td><td class="type">ScrollApi</td><td class="desc">Where the viewport is, and moving it. <small>(read-only)</small></td></tr>
10126
+ <tr><td class="name">export</td><td class="type">ExportApi</td><td class="desc">CSV, Excel and clipboard. <small>(read-only)</small></td></tr>
10127
+ <tr><td class="name">import</td><td class="type">ImportApi</td><td class="desc">Bringing rows in from CSV/TSV text, a file, the clipboard or a drop. <small>(read-only)</small></td></tr>
10128
+ <tr><td class="name">state</td><td class="type">StateApi</td><td class="desc">Everything the user arranged, as a serialisable object. <small>(read-only)</small></td></tr>
10129
+ <tr><td class="name">overlay</td><td class="type">OverlayApi</td><td class="desc">The loading, empty and error surfaces drawn over the grid. <small>(read-only)</small></td></tr>
10130
+ <tr><td class="name">history</td><td class="type">HistoryApi</td><td class="desc">Undo and redo over edits and structural changes. <small>(read-only)</small></td></tr>
10131
+ <tr><td class="name">views</td><td class="type">ViewsApi</td><td class="desc">Saved arrangements the user can switch between. <small>(read-only)</small></td></tr>
10132
+ <tr><td class="name">diff</td><td class="type">DiffApi</td><td class="desc">What changed against a baseline, cell by cell. <small>(read-only)</small></td></tr>
10133
+ <tr><td class="name">permissions</td><td class="type">PermissionsApi</td><td class="desc">Who may see, edit and export what. <small>(read-only)</small></td></tr>
10134
+ <tr><td class="name">ai</td><td class="type">AiApi</td><td class="desc">A machine-readable description of the grid, for a model to read. <small>(read-only)</small></td></tr>
10135
+ <tr><td class="name">messages</td><td class="type">MessagesApi</td><td class="desc">Translation: the catalogue and the active locale. <small>(read-only)</small></td></tr>
10136
+ <tr><td class="name">licence</td><td class="type">LicenceApi</td><td class="desc">Licence state, and setting a key after construction. <small>(read-only)</small></td></tr>
10137
+ <tr><td class="name">pagination</td><td class="type">PaginationApi</td><td class="desc">Pages, where the grid is paged rather than scrolled. <small>(read-only)</small></td></tr>
10138
+ <tr><td class="name">highlight</td><td class="type">HighlightApi</td><td class="desc">Transient emphasis on a row, column or cell. <small>(read-only)</small></td></tr>
10139
+ <tr><td class="name">find</td><td class="type">FindApi</td><td class="desc">In-grid find: locate text without filtering, and step through the matches. <small>(read-only)</small></td></tr>
10140
+ <tr><td class="name">redaction</td><td class="type">RedactionApi</td><td class="desc">Values hidden from view and from export. <small>(read-only)</small></td></tr>
10141
+ <tr><td class="name">capture</td><td class="type">(opts?: CaptureOptions): Promise&lt;Blob&gt;</td><td class="desc">An image of the grid as drawn, where the module is installed. <small>(optional)</small></td></tr>
10142
+ <tr><td class="name">annotate</td><td class="type">AnnotationApi</td><td class="desc">Drawing over the grid, where the module is installed. <small>(optional)</small></td></tr>
10143
+ <tr><td class="name">presentation</td><td class="type">PresentationApi</td><td class="desc">Full screen, scaling and chrome suppression. <small>(read-only)</small></td></tr>
10144
+ <tr><td class="name">pivotView</td><td class="type">PivotViewApi</td><td class="desc">Expand and collapse the pivot presentation's axes; the state a view carries. <small>(read-only)</small></td></tr>
10145
+ <tr><td class="name">updates</td><td class="type">UpdatesApi</td><td class="desc">The live feed: pausing it, flushing it, and what it has done. <small>(read-only)</small></td></tr>
10146
+ <tr><td class="name">timeline</td><td class="type">TimelineApi</td><td class="desc">Replaying the changes the grid has seen. <small>(read-only)</small></td></tr>
10147
+ <tr><td class="name">crossFilter</td><td class="type">CrossFilter</td><td class="desc">Cross-filtering, a derived grid filtering the grid it derives from. <small>(read-only)</small></td></tr>
10148
+ <tr><td class="name">facets</td><td class="type">FacetsApi</td><td class="desc">Header distributions, and the filters clicking one creates. <small>(read-only)</small></td></tr>
10149
+ <tr><td class="name">detail</td><td class="type">DetailApi</td><td class="desc">The expandable panel beneath a row. <small>(read-only)</small></td></tr>
10150
+ <tr><td class="name">comments</td><td class="type">CommentsApi</td><td class="desc">Threads attached to rows and cells. <small>(read-only)</small></td></tr>
10151
+ <tr><td class="name">presence</td><td class="type">PresenceApi</td><td class="desc">Who else is looking, and where. <small>(read-only)</small></td></tr>
10152
+ <tr><td class="name">diagnostics</td><td class="type">DiagnosticsApi</td><td class="desc">What the grid is doing, for when it is doing it slowly. <small>(read-only)</small></td></tr>
10153
+ <tr><td class="name">statistics</td><td class="type">StatisticsApi</td><td class="desc">Reductions, profiles, correlations, capability and intervals. <small>(read-only)</small></td></tr>
10154
+ <tr><td class="name">formatting</td><td class="type">FormattingApi</td><td class="desc">Formatting a value as the grid would, outside a cell. <small>(read-only)</small></td></tr>
10155
+ <tr><td class="name">validation</td><td class="type">ValidationApi</td><td class="desc">Declarative column validation: why a write was refused, and clearing marks. <small>(read-only)</small></td></tr>
10156
+ <tr><td class="name">maximise</td><td class="type">MaximiseApi</td><td class="desc">Full-screen control, where it is enabled. <small>(read-only, optional)</small></td></tr>
10157
+ <tr><td class="name">element</td><td class="type">HTMLElement | null</td><td class="desc">The element you passed to `createGrid`, not the grid's own root. The grid builds its `.lattice` root *inside* that element, so `el.closest('.lattice')` never matches this, and a theme attribute set on it has no effect, the theme is read from the root within. Use `element.querySelector('.lattice')` for the grid's own root. <small>(read-only)</small></td></tr>
10158
+ <tr><td class="name">destroyed</td><td class="type">boolean</td><td class="desc">Whether `destroy` has run. Every other member is inert afterwards. <small>(read-only)</small></td></tr>
10159
+ <tr><td class="name">ready</td><td class="type">boolean</td><td class="desc">False until the first render has been laid out. <small>(read-only)</small></td></tr>
10160
+ <tr><td class="name">config</td><td class="type">(): GridConfig</td><td class="desc">The resolved configuration, as one object.</td></tr>
10161
+ <tr><td class="name">setAll</td><td class="type">(values: Partial&lt;GridConfig&gt;): void</td><td class="desc">Apply several configuration changes as one update rather than several.</td></tr>
10162
+ <tr><td class="name">on</td><td class="type">(event: EventName, handler: EventHandler): Unsubscribe</td><td class="desc">Listen. Returns the function that stops listening.</td></tr>
10163
+ <tr><td class="name">once</td><td class="type">(event: EventName, handler: EventHandler): Unsubscribe</td><td class="desc">Listen until it fires once.</td></tr>
10164
+ <tr><td class="name">off</td><td class="type">(event: EventName, handler: EventHandler): void</td><td class="desc">Stop listening.</td></tr>
10165
+ <tr><td class="name">emit</td><td class="type">(event: string, payload?: Record&lt;string, unknown&gt;): void</td><td class="desc">Raise an event of your own on the grid's bus.</td></tr>
10166
+ <tr><td class="name">setPinnedRows</td><td class="type">(rows: unknown[], opts?: { edge?: 'top' | 'bottom' }): void</td><td class="desc">Pin rows above or below the scrolling body. The rows render through the ordinary column pipeline but are not part of the data: not counted, sorted, filtered, grouped, selectable or exported. Pass a new array rather than mutating the one you passed before: array identity is how the grid knows the pinned rows have changed.</td></tr>
10167
+ <tr><td class="name">getPinnedRows</td><td class="type">(opts?: { edge?: 'top' | 'bottom' }): unknown[]</td><td class="desc">The objects currently pinned at one edge, as a copy.</td></tr>
10168
+ <tr><td class="name">form</td><td class="type">RowFormApi</td><td class="desc">The row form. Declines when `rowForm` is not configured. <small>(read-only)</small></td></tr>
10169
+ <tr><td class="name">getVersion</td><td class="type">(): string</td><td class="desc">The library version.</td></tr>
10170
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc">Release everything: listeners, timers, workers and the DOM the grid made.</td></tr>
10171
+ </tbody>
10172
+ </table>
10173
+ </div>
10174
+ <h3 id="type-GridConfig">GridConfig</h3>
10175
+ <div class="table-wrap">
10176
+ <table>
10177
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10178
+ <tbody>
10179
+ <tr><td class="name">columns</td><td class="type">(Column | ColumnGroup)[]</td><td class="desc">The columns, in order. A group nests columns under one heading. <small>(optional)</small></td></tr>
10180
+ <tr><td class="name">columnGroups</td><td class="type">ColumnGroup[]</td><td class="desc">Header groups declared separately from the columns they contain. <small>(optional)</small></td></tr>
10181
+ <tr><td class="name">rows</td><td class="type">unknown[]</td><td class="desc">The data, for a memory grid. Use `source` for anything fetched. <small>(optional)</small></td></tr>
10182
+ <tr><td class="name">rowKey</td><td class="type">string | string[] | ((row: unknown) =&gt; string | string[])</td><td class="desc">What identifies a row. Everything that survives a refresh (selection, expansion, and edits in flight) is keyed on it, so it must be stable and unique. A derived grid defaults to its own derived key. Three shapes: a field name (`'id'`, dot paths allowed); an array of field names, joined into one composite key (`['tenantId', 'circuitId']`); or a function of the row (`row =&gt; \`${row.tenantId}#${row.circuitId}\``), itself allowed to return an array to the same effect. <small>(optional)</small></td></tr>
10183
+ <tr><td class="name">source</td><td class="type">SourceConfig</td><td class="desc">Where rows come from: memory, paged, remote, stream or derived. <small>(optional)</small></td></tr>
10184
+ <tr><td class="name">ingest</td><td class="type">IngestConfig</td><td class="desc">How rows are ingested into the column store. <small>(optional)</small></td></tr>
10185
+ <tr><td class="name">columnDefaults</td><td class="type">Column</td><td class="desc">Applied to every column before its own settings. <small>(optional)</small></td></tr>
10186
+ <tr><td class="name">columnPresets</td><td class="type">Record&lt;string, Column&gt;</td><td class="desc">Named bundles of column settings, referenced by a column's `preset`. <small>(optional)</small></td></tr>
10187
+ <tr><td class="name">dataTypes</td><td class="type">Record&lt;string, DataType&gt;</td><td class="desc">Your own data types, alongside the built-in catalogue. <small>(optional)</small></td></tr>
10188
+ <tr><td class="name">sampleSize</td><td class="type">number</td><td class="desc">Values sampled per undeclared column when inferring its type. Default 100. <small>(optional)</small></td></tr>
10189
+ <tr><td class="name">targetSize</td><td class="type">'default' | 'large'</td><td class="desc">Raise every interactive target to a comfortable size for touch, without changing the type. `'large'` asks for it; `'default'` opts out of the coarse-pointer rule that would otherwise apply it. <small>(optional)</small></td></tr>
10190
+ <tr><td class="name">components</td><td class="type">Record&lt;string, RendererCtor | EditorCtor | FilterCtor&gt;</td><td class="desc">Your own renderers, editors and filters, registered by name. <small>(optional)</small></td></tr>
10191
+ <tr><td class="name">pipes</td><td class="type">Record&lt;string, (value: unknown, ...args: string[]) =&gt; string&gt;</td><td class="desc">Named text transforms usable from a format mask or a template. <small>(optional)</small></td></tr>
10192
+ <tr><td class="name">totalFns</td><td class="type">Record&lt;string, TotalFn&gt;</td><td class="desc">Your own reductions, alongside the built-in ones. <small>(optional)</small></td></tr>
10193
+ <tr><td class="name">variants</td><td class="type">Record&lt;string, VariantDefinition&gt;</td><td class="desc">Named appearance variants a row or cell can be switched into by a rule. <small>(optional)</small></td></tr>
10194
+ <tr><td class="name">tree</td><td class="type">TreeConfig</td><td class="desc">Hierarchical rows: where the parent link or the path lives. <small>(optional)</small></td></tr>
10195
+ <tr><td class="name">detail</td><td class="type">DetailConfig</td><td class="desc">The expandable panel beneath a row. <small>(optional)</small></td></tr>
10196
+ <tr><td class="name">selection</td><td class="type">SelectionConfig | 'single' | 'multiple' | 'none'</td><td class="desc">What the user may select, and how selection behaves across groups. The `'none'` shorthand is `{ mode: 'none' }` and behaves identically: no row selection, and no cell ranges or fill handle either. <small>(optional)</small></td></tr>
10197
+ <tr><td class="name">edit</td><td class="type">EditConfig | boolean</td><td class="desc">Editing, and how a change is committed and validated. <small>(optional)</small></td></tr>
10198
+ <tr><td class="name">pagination</td><td class="type">PaginationConfig | boolean</td><td class="desc">Page the rows rather than scrolling them. <small>(optional)</small></td></tr>
10199
+ <tr><td class="name">locale</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10200
+ <tr><td class="name">direction</td><td class="type">'ltr' | 'rtl' | 'auto'</td><td class="desc">Writing direction. Omit it, or say `'auto'`, to settle it from the element's own computed `dir` and then from `locale`: `ar`, `he`, `fa` and the rest resolve to `rtl`. In a right-to-left grid the logical alignments `start`/`end` mirror while the physical `left`/`right` do not (see {@link Align}). <small>(optional)</small></td></tr>
10201
+ <tr><td class="name">timeZone</td><td class="type">string</td><td class="desc">IANA zone every date column formats in, e.g. 'Europe/London' or 'UTC'. Omit to use each viewer's own zone. A column's own `format.timeZone` wins. <small>(optional)</small></td></tr>
9487
10202
  <tr><td class="name">theme</td><td class="type">Theme</td><td class="desc">The visual theme. <small>(optional)</small></td></tr>
9488
10203
  <tr><td class="name">density</td><td class="type">Density</td><td class="desc">Row height and padding as a named step, rather than pixel by pixel. <small>(optional)</small></td></tr>
9489
10204
  <tr><td class="name">gridLines</td><td class="type">boolean | 'both' | 'horizontal' | 'vertical' | 'none' | 'rows' | 'columns'</td><td class="desc">Which rules are drawn between cells. `'both'` by default. The two axes are separate decisions: horizontal rules help the eye track along a row, vertical ones stop adjacent values running together. `false` or `'none'` draws neither. Only the rules *between data* are affected, the header's underline, the pinned seams and the totals separator are structure, not grid lines. <small>(optional)</small></td></tr>
@@ -9880,6 +10595,714 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9880
10595
  </tbody>
9881
10596
  </table>
9882
10597
  </div>
10598
+ <h3 id="type-Kanban">Kanban</h3>
10599
+ <p class="section-note">A board instance: a kanban view of grid rows as cards grouped into columns. It consumes data through the same keyed-diff `rows.apply` contract a grid exposes, so `dataRouter.attach(value, board)` drives it like any other viewer.</p>
10600
+ <div class="table-wrap">
10601
+ <table>
10602
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10603
+ <tbody>
10604
+ <tr><td class="name">el</td><td class="type">unknown | null</td><td class="desc"><small>(read-only)</small></td></tr>
10605
+ <tr><td class="name">rowKey</td><td class="type">string | ((row: KanbanRow) =&gt; unknown)</td><td class="desc"><small>(read-only)</small></td></tr>
10606
+ <tr><td class="name">rows</td><td class="type">KanbanRows</td><td class="desc"></td></tr>
10607
+ <tr><td class="name">sla</td><td class="type">KanbanSla</td><td class="desc">The card-aging / SLA monitor, present only when a `sla` config was supplied (BACKLOG-0000960). <small>(optional)</small></td></tr>
10608
+ <tr><td class="name">columns</td><td class="type">(): KanbanColumn[]</td><td class="desc"></td></tr>
10609
+ <tr><td class="name">column</td><td class="type">(id: string): KanbanColumn | undefined</td><td class="desc"></td></tr>
10610
+ <tr><td class="name">count</td><td class="type">(id: string): number</td><td class="desc"></td></tr>
10611
+ <tr><td class="name">points</td><td class="type">(id: string): number</td><td class="desc"></td></tr>
10612
+ <tr><td class="name">cards</td><td class="type">(): KanbanCard[]</td><td class="desc"></td></tr>
10613
+ <tr><td class="name">card</td><td class="type">(key: unknown): KanbanCard | undefined</td><td class="desc"></td></tr>
10614
+ <tr><td class="name">on</td><td class="type">(name: string, fn: (event: KanbanEvent) =&gt; void): () =&gt; void</td><td class="desc"></td></tr>
10615
+ <tr><td class="name">off</td><td class="type">(name: string, fn: (event: KanbanEvent) =&gt; void): void</td><td class="desc"></td></tr>
10616
+ <tr><td class="name">readonly</td><td class="type">(scope?: { column?: string; card?: unknown }): boolean</td><td class="desc"></td></tr>
10617
+ <tr><td class="name">move</td><td class="type">(keys: unknown | unknown[], toColumn: string, toIndex?: number | null, toLane?: string): Promise&lt;{ moved: unknown[]; reverted: boolean }&gt;</td><td class="desc">Move one or more cards to a column (and, with an order property, to a position within it), through the `onBeforeMove` veto and the grid's shipped write-back path. The single entry point behind drag-and-drop and keyboard move.</td></tr>
10618
+ <tr><td class="name">selection</td><td class="type">(): unknown[]</td><td class="desc">The selected card keys.</td></tr>
10619
+ <tr><td class="name">isSelected</td><td class="type">(key: unknown): boolean</td><td class="desc">Whether a card is selected.</td></tr>
10620
+ <tr><td class="name">select</td><td class="type">(keys: unknown | unknown[], mode?: 'set' | 'add' | 'toggle' | 'remove'): Kanban</td><td class="desc">Change the selection: `set` (replace), `add`, `toggle` or `remove`.</td></tr>
10621
+ <tr><td class="name">clearSelection</td><td class="type">(): Kanban</td><td class="desc">Clear the selection.</td></tr>
10622
+ <tr><td class="name">collapseColumn</td><td class="type">(id: string, collapsed?: boolean): Kanban</td><td class="desc">Collapse, expand or toggle a column (emits `column:collapse`).</td></tr>
10623
+ <tr><td class="name">collapseLane</td><td class="type">(id: string, collapsed?: boolean): Kanban</td><td class="desc">Collapse, expand or toggle a swimlane (emits `swimlane:collapse`).</td></tr>
10624
+ <tr><td class="name">reorderColumns</td><td class="type">(order: string[]): Kanban</td><td class="desc">Reorder the columns to the given id order (emits `column:reorder`).</td></tr>
10625
+ <tr><td class="name">moveColumn</td><td class="type">(id: string, beforeId: string | null): Kanban</td><td class="desc">Move one column before another (or to the end); emits `column:reorder`.</td></tr>
10626
+ <tr><td class="name">reorderLanes</td><td class="type">(order: string[]): Kanban</td><td class="desc">Reorder the swimlanes to the given id order (emits `swimlane:reorder`).</td></tr>
10627
+ <tr><td class="name">moveLane</td><td class="type">(id: string, beforeId: string | null): Kanban</td><td class="desc">Move one swimlane before another (or to the end); emits `swimlane:reorder`.</td></tr>
10628
+ <tr><td class="name">filters</td><td class="type">KanbanFilters</td><td class="desc">Named card predicates, composed with AND (BACKLOG-0001229). See {@link KanbanFilters}.</td></tr>
10629
+ <tr><td class="name">setFilter</td><td class="type">(fn: ((row: KanbanRow, card: KanbanCard) =&gt; boolean) | null): Kanban</td><td class="desc">Set a predicate filter over cards, or clear it with null. Sugar for `filters.where(filters.DEFAULT, fn)`.</td></tr>
10630
+ <tr><td class="name">setQuickFilter</td><td class="type">(text: string): Kanban</td><td class="desc">Set the quick-filter text matched across card fields. Independent of every `filters.where` predicate.</td></tr>
10631
+ <tr><td class="name">facets</td><td class="type">(property: string): { value: unknown; count: number }[]</td><td class="desc">Distinct values of a property with card counts — the raw material for a facet control.</td></tr>
10632
+ <tr><td class="name">BACKLOG</td><td class="type">unknown</td><td class="desc">The sentinel `setSprint` value that selects the backlog (cards with no sprint). <small>(read-only)</small></td></tr>
10633
+ <tr><td class="name">setSprint</td><td class="type">(sprint: unknown): Kanban</td><td class="desc">Select the shown sprint (`BACKLOG` for the backlog, undefined for all); emits `sprint:changed`.</td></tr>
10634
+ <tr><td class="name">showBacklog</td><td class="type">(): Kanban</td><td class="desc">Show only the backlog (cards with no sprint).</td></tr>
10635
+ <tr><td class="name">setEpic</td><td class="type">(epic: unknown): Kanban</td><td class="desc">Select the shown epic (undefined for all); emits `epic:changed`.</td></tr>
10636
+ <tr><td class="name">sprints</td><td class="type">(): unknown[]</td><td class="desc">The distinct sprint values (the switcher's options); a configured `sprints` dataset pins the order.</td></tr>
10637
+ <tr><td class="name">sprintDefs</td><td class="type">(): { id: unknown; title: string }[]</td><td class="desc">The sprint dataset as `{ id, title }` descriptors — the configured list plus any data-only sprint.</td></tr>
10638
+ <tr><td class="name">epics</td><td class="type">(): unknown[]</td><td class="desc">The distinct epic values.</td></tr>
10639
+ <tr><td class="name">rollup</td><td class="type">(property: string): { value: unknown; count: number; points: number; doneCount: number; donePoints: number; progress: number }[]</td><td class="desc">Roll rows up by a property: per-bucket count, points, done and progress.</td></tr>
10640
+ <tr><td class="name">epicRollup</td><td class="type">(): { value: unknown; count: number; points: number; doneCount: number; donePoints: number; progress: number }[]</td><td class="desc">The epic rollup (empty when no epic property is configured).</td></tr>
10641
+ <tr><td class="name">canExpand</td><td class="type">(card: KanbanCard): boolean</td><td class="desc">Whether a card can be expanded to a child pop-out.</td></tr>
10642
+ <tr><td class="name">expand</td><td class="type">(key: unknown): Promise&lt;object | null&gt;</td><td class="desc">Open a card's children in a pop-out (drawer/modal/inline); emits `card:expand`/`card:drill`.</td></tr>
10643
+ <tr><td class="name">closeDetail</td><td class="type">(): Kanban</td><td class="desc">Close any open card pop-out.</td></tr>
10644
+ <tr><td class="name">isFieldEditable</td><td class="type">(name: string): boolean</td><td class="desc">Whether a mapped card field is opted into inline edit and writable.</td></tr>
10645
+ <tr><td class="name">editCard</td><td class="type">(key: unknown, name?: string): object | null</td><td class="desc">Start inline editing a card's field (the grid's own field editor when bound); no-op headless.</td></tr>
10646
+ <tr><td class="name">applyEdit</td><td class="type">(key: unknown, name: string, value: unknown): Promise&lt;boolean&gt;</td><td class="desc">Commit an inline edit through the write-back path (grid.edit.setCells when bound); emits `card:edit`.</td></tr>
10647
+ <tr><td class="name">addCard</td><td class="type">(columnId: string, seed?: KanbanRow): unknown | Promise&lt;unknown&gt;</td><td class="desc">Add a card to a column and open it in inline edit; emits `card:add`. Returns the new key directly, or a Promise of it when `onAddCard` returns a Promise or a `beforeAdd` handler defers (BACKLOG-0001230); a rejected `onAddCard` Promise resolves this to `null` with no card added.</td></tr>
10648
+ <tr><td class="name">getState</td><td class="type">(): object</td><td class="desc">Serialise the restorable state: collapsed columns/lanes, order, filter, sprint/epic, selection.</td></tr>
10649
+ <tr><td class="name">setState</td><td class="type">(snapshot: object): Kanban</td><td class="desc">Restore a state snapshot from {@link Kanban#getState}.</td></tr>
10650
+ <tr><td class="name">setLoading</td><td class="type">(loading: boolean): Kanban</td><td class="desc">Mark the board loading (renders a host-localised loading state).</td></tr>
10651
+ <tr><td class="name">setError</td><td class="type">(message: string | null): Kanban</td><td class="desc">Set (or clear with null) an error state, rendered as a host-supplied message.</td></tr>
10652
+ <tr><td class="name">setRows</td><td class="type">(rows: KanbanRow[]): Kanban</td><td class="desc"></td></tr>
10653
+ <tr><td class="name">setColumns</td><td class="type">(defs: KanbanColumnDef[]): Kanban</td><td class="desc">Replace the board's configured column set (BACKLOG-0001228). Keeps card placement and interaction state (collapsed columns, column order, quick filter, selection) for every column id that survives; a dropped id is not specially handled — a card whose value has nowhere configured to go re-derives an ad hoc column rather than becoming `unplaced` (the same "never silently drop a card" rule an unconfigured value already gets).</td></tr>
10654
+ <tr><td class="name">refresh</td><td class="type">(): Kanban</td><td class="desc"></td></tr>
10655
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc"></td></tr>
10656
+ </tbody>
10657
+ </table>
10658
+ </div>
10659
+ <h3 id="type-KanbanCard">KanbanCard</h3>
10660
+ <p class="section-note">A card model — one row as it appears on the board. `fields` holds the resolved display text for each mapped card field; `columnId` is the column the card sits in; `points` is the numeric points value (0 when absent). `swimlane`/`sprint`/`epic`/`order` are read from their configured properties and carried for the later cycles that render them.</p>
10661
+ <div class="table-wrap">
10662
+ <table>
10663
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10664
+ <tbody>
10665
+ <tr><td class="name">key</td><td class="type">unknown</td><td class="desc"></td></tr>
10666
+ <tr><td class="name">row</td><td class="type">KanbanRow</td><td class="desc"></td></tr>
10667
+ <tr><td class="name">columnId</td><td class="type">string | null</td><td class="desc"></td></tr>
10668
+ <tr><td class="name">points</td><td class="type">number</td><td class="desc"></td></tr>
10669
+ <tr><td class="name">hasPoints</td><td class="type">boolean</td><td class="desc"></td></tr>
10670
+ <tr><td class="name">order</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10671
+ <tr><td class="name">swimlane</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10672
+ <tr><td class="name">sprint</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10673
+ <tr><td class="name">epic</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10674
+ <tr><td class="name">fields</td><td class="type">Record&lt;string, string&gt;</td><td class="desc"></td></tr>
10675
+ </tbody>
10676
+ </table>
10677
+ </div>
10678
+ <h3 id="type-KanbanCardMap">KanbanCardMap</h3>
10679
+ <p class="section-note">The field-to-property mapping that drives the card template.</p>
10680
+ <div class="table-wrap">
10681
+ <table>
10682
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10683
+ <tbody>
10684
+ <tr><td class="name">title</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10685
+ <tr><td class="name">subtitle</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10686
+ <tr><td class="name">labels</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10687
+ <tr><td class="name">assignee</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10688
+ <tr><td class="name">due</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10689
+ <tr><td class="name">cover</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10690
+ <tr><td class="name">progress</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10691
+ <tr><td class="name">badges</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10692
+ <tr><td class="name">accent</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10693
+ </tbody>
10694
+ </table>
10695
+ </div>
10696
+ <h3 id="type-KanbanChildren">KanbanChildren</h3>
10697
+ <p class="section-note">Card pop-out configuration. The child view is a full composed grid (via `factory`, a `createGrid`), a nested board (`asBoard`), or a custom `render`. The child set is the rows whose `property` equals the card key, or the `load(card)` result. Recursion falls out: a nested board can pop its own children.</p>
10698
+ <div class="table-wrap">
10699
+ <table>
10700
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10701
+ <tbody>
10702
+ <tr><td class="name">property</td><td class="type">string</td><td class="desc">Parent-id property linking child rows to a card within the same dataset. <small>(optional)</small></td></tr>
10703
+ <tr><td class="name">load</td><td class="type">(card: KanbanCard) =&gt; KanbanRow[] | Promise&lt;KanbanRow[]&gt;</td><td class="desc">Per-card child rows, sync or async — an alternative (or addition) to `property`. <small>(optional)</small></td></tr>
10704
+ <tr><td class="name">hasChildren</td><td class="type">(card: KanbanCard) =&gt; boolean</td><td class="desc">Whether a card can be expanded, overriding the property/load inference. <small>(optional)</small></td></tr>
10705
+ <tr><td class="name">present</td><td class="type">'drawer' | 'modal' | 'inline'</td><td class="desc">Where the pop-out appears (default `drawer`). <small>(optional)</small></td></tr>
10706
+ <tr><td class="name">factory</td><td class="type">(container: HTMLElement, options: object) =&gt; { destroy?: () =&gt; void }</td><td class="desc">The grid factory (a `createGrid`) that builds the child grid. <small>(optional)</small></td></tr>
10707
+ <tr><td class="name">asBoard</td><td class="type">boolean</td><td class="desc">Make the child a nested board (recursive) instead of a grid. <small>(optional)</small></td></tr>
10708
+ <tr><td class="name">gridOptions</td><td class="type">object | ((card: KanbanCard) =&gt; object)</td><td class="desc">Options for the child grid/board — an object or `fn(card)`. <small>(optional)</small></td></tr>
10709
+ <tr><td class="name">render</td><td class="type">(container: HTMLElement, ctx: { card: KanbanCard; rows: KanbanRow[]; board: Kanban; depth: number }) =&gt; (void | (() =&gt; void))</td><td class="desc">Fully custom child render; returns a cleanup function. <small>(optional)</small></td></tr>
10710
+ <tr><td class="name">title</td><td class="type">(card: KanbanCard) =&gt; string</td><td class="desc">The pop-out title (default the card title). <small>(optional)</small></td></tr>
10711
+ </tbody>
10712
+ </table>
10713
+ </div>
10714
+ <h3 id="type-KanbanColumn">KanbanColumn</h3>
10715
+ <p class="section-note">A column with its cards and aggregates. `over` is true when `count` exceeds `wipLimit`.</p>
10716
+ <div class="table-wrap">
10717
+ <table>
10718
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10719
+ <tbody>
10720
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
10721
+ <tr><td class="name">title</td><td class="type">string</td><td class="desc"></td></tr>
10722
+ <tr><td class="name">color</td><td class="type">string | null</td><td class="desc"></td></tr>
10723
+ <tr><td class="name">wipLimit</td><td class="type">number | null</td><td class="desc"></td></tr>
10724
+ <tr><td class="name">collapsed</td><td class="type">boolean</td><td class="desc"></td></tr>
10725
+ <tr><td class="name">cards</td><td class="type">KanbanCard[]</td><td class="desc"></td></tr>
10726
+ <tr><td class="name">count</td><td class="type">number</td><td class="desc"></td></tr>
10727
+ <tr><td class="name">points</td><td class="type">number</td><td class="desc"></td></tr>
10728
+ <tr><td class="name">over</td><td class="type">boolean</td><td class="desc"></td></tr>
10729
+ </tbody>
10730
+ </table>
10731
+ </div>
10732
+ <h3 id="type-KanbanConfig">KanbanConfig</h3>
10733
+ <p class="section-note">Kanban configuration. Every structural property is named here so the same board maps DemandFlow (a status field, `points`, `sprint`, `epic`, a swimlane property) and any customer schema without code change.</p>
10734
+ <div class="table-wrap">
10735
+ <table>
10736
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10737
+ <tbody>
10738
+ <tr><td class="name">rows</td><td class="type">KanbanRow[]</td><td class="desc"><small>(optional)</small></td></tr>
10739
+ <tr><td class="name">grid</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10740
+ <tr><td class="name">rowKey</td><td class="type">string | ((row: KanbanRow) =&gt; unknown)</td><td class="desc"><small>(optional)</small></td></tr>
10741
+ <tr><td class="name">columnProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10742
+ <tr><td class="name">columns</td><td class="type">KanbanColumnDef[]</td><td class="desc"><small>(optional)</small></td></tr>
10743
+ <tr><td class="name">columnOrder</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
10744
+ <tr><td class="name">pointsProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10745
+ <tr><td class="name">showPoints</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
10746
+ <tr><td class="name">orderProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10747
+ <tr><td class="name">swimlaneProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10748
+ <tr><td class="name">swimlanes</td><td class="type">boolean</td><td class="desc">Render the 2D swimlane layout using `swimlaneProperty` (default false). <small>(optional)</small></td></tr>
10749
+ <tr><td class="name">lanes</td><td class="type">(string | { id: string; title?: string })[]</td><td class="desc">Explicit lane definitions; otherwise lanes come from the distinct swimlane values. <small>(optional)</small></td></tr>
10750
+ <tr><td class="name">laneOrder</td><td class="type">string[]</td><td class="desc">An explicit lane order by id (also set by a lane-header-drag reorder). <small>(optional)</small></td></tr>
10751
+ <tr><td class="name">enforceWip</td><td class="type">boolean</td><td class="desc">Enforce `wipLimit` as a hard gate: a move that would exceed it is refused (default false). <small>(optional)</small></td></tr>
10752
+ <tr><td class="name">cardRenderer</td><td class="type">(card: KanbanCard, ctx: { column: KanbanColumn; readonly: boolean; el: HTMLElement; doc: Document }) =&gt; string | Node | void</td><td class="desc">A custom card template: return an HTML string or a DOM node to own the whole card body. <small>(optional)</small></td></tr>
10753
+ <tr><td class="name">sprintProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10754
+ <tr><td class="name">epicProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10755
+ <tr><td class="name">sprints</td><td class="type">(string | { id: unknown; title?: string })[]</td><td class="desc">A configurable sprint dataset: the canonical sprint list (order + titles), shown even when empty. <small>(optional)</small></td></tr>
10756
+ <tr><td class="name">sprint</td><td class="type">unknown</td><td class="desc">The initially selected sprint id, `Kanban.BACKLOG`, or undefined for all. <small>(optional)</small></td></tr>
10757
+ <tr><td class="name">epic</td><td class="type">unknown</td><td class="desc">The initially selected epic id, or undefined for all. <small>(optional)</small></td></tr>
10758
+ <tr><td class="name">doneColumns</td><td class="type">string[]</td><td class="desc">Column ids that count as "done" for a rollup's progress (also a column def's `done: true`). <small>(optional)</small></td></tr>
10759
+ <tr><td class="name">children</td><td class="type">KanbanChildren</td><td class="desc">Card pop-out: a nested child grid or board (master-detail by composition). <small>(optional)</small></td></tr>
10760
+ <tr><td class="name">virtualize</td><td class="type">boolean | { rowHeight?: number; overscan?: number; threshold?: number; viewport?: number }</td><td class="desc">Card virtualization for tall columns: true, or `{ rowHeight, overscan, threshold, viewport }`. <small>(optional)</small></td></tr>
10761
+ <tr><td class="name">sla</td><td class="type">KanbanSlaConfig</td><td class="desc">Card aging / SLA highlighting (BACKLOG-0000960): warn/breach thresholds (globally, per column and/or per lane) that age each card and fire `card:sla` on a rising crossing. Opt-in; reached at runtime as {@link Kanban#sla}. See {@link KanbanSlaConfig}. <small>(optional)</small></td></tr>
10762
+ <tr><td class="name">state</td><td class="type">object</td><td class="desc">A saved board state (from `getState`) to restore on construction. <small>(optional)</small></td></tr>
10763
+ <tr><td class="name">addCard</td><td class="type">boolean</td><td class="desc">Show a per-column add-card affordance. <small>(optional)</small></td></tr>
10764
+ <tr><td class="name">onCardEdit</td><td class="type">(event: { card: KanbanCard; key: unknown; field: string; fieldPath: string; value: unknown }) =&gt; boolean | void | Promise&lt;boolean | void&gt;</td><td class="desc">Persist a standalone inline edit; return false or a rejected promise to revert. <small>(optional)</small></td></tr>
10765
+ <tr><td class="name">onAddCard</td><td class="type">(columnId: string) =&gt; KanbanRow | Promise&lt;KanbanRow&gt; | void</td><td class="desc">Create a card for a column on add-card; return the row to create (with its key), a Promise of that row, or nothing to auto-generate. A rejected Promise creates no card and leaves the board unchanged (BACKLOG-0001230). <small>(optional)</small></td></tr>
10766
+ <tr><td class="name">filter</td><td class="type">(row: KanbanRow, card: KanbanCard) =&gt; boolean</td><td class="desc">A predicate filter over cards; only matching cards are shown. <small>(optional)</small></td></tr>
10767
+ <tr><td class="name">quickFilter</td><td class="type">string</td><td class="desc">Quick-filter text matched case-insensitively across card fields. <small>(optional)</small></td></tr>
10768
+ <tr><td class="name">card</td><td class="type">KanbanCardMap</td><td class="desc"><small>(optional)</small></td></tr>
10769
+ <tr><td class="name">readonly</td><td class="type">KanbanReadonly</td><td class="desc"><small>(optional)</small></td></tr>
10770
+ <tr><td class="name">ariaLabel</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10771
+ <tr><td class="name">emptyText</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10772
+ <tr><td class="name">selectable</td><td class="type">boolean</td><td class="desc">Whether card selection is enabled (default true). <small>(optional)</small></td></tr>
10773
+ <tr><td class="name">labels</td><td class="type">Record&lt;string, string&gt;</td><td class="desc">Host-localised words for the move announcements (grabbed/moved/dropped/reverted/cancelled). <small>(optional)</small></td></tr>
10774
+ <tr><td class="name">onBeforeMove</td><td class="type">(card: KanbanCard, from: string | null, to: string, index: number | null) =&gt; boolean | Promise&lt;boolean&gt;</td><td class="desc">Veto/confirm a move before any write. Return `false` (or a promise of it) to refuse; `from`/`to` are column ids, `index` the target position. <small>(optional)</small></td></tr>
10775
+ <tr><td class="name">onCardMove</td><td class="type">(event: KanbanMoveEvent) =&gt; boolean | void | Promise&lt;boolean | void&gt;</td><td class="desc">Persist a move on a standalone (non-grid) board. Return `false` or a rejected promise to revert the optimistic move. On a grid-bound board the grid's write-back pipeline persists instead and this is not called. <small>(optional)</small></td></tr>
10776
+ <tr><td class="name">contextMenu</td><td class="type">KanbanMenuItem[] | ((card: KanbanCard, selected: KanbanCard[]) =&gt; KanbanMenuItem[])</td><td class="desc">A per-card context menu: items, or `fn(card, selectedCards)` returning items. Suppresses `card:contextmenu`. <small>(optional)</small></td></tr>
10777
+ <tr><td class="name">onCardClick</td><td class="type">(event: KanbanEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10778
+ <tr><td class="name">onCardDblClick</td><td class="type">(event: KanbanEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10779
+ <tr><td class="name">onCardContextMenu</td><td class="type">(event: KanbanEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10780
+ </tbody>
10781
+ </table>
10782
+ </div>
10783
+ <h3 id="type-KanbanEditor">KanbanEditor</h3>
10784
+ <p class="section-note">A card field editor handle returned by a host editor factory.</p>
10785
+ <div class="table-wrap">
10786
+ <table>
10787
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10788
+ <tbody>
10789
+ <tr><td class="name">el</td><td class="type">HTMLElement</td><td class="desc"></td></tr>
10790
+ <tr><td class="name">focus</td><td class="type">() =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10791
+ <tr><td class="name">destroy</td><td class="type">() =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10792
+ </tbody>
10793
+ </table>
10794
+ </div>
10795
+ <h3 id="type-KanbanEvent">KanbanEvent</h3>
10796
+ <p class="section-note">The payload every board event carries.</p>
10797
+ <div class="table-wrap">
10798
+ <table>
10799
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10800
+ <tbody>
10801
+ <tr><td class="name">card</td><td class="type">KanbanCard</td><td class="desc"></td></tr>
10802
+ <tr><td class="name">column</td><td class="type">string | null</td><td class="desc"></td></tr>
10803
+ <tr><td class="name">el</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10804
+ <tr><td class="name">originalEvent</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10805
+ </tbody>
10806
+ </table>
10807
+ </div>
10808
+ <h3 id="type-KanbanFilters">KanbanFilters</h3>
10809
+ <p class="section-note">Named card predicates, composed with AND (BACKLOG-0001229), following the grid's `filters.where` convention (BACKLOG-0001202). Several may be registered under different names at once; each can be replaced or removed without touching the others. `setFilter(fn)` is unchanged sugar for `where(DEFAULT, fn)` / `where(DEFAULT, null)`.</p>
10810
+ <div class="table-wrap">
10811
+ <table>
10812
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10813
+ <tbody>
10814
+ <tr><td class="name">DEFAULT</td><td class="type">string</td><td class="desc">The reserved name `board.setFilter` registers/removes under. <small>(read-only)</small></td></tr>
10815
+ <tr><td class="name">where</td><td class="type">(): string[]</td><td class="desc">The registered names, in registration order.</td></tr>
10816
+ <tr><td class="name">where</td><td class="type">(name: string, predicate: (row: KanbanRow, card: KanbanCard) =&gt; boolean): Kanban</td><td class="desc">Register or replace the predicate under `name`.</td></tr>
10817
+ <tr><td class="name">where</td><td class="type">(name: string, predicate: null): Kanban</td><td class="desc">Remove whatever is registered under `name`; a no-op if nothing was.</td></tr>
10818
+ <tr><td class="name">reapply</td><td class="type">(name?: string): boolean</td><td class="desc">Re-run every named predicate (or one, by name) and re-render.</td></tr>
10819
+ </tbody>
10820
+ </table>
10821
+ </div>
10822
+ <h3 id="type-KanbanMenuItem">KanbanMenuItem</h3>
10823
+ <p class="section-note">One context-menu item. `action` receives the card, the selected cards, and the board.</p>
10824
+ <div class="table-wrap">
10825
+ <table>
10826
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10827
+ <tbody>
10828
+ <tr><td class="name">label</td><td class="type">string</td><td class="desc"></td></tr>
10829
+ <tr><td class="name">action</td><td class="type">(ctx: { card: KanbanCard; cards: KanbanCard[]; board: Kanban }) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10830
+ <tr><td class="name">disabled</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
10831
+ </tbody>
10832
+ </table>
10833
+ </div>
10834
+ <h3 id="type-KanbanMoveEvent">KanbanMoveEvent</h3>
10835
+ <p class="section-note">The payload of a `card:move` (and `card:reverted`) event.</p>
10836
+ <div class="table-wrap">
10837
+ <table>
10838
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10839
+ <tbody>
10840
+ <tr><td class="name">keys</td><td class="type">unknown[]</td><td class="desc"></td></tr>
10841
+ <tr><td class="name">cards</td><td class="type">KanbanCard[]</td><td class="desc"></td></tr>
10842
+ <tr><td class="name">from</td><td class="type">(string | null)[]</td><td class="desc"></td></tr>
10843
+ <tr><td class="name">to</td><td class="type">string</td><td class="desc"></td></tr>
10844
+ <tr><td class="name">index</td><td class="type">number | null</td><td class="desc"></td></tr>
10845
+ <tr><td class="name">orders</td><td class="type">number[] | null</td><td class="desc"></td></tr>
10846
+ </tbody>
10847
+ </table>
10848
+ </div>
10849
+ <h3 id="type-KanbanRows">KanbanRows</h3>
10850
+ <p class="section-note">The keyed-diff consumer surface a board shares with a grid, so a Data Router routes to it directly.</p>
10851
+ <div class="table-wrap">
10852
+ <table>
10853
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10854
+ <tbody>
10855
+ <tr><td class="name">apply</td><td class="type">(change: { add?: KanbanRow[]; update?: KanbanRow[]; remove?: unknown[] }): void</td><td class="desc"></td></tr>
10856
+ <tr><td class="name">forEach</td><td class="type">(fn: (row: KanbanRow, key: unknown) =&gt; void): void</td><td class="desc"></td></tr>
10857
+ <tr><td class="name">count</td><td class="type">number</td><td class="desc"><small>(read-only)</small></td></tr>
10858
+ </tbody>
10859
+ </table>
10860
+ </div>
10861
+ <h3 id="type-KanbanSla">KanbanSla</h3>
10862
+ <p class="section-note">The card-aging / SLA monitor (BACKLOG-0000960), reached as {@link Kanban#sla} when a `sla` config is supplied. Pure and DOM-free: it computes each card's ageing state from the board's card model and the flow transition log, and the view paints it.</p>
10863
+ <div class="table-wrap">
10864
+ <table>
10865
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10866
+ <tbody>
10867
+ <tr><td class="name">config</td><td class="type">object</td><td class="desc">The normalised SLA config (read-only). <small>(read-only)</small></td></tr>
10868
+ <tr><td class="name">sync</td><td class="type">(): KanbanSla</td><td class="desc">Recompute every card's SLA state without emitting anything.</td></tr>
10869
+ <tr><td class="name">evaluate</td><td class="type">(opts?: { emit?: boolean }): KanbanSlaState[]</td><td class="desc">Recompute and fire `card:sla`/`onWarn`/`onBreach` on each rising crossing.</td></tr>
10870
+ <tr><td class="name">start</td><td class="type">(): KanbanSla</td><td class="desc">Establish the baseline, notify on the current state, and start the optional tick.</td></tr>
10871
+ <tr><td class="name">stateFor</td><td class="type">(cardOrKey: KanbanCard | unknown): KanbanSlaState | null</td><td class="desc">The SLA state of one card (by card model or key), or null when unknown.</td></tr>
10872
+ <tr><td class="name">states</td><td class="type">(): KanbanSlaState[]</td><td class="desc">Every card's current SLA state.</td></tr>
10873
+ <tr><td class="name">breaches</td><td class="type">(): KanbanSlaState[]</td><td class="desc">The cards currently at breach level.</td></tr>
10874
+ <tr><td class="name">warnings</td><td class="type">(): KanbanSlaState[]</td><td class="desc">The cards currently at warn level (not yet breached).</td></tr>
10875
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc">Stop the tick and drop the board subscriptions.</td></tr>
10876
+ </tbody>
10877
+ </table>
10878
+ </div>
10879
+ <h3 id="type-KanbanSlaConfig">KanbanSlaConfig</h3>
10880
+ <p class="section-note">Card-aging / SLA configuration (BACKLOG-0000960). A card is measured against a `warn` and a `breach` threshold; the view puts an age chip on aged cards and a highlight on breached ones, and a rising crossing fires the `card:sla` event and the matching `onWarn`/`onBreach` callback (signature `(level, rows)`, the Data Router alert handler's). Thresholds resolve most-specific-first: lane → column → global. Reached at runtime as {@link Kanban#sla}.</p>
10881
+ <div class="table-wrap">
10882
+ <table>
10883
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10884
+ <tbody>
10885
+ <tr><td class="name">warn</td><td class="type">KanbanSlaThreshold</td><td class="desc">The global warn threshold. <small>(optional)</small></td></tr>
10886
+ <tr><td class="name">breach</td><td class="type">KanbanSlaThreshold</td><td class="desc">The global breach threshold. <small>(optional)</small></td></tr>
10887
+ <tr><td class="name">columns</td><td class="type">Record&lt;string, KanbanSlaThreshold | { warn?: KanbanSlaThreshold; breach?: KanbanSlaThreshold }&gt;</td><td class="desc">Per-column overrides by column id (each a threshold or a `{ warn, breach }` pair). <small>(optional)</small></td></tr>
10888
+ <tr><td class="name">lanes</td><td class="type">Record&lt;string, KanbanSlaThreshold | { warn?: KanbanSlaThreshold; breach?: KanbanSlaThreshold }&gt;</td><td class="desc">Per-swimlane overrides by lane id (each a threshold or a `{ warn, breach }` pair). <small>(optional)</small></td></tr>
10889
+ <tr><td class="name">basis</td><td class="type">'column' | 'board'</td><td class="desc">Where the ageing clock starts: `'column'` (default) measures time in the card's current column; `'board'` measures age since the card arrived/was created. <small>(optional)</small></td></tr>
10890
+ <tr><td class="name">enteredProperty</td><td class="type">string</td><td class="desc">A row property holding the wall-clock time the card entered its column. <small>(optional)</small></td></tr>
10891
+ <tr><td class="name">createdProperty</td><td class="type">string</td><td class="desc">A row property holding the wall-clock time the card was created. <small>(optional)</small></td></tr>
10892
+ <tr><td class="name">ignoreDone</td><td class="type">boolean</td><td class="desc">Whether cards in a done column are exempt from ageing (default true). <small>(optional)</small></td></tr>
10893
+ <tr><td class="name">useTransitionLog</td><td class="type">boolean</td><td class="desc">Whether the flow transition log drives the ageing basis when present (default true). <small>(optional)</small></td></tr>
10894
+ <tr><td class="name">showAge</td><td class="type">'always' | 'threshold'</td><td class="desc">Show the age chip on every aged card (`'always'`), or only on warn/breach (`'threshold'`, default). <small>(optional)</small></td></tr>
10895
+ <tr><td class="name">now</td><td class="type">() =&gt; number</td><td class="desc">A wall-clock epoch clock, injectable for deterministic tests (default `Date.now`). <small>(optional)</small></td></tr>
10896
+ <tr><td class="name">tick</td><td class="type">number</td><td class="desc">A re-check interval in ms so a card breaching by sitting still still lights up (0 = off). <small>(optional)</small></td></tr>
10897
+ <tr><td class="name">onWarn</td><td class="type">(level: 'warn' | 'breach', rows: KanbanRow[]) =&gt; void</td><td class="desc">Called on a rising crossing to warn level, `(level, rows)` — the router alert handler's shape. <small>(optional)</small></td></tr>
10898
+ <tr><td class="name">onBreach</td><td class="type">(level: 'warn' | 'breach', rows: KanbanRow[]) =&gt; void</td><td class="desc">Called on a rising crossing to breach level, `(level, rows)` — the router alert handler's shape. <small>(optional)</small></td></tr>
10899
+ </tbody>
10900
+ </table>
10901
+ </div>
10902
+ <h3 id="type-KanbanSlaState">KanbanSlaState</h3>
10903
+ <p class="section-note">The computed SLA state of one card (BACKLOG-0000960).</p>
10904
+ <div class="table-wrap">
10905
+ <table>
10906
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10907
+ <tbody>
10908
+ <tr><td class="name">key</td><td class="type">unknown</td><td class="desc"></td></tr>
10909
+ <tr><td class="name">columnId</td><td class="type">string | null</td><td class="desc"></td></tr>
10910
+ <tr><td class="name">lane</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10911
+ <tr><td class="name">start</td><td class="type">number | null</td><td class="desc">The ageing-clock start epoch (ms), or null when no time source could be resolved.</td></tr>
10912
+ <tr><td class="name">ageMs</td><td class="type">number | null</td><td class="desc">The card's age in ms, or null when unknown.</td></tr>
10913
+ <tr><td class="name">ageText</td><td class="type">string</td><td class="desc">A short human age label (`2d`, `5h`, …), '' when unknown.</td></tr>
10914
+ <tr><td class="name">warnMs</td><td class="type">number | null</td><td class="desc">The resolved warn threshold in ms, or null.</td></tr>
10915
+ <tr><td class="name">breachMs</td><td class="type">number | null</td><td class="desc">The resolved breach threshold in ms, or null.</td></tr>
10916
+ <tr><td class="name">level</td><td class="type">'ok' | 'warn' | 'breach' | null</td><td class="desc">The classified level, or null when the card cannot be aged.</td></tr>
10917
+ <tr><td class="name">breached</td><td class="type">boolean</td><td class="desc">True when `level` is `'breach'`.</td></tr>
10918
+ </tbody>
10919
+ </table>
10920
+ </div>
10921
+ <h3 id="type-KPI">KPI</h3>
10922
+ <p class="section-note">A KPI / stat-tile panel: a grid of aggregate tiles over a dataset. It consumes data through the same keyed-diff `rows.apply` contract a grid exposes, so `dataRouter.attach(value, kpi)` drives it like any other viewer, updating each tile incrementally from the routed delta.</p>
10923
+ <div class="table-wrap">
10924
+ <table>
10925
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10926
+ <tbody>
10927
+ <tr><td class="name">el</td><td class="type">unknown | null</td><td class="desc"><small>(read-only)</small></td></tr>
10928
+ <tr><td class="name">rowKey</td><td class="type">string | ((row: KPIRow) =&gt; unknown)</td><td class="desc"><small>(read-only)</small></td></tr>
10929
+ <tr><td class="name">tree</td><td class="type">boolean</td><td class="desc">Whether the panel renders as a hierarchy rather than a flat tile grid. <small>(read-only)</small></td></tr>
10930
+ <tr><td class="name">rows</td><td class="type">KPIRows</td><td class="desc"></td></tr>
10931
+ <tr><td class="name">tiles</td><td class="type">(): KPITileModel[]</td><td class="desc"></td></tr>
10932
+ <tr><td class="name">tile</td><td class="type">(id: string): KPITileModel | undefined</td><td class="desc"></td></tr>
10933
+ <tr><td class="name">value</td><td class="type">(id: string): unknown</td><td class="desc"></td></tr>
10934
+ <tr><td class="name">nodes</td><td class="type">(): KPINodeModel[]</td><td class="desc">The top-level nodes of the hierarchy. Empty on a flat panel.</td></tr>
10935
+ <tr><td class="name">node</td><td class="type">(key: string): KPINodeModel | undefined</td><td class="desc">One node by its key, at any depth.</td></tr>
10936
+ <tr><td class="name">visibleNodes</td><td class="type">(): KPINodeModel[]</td><td class="desc">The nodes on screen: the roots, plus the children of every open branch.</td></tr>
10937
+ <tr><td class="name">expand</td><td class="type">(key: string): KPI</td><td class="desc"></td></tr>
10938
+ <tr><td class="name">collapse</td><td class="type">(key: string): KPI</td><td class="desc"></td></tr>
10939
+ <tr><td class="name">toggle</td><td class="type">(key: string): KPI</td><td class="desc"></td></tr>
10940
+ <tr><td class="name">setRows</td><td class="type">(rows: KPIRow[]): KPI</td><td class="desc"></td></tr>
10941
+ <tr><td class="name">refresh</td><td class="type">(): KPI</td><td class="desc"></td></tr>
10942
+ <tr><td class="name">getState</td><td class="type">(): object</td><td class="desc"></td></tr>
10943
+ <tr><td class="name">setState</td><td class="type">(snapshot: object): KPI</td><td class="desc"></td></tr>
10944
+ <tr><td class="name">on</td><td class="type">(name: string, fn: (event: KPIEvent) =&gt; void): () =&gt; void</td><td class="desc"></td></tr>
10945
+ <tr><td class="name">off</td><td class="type">(name: string, fn: (event: KPIEvent) =&gt; void): void</td><td class="desc"></td></tr>
10946
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc"></td></tr>
10947
+ </tbody>
10948
+ </table>
10949
+ </div>
10950
+ <h3 id="type-KPIBand">KPIBand</h3>
10951
+ <p class="section-note">An explicit band: the `status` of the first band whose half-open `[min, max)` contains the value.</p>
10952
+ <div class="table-wrap">
10953
+ <table>
10954
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10955
+ <tbody>
10956
+ <tr><td class="name">min</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10957
+ <tr><td class="name">max</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10958
+ <tr><td class="name">status</td><td class="type">'good' | 'warn' | 'critical'</td><td class="desc"></td></tr>
10959
+ </tbody>
10960
+ </table>
10961
+ </div>
10962
+ <h3 id="type-KPIConfig">KPIConfig</h3>
10963
+ <p class="section-note">KPI panel configuration.</p>
10964
+ <div class="table-wrap">
10965
+ <table>
10966
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10967
+ <tbody>
10968
+ <tr><td class="name">rows</td><td class="type">KPIRow[]</td><td class="desc"><small>(optional)</small></td></tr>
10969
+ <tr><td class="name">grid</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10970
+ <tr><td class="name">rowKey</td><td class="type">string | ((row: KPIRow) =&gt; unknown)</td><td class="desc"><small>(optional)</small></td></tr>
10971
+ <tr><td class="name">fields</td><td class="type">string[]</td><td class="desc">Extra columns of the bound `grid` to project onto the rows a tile `filter` sees, beyond the fields the tiles themselves declare. A grid-bound panel hands a filter a projection, not a whole grid row, so a filter over a column no tile names would otherwise read `undefined` and report a confident zero. Ignored on a panel over a plain `rows` array. <small>(optional)</small></td></tr>
10972
+ <tr><td class="name">tiles</td><td class="type">KPITile[]</td><td class="desc"><small>(optional)</small></td></tr>
10973
+ <tr><td class="name">columns</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10974
+ <tr><td class="name">ariaLabel</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10975
+ <tr><td class="name">nullText</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10976
+ <tr><td class="name">tree</td><td class="type">KPITreeConfig | false</td><td class="desc">Arrange the tiles as a hierarchy; `false` keeps the panel flat. <small>(optional)</small></td></tr>
10977
+ <tr><td class="name">messages</td><td class="type">{ t(key: string, params?: Record&lt;string, unknown&gt;): string }</td><td class="desc">The catalogue the panel's own text is read from. A panel routinely has no grid to borrow one off — two of its three input modes have none — so this is the first-class way to translate it. A grid's own `messages` satisfies the shape; a key it does not carry falls back to English. <small>(optional)</small></td></tr>
10978
+ <tr><td class="name">onTileClick</td><td class="type">(event: KPIEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10979
+ <tr><td class="name">onTileDblClick</td><td class="type">(event: KPIEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10980
+ <tr><td class="name">onTileContextMenu</td><td class="type">(event: KPIEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10981
+ <tr><td class="name">onNodeToggle</td><td class="type">(event: { key: string; expanded: boolean; node?: KPINodeModel }) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10982
+ <tr><td class="name">onChange</td><td class="type">(event: { model: { tiles: KPITileModel[]; nodes?: KPINodeModel[] } }) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10983
+ </tbody>
10984
+ </table>
10985
+ </div>
10986
+ <h3 id="type-KPIEvent">KPIEvent</h3>
10987
+ <p class="section-note">The payload every tile event carries.</p>
10988
+ <div class="table-wrap">
10989
+ <table>
10990
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10991
+ <tbody>
10992
+ <tr><td class="name">tile</td><td class="type">KPITileModel</td><td class="desc"></td></tr>
10993
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
10994
+ <tr><td class="name">originalEvent</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10995
+ </tbody>
10996
+ </table>
10997
+ </div>
10998
+ <h3 id="type-KPINodeModel">KPINodeModel</h3>
10999
+ <p class="section-note">One node of the rail. **No value rolls up.** `value` and `formatted` are the node's own tile's reading, and are `null` on a level the hierarchy synthesised, because the running accumulators cannot be composed without a rescan. **Severity does.** `rollup` is the worst status at or below the node, which is what a collapsed branch reports. `unknown` is excluded from it on purpose — ranking "nothing was measured" as the worst would hide a real warning underneath it — and is surfaced as `unknown`, a count of the descendants that measured nothing, so neither can pass unnoticed.</p>
11000
+ <div class="table-wrap">
11001
+ <table>
11002
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11003
+ <tbody>
11004
+ <tr><td class="name">key</td><td class="type">string</td><td class="desc">The node's stable identity: the tile id, or the path of a synthesised level.</td></tr>
11005
+ <tr><td class="name">id</td><td class="type">string | null</td><td class="desc">The tile id, or null on a synthesised level.</td></tr>
11006
+ <tr><td class="name">label</td><td class="type">string</td><td class="desc"></td></tr>
11007
+ <tr><td class="name">level</td><td class="type">number</td><td class="desc">Depth, 0 at the top level.</td></tr>
11008
+ <tr><td class="name">posinset</td><td class="type">number</td><td class="desc">Its place among its siblings, from 1, and how many there are.</td></tr>
11009
+ <tr><td class="name">setsize</td><td class="type">number</td><td class="desc"></td></tr>
11010
+ <tr><td class="name">hasChildren</td><td class="type">boolean</td><td class="desc"></td></tr>
11011
+ <tr><td class="name">expanded</td><td class="type">boolean</td><td class="desc"></td></tr>
11012
+ <tr><td class="name">children</td><td class="type">KPINodeModel[]</td><td class="desc"></td></tr>
11013
+ <tr><td class="name">tile</td><td class="type">KPITileModel | null</td><td class="desc">The node's own tile, or null on a synthesised level.</td></tr>
11014
+ <tr><td class="name">value</td><td class="type">unknown</td><td class="desc"></td></tr>
11015
+ <tr><td class="name">formatted</td><td class="type">string | null</td><td class="desc"></td></tr>
11016
+ <tr><td class="name">status</td><td class="type">'good' | 'warn' | 'critical' | 'unknown' | null</td><td class="desc">The node's own status.</td></tr>
11017
+ <tr><td class="name">rollup</td><td class="type">'good' | 'warn' | 'critical' | null</td><td class="desc">The worst status at or below the node. Never `unknown`.</td></tr>
11018
+ <tr><td class="name">unknown</td><td class="type">number</td><td class="desc">How many tiles at or below the node measured nothing.</td></tr>
11019
+ <tr><td class="name">items</td><td class="type">number</td><td class="desc">How many tiles are at or below the node.</td></tr>
11020
+ </tbody>
11021
+ </table>
11022
+ </div>
11023
+ <h3 id="type-KPIRows">KPIRows</h3>
11024
+ <p class="section-note">The keyed-diff consumer surface a KPI panel shares with a grid, so a Data Router routes to it directly.</p>
11025
+ <div class="table-wrap">
11026
+ <table>
11027
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11028
+ <tbody>
11029
+ <tr><td class="name">apply</td><td class="type">(change: { add?: KPIRow[]; update?: KPIRow[]; remove?: unknown[] }): void</td><td class="desc"></td></tr>
11030
+ <tr><td class="name">forEach</td><td class="type">(fn: (row: KPIRow, key: unknown) =&gt; void): void</td><td class="desc"></td></tr>
11031
+ <tr><td class="name">count</td><td class="type">number</td><td class="desc"><small>(read-only)</small></td></tr>
11032
+ </tbody>
11033
+ </table>
11034
+ </div>
11035
+ <h3 id="type-KPISparkline">KPISparkline</h3>
11036
+ <p class="section-note">An optional sparkline series: the `y` field plotted in order of the `x` field (or insertion).</p>
11037
+ <div class="table-wrap">
11038
+ <table>
11039
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11040
+ <tbody>
11041
+ <tr><td class="name">x</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
11042
+ <tr><td class="name">y</td><td class="type">string | ((row: KPIRow) =&gt; unknown)</td><td class="desc"></td></tr>
11043
+ </tbody>
11044
+ </table>
11045
+ </div>
11046
+ <h3 id="type-KPIThresholds">KPIThresholds</h3>
11047
+ <p class="section-note">A semantic threshold: two cut points and a direction. `higherIsBetter` (the default) makes a value at/above `warn` good, at/above `critical` a warning, below it critical; `lowerIsBetter` mirrors it. Colour is a host concern.</p>
11048
+ <div class="table-wrap">
11049
+ <table>
11050
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11051
+ <tbody>
11052
+ <tr><td class="name">warn</td><td class="type">number</td><td class="desc"></td></tr>
11053
+ <tr><td class="name">critical</td><td class="type">number</td><td class="desc"></td></tr>
11054
+ <tr><td class="name">direction</td><td class="type">'higherIsBetter' | 'lowerIsBetter'</td><td class="desc"><small>(optional)</small></td></tr>
11055
+ </tbody>
11056
+ </table>
11057
+ </div>
11058
+ <h3 id="type-KPITile">KPITile</h3>
11059
+ <p class="section-note">One tile: an aggregate over the routed rows, with optional filter, format, threshold and trend.</p>
11060
+ <div class="table-wrap">
11061
+ <table>
11062
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11063
+ <tbody>
11064
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc">A stable identity for the tile (defaults to the label, then the index). <small>(optional)</small></td></tr>
11065
+ <tr><td class="name">label</td><td class="type">string</td><td class="desc">The tile's accessible label. <small>(optional)</small></td></tr>
11066
+ <tr><td class="name">aggregation</td><td class="type">KPIAggregation | ((rows: KPIRow[], tile: object) =&gt; unknown)</td><td class="desc">The aggregation kind, or a reducer `(rows, tile) =&gt; value` for a custom tile. <small>(optional)</small></td></tr>
11067
+ <tr><td class="name">compute</td><td class="type">(rows: KPIRow[], tile: object) =&gt; unknown</td><td class="desc">The reducer for a `custom` aggregation, when `aggregation` is the string `'custom'`. <small>(optional)</small></td></tr>
11068
+ <tr><td class="name">field</td><td class="type">string | ((row: KPIRow) =&gt; unknown)</td><td class="desc">The field the aggregation reads (a path or accessor). Ignored by `count`. <small>(optional)</small></td></tr>
11069
+ <tr><td class="name">filter</td><td class="type">(row: KPIRow) =&gt; boolean</td><td class="desc">A predicate limiting the rows this tile aggregates. <small>(optional)</small></td></tr>
11070
+ <tr><td class="name">format</td><td class="type">KPIFormat</td><td class="desc">Value formatting. <small>(optional)</small></td></tr>
11071
+ <tr><td class="name">target</td><td class="type">number</td><td class="desc">A comparison target rendered alongside the value. <small>(optional)</small></td></tr>
11072
+ <tr><td class="name">baseline</td><td class="type">number</td><td class="desc">A baseline the tile's delta is measured against. <small>(optional)</small></td></tr>
11073
+ <tr><td class="name">thresholds</td><td class="type">KPIThresholds</td><td class="desc">Threshold bands, either two cut points or an explicit band list. <small>(optional)</small></td></tr>
11074
+ <tr><td class="name">bands</td><td class="type">KPIBand[]</td><td class="desc">Explicit status bands (an alternative to `thresholds`). <small>(optional)</small></td></tr>
11075
+ <tr><td class="name">sparkline</td><td class="type">KPISparkline | string</td><td class="desc">A trend sparkline series. <small>(optional)</small></td></tr>
11076
+ </tbody>
11077
+ </table>
11078
+ </div>
11079
+ <h3 id="type-KPITileModel">KPITileModel</h3>
11080
+ <p class="section-note">A computed tile, as it appears in the model.</p>
11081
+ <div class="table-wrap">
11082
+ <table>
11083
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11084
+ <tbody>
11085
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
11086
+ <tr><td class="name">label</td><td class="type">string</td><td class="desc"></td></tr>
11087
+ <tr><td class="name">aggregation</td><td class="type">string</td><td class="desc"></td></tr>
11088
+ <tr><td class="name">field</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
11089
+ <tr><td class="name">value</td><td class="type">unknown</td><td class="desc"></td></tr>
11090
+ <tr><td class="name">formatted</td><td class="type">string</td><td class="desc"></td></tr>
11091
+ <tr><td class="name">status</td><td class="type">'good' | 'warn' | 'critical' | 'unknown' | null</td><td class="desc">The tile's semantic band, or `unknown` when the tile measured nothing. `unknown` is decided from data presence before any threshold is consulted: an aggregation over nothing returns the identity of its operation (`sum` and `count` return 0), and 0 is a number a threshold grades, so without it an empty panel would report as a healthy one. Two things make a tile `unknown`: the panel holds no rows at all, or the tile's `field` names no column on the bound grid, so it never read a cell to reduce over. A tile whose `filter` matches none of the rows the panel *does* hold is neither — it has measured a real zero and is banded normally. `null` means the tile has no thresholds or bands configured.</td></tr>
11092
+ <tr><td class="name">target</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
11093
+ <tr><td class="name">baseline</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
11094
+ <tr><td class="name">delta</td><td class="type">number | null</td><td class="desc"></td></tr>
11095
+ <tr><td class="name">deltaPercent</td><td class="type">number | null</td><td class="desc"></td></tr>
11096
+ <tr><td class="name">deltaFormatted</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
11097
+ <tr><td class="name">count</td><td class="type">number</td><td class="desc"></td></tr>
11098
+ <tr><td class="name">sparkline</td><td class="type">number[] | null</td><td class="desc"></td></tr>
11099
+ </tbody>
11100
+ </table>
11101
+ </div>
11102
+ <h3 id="type-KPITreeConfig">KPITreeConfig</h3>
11103
+ <p class="section-note">The hierarchy a KPI panel arranges its tiles into (BACKLOG-0001059): a rail of top-level items that expand to the indicators beneath them, each parent highlighted with the worst status below it. The shape is declared with `path` or `parentKey` — the same two shapes the grid's tree data and the tree-select editor take — over the **tile specs**, not the rows. With neither declared, one is derived by splitting the tile ids on `separator`, so `system.compute.cpu` files itself under Compute under System. A panel whose ids carry no separator stays flat, and `false` keeps it flat whatever they look like. A tile's `field` is never a source: a dot there already means a nested object property.</p>
11104
+ <div class="table-wrap">
11105
+ <table>
11106
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11107
+ <tbody>
11108
+ <tr><td class="name">path</td><td class="type">(tile: KPITile) =&gt; (string | number)[]</td><td class="desc">The tile's own place in the hierarchy, its own segment last. <small>(optional)</small></td></tr>
11109
+ <tr><td class="name">parentKey</td><td class="type">string | ((tile: KPITile) =&gt; unknown)</td><td class="desc">The id of the tile this one sits under, or a reader for it. <small>(optional)</small></td></tr>
11110
+ <tr><td class="name">orphans</td><td class="type">'root' | string</td><td class="desc">The heading tiles whose parent is not in the panel are gathered under. <small>(optional)</small></td></tr>
11111
+ <tr><td class="name">separator</td><td class="type">string</td><td class="desc">The separator a derived hierarchy splits a tile id on. Defaults to `.`. <small>(optional)</small></td></tr>
11112
+ <tr><td class="name">expanded</td><td class="type">true | string[]</td><td class="desc">Which branches start open: every one (`true`), or these node keys. <small>(optional)</small></td></tr>
11113
+ </tbody>
11114
+ </table>
11115
+ </div>
11116
+ <h3 id="type-Layout">Layout</h3>
11117
+ <p class="section-note">A reconfigurable dashboard: a cell grid inside an element, and a set of windows on it that a user can move, resize and close by pointer or by keyboard (BACKLOG-0001108). The module is **payload-agnostic**: a window body is a container with an id, which this module creates and sizes and never reads. It tells a payload it was resized by emitting `window:resized`; it never calls into one, because it cannot know what one is.</p>
11118
+ <div class="table-wrap">
11119
+ <table>
11120
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11121
+ <tbody>
11122
+ <tr><td class="name">el</td><td class="type">HTMLElement</td><td class="desc"><small>(read-only)</small></td></tr>
11123
+ <tr><td class="name">windows</td><td class="type">(): string[]</td><td class="desc">The window ids, in mount order.</td></tr>
11124
+ <tr><td class="name">payload</td><td class="type">(id: string): HTMLElement | null</td><td class="desc">The payload container for a window, or `null`.</td></tr>
11125
+ <tr><td class="name">window</td><td class="type">(id: string): LayoutWindow | null</td><td class="desc">A copy of one window's current descriptor, or `null`.</td></tr>
11126
+ <tr><td class="name">add</td><td class="type">(spec: LayoutWindow): HTMLElement</td><td class="desc">Add a window after mount; returns its payload container.</td></tr>
11127
+ <tr><td class="name">move</td><td class="type">(id: string, to: Partial&lt;LayoutPlacement&gt;): boolean | Promise&lt;boolean&gt;</td><td class="desc">Move or resize a window, through the same before-events the drag uses.</td></tr>
11128
+ <tr><td class="name">close</td><td class="type">(id: string): boolean | Promise&lt;boolean&gt;</td><td class="desc">Close a window through `beforeWindowClose`; the payload is not destroyed.</td></tr>
11129
+ <tr><td class="name">maximise</td><td class="type">(id: string): boolean</td><td class="desc">Blow one window up to fill the layout host, hiding the rest. It fills the **host element**, not the browser window, so there is no `position: fixed` (whose containing block is the nearest ancestor carrying a `transform` or a `contain`, which is why the same rule fills the screen on one page and lands in a 300px box on the next), no reparenting and nothing that can disturb the page around the dashboard. **Nothing moves**: no compaction runs, no placement changes, and the payload container is the same DOM node throughout. **Escape restores it**, from anywhere inside the layout — a focused grid body cell or column heading included — unless a payload has already claimed the key: an open cell editor, filter menu or column menu closes first, and the next Escape restores the window. Afterwards focus lands on the window's maximise control. A minimised window is expanded first, and maximising a second window restores the first.</td></tr>
11130
+ <tr><td class="name">minimise</td><td class="type">(id: string): boolean</td><td class="desc">Collapse one window to a single row: its payload is hidden and its chrome stays, carrying the control that brings it back. On screen it becomes one row and the windows below pull up into the space under `compact: 'vertical'`. In the arrangement nothing moves at all — the collapse is a projection of it — so `restore()` gives back exactly the arrangement that was there, in **any** order and with any number of other windows still collapsed. A window with `chrome: false` is refused, with a warning naming it.</td></tr>
11131
+ <tr><td class="name">restore</td><td class="type">(id: string): boolean</td><td class="desc">Leave whichever display mode a window is in; `false` when it was in none.</td></tr>
11132
+ <tr><td class="name">maximised</td><td class="type">(): string | null</td><td class="desc">The id of the window filling the host, or `null`. At most one.</td></tr>
11133
+ <tr><td class="name">minimised</td><td class="type">(): string[]</td><td class="desc">The ids of every currently minimised window, in mount order.</td></tr>
11134
+ <tr><td class="name">getLayout</td><td class="type">(): LayoutSnapshot</td><td class="desc">The full current arrangement. **A mode is not an arrangement**: this reports the *underlying* placement of a maximised or minimised window — where it will be when restored — never the geometry it is drawn at.</td></tr>
11135
+ <tr><td class="name">setLayout</td><td class="type">(incoming: LayoutSnapshot | LayoutWindow[]): number</td><td class="desc">Restore an arrangement; never throws on garbage.</td></tr>
11136
+ <tr><td class="name">getState</td><td class="type">(): { version: number; layout: LayoutSnapshot }</td><td class="desc">A versioned snapshot, following core's and gantt's shape.</td></tr>
11137
+ <tr><td class="name">setState</td><td class="type">(snapshot: unknown): number</td><td class="desc">Restore a `getState()` snapshot; never throws on garbage.</td></tr>
11138
+ <tr><td class="name">setInteractive</td><td class="type">(value: boolean | Partial&lt;LayoutInteractive&gt;): LayoutInteractive</td><td class="desc">Lock or unlock the dashboard at runtime — the "Edit layout" button. A boolean sets all three capabilities; an object sets only the keys it carries. Nothing is destroyed, so every payload survives the toggle. The asymmetry is deliberate: **you can always take a capability away; you can never grant one where the developer said no.** `setInteractive(false)` locks every window, including one whose own spec says `movable: true`; `setInteractive(true)` unlocks only the windows that never opted out. `config.movable: false` and `setInteractive(false)` are deliberately not the same thing: the config states the *default* for windows that declare nothing (and `false` is already that default, so it takes nothing away from a window that opted in), while this is an *active lock*. A key carrying `undefined` is treated as absent, so `setInteractive(getInteractive())` is a no-op in every state. A locked layout is not a read-only dashboard: this module never reads or writes a payload, so a grid inside a window is made read-only with the grid's own settings.</td></tr>
11139
+ <tr><td class="name">getInteractive</td><td class="type">(): LayoutInteractive</td><td class="desc">The layout-level interactivity now in force, as a copy — `undefined` where no layout-level default is set, so the result round-trips through `setInteractive`.</td></tr>
11140
+ <tr><td class="name">refresh</td><td class="type">(): number</td><td class="desc">Re-measure every window and emit `window:resized` for those that changed.</td></tr>
11141
+ <tr><td class="name">on</td><td class="type">(</td><td class="desc"></td></tr>
11142
+ <tr><td class="name">off</td><td class="type">(name: string, fn: (event: any) =&gt; unknown): void</td><td class="desc"></td></tr>
11143
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc">Tear the layout down; whatever the host mounted in a payload is the host's to destroy.</td></tr>
11144
+ </tbody>
11145
+ </table>
11146
+ </div>
11147
+ <h3 id="type-LayoutChangedEvent">LayoutChangedEvent</h3>
11148
+ <p class="section-note">The payload of `layout:changed`: the whole arrangement, plus what moved it.</p>
11149
+ <div class="table-wrap">
11150
+ <table>
11151
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11152
+ <tbody>
11153
+ <tr><td class="name">cause</td><td class="type">string</td><td class="desc"></td></tr>
11154
+ </tbody>
11155
+ </table>
11156
+ </div>
11157
+ <h3 id="type-LayoutCloseEvent">LayoutCloseEvent</h3>
11158
+ <p class="section-note">The payload of `window:closed` and `beforeWindowClose`.</p>
11159
+ <div class="table-wrap">
11160
+ <table>
11161
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11162
+ <tbody>
11163
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
11164
+ <tr><td class="name">payloadId</td><td class="type">string</td><td class="desc"></td></tr>
11165
+ <tr><td class="name">payload</td><td class="type">HTMLElement</td><td class="desc">The payload container, handed back so the host can destroy what it mounted. <small>(optional)</small></td></tr>
11166
+ <tr><td class="name">origin</td><td class="type">'api' | 'user'</td><td class="desc"><small>(optional)</small></td></tr>
11167
+ <tr><td class="name">reason</td><td class="type">string | null</td><td class="desc"><small>(optional)</small></td></tr>
11168
+ <tr><td class="name">preventDefault</td><td class="type">(reason?: string) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11169
+ <tr><td class="name">defaultPrevented</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
11170
+ </tbody>
11171
+ </table>
11172
+ </div>
11173
+ <h3 id="type-LayoutConfig">LayoutConfig</h3>
11174
+ <p class="section-note">Dashboard layout configuration.</p>
11175
+ <div class="table-wrap">
11176
+ <table>
11177
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11178
+ <tbody>
11179
+ <tr><td class="name">columns</td><td class="type">number</td><td class="desc">Cell columns across the mounted element (default 12). <small>(optional)</small></td></tr>
11180
+ <tr><td class="name">rows</td><td class="type">number</td><td class="desc">Cell rows down the mounted element (default 6). <small>(optional)</small></td></tr>
11181
+ <tr><td class="name">overflowX</td><td class="type">'static' | 'scroll'</td><td class="desc">Horizontal overflow (default `'static'`). <small>(optional)</small></td></tr>
11182
+ <tr><td class="name">overflowY</td><td class="type">'static' | 'scroll'</td><td class="desc">Vertical overflow (default `'static'`). <small>(optional)</small></td></tr>
11183
+ <tr><td class="name">columnWidth</td><td class="type">number | string</td><td class="desc">Fixed column track size, used only when `overflowX` is `'scroll'` (default `'240px'`). <small>(optional)</small></td></tr>
11184
+ <tr><td class="name">rowHeight</td><td class="type">number | string</td><td class="desc">Fixed row track size, used only when `overflowY` is `'scroll'` (default `'160px'`). <small>(optional)</small></td></tr>
11185
+ <tr><td class="name">gap</td><td class="type">number | string</td><td class="desc">The gap between cells (default `'8px'`). <small>(optional)</small></td></tr>
11186
+ <tr><td class="name">padding</td><td class="type">number | string</td><td class="desc">The default padding inside a window (default `'5px'`). <small>(optional)</small></td></tr>
11187
+ <tr><td class="name">compact</td><td class="type">'vertical' | 'horizontal' | 'none'</td><td class="desc">Rearrangement (default `'vertical'`). One gravity direction, never two: `'vertical'` pushes displaced windows down and then floats everything up, `'horizontal'` pushes them right and then floats everything left — so dragging a window out of a row closes the hole sideways — and `'none'` leaves every placement exactly where it was put. An unrecognised value warns once, naming what it got, and falls back to `'vertical'`. <small>(optional)</small></td></tr>
11188
+ <tr><td class="name">movable</td><td class="type">boolean</td><td class="desc">The default `movable` for every window that does not declare its own (default `false`). This states a default, so `false` takes nothing away from a window that declared `movable: true`; `setInteractive(false)` is the active lock that does. <small>(optional)</small></td></tr>
11189
+ <tr><td class="name">resizable</td><td class="type">boolean</td><td class="desc">The default `resizable` for windows that declare none (default `false`); see `movable`. <small>(optional)</small></td></tr>
11190
+ <tr><td class="name">closable</td><td class="type">boolean</td><td class="desc">The default `closable` for windows that declare none (default `false`); see `movable`. <small>(optional)</small></td></tr>
11191
+ <tr><td class="name">maximisable</td><td class="type">boolean</td><td class="desc">The default `maximisable` for windows that declare none (default `false`). Not touched by `setInteractive()`: a display mode neither moves nor resizes a window in the arrangement, so a locked dashboard can still be blown up to read. <small>(optional)</small></td></tr>
11192
+ <tr><td class="name">minimisable</td><td class="type">boolean</td><td class="desc">The default `minimisable` for windows that declare none (default `false`); see `maximisable`. <small>(optional)</small></td></tr>
11193
+ <tr><td class="name">windows</td><td class="type">LayoutWindow[]</td><td class="desc">The windows, in mount order. <small>(optional)</small></td></tr>
11194
+ <tr><td class="name">layout</td><td class="type">LayoutSnapshot</td><td class="desc">An arrangement to apply at mount, as produced by `getLayout()`. <small>(optional)</small></td></tr>
11195
+ <tr><td class="name">ariaLabel</td><td class="type">string</td><td class="desc">The layout region's accessible name. <small>(optional)</small></td></tr>
11196
+ <tr><td class="name">messages</td><td class="type">{ t(key: string, params?: Record&lt;string, unknown&gt;): string }</td><td class="desc">A message catalogue, e.g. `grid.messages`; built-in English seeds otherwise. <small>(optional)</small></td></tr>
11197
+ <tr><td class="name">onWindowMoved</td><td class="type">(event: LayoutMoveEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11198
+ <tr><td class="name">onWindowResized</td><td class="type">(event: LayoutResizeEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11199
+ <tr><td class="name">onWindowClosed</td><td class="type">(event: LayoutCloseEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11200
+ <tr><td class="name">onLayoutChanged</td><td class="type">(event: LayoutChangedEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11201
+ <tr><td class="name">onBeforeWindowMove</td><td class="type">(event: LayoutMoveEvent) =&gt; boolean | void | Promise&lt;boolean&gt;</td><td class="desc"><small>(optional)</small></td></tr>
11202
+ <tr><td class="name">onBeforeWindowResize</td><td class="type">(event: LayoutMoveEvent) =&gt; boolean | void | Promise&lt;boolean&gt;</td><td class="desc"><small>(optional)</small></td></tr>
11203
+ <tr><td class="name">onBeforeWindowClose</td><td class="type">(event: LayoutCloseEvent) =&gt; boolean | void | Promise&lt;boolean&gt;</td><td class="desc"><small>(optional)</small></td></tr>
11204
+ <tr><td class="name">onWindowMoveCancelled</td><td class="type">(event: LayoutMoveEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11205
+ <tr><td class="name">onWindowResizeCancelled</td><td class="type">(event: LayoutMoveEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11206
+ <tr><td class="name">onWindowCloseCancelled</td><td class="type">(event: LayoutCloseEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11207
+ </tbody>
11208
+ </table>
11209
+ </div>
11210
+ <h3 id="type-LayoutInteractive">LayoutInteractive</h3>
11211
+ <p class="section-note">The three capabilities a layout-level default and `setInteractive()` cover. These are the layout **defaults**, not the per-window resolution: a window that declared `movable: false` stays pinned whatever these say. Three values, not two. `undefined` means no layout-level default is in force and each window's own flag decides; `true` unlocks everything that did not opt out; `false` is an active lock. Reporting `undefined` as `false` would read correctly and round-trip wrongly, so it is reported as it is.</p>
11212
+ <div class="table-wrap">
11213
+ <table>
11214
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11215
+ <tbody>
11216
+ <tr><td class="name">movable</td><td class="type">boolean | undefined</td><td class="desc"></td></tr>
11217
+ <tr><td class="name">resizable</td><td class="type">boolean | undefined</td><td class="desc"></td></tr>
11218
+ <tr><td class="name">closable</td><td class="type">boolean | undefined</td><td class="desc"></td></tr>
11219
+ </tbody>
11220
+ </table>
11221
+ </div>
11222
+ <h3 id="type-LayoutMoveEvent">LayoutMoveEvent</h3>
11223
+ <p class="section-note">The payload of `window:moved`, `beforeWindowMove`, `beforeWindowResize`.</p>
11224
+ <div class="table-wrap">
11225
+ <table>
11226
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11227
+ <tbody>
11228
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
11229
+ <tr><td class="name">from</td><td class="type">LayoutPlacement</td><td class="desc"></td></tr>
11230
+ <tr><td class="name">to</td><td class="type">LayoutPlacement</td><td class="desc">Where the window was asked to go.</td></tr>
11231
+ <tr><td class="name">landed</td><td class="type">LayoutPlacement</td><td class="desc">Where it actually ended up, which under `compact: 'vertical'` may differ. <small>(optional)</small></td></tr>
11232
+ <tr><td class="name">origin</td><td class="type">'api' | 'user' | 'init'</td><td class="desc"><small>(optional)</small></td></tr>
11233
+ <tr><td class="name">reason</td><td class="type">string | null</td><td class="desc"><small>(optional)</small></td></tr>
11234
+ <tr><td class="name">preventDefault</td><td class="type">(reason?: string) =&gt; void</td><td class="desc">Cancel the action (only meaningful on a `before*` event). <small>(optional)</small></td></tr>
11235
+ <tr><td class="name">defaultPrevented</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
11236
+ </tbody>
11237
+ </table>
11238
+ </div>
11239
+ <h3 id="type-LayoutPlacement">LayoutPlacement</h3>
11240
+ <p class="section-note">A cell placement, as carried on the move and resize events.</p>
11241
+ <div class="table-wrap">
11242
+ <table>
11243
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11244
+ <tbody>
11245
+ <tr><td class="name">xPos</td><td class="type">number</td><td class="desc"></td></tr>
11246
+ <tr><td class="name">yPos</td><td class="type">number</td><td class="desc"></td></tr>
11247
+ <tr><td class="name">xSize</td><td class="type">number</td><td class="desc"></td></tr>
11248
+ <tr><td class="name">ySize</td><td class="type">number</td><td class="desc"></td></tr>
11249
+ </tbody>
11250
+ </table>
11251
+ </div>
11252
+ <h3 id="type-LayoutResizeEvent">LayoutResizeEvent</h3>
11253
+ <p class="section-note">The payload of `window:resized` — the measured **content box** of the payload container, not a cell count. Emitted when the container genuinely changes size, including on the opening frame; never with a zero box.</p>
11254
+ <div class="table-wrap">
11255
+ <table>
11256
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11257
+ <tbody>
11258
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
11259
+ <tr><td class="name">payloadId</td><td class="type">string</td><td class="desc"></td></tr>
11260
+ <tr><td class="name">payload</td><td class="type">HTMLElement</td><td class="desc">The payload container itself, so a host can act on it directly.</td></tr>
11261
+ <tr><td class="name">width</td><td class="type">number</td><td class="desc"></td></tr>
11262
+ <tr><td class="name">height</td><td class="type">number</td><td class="desc"></td></tr>
11263
+ <tr><td class="name">xPos</td><td class="type">number</td><td class="desc"></td></tr>
11264
+ <tr><td class="name">yPos</td><td class="type">number</td><td class="desc"></td></tr>
11265
+ <tr><td class="name">xSize</td><td class="type">number</td><td class="desc"></td></tr>
11266
+ <tr><td class="name">ySize</td><td class="type">number</td><td class="desc"></td></tr>
11267
+ </tbody>
11268
+ </table>
11269
+ </div>
11270
+ <h3 id="type-LayoutSnapshot">LayoutSnapshot</h3>
11271
+ <p class="section-note">The plain, JSON-safe arrangement `getLayout()` returns and `setLayout()` takes.</p>
11272
+ <div class="table-wrap">
11273
+ <table>
11274
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11275
+ <tbody>
11276
+ <tr><td class="name">columns</td><td class="type">number</td><td class="desc"></td></tr>
11277
+ <tr><td class="name">rows</td><td class="type">number</td><td class="desc"></td></tr>
11278
+ <tr><td class="name">windows</td><td class="type">{ id: string; xPos: number; yPos: number; xSize: number; ySize: number }[]</td><td class="desc"></td></tr>
11279
+ </tbody>
11280
+ </table>
11281
+ </div>
11282
+ <h3 id="type-LayoutWindow">LayoutWindow</h3>
11283
+ <p class="section-note">One window on the cell grid. Deliberately **not** named `WindowSpec`: that name is already taken by the rolling-statistics window (`{ kind: 'count'|'time'|'session', span, size }`) and reusing it would put `kind: 'session'` next to a dashboard pane.</p>
11284
+ <div class="table-wrap">
11285
+ <table>
11286
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11287
+ <tbody>
11288
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc">A stable, unique id. Required.</td></tr>
11289
+ <tr><td class="name">xPos</td><td class="type">number</td><td class="desc">The 1-based column the window starts in. Auto-placed when omitted. <small>(optional)</small></td></tr>
11290
+ <tr><td class="name">yPos</td><td class="type">number</td><td class="desc">The 1-based row the window starts in. Auto-placed when omitted. <small>(optional)</small></td></tr>
11291
+ <tr><td class="name">xSize</td><td class="type">number</td><td class="desc">How many columns it spans (default 1). <small>(optional)</small></td></tr>
11292
+ <tr><td class="name">ySize</td><td class="type">number</td><td class="desc">How many rows it spans (default 1). <small>(optional)</small></td></tr>
11293
+ <tr><td class="name">title</td><td class="type">string</td><td class="desc">The title shown in the chrome bar, and the name every control takes. <small>(optional)</small></td></tr>
11294
+ <tr><td class="name">chrome</td><td class="type">boolean</td><td class="desc">Whether to draw the title bar (default `true`). <small>(optional)</small></td></tr>
11295
+ <tr><td class="name">closable</td><td class="type">boolean</td><td class="desc">Whether to offer a close button (default `false`). <small>(optional)</small></td></tr>
11296
+ <tr><td class="name">movable</td><td class="type">boolean</td><td class="desc">Whether the window can be moved by drag or keyboard (default `false`). <small>(optional)</small></td></tr>
11297
+ <tr><td class="name">resizable</td><td class="type">boolean</td><td class="desc">Whether the window can be resized by drag or keyboard (default `false`). <small>(optional)</small></td></tr>
11298
+ <tr><td class="name">maximisable</td><td class="type">boolean</td><td class="desc">Whether to offer a maximise control in the chrome (default `false`). Maximising fills the **layout host**, not the browser window, and hides every other window for the duration. Escape restores it, unless a payload has already claimed the key. <small>(optional)</small></td></tr>
11299
+ <tr><td class="name">minimisable</td><td class="type">boolean</td><td class="desc">Whether to offer a minimise control in the chrome (default `false`). A window with `chrome: false` cannot be minimised whatever this says: there would be nothing left on screen to restore it with. <small>(optional)</small></td></tr>
11300
+ <tr><td class="name">padding</td><td class="type">number | string</td><td class="desc">Padding inside the window; the layout's `padding` (default `'5px'`) otherwise. <small>(optional)</small></td></tr>
11301
+ <tr><td class="name">payloadId</td><td class="type">string</td><td class="desc">The `id` given to the payload container (default `` `${id}-body` ``). <small>(optional)</small></td></tr>
11302
+ <tr><td class="name">ariaLabel</td><td class="type">string</td><td class="desc">The window's accessible name, when the title alone is not enough context. <small>(optional)</small></td></tr>
11303
+ </tbody>
11304
+ </table>
11305
+ </div>
9883
11306
  <h3 id="type-LicenceApi">LicenceApi</h3>
9884
11307
  <div class="table-wrap">
9885
11308
  <table>
@@ -10395,9 +11818,9 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
10395
11818
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10396
11819
  <tbody>
10397
11820
  <tr><td class="name">pushed</td><td class="type">RemoteRequest</td><td class="desc">The query the adapter was given.</td></tr>
10398
- <tr><td class="name">residual</td><td class="type">{ filters: object | null; sort: SortEntry[] | null; quick: string }</td><td class="desc">What the grid applied afterwards.</td></tr>
11821
+ <tr><td class="name">residual</td><td class="type">{</td><td class="desc">What the grid applied afterwards. `where` is the host predicate runtime when one survived the `whereRowLimit` gate, and `null` when none was registered or the gate refused it (BACKLOG-0001268).</td></tr>
10399
11822
  <tr><td class="name">needsAll</td><td class="type">boolean</td><td class="desc">Whether the whole result had to be fetched rather than a window.</td></tr>
10400
- <tr><td class="name">unpushed</td><td class="type">string[]</td><td class="desc">Which parts could not be pushed: `filter`, `sort`, `quick`.</td></tr>
11823
+ <tr><td class="name">unpushed</td><td class="type">string[]</td><td class="desc">Which parts could not be pushed: `filter`, `sort`, `quick`, `where`.</td></tr>
10401
11824
  <tr><td class="name">full</td><td class="type">boolean</td><td class="desc">Whether the whole result was fetched because `fullDataset` is on, rather than only because residual work forced it. When true, totals and statistics reduce over the whole matching set and the windowed-stat warning is silent.</td></tr>
10402
11825
  <tr><td class="name">aggregates</td><td class="type">{</td><td class="desc">Per-aggregate provenance, present only when the last request computed aggregates (BACKLOG-0000730 Part B): which statistics the engine computed and which the client did, with the class the pushdown map assigned each. Under grouping it also carries the `groupBy` the subtotals were computed over. Build-time inspection, not a runtime per-figure marker. <small>(optional)</small></td></tr>
10403
11826
  </tbody>
@@ -10414,6 +11837,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
10414
11837
  <tr><td class="name">fullDataset</td><td class="type">PushdownFullDatasetConfig</td><td class="desc">Opt-in full-dataset pull. Off unless `fullDataset.enabled` is set. See {@link PushdownFullDatasetConfig}. <small>(optional)</small></td></tr>
10415
11838
  <tr><td class="name">aggregates</td><td class="type">PushdownAggregatesConfig</td><td class="desc">Design-time aggregate-pushdown policy. Absent = client-side (today's behaviour). See {@link PushdownAggregatesConfig}. <small>(optional)</small></td></tr>
10416
11839
  <tr><td class="name">allowPartialResults</td><td class="type">boolean</td><td class="desc">Accept a partial/paged result to a whole-set request when residual work (a filter, sort or quick search) will run over it client-side. Off by default: such a shortfall is refused with a thrown error, because filtering or sorting a fraction of the result presents the wrong rows as the whole filtered set — a wrong answer, not a slow one. Set `true` only when you knowingly accept that risk (e.g. an adapter that cannot page and a result small enough not to matter); the old warn-once-and-proceed behaviour is then kept. It never changes the fullDataset memory-guard or the no-residual short-return warning. <small>(optional)</small></td></tr>
11840
+ <tr><td class="name">whereRowLimit</td><td class="type">number</td><td class="desc">The most rows the source will fetch and hold in order to run a twinless `where` predicate as the residual (BACKLOG-0001268). Defaults to `50_000`, the same anchor as the grid's `workerThreshold` — the size at which this codebase already judges a dataset big enough to need different handling. A `where` predicate is a host function no engine can evaluate, so the only way to honour one is to fetch every matching row and filter here. That silently turns a windowed grid into a whole-dataset download, which is the thing a pushdown source exists to avoid. So it is a gate, not a free upgrade: at or past this many matching rows the predicate is **refused and warned about** — the rows it would exclude stay on screen — rather than the download being taken on the host's behalf. An adapter that reports no row total counts as over the limit, because guessing the other way is guessing your way into the download. Raise it when you want that download; the `{ condition }` twin is the route that narrows the fetch itself and works at any size. <small>(optional)</small></td></tr>
10417
11841
  </tbody>
10418
11842
  </table>
10419
11843
  </div>
@@ -10587,6 +12011,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
10587
12011
  <tr><td class="name">sort</td><td class="type">SortEntry[]</td><td class="desc"></td></tr>
10588
12012
  <tr><td class="name">context</td><td class="type">unknown</td><td class="desc"></td></tr>
10589
12013
  <tr><td class="name">signal</td><td class="type">AbortSignal</td><td class="desc"></td></tr>
12014
+ <tr><td class="name">where</td><td class="type">WhereRuntime</td><td class="desc">The `where` predicates in force, as a runtime the source can evaluate but not mutate (BACKLOG-0001268). Present **only when at least one predicate is registered**, so a grid that does not use `where` sends the request it always sent, field for field. A host `fetch` may ignore it, and every existing one does: it is a host function, so there is nothing to serialise and no engine can evaluate it — `passes` is dropped by `JSON.stringify` the way `signal` already is. It is carried for the one reader that can act on it, `createPushdownSource`, which runs it as the residual over the matching set when that set is under `whereRowLimit`. The `{ condition }` twin remains the route that narrows the fetch itself, at any size. <small>(optional)</small></td></tr>
10590
12015
  </tbody>
10591
12016
  </table>
10592
12017
  </div>
@@ -10664,6 +12089,19 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
10664
12089
  </tbody>
10665
12090
  </table>
10666
12091
  </div>
12092
+ <h3 id="type-RouteOptions">RouteOptions</h3>
12093
+ <p class="section-note">Per-route reshaping options (v3, BACKLOG-0000887): `transform` maps/renames/ derives each row before the grid sees it; `filter` gives the grid only the rows it admits; `sort` (a comparator or `{ key, dir }`) orders what the grid receives. `rowKey` overrides the router default. All optional.</p>
12094
+ <div class="table-wrap">
12095
+ <table>
12096
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12097
+ <tbody>
12098
+ <tr><td class="name">rowKey</td><td class="type">(string | ((row: RouterRecord) =&gt; unknown))</td><td class="desc"><small>(optional)</small></td></tr>
12099
+ <tr><td class="name">transform</td><td class="type">(row: RouterRecord) =&gt; RouterRecord</td><td class="desc"><small>(optional)</small></td></tr>
12100
+ <tr><td class="name">filter</td><td class="type">(row: RouterRecord) =&gt; boolean</td><td class="desc"><small>(optional)</small></td></tr>
12101
+ <tr><td class="name">sort</td><td class="type">(((a: RouterRecord, b: RouterRecord) =&gt; number) | { key: string; dir?: 'asc' | 'desc' })</td><td class="desc"><small>(optional)</small></td></tr>
12102
+ </tbody>
12103
+ </table>
12104
+ </div>
10667
12105
  <h3 id="type-Row">Row</h3>
10668
12106
  <div class="table-wrap">
10669
12107
  <table>
@@ -11078,6 +12516,87 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11078
12516
  </tbody>
11079
12517
  </table>
11080
12518
  </div>
12519
+ <h3 id="type-TabChangeEvent">TabChangeEvent</h3>
12520
+ <p class="section-note">The payload every tab-change event carries.</p>
12521
+ <div class="table-wrap">
12522
+ <table>
12523
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12524
+ <tbody>
12525
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
12526
+ <tr><td class="name">previousId</td><td class="type">string | null</td><td class="desc"></td></tr>
12527
+ <tr><td class="name">origin</td><td class="type">'api' | 'user' | 'init'</td><td class="desc"><small>(optional)</small></td></tr>
12528
+ <tr><td class="name">reason</td><td class="type">string | null</td><td class="desc"><small>(optional)</small></td></tr>
12529
+ <tr><td class="name">preventDefault</td><td class="type">(reason?: string) =&gt; void</td><td class="desc">Cancel the switch (only meaningful on `beforeTabChange`). <small>(optional)</small></td></tr>
12530
+ <tr><td class="name">defaultPrevented</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
12531
+ </tbody>
12532
+ </table>
12533
+ </div>
12534
+ <h3 id="type-TabDescriptor">TabDescriptor</h3>
12535
+ <p class="section-note">One tab: an id, a display label, a grid config, and — for a derived tab — the parent tab id plus the narrowing forwarded onto the derived source built for it (`source: { mode: 'derived', from: &lt;parent's grid&gt;, ... }`). The derivation keys are the ones `packages/core/src/source/derive.js` already understands; this module invents none of its own.</p>
12536
+ <div class="table-wrap">
12537
+ <table>
12538
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12539
+ <tbody>
12540
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc">A stable, unique id. Required.</td></tr>
12541
+ <tr><td class="name">label</td><td class="type">string</td><td class="desc">The tab button's text. Defaults to `id`. <small>(optional)</small></td></tr>
12542
+ <tr><td class="name">config</td><td class="type">object</td><td class="desc">The config for this tab's body: the grid config passed to `createGrid` (merged with the derived `source`, when `from` is set), or — with `view` — that viewer's own config. <small>(optional)</small></td></tr>
12543
+ <tr><td class="name">view</td><td class="type">(el: HTMLElement, config: object) =&gt; unknown</td><td class="desc">Mount something other than a grid in this tab: the factory that builds it, called as `(el, config) =&gt; instance`. `createKanban` and `createKPI` have that signature already; a Gantt is adapted in a line (`(el, config) =&gt; createGantt({ ...config, element: el })`). The factory is injected rather than imported, exactly as `createGrid` is. A `view` tab derives from `from` exactly as a grid tab does: a headless grid carries the derived source and its rows are piped into the viewer through `rows.apply`, so deriving into one needs `createHeadlessGrid` injected too. <small>(optional)</small></td></tr>
12544
+ <tr><td class="name">from</td><td class="type">string</td><td class="desc">The parent tab id to derive from. When set, `config.source` is built for you and any of your own is replaced (with a warning). <small>(optional)</small></td></tr>
12545
+ <tr><td class="name">where</td><td class="type">(row: unknown) =&gt; boolean</td><td class="desc">Row predicate forwarded to the derived source. <small>(optional)</small></td></tr>
12546
+ <tr><td class="name">group</td><td class="type">unknown</td><td class="desc">Group-by forwarded to the derived source. <small>(optional)</small></td></tr>
12547
+ <tr><td class="name">groupBy</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
12548
+ <tr><td class="name">bucket</td><td class="type">unknown</td><td class="desc">Time-bucketing forwarded to the derived source. <small>(optional)</small></td></tr>
12549
+ <tr><td class="name">join</td><td class="type">unknown</td><td class="desc">Join spec forwarded to the derived source. <small>(optional)</small></td></tr>
12550
+ <tr><td class="name">unnest</td><td class="type">unknown</td><td class="desc">Array-field unnesting forwarded to the derived source. <small>(optional)</small></td></tr>
12551
+ <tr><td class="name">refresh</td><td class="type">'live' | 'idle' | 'manual' | number</td><td class="desc">`'live' | 'idle' | 'manual' | number` forwarded to the derived source. <small>(optional)</small></td></tr>
12552
+ <tr><td class="name">crossFilter</td><td class="type">unknown</td><td class="desc">Cross-filter wiring forwarded to the derived source. <small>(optional)</small></td></tr>
12553
+ <tr><td class="name">follow</td><td class="type">'filtered' | 'all' | 'selected' | 'grouped'</td><td class="desc">Which slice of the parent's rows to derive from: `'filtered' | 'all' | 'selected' | 'grouped'`. <small>(optional)</small></td></tr>
12554
+ <tr><td class="name">limit</td><td class="type">number</td><td class="desc">Row limit forwarded to the derived source. <small>(optional)</small></td></tr>
12555
+ <tr><td class="name">sort</td><td class="type">unknown</td><td class="desc">Sort forwarded to the derived source. <small>(optional)</small></td></tr>
12556
+ <tr><td class="name">profile</td><td class="type">unknown</td><td class="desc">Statistical-profile derivation, forwarded to the derived source. <small>(optional)</small></td></tr>
12557
+ <tr><td class="name">ariaLabel</td><td class="type">string</td><td class="desc">This tab's panel's own `aria-label`, when the label alone is not enough context. <small>(optional)</small></td></tr>
12558
+ <tr><td class="name">icon</td><td class="type">string | HTMLElement</td><td class="desc">A leading icon: a single character or emoji, or an element you built. Never a markup string — nothing here parses HTML. Decorative, so it is hidden from assistive technology. <small>(optional)</small></td></tr>
12559
+ <tr><td class="name">badge</td><td class="type">true | number | string | ((count: number | null, tab: { id: string; label: string; from: string | null }) =&gt; unknown)</td><td class="desc">A count badge. `true` shows this tab's own live row count and follows it; a number or string is static; a function is given the live count and returns what to show (`null` hides it). Off when absent. <small>(optional)</small></td></tr>
12560
+ <tr><td class="name">badgeTone</td><td class="type">'good' | 'warn' | 'bad' | 'unknown' | ((count: number | null, tab: { id: string; label: string; from: string | null }) =&gt; 'good' | 'warn' | 'bad' | 'unknown' | null)</td><td class="desc">The badge's tone, declared by the host rather than derived from a threshold: `'good' | 'warn' | 'bad' | 'unknown'`, or a function of the live count returning one. <small>(optional)</small></td></tr>
12561
+ </tbody>
12562
+ </table>
12563
+ </div>
12564
+ <h3 id="type-Tabs">Tabs</h3>
12565
+ <p class="section-note">A tabbed grid: a `role="tablist"` strip above a stack of `role="tabpanel"` regions, each hosting its own, independently-configured grid instance (BACKLOG-0001039). A tab's grid mounts on first activation and is kept alive, hidden, until `destroy()`.</p>
12566
+ <div class="table-wrap">
12567
+ <table>
12568
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12569
+ <tbody>
12570
+ <tr><td class="name">el</td><td class="type">HTMLElement</td><td class="desc"><small>(read-only)</small></td></tr>
12571
+ <tr><td class="name">activeId</td><td class="type">string</td><td class="desc">The currently active tab id. <small>(read-only)</small></td></tr>
12572
+ <tr><td class="name">tabs</td><td class="type">(): string[]</td><td class="desc">The configured tab ids, in order.</td></tr>
12573
+ <tr><td class="name">tab</td><td class="type">(id: string): unknown | null</td><td class="desc">The live grid instance for a tab, or `null` before it has been materialised.</td></tr>
12574
+ <tr><td class="name">isMounted</td><td class="type">(id: string): boolean</td><td class="desc">Whether a tab's grid has been created yet.</td></tr>
12575
+ <tr><td class="name">activate</td><td class="type">(id: string, opts?: { origin?: 'api' | 'user' }): boolean | Promise&lt;boolean&gt;</td><td class="desc">Switch the active tab, gated by `beforeTabChange`.</td></tr>
12576
+ <tr><td class="name">on</td><td class="type">(name: 'beforeTabChange' | 'tab:changed' | 'tabChange:cancelled' | string, fn: (event: TabChangeEvent) =&gt; void): () =&gt; void</td><td class="desc"></td></tr>
12577
+ <tr><td class="name">off</td><td class="type">(name: string, fn: (event: TabChangeEvent) =&gt; void): void</td><td class="desc"></td></tr>
12578
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc">Tear the whole strip down; destroys every mounted tab's grid.</td></tr>
12579
+ </tbody>
12580
+ </table>
12581
+ </div>
12582
+ <h3 id="type-TabsConfig">TabsConfig</h3>
12583
+ <p class="section-note">Tabbed-grid configuration.</p>
12584
+ <div class="table-wrap">
12585
+ <table>
12586
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12587
+ <tbody>
12588
+ <tr><td class="name">createGrid</td><td class="type">(el: HTMLElement, config: object) =&gt; unknown</td><td class="desc">The grid factory to mount each tab with, e.g. `import { createGrid } from '@toclocoinc/lattice-grid'`. Required.</td></tr>
12589
+ <tr><td class="name">createHeadlessGrid</td><td class="type">(config: object) =&gt; unknown</td><td class="desc">The headless grid factory, injected the same way and for the same reason. Optional, and only needed for badges: with it, a tab that has never been activated still carries a live count, computed with no DOM. Without it, such a tab shows no badge until its first activation. <small>(optional)</small></td></tr>
12590
+ <tr><td class="name">tabs</td><td class="type">TabDescriptor[]</td><td class="desc">The tabs, in display order. Required, at least one.</td></tr>
12591
+ <tr><td class="name">active</td><td class="type">string</td><td class="desc">The initially active tab id. Defaults to the first tab. <small>(optional)</small></td></tr>
12592
+ <tr><td class="name">ariaLabel</td><td class="type">string</td><td class="desc">The tablist landmark's accessible name. <small>(optional)</small></td></tr>
12593
+ <tr><td class="name">messages</td><td class="type">{ t(key: string, params?: Record&lt;string, unknown&gt;): string }</td><td class="desc">An explicit message-catalogue override; otherwise a mounted tab's own `grid.messages` is used. <small>(optional)</small></td></tr>
12594
+ <tr><td class="name">onTabChange</td><td class="type">(event: TabChangeEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
12595
+ <tr><td class="name">onBeforeTabChange</td><td class="type">(event: TabChangeEvent) =&gt; boolean | void | Promise&lt;boolean&gt;</td><td class="desc"><small>(optional)</small></td></tr>
12596
+ <tr><td class="name">onTabChangeCancelled</td><td class="type">(event: TabChangeEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
12597
+ </tbody>
12598
+ </table>
12599
+ </div>
11081
12600
  <h3 id="type-TextFormat">TextFormat</h3>
11082
12601
  <div class="table-wrap">
11083
12602
  <table>
@@ -11370,7 +12889,20 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11370
12889
  <tbody>
11371
12890
  <tr><td class="name">deps</td><td class="type">string[]</td><td class="desc">The columns the predicate reads, in the same spirit as `value.deps` on a computed column (§8.4.2). Declared, the verdict is cached per row and re-run only when one of these columns changes on that row. Omitted, the predicate is treated as reading the whole row and is called on every pass — never stale, and never skipped either. <small>(optional)</small></td></tr>
11372
12891
  <tr><td class="name">pinned</td><td class="type">boolean</td><td class="desc">Survive `filters.clear()`. For a predicate that is not the user's filter — row-level permissions, tenant scoping — where a "clear filters" button must never widen what the user can see. <small>(optional)</small></td></tr>
11373
- <tr><td class="name">condition</td><td class="type">FilterSet</td><td class="desc">A declarative twin of the predicate, pushed to the source while the function stays as the residual. On a pushdown engine this narrows the fetch instead of filtering a page client-side. It must be implied by the predicate: the grid ANDs both, so a twin wider than the function costs only time, while one narrower than it hides rows the function would have kept. <small>(optional)</small></td></tr>
12892
+ <tr><td class="name">condition</td><td class="type">FilterSet</td><td class="desc">A declarative twin of the predicate, pushed to the source while the function stays as the residual. On a pushdown engine this narrows the fetch instead of filtering a page client-side. It must be implied by the predicate: the grid ANDs both, so a twin wider than the function costs only time, while one narrower than it hides rows the function would have kept. **The twin is what works at any size.** Without one, a pushdown source can still run the function — but only as the residual over the whole matching set, so it does so only while that set is under `whereRowLimit` (default `50_000`) and refuses loudly past it (BACKLOG-0001268). A paged or remote source cannot run it at all and warns at registration. The twin is pushed to the engine, so it narrows the fetch itself and none of that applies. <small>(optional)</small></td></tr>
12893
+ </tbody>
12894
+ </table>
12895
+ </div>
12896
+ <h3 id="type-WhereRuntime">WhereRuntime</h3>
12897
+ <p class="section-note">The `where` predicates in force, as a source sees them (BACKLOG-0001268). A snapshot rather than the model, so a source can evaluate the predicates but cannot register or remove one through it.</p>
12898
+ <div class="table-wrap">
12899
+ <table>
12900
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12901
+ <tbody>
12902
+ <tr><td class="name">active</td><td class="type">boolean</td><td class="desc">Whether any predicate is registered at all.</td></tr>
12903
+ <tr><td class="name">names</td><td class="type">string[]</td><td class="desc">The registered names, in registration order — for diagnostics.</td></tr>
12904
+ <tr><td class="name">version</td><td class="type">number</td><td class="desc">Bumped on every registration or removal, so a cache key can track it.</td></tr>
12905
+ <tr><td class="name">passes</td><td class="type">(row: unknown, key?: string): boolean</td><td class="desc">Does this row survive every registered predicate?</td></tr>
11374
12906
  </tbody>
11375
12907
  </table>
11376
12908
  </div>
@@ -11400,7 +12932,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11400
12932
  <!-- END GENERATED TYPE REFERENCE -->
11401
12933
 
11402
12934
  <footer>
11403
- Lattice Grid 1.59.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
12935
+ Lattice Grid 1.61.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
11404
12936
  This document describes the behaviour of the shipped library. Where this guide and the code
11405
12937
  disagree, the code wins: please <a href="https://www.latticegrid.dev">tell us</a>.
11406
12938
  </footer>