@toclocoinc/lattice-grid 1.58.0 → 1.60.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 (113) hide show
  1. package/README.md +3 -3
  2. package/docs/API.html +1773 -73
  3. package/docs/api-detail.html +321 -5
  4. package/lattice-grid.d.ts +281 -2842
  5. package/lattice-grid.esm.min.js +879 -148
  6. package/lattice-grid.min.cjs +879 -148
  7. package/lattice-grid.min.css +1 -1
  8. package/lattice-grid.min.js +879 -148
  9. package/modules/ai.d.ts +401 -0
  10. package/modules/ai.esm.min.js +25 -6
  11. package/modules/ai.min.cjs +25 -6
  12. package/modules/ai.min.js +25 -6
  13. package/modules/angular.d.ts +31 -0
  14. package/modules/angular.esm.min.js +3 -2
  15. package/modules/angular.min.cjs +3 -2
  16. package/modules/angular.min.js +3 -2
  17. package/modules/chart-alluvial.d.ts +18 -0
  18. package/modules/chart-alluvial.esm.min.js +1 -1
  19. package/modules/chart-arc.d.ts +18 -0
  20. package/modules/chart-arc.esm.min.js +1 -1
  21. package/modules/chart-bubblemap.d.ts +18 -0
  22. package/modules/chart-bubblemap.esm.min.js +1 -1
  23. package/modules/chart-bump.d.ts +12 -0
  24. package/modules/chart-bump.esm.min.js +1 -1
  25. package/modules/chart-calendar.d.ts +12 -0
  26. package/modules/chart-calendar.esm.min.js +1 -1
  27. package/modules/chart-decomposition.d.ts +20 -0
  28. package/modules/chart-decomposition.esm.min.js +1 -1
  29. package/modules/chart-diverging.d.ts +12 -0
  30. package/modules/chart-diverging.esm.min.js +1 -1
  31. package/modules/chart-dumbbell.d.ts +18 -0
  32. package/modules/chart-dumbbell.esm.min.js +1 -1
  33. package/modules/chart-fan.d.ts +18 -0
  34. package/modules/chart-fan.esm.min.js +1 -1
  35. package/modules/chart-hexbin.d.ts +18 -0
  36. package/modules/chart-hexbin.esm.min.js +1 -1
  37. package/modules/chart-hexmap.d.ts +18 -0
  38. package/modules/chart-hexmap.esm.min.js +1 -1
  39. package/modules/chart-icicle.d.ts +12 -0
  40. package/modules/chart-icicle.esm.min.js +1 -1
  41. package/modules/chart-parallel.d.ts +19 -0
  42. package/modules/chart-parallel.esm.min.js +1 -1
  43. package/modules/chart-ridgeline.d.ts +14 -0
  44. package/modules/chart-ridgeline.esm.min.js +1 -1
  45. package/modules/chart-roc.d.ts +20 -0
  46. package/modules/chart-roc.esm.min.js +1 -1
  47. package/modules/chart-slope.d.ts +12 -0
  48. package/modules/chart-slope.esm.min.js +1 -1
  49. package/modules/chart-splom.d.ts +19 -0
  50. package/modules/chart-splom.esm.min.js +1 -1
  51. package/modules/chart-waffle.d.ts +12 -0
  52. package/modules/chart-waffle.esm.min.js +1 -1
  53. package/modules/charts.d.ts +122 -0
  54. package/modules/charts.esm.min.js +4 -4
  55. package/modules/charts.min.cjs +4 -4
  56. package/modules/charts.min.js +4 -4
  57. package/modules/data-router.d.ts +91 -0
  58. package/modules/data-router.esm.min.js +109 -17
  59. package/modules/data-router.min.cjs +109 -17
  60. package/modules/data-router.min.js +109 -17
  61. package/modules/devtools.d.ts +28 -0
  62. package/modules/devtools.esm.min.js +2 -2
  63. package/modules/devtools.min.cjs +2 -2
  64. package/modules/devtools.min.js +2 -2
  65. package/modules/dhtmlx-compat.d.ts +19 -0
  66. package/modules/dhtmlx-compat.esm.min.js +4 -4
  67. package/modules/dhtmlx-compat.min.cjs +4 -4
  68. package/modules/dhtmlx-compat.min.js +4 -4
  69. package/modules/gantt.d.ts +515 -0
  70. package/modules/gantt.esm.min.js +109 -33
  71. package/modules/gantt.min.cjs +109 -33
  72. package/modules/gantt.min.js +109 -33
  73. package/modules/htmx.d.ts +176 -0
  74. package/modules/htmx.esm.min.js +879 -148
  75. package/modules/htmx.min.cjs +879 -148
  76. package/modules/htmx.min.js +879 -148
  77. package/modules/kanban.d.ts +492 -0
  78. package/modules/kanban.esm.min.js +4 -4
  79. package/modules/kanban.min.cjs +4 -4
  80. package/modules/kanban.min.js +4 -4
  81. package/modules/kpi.d.ts +255 -0
  82. package/modules/kpi.esm.min.js +40 -7
  83. package/modules/kpi.min.cjs +40 -7
  84. package/modules/kpi.min.js +40 -7
  85. package/modules/layout.d.ts +332 -0
  86. package/modules/layout.esm.min.js +59 -6
  87. package/modules/layout.min.cjs +59 -6
  88. package/modules/layout.min.js +59 -6
  89. package/modules/mock-socket.d.ts +114 -0
  90. package/modules/mock-socket.esm.min.js +2 -2
  91. package/modules/mock-socket.min.cjs +2 -2
  92. package/modules/mock-socket.min.js +2 -2
  93. package/modules/react.d.ts +25 -0
  94. package/modules/react.esm.min.js +3 -2
  95. package/modules/react.min.cjs +3 -2
  96. package/modules/react.min.js +3 -2
  97. package/modules/svelte.d.ts +26 -0
  98. package/modules/svelte.esm.min.js +3 -2
  99. package/modules/svelte.min.cjs +3 -2
  100. package/modules/svelte.min.js +3 -2
  101. package/modules/tabs.d.ts +133 -0
  102. package/modules/tabs.esm.min.js +411 -9
  103. package/modules/tabs.min.cjs +411 -9
  104. package/modules/tabs.min.js +411 -9
  105. package/modules/vue.d.ts +24 -0
  106. package/modules/vue.esm.min.js +3 -2
  107. package/modules/vue.min.cjs +3 -2
  108. package/modules/vue.min.js +3 -2
  109. package/modules/webcomponent.d.ts +47 -0
  110. package/modules/webcomponent.esm.min.js +879 -148
  111. package/modules/webcomponent.min.cjs +879 -148
  112. package/modules/webcomponent.min.js +879 -148
  113. 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.58.0</p>
363
+ <p class="rail__sub">API reference · v1.60.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.58.0</span>
446
+ <span class="chip">Version 1.60.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>
@@ -944,7 +944,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
944
944
  <tbody>
945
945
  <tr><td class="name">statusBar</td><td class="type">boolean | { panels }</td><td class="desc">Composable panels along the bottom. Default set: <code>rowCount</code>, <code>selectedCount</code>, <code>aggregation</code>, <code>comments</code>, <code>updates</code>, <code>progress</code>. Each is silent when it has nothing to report.</td></tr>
946
946
  <tr><td class="name">maximise</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc"><code>false</code> removes the rail button and <code>grid.maximise</code>, for an application with its own full-screen mode.</td></tr>
947
- <tr><td class="name">toolPanel</td><td class="type">boolean | object</td><td class="desc">Side dock. <code>panels</code>: <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>, <code>statistics</code>, <code>regression</code>. <code>side: 'left'</code> makes it the icon rail, which also turns on <code>actions</code> (<code>undo</code>, <code>redo</code>, <code>pause</code>, <code>restore</code>, <code>maximise</code>, then the export group: <code>export</code>, <code>excel</code>, <code>clipboard</code>, <code>print</code>: nine in all, and an array takes these names rather than the button labels) and <code>icons</code>. An explicit array <em>replaces</em> that list rather than extending it; a bare <code>'-'</code> in it renders a divider between groups. <code>exportName</code> names the CSV. <code>annotate: true</code> adds the native annotation tools — <code>pen</code>, <code>arrow</code>, <code>rect</code>, <code>highlight</code> — to the rail as toggle buttons (pressed while in use, pressed again to exit); they also appear automatically for the duration of a presentation.</td></tr>
947
+ <tr><td class="name">toolPanel</td><td class="type">boolean | object</td><td class="desc">Side dock. <code>panels</code>: <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>, <code>statistics</code>, <code>regression</code>. <code>side: 'left'</code> makes it the icon rail, which also turns on <code>actions</code> (<code>undo</code>, <code>redo</code>, <code>pause</code>, <code>restore</code>, <code>maximise</code>, then the export group: <code>export</code>, <code>excel</code>, <code>clipboard</code>, <code>print</code>: nine in all, and an array takes these names rather than the button labels) and <code>icons</code>. An explicit array <em>replaces</em> that list rather than extending it; a bare <code>'-'</code> in it renders a divider between groups. <code>exportName</code> names the CSV. <code>annotate</code> is a three-state option, not a boolean flag. <code>annotate: true</code> adds the native annotation tools — <code>pen</code>, <code>arrow</code>, <code>rect</code>, <code>highlight</code> — to the rail as toggle buttons (pressed while in use, pressed again to exit), and they stay put whether or not a presentation is running. <code>annotate: false</code> opts OUT: none of the four tools are ever added, presentation or not. Omitting <code>annotate</code> keeps the default: the tools are off until a presentation starts, appear for its duration, and leave when it ends.</td></tr>
948
948
  <tr><td class="name">groupPanel</td><td class="type">boolean | object</td><td class="desc">A drag-and-drop group-by strip above the column header — the row-group panel. Drag a heading into it to group by that column; the active groups show as removable, reorderable chips, and reordering the chips changes the nesting order. It is keyboard-operable — arrows move between chips, <code>Shift</code> with an arrow reorders, <code>Delete</code> ungroups, and an add control groups any column — and every change is announced through the live region. Off by default and non-breaking; it drives the same model as <code>grid.columns.group()</code> and reimplements nothing. The object form takes <code>hint</code>, the placeholder shown while nothing is grouped.</td></tr>
949
949
  <tr><td class="name">kpis</td><td class="type">StatConfig[]</td><td class="desc">A built-in KPI/stat strip: a labelled band of stat tiles the grid places for you above the column header. Each entry is a <code>createStat</code> spec — <code>of</code>, <code>fn</code>, <code>title</code>, <code>interval</code>, <code>footer</code>, <code>format</code> and the rest, minus <code>grid</code> and <code>container</code>, which the grid supplies — so a strip tile and a hand-placed one are the same object. The tiles follow the grid's filters, recomputing on every change like a stand-alone stat does. Off by default and non-breaking; it reuses <code>createStat</code> and reimplements no compute.</td></tr>
950
950
  <tr><td class="name">timeZone</td><td class="type">string</td><td class="desc">An IANA zone every date column formats and parses in, so a grid shows one zone whatever the viewer's machine says. Individual columns may override it.</td></tr>
@@ -968,6 +968,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
968
968
  <tr><td class="name">cornerRadius</td><td class="type">boolean | number | string</td><td class="dflt">, </td><td class="desc">Round the grid's outer corners. <code>true</code> adopts the theme's radius, a number is pixels, a string is used as written.</td></tr>
969
969
  <tr><td class="name">stripedRows</td><td class="type">boolean</td><td class="dflt">false</td><td class="desc">Shade alternate data rows (zebra striping). Strictly opt-in, so an existing grid is unchanged on upgrade. Parity follows each row's logical index, so a stripe survives a scroll; group headings, footers and the grand total are never striped; selection and hover still win. Uses the theme's <code>--lattice-surface-alt</code>, so dark, high-contrast and terminal come for free.</td></tr>
970
970
  <tr><td class="name">verticalAlign</td><td class="type">'top' | 'middle' | 'bottom'</td><td class="dflt">, </td><td class="desc">Vertical alignment of cell content within a row, as a default for every column &mdash; the vertical counterpart to the per-column <code>align</code>. A column's own <code>verticalAlign</code> (or <code>cell.verticalAlign</code>) overrides it. Omitted, the grid keeps its historical placement (centred in a fixed-height row, top in an <code>autoHeight</code> row), so an existing grid is unchanged on upgrade. Setting a value aligns every column uniformly, including auto-height rows, unless a column opts out. See <a href="api-detail.html#vertical-align">Vertical alignment</a>.</td></tr>
971
+ <tr><td class="name">tooltip</td><td class="type">TooltipConfig</td><td class="dflt">, </td><td class="desc"><code>{ delay, maxWidth }</code> &mdash; grid-level defaults for the rich cell tooltip. <code>delay</code> is how long the pointer or the keyboard cursor must rest on a cell before anything is built, 400ms by default; <code>maxWidth</code> is how wide the tooltip may grow (a number is pixels, a string is used as written). Defaults only: it switches nothing on, and a grid whose columns declare no <code>cell.tooltip</code> has no tooltips whatever is set here. See <a href="#cell-tooltips">Rich cell tooltips</a>.</td></tr>
971
972
  <tr><td class="name">scrollbars</td><td class="type">'auto' | 'always' | { x, y }</td><td class="dflt">'auto'</td><td class="desc">Keep the scroll viewport's scrollbars visible. <code>'auto'</code> is the platform's native behaviour, where overlay scrollbars fade when idle; <code>'always'</code> keeps both axes shown whether or not the pointer is over the grid. The object form <code>{ x, y }</code> pins each axis on its own, so <code>{ y: 'always' }</code> keeps the vertical bar while the horizontal one stays native. Omitted, the grid is unchanged on upgrade. See <a href="api-detail.html#scrollbars">Always-visible scrollbars</a>.</td></tr>
972
973
  <tr><td class="name">columnTagFilter</td><td class="type">boolean | { multiple, label }</td><td class="dflt">, </td><td class="desc">A bar above the headings for showing only the columns carrying a chosen tag. Draws nothing unless some column has <code>tags</code>. See <a href="api-detail.html#column-tags">Column tags</a>.</td></tr>
973
974
  <tr><td class="name">rowTemplate</td><td class="type">string | { template, cardsPerRow, maxCardWidth, gap, className, role, itemRole }</td><td class="dflt">, </td><td class="desc">Draw each row with a template instead of dividing it into columns, a card list, a feed, a search-result list. Compiles once; binds with <code>{{data.field}}</code>. <code>cardsPerRow</code> or <code>maxCardWidth</code> puts several on a line. The pipeline underneath is unchanged. See <a href="api-detail.html#cards">Cards, lists and feeds</a>.</td></tr>
@@ -1091,7 +1092,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
1091
1092
  <tr><td class="name">class</td><td class="type">string | string[] | (p) =&gt; …</td><td class="desc">Classes for this column's cells.</td></tr>
1092
1093
  <tr><td class="name">classWhen</td><td class="type">{ [class]: (p) =&gt; boolean }</td><td class="desc">A class per predicate, re-evaluated as values change.</td></tr>
1093
1094
  <tr><td class="name">style / css</td><td class="type">CellStyle | (p) =&gt; CellStyle</td><td class="desc">Inline styles, static or computed.</td></tr>
1094
- <tr><td class="name">tooltip</td><td class="type">string | (p) =&gt; string</td><td class="desc"></td></tr>
1095
+ <tr><td class="name">tooltip</td><td class="type">string | (p) =&gt; string | ColumnTooltipSpec</td><td class="desc">A string or a function is the plain-text case and becomes the browser's own <code>title</code>. An <strong>object</strong> is a tooltip the grid draws itself &mdash; <code>{ render, mount, unmount }</code> &mdash; which can carry structure, markup or live content, is shown on keyboard focus as well as hover, and can be dismissed with Escape. See <a href="#cell-tooltips">Rich cell tooltips</a>.</td></tr>
1095
1096
  <tr><td class="name">align</td><td class="type">'start' | 'center' | 'end' | 'left' | 'right'</td><td class="desc">Horizontal alignment; also accepted at the top level of the column. <code>start</code>, <code>center</code> and <code>end</code> are <strong>logical</strong>: they follow the writing direction, so an <code>end</code>-aligned number column sits on the right edge in a left-to-right grid and on the left edge in a right-to-left one (<code>direction</code>). <code>left</code> and <code>right</code> are <strong>physical</strong>: they name an edge and keep it in both directions. <code>centre</code> is accepted for <code>center</code>. Omitted, the column takes its data type's default (numbers <code>end</code>, booleans <code>center</code>, text <code>start</code>). The heading follows the cell unless <code>header.align</code> says otherwise.</td></tr>
1096
1097
  <tr><td class="name">wrap / autoHeight</td><td class="type">, </td><td class="desc">Presentation flags.</td></tr>
1097
1098
  <tr><td class="name">spanColumns / spanRows</td><td class="type">(p) =&gt; number</td><td class="desc">Spanned cells render in their own layer so row recycling cannot clip them.</td></tr>
@@ -1119,12 +1120,97 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
1119
1120
  <tr><td class="name">edit</td><td class="desc"><code>enabled</code> (boolean or predicate), <code>editor</code>, <code>props</code>, <code>popup</code>, <code>validate</code></td></tr>
1120
1121
  <tr><td class="name">sort</td><td class="desc"><code>enabled</code>, <code>direction</code>, <code>order</code>, <code>nullsFirst</code></td></tr>
1121
1122
  <tr><td class="name">filter</td><td class="desc"><code>enabled</code>, <code>type</code>, <code>props</code></td></tr>
1122
- <tr><td class="name">layout</td><td class="desc"><code>width</code>, <code>min</code>, <code>max</code>, <code>flex</code>, <code>pin</code>, <code>hidden</code>, <code>resizable</code>, <code>movable</code>, <code>lockVisible</code>, <code>lockPosition</code>. <code>width</code> is a pixel number or a percentage string (<code>'25%'</code>): a share of the grid's inner width that <strong>follows the viewport</strong> &mdash; after the container changes size the column is re-resolved against the new width, clamped to its <code>min</code>/<code>max</code>, so a <code>'50%'</code> column is half of an 800px grid and half of the same grid at 400px. Percentages summing past 100 overflow and scroll rather than being scaled down. A <code>pin</code> of <code>&#39;start&#39;</code> or <code>&#39;end&#39;</code> holds the viewport edge while there is something to scroll; where the columns do not fill the grid, spare width falls beyond the last column rather than in front of it.</td></tr>
1123
+ <tr><td class="name">layout</td><td class="desc"><code>width</code>, <code>fit</code>, <code>min</code>, <code>max</code>, <code>flex</code>, <code>pin</code>, <code>hidden</code>, <code>resizable</code>, <code>movable</code>, <code>lockVisible</code>, <code>lockPosition</code>. <code>fit: 'content'</code> is the declarative form of <code>columns.autoSize()</code>: the column is sized to what it is showing on the first paint and measured again whenever the rows change, the columns are shown, hidden, reordered or pinned, or the grid is resized &mdash; but <strong>not</strong> as it scrolls, which would make the columns jitter. It measures the mounted rows and the heading, as <code>autoSize()</code> does, so it sizes to visible content rather than to the widest value in the dataset. A declared <code>width</code> outranks it, and so does a width the user drags to, a resize being recorded as a <code>width</code>; <code>flex</code> is resolved first and wins. <code>width</code> is a pixel number or a percentage string (<code>'25%'</code>): a share of the grid's inner width that <strong>follows the viewport</strong> &mdash; after the container changes size the column is re-resolved against the new width, clamped to its <code>min</code>/<code>max</code>, so a <code>'50%'</code> column is half of an 800px grid and half of the same grid at 400px. Percentages summing past 100 overflow and scroll rather than being scaled down. A <code>pin</code> of <code>&#39;start&#39;</code> or <code>&#39;end&#39;</code> holds the viewport edge while there is something to scroll; where the columns do not fill the grid, spare width falls beyond the last column rather than in front of it.</td></tr>
1123
1124
  <tr><td class="name">header</td><td class="desc"><code>template</code>, <code>render</code>, <code>props</code>, <code>class</code>, <code>tooltip</code>, <code>align</code>. <code>render</code> draws a custom heading and may be a <strong>function</strong> or a <strong>component</strong> (a class with a <code>render</code> method); the two forms are interchangeable and each may either append to the passed heading element itself (returning nothing) or <em>return</em> an <code>Element</code> (attached for you) or a <code>string</code> (used as the heading text). <code>class</code> adds a class to the heading cell; <code>template</code> is not read.</td></tr>
1124
1125
  </tbody>
1125
1126
  </table>
1126
1127
  </div>
1127
1128
 
1129
+ <h2 id="cell-tooltips">Rich cell tooltips</h2>
1130
+ <p class="lead-in">
1131
+ <code>cell.tooltip</code> as a string or a function gives you the browser's own
1132
+ <code>title</code>: one line of plain text, on the browser's schedule, unstyled, and invisible
1133
+ to a keyboard user. The <strong>object</strong> form declares a tooltip the grid draws instead,
1134
+ so a cell can show a related record, a small chart, a list of validation errors or an edit
1135
+ history (BACKLOG-0001204). The plain-text form is untouched and still becomes a
1136
+ <code>title</code>, so an existing grid behaves exactly as it did.
1137
+ </p>
1138
+ <p class="lead-in">
1139
+ <code>render(params)</code> returns one of four things, and the difference between the last two
1140
+ is a <strong>security property</strong> rather than a matter of taste:
1141
+ </p>
1142
+ <div class="table-wrap">
1143
+ <table>
1144
+ <thead><tr><th>Return</th><th>Rendered as</th></tr></thead>
1145
+ <tbody>
1146
+ <tr><td class="name">an <code>HTMLElement</code></td><td class="desc">Attached as it is. Your DOM, your responsibility.</td></tr>
1147
+ <tr><td class="name"><code>{ title, rows, note }</code></td><td class="desc">A <code>TooltipSpec</code>, drawn by the grid: a heading, label/value lines, and a closing note. <strong>Every field is written as text</strong>, so a spec built out of row values needs no escaping.</td></tr>
1148
+ <tr><td class="name"><code>{ html: '&hellip;' }</code></td><td class="desc">The <strong>only</strong> wrapper that inserts markup, scrubbed of script by the same rules the cell layer applies to <code>allowUnsafeTemplates</code> output.</td></tr>
1149
+ <tr><td class="name">a string</td><td class="desc"><strong>Always text</strong>, whatever it contains. A string holding <code>&lt;b&gt;bold&lt;/b&gt;</code> shows those characters; it does not embolden.</td></tr>
1150
+ </tbody>
1151
+ </table>
1152
+ </div>
1153
+ <p class="lead-in">
1154
+ That last rule is the load-bearing one. The most natural tooltip anyone writes is
1155
+ <code>render: (p) =&gt; p.value</code>, and a value comes from row data &mdash; data the developer
1156
+ did not write and usually cannot audit. If a bare string were treated as markup, a name field
1157
+ holding <code>&lt;img src=x onerror=&hellip;&gt;</code> would execute and nothing in the code
1158
+ would have looked dangerous. Markup therefore has to be asked for in the source, where a
1159
+ reviewer can see it, and no value arriving from data can promote itself.
1160
+ </p>
1161
+ <p class="lead-in">
1162
+ <code>mount(el, params)</code> and <code>unmount(el)</code> carry live content. Inside
1163
+ <code>mount</code> you call <code>createChart</code> or <code>createKPI</code> from a module
1164
+ bundle <em>your</em> application loaded &mdash; the grid core never imports a module &mdash; and
1165
+ <code>unmount</code> is called every time the tooltip closes, so nothing keeps running behind a
1166
+ hidden box. One tooltip element is built and re-used for every cell.
1167
+ </p>
1168
+ <p class="lead-in">
1169
+ <strong>Accessibility.</strong> Nothing is built until the pointer or the keyboard cursor has
1170
+ rested on the cell for <code>delay</code> (400ms by default), so sweeping across the grid mounts
1171
+ nothing. <strong>Focusing a cell shows the same tooltip</strong> after the same delay and the
1172
+ cell points at it with <code>aria-describedby</code>; the tooltip can be hovered without
1173
+ closing, and <strong>Escape dismisses it</strong> (WCAG 2.2 AA, 1.4.13). Escape is only consumed
1174
+ while a tooltip is open, so it still reaches the editor, the menu and the maximised view.
1175
+ </p>
1176
+ <p class="lead-in">
1177
+ <strong>It closes on scroll.</strong> Rows and cells are pooled and re-used, so a tooltip left
1178
+ open across a scroll would be anchored to a node that is now showing a <em>different row</em>.
1179
+ Closing is the honest answer and costs nothing: the browser re-hit-tests after a scroll, so a
1180
+ pointer parked over the grid simply gets a fresh tooltip for the row that is actually there.
1181
+ Content is resolved when the tooltip opens rather than when the pointer arrived, so it always
1182
+ names the row that node is showing at the moment it opens.
1183
+ </p>
1184
+
1185
+ <div class="example">
1186
+ <p class="example__label">Grid-level defaults, and a column declaring a spec tooltip</p>
1187
+ <pre data-run="js" data-expect="250, 360, Ada, a" data-covers="config:tooltip config:delay config:maxWidth"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
1188
+
1189
+ <span class="kw">const</span> grid = createHeadlessGrid({
1190
+ tooltip: { delay: <span class="num">250</span>, maxWidth: <span class="num">360</span> }, <span class="cmt">// the defaults for every tooltip</span>
1191
+ columns: [
1192
+ { field: <span class="str">'name'</span> },
1193
+ {
1194
+ field: <span class="str">'owner'</span>,
1195
+ cell: {
1196
+ tooltip: {
1197
+ <span class="cmt">// Returned as a spec: the grid writes every field as text.</span>
1198
+ render: (p) =&gt; ({ title: p.value, rows: [{ label: <span class="str">'Row'</span>, value: p.key }] }),
1199
+ },
1200
+ },
1201
+ },
1202
+ ],
1203
+ rows: [{ name: <span class="str">'a'</span>, owner: <span class="str">'Ada'</span> }],
1204
+ rowKey: <span class="str">'name'</span>,
1205
+ });
1206
+
1207
+ <span class="kw">const</span> defaults = grid.get(<span class="str">'tooltip'</span>);
1208
+ <span class="kw">const</span> spec = grid.columns.get(<span class="str">'owner'</span>).cell.tooltip;
1209
+ <span class="kw">const</span> content = spec.render({ value: <span class="str">'Ada'</span>, key: <span class="str">'a'</span> });
1210
+ grid.destroy();
1211
+ <span class="kw">return</span> `${defaults.delay}, ${defaults.maxWidth}, ${content.title}, ${content.rows[0].value}`;</code></pre>
1212
+ </div>
1213
+
1128
1214
  <h2 id="grid-methods">Grid methods</h2>
1129
1215
  <p class="section-note">Top-level members. Everything else hangs off a namespace.</p>
1130
1216
 
@@ -1132,7 +1218,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
1132
1218
  <table>
1133
1219
  <thead><tr><th>Member</th><th>Returns</th><th>Description</th></tr></thead>
1134
1220
  <tbody>
1135
- <tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.58.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.60.0'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
1136
1222
  <tr><td class="sig">get(key)</td><td class="type">unknown</td><td class="desc">Read any configuration key.</td></tr>
1137
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>
1138
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>
@@ -3974,6 +4060,58 @@ app.get('/api/orders', async (req, res) =&gt; {
3974
4060
  <span class="kw">const</span> block = <span class="kw">await</span> lax.fetch(req);
3975
4061
  <span class="kw">return</span> refused ? `refused; opted in: ${block.rows.length} rows` : 'not refused';</code></pre>
3976
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
+
3977
4115
  <h4 id="pushdown-aggregates">Pushing statistics down: <code>aggregates</code></h4>
3978
4116
  <p class="section-note">
3979
4117
  A DuckDB-class engine can compute a median or a standard deviation over the whole matching set
@@ -4260,6 +4398,10 @@ off(); <span class="cmt">// on() returns i
4260
4398
  <tr><td class="name">cell:edit:end</td><td class="type">{ row, key, colId, valid, errors }</td><td class="desc">It closed: committed or cancelled.</td></tr>
4261
4399
  <tr><td class="name">cell:clicked</td><td class="type">{ row, key, index, colId, column, value, text, event }</td><td class="desc">A cell was clicked. Announcement only: nothing is consumed, so editing and selection behave unchanged.</td></tr>
4262
4400
  <tr><td class="name">cell:dblclicked</td><td class="type">{ ...as cell:clicked }</td><td class="desc"></td></tr>
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>
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>
4263
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>
4264
4406
  <tr><td class="name">row:dblclicked</td><td class="type">{ row, key, index, event }</td><td class="desc"></td></tr>
4265
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>
@@ -5082,7 +5224,7 @@ grid.destroy();
5082
5224
  ].join(' | ');</code></pre>
5083
5225
 
5084
5226
  <h2 id="datarouter">The data router</h2>
5085
- <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>
5086
5228
  <pre><code>import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
5087
5229
 
5088
5230
  const router = createDataRouter({
@@ -5277,7 +5419,7 @@ customers.selection.set(['c1']); <span class="cmt">// emea; no selection in orde
5277
5419
  customers.destroy(); orders.destroy(); lines.destroy(); router.destroy();
5278
5420
  <span class="kw">return</span> out.join(' | ');</code></pre>
5279
5421
 
5280
- <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>
5281
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');
5282
5424
  <span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
5283
5425
 
@@ -5296,6 +5438,65 @@ router.apply([{ op: 'upsert', row: { id: 'a', n: 99, v: 4 } }]); <span class="cm
5296
5438
  g.destroy(); router.destroy();
5297
5439
  <span class="kw">return</span> [n, dropped, last].join(' | ');</code></pre>
5298
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
+
5299
5500
  <h3 id="datarouter-example">One feed, three grids, executed</h3>
5300
5501
  <p class="section-note">A single snapshot fanned to an orders grid, an invoices grid and a "rest" sink, then a
5301
5502
  delta that changes a row's partition &mdash; proving the fan-out, the sink, and that a moved row
@@ -5474,6 +5675,7 @@ g.destroy(); router.destroy();
5474
5675
 
5475
5676
  <h2 id="ganttmodule">The Gantt module</h2>
5476
5677
  <p><code>modules/gantt</code> is a separate, opt-in project-planning module &mdash; its own bundle, imported only when you want it, changing nothing in the grid core. It turns a task list into a real schedule: a <strong>CPM (Critical Path Method) engine</strong> computes each task's early/late start and finish, its slack (total float), and the zero-float <strong>critical path</strong>, recomputing on every edit. <code>computeSchedule(tasks, deps)</code> is the pure engine; <code>createGantt(opts)</code> is a controller that holds the model, recomputes on <code>setTasks</code>/<code>setDependencies</code>/<code>applyEdit</code>, and emits <code>schedule</code> (or <code>error</code>). Dependencies are the four standard link types &mdash; <code>LINK_TYPES</code> is <code>['FS','SS','FF','SF']</code> &mdash; each with optional lag/lead. A <strong>milestone</strong> is a zero-duration task scheduled as a point; a <strong>summary</strong> task (any task named as another's <code>parent</code>) is derived from its children (start = earliest child, end = latest child, duration-weighted progress) and is not scheduled itself. Bad input never throws or loops: a dependency cycle is refused and reported with a code from <code>SCHEDULE_ERROR</code>, and <code>findViolations</code> flags any task placed earlier than its predecessors allow. <code>toISODate</code> converts an engine day-number back to a calendar date for display.</p>
5678
+ <p><strong>Feed it the rows you already have.</strong> <code>fields</code> names your own task properties for the scheduler &mdash; <code>{ id: 'taskId', start: 'startDate', name: 'jobName', duration: 'dur' }</code>, each a field name or a reader <code>(row) =&gt; value</code> &mdash; so a task list arrives as it is rather than being renamed first. The vocabulary is <code>id</code>, <code>name</code>, <code>start</code>, <code>end</code>, <code>duration</code>, <code>milestone</code>, <code>percentComplete</code>, <code>parent</code>, <code>baselineStart</code>, <code>baselineEnd</code>, <code>constraint</code> and <code>constraintDate</code>; anything you leave unmapped reads its canonical name, so an existing plan is unaffected. <code>rowKey</code> reaches the scheduler too, so a row carrying <code>taskId</code> and no <code>id</code> is identified by it &mdash; a task's own <code>id</code> still wins where it has one. It is a <strong>read</strong> mapping: <code>applyEdit</code> and <code>level()</code> write the canonical property, so each says so plainly rather than writing to a field the schedule is not read from, and <code>assignee</code>/<code>cost</code>/<code>actualCost</code> belong to the resource and earned-value layers rather than to this list.</p>
5477
5679
  <pre><code>import { createGantt, computeSchedule } from '@toclocoinc/lattice-grid/modules/gantt';
5478
5680
 
5479
5681
  const plan = createGantt({
@@ -5493,7 +5695,8 @@ plan.applyEdit({ id: 'design', duration: 7 }); // recomputes; the critical path
5493
5695
  <table>
5494
5696
  <thead><tr><th>Function</th><th>What it does</th></tr></thead>
5495
5697
  <tbody>
5496
- <tr><td class="sig">createGantt({ tasks?, dependencies?, projectStart?, autoSchedule?, grid? })</td><td class="desc">Create a controller over a task list and a dependency list. Computes the CPM schedule immediately and on every edit; <code>on('schedule'|'error', fn)</code> subscribes; <code>applyEdit</code>/<code>setTasks</code>/<code>setDependencies</code> mutate and recompute; <code>grid</code> is kept for the write-back binding.</td></tr>
5698
+ <tr><td class="sig">createGantt({ tasks?, dependencies?, projectStart?, autoSchedule?, grid?, fields?, rowKey? })</td><td class="desc">Create a controller over a task list and a dependency list. Computes the CPM schedule immediately and on every edit; <code>on('schedule'|'error', fn)</code> subscribes; <code>applyEdit</code>/<code>setTasks</code>/<code>setDependencies</code> mutate and recompute; <code>grid</code> is kept for the write-back binding. <code>fields</code> maps your own task property names onto the ones the scheduler reads, and <code>rowKey</code> identifies a task for both <code>rows.apply</code> and the schedule.</td></tr>
5699
+ <tr><td class="sig">rows.apply(change) / rows.forEach(fn) / rows.count</td><td class="desc">The live consumer surface, the same keyed diff a grid, a board and a KPI panel accept, so one feed &mdash; or one tab strip &mdash; drives them all: <code>apply({ add, update, remove })</code> upserts by <code>rowKey</code> and recomputes, <code>forEach</code> visits every task, and <code>count</code> is how many the plan holds.</td></tr>
5497
5700
  <tr><td class="sig">computeSchedule(tasks, deps?, { projectStart? })</td><td class="desc">The pure CPM engine: forward/backward passes over the leaf tasks honouring FS/SS/FF/SF + lag, slack/float and the zero-float critical path, with summaries derived and cycles refused. Returns <code>{ ok, tasks, critical, criticalPaths, projectDuration, ... }</code> or <code>{ ok:false, error }</code>.</td></tr>
5498
5701
  <tr><td class="sig">findViolations(tasks, schedule)</td><td class="desc">The tasks whose user-placed start begins earlier than CPM allows (the manual-with-validation flag). Summaries, whose dates are derived, are skipped.</td></tr>
5499
5702
  <tr><td class="sig">toISODate(day)</td><td class="desc">Format an engine day-number as an ISO calendar date (<code>YYYY-MM-DD</code>, UTC).</td></tr>
@@ -6100,24 +6303,37 @@ import { createTabs } from '@toclocoinc/lattice-grid/modules/tabs';
6100
6303
 
6101
6304
  const tabs = createTabs(document.querySelector('#tabs'), {
6102
6305
  createGrid, <span class="cmt">// injected -- see below</span>
6306
+ createHeadlessGrid, <span class="cmt">// optional: lets an unvisited tab still carry a count</span>
6103
6307
  tabs: [
6104
6308
  { id: 'all', label: 'All', config: { rowKey: 'id', rows, columns } },
6105
6309
  { id: 'open', label: 'Open', from: 'all', where: (r) =&gt; r.stage === 'Open',
6106
6310
  follow: 'filtered', refresh: 'live', config: { columns } },
6107
6311
  { id: 'breached', label: 'Breached', from: 'open', <span class="cmt">// derives from Open, not All -- a chain</span>
6108
- where: (r) =&gt; r.daysOverdue &gt; 0, config: { columns } },
6312
+ where: (r) =&gt; r.daysOverdue &gt; 0, config: { columns },
6313
+ icon: '⚠️', badge: true, <span class="cmt">// "Breached 14" -- and it follows Open's filter</span>
6314
+ badgeTone: (count) =&gt; (count &gt; 0 ? 'bad' : 'good') },
6109
6315
  ],
6110
6316
  onBeforeTabChange: ({ id }) =&gt; !hasUnsavedEdit(), <span class="cmt">// veto a switch</span>
6111
6317
  });</code></pre>
6112
6318
  <p><strong>Lifecycle.</strong> A tab's grid mounts on <em>first activation</em>, not up front, and then stays alive &mdash; hidden, never destroyed &mdash; until the whole strip is. Per-tab scroll, selection, filters, sort, grouping, expansion — and an open cell/row editor — therefore survive a switch away and back natively, by simply not touching that grid instance, rather than through a lossy serialise/restore round-trip: leave a tab mid-edit, switch away, switch back, and the editor is exactly as it was left, uncommitted and undiscarded. Activating a derived tab materialises its whole ancestor chain first (mounted, hidden), and a cyclic <code>from</code> graph is refused &mdash; naming the exact cycle &mdash; when <code>createTabs</code> is called, not at first click.</p>
6113
6319
  <p><strong>A hidden tab costs nothing this module can spend.</strong> An inactive panel carries the <code>hidden</code> attribute (<code>display:none</code>); the module runs no timer, observer or repaint of its own against it. Measured with a real browser (<code>bench/tabs-idle.mjs</code>): several mounted-but-hidden, untouched tabs cost the same idle CPU as none at all. The one honestly-reported exception is not this module's: a <em>derived</em> tab's row model still re-derives on every change to its (possibly hidden) parent &mdash; by design, so reactivating it is instant rather than a stale flash &mdash; and the engine's own repaint listener for a derived source's <code>rows:changed</code> calls the renderer directly, bypassing <code>grid.updates.pause()</code> (which only holds the streaming-ingestion path). A hidden derived tab therefore still runs a read/compute/write pass on every parent change, even though the write phase paints a zero-size viewport; the bench measures and reports the size of that gap rather than leaving it inferred.</p>
6320
+ <p><strong>A tab can say what is waiting on it.</strong> <code>badge: true</code> puts that tab's own live row count on it &mdash; and for a derived tab the count <em>follows</em>, so filtering the parent restates the child's badge with it. A number or a string is a static badge; a function is handed the live count and returns what to show (<code>null</code> hides it). The tone is declared, never inferred from a threshold this module would have had to invent: <code>badgeTone</code> takes <code>good</code>, <code>warn</code>, <code>bad</code> or <code>unknown</code> &mdash; the same vocabulary a statistic tile grades to, so one <code>data-tone</code> rule in a theme dresses both &mdash; or a function of the count. <code>icon</code> adds a leading glyph: a single character or emoji, or an element you built. <strong>Never a markup string</strong>; nothing in this module parses HTML, and anything it cannot use is warned about once and ignored rather than coerced. Badges are <strong>off by default</strong>, and a tab that asks for none renders exactly the DOM it did before.</p>
6321
+ <p><strong>A tab nobody has clicked can still carry a count.</strong> Tabs mount lazily, so the tab most worth badging &mdash; the one you want to glance at &mdash; is precisely the one with no grid behind it. Inject the optional <code>createHeadlessGrid</code>, the same way <code>createGrid</code> is already injected and for the same reason, and an unactivated tab's count is computed with no element and no renderer, correct from first paint and still following its parent's filter. Leave it out and nothing breaks: that tab simply shows no badge until it is first activated, and the module says so once, naming the option. When the strip is narrow it is the <strong>label</strong> that gives way &mdash; it ellipsises while the count and the icon stay whole &mdash; and the strip still wraps rather than growing a scroll affordance.</p>
6322
+ <p><strong>A tab body does not have to be a grid.</strong> Give a tab a <code>view</code> &mdash; the factory that mounts its body, called as <code>(el, config) =&gt; instance</code> &mdash; and the tab hosts a <strong>kanban board, a KPI strip or a Gantt</strong> instead. <code>createKanban</code> and <code>createKPI</code> already have that signature, so they are passed straight in; the Gantt takes a single options object and mounts itself when given an element, so it is adapted in a line: <code>view: (el, config) =&gt; createGantt({ ...config, element: el })</code>. The factory is <strong>injected, never imported</strong>, for the same reason <code>createGrid</code> is. Such a tab's <code>config</code> is that viewer's own config.</p>
6323
+ <pre><code>{ id: 'board', label: 'Board', from: 'open', <span class="cmt">// derives from the Open tab</span>
6324
+ where: (r) =&gt; r.owner === me, follow: 'filtered', refresh: 'live',
6325
+ view: createKanban, badge: true,
6326
+ config: { rowKey: 'id', columnProperty: 'stage' } }</code></pre>
6327
+ <p><strong>And it derives exactly as a grid tab does.</strong> That is the point rather than a bonus: <code>from</code> plus the same narrowing (<code>where</code>, <code>group</code>, <code>join</code>, <code>follow</code>, &hellip;) applies to a board or a KPI strip just as it does to a grid, so filtering the parent tab restates the board beside it. The mechanism is the derived source you already have: for a non-grid body the module materialises a <strong>headless grid</strong> carrying that same derived source and pipes its rows into the viewer through <code>rows.apply({ add, update, remove })</code> &mdash; the keyed diff the grid, the board, the KPI panel and the Gantt all already accept from a Data Router. Nothing new is invented, the viewer stays unaware it is in a tab, and the module still imports no engine code. Deriving into a viewer therefore needs <code>createHeadlessGrid</code> injected alongside <code>createGrid</code>; without it the body mounts with its own rows and follows nothing, and the module says so once. A <code>view</code> tab with no <code>from</code> simply holds its own rows.</p>
6328
+ <p><strong>A body you have never opened is complete the moment you open it.</strong> Tabs mount lazily, so a board first shown after its parent has been filtered twice has a lot to catch up on &mdash; and because the headless grid holds the rows, there is no feed to join late: the body is seeded with the current set at mount and agrees exactly with a sibling that was mounted earlier. A count badge works on all three the same way, so a Gantt tab can read "Plan 14" as readily as a grid tab.</p>
6114
6329
  <p><strong>Accessibility.</strong> A real <code>role="tablist"</code>/<code>"tab"</code>/<code>"tabpanel"</code> with <code>aria-selected</code> and a roving <code>tabindex</code>, imitating the grid's own column-header keyboard model rather than the tool panel's tablist (which has the roles but no arrow-key handling). This is <strong>manual activation</strong>: <kbd>&larr;</kbd>/<kbd>&rarr;</kbd> and <kbd>Home</kbd>/<kbd>End</kbd> move the roving tab stop without switching the panel or mounting a grid; <kbd>Enter</kbd>/<kbd>Space</kbd>, or a click, activates. The newly active tab's label is announced through a polite live region.</p>
6115
6330
  <div class="table-wrap">
6116
6331
  <table>
6117
6332
  <thead><tr><th>Member</th><th>Description</th></tr></thead>
6118
6333
  <tbody>
6119
- <tr><td class="sig">createTabs(el, config)</td><td class="desc">Create a tabbed grid. <code>config.createGrid</code> is required (injected, not imported); <code>config.tabs</code> is a non-empty array of tab descriptors, each an <code>id</code>, a <code>label</code>, a grid <code>config</code>, and optionally <code>from</code> plus the derivation narrowing (<code>where</code>, <code>group</code>, <code>groupBy</code>, <code>bucket</code>, <code>join</code>, <code>unnest</code>, <code>refresh</code>, <code>crossFilter</code>, <code>follow</code>, <code>limit</code>, <code>sort</code>, <code>profile</code>) forwarded onto the derived source built for it.</td></tr>
6120
- <tr><td class="sig">tabs() / tab(id) / isMounted(id)</td><td class="desc">The configured tab ids, in order; a tab's live grid instance (or <code>null</code> before its first activation); whether a tab has been materialised yet.</td></tr>
6334
+ <tr><td class="sig">createTabs(el, config)</td><td class="desc">Create a tabbed grid. <code>config.createGrid</code> is required (injected, not imported); <code>config.createHeadlessGrid</code> is optional, and is what gives a never-activated tab a live count and lets a non-grid body derive. <code>config.tabs</code> is a non-empty array of tab descriptors, each an <code>id</code>, a <code>label</code>, a <code>config</code>, optionally an <code>icon</code> and a <code>badge</code>/<code>badgeTone</code>, and optionally <code>from</code> plus the derivation narrowing (<code>where</code>, <code>group</code>, <code>groupBy</code>, <code>bucket</code>, <code>join</code>, <code>unnest</code>, <code>refresh</code>, <code>crossFilter</code>, <code>follow</code>, <code>limit</code>, <code>sort</code>, <code>profile</code>) forwarded onto the derived source built for it.</td></tr>
6335
+ <tr><td class="sig">tab.view</td><td class="desc">Mount something other than a grid in this tab: the factory that builds it, called as <code>(el, config) =&gt; instance</code> &mdash; <code>createKanban</code>, <code>createKPI</code>, or a one-line Gantt adapter. The tab's <code>config</code> is then that viewer's own config, and the tab derives from <code>from</code> exactly as a grid tab does (which needs <code>createHeadlessGrid</code>).</td></tr>
6336
+ <tr><td class="sig">tabs() / tab(id) / isMounted(id)</td><td class="desc">The configured tab ids, in order; the live instance mounted in a tab &mdash; the grid, or the viewer for a <code>view</code> tab (or <code>null</code> before its first activation); whether a tab has been materialised yet.</td></tr>
6121
6337
  <tr><td class="sig">activate(id, opts)</td><td class="desc">Switch the active tab, gated by <code>beforeTabChange</code>. Returns <code>true</code>/<code>false</code> synchronously with no handler registered, or a <code>Promise&lt;boolean&gt;</code> when a handler deferred.</td></tr>
6122
6338
  <tr><td class="sig">on(name, fn) / off(name, fn)</td><td class="desc">Events: <code>tab:changed</code>, the cancellable <code>beforeTabChange</code> (call <code>preventDefault(reason?)</code> or return <code>false</code> to veto), and its paired <code>tabChange:cancelled</code>. Config sugar: <code>onTabChange</code>, <code>onBeforeTabChange</code>, <code>onTabChangeCancelled</code>.</td></tr>
6123
6339
  <tr><td class="sig">destroy()</td><td class="desc">Tear the whole strip down; destroys every mounted tab's grid (each isolated, so one throwing does not strand the rest).</td></tr>
@@ -6126,7 +6342,7 @@ const tabs = createTabs(document.querySelector('#tabs'), {
6126
6342
  </div>
6127
6343
  <h3 id="tabs-live-example">All / Open / Breached, a two-deep derivation chain, executed</h3>
6128
6344
  <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>
6129
- <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');
6345
+ <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');
6130
6346
  <span class="kw">const</span> { root } = createTestDom();
6131
6347
  <span class="kw">const</span> { createGrid } = <span class="kw">await</span> import('../packages/dom/src/index.js');
6132
6348
  <span class="kw">const</span> { createTabs } = <span class="kw">await</span> import('../packages/modules/tabs/index.js');
@@ -6154,6 +6370,73 @@ tabs.activate('breached'); <span class="cmt">// materialises
6154
6370
  tabs.destroy();
6155
6371
  <span class="kw">return</span> counts.join(','); <span class="cmt">// 4,3,2</span></code></pre>
6156
6372
 
6373
+ <h3 id="tabs-governed-example">Opening on a chosen tab, and vetoing a switch, executed</h3>
6374
+ <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>
6375
+ <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');
6376
+ <span class="kw">const</span> { root } = createTestDom();
6377
+ <span class="kw">const</span> { createGrid } = <span class="kw">await</span> import('../packages/dom/src/index.js');
6378
+ <span class="kw">const</span> { createTabs } = <span class="kw">await</span> import('../packages/modules/tabs/index.js');
6379
+
6380
+ <span class="kw">const</span> rows = [{ id: 1 }];
6381
+ <span class="kw">const</span> columns = [{ field: 'id' }];
6382
+ <span class="kw">const</span> log = [];
6383
+
6384
+ <span class="kw">const</span> tabs = createTabs(root, {
6385
+ createGrid,
6386
+ active: 'review', <span class="cmt">// open here, not on the first tab</span>
6387
+ tabs: [
6388
+ { id: 'draft', label: 'Draft', config: { rowKey: 'id', rows, columns } },
6389
+ { id: 'review', label: 'Review', config: { rowKey: 'id', rows, columns } },
6390
+ { id: 'locked', label: 'Locked', config: { rowKey: 'id', rows, columns } },
6391
+ ],
6392
+ onTabChange: (e) =&gt; log.push(`changed:${e.id}`),
6393
+ onBeforeTabChange: (e) =&gt; { <span class="kw">if</span> (e.id === 'locked') e.preventDefault('sealed'); },
6394
+ onTabChangeCancelled: (e) =&gt; log.push(`cancelled:${e.id}(${e.reason})`),
6395
+ });
6396
+
6397
+ <span class="kw">const</span> opened = tabs.activeId; <span class="cmt">// 'review' -- config.active won</span>
6398
+ tabs.activate('draft'); <span class="cmt">// allowed, so onTabChange fires</span>
6399
+ tabs.activate('locked'); <span class="cmt">// vetoed, so onTabChangeCancelled fires</span>
6400
+ <span class="kw">const</span> ended = tabs.activeId; <span class="cmt">// still 'draft'</span>
6401
+ tabs.destroy();
6402
+ <span class="kw">return</span> `${opened} | ${log.join('; ')} | ${ended}`;</code></pre>
6403
+
6404
+ <h3 id="tabs-headless-example">A tab nobody has clicked, carrying a live count, executed</h3>
6405
+ <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>
6406
+ <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');
6407
+ <span class="kw">const</span> { root } = createTestDom();
6408
+ <span class="kw">const</span> { createGrid } = <span class="kw">await</span> import('../packages/dom/src/index.js');
6409
+ <span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
6410
+ <span class="kw">const</span> { createTabs } = <span class="kw">await</span> import('../packages/modules/tabs/index.js');
6411
+
6412
+ <span class="kw">const</span> rows = [
6413
+ { id: 1, stage: 'Open', daysOverdue: 0 },
6414
+ { id: 2, stage: 'Open', daysOverdue: 5 },
6415
+ { id: 3, stage: 'Won', daysOverdue: 0 },
6416
+ { id: 4, stage: 'Open', daysOverdue: 2 },
6417
+ ];
6418
+ <span class="kw">const</span> columns = [{ field: 'id' }, { field: 'stage' }, { field: 'daysOverdue', type: 'number' }];
6419
+
6420
+ <span class="kw">const</span> tabs = createTabs(root, {
6421
+ createGrid,
6422
+ createHeadlessGrid, <span class="cmt">// what gives an unmounted tab a count</span>
6423
+ tabs: [
6424
+ { id: 'all', label: 'All', badge: <span class="kw">true</span>, config: { rowKey: 'id', rows, columns } },
6425
+ { id: 'breached', label: 'Breached', from: 'all', where: (r) =&gt; r.daysOverdue &gt; 0,
6426
+ follow: 'filtered', refresh: 'live', badge: <span class="kw">true</span>, config: { columns } },
6427
+ ],
6428
+ });
6429
+
6430
+ <span class="cmt">// 'breached' has never been activated, so it has no grid of its own.</span>
6431
+ <span class="kw">const</span> button = root.querySelectorAll('[role=tab]')
6432
+ .find((b) =&gt; b.getAttribute('data-tab-id') === 'breached');
6433
+ <span class="kw">const</span> badge = button.children
6434
+ .find((c) =&gt; String(c.className || '').includes('lat-tabs__badge'));
6435
+ <span class="kw">const</span> state = tabs.isMounted('breached') ? 'mounted' : 'unmounted';
6436
+ <span class="kw">const</span> text = String(badge.textContent).trim().replace(/\s+/g, ' ');
6437
+ tabs.destroy();
6438
+ <span class="kw">return</span> `${state} | ${text}`; <span class="cmt">// unmounted | 2 rows</span></code></pre>
6439
+
6157
6440
  <h2 id="layout">The dashboard layout</h2>
6158
6441
  <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>
6159
6442
  <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>
@@ -6175,25 +6458,25 @@ const layout = createLayout(document.querySelector('#dash'), {
6175
6458
  createGrid(layout.payload('pipeline'), { rowKey: 'id', rows, columns });</code></pre>
6176
6459
  <p><strong>Two independent overflow axes, not one setting.</strong> <code>overflowX</code> and <code>overflowY</code> are each <code>'static'</code> or <code>'scroll'</code>, because a dashboard that scrolls both ways is ordinary and a single enum cannot express it. The difference is what happens to a track's size. A <strong>static</strong> axis divides the mounted element with <code>minmax(0, 1fr)</code> &mdash; never a bare <code>1fr</code>, whose implicit <code>auto</code> minimum lets one stubborn payload drag a track past the container. A <strong>scrolling</strong> axis repeats a <em>fixed</em> track (<code>columnWidth</code> / <code>rowHeight</code>) and the canvas extends past the viewport, which then scrolls. That is the owner's "maintaining their sizing", and it is the difference between this and <code>flex-wrap</code>: measured in a real browser, ten 200px columns in a 600px host paint at 200px each over a 2000px canvas, and <em>shrinking the host to 300px leaves the column at 200px</em> and scrolls further.</p>
6177
6460
  <p><strong>Spacing takes a real CSS length.</strong> <code>gap</code>, <code>padding</code>, <code>columnWidth</code> and <code>rowHeight</code> each accept a number (pixels), or a string: <code>'200px'</code>, <code>'25%'</code>, <code>'1fr'</code>, <code>'2rem'</code>, <code>'10vh'</code>. Percentages that sum past 100 are allowed to overflow and scroll rather than being silently scaled down, which is the honest outcome. Anything outside that vocabulary &mdash; including <code>calc()</code> and <code>var()</code> &mdash; is refused by name with one warning and replaced by the default, because the value is written into an inline style.</p>
6178
- <p><strong>Rearrangement.</strong> <code>compact: 'vertical'</code> (the default) pushes displaced windows down and then pulls everything up into whatever space that left, so a window dropped into empty space falls to the top of its column &mdash; <code>window:moved</code> carries both <code>to</code> (where it was asked to go) and <code>landed</code> (where it actually ended up). <code>compact: 'none'</code> keeps every window exactly where it is put. There is no horizontal compactor: pushing sideways has no single obviously-correct direction, and getting it wrong silently rearranges a dashboard a user carefully built.</p>
6461
+ <p><strong>Rearrangement.</strong> <code>compact: 'vertical'</code> (the default) pushes displaced windows down and then pulls everything up into whatever space that left, so a window dropped into empty space falls to the top of its column &mdash; <code>window:moved</code> carries both <code>to</code> (where it was asked to go) and <code>landed</code> (where it actually ended up). <code>compact: 'none'</code> keeps every window exactly where it is put. <code>compact: 'horizontal'</code> floats windows <strong>left</strong> into vacated space instead, so dragging a window out of a row closes the hole sideways rather than leaving it open. It is a third <em>value</em>, not a second pass over the other axis: one gravity direction, never two, because two directions each vacate space the other wants and the arrangement would then depend on which ran last. It settles windows in a canonical <strong>left-to-right, then top-to-bottom</strong> order, exactly mirroring vertical's <strong>top-to-bottom, then left-to-right</strong>, so the result depends only on where the windows are and never on the order they were declared or dragged in &mdash; the same guarantee vertical has always made, now made on both axes and asserted over all 720 declaration orders of a six-window dashboard on each axis. A value that is none of the three warns once, naming what it was given, and falls back to <code>'vertical'</code>. <strong>The axis a mode compacts along is the axis that can overflow:</strong> under <code>'horizontal'</code> a window that genuinely does not fit the columns settles in an implicit track and can be clipped on a <code>static</code> horizontal axis &mdash; exactly as a <code>'vertical'</code> dashboard can be clipped past its rows on a <code>static</code> vertical axis today. <code>overflowX: 'scroll'</code> is to <code>'horizontal'</code> what <code>overflowY: 'scroll'</code> is to the default.</p>
6179
6462
  <p><strong>Keyboard, to the same standard as the drag.</strong> Every movable and resizable window carries a focusable handle running the full grab / move / drop / cancel model the kanban board established: <kbd>Space</kbd> or <kbd>Enter</kbd> grabs, the arrow keys move a tentative placement, <kbd>Enter</kbd> drops it through the same <code>beforeWindowMove</code> gate the pointer drag uses, and <kbd>Escape</kbd> cancels. A polite live region announces every step &mdash; grabbed, each tentative position with its column and row, dropped, cancelled, and <em>reverted</em> when a handler vetoes the drop &mdash; and focus returns to the handle afterwards. A window with <code>chrome: false</code> still gets a handle, because a movable window a keyboard user cannot move is not movable.</p>
6180
6463
  <p><strong>An &ldquo;Edit layout&rdquo; button, without rebuilding the dashboard.</strong> <code>closable</code>, <code>movable</code> and <code>resizable</code> also take a <em>layout-level</em> default, so unlocking a twelve-window dashboard is one setting rather than twenty-four, and <code>setInteractive(true|false|{movable, resizable, closable})</code> changes that default at runtime &mdash; unlock, let the user rearrange, lock again and save <code>getLayout()</code>. Nothing is destroyed and nothing is rebuilt, so every grid, chart and board mounted in a window survives the toggle untouched. <strong>The asymmetry is deliberate: you can always take a capability away; you can never grant one where the developer said no.</strong> <code>setInteractive(false)</code> locks every window, including one whose own spec says <code>movable: true</code>, so a dashboard hard-locks in a single call without auditing twelve window specs; <code>setInteractive(true)</code> unlocks only the windows that never opted out, so a masthead declared <code>movable: false</code> stays pinned. Both halves of the enforcement move together &mdash; the handles a window renders <em>and</em> the checks the pointer and keyboard paths make, because removing a handle stops a mouse while only the gesture check stops a keyboard user already standing on one. <strong><code>config.movable: false</code> and <code>setInteractive(false)</code> are deliberately not the same thing:</strong> the config states the <em>default</em> for windows that declare nothing &mdash; and <code>false</code> is already that default, so it takes nothing away from a window that declared <code>movable: true</code> &mdash; while <code>setInteractive(false)</code> is an <em>active lock</em> that pins every window whatever its own spec says. <code>getInteractive()</code> reports all three states rather than two: <code>undefined</code> where no layout-level default is in force, <code>true</code>, or <code>false</code> for a lock. Reporting &ldquo;unset&rdquo; as <code>false</code> would read correctly and round-trip wrongly, so <code>setInteractive(getInteractive())</code> is a no-op in every state, and a key carrying <code>undefined</code> means &ldquo;leave this capability alone&rdquo;. Interactivity is a <em>mode</em>, not part of the arrangement: <code>getLayout()</code> does not carry it, <code>setLayout()</code> does not read it, and no event fires. <strong>A locked layout is not a read-only dashboard:</strong> the module creates the payload container and never reads or writes its contents, so a grid inside a window is made read-only with the grid's own settings &mdash; a dashboard that must not be edited is two decisions, not one.</p>
6181
6464
  <p><strong>Which payloads re-lay-out on <code>window:resized</code>, honestly.</strong> The grid and the chart each own a <code>ResizeObserver</code> and respond correctly; the Gantt does too since 1.52.0; kanban and KPI do no JS work at all on a resize and need none, because they reflow by CSS construction (a kanban column keeps its 280px and the board starts scrolling). Every one of those is measured in <code>test/layout-browser.test.js</code> rather than asserted, including a grid column declared as a <em>percentage</em> (<code>layout: { width: '50%' }</code>), which follows the window (BACKLOG-0001117): half of the viewport at 800px, half of it again at 400px.</p>
6182
6465
  <p><strong>Idle cost, measured.</strong> A twelve-window dashboard is <strong>indistinguishable from a page with no layout module on it at all</strong>. Over 8 seconds of real Chrome (<code>bench/layout-idle.mjs</code>), twelve windows with empty payloads, twelve <em>independent</em> live grids in them, and a single lone grid with no layout module all sit in the same few-millisecond band &mdash; under a tenth of one percent of a core. They are not separated here because they cannot be: seven runs across two machines land between 3.1ms and 9.0ms and the ordering between them inverts run to run, so a stated delta would be reporting the noise floor. The module adds no timer, no frame loop and no polling, and owns exactly one <code>ResizeObserver</code> for the whole layout rather than one per window. <strong>The one figure that is a result rather than noise is not this module's:</strong> twelve grids <em>derived</em> from one shared parent filtered at 20Hz cost <strong>3,155&ndash;3,409ms</strong> over the same 8 seconds &mdash; several hundred times the quiet band, and stable across every run &mdash; because the engine's repaint listener for a derived source's <code>rows:changed</code> calls the renderer directly and bypasses <code>grid.updates.pause()</code>. The bench reports the size of that gap rather than leaving it inferred.</p>
6183
6466
  <p><strong>Closing a window does not destroy its payload.</strong> <code>window:closed</code> hands the payload container back; whatever you mounted inside it is yours to destroy. Stated plainly because a leaked grid per closed window is the obvious failure, and this module has no way to know that a <code>div</code> contains something with a <code>destroy()</code>.</p>
6184
- <p><strong>Not in v1:</strong> horizontal compaction; per-frame drag events; nested layouts; window maximise/minimise; tabbed windows (that is <code>modules/tabs</code>); and <strong>responsive breakpoints &mdash; a twelve-window dashboard on a phone is unsolved, and this does not pretend otherwise</strong>. Server-side persistence is the host's, with <code>getLayout()</code>.</p>
6467
+ <p><strong>Not in v1:</strong> per-frame drag events; nested layouts; tabbed windows (that is <code>modules/tabs</code>); and <strong>responsive breakpoints &mdash; a twelve-window dashboard on a phone is unsolved, and this does not pretend otherwise</strong>. Server-side persistence is the host's, with <code>getLayout()</code>.</p>
6185
6468
  <div class="table-wrap">
6186
6469
  <table>
6187
6470
  <thead><tr><th>Member</th><th>Description</th></tr></thead>
6188
6471
  <tbody>
6189
- <tr><td class="sig">createLayout(el, config)</td><td class="desc">Create a dashboard layout. <code>columns</code>/<code>rows</code> (default 12/6) divide the element; <code>overflowX</code>/<code>overflowY</code> are each <code>'static'</code> or <code>'scroll'</code>; <code>columnWidth</code>/<code>rowHeight</code> are the fixed track sizes a scrolling axis uses; <code>gap</code> (8px), <code>padding</code> (5px) and <code>compact</code> (<code>'vertical'</code>) complete it. A second mount on the same element is refused by name.</td></tr>
6472
+ <tr><td class="sig">createLayout(el, config)</td><td class="desc">Create a dashboard layout. <code>columns</code>/<code>rows</code> (default 12/6) divide the element; <code>overflowX</code>/<code>overflowY</code> are each <code>'static'</code> or <code>'scroll'</code>; <code>columnWidth</code>/<code>rowHeight</code> are the fixed track sizes a scrolling axis uses; <code>gap</code> (8px), <code>padding</code> (5px) and <code>compact</code> (<code>'vertical'</code>, <code>'horizontal'</code> or <code>'none'</code>, default <code>'vertical'</code>) complete it. A second mount on the same element is refused by name.</td></tr>
6190
6473
  <tr><td class="sig">config.windows[]</td><td class="desc">Each window: <code>id</code> (required, unique), <code>xPos</code>/<code>yPos</code>/<code>xSize</code>/<code>ySize</code> in 1-based cells (auto-placed in the first free cell when omitted), <code>title</code>, <code>chrome</code> (default <code>true</code>), and <code>closable</code>/<code>movable</code>/<code>resizable</code>/<code>maximisable</code>/<code>minimisable</code> (all default <code>false</code>, so a dashboard the developer wants fixed is fixed without opting out of anything; each also takes a layout-level default of the same name, which a window's own boolean overrides). <code>padding</code> and <code>payloadId</code> (default <code>`${id}-body`</code>) override per window.</td></tr>
6191
6474
  <tr><td class="sig">payload(id) / window(id) / windows()</td><td class="desc">The payload container for a window &mdash; the <code>div</code> carrying its <code>payloadId</code>, which you fill; a copy of a window's current descriptor; every window id in mount order.</td></tr>
6192
6475
  <tr><td class="sig">add(spec) / move(id, to) / close(id)</td><td class="desc">Add a window after mount (returns its payload container); move or resize one through the same before-events the drag uses; close one through <code>beforeWindowClose</code>. <code>move</code> and <code>close</code> return <code>true</code>/<code>false</code> synchronously with no handler registered, or a <code>Promise&lt;boolean&gt;</code> when a handler deferred.</td></tr>
6193
6476
  <tr><td class="sig">getLayout() / setLayout(snapshot)</td><td class="desc">The full current arrangement as plain JSON (<code>{columns, rows, windows: [{id, xPos, yPos, xSize, ySize}]}</code>), and its restore. <code>setLayout</code> never throws on garbage, and an entry naming a window that does not exist yet is <em>retained</em> and applied when that window is added.</td></tr>
6194
6477
  <tr><td class="sig">getState() / setState(snapshot)</td><td class="desc">The versioned persistence pair, following core's and the Gantt's shape: no arguments in, one plain JSON-safe object out, and <code>setState</code> survives whatever is handed to it.</td></tr>
6195
6478
  <tr><td class="sig">setInteractive(value) / getInteractive()</td><td class="desc">Lock or unlock the whole dashboard at runtime, without destroying it. A boolean sets <code>movable</code>, <code>resizable</code> and <code>closable</code> together; an object sets only the keys it carries, and a key carrying <code>undefined</code> is treated as absent; <code>getInteractive()</code> returns the layout-level values as a copy, three-valued (<code>undefined</code> for unset, <code>true</code>, or <code>false</code> for a lock) so that <code>setInteractive(getInteractive())</code> is a no-op in every state. The config keys of the same name state the <em>default</em>; only this method takes a capability away. Locking always wins and unlocking never overrides an opt-out: <code>setInteractive(false)</code> pins a window that declared <code>movable: true</code>, and <code>setInteractive(true)</code> leaves a window that declared <code>movable: false</code> pinned. No event fires and <code>getLayout()</code> is unchanged &mdash; a mode is not an arrangement. It does not touch <code>maximisable</code> or <code>minimisable</code> either, for the same reason.</td></tr>
6196
- <tr><td class="sig">maximise(id) / minimise(id) / restore(id)</td><td class="desc"><strong>Maximise fills the layout host</strong> &mdash; the element you mounted on &mdash; not the browser window, and hides every other window for the duration. That is deliberate: filling the viewport means <code>position: fixed</code>, whose containing block is the nearest ancestor carrying a <code>transform</code>, <code>filter</code>, <code>contain</code> or <code>will-change</code>, so the same rule fills the screen on one page and lands in a 300px box on the next; filling the host is a geometry change inside the layout and cannot disturb the page around it. <strong>Nothing moves</strong>: no compaction runs, no placement changes, and the payload container is the same DOM node throughout, so whatever you mounted in it is untouched. <strong>Escape restores it</strong> from anywhere inside the layout &mdash; a focused grid body cell or column heading included &mdash; unless something inside has already claimed the key: an open cell editor, a filter menu or a column menu closes first, and the next Escape restores the window. A grid claims only an Escape it actually used, so a maximised grid never keeps the key (BACKLOG-0001143). Afterwards focus lands on the window's maximise control, so a keyboard user is somewhere they can act rather than wherever the payload left them. <code>minimise(id)</code> draws a window as a single row and hides its payload, keeping the chrome that carries the way back &mdash; so under <code>compact: 'vertical'</code> the windows below <em>pull up</em> on screen, which is the point of minimising one. <strong>In the arrangement, nothing moves at all:</strong> the collapse is a projection of the dashboard, not a change to it, so <code>restore(id)</code> gives back exactly the arrangement that was there &mdash; in <strong>any</strong> order, with any number of other windows still collapsed. All 14,400 minimise/restore orderings of a five-window dashboard are asserted. A window with <code>chrome: false</code> is refused by name: there would be nothing left on screen to restore it with. The controls are opt-in per window (<code>maximisable</code>, <code>minimisable</code>) with a layout-level default of the same name, and <code>setInteractive()</code> does <strong>not</strong> touch them &mdash; a display mode neither moves nor resizes a window in the arrangement, so a locked dashboard can still be blown up to read.</td></tr>
6479
+ <tr><td class="sig">maximise(id) / minimise(id) / restore(id)</td><td class="desc"><strong>Maximise fills the layout host</strong> &mdash; the element you mounted on &mdash; not the browser window, and hides every other window for the duration. That is deliberate: filling the viewport means <code>position: fixed</code>, whose containing block is the nearest ancestor carrying a <code>transform</code>, <code>filter</code>, <code>contain</code> or <code>will-change</code>, so the same rule fills the screen on one page and lands in a 300px box on the next; filling the host is a geometry change inside the layout and cannot disturb the page around it. <strong>Nothing moves</strong>: no compaction runs, no placement changes, and the payload container is the same DOM node throughout, so whatever you mounted in it is untouched. <strong>Escape restores it</strong> from anywhere inside the layout &mdash; a focused grid body cell or column heading included &mdash; unless something inside has already claimed the key: an open cell editor, a filter menu or a column menu closes first, and the next Escape restores the window. A grid claims only an Escape it actually used, so a maximised grid never keeps the key (BACKLOG-0001143). Afterwards focus lands on the window's maximise control, so a keyboard user is somewhere they can act rather than wherever the payload left them. <code>minimise(id)</code> draws a window as a single row and hides its payload, keeping the chrome that carries the way back &mdash; so the rest of the dashboard closes up around it under whichever <code>compact</code> mode is configured &mdash; the windows below <em>pull up</em> under <code>'vertical'</code>, the windows beside it float <em>left</em> under <code>'horizontal'</code> &mdash; which is the point of minimising one. <strong>In the arrangement, nothing moves at all:</strong> the collapse is a projection of the dashboard, not a change to it, so <code>restore(id)</code> gives back exactly the arrangement that was there &mdash; in <strong>any</strong> order, with any number of other windows still collapsed. All 14,400 minimise/restore orderings of a five-window dashboard are asserted. A window with <code>chrome: false</code> is refused by name: there would be nothing left on screen to restore it with. The controls are opt-in per window (<code>maximisable</code>, <code>minimisable</code>) with a layout-level default of the same name, and <code>setInteractive()</code> does <strong>not</strong> touch them &mdash; a display mode neither moves nor resizes a window in the arrangement, so a locked dashboard can still be blown up to read.</td></tr>
6197
6480
  <tr><td class="sig">maximised() / minimised()</td><td class="desc">The id of the window filling the host (at most one &mdash; maximising a second restores the first), or <code>null</code>; and the ids of every minimised window in mount order. Neither state is part of <code>getLayout()</code>: a mode is not an arrangement, so <code>getLayout()</code> reports the <em>underlying</em> placement in both states &mdash; where the window will be when restored &mdash; and <code>setLayout()</code> never restores anyone into a mode, moving a minimised window <em>under</em> it instead.</td></tr>
6198
6481
  <tr><td class="sig">refresh()</td><td class="desc">Re-measure every window and emit <code>window:resized</code> for those that changed. Called automatically; exposed for a host that changed something the module cannot observe, such as revealing an ancestor.</td></tr>
6199
6482
  <tr><td class="sig">on(name, fn) / off(name, fn)</td><td class="desc">Events: <code>window:moved</code>, <code>window:resized</code>, <code>window:closed</code>, <code>layout:changed</code>; the cancellable <code>beforeWindowMove</code>, <code>beforeWindowResize</code> and <code>beforeWindowClose</code> (call <code>preventDefault(reason?)</code> or return <code>false</code>), each paired with <code>windowMove:cancelled</code>, <code>windowResize:cancelled</code> and <code>windowClose:cancelled</code>. <code>'*'</code> subscribes to every past-tense event and is deliberately never delivered a before-event. Config sugar for all ten. Drag progress is <strong>not</strong> emitted per frame.</td></tr>
@@ -6656,7 +6939,9 @@ grid.filters.reapply(); <span class="cmt">// re-run all</span
6656
6939
  </div>
6657
6940
 
6658
6941
  <div class="note">
6659
- <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>
6942
+ <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>
6943
+ <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>
6944
+ <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>
6660
6945
  </div>
6661
6946
 
6662
6947
  <div class="note">
@@ -7179,7 +7464,7 @@ grid.destroy();
7179
7464
  <p class="section-note">Each documented event is subscribed to and unsubscribed on every build. A consumer
7180
7465
  wiring a handler to a renamed event gets silence, which is indistinguishable from an event that
7181
7466
  has not fired yet — so the name is checked rather than left to be discovered.</p>
7182
- <pre data-run="js" data-expect="112" data-covers="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');
7467
+ <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');
7183
7468
 
7184
7469
  <span class="cmt">// Every documented event name, checked against the bus that would carry it.</span>
7185
7470
  <span class="cmt">// Subscribing to a name the grid does not know is the failure this catches:</span>
@@ -7188,6 +7473,7 @@ grid.destroy();
7188
7473
  <span class="kw">const</span> documented = [
7189
7474
  'cell:changed', 'cell:clicked', 'cell:confirmed', 'cell:conflict',
7190
7475
  'cell:contextmenu', 'cell:dblclicked', 'cell:edit:end', 'cell:edit:start',
7476
+ 'cell:mouseover', 'cell:mouseout', 'cell:mousedown', 'cell:mouseup',
7191
7477
  'cell:pending', 'cell:reverted', 'clipboard:copy', 'column:filter:open', 'column:profile:open', 'column:grouped',
7192
7478
  'column:menu:open', 'column:pivoted', 'column:resized', 'columns:changed',
7193
7479
  'columns:tagged', 'comment:added', 'comment:deleted', 'comment:edited',
@@ -7544,6 +7830,32 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
7544
7830
  </tbody>
7545
7831
  </table>
7546
7832
  </div>
7833
+ <h3 id="type-AI">AI</h3>
7834
+ <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>
7835
+ <div class="table-wrap">
7836
+ <table>
7837
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7838
+ <tbody>
7839
+ <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>
7840
+ <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>
7841
+ <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>
7842
+ <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>
7843
+ <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>
7844
+ <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>
7845
+ <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>
7846
+ <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>
7847
+ <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>
7848
+ <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>
7849
+ <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>
7850
+ <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>
7851
+ <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>
7852
+ <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>
7853
+ <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>
7854
+ <tr><td class="name">off</td><td class="type">(name: string, fn: (payload: object) =&gt; void): void</td><td class="desc"></td></tr>
7855
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc"></td></tr>
7856
+ </tbody>
7857
+ </table>
7858
+ </div>
7547
7859
  <h3 id="type-AiApi">AiApi</h3>
7548
7860
  <div class="table-wrap">
7549
7861
  <table>
@@ -7558,6 +7870,200 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
7558
7870
  </tbody>
7559
7871
  </table>
7560
7872
  </div>
7873
+ <h3 id="type-AIApplyReport">AIApplyReport</h3>
7874
+ <p class="section-note">The report from applying an ask-your-data query.</p>
7875
+ <div class="table-wrap">
7876
+ <table>
7877
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7878
+ <tbody>
7879
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc"></td></tr>
7880
+ <tr><td class="name">applied</td><td class="type">string[]</td><td class="desc">The action types that were applied.</td></tr>
7881
+ <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>
7882
+ <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>
7883
+ <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>
7884
+ </tbody>
7885
+ </table>
7886
+ </div>
7887
+ <h3 id="type-AIConfig">AIConfig</h3>
7888
+ <p class="section-note">AI module configuration.</p>
7889
+ <div class="table-wrap">
7890
+ <table>
7891
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7892
+ <tbody>
7893
+ <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>
7894
+ <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>
7895
+ <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>
7896
+ <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>
7897
+ <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>
7898
+ <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>
7899
+ <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>
7900
+ <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>
7901
+ <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>
7902
+ <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>
7903
+ <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>
7904
+ <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>
7905
+ <tr><td class="name">locale</td><td class="type">string</td><td class="desc">Locale for figure formatting. <small>(optional)</small></td></tr>
7906
+ <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>
7907
+ <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>
7908
+ <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>
7909
+ <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>
7910
+ <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>
7911
+ </tbody>
7912
+ </table>
7913
+ </div>
7914
+ <h3 id="type-AIDiffEntry">AIDiffEntry</h3>
7915
+ <p class="section-note">One before/after change in a governed-actor proposal (BACKLOG-0000967).</p>
7916
+ <div class="table-wrap">
7917
+ <table>
7918
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7919
+ <tbody>
7920
+ <tr><td class="name">key</td><td class="type">string</td><td class="desc">The target row key.</td></tr>
7921
+ <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>
7922
+ <tr><td class="name">colId</td><td class="type">string</td><td class="desc">The target column id.</td></tr>
7923
+ <tr><td class="name">colTitle</td><td class="type">string</td><td class="desc">The column's title, for the diff header.</td></tr>
7924
+ <tr><td class="name">oldValue</td><td class="type">unknown</td><td class="desc">The current stored value.</td></tr>
7925
+ <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>
7926
+ <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>
7927
+ <tr><td class="name">newDisplay</td><td class="type">string</td><td class="desc">The proposed value as shown.</td></tr>
7928
+ </tbody>
7929
+ </table>
7930
+ </div>
7931
+ <h3 id="type-AIFact">AIFact</h3>
7932
+ <p class="section-note">A single computed figure a narrative is grounded on.</p>
7933
+ <div class="table-wrap">
7934
+ <table>
7935
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7936
+ <tbody>
7937
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
7938
+ <tr><td class="name">label</td><td class="type">string</td><td class="desc"></td></tr>
7939
+ <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>
7940
+ <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>
7941
+ <tr><td class="name">kind</td><td class="type">string</td><td class="desc"></td></tr>
7942
+ <tr><td class="name">colId</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
7943
+ </tbody>
7944
+ </table>
7945
+ </div>
7946
+ <h3 id="type-AIFactsPacket">AIFactsPacket</h3>
7947
+ <p class="section-note">The facts packet a narrative grounds on.</p>
7948
+ <div class="table-wrap">
7949
+ <table>
7950
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7951
+ <tbody>
7952
+ <tr><td class="name">target</td><td class="type">AITarget</td><td class="desc"></td></tr>
7953
+ <tr><td class="name">facts</td><td class="type">AIFact[]</td><td class="desc"></td></tr>
7954
+ <tr><td class="name">groundedValues</td><td class="type">number[]</td><td class="desc">The numeric values seeding the reconciliation registry.</td></tr>
7955
+ <tr><td class="name">meta</td><td class="type">{</td><td class="desc"></td></tr>
7956
+ </tbody>
7957
+ </table>
7958
+ </div>
7959
+ <h3 id="type-AINarrative">AINarrative</h3>
7960
+ <p class="section-note">The result of a narrative: reconciled prose plus what grounded and what did not.</p>
7961
+ <div class="table-wrap">
7962
+ <table>
7963
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7964
+ <tbody>
7965
+ <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>
7966
+ <tr><td class="name">facts</td><td class="type">AIFact[]</td><td class="desc"></td></tr>
7967
+ <tr><td class="name">grounded</td><td class="type">string[]</td><td class="desc">The figures that reconciled against a computed value.</td></tr>
7968
+ <tr><td class="name">flagged</td><td class="type">string[]</td><td class="desc">The figures removed as ungrounded.</td></tr>
7969
+ <tr><td class="name">packet</td><td class="type">AIFactsPacket</td><td class="desc"></td></tr>
7970
+ <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>
7971
+ <tr><td class="name">mode</td><td class="type">'tools' | 'packet'</td><td class="desc"></td></tr>
7972
+ </tbody>
7973
+ </table>
7974
+ </div>
7975
+ <h3 id="type-AIProposal">AIProposal</h3>
7976
+ <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>
7977
+ <div class="table-wrap">
7978
+ <table>
7979
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7980
+ <tbody>
7981
+ <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>
7982
+ <tr><td class="name">instruction</td><td class="type">string</td><td class="desc">The user's instruction.</td></tr>
7983
+ <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>
7984
+ <tr><td class="name">scopeCount</td><td class="type">number</td><td class="desc">How many rows the scope covers.</td></tr>
7985
+ <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>
7986
+ <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>
7987
+ <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>
7988
+ <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>
7989
+ <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>
7990
+ <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>
7991
+ <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>
7992
+ <tr><td class="name">applied</td><td class="type">AIProposalReport | null</td><td class="desc">The apply report once applied, or null.</td></tr>
7993
+ <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>
7994
+ <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>
7995
+ </tbody>
7996
+ </table>
7997
+ </div>
7998
+ <h3 id="type-AIProposalReport">AIProposalReport</h3>
7999
+ <p class="section-note">The report from applying a governed-actor proposal.</p>
8000
+ <div class="table-wrap">
8001
+ <table>
8002
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8003
+ <tbody>
8004
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc">True when at least one edit landed.</td></tr>
8005
+ <tr><td class="name">applied</td><td class="type">number</td><td class="desc">How many edits landed through the gate.</td></tr>
8006
+ <tr><td class="name">requested</td><td class="type">number</td><td class="desc">How many edits were attempted.</td></tr>
8007
+ <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>
8008
+ <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>
8009
+ </tbody>
8010
+ </table>
8011
+ </div>
8012
+ <h3 id="type-AIQueryResult">AIQueryResult</h3>
8013
+ <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>
8014
+ <div class="table-wrap">
8015
+ <table>
8016
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8017
+ <tbody>
8018
+ <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>
8019
+ <tr><td class="name">question</td><td class="type">string</td><td class="desc">The user's question.</td></tr>
8020
+ <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>
8021
+ <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>
8022
+ <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>
8023
+ <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>
8024
+ <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>
8025
+ <tr><td class="name">spec</td><td class="type">{ actions: object[] }</td><td class="desc">The validated query spec as data.</td></tr>
8026
+ <tr><td class="name">applied</td><td class="type">AIApplyReport | null</td><td class="desc">The apply report once applied, or null.</td></tr>
8027
+ <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>
8028
+ <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>
8029
+ </tbody>
8030
+ </table>
8031
+ </div>
8032
+ <h3 id="type-AIRiskFacts">AIRiskFacts</h3>
8033
+ <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>
8034
+ <div class="table-wrap">
8035
+ <table>
8036
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8037
+ <tbody>
8038
+ <tr><td class="name">facts</td><td class="type">AIFact[]</td><td class="desc"></td></tr>
8039
+ <tr><td class="name">meta</td><td class="type">{</td><td class="desc"></td></tr>
8040
+ </tbody>
8041
+ </table>
8042
+ </div>
8043
+ <h3 id="type-AITarget">AITarget</h3>
8044
+ <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>
8045
+ <div class="table-wrap">
8046
+ <table>
8047
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8048
+ <tbody>
8049
+ <tr><td class="name">kind</td><td class="type">'view' | 'column' | 'forecast' | 'kpi' | 'chart' | 'risk'</td><td class="desc"><small>(optional)</small></td></tr>
8050
+ <tr><td class="name">colId</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
8051
+ <tr><td class="name">options</td><td class="type">object</td><td class="desc">Forecast options, for `kind: 'forecast'`. <small>(optional)</small></td></tr>
8052
+ <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>
8053
+ <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>
8054
+ <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>
8055
+ <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>
8056
+ <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>
8057
+ <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>
8058
+ <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>
8059
+ <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>
8060
+ <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>
8061
+ <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>
8062
+ <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>
8063
+ <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>
8064
+ </tbody>
8065
+ </table>
8066
+ </div>
7561
8067
  <h3 id="type-AnnotationApi">AnnotationApi</h3>
7562
8068
  <div class="table-wrap">
7563
8069
  <table>
@@ -7945,6 +8451,19 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
7945
8451
  </tbody>
7946
8452
  </table>
7947
8453
  </div>
8454
+ <h3 id="type-ChartTypeDefinition">ChartTypeDefinition</h3>
8455
+ <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>
8456
+ <div class="table-wrap">
8457
+ <table>
8458
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8459
+ <tbody>
8460
+ <tr><td class="name">draw</td><td class="type">(ctx: object) =&gt; object</td><td class="desc"></td></tr>
8461
+ <tr><td class="name">bind</td><td class="type">(grid: Grid, spec: ChartSpec) =&gt; object</td><td class="desc"><small>(optional)</small></td></tr>
8462
+ <tr><td class="name">freeform</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
8463
+ <tr><td class="name">labelled</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
8464
+ </tbody>
8465
+ </table>
8466
+ </div>
7948
8467
  <h3 id="type-Chunk">Chunk</h3>
7949
8468
  <div class="table-wrap">
7950
8469
  <table>
@@ -7999,6 +8518,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
7999
8518
  <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>
8000
8519
  <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>
8001
8520
  <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>
8521
+ <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>
8002
8522
  <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>
8003
8523
  <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>
8004
8524
  <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>
@@ -8021,7 +8541,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
8021
8541
  <tr><td class="name">class</td><td class="type">string | string[] | ((p: CellParams) =&gt; string | string[])</td><td class="desc"><small>(optional)</small></td></tr>
8022
8542
  <tr><td class="name">classWhen</td><td class="type">Record&lt;string, string | ((p: CellParams) =&gt; boolean)&gt;</td><td class="desc"><small>(optional)</small></td></tr>
8023
8543
  <tr><td class="name">style</td><td class="type">CellStyle | ((p: CellParams) =&gt; CellStyle)</td><td class="desc"><small>(optional)</small></td></tr>
8024
- <tr><td class="name">tooltip</td><td class="type">string | ((p: CellParams) =&gt; string)</td><td class="desc"><small>(optional)</small></td></tr>
8544
+ <tr><td class="name">tooltip</td><td class="type">string | ((p: CellParams) =&gt; string) | ColumnTooltipSpec</td><td class="desc">A tooltip for this column's cells. A string or a function is the plain-text case and becomes the browser's own `title`. An object is a {@link ColumnTooltipSpec}: a tooltip the grid draws, which can carry structure, markup or live content and which a keyboard user can reach (BACKLOG-0001204). <small>(optional)</small></td></tr>
8025
8545
  <tr><td class="name">align</td><td class="type">Align</td><td class="desc"><small>(optional)</small></td></tr>
8026
8546
  <tr><td class="name">verticalAlign</td><td class="type">VAlign</td><td class="desc">Vertical alignment of this column's cell content, overriding the grid-level `verticalAlign` for this column alone (BACKLOG-0000989). Accepted at the top level of the column too, as `align` is. <small>(optional)</small></td></tr>
8027
8547
  <tr><td class="name">wrap</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
@@ -8172,6 +8692,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
8172
8692
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8173
8693
  <tbody>
8174
8694
  <tr><td class="name">width</td><td class="type">number | string</td><td class="desc">A pixel width, or a percentage of the grid's inner width as a string, `'25%'`. A percentage is a share of the *whole* grid. `flex` divides only the space left over after fixed columns, so the two are not interchangeable: `flex: 25` on four columns is a quarter of the remainder, which is a quarter of the grid only when nothing else is fixed. <small>(optional)</small></td></tr>
8695
+ <tr><td class="name">fit</td><td class="type">'content'</td><td class="desc">`'content'` sizes the column to what it is actually showing, the way `columns.autoSize()` does, and keeps doing it: on the first paint, and again whenever the rows change, the columns are shown, hidden, reordered or pinned, or the grid is resized. It is the declarative form of the imperative call, so a host no longer has to re-issue `autoSize()` after every data change. Sized to the *visible* content, not to the widest value in the dataset: the measurement reads the rows the renderer has mounted, because measuring a million rows is not a plan. It measures the heading too, so a column whose title is longer than its values widens to show the title. **Anything the caller states outranks it.** A declared `width` wins, and so does a width the user drags to — a resize is recorded as a `width`, so from that moment the column is that wide and the fit no longer touches it. `min` and `max` clamp the fitted width as they clamp any other. `flex` is resolved before this and wins, the two being contradictory instructions: `flex` fits the column to the *grid*, this fits it to the *content*. Not re-measured on scroll, deliberately: different rows mount as the grid scrolls, and re-fitting against them would make the columns jitter under the reader. <small>(optional)</small></td></tr>
8175
8696
  <tr><td class="name">min</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
8176
8697
  <tr><td class="name">max</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
8177
8698
  <tr><td class="name">flex</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
@@ -8289,6 +8810,18 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
8289
8810
  </tbody>
8290
8811
  </table>
8291
8812
  </div>
8813
+ <h3 id="type-ColumnTooltipSpec">ColumnTooltipSpec</h3>
8814
+ <p class="section-note">A rich, keyboard-accessible tooltip for a column's cells (BACKLOG-0001204) — the object form of `cell.tooltip`, drawn by the grid rather than handed to the browser as a native `title`. Shown after a delay (`tooltip.delay`, 400ms by default) on hover *and* on keyboard focus; the cell points at it with `aria-describedby`; it can be hovered without closing and Escape dismisses it (WCAG 2.2 AA, 1.4.13). It closes on scroll, because rows are pooled and a bubble left open would be anchored to a node that is now showing a different row.</p>
8815
+ <div class="table-wrap">
8816
+ <table>
8817
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
8818
+ <tbody>
8819
+ <tr><td class="name">render</td><td class="type">(params: TooltipParams) =&gt; HTMLElement | TooltipSpec | { html: string } | string | null | undefined</td><td class="desc">Produce the content. Four shapes, and the difference between the last two is a security property rather than a style choice: - an **element** — your own DOM, attached as it is; - a **{@link TooltipSpec}** — `{ title, rows, note }`, rendered as text; - **`{ html }`** — the only wrapper that inserts markup, scrubbed of script the same way `allowUnsafeTemplates` output is; - a **string** — *always* text, never markup. The last rule is what makes `render: (p) =&gt; p.value` safe: a value comes from row data, and data must not be able to promote itself to HTML. <small>(optional)</small></td></tr>
8820
+ <tr><td class="name">mount</td><td class="type">(el: HTMLElement, params: TooltipParams) =&gt; void</td><td class="desc">Put live content in the tooltip — a sparkline, a KPI tile — by calling into a module bundle your application loaded. The grid core never imports a module, so anything live is mounted here by you. <small>(optional)</small></td></tr>
8821
+ <tr><td class="name">unmount</td><td class="type">(el: HTMLElement) =&gt; void</td><td class="desc">Tear down whatever `mount` built. Called every time the tooltip closes, so nothing keeps running behind a hidden box. <small>(optional)</small></td></tr>
8822
+ </tbody>
8823
+ </table>
8824
+ </div>
8292
8825
  <h3 id="type-ColumnValidation">ColumnValidation</h3>
8293
8826
  <p class="section-note">Declarative edit-validation rules for a column (BACKLOG-0000956). Rules are checked in a fixed order — `required` first, then the value-shape rules, then the functions — and the first failure wins. A blank but optional value passes everything after `required`: an empty cell is empty, not "below the minimum". A failure vetoes the commit through `beforeEdit` and marks the cell; the cancellation carries `reason: 'validation:&lt;code&gt;'`.</p>
8294
8827
  <div class="table-wrap">
@@ -8524,6 +9057,24 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
8524
9057
  </tbody>
8525
9058
  </table>
8526
9059
  </div>
9060
+ <h3 id="type-DataRouter">DataRouter</h3>
9061
+ <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>
9062
+ <div class="table-wrap">
9063
+ <table>
9064
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9065
+ <tbody>
9066
+ <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>
9067
+ <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>
9068
+ <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>
9069
+ <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>
9070
+ <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>
9071
+ <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>
9072
+ <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>
9073
+ <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>
9074
+ <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>
9075
+ </tbody>
9076
+ </table>
9077
+ </div>
8527
9078
  <h3 id="type-DatasetColumnDifference">DatasetColumnDifference</h3>
8528
9079
  <div class="table-wrap">
8529
9080
  <table>
@@ -9024,6 +9575,18 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9024
9575
  </tbody>
9025
9576
  </table>
9026
9577
  </div>
9578
+ <h3 id="type-FeedMessage">FeedMessage</h3>
9579
+ <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>
9580
+ <div class="table-wrap">
9581
+ <table>
9582
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9583
+ <tbody>
9584
+ <tr><td class="name">kind</td><td class="type">'snapshot' | 'delta'</td><td class="desc"></td></tr>
9585
+ <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>
9586
+ <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>
9587
+ </tbody>
9588
+ </table>
9589
+ </div>
9027
9590
  <h3 id="type-Filter">Filter</h3>
9028
9591
  <div class="table-wrap">
9029
9592
  <table>
@@ -9279,58 +9842,325 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9279
9842
  </tbody>
9280
9843
  </table>
9281
9844
  </div>
9282
- <h3 id="type-Grid">Grid</h3>
9845
+ <h3 id="type-Gantt">Gantt</h3>
9846
+ <p class="section-note">A headless Gantt controller: holds the model, recomputes on edits, emits changes.</p>
9847
+ <div class="table-wrap">
9848
+ <table>
9849
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9850
+ <tbody>
9851
+ <tr><td class="name">tasks</td><td class="type">GanttTask[]</td><td class="desc"><small>(read-only)</small></td></tr>
9852
+ <tr><td class="name">dependencies</td><td class="type">GanttDependency[]</td><td class="desc"><small>(read-only)</small></td></tr>
9853
+ <tr><td class="name">schedule</td><td class="type">GanttSchedule | null</td><td class="desc"><small>(read-only)</small></td></tr>
9854
+ <tr><td class="name">critical</td><td class="type">string[]</td><td class="desc"><small>(read-only)</small></td></tr>
9855
+ <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>
9856
+ <tr><td class="name">autoSchedule</td><td class="type">boolean</td><td class="desc"><small>(read-only)</small></td></tr>
9857
+ <tr><td class="name">grid</td><td class="type">unknown</td><td class="desc"><small>(read-only)</small></td></tr>
9858
+ <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>
9859
+ <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>
9860
+ <tr><td class="name">setTasks</td><td class="type">(tasks: GanttTask[]): GanttSchedule</td><td class="desc"></td></tr>
9861
+ <tr><td class="name">setDependencies</td><td class="type">(deps: GanttDependency[]): GanttSchedule</td><td class="desc"></td></tr>
9862
+ <tr><td class="name">applyEdit</td><td class="type">(patch: { id: string | number; start?: number; end?: number; duration?: number }, editOpts?: { writeBack?: boolean }): GanttSchedule</td><td class="desc"></td></tr>
9863
+ <tr><td class="name">compute</td><td class="type">(): GanttSchedule</td><td class="desc"></td></tr>
9864
+ <tr><td class="name">findViolations</td><td class="type">(): GanttViolation[]</td><td class="desc"></td></tr>
9865
+ <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>
9866
+ <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>
9867
+ <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>
9868
+ <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>
9869
+ <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>
9870
+ <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>
9871
+ <tr><td class="name">off</td><td class="type">(event: 'schedule' | 'error', fn: (payload: unknown) =&gt; void): void</td><td class="desc"></td></tr>
9872
+ <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>
9873
+ <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 (Task Name tree with expand/collapse, assignee avatars, a circular % ring, 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.</td></tr>
9874
+ <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>
9875
+ <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>
9876
+ <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>
9877
+ <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>
9878
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc"></td></tr>
9879
+ </tbody>
9880
+ </table>
9881
+ </div>
9882
+ <h3 id="type-GanttConflict">GanttConflict</h3>
9883
+ <p class="section-note">An unhonourable scheduling constraint, reported rather than obeyed.</p>
9283
9884
  <div class="table-wrap">
9284
9885
  <table>
9285
9886
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9286
9887
  <tbody>
9287
- <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>
9288
- <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>
9289
- <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>
9290
- <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>
9291
- <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>
9292
- <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>
9293
- <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>
9294
- <tr><td class="name">export</td><td class="type">ExportApi</td><td class="desc">CSV, Excel and clipboard. <small>(read-only)</small></td></tr>
9295
- <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>
9296
- <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>
9297
- <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>
9298
- <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>
9299
- <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>
9300
- <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>
9301
- <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>
9302
- <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>
9303
- <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>
9304
- <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>
9305
- <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>
9306
- <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>
9307
- <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>
9308
- <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>
9309
- <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>
9310
- <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>
9311
- <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>
9312
- <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>
9313
- <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>
9314
- <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>
9315
- <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>
9316
- <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>
9317
- <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>
9318
- <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>
9319
- <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>
9320
- <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>
9321
- <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>
9322
- <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>
9323
- <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>
9324
- <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>
9325
- <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>
9326
- <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>
9327
- <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>
9328
- <tr><td class="name">config</td><td class="type">(): GridConfig</td><td class="desc">The resolved configuration, as one object.</td></tr>
9329
- <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>
9330
- <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>
9331
- <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>
9332
- <tr><td class="name">off</td><td class="type">(event: EventName, handler: EventHandler): void</td><td class="desc">Stop listening.</td></tr>
9333
- <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>
9888
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
9889
+ <tr><td class="name">type</td><td class="type">string</td><td class="desc"></td></tr>
9890
+ <tr><td class="name">at</td><td class="type">number | null</td><td class="desc"></td></tr>
9891
+ <tr><td class="name">earliestFeasible</td><td class="type">number</td><td class="desc"></td></tr>
9892
+ </tbody>
9893
+ </table>
9894
+ </div>
9895
+ <h3 id="type-GanttDependency">GanttDependency</h3>
9896
+ <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>
9897
+ <div class="table-wrap">
9898
+ <table>
9899
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9900
+ <tbody>
9901
+ <tr><td class="name">from</td><td class="type">string | number</td><td class="desc"></td></tr>
9902
+ <tr><td class="name">to</td><td class="type">string | number</td><td class="desc"></td></tr>
9903
+ <tr><td class="name">type</td><td class="type">GanttLinkType | `${GanttLinkType}${'+' | '-'}${number}`</td><td class="desc"><small>(optional)</small></td></tr>
9904
+ <tr><td class="name">lag</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
9905
+ </tbody>
9906
+ </table>
9907
+ </div>
9908
+ <h3 id="type-GanttEarnedValue">GanttEarnedValue</h3>
9909
+ <p class="section-note">The earned-value result at a status date (BACKLOG-0000958).</p>
9910
+ <div class="table-wrap">
9911
+ <table>
9912
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9913
+ <tbody>
9914
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc"></td></tr>
9915
+ <tr><td class="name">error</td><td class="type">{ code: string; message: string }</td><td class="desc"><small>(optional)</small></td></tr>
9916
+ <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>
9917
+ <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>
9918
+ <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>
9919
+ <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>
9920
+ </tbody>
9921
+ </table>
9922
+ </div>
9923
+ <h3 id="type-GanttEarnedValueRow">GanttEarnedValueRow</h3>
9924
+ <p class="section-note">Earned-value metrics for one task or the whole project (BACKLOG-0000958).</p>
9925
+ <div class="table-wrap">
9926
+ <table>
9927
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9928
+ <tbody>
9929
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
9930
+ <tr><td class="name">name</td><td class="type">string</td><td class="desc"></td></tr>
9931
+ <tr><td class="name">isSummary</td><td class="type">boolean</td><td class="desc"></td></tr>
9932
+ <tr><td class="name">isMilestone</td><td class="type">boolean</td><td class="desc"></td></tr>
9933
+ <tr><td class="name">percentComplete</td><td class="type">number | null</td><td class="desc"></td></tr>
9934
+ <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>
9935
+ <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>
9936
+ <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>
9937
+ <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>
9938
+ <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>
9939
+ <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>
9940
+ <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>
9941
+ <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>
9942
+ <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>
9943
+ <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>
9944
+ </tbody>
9945
+ </table>
9946
+ </div>
9947
+ <h3 id="type-GanttLevelResult">GanttLevelResult</h3>
9948
+ <p class="section-note">The result of resource leveling: the shifted tasks and what moved (BACKLOG-0000948).</p>
9949
+ <div class="table-wrap">
9950
+ <table>
9951
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9952
+ <tbody>
9953
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc"></td></tr>
9954
+ <tr><td class="name">resolved</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
9955
+ <tr><td class="name">tasks</td><td class="type">GanttTask[]</td><td class="desc"><small>(optional)</small></td></tr>
9956
+ <tr><td class="name">schedule</td><td class="type">GanttSchedule</td><td class="desc"><small>(optional)</small></td></tr>
9957
+ <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>
9958
+ <tr><td class="name">remaining</td><td class="type">GanttOverAllocation[]</td><td class="desc"><small>(optional)</small></td></tr>
9959
+ <tr><td class="name">error</td><td class="type">{ code: string; message: string }</td><td class="desc"><small>(optional)</small></td></tr>
9960
+ </tbody>
9961
+ </table>
9962
+ </div>
9963
+ <h3 id="type-GanttMSPDIModel">GanttMSPDIModel</h3>
9964
+ <p class="section-note">The model {@link importMSPDI} returns and {@link exportMSPDI} takes.</p>
9965
+ <div class="table-wrap">
9966
+ <table>
9967
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9968
+ <tbody>
9969
+ <tr><td class="name">tasks</td><td class="type">GanttTask[]</td><td class="desc"></td></tr>
9970
+ <tr><td class="name">dependencies</td><td class="type">GanttDependency[]</td><td class="desc"><small>(optional)</small></td></tr>
9971
+ <tr><td class="name">resources</td><td class="type">GanttResourceSpec</td><td class="desc"><small>(optional)</small></td></tr>
9972
+ <tr><td class="name">projectStart</td><td class="type">number | string | Date</td><td class="desc"><small>(optional)</small></td></tr>
9973
+ <tr><td class="name">calendar</td><td class="type">GanttCalendar | null</td><td class="desc"><small>(optional)</small></td></tr>
9974
+ <tr><td class="name">schedule</td><td class="type">GanttSchedule</td><td class="desc"><small>(optional)</small></td></tr>
9975
+ </tbody>
9976
+ </table>
9977
+ </div>
9978
+ <h3 id="type-GanttOverAllocation">GanttOverAllocation</h3>
9979
+ <p class="section-note">A resource booked beyond its capacity across concurrent tasks (BACKLOG-0000948).</p>
9980
+ <div class="table-wrap">
9981
+ <table>
9982
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9983
+ <tbody>
9984
+ <tr><td class="name">resource</td><td class="type">string</td><td class="desc"></td></tr>
9985
+ <tr><td class="name">capacity</td><td class="type">number</td><td class="desc"></td></tr>
9986
+ <tr><td class="name">start</td><td class="type">number</td><td class="desc"></td></tr>
9987
+ <tr><td class="name">end</td><td class="type">number</td><td class="desc"></td></tr>
9988
+ <tr><td class="name">load</td><td class="type">number</td><td class="desc"></td></tr>
9989
+ <tr><td class="name">taskIds</td><td class="type">string[]</td><td class="desc"></td></tr>
9990
+ </tbody>
9991
+ </table>
9992
+ </div>
9993
+ <h3 id="type-GanttResourceLoad">GanttResourceLoad</h3>
9994
+ <p class="section-note">The per-resource load and the over-allocations across a schedule (BACKLOG-0000948).</p>
9995
+ <div class="table-wrap">
9996
+ <table>
9997
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9998
+ <tbody>
9999
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc"></td></tr>
10000
+ <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>
10001
+ <tr><td class="name">overAllocations</td><td class="type">GanttOverAllocation[]</td><td class="desc"></td></tr>
10002
+ <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>
10003
+ </tbody>
10004
+ </table>
10005
+ </div>
10006
+ <h3 id="type-GanttResourceSegment">GanttResourceSegment</h3>
10007
+ <p class="section-note">One contiguous load segment for a resource: how many units are booked over a span.</p>
10008
+ <div class="table-wrap">
10009
+ <table>
10010
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10011
+ <tbody>
10012
+ <tr><td class="name">start</td><td class="type">number</td><td class="desc"></td></tr>
10013
+ <tr><td class="name">end</td><td class="type">number</td><td class="desc"></td></tr>
10014
+ <tr><td class="name">load</td><td class="type">number</td><td class="desc"></td></tr>
10015
+ <tr><td class="name">taskIds</td><td class="type">string[]</td><td class="desc"></td></tr>
10016
+ </tbody>
10017
+ </table>
10018
+ </div>
10019
+ <h3 id="type-GanttSchedule">GanttSchedule</h3>
10020
+ <p class="section-note">A CPM schedule result: per-task dates/float and the critical path, or an error.</p>
10021
+ <div class="table-wrap">
10022
+ <table>
10023
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10024
+ <tbody>
10025
+ <tr><td class="name">ok</td><td class="type">boolean</td><td class="desc"></td></tr>
10026
+ <tr><td class="name">error</td><td class="type">{ code: string; message: string; cycle?: string[] }</td><td class="desc"><small>(optional)</small></td></tr>
10027
+ <tr><td class="name">tasks</td><td class="type">Map&lt;string, GanttScheduledTask&gt;</td><td class="desc"><small>(optional)</small></td></tr>
10028
+ <tr><td class="name">order</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
10029
+ <tr><td class="name">critical</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
10030
+ <tr><td class="name">criticalPaths</td><td class="type">string[][]</td><td class="desc"><small>(optional)</small></td></tr>
10031
+ <tr><td class="name">projectStart</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10032
+ <tr><td class="name">projectFinish</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10033
+ <tr><td class="name">projectDuration</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10034
+ <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>
10035
+ <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>
10036
+ <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>
10037
+ <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>
10038
+ </tbody>
10039
+ </table>
10040
+ </div>
10041
+ <h3 id="type-GanttScheduledTask">GanttScheduledTask</h3>
10042
+ <p class="section-note">The computed CPM values for one task (a leaf is scheduled, a summary derived).</p>
10043
+ <div class="table-wrap">
10044
+ <table>
10045
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10046
+ <tbody>
10047
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
10048
+ <tr><td class="name">name</td><td class="type">string</td><td class="desc"></td></tr>
10049
+ <tr><td class="name">duration</td><td class="type">number</td><td class="desc"></td></tr>
10050
+ <tr><td class="name">es</td><td class="type">number</td><td class="desc"></td></tr>
10051
+ <tr><td class="name">ef</td><td class="type">number</td><td class="desc"></td></tr>
10052
+ <tr><td class="name">ls</td><td class="type">number</td><td class="desc"></td></tr>
10053
+ <tr><td class="name">lf</td><td class="type">number</td><td class="desc"></td></tr>
10054
+ <tr><td class="name">totalFloat</td><td class="type">number</td><td class="desc"></td></tr>
10055
+ <tr><td class="name">critical</td><td class="type">boolean</td><td class="desc"></td></tr>
10056
+ <tr><td class="name">percentComplete</td><td class="type">number | null</td><td class="desc"></td></tr>
10057
+ <tr><td class="name">parent</td><td class="type">string | null</td><td class="desc"></td></tr>
10058
+ <tr><td class="name">isSummary</td><td class="type">boolean</td><td class="desc"></td></tr>
10059
+ <tr><td class="name">isMilestone</td><td class="type">boolean</td><td class="desc"></td></tr>
10060
+ <tr><td class="name">children</td><td class="type">string[]</td><td class="desc"></td></tr>
10061
+ <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>
10062
+ <tr><td class="name">baselineEnd</td><td class="type">number | null</td><td class="desc"><small>(optional)</small></td></tr>
10063
+ <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>
10064
+ <tr><td class="name">finishVariance</td><td class="type">number | null</td><td class="desc"><small>(optional)</small></td></tr>
10065
+ <tr><td class="name">durationVariance</td><td class="type">number | null</td><td class="desc"><small>(optional)</small></td></tr>
10066
+ </tbody>
10067
+ </table>
10068
+ </div>
10069
+ <h3 id="type-GanttTask">GanttTask</h3>
10070
+ <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>
10071
+ <div class="table-wrap">
10072
+ <table>
10073
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10074
+ <tbody>
10075
+ <tr><td class="name">id</td><td class="type">string | number</td><td class="desc"></td></tr>
10076
+ <tr><td class="name">name</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10077
+ <tr><td class="name">start</td><td class="type">number | string | Date</td><td class="desc"><small>(optional)</small></td></tr>
10078
+ <tr><td class="name">end</td><td class="type">number | string | Date</td><td class="desc"><small>(optional)</small></td></tr>
10079
+ <tr><td class="name">duration</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10080
+ <tr><td class="name">percentComplete</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10081
+ <tr><td class="name">milestone</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
10082
+ <tr><td class="name">parent</td><td class="type">string | number</td><td class="desc"><small>(optional)</small></td></tr>
10083
+ <tr><td class="name">baselineStart</td><td class="type">number | string | Date</td><td class="desc"><small>(optional)</small></td></tr>
10084
+ <tr><td class="name">baselineEnd</td><td class="type">number | string | Date</td><td class="desc"><small>(optional)</small></td></tr>
10085
+ <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>
10086
+ <tr><td class="name">constraint</td><td class="type">GanttConstraintType</td><td class="desc"><small>(optional)</small></td></tr>
10087
+ <tr><td class="name">constraintDate</td><td class="type">number | string | Date</td><td class="desc"><small>(optional)</small></td></tr>
10088
+ <tr><td class="name">assignee</td><td class="type">string | string[]</td><td class="desc"><small>(optional)</small></td></tr>
10089
+ <tr><td class="name">assignees</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
10090
+ <tr><td class="name">owner</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10091
+ <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>
10092
+ <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>
10093
+ <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>
10094
+ <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>
10095
+ <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>
10096
+ </tbody>
10097
+ </table>
10098
+ </div>
10099
+ <h3 id="type-GanttViolation">GanttViolation</h3>
10100
+ <p class="section-note">A placement violation flagged by `findViolations`.</p>
10101
+ <div class="table-wrap">
10102
+ <table>
10103
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10104
+ <tbody>
10105
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
10106
+ <tr><td class="name">placedStart</td><td class="type">number</td><td class="desc"></td></tr>
10107
+ <tr><td class="name">earliestStart</td><td class="type">number</td><td class="desc"></td></tr>
10108
+ <tr><td class="name">by</td><td class="type">number</td><td class="desc"></td></tr>
10109
+ </tbody>
10110
+ </table>
10111
+ </div>
10112
+ <h3 id="type-Grid">Grid</h3>
10113
+ <div class="table-wrap">
10114
+ <table>
10115
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10116
+ <tbody>
10117
+ <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>
10118
+ <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>
10119
+ <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>
10120
+ <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>
10121
+ <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>
10122
+ <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>
10123
+ <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>
10124
+ <tr><td class="name">export</td><td class="type">ExportApi</td><td class="desc">CSV, Excel and clipboard. <small>(read-only)</small></td></tr>
10125
+ <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>
10126
+ <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>
10127
+ <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>
10128
+ <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>
10129
+ <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>
10130
+ <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>
10131
+ <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>
10132
+ <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>
10133
+ <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>
10134
+ <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>
10135
+ <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>
10136
+ <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>
10137
+ <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>
10138
+ <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>
10139
+ <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>
10140
+ <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>
10141
+ <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>
10142
+ <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>
10143
+ <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>
10144
+ <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>
10145
+ <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>
10146
+ <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>
10147
+ <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>
10148
+ <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>
10149
+ <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>
10150
+ <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>
10151
+ <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>
10152
+ <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>
10153
+ <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>
10154
+ <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>
10155
+ <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>
10156
+ <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>
10157
+ <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>
10158
+ <tr><td class="name">config</td><td class="type">(): GridConfig</td><td class="desc">The resolved configuration, as one object.</td></tr>
10159
+ <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>
10160
+ <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>
10161
+ <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>
10162
+ <tr><td class="name">off</td><td class="type">(event: EventName, handler: EventHandler): void</td><td class="desc">Stop listening.</td></tr>
10163
+ <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>
9334
10164
  <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>
9335
10165
  <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>
9336
10166
  <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>
@@ -9373,6 +10203,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9373
10203
  <tr><td class="name">cornerRadius</td><td class="type">boolean | number | string</td><td class="desc">Round the grid's outer corners. Square by default. `true` adopts the theme's own radius; a number is pixels; a string is used as written, so a host can pass its own token or a relative unit. <small>(optional)</small></td></tr>
9374
10204
  <tr><td class="name">stripedRows</td><td class="type">boolean</td><td class="desc">Shade alternate data rows (zebra striping). Off by default, and strictly opt-in: an existing grid must look exactly the same on upgrade. When `true`, every other data row takes the theme's `--lattice-surface-alt` background, which every palette already defines, so dark, high-contrast and terminal stripe correctly without extra work. Parity follows the row's *logical* index, not its position in the DOM, so a row keeps its stripe across a scroll even though the rows are recycled. Structural rows — group headings, group footers and the grand total — are never striped, and both selection and hover still win over the stripe. <small>(optional)</small></td></tr>
9375
10205
  <tr><td class="name">verticalAlign</td><td class="type">VAlign</td><td class="desc">Vertical alignment of cell content within a row, as a default for every column (BACKLOG-0000989). `top`, `middle` or `bottom`; a column's own `verticalAlign` overrides it for that column. The horizontal counterpart is the per-column `align`. Omitted, the grid keeps its historical placement — content centred in a fixed-height row and top-aligned in an `autoHeight` row — so an existing grid is unchanged on upgrade. Setting a value aligns every column uniformly, including `autoHeight` rows, unless a column opts out. <small>(optional)</small></td></tr>
10206
+ <tr><td class="name">tooltip</td><td class="type">TooltipConfig</td><td class="desc">Defaults for the rich cell tooltip (BACKLOG-0001204). The tooltip itself is declared per column, on `cell.tooltip`; this only carries the settings that are a house style rather than a per-column decision. It switches nothing on: a column with no `cell.tooltip` has no tooltip whatever is set here. <small>(optional)</small></td></tr>
9376
10207
  <tr><td class="name">scrollbars</td><td class="type">ScrollbarMode | { x?: ScrollbarMode; y?: ScrollbarMode }</td><td class="desc">Keep the scroll viewport's scrollbars visible (BACKLOG-0000990). `'auto'` (the default) is the platform's native behaviour, where overlay scrollbars fade when idle. `'always'` keeps both axes shown whether or not the pointer is over the grid. The object form controls each axis on its own — `{ y: 'always' }` pins the vertical bar while the horizontal one stays native. Omitted, the grid is unchanged on upgrade. <small>(optional)</small></td></tr>
9377
10208
  <tr><td class="name">columnTagFilter</td><td class="type">boolean | { multiple?: boolean; label?: string }</td><td class="desc">Show a bar above the column headings for filtering columns by tag. Off by default, and it draws nothing unless some column carries a `tags` entry. `multiple: true` lets more than one tag be chosen at once. Only tagged columns are ever hidden, so an untagged account or total column stays visible whatever is selected. <small>(optional)</small></td></tr>
9378
10209
  <tr><td class="name">anomalySummary</td><td class="type">boolean | { column?: string; label?: string }</td><td class="desc">Show a small chip in the grid chrome that reads how many rows an anomaly shadow column has flagged, and filters the grid to exactly those when it is clicked (BACKLOG-0000799). Off by default, and it draws nothing unless a column declares a `shadow: { kind: 'anomalyFlag' }`. The count and the filter both read that one shadow column, so the number on the chip is the number of rows the click reveals. `column` names the base column to summarise when more than one anomaly-flag shadow is present; `label` overrides the chip's wording. <small>(optional)</small></td></tr>
@@ -9409,7 +10240,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9409
10240
  <tr><td class="name">workerUrl</td><td class="type">string</td><td class="desc">Where to load the worker kernel from, when hosting it yourself. <small>(optional)</small></td></tr>
9410
10241
  <tr><td class="name">sharedMemory</td><td class="type">boolean</td><td class="desc">Use a shared buffer for the worker, where the page's headers allow it. <small>(optional)</small></td></tr>
9411
10242
  <tr><td class="name">groupFooter</td><td class="type">boolean</td><td class="desc">A totals line at the foot of each group as well as the grid. <small>(optional)</small></td></tr>
9412
- <tr><td class="name">groupRenderer</td><td class="type">(params: GroupRowParams): string | Node | void</td><td class="desc">Draw the group row yourself. The grid's own group row is an expander, a label and a count. A host that needs more — a section header with a points rollup, a done/total count and a progress bar — supplies this instead, and owns the whole row: it is drawn as one band across every column, and no ordinary cells are mounted for it. Return an HTML string, or a node, or write into `params.element` and return nothing. Unlike `fullWidth.render`, a string here **is** inserted as markup, on the same footing as the board's `cardRenderer`: this is your own template for a row the grid synthesised, not a value out of your data. The chevron is yours to draw and yours to wire: give any element in your markup `data-lat-group-toggle` and a click on it expands or collapses the group, or call `params.toggle()` from a node you built yourself. <small>(optional)</small></td></tr>
10243
+ <tr><td class="name">groupRenderer</td><td class="type">(params: GroupRowParams) =&gt; string | Node | void</td><td class="desc">Draw the group row yourself. The grid's own group row is an expander, a label and a count. A host that needs more — a section header with a points rollup, a done/total count and a progress bar — supplies this instead, and owns the whole row: it is drawn as one band across every column, and no ordinary cells are mounted for it. Return an HTML string, or a node, or write into `params.element` and return nothing. Unlike `fullWidth.render`, a string here **is** inserted as markup, on the same footing as the board's `cardRenderer`: this is your own template for a row the grid synthesised, not a value out of your data. The chevron is yours to draw and yours to wire: give any element in your markup `data-lat-group-toggle` and a click on it expands or collapses the group, or call `params.toggle()` from a node you built yourself. <small>(optional)</small></td></tr>
9413
10244
  <tr><td class="name">groupDefaultExpanded</td><td class="type">boolean | number | ((group: GroupInfo) =&gt; boolean)</td><td class="desc">Which groups start expanded, before anyone has opened or closed one. `true` (the default) opens every group, `false` closes every group, a number opens the first N levels (`0` closes everything, a negative opens every level), and a predicate answers per group — the current sprint's section open while the rest start closed. Only ever consulted for a group nobody has touched: once the user or your code expands or collapses one, that decision stands. <small>(optional)</small></td></tr>
9414
10245
  <tr><td class="name">grandTotalRow</td><td class="type">boolean | 'bottom'</td><td class="desc">Where the grand total goes. `true` adds it as the last display row, counted by `rows.count()` like any other. `'bottom'` pins it beneath the viewport instead, so it stays in view while the rows scroll and is *not* part of `rows.count()`. Omitted or `false` means no grand total row. <small>(optional)</small></td></tr>
9415
10246
  <tr><td class="name">pinnedTopRows</td><td class="type">unknown[]</td><td class="desc">Rows pinned above the scrolling body. The objects are rendered through the ordinary column pipeline but are not part of the data: not counted by `rows.count()`, not sorted, filtered, grouped, selectable or exported. Use it for a totals line or a units row that must stay against the header. <small>(optional)</small></td></tr>
@@ -9762,6 +10593,714 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9762
10593
  </tbody>
9763
10594
  </table>
9764
10595
  </div>
10596
+ <h3 id="type-Kanban">Kanban</h3>
10597
+ <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>
10598
+ <div class="table-wrap">
10599
+ <table>
10600
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10601
+ <tbody>
10602
+ <tr><td class="name">el</td><td class="type">unknown | null</td><td class="desc"><small>(read-only)</small></td></tr>
10603
+ <tr><td class="name">rowKey</td><td class="type">string | ((row: KanbanRow) =&gt; unknown)</td><td class="desc"><small>(read-only)</small></td></tr>
10604
+ <tr><td class="name">rows</td><td class="type">KanbanRows</td><td class="desc"></td></tr>
10605
+ <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>
10606
+ <tr><td class="name">columns</td><td class="type">(): KanbanColumn[]</td><td class="desc"></td></tr>
10607
+ <tr><td class="name">column</td><td class="type">(id: string): KanbanColumn | undefined</td><td class="desc"></td></tr>
10608
+ <tr><td class="name">count</td><td class="type">(id: string): number</td><td class="desc"></td></tr>
10609
+ <tr><td class="name">points</td><td class="type">(id: string): number</td><td class="desc"></td></tr>
10610
+ <tr><td class="name">cards</td><td class="type">(): KanbanCard[]</td><td class="desc"></td></tr>
10611
+ <tr><td class="name">card</td><td class="type">(key: unknown): KanbanCard | undefined</td><td class="desc"></td></tr>
10612
+ <tr><td class="name">on</td><td class="type">(name: string, fn: (event: KanbanEvent) =&gt; void): () =&gt; void</td><td class="desc"></td></tr>
10613
+ <tr><td class="name">off</td><td class="type">(name: string, fn: (event: KanbanEvent) =&gt; void): void</td><td class="desc"></td></tr>
10614
+ <tr><td class="name">readonly</td><td class="type">(scope?: { column?: string; card?: unknown }): boolean</td><td class="desc"></td></tr>
10615
+ <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>
10616
+ <tr><td class="name">selection</td><td class="type">(): unknown[]</td><td class="desc">The selected card keys.</td></tr>
10617
+ <tr><td class="name">isSelected</td><td class="type">(key: unknown): boolean</td><td class="desc">Whether a card is selected.</td></tr>
10618
+ <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>
10619
+ <tr><td class="name">clearSelection</td><td class="type">(): Kanban</td><td class="desc">Clear the selection.</td></tr>
10620
+ <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>
10621
+ <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>
10622
+ <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>
10623
+ <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>
10624
+ <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>
10625
+ <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>
10626
+ <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>
10627
+ <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>
10628
+ <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>
10629
+ <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>
10630
+ <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>
10631
+ <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>
10632
+ <tr><td class="name">showBacklog</td><td class="type">(): Kanban</td><td class="desc">Show only the backlog (cards with no sprint).</td></tr>
10633
+ <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>
10634
+ <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>
10635
+ <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>
10636
+ <tr><td class="name">epics</td><td class="type">(): unknown[]</td><td class="desc">The distinct epic values.</td></tr>
10637
+ <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>
10638
+ <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>
10639
+ <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>
10640
+ <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>
10641
+ <tr><td class="name">closeDetail</td><td class="type">(): Kanban</td><td class="desc">Close any open card pop-out.</td></tr>
10642
+ <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>
10643
+ <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>
10644
+ <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>
10645
+ <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>
10646
+ <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>
10647
+ <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>
10648
+ <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>
10649
+ <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>
10650
+ <tr><td class="name">setRows</td><td class="type">(rows: KanbanRow[]): Kanban</td><td class="desc"></td></tr>
10651
+ <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>
10652
+ <tr><td class="name">refresh</td><td class="type">(): Kanban</td><td class="desc"></td></tr>
10653
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc"></td></tr>
10654
+ </tbody>
10655
+ </table>
10656
+ </div>
10657
+ <h3 id="type-KanbanCard">KanbanCard</h3>
10658
+ <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>
10659
+ <div class="table-wrap">
10660
+ <table>
10661
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10662
+ <tbody>
10663
+ <tr><td class="name">key</td><td class="type">unknown</td><td class="desc"></td></tr>
10664
+ <tr><td class="name">row</td><td class="type">KanbanRow</td><td class="desc"></td></tr>
10665
+ <tr><td class="name">columnId</td><td class="type">string | null</td><td class="desc"></td></tr>
10666
+ <tr><td class="name">points</td><td class="type">number</td><td class="desc"></td></tr>
10667
+ <tr><td class="name">hasPoints</td><td class="type">boolean</td><td class="desc"></td></tr>
10668
+ <tr><td class="name">order</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10669
+ <tr><td class="name">swimlane</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10670
+ <tr><td class="name">sprint</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10671
+ <tr><td class="name">epic</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10672
+ <tr><td class="name">fields</td><td class="type">Record&lt;string, string&gt;</td><td class="desc"></td></tr>
10673
+ </tbody>
10674
+ </table>
10675
+ </div>
10676
+ <h3 id="type-KanbanCardMap">KanbanCardMap</h3>
10677
+ <p class="section-note">The field-to-property mapping that drives the card template.</p>
10678
+ <div class="table-wrap">
10679
+ <table>
10680
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10681
+ <tbody>
10682
+ <tr><td class="name">title</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10683
+ <tr><td class="name">subtitle</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10684
+ <tr><td class="name">labels</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10685
+ <tr><td class="name">assignee</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10686
+ <tr><td class="name">due</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10687
+ <tr><td class="name">cover</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10688
+ <tr><td class="name">progress</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10689
+ <tr><td class="name">badges</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10690
+ <tr><td class="name">accent</td><td class="type">KanbanFieldMap</td><td class="desc"><small>(optional)</small></td></tr>
10691
+ </tbody>
10692
+ </table>
10693
+ </div>
10694
+ <h3 id="type-KanbanChildren">KanbanChildren</h3>
10695
+ <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>
10696
+ <div class="table-wrap">
10697
+ <table>
10698
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10699
+ <tbody>
10700
+ <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>
10701
+ <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>
10702
+ <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>
10703
+ <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>
10704
+ <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>
10705
+ <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>
10706
+ <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>
10707
+ <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>
10708
+ <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>
10709
+ </tbody>
10710
+ </table>
10711
+ </div>
10712
+ <h3 id="type-KanbanColumn">KanbanColumn</h3>
10713
+ <p class="section-note">A column with its cards and aggregates. `over` is true when `count` exceeds `wipLimit`.</p>
10714
+ <div class="table-wrap">
10715
+ <table>
10716
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10717
+ <tbody>
10718
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
10719
+ <tr><td class="name">title</td><td class="type">string</td><td class="desc"></td></tr>
10720
+ <tr><td class="name">color</td><td class="type">string | null</td><td class="desc"></td></tr>
10721
+ <tr><td class="name">wipLimit</td><td class="type">number | null</td><td class="desc"></td></tr>
10722
+ <tr><td class="name">collapsed</td><td class="type">boolean</td><td class="desc"></td></tr>
10723
+ <tr><td class="name">cards</td><td class="type">KanbanCard[]</td><td class="desc"></td></tr>
10724
+ <tr><td class="name">count</td><td class="type">number</td><td class="desc"></td></tr>
10725
+ <tr><td class="name">points</td><td class="type">number</td><td class="desc"></td></tr>
10726
+ <tr><td class="name">over</td><td class="type">boolean</td><td class="desc"></td></tr>
10727
+ </tbody>
10728
+ </table>
10729
+ </div>
10730
+ <h3 id="type-KanbanConfig">KanbanConfig</h3>
10731
+ <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>
10732
+ <div class="table-wrap">
10733
+ <table>
10734
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10735
+ <tbody>
10736
+ <tr><td class="name">rows</td><td class="type">KanbanRow[]</td><td class="desc"><small>(optional)</small></td></tr>
10737
+ <tr><td class="name">grid</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10738
+ <tr><td class="name">rowKey</td><td class="type">string | ((row: KanbanRow) =&gt; unknown)</td><td class="desc"><small>(optional)</small></td></tr>
10739
+ <tr><td class="name">columnProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10740
+ <tr><td class="name">columns</td><td class="type">KanbanColumnDef[]</td><td class="desc"><small>(optional)</small></td></tr>
10741
+ <tr><td class="name">columnOrder</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
10742
+ <tr><td class="name">pointsProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10743
+ <tr><td class="name">showPoints</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
10744
+ <tr><td class="name">orderProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10745
+ <tr><td class="name">swimlaneProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10746
+ <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>
10747
+ <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>
10748
+ <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>
10749
+ <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>
10750
+ <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>
10751
+ <tr><td class="name">sprintProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10752
+ <tr><td class="name">epicProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10753
+ <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>
10754
+ <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>
10755
+ <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>
10756
+ <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>
10757
+ <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>
10758
+ <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>
10759
+ <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>
10760
+ <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>
10761
+ <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>
10762
+ <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>
10763
+ <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>
10764
+ <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>
10765
+ <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>
10766
+ <tr><td class="name">card</td><td class="type">KanbanCardMap</td><td class="desc"><small>(optional)</small></td></tr>
10767
+ <tr><td class="name">readonly</td><td class="type">KanbanReadonly</td><td class="desc"><small>(optional)</small></td></tr>
10768
+ <tr><td class="name">ariaLabel</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10769
+ <tr><td class="name">emptyText</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10770
+ <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>
10771
+ <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>
10772
+ <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>
10773
+ <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>
10774
+ <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>
10775
+ <tr><td class="name">onCardClick</td><td class="type">(event: KanbanEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10776
+ <tr><td class="name">onCardDblClick</td><td class="type">(event: KanbanEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10777
+ <tr><td class="name">onCardContextMenu</td><td class="type">(event: KanbanEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10778
+ </tbody>
10779
+ </table>
10780
+ </div>
10781
+ <h3 id="type-KanbanEditor">KanbanEditor</h3>
10782
+ <p class="section-note">A card field editor handle returned by a host editor factory.</p>
10783
+ <div class="table-wrap">
10784
+ <table>
10785
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10786
+ <tbody>
10787
+ <tr><td class="name">el</td><td class="type">HTMLElement</td><td class="desc"></td></tr>
10788
+ <tr><td class="name">focus</td><td class="type">() =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10789
+ <tr><td class="name">destroy</td><td class="type">() =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10790
+ </tbody>
10791
+ </table>
10792
+ </div>
10793
+ <h3 id="type-KanbanEvent">KanbanEvent</h3>
10794
+ <p class="section-note">The payload every board event carries.</p>
10795
+ <div class="table-wrap">
10796
+ <table>
10797
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10798
+ <tbody>
10799
+ <tr><td class="name">card</td><td class="type">KanbanCard</td><td class="desc"></td></tr>
10800
+ <tr><td class="name">column</td><td class="type">string | null</td><td class="desc"></td></tr>
10801
+ <tr><td class="name">el</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10802
+ <tr><td class="name">originalEvent</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10803
+ </tbody>
10804
+ </table>
10805
+ </div>
10806
+ <h3 id="type-KanbanFilters">KanbanFilters</h3>
10807
+ <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>
10808
+ <div class="table-wrap">
10809
+ <table>
10810
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10811
+ <tbody>
10812
+ <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>
10813
+ <tr><td class="name">where</td><td class="type">(): string[]</td><td class="desc">The registered names, in registration order.</td></tr>
10814
+ <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>
10815
+ <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>
10816
+ <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>
10817
+ </tbody>
10818
+ </table>
10819
+ </div>
10820
+ <h3 id="type-KanbanMenuItem">KanbanMenuItem</h3>
10821
+ <p class="section-note">One context-menu item. `action` receives the card, the selected cards, and the board.</p>
10822
+ <div class="table-wrap">
10823
+ <table>
10824
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10825
+ <tbody>
10826
+ <tr><td class="name">label</td><td class="type">string</td><td class="desc"></td></tr>
10827
+ <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>
10828
+ <tr><td class="name">disabled</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
10829
+ </tbody>
10830
+ </table>
10831
+ </div>
10832
+ <h3 id="type-KanbanMoveEvent">KanbanMoveEvent</h3>
10833
+ <p class="section-note">The payload of a `card:move` (and `card:reverted`) event.</p>
10834
+ <div class="table-wrap">
10835
+ <table>
10836
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10837
+ <tbody>
10838
+ <tr><td class="name">keys</td><td class="type">unknown[]</td><td class="desc"></td></tr>
10839
+ <tr><td class="name">cards</td><td class="type">KanbanCard[]</td><td class="desc"></td></tr>
10840
+ <tr><td class="name">from</td><td class="type">(string | null)[]</td><td class="desc"></td></tr>
10841
+ <tr><td class="name">to</td><td class="type">string</td><td class="desc"></td></tr>
10842
+ <tr><td class="name">index</td><td class="type">number | null</td><td class="desc"></td></tr>
10843
+ <tr><td class="name">orders</td><td class="type">number[] | null</td><td class="desc"></td></tr>
10844
+ </tbody>
10845
+ </table>
10846
+ </div>
10847
+ <h3 id="type-KanbanRows">KanbanRows</h3>
10848
+ <p class="section-note">The keyed-diff consumer surface a board shares with a grid, so a Data Router routes to it directly.</p>
10849
+ <div class="table-wrap">
10850
+ <table>
10851
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10852
+ <tbody>
10853
+ <tr><td class="name">apply</td><td class="type">(change: { add?: KanbanRow[]; update?: KanbanRow[]; remove?: unknown[] }): void</td><td class="desc"></td></tr>
10854
+ <tr><td class="name">forEach</td><td class="type">(fn: (row: KanbanRow, key: unknown) =&gt; void): void</td><td class="desc"></td></tr>
10855
+ <tr><td class="name">count</td><td class="type">number</td><td class="desc"><small>(read-only)</small></td></tr>
10856
+ </tbody>
10857
+ </table>
10858
+ </div>
10859
+ <h3 id="type-KanbanSla">KanbanSla</h3>
10860
+ <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>
10861
+ <div class="table-wrap">
10862
+ <table>
10863
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10864
+ <tbody>
10865
+ <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>
10866
+ <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>
10867
+ <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>
10868
+ <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>
10869
+ <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>
10870
+ <tr><td class="name">states</td><td class="type">(): KanbanSlaState[]</td><td class="desc">Every card's current SLA state.</td></tr>
10871
+ <tr><td class="name">breaches</td><td class="type">(): KanbanSlaState[]</td><td class="desc">The cards currently at breach level.</td></tr>
10872
+ <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>
10873
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc">Stop the tick and drop the board subscriptions.</td></tr>
10874
+ </tbody>
10875
+ </table>
10876
+ </div>
10877
+ <h3 id="type-KanbanSlaConfig">KanbanSlaConfig</h3>
10878
+ <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>
10879
+ <div class="table-wrap">
10880
+ <table>
10881
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10882
+ <tbody>
10883
+ <tr><td class="name">warn</td><td class="type">KanbanSlaThreshold</td><td class="desc">The global warn threshold. <small>(optional)</small></td></tr>
10884
+ <tr><td class="name">breach</td><td class="type">KanbanSlaThreshold</td><td class="desc">The global breach threshold. <small>(optional)</small></td></tr>
10885
+ <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>
10886
+ <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>
10887
+ <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>
10888
+ <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>
10889
+ <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>
10890
+ <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>
10891
+ <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>
10892
+ <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>
10893
+ <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>
10894
+ <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>
10895
+ <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>
10896
+ <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>
10897
+ </tbody>
10898
+ </table>
10899
+ </div>
10900
+ <h3 id="type-KanbanSlaState">KanbanSlaState</h3>
10901
+ <p class="section-note">The computed SLA state of one card (BACKLOG-0000960).</p>
10902
+ <div class="table-wrap">
10903
+ <table>
10904
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10905
+ <tbody>
10906
+ <tr><td class="name">key</td><td class="type">unknown</td><td class="desc"></td></tr>
10907
+ <tr><td class="name">columnId</td><td class="type">string | null</td><td class="desc"></td></tr>
10908
+ <tr><td class="name">lane</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10909
+ <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>
10910
+ <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>
10911
+ <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>
10912
+ <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>
10913
+ <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>
10914
+ <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>
10915
+ <tr><td class="name">breached</td><td class="type">boolean</td><td class="desc">True when `level` is `'breach'`.</td></tr>
10916
+ </tbody>
10917
+ </table>
10918
+ </div>
10919
+ <h3 id="type-KPI">KPI</h3>
10920
+ <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>
10921
+ <div class="table-wrap">
10922
+ <table>
10923
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10924
+ <tbody>
10925
+ <tr><td class="name">el</td><td class="type">unknown | null</td><td class="desc"><small>(read-only)</small></td></tr>
10926
+ <tr><td class="name">rowKey</td><td class="type">string | ((row: KPIRow) =&gt; unknown)</td><td class="desc"><small>(read-only)</small></td></tr>
10927
+ <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>
10928
+ <tr><td class="name">rows</td><td class="type">KPIRows</td><td class="desc"></td></tr>
10929
+ <tr><td class="name">tiles</td><td class="type">(): KPITileModel[]</td><td class="desc"></td></tr>
10930
+ <tr><td class="name">tile</td><td class="type">(id: string): KPITileModel | undefined</td><td class="desc"></td></tr>
10931
+ <tr><td class="name">value</td><td class="type">(id: string): unknown</td><td class="desc"></td></tr>
10932
+ <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>
10933
+ <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>
10934
+ <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>
10935
+ <tr><td class="name">expand</td><td class="type">(key: string): KPI</td><td class="desc"></td></tr>
10936
+ <tr><td class="name">collapse</td><td class="type">(key: string): KPI</td><td class="desc"></td></tr>
10937
+ <tr><td class="name">toggle</td><td class="type">(key: string): KPI</td><td class="desc"></td></tr>
10938
+ <tr><td class="name">setRows</td><td class="type">(rows: KPIRow[]): KPI</td><td class="desc"></td></tr>
10939
+ <tr><td class="name">refresh</td><td class="type">(): KPI</td><td class="desc"></td></tr>
10940
+ <tr><td class="name">getState</td><td class="type">(): object</td><td class="desc"></td></tr>
10941
+ <tr><td class="name">setState</td><td class="type">(snapshot: object): KPI</td><td class="desc"></td></tr>
10942
+ <tr><td class="name">on</td><td class="type">(name: string, fn: (event: KPIEvent) =&gt; void): () =&gt; void</td><td class="desc"></td></tr>
10943
+ <tr><td class="name">off</td><td class="type">(name: string, fn: (event: KPIEvent) =&gt; void): void</td><td class="desc"></td></tr>
10944
+ <tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc"></td></tr>
10945
+ </tbody>
10946
+ </table>
10947
+ </div>
10948
+ <h3 id="type-KPIBand">KPIBand</h3>
10949
+ <p class="section-note">An explicit band: the `status` of the first band whose half-open `[min, max)` contains the value.</p>
10950
+ <div class="table-wrap">
10951
+ <table>
10952
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10953
+ <tbody>
10954
+ <tr><td class="name">min</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10955
+ <tr><td class="name">max</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10956
+ <tr><td class="name">status</td><td class="type">'good' | 'warn' | 'critical'</td><td class="desc"></td></tr>
10957
+ </tbody>
10958
+ </table>
10959
+ </div>
10960
+ <h3 id="type-KPIConfig">KPIConfig</h3>
10961
+ <p class="section-note">KPI panel configuration.</p>
10962
+ <div class="table-wrap">
10963
+ <table>
10964
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10965
+ <tbody>
10966
+ <tr><td class="name">rows</td><td class="type">KPIRow[]</td><td class="desc"><small>(optional)</small></td></tr>
10967
+ <tr><td class="name">grid</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10968
+ <tr><td class="name">rowKey</td><td class="type">string | ((row: KPIRow) =&gt; unknown)</td><td class="desc"><small>(optional)</small></td></tr>
10969
+ <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>
10970
+ <tr><td class="name">tiles</td><td class="type">KPITile[]</td><td class="desc"><small>(optional)</small></td></tr>
10971
+ <tr><td class="name">columns</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
10972
+ <tr><td class="name">ariaLabel</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10973
+ <tr><td class="name">nullText</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
10974
+ <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>
10975
+ <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>
10976
+ <tr><td class="name">onTileClick</td><td class="type">(event: KPIEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10977
+ <tr><td class="name">onTileDblClick</td><td class="type">(event: KPIEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10978
+ <tr><td class="name">onTileContextMenu</td><td class="type">(event: KPIEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
10979
+ <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>
10980
+ <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>
10981
+ </tbody>
10982
+ </table>
10983
+ </div>
10984
+ <h3 id="type-KPIEvent">KPIEvent</h3>
10985
+ <p class="section-note">The payload every tile event carries.</p>
10986
+ <div class="table-wrap">
10987
+ <table>
10988
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10989
+ <tbody>
10990
+ <tr><td class="name">tile</td><td class="type">KPITileModel</td><td class="desc"></td></tr>
10991
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
10992
+ <tr><td class="name">originalEvent</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
10993
+ </tbody>
10994
+ </table>
10995
+ </div>
10996
+ <h3 id="type-KPINodeModel">KPINodeModel</h3>
10997
+ <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>
10998
+ <div class="table-wrap">
10999
+ <table>
11000
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11001
+ <tbody>
11002
+ <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>
11003
+ <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>
11004
+ <tr><td class="name">label</td><td class="type">string</td><td class="desc"></td></tr>
11005
+ <tr><td class="name">level</td><td class="type">number</td><td class="desc">Depth, 0 at the top level.</td></tr>
11006
+ <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>
11007
+ <tr><td class="name">setsize</td><td class="type">number</td><td class="desc"></td></tr>
11008
+ <tr><td class="name">hasChildren</td><td class="type">boolean</td><td class="desc"></td></tr>
11009
+ <tr><td class="name">expanded</td><td class="type">boolean</td><td class="desc"></td></tr>
11010
+ <tr><td class="name">children</td><td class="type">KPINodeModel[]</td><td class="desc"></td></tr>
11011
+ <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>
11012
+ <tr><td class="name">value</td><td class="type">unknown</td><td class="desc"></td></tr>
11013
+ <tr><td class="name">formatted</td><td class="type">string | null</td><td class="desc"></td></tr>
11014
+ <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>
11015
+ <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>
11016
+ <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>
11017
+ <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>
11018
+ </tbody>
11019
+ </table>
11020
+ </div>
11021
+ <h3 id="type-KPIRows">KPIRows</h3>
11022
+ <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>
11023
+ <div class="table-wrap">
11024
+ <table>
11025
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11026
+ <tbody>
11027
+ <tr><td class="name">apply</td><td class="type">(change: { add?: KPIRow[]; update?: KPIRow[]; remove?: unknown[] }): void</td><td class="desc"></td></tr>
11028
+ <tr><td class="name">forEach</td><td class="type">(fn: (row: KPIRow, key: unknown) =&gt; void): void</td><td class="desc"></td></tr>
11029
+ <tr><td class="name">count</td><td class="type">number</td><td class="desc"><small>(read-only)</small></td></tr>
11030
+ </tbody>
11031
+ </table>
11032
+ </div>
11033
+ <h3 id="type-KPISparkline">KPISparkline</h3>
11034
+ <p class="section-note">An optional sparkline series: the `y` field plotted in order of the `x` field (or insertion).</p>
11035
+ <div class="table-wrap">
11036
+ <table>
11037
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11038
+ <tbody>
11039
+ <tr><td class="name">x</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
11040
+ <tr><td class="name">y</td><td class="type">string | ((row: KPIRow) =&gt; unknown)</td><td class="desc"></td></tr>
11041
+ </tbody>
11042
+ </table>
11043
+ </div>
11044
+ <h3 id="type-KPIThresholds">KPIThresholds</h3>
11045
+ <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>
11046
+ <div class="table-wrap">
11047
+ <table>
11048
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11049
+ <tbody>
11050
+ <tr><td class="name">warn</td><td class="type">number</td><td class="desc"></td></tr>
11051
+ <tr><td class="name">critical</td><td class="type">number</td><td class="desc"></td></tr>
11052
+ <tr><td class="name">direction</td><td class="type">'higherIsBetter' | 'lowerIsBetter'</td><td class="desc"><small>(optional)</small></td></tr>
11053
+ </tbody>
11054
+ </table>
11055
+ </div>
11056
+ <h3 id="type-KPITile">KPITile</h3>
11057
+ <p class="section-note">One tile: an aggregate over the routed rows, with optional filter, format, threshold and trend.</p>
11058
+ <div class="table-wrap">
11059
+ <table>
11060
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11061
+ <tbody>
11062
+ <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>
11063
+ <tr><td class="name">label</td><td class="type">string</td><td class="desc">The tile's accessible label. <small>(optional)</small></td></tr>
11064
+ <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>
11065
+ <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>
11066
+ <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>
11067
+ <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>
11068
+ <tr><td class="name">format</td><td class="type">KPIFormat</td><td class="desc">Value formatting. <small>(optional)</small></td></tr>
11069
+ <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>
11070
+ <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>
11071
+ <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>
11072
+ <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>
11073
+ <tr><td class="name">sparkline</td><td class="type">KPISparkline | string</td><td class="desc">A trend sparkline series. <small>(optional)</small></td></tr>
11074
+ </tbody>
11075
+ </table>
11076
+ </div>
11077
+ <h3 id="type-KPITileModel">KPITileModel</h3>
11078
+ <p class="section-note">A computed tile, as it appears in the model.</p>
11079
+ <div class="table-wrap">
11080
+ <table>
11081
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11082
+ <tbody>
11083
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
11084
+ <tr><td class="name">label</td><td class="type">string</td><td class="desc"></td></tr>
11085
+ <tr><td class="name">aggregation</td><td class="type">string</td><td class="desc"></td></tr>
11086
+ <tr><td class="name">field</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
11087
+ <tr><td class="name">value</td><td class="type">unknown</td><td class="desc"></td></tr>
11088
+ <tr><td class="name">formatted</td><td class="type">string</td><td class="desc"></td></tr>
11089
+ <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>
11090
+ <tr><td class="name">target</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
11091
+ <tr><td class="name">baseline</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
11092
+ <tr><td class="name">delta</td><td class="type">number | null</td><td class="desc"></td></tr>
11093
+ <tr><td class="name">deltaPercent</td><td class="type">number | null</td><td class="desc"></td></tr>
11094
+ <tr><td class="name">deltaFormatted</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
11095
+ <tr><td class="name">count</td><td class="type">number</td><td class="desc"></td></tr>
11096
+ <tr><td class="name">sparkline</td><td class="type">number[] | null</td><td class="desc"></td></tr>
11097
+ </tbody>
11098
+ </table>
11099
+ </div>
11100
+ <h3 id="type-KPITreeConfig">KPITreeConfig</h3>
11101
+ <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>
11102
+ <div class="table-wrap">
11103
+ <table>
11104
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11105
+ <tbody>
11106
+ <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>
11107
+ <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>
11108
+ <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>
11109
+ <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>
11110
+ <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>
11111
+ </tbody>
11112
+ </table>
11113
+ </div>
11114
+ <h3 id="type-Layout">Layout</h3>
11115
+ <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>
11116
+ <div class="table-wrap">
11117
+ <table>
11118
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11119
+ <tbody>
11120
+ <tr><td class="name">el</td><td class="type">HTMLElement</td><td class="desc"><small>(read-only)</small></td></tr>
11121
+ <tr><td class="name">windows</td><td class="type">(): string[]</td><td class="desc">The window ids, in mount order.</td></tr>
11122
+ <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>
11123
+ <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>
11124
+ <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>
11125
+ <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>
11126
+ <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>
11127
+ <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>
11128
+ <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>
11129
+ <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>
11130
+ <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>
11131
+ <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>
11132
+ <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>
11133
+ <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>
11134
+ <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>
11135
+ <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>
11136
+ <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>
11137
+ <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>
11138
+ <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>
11139
+ <tr><td class="name">on</td><td class="type">(</td><td class="desc"></td></tr>
11140
+ <tr><td class="name">off</td><td class="type">(name: string, fn: (event: any) =&gt; unknown): void</td><td class="desc"></td></tr>
11141
+ <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>
11142
+ </tbody>
11143
+ </table>
11144
+ </div>
11145
+ <h3 id="type-LayoutChangedEvent">LayoutChangedEvent</h3>
11146
+ <p class="section-note">The payload of `layout:changed`: the whole arrangement, plus what moved it.</p>
11147
+ <div class="table-wrap">
11148
+ <table>
11149
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11150
+ <tbody>
11151
+ <tr><td class="name">cause</td><td class="type">string</td><td class="desc"></td></tr>
11152
+ </tbody>
11153
+ </table>
11154
+ </div>
11155
+ <h3 id="type-LayoutCloseEvent">LayoutCloseEvent</h3>
11156
+ <p class="section-note">The payload of `window:closed` and `beforeWindowClose`.</p>
11157
+ <div class="table-wrap">
11158
+ <table>
11159
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11160
+ <tbody>
11161
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
11162
+ <tr><td class="name">payloadId</td><td class="type">string</td><td class="desc"></td></tr>
11163
+ <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>
11164
+ <tr><td class="name">origin</td><td class="type">'api' | 'user'</td><td class="desc"><small>(optional)</small></td></tr>
11165
+ <tr><td class="name">reason</td><td class="type">string | null</td><td class="desc"><small>(optional)</small></td></tr>
11166
+ <tr><td class="name">preventDefault</td><td class="type">(reason?: string) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11167
+ <tr><td class="name">defaultPrevented</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
11168
+ </tbody>
11169
+ </table>
11170
+ </div>
11171
+ <h3 id="type-LayoutConfig">LayoutConfig</h3>
11172
+ <p class="section-note">Dashboard layout configuration.</p>
11173
+ <div class="table-wrap">
11174
+ <table>
11175
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11176
+ <tbody>
11177
+ <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>
11178
+ <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>
11179
+ <tr><td class="name">overflowX</td><td class="type">'static' | 'scroll'</td><td class="desc">Horizontal overflow (default `'static'`). <small>(optional)</small></td></tr>
11180
+ <tr><td class="name">overflowY</td><td class="type">'static' | 'scroll'</td><td class="desc">Vertical overflow (default `'static'`). <small>(optional)</small></td></tr>
11181
+ <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>
11182
+ <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>
11183
+ <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>
11184
+ <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>
11185
+ <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>
11186
+ <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>
11187
+ <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>
11188
+ <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>
11189
+ <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>
11190
+ <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>
11191
+ <tr><td class="name">windows</td><td class="type">LayoutWindow[]</td><td class="desc">The windows, in mount order. <small>(optional)</small></td></tr>
11192
+ <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>
11193
+ <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>
11194
+ <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>
11195
+ <tr><td class="name">onWindowMoved</td><td class="type">(event: LayoutMoveEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11196
+ <tr><td class="name">onWindowResized</td><td class="type">(event: LayoutResizeEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11197
+ <tr><td class="name">onWindowClosed</td><td class="type">(event: LayoutCloseEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11198
+ <tr><td class="name">onLayoutChanged</td><td class="type">(event: LayoutChangedEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11199
+ <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>
11200
+ <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>
11201
+ <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>
11202
+ <tr><td class="name">onWindowMoveCancelled</td><td class="type">(event: LayoutMoveEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11203
+ <tr><td class="name">onWindowResizeCancelled</td><td class="type">(event: LayoutMoveEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11204
+ <tr><td class="name">onWindowCloseCancelled</td><td class="type">(event: LayoutCloseEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
11205
+ </tbody>
11206
+ </table>
11207
+ </div>
11208
+ <h3 id="type-LayoutInteractive">LayoutInteractive</h3>
11209
+ <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>
11210
+ <div class="table-wrap">
11211
+ <table>
11212
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11213
+ <tbody>
11214
+ <tr><td class="name">movable</td><td class="type">boolean | undefined</td><td class="desc"></td></tr>
11215
+ <tr><td class="name">resizable</td><td class="type">boolean | undefined</td><td class="desc"></td></tr>
11216
+ <tr><td class="name">closable</td><td class="type">boolean | undefined</td><td class="desc"></td></tr>
11217
+ </tbody>
11218
+ </table>
11219
+ </div>
11220
+ <h3 id="type-LayoutMoveEvent">LayoutMoveEvent</h3>
11221
+ <p class="section-note">The payload of `window:moved`, `beforeWindowMove`, `beforeWindowResize`.</p>
11222
+ <div class="table-wrap">
11223
+ <table>
11224
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11225
+ <tbody>
11226
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
11227
+ <tr><td class="name">from</td><td class="type">LayoutPlacement</td><td class="desc"></td></tr>
11228
+ <tr><td class="name">to</td><td class="type">LayoutPlacement</td><td class="desc">Where the window was asked to go.</td></tr>
11229
+ <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>
11230
+ <tr><td class="name">origin</td><td class="type">'api' | 'user' | 'init'</td><td class="desc"><small>(optional)</small></td></tr>
11231
+ <tr><td class="name">reason</td><td class="type">string | null</td><td class="desc"><small>(optional)</small></td></tr>
11232
+ <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>
11233
+ <tr><td class="name">defaultPrevented</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
11234
+ </tbody>
11235
+ </table>
11236
+ </div>
11237
+ <h3 id="type-LayoutPlacement">LayoutPlacement</h3>
11238
+ <p class="section-note">A cell placement, as carried on the move and resize events.</p>
11239
+ <div class="table-wrap">
11240
+ <table>
11241
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11242
+ <tbody>
11243
+ <tr><td class="name">xPos</td><td class="type">number</td><td class="desc"></td></tr>
11244
+ <tr><td class="name">yPos</td><td class="type">number</td><td class="desc"></td></tr>
11245
+ <tr><td class="name">xSize</td><td class="type">number</td><td class="desc"></td></tr>
11246
+ <tr><td class="name">ySize</td><td class="type">number</td><td class="desc"></td></tr>
11247
+ </tbody>
11248
+ </table>
11249
+ </div>
11250
+ <h3 id="type-LayoutResizeEvent">LayoutResizeEvent</h3>
11251
+ <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>
11252
+ <div class="table-wrap">
11253
+ <table>
11254
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11255
+ <tbody>
11256
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
11257
+ <tr><td class="name">payloadId</td><td class="type">string</td><td class="desc"></td></tr>
11258
+ <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>
11259
+ <tr><td class="name">width</td><td class="type">number</td><td class="desc"></td></tr>
11260
+ <tr><td class="name">height</td><td class="type">number</td><td class="desc"></td></tr>
11261
+ <tr><td class="name">xPos</td><td class="type">number</td><td class="desc"></td></tr>
11262
+ <tr><td class="name">yPos</td><td class="type">number</td><td class="desc"></td></tr>
11263
+ <tr><td class="name">xSize</td><td class="type">number</td><td class="desc"></td></tr>
11264
+ <tr><td class="name">ySize</td><td class="type">number</td><td class="desc"></td></tr>
11265
+ </tbody>
11266
+ </table>
11267
+ </div>
11268
+ <h3 id="type-LayoutSnapshot">LayoutSnapshot</h3>
11269
+ <p class="section-note">The plain, JSON-safe arrangement `getLayout()` returns and `setLayout()` takes.</p>
11270
+ <div class="table-wrap">
11271
+ <table>
11272
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11273
+ <tbody>
11274
+ <tr><td class="name">columns</td><td class="type">number</td><td class="desc"></td></tr>
11275
+ <tr><td class="name">rows</td><td class="type">number</td><td class="desc"></td></tr>
11276
+ <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>
11277
+ </tbody>
11278
+ </table>
11279
+ </div>
11280
+ <h3 id="type-LayoutWindow">LayoutWindow</h3>
11281
+ <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>
11282
+ <div class="table-wrap">
11283
+ <table>
11284
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
11285
+ <tbody>
11286
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc">A stable, unique id. Required.</td></tr>
11287
+ <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>
11288
+ <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>
11289
+ <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>
11290
+ <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>
11291
+ <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>
11292
+ <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>
11293
+ <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>
11294
+ <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>
11295
+ <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>
11296
+ <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>
11297
+ <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>
11298
+ <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>
11299
+ <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>
11300
+ <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>
11301
+ </tbody>
11302
+ </table>
11303
+ </div>
9765
11304
  <h3 id="type-LicenceApi">LicenceApi</h3>
9766
11305
  <div class="table-wrap">
9767
11306
  <table>
@@ -10277,9 +11816,9 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
10277
11816
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
10278
11817
  <tbody>
10279
11818
  <tr><td class="name">pushed</td><td class="type">RemoteRequest</td><td class="desc">The query the adapter was given.</td></tr>
10280
- <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>
11819
+ <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>
10281
11820
  <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>
10282
- <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>
11821
+ <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>
10283
11822
  <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>
10284
11823
  <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>
10285
11824
  </tbody>
@@ -10296,6 +11835,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
10296
11835
  <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>
10297
11836
  <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>
10298
11837
  <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>
11838
+ <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>
10299
11839
  </tbody>
10300
11840
  </table>
10301
11841
  </div>
@@ -10469,6 +12009,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
10469
12009
  <tr><td class="name">sort</td><td class="type">SortEntry[]</td><td class="desc"></td></tr>
10470
12010
  <tr><td class="name">context</td><td class="type">unknown</td><td class="desc"></td></tr>
10471
12011
  <tr><td class="name">signal</td><td class="type">AbortSignal</td><td class="desc"></td></tr>
12012
+ <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>
10472
12013
  </tbody>
10473
12014
  </table>
10474
12015
  </div>
@@ -10546,6 +12087,19 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
10546
12087
  </tbody>
10547
12088
  </table>
10548
12089
  </div>
12090
+ <h3 id="type-RouteOptions">RouteOptions</h3>
12091
+ <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>
12092
+ <div class="table-wrap">
12093
+ <table>
12094
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12095
+ <tbody>
12096
+ <tr><td class="name">rowKey</td><td class="type">(string | ((row: RouterRecord) =&gt; unknown))</td><td class="desc"><small>(optional)</small></td></tr>
12097
+ <tr><td class="name">transform</td><td class="type">(row: RouterRecord) =&gt; RouterRecord</td><td class="desc"><small>(optional)</small></td></tr>
12098
+ <tr><td class="name">filter</td><td class="type">(row: RouterRecord) =&gt; boolean</td><td class="desc"><small>(optional)</small></td></tr>
12099
+ <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>
12100
+ </tbody>
12101
+ </table>
12102
+ </div>
10549
12103
  <h3 id="type-Row">Row</h3>
10550
12104
  <div class="table-wrap">
10551
12105
  <table>
@@ -10960,6 +12514,87 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
10960
12514
  </tbody>
10961
12515
  </table>
10962
12516
  </div>
12517
+ <h3 id="type-TabChangeEvent">TabChangeEvent</h3>
12518
+ <p class="section-note">The payload every tab-change event carries.</p>
12519
+ <div class="table-wrap">
12520
+ <table>
12521
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12522
+ <tbody>
12523
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
12524
+ <tr><td class="name">previousId</td><td class="type">string | null</td><td class="desc"></td></tr>
12525
+ <tr><td class="name">origin</td><td class="type">'api' | 'user' | 'init'</td><td class="desc"><small>(optional)</small></td></tr>
12526
+ <tr><td class="name">reason</td><td class="type">string | null</td><td class="desc"><small>(optional)</small></td></tr>
12527
+ <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>
12528
+ <tr><td class="name">defaultPrevented</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
12529
+ </tbody>
12530
+ </table>
12531
+ </div>
12532
+ <h3 id="type-TabDescriptor">TabDescriptor</h3>
12533
+ <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>
12534
+ <div class="table-wrap">
12535
+ <table>
12536
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12537
+ <tbody>
12538
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc">A stable, unique id. Required.</td></tr>
12539
+ <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>
12540
+ <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>
12541
+ <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>
12542
+ <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>
12543
+ <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>
12544
+ <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>
12545
+ <tr><td class="name">groupBy</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
12546
+ <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>
12547
+ <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>
12548
+ <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>
12549
+ <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>
12550
+ <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>
12551
+ <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>
12552
+ <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>
12553
+ <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>
12554
+ <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>
12555
+ <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>
12556
+ <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>
12557
+ <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>
12558
+ <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>
12559
+ </tbody>
12560
+ </table>
12561
+ </div>
12562
+ <h3 id="type-Tabs">Tabs</h3>
12563
+ <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>
12564
+ <div class="table-wrap">
12565
+ <table>
12566
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12567
+ <tbody>
12568
+ <tr><td class="name">el</td><td class="type">HTMLElement</td><td class="desc"><small>(read-only)</small></td></tr>
12569
+ <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>
12570
+ <tr><td class="name">tabs</td><td class="type">(): string[]</td><td class="desc">The configured tab ids, in order.</td></tr>
12571
+ <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>
12572
+ <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>
12573
+ <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>
12574
+ <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>
12575
+ <tr><td class="name">off</td><td class="type">(name: string, fn: (event: TabChangeEvent) =&gt; void): void</td><td class="desc"></td></tr>
12576
+ <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>
12577
+ </tbody>
12578
+ </table>
12579
+ </div>
12580
+ <h3 id="type-TabsConfig">TabsConfig</h3>
12581
+ <p class="section-note">Tabbed-grid configuration.</p>
12582
+ <div class="table-wrap">
12583
+ <table>
12584
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12585
+ <tbody>
12586
+ <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>
12587
+ <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>
12588
+ <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>
12589
+ <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>
12590
+ <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>
12591
+ <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>
12592
+ <tr><td class="name">onTabChange</td><td class="type">(event: TabChangeEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
12593
+ <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>
12594
+ <tr><td class="name">onTabChangeCancelled</td><td class="type">(event: TabChangeEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
12595
+ </tbody>
12596
+ </table>
12597
+ </div>
10963
12598
  <h3 id="type-TextFormat">TextFormat</h3>
10964
12599
  <div class="table-wrap">
10965
12600
  <table>
@@ -10993,6 +12628,58 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
10993
12628
  </tbody>
10994
12629
  </table>
10995
12630
  </div>
12631
+ <h3 id="type-TooltipConfig">TooltipConfig</h3>
12632
+ <p class="section-note">Grid-level defaults for the rich cell tooltip (BACKLOG-0001204), set once for every column rather than repeated on each. Defaults only: it switches nothing on. A tooltip exists because a column declares `cell.tooltip`, and a grid whose columns declare none has no tooltips whatever is set here.</p>
12633
+ <div class="table-wrap">
12634
+ <table>
12635
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12636
+ <tbody>
12637
+ <tr><td class="name">delay</td><td class="type">number</td><td class="desc">How long the pointer or the keyboard cursor must rest on a cell before the tooltip is built, in milliseconds. 400 by default. The delay is why a pointer sweeping across the grid mounts nothing: a tooltip that built a chart on every cell it crossed would be unusable, and `0` asks for exactly that. <small>(optional)</small></td></tr>
12638
+ <tr><td class="name">maxWidth</td><td class="type">number | string</td><td class="desc">How wide the tooltip may grow. A number is pixels; a string is used as written. <small>(optional)</small></td></tr>
12639
+ </tbody>
12640
+ </table>
12641
+ </div>
12642
+ <h3 id="type-TooltipParams">TooltipParams</h3>
12643
+ <p class="section-note">What a tooltip's `render` and `mount` are given: the same identification `cell:clicked` carries, plus the cell element itself and the grid. Resolved from the DOM at the moment the tooltip opens rather than when the pointer arrived, so a pooled row re-used in between names the row it is showing now.</p>
12644
+ <div class="table-wrap">
12645
+ <table>
12646
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12647
+ <tbody>
12648
+ <tr><td class="name">row</td><td class="type">Row</td><td class="desc">The row under the pointer or the keyboard cursor.</td></tr>
12649
+ <tr><td class="name">key</td><td class="type">string</td><td class="desc">That row's key.</td></tr>
12650
+ <tr><td class="name">index</td><td class="type">number</td><td class="desc">Its display index.</td></tr>
12651
+ <tr><td class="name">colId</td><td class="type">string</td><td class="desc">The column the cell belongs to.</td></tr>
12652
+ <tr><td class="name">column</td><td class="type">Column</td><td class="desc">The resolved column.</td></tr>
12653
+ <tr><td class="name">value</td><td class="type">unknown</td><td class="desc">The cell's value.</td></tr>
12654
+ <tr><td class="name">text</td><td class="type">string</td><td class="desc">The cell's formatted text.</td></tr>
12655
+ <tr><td class="name">cell</td><td class="type">HTMLElement</td><td class="desc">The cell element the tooltip is anchored to.</td></tr>
12656
+ <tr><td class="name">grid</td><td class="type">Grid</td><td class="desc">The grid.</td></tr>
12657
+ </tbody>
12658
+ </table>
12659
+ </div>
12660
+ <h3 id="type-TooltipRow">TooltipRow</h3>
12661
+ <p class="section-note">One label/value line in a {@link TooltipSpec}. Both halves are written as text by the grid, whatever they contain.</p>
12662
+ <div class="table-wrap">
12663
+ <table>
12664
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12665
+ <tbody>
12666
+ <tr><td class="name">label</td><td class="type">unknown</td><td class="desc">The line's label, drawn on the leading edge. <small>(optional)</small></td></tr>
12667
+ <tr><td class="name">value</td><td class="type">unknown</td><td class="desc">The line's value, drawn on the trailing edge. <small>(optional)</small></td></tr>
12668
+ </tbody>
12669
+ </table>
12670
+ </div>
12671
+ <h3 id="type-TooltipSpec">TooltipSpec</h3>
12672
+ <p class="section-note">Structured tooltip content the grid renders for you (BACKLOG-0001204): a heading, a list of label/value lines, and a closing note. Every field is written as **text**, never as markup, so a spec built out of row values needs no escaping and cannot become HTML by accident. Return `{ html }` from `render` when markup is genuinely wanted.</p>
12673
+ <div class="table-wrap">
12674
+ <table>
12675
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12676
+ <tbody>
12677
+ <tr><td class="name">title</td><td class="type">unknown</td><td class="desc">A heading for the tooltip. <small>(optional)</small></td></tr>
12678
+ <tr><td class="name">rows</td><td class="type">TooltipRow[]</td><td class="desc">Label/value lines, in order. <small>(optional)</small></td></tr>
12679
+ <tr><td class="name">note</td><td class="type">unknown</td><td class="desc">A closing note under the lines, drawn quieter than them. <small>(optional)</small></td></tr>
12680
+ </tbody>
12681
+ </table>
12682
+ </div>
10996
12683
  <h3 id="type-TopValue">TopValue</h3>
10997
12684
  <p class="section-note">One row of a categorical column's top-values table (BACKLOG-0000959).</p>
10998
12685
  <div class="table-wrap">
@@ -11200,7 +12887,20 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11200
12887
  <tbody>
11201
12888
  <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>
11202
12889
  <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>
11203
- <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>
12890
+ <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>
12891
+ </tbody>
12892
+ </table>
12893
+ </div>
12894
+ <h3 id="type-WhereRuntime">WhereRuntime</h3>
12895
+ <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>
12896
+ <div class="table-wrap">
12897
+ <table>
12898
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12899
+ <tbody>
12900
+ <tr><td class="name">active</td><td class="type">boolean</td><td class="desc">Whether any predicate is registered at all.</td></tr>
12901
+ <tr><td class="name">names</td><td class="type">string[]</td><td class="desc">The registered names, in registration order — for diagnostics.</td></tr>
12902
+ <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>
12903
+ <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>
11204
12904
  </tbody>
11205
12905
  </table>
11206
12906
  </div>
@@ -11230,7 +12930,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11230
12930
  <!-- END GENERATED TYPE REFERENCE -->
11231
12931
 
11232
12932
  <footer>
11233
- Lattice Grid 1.58.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
12933
+ Lattice Grid 1.60.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
11234
12934
  This document describes the behaviour of the shipped library. Where this guide and the code
11235
12935
  disagree, the code wins: please <a href="https://www.latticegrid.dev">tell us</a>.
11236
12936
  </footer>