@toclocoinc/lattice-grid 1.66.0 → 1.68.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 (163) hide show
  1. package/README.md +1 -1
  2. package/angular/package.json +1 -1
  3. package/docs/API.html +87 -29
  4. package/docs/api-detail.html +40 -5
  5. package/lattice-grid.d.ts +20 -7
  6. package/lattice-grid.esm.min.js +35 -11
  7. package/lattice-grid.min.cjs +35 -11
  8. package/lattice-grid.min.js +35 -11
  9. package/modules/ai.d.ts +1 -1
  10. package/modules/ai.esm.min.js +3 -3
  11. package/modules/ai.min.cjs +3 -3
  12. package/modules/ai.min.js +3 -3
  13. package/modules/angular.d.ts +1 -1
  14. package/modules/angular.esm.min.js +3 -3
  15. package/modules/angular.min.cjs +3 -3
  16. package/modules/angular.min.js +3 -3
  17. package/modules/chart-alluvial.d.ts +1 -1
  18. package/modules/chart-alluvial.esm.min.js +1 -1
  19. package/modules/chart-alluvial.min.cjs +1 -1
  20. package/modules/chart-alluvial.min.js +1 -1
  21. package/modules/chart-arc.d.ts +1 -1
  22. package/modules/chart-arc.esm.min.js +1 -1
  23. package/modules/chart-arc.min.cjs +1 -1
  24. package/modules/chart-arc.min.js +1 -1
  25. package/modules/chart-bubblemap.d.ts +1 -1
  26. package/modules/chart-bubblemap.esm.min.js +1 -1
  27. package/modules/chart-bubblemap.min.cjs +1 -1
  28. package/modules/chart-bubblemap.min.js +1 -1
  29. package/modules/chart-bump.d.ts +1 -1
  30. package/modules/chart-bump.esm.min.js +1 -1
  31. package/modules/chart-bump.min.cjs +1 -1
  32. package/modules/chart-bump.min.js +1 -1
  33. package/modules/chart-calendar.d.ts +1 -1
  34. package/modules/chart-calendar.esm.min.js +1 -1
  35. package/modules/chart-calendar.min.cjs +1 -1
  36. package/modules/chart-calendar.min.js +1 -1
  37. package/modules/chart-decomposition.d.ts +1 -1
  38. package/modules/chart-decomposition.esm.min.js +1 -1
  39. package/modules/chart-decomposition.min.cjs +1 -1
  40. package/modules/chart-decomposition.min.js +1 -1
  41. package/modules/chart-diverging.d.ts +1 -1
  42. package/modules/chart-diverging.esm.min.js +1 -1
  43. package/modules/chart-diverging.min.cjs +1 -1
  44. package/modules/chart-diverging.min.js +1 -1
  45. package/modules/chart-dumbbell.d.ts +1 -1
  46. package/modules/chart-dumbbell.esm.min.js +1 -1
  47. package/modules/chart-dumbbell.min.cjs +1 -1
  48. package/modules/chart-dumbbell.min.js +1 -1
  49. package/modules/chart-fan.d.ts +1 -1
  50. package/modules/chart-fan.esm.min.js +1 -1
  51. package/modules/chart-fan.min.cjs +1 -1
  52. package/modules/chart-fan.min.js +1 -1
  53. package/modules/chart-hexbin.d.ts +1 -1
  54. package/modules/chart-hexbin.esm.min.js +1 -1
  55. package/modules/chart-hexbin.min.cjs +1 -1
  56. package/modules/chart-hexbin.min.js +1 -1
  57. package/modules/chart-hexmap.d.ts +1 -1
  58. package/modules/chart-hexmap.esm.min.js +1 -1
  59. package/modules/chart-hexmap.min.cjs +1 -1
  60. package/modules/chart-hexmap.min.js +1 -1
  61. package/modules/chart-icicle.d.ts +1 -1
  62. package/modules/chart-icicle.esm.min.js +1 -1
  63. package/modules/chart-icicle.min.cjs +1 -1
  64. package/modules/chart-icicle.min.js +1 -1
  65. package/modules/chart-markermap.d.ts +1 -1
  66. package/modules/chart-markermap.esm.min.js +1 -1
  67. package/modules/chart-markermap.min.cjs +1 -1
  68. package/modules/chart-markermap.min.js +1 -1
  69. package/modules/chart-parallel.d.ts +1 -1
  70. package/modules/chart-parallel.esm.min.js +1 -1
  71. package/modules/chart-parallel.min.cjs +1 -1
  72. package/modules/chart-parallel.min.js +1 -1
  73. package/modules/chart-ridgeline.d.ts +1 -1
  74. package/modules/chart-ridgeline.esm.min.js +1 -1
  75. package/modules/chart-ridgeline.min.cjs +1 -1
  76. package/modules/chart-ridgeline.min.js +1 -1
  77. package/modules/chart-roc.d.ts +1 -1
  78. package/modules/chart-roc.esm.min.js +1 -1
  79. package/modules/chart-roc.min.cjs +1 -1
  80. package/modules/chart-roc.min.js +1 -1
  81. package/modules/chart-slope.d.ts +1 -1
  82. package/modules/chart-slope.esm.min.js +1 -1
  83. package/modules/chart-slope.min.cjs +1 -1
  84. package/modules/chart-slope.min.js +1 -1
  85. package/modules/chart-splom.d.ts +1 -1
  86. package/modules/chart-splom.esm.min.js +1 -1
  87. package/modules/chart-splom.min.cjs +1 -1
  88. package/modules/chart-splom.min.js +1 -1
  89. package/modules/chart-waffle.d.ts +1 -1
  90. package/modules/chart-waffle.esm.min.js +1 -1
  91. package/modules/chart-waffle.min.cjs +1 -1
  92. package/modules/chart-waffle.min.js +1 -1
  93. package/modules/charts.d.ts +1 -1
  94. package/modules/charts.esm.min.js +1376 -1249
  95. package/modules/charts.min.cjs +1376 -1249
  96. package/modules/charts.min.js +1376 -1249
  97. package/modules/data-router.d.ts +8 -3
  98. package/modules/data-router.esm.min.js +229 -11
  99. package/modules/data-router.min.cjs +229 -11
  100. package/modules/data-router.min.js +229 -11
  101. package/modules/devtools.d.ts +1 -1
  102. package/modules/devtools.esm.min.js +1 -1
  103. package/modules/devtools.min.cjs +1 -1
  104. package/modules/devtools.min.js +1 -1
  105. package/modules/dhtmlx-compat.d.ts +1 -1
  106. package/modules/dhtmlx-compat.esm.min.js +3 -3
  107. package/modules/dhtmlx-compat.min.cjs +3 -3
  108. package/modules/dhtmlx-compat.min.js +3 -3
  109. package/modules/gantt.d.ts +8 -3
  110. package/modules/gantt.esm.min.js +56 -8
  111. package/modules/gantt.min.cjs +56 -8
  112. package/modules/gantt.min.js +56 -8
  113. package/modules/geo-europe-nuts.d.ts +1 -1
  114. package/modules/geo-europe-nuts.esm.min.js +1 -1
  115. package/modules/geo-uk.d.ts +1 -1
  116. package/modules/geo-uk.esm.min.js +1 -1
  117. package/modules/geo-us-states.d.ts +1 -1
  118. package/modules/geo-us-states.esm.min.js +1 -1
  119. package/modules/geo-world-110m.d.ts +1 -1
  120. package/modules/geo-world-110m.esm.min.js +1 -1
  121. package/modules/geo-world-50m.d.ts +1 -1
  122. package/modules/geo-world-50m.esm.min.js +1 -1
  123. package/modules/htmx.d.ts +1 -1
  124. package/modules/htmx.esm.min.js +35 -11
  125. package/modules/htmx.min.cjs +35 -11
  126. package/modules/htmx.min.js +35 -11
  127. package/modules/kanban.d.ts +10 -3
  128. package/modules/kanban.esm.min.js +24 -4
  129. package/modules/kanban.min.cjs +24 -4
  130. package/modules/kanban.min.js +24 -4
  131. package/modules/kpi.d.ts +35 -10
  132. package/modules/kpi.esm.min.js +55 -7
  133. package/modules/kpi.min.cjs +55 -7
  134. package/modules/kpi.min.js +55 -7
  135. package/modules/layout.d.ts +1 -1
  136. package/modules/layout.esm.min.js +3 -3
  137. package/modules/layout.min.cjs +3 -3
  138. package/modules/layout.min.js +3 -3
  139. package/modules/mock-socket.d.ts +1 -1
  140. package/modules/mock-socket.esm.min.js +1 -1
  141. package/modules/mock-socket.min.cjs +1 -1
  142. package/modules/mock-socket.min.js +1 -1
  143. package/modules/react.d.ts +1 -1
  144. package/modules/react.esm.min.js +3 -3
  145. package/modules/react.min.cjs +3 -3
  146. package/modules/react.min.js +3 -3
  147. package/modules/svelte.d.ts +1 -1
  148. package/modules/svelte.esm.min.js +3 -3
  149. package/modules/svelte.min.cjs +3 -3
  150. package/modules/svelte.min.js +3 -3
  151. package/modules/tabs.d.ts +1 -1
  152. package/modules/tabs.esm.min.js +13 -14
  153. package/modules/tabs.min.cjs +13 -14
  154. package/modules/tabs.min.js +13 -14
  155. package/modules/vue.d.ts +1 -1
  156. package/modules/vue.esm.min.js +3 -3
  157. package/modules/vue.min.cjs +3 -3
  158. package/modules/vue.min.js +3 -3
  159. package/modules/webcomponent.d.ts +1 -1
  160. package/modules/webcomponent.esm.min.js +35 -11
  161. package/modules/webcomponent.min.cjs +35 -11
  162. package/modules/webcomponent.min.js +35 -11
  163. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,7 @@ dependencies, no build step required. Optional adapters for React, Vue, Svelte
5
5
  and Web Components ship alongside it, and Angular has compiled components of its
6
6
  own at `@toclocoinc/lattice-grid/angular` — inside this package, not beside it.
7
7
 
8
- Version 1.66.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
8
+ Version 1.68.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
9
9
 
10
10
  ---
11
11
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.66.0",
2
+ "version": "1.68.0",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
5
  "module": "./fesm2022/toclocoinc-lattice-grid-angular.mjs",
package/docs/API.html CHANGED
@@ -325,6 +325,9 @@
325
325
 
326
326
  .chip--new { border-color: var(--accent); color: var(--accent); }
327
327
 
328
+ .chip--icon { display: inline-flex; align-items: center; gap: 4px; }
329
+ .chip-icon { width: 12px; height: 12px; flex: none; }
330
+
328
331
  footer {
329
332
  margin-top: 72px;
330
333
  padding-top: 22px;
@@ -360,7 +363,7 @@
360
363
  <div class="shell">
361
364
  <aside class="rail">
362
365
  <p class="rail__brand">Lattice Grid</p>
363
- <p class="rail__sub">API reference · v1.66.0</p>
366
+ <p class="rail__sub">API reference · v1.68.0</p>
364
367
  <nav>
365
368
  <div class="rail__group">
366
369
  <span class="rail__label">Start</span>
@@ -453,7 +456,7 @@
453
456
  </header>
454
457
 
455
458
  <p class="chips">
456
- <span class="chip">Version 1.66.0</span>
459
+ <span class="chip">Version 1.68.0</span>
457
460
  <span class="chip">Zero dependencies</span>
458
461
  <span class="chip"><a href="api-detail.html">Developer guide &rarr;</a></span>
459
462
  </p>
@@ -1957,7 +1960,7 @@ grid.destroy();
1957
1960
  <table>
1958
1961
  <thead><tr><th>Member</th><th>Returns</th><th>Description</th></tr></thead>
1959
1962
  <tbody>
1960
- <tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.66.0'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
1963
+ <tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.68.0'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
1961
1964
  <tr><td class="sig">get(key)</td><td class="type">unknown</td><td class="desc">Read any configuration key.</td></tr>
1962
1965
  <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>
1963
1966
  <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>
@@ -7363,7 +7366,7 @@ const kpi = createKPI(document.querySelector('#kpis'), {
7363
7366
  ],
7364
7367
  onTileClick: ({ tile }) =&gt; drillInto(tile.id),
7365
7368
  });</code></pre>
7366
- <p>Each tile names an <strong>aggregation</strong> (<code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, <code>countDistinct</code>, or a <code>custom</code> reducer <code>(rows, tile) =&gt; value</code>), the <strong>field</strong> it reads, and an optional <strong>filter</strong> predicate. Formatting (<code>number</code>/<code>currency</code>/<code>percent</code>/<code>compact</code>, with decimals, currency and locale), a <strong>baseline</strong> for a delta, and semantic <strong>threshold bands</strong> (two cut points with a direction, or an explicit <code>bands</code> list) are all per tile; the band is a semantic name (<code>good</code>/<code>warn</code>/<code>critical</code>), separate from any accent colour. An optional <strong>sparkline</strong> plots a <code>{ x, y }</code> series in sequence order. Each tile is a labelled <code>&lt;figure&gt;</code>, focusable and keyboard-activatable, its value announced; the sparkline transition respects <code>prefers-reduced-motion</code>.</p>
7369
+ <p>Each tile names an <strong>aggregation</strong> (<code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, <code>countDistinct</code>, or a <code>custom</code> reducer <code>(rows, tile) =&gt; value</code>), the <strong>field</strong> it reads, and an optional <strong>filter</strong> predicate. Formatting (<code>number</code>/<code>currency</code>/<code>percent</code>/<code>compact</code>, with decimals, currency and locale), a <strong>baseline</strong> for a delta, and semantic <strong>threshold bands</strong> (two cut points with a direction, or an explicit <code>bands</code> list) are all per tile; the band is a semantic name (<code>good</code>/<code>warn</code>/<code>critical</code>), separate from any accent colour. <code>delta: 'absolute' | 'relative' | 'both'</code> (default <code>'both'</code>) chooses what the movement line prints against the baseline (BACKLOG-0001673) &mdash; the difference alone, the percentage alone, or both together, because the percentage of a rate series (4.10 vs a 4.30 baseline) is meaningless and a host needs to keep the line without it. An optional <strong>sparkline</strong> plots a <code>{ x, y }</code> series in sequence order. Each tile is a labelled <code>&lt;figure&gt;</code>, focusable and keyboard-activatable, its value announced; the sparkline transition respects <code>prefers-reduced-motion</code>.</p>
7367
7370
  <p><strong>A panel with no data says so.</strong> A tile's status is <code>good</code>, <code>warn</code>, <code>critical</code>, <code>null</code> (no thresholds) &mdash; or <code>unknown</code>, which means the tile <em>measured nothing</em>. Two things cause that: the panel holds <em>no rows at all</em>, or the tile's <code>field</code> names no column on the bound grid, so it never read a cell to reduce over. That fourth status is decided from data presence <em>before</em> any threshold is consulted, because an aggregation over nothing returns the identity of its operation (<code>sum</code> and <code>count</code> return 0) and 0 is a number a threshold grades: under <code>lowerIsBetter</code> cut points it grades <code>good</code>, so an empty panel would otherwise read as a healthy one. An <code>unknown</code> tile reports a <code>null</code> value, renders the <code>nullText</code> placeholder rather than a zero, and is marked with a dashed edge and a visible &ldquo;No data&rdquo; caption that also forms part of its accessible name &mdash; the state is never carried by colour alone. Because <code>unknown</code> is an explicit status rather than a severity, anything rolling tiles up can surface &ldquo;not measured&rdquo; instead of inheriting a false green.</p>
7368
7371
  <p><strong>A measured zero is still a measurement.</strong> A tile whose <code>filter</code> matches none of the rows the panel <em>does</em> hold is a different thing: no open incidents is genuinely good, so it reads <code>0</code> and is graded on its thresholds exactly as before. Only an empty panel is <code>unknown</code>.</p>
7369
7372
  <p><strong>Staleness is a different question, and a time-bounded source answers it.</strong> <code>unknown</code> is about the absence of rows, not their staleness: a panel over an unbounded store keeps showing the last values it was given when its feed goes quiet, because those rows are still there. Give the underlying source a <a href="#sources">rolling time window</a> (<code>maxAge</code> with <code>ageBy</code>) and its rows age out while the feed is silent, so the panel drains and reports <code>unknown</code> the next time it reads them &mdash; a grid-bound panel as the grid announces the change (or when the host calls <code>refresh()</code>), a routed one as the removals reach <code>rows.apply</code>.</p>
@@ -7442,7 +7445,7 @@ const kpi = createKPI(document.querySelector('#kpis'), {
7442
7445
  kpi.destroy();
7443
7446
  <span class="kw">return</span> [shapes, shaped, ignoredAgg].join(' | ');</code></pre>
7444
7447
  <h3 id="kpi-tree">The KPI tree: top-level items that expand to the indicators beneath them</h3>
7445
- <p>Set <code>tree</code> and the panel becomes a <strong>rail</strong> instead of a grid of tiles: a small number of top-level items, each expanding to the indicators underneath it, with the parent telling you at a glance whether anything below needs attention. <code>Compute</code> expands to <code>psi</code> and <code>cpu</code>; collapsed, it still shows you that one of them is in breach.</p>
7448
+ <p>Set <code>tree</code> and the panel becomes a <strong>rail</strong> instead of a grid of tiles: a small number of top-level items, each expanding to the indicators underneath it, with the parent telling you at a glance whether anything below needs attention. <code>Compute</code> expands to <code>psi</code> and <code>cpu</code>; collapsed, it still shows you that one of them is in breach. <code>tree</code> is the opt-in (BACKLOG-0001673): a dotted tile id is not a hierarchy on its own &mdash; with <code>tree</code> unset, the ids you see below would stay plain, unsplit tile ids and the panel would render every tile flat, warning once that setting <code>tree</code> is how the hierarchy is reached.</p>
7446
7449
  <pre><code>const kpi = createKPI(document.querySelector('#rail'), {
7447
7450
  rows, rowKey: 'id',
7448
7451
  tiles: [
@@ -7458,7 +7461,7 @@ kpi.destroy();
7458
7461
 
7459
7462
  kpi.nodes()[0].rollup; <span class="cmt">// 'critical' — even with the branch shut</span>
7460
7463
  kpi.expand(kpi.nodes()[0].key); <span class="cmt">// open it</span></code></pre>
7461
- <p><strong>Where the shape comes from &mdash; two sources, in this order.</strong> <em>Declared:</em> <code>tree: { path }</code> or <code>tree: { parentKey }</code> over the <em>tile specs</em>, the same two shapes the grid's <a href="#tree-data">tree data</a> and the tree-select editor already take, so a hierarchy you have configured once needs no second vocabulary. A <code>path</code> is the tile's <em>own</em> place, its own segment last &mdash; <code>['System','Compute','cpu']</code>, exactly as <code>['EMEA','UK','Colchester']</code> is Colchester's path and not its parent's &mdash; and levels no tile represents are synthesised, so <code>System</code> and <code>Compute</code> appear without a tile of their own. <em>Derived:</em> with neither declared, the tile ids are split on <code>separator</code> (default <code>.</code>), so <code>system.compute.cpu</code> files itself. A panel whose ids carry no separator is flat and renders exactly as it always did; <code>tree: false</code> keeps it flat whatever the ids look like. A tile's <code>field</code> is never a source &mdash; a dot there already means a nested object property, and overloading it would make <code>field: 'cpu.util'</code> ambiguous.</p>
7464
+ <p><strong>Where the shape comes from &mdash; two sources, in this order, once <code>tree</code> is set.</strong> <em>Declared:</em> <code>tree: { path }</code> or <code>tree: { parentKey }</code> over the <em>tile specs</em>, the same two shapes the grid's <a href="#tree-data">tree data</a> and the tree-select editor already take, so a hierarchy you have configured once needs no second vocabulary. A <code>path</code> is the tile's <em>own</em> place, its own segment last &mdash; <code>['System','Compute','cpu']</code>, exactly as <code>['EMEA','UK','Colchester']</code> is Colchester's path and not its parent's &mdash; and levels no tile represents are synthesised, so <code>System</code> and <code>Compute</code> appear without a tile of their own. <em>Derived:</em> with neither declared, the tile ids are split on <code>separator</code> (default <code>.</code>), so <code>system.compute.cpu</code> files itself &mdash; but only once <code>tree</code> is given an object (<code>{}</code> included); unset, a dot in an id means nothing and the panel warns once rather than building a hierarchy you never asked for. A panel whose ids carry no separator is flat and renders exactly as it always did even with <code>tree</code> set; <code>tree: false</code>, same as leaving it unset, keeps it flat whatever the ids look like, silently. A tile's <code>field</code> is never a source &mdash; a dot there already means a nested object property, and overloading it would make <code>field: 'cpu.util'</code> ambiguous.</p>
7462
7465
  <p><strong>No value rolls up; severity does.</strong> A parent shows no aggregated number. That is not a simplification: the running accumulators expose <code>add</code>/<code>remove</code>/<code>value</code> and no merge, so <code>avg</code>, <code>countDistinct</code> and a <code>custom</code> reducer cannot be composed from their children without rescanning, and a per-aggregation exception list would be a number that is right for a sum and wrong for an average. A parent that has a tile of its own still shows <em>that tile's</em> reading. What does roll up is the status: <code>rollup</code> is the worst severity at or below the node, across as many levels as you have, and it is what a collapsed branch reports.</p>
7463
7466
  <p><strong>Nothing measured is not good news, and it does not win the roll-up either.</strong> A leaf that measured nothing is <code>unknown</code> (see above), and <code>unknown</code> is deliberately excluded from <code>rollup</code>: ranking &ldquo;not measured&rdquo; as the worst thing beneath a parent would hide a real warning under it. It is surfaced separately instead &mdash; <code>node.unknown</code> counts the descendants that measured nothing, and the node renders it in words (&ldquo;2 unknown&rdquo;) and puts it in its accessible name. So neither way of being wrong is available: silence cannot read as green, and it cannot bury an amber.</p>
7464
7467
  <p><strong>Accessible by construction.</strong> The rail is a real <code>role="tree"</code> with the APG keyboard model &mdash; <kbd>&rarr;</kbd> opens a closed branch and otherwise steps into it, <kbd>&larr;</kbd> closes an open one and otherwise steps out to its parent, <kbd>&uarr;</kbd>/<kbd>&darr;</kbd> walk what is visible, <kbd>Home</kbd>/<kbd>End</kbd> jump to its ends, <kbd>Enter</kbd>/<kbd>Space</kbd> activate &mdash; with a roving tabindex, and <code>aria-level</code>, <code>aria-posinset</code> and <code>aria-setsize</code> on every node, because a reader cannot count what a collapsed branch has left out of the DOM. Status carries a <strong>shape</strong> as well as a colour (a filled circle, a triangle, a square, a hollow circle), not one dot in three colours, and a parent's rolled-up status is <em>in its accessible name</em>: &ldquo;Compute, 6 items, worst status critical&rdquo;, announced as one string. Every phrase is a catalogue key: pass <code>messages</code> (any <code>{ t(key, params) }</code>, including a grid's own) to translate the panel, and a key your catalogue lacks falls back to English rather than printing the key.</p>
@@ -7468,8 +7471,10 @@ kpi.expand(kpi.nodes()[0].key); <span class="cmt">// open it</span></code><
7468
7471
  <p><strong>A collapsed branch costs data, not paint.</strong> Every node's status is computed whether or not you can see it &mdash; that is what makes the rail worth having &mdash; while a collapsed branch contributes no DOM at all. It is the same division the grid's grouping already makes between its totals walk and its display walk. Expansion is patched in place, keyed on the node, so a live routed feed does not throw a keyboard user off the node they are standing on.</p>
7469
7472
  <h3 id="kpi-tree-example">A collapsed parent reports the red leaf underneath it, executed</h3>
7470
7473
  <p class="section-note">Two subsystems from dotted tile ids, one of them in breach, with nothing expanded; then the same
7471
- rail with no data at all. Run headless on every build.</p>
7472
- <pre data-run="js" data-expect="compute false critical | cpu critical | null 2" data-covers="export:createKPI"><code><span class="kw">const</span> { createKPI } = <span class="kw">await</span> import('../packages/modules/kpi/index.js');
7474
+ rail with no data at all. <code>tree</code> is the opt-in (BACKLOG-0001673) &mdash; a dotted id alone does
7475
+ not build a hierarchy, so this example sets <code>tree: {}</code> deliberately, the way any host must. Run
7476
+ headless on every build.</p>
7477
+ <pre data-run="js" data-expect="compute false critical | cpu critical | null 2" data-covers="export:createKPI config:tree"><code><span class="kw">const</span> { createKPI } = <span class="kw">await</span> import('../packages/modules/kpi/index.js');
7473
7478
 
7474
7479
  <span class="kw">const</span> fewerIsBetter = { warn: 70, critical: 90, direction: 'lowerIsBetter' };
7475
7480
  <span class="kw">const</span> tiles = [
@@ -7479,8 +7484,9 @@ kpi.expand(kpi.nodes()[0].key); <span class="cmt">// open it</span></code><
7479
7484
  thresholds: { warn: 100, critical: 300, direction: 'lowerIsBetter' } },
7480
7485
  ];
7481
7486
 
7482
- <span class="cmt">// No `tree` block: the dots in the tile ids are the hierarchy.</span>
7483
- <span class="kw">const</span> kpi = createKPI(null, { rows: [{ id: 'h1', cpu: 94, mem: 40, latency: 12 }], rowKey: 'id', tiles });
7487
+ <span class="cmt">// `tree: {}` is the opt-in: without it, these dotted ids would stay flat tiles</span>
7488
+ <span class="cmt">// and the panel would warn once instead of building a hierarchy.</span>
7489
+ <span class="kw">const</span> kpi = createKPI(null, { rows: [{ id: 'h1', cpu: 94, mem: 40, latency: 12 }], rowKey: 'id', tree: {}, tiles });
7484
7490
 
7485
7491
  <span class="kw">const</span> compute = kpi.nodes()[0];
7486
7492
  <span class="cmt">// Nobody has opened it, and it reports the breach anyway.</span>
@@ -7488,7 +7494,7 @@ kpi.expand(kpi.nodes()[0].key); <span class="cmt">// open it</span></code><
7488
7494
  <span class="kw">const</span> leaf = [compute.children[0].label, compute.children[0].status].join(' '); <span class="cmt">// cpu critical</span>
7489
7495
 
7490
7496
  <span class="cmt">// Nothing delivered: the parent reports the silence rather than a false green.</span>
7491
- <span class="kw">const</span> quiet = createKPI(null, { rows: [], rowKey: 'id', tiles });
7497
+ <span class="kw">const</span> quiet = createKPI(null, { rows: [], rowKey: 'id', tree: {}, tiles });
7492
7498
  <span class="kw">const</span> silent = String(quiet.nodes()[0].rollup) + ' ' + quiet.nodes()[0].unknown; <span class="cmt">// null 2</span>
7493
7499
 
7494
7500
  <span class="kw">return</span> [shut, leaf, silent].join(' | ');</code></pre>
@@ -8200,15 +8206,65 @@ createGrid(el, {
8200
8206
 
8201
8207
  <h3>Icons</h3>
8202
8208
  <p>Inline SVG sprites, overridable by name through <code>registerIcon(name, def)</code>.</p>
8209
+ <!-- icon-gallery:start -->
8203
8210
  <div class="chips">
8204
- <span class="chip">chevronRight</span><span class="chip">chevronDown</span><span class="chip">chevronUp</span><span class="chip">chevronLeft</span>
8205
- <span class="chip">check</span><span class="chip">dash</span><span class="chip">close</span><span class="chip">plus</span><span class="chip">minus</span>
8206
- <span class="chip">info</span><span class="chip">success</span><span class="chip">warning</span><span class="chip">danger</span>
8207
- <span class="chip">clock</span><span class="chip">lock</span><span class="chip">link</span><span class="chip">external</span><span class="chip">filter</span>
8208
- <span class="chip">sortAsc</span><span class="chip">sortDesc</span><span class="chip">menu</span><span class="chip">drag</span>
8209
- <span class="chip">star</span><span class="chip">heart</span><span class="chip">circleFilled</span><span class="chip">square</span><span class="chip">bolt</span><span class="chip">flag</span><span class="chip">thumbUp</span>
8210
- <span class="chip">eye</span><span class="chip">eyeOff</span><span class="chip">copy</span><span class="chip">present</span><span class="chip">blank</span>
8211
- </div>
8211
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3.5 10.5 8 6 12.5"></path></svg>chevronRight</span>
8212
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 6 8 10.5 12.5 6"></path></svg>chevronDown</span>
8213
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 10 8 5.5 12.5 10"></path></svg>chevronUp</span>
8214
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3.5 5.5 8 10 12.5"></path></svg>chevronLeft</span>
8215
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.5 6.5 11.5 12.5 4.5"></path></svg>check</span>
8216
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h8"></path></svg>dash</span>
8217
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4l8 8M12 4l-8 8"></path></svg>close</span>
8218
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3.5v9M3.5 8h9"></path></svg>plus</span>
8219
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8h9"></path></svg>minus</span>
8220
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Zm.75 4a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM7.25 7h1.5v4.5h-1.5Z"></path></svg>info</span>
8221
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Zm3.2 4.4-4 4.6-2.4-2.3 1-1.1 1.3 1.2 3-3.4Z"></path></svg>success</span>
8222
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 1.5 15 14H1Zm-.75 4v4h1.5v-4Zm0 5v1.5h1.5V10.5Z"></path></svg>warning</span>
8223
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Zm-.75 3h1.5v5h-1.5Zm0 6h1.5V12h-1.5Z"></path></svg>danger</span>
8224
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Zm-.75 3h1.5v3.7l2.5 1.5-.75 1.3-3.25-2Z"></path></svg>clock</span>
8225
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M4.5 7V5a3.5 3.5 0 1 1 7 0v2H13v7H3V7Zm1.5 0h4V5a2 2 0 1 0-4 0Z"></path></svg>lock</span>
8226
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 9.5a3 3 0 0 1 0-4l2-2a3 3 0 0 1 4 4l-1 1M9.5 6.5a3 3 0 0 1 0 4l-2 2a3 3 0 0 1-4-4l1-1"></path></svg>link</span>
8227
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 3.5h-3v9h9v-3M9.5 3.5h3v3M12.5 3.5 7 9"></path></svg>external</span>
8228
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M2 3h12l-4.5 5.2V13L6.5 11.5V8.2Z"></path></svg>filter</span>
8229
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M4.5 3h2.5v10H4.5zM9 3h2.5v10H9z"></path></svg>pause</span>
8230
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M4.5 3l8 5-8 5z"></path></svg>play</span>
8231
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 13.5h11"></path><path d="M4.5 13.5v-4"></path><path d="M8 13.5v-8"></path><path d="M11.5 13.5v-6"></path></svg>chart</span>
8232
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 1.5a6.5 6.5 0 0 0 0 13c.7 0 1.2-.5 1.2-1.1 0-.3-.1-.6-.3-.8a1.1 1.1 0 0 1 .8-1.9h1.4A3.4 3.4 0 0 0 14.5 7c0-3-2.9-5.5-6.5-5.5Z"></path><path d="M4.6 7.6a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM6.7 4.6a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM9.9 4.7a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM11.9 6.9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"></path></svg>palette</span>
8233
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8h6.5a3.25 3.25 0 0 1 0 6.5H6M3 8l3-3M3 8l3 3"></path></svg>undo</span>
8234
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M13 8H6.5a3.25 3.25 0 0 0 0 6.5H10M13 8l-3-3M13 8l-3 3"></path></svg>redo</span>
8235
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 2.5h11v11h-11zM6.5 2.5v11M10 2.5v11"></path></svg>columns</span>
8236
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.5v7.5M8 10 5 7M8 10l3-3M2.5 12.5h11"></path></svg>download</span>
8237
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8a5 5 0 1 0 1.6-3.7M3 3v2.8h2.8"></path></svg>restore</span>
8238
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2.5h10v11H3zM3 6h10M3 9.5h10M6.5 6v7.5"></path></svg>spreadsheet</span>
8239
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 6V2.5h7V6M4.5 11.5h-2V6h11v5.5h-2M4.5 9.5h7v4h-7z"></path></svg>print</span>
8240
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 6V2.5H6M10 2.5h3.5V6M13.5 10v3.5H10M6 13.5H2.5V10"></path></svg>maximise</span>
8241
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2.5V6H2.5M13.5 6H10V2.5M10 13.5V10h3.5M2.5 10H6v3.5"></path></svg>minimise</span>
8242
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2.5h8v11L8 11l-4 2.5z"></path></svg>views</span>
8243
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 2.5a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9M10.4 10.4 13.5 13.5"></path></svg>search</span>
8244
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 11.2 11 2.7l2.3 2.3-8.5 8.5-3 .7z"></path></svg>pencil</span>
8245
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4.5h10M6.5 4.5V3h3v1.5M4.5 4.5l.7 8.5h5.6l.7-8.5M6.8 7v3.7M9.2 7v3.7"></path></svg>trash</span>
8246
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M11.5 5.5a1.8 1.8 0 1 0 0-3.6 1.8 1.8 0 0 0 0 3.6M4.5 9.8a1.8 1.8 0 1 0 0-3.6 1.8 1.8 0 0 0 0 3.6M11.5 14.1a1.8 1.8 0 1 0 0-3.6 1.8 1.8 0 0 0 0 3.6M6.1 7.1l3.8-1.9M6.1 8.9l3.8 1.9"></path></svg>share</span>
8247
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v6M5 8h6l1 2.5H4zM8 10.5V14"></path></svg>pin</span>
8248
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 3.5 12 8H4Z"></path></svg>sortAsc</span>
8249
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 12.5 4 8h8Z"></path></svg>sortDesc</span>
8250
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"></path></svg>menu</span>
8251
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M6 3.5h1.5V5H6Zm3 0h1.5V5H9ZM6 7.25h1.5v1.5H6Zm3 0h1.5v1.5H9ZM6 11h1.5v1.5H6Zm3 0h1.5v1.5H9Z"></path></svg>drag</span>
8252
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 1.6l1.96 3.97 4.38.64-3.17 3.09.75 4.36L8 11.6l-3.92 2.06.75-4.36L1.66 6.21l4.38-.64Z"></path></svg>star</span>
8253
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 14.2 2.4 8.6a3.3 3.3 0 0 1 4.67-4.67L8 4.86l.93-.93A3.3 3.3 0 0 1 13.6 8.6Z"></path></svg>heart</span>
8254
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2Z"></path></svg>circleFilled</span>
8255
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 3h10v10H3Z"></path></svg>square</span>
8256
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M9.5 1.5 3.5 9h3.2l-.7 5.5L12.5 7H9.3Z"></path></svg>bolt</span>
8257
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M4 1.5h1.5v13H4Zm1.5 1h7.5l-1.6 2.6L13 7.7H5.5Z"></path></svg>flag</span>
8258
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 13 12.5 3.5"></path><path d="M7.5 3.5H12.5V8.5"></path></svg>arrow</span>
8259
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11 10 5l2 2-6 6H4z"></path><path d="M3 14.5h10"></path></svg>highlight</span>
8260
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M5.5 6.8 8.6 1.5a1.6 1.6 0 0 1 1.5 2.1L9.4 6h3.4a1.4 1.4 0 0 1 1.35 1.78l-1.3 4.6A1.9 1.9 0 0 1 11 13.8H5.5ZM4 6.5H1.5v7.3H4Z"></path></svg>thumbUp</span>
8261
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 3.2c3.3 0 6 3 6.6 4.5C14 9.3 11.3 12.3 8 12.3S2 9.3 1.4 7.7C2 6.2 4.7 3.2 8 3.2Zm0 2a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Z"></path></svg>eye</span>
8262
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.2 2.9 13.1 13.8M8 3.2c3.3 0 6 3 6.6 4.5a9.4 9.4 0 0 1-1.9 2.4M5 4.2A9 9 0 0 0 1.4 7.7C2 9.3 4.7 12.3 8 12.3c.9 0 1.7-.2 2.5-.5M6.3 6a2.5 2.5 0 0 0 3.4 3.5"></path></svg>eyeOff</span>
8263
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5.5 2.5h8v8M2.5 5.5h8v8h-8Z"></path></svg>copy</span>
8264
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 2.5h12v8H2Z"></path><path d="M8 10.5v3"></path><path d="M6 13.5h4"></path></svg>present</span>
8265
+ <span class="chip chip--icon"><svg class="chip-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor"><path d=""></path></svg>blank</span>
8266
+ </div>
8267
+ <!-- icon-gallery:end -->
8212
8268
 
8213
8269
  <h2 id="operators">Filter grammar</h2>
8214
8270
  <p class="section-note">
@@ -9666,7 +9722,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9666
9722
  </table>
9667
9723
  </div>
9668
9724
  <h3 id="type-ChartAnnotation">ChartAnnotation</h3>
9669
- <p class="section-note">One declarative annotation (BACKLOG-0000744, extended by BACKLOG-0000953). A reference or target line, a shaded band, a callout, or an `event` marker. Its value is a constant `value` (or `from`/`to` for a band), or a `compute` reduction of the data it annotates — `mean`, `median`, `min`, `max`, or `p95` for a percentile — so it follows the data as the grid is filtered. A band with `orient: 'vertical'` shades an x-range instead — an event window, a maintenance period — and an `event` marker is a labelled vertical rule with a flag at a position on the x axis. Every annotation names the axis it reads, which on a dual-axis chart is what stops it being placed against the wrong scale, and is written into the accessible table as a sentence — a vertical marker and an event stating the position they sit at, because a screen-reader user needs where and when, not only that a marker exists.</p>
9725
+ <p class="section-note">One declarative annotation (BACKLOG-0000744, extended by BACKLOG-0000953). A reference or target line, a shaded band, a callout, or an `event` marker. Its value is a constant `value` (or `from`/`to` for a band), or a `compute` reduction of the data it annotates — `mean`, `median`, `min`, `max`, or `p95` for a percentile — so it follows the data as the grid is filtered. A band with `orient: 'vertical'` shades an x-range instead — an event window, a maintenance period — and an `event` marker is a labelled vertical rule with a flag at a position on the x axis. Every annotation names the axis it reads, which on a dual-axis chart is what stops it being placed against the wrong scale, and is written into the accessible table as a sentence — a vertical marker and an event stating the position they sit at, because a screen-reader user needs where and when, not only that a marker exists. An annotation the chart cannot place — an x value it cannot resolve on the axis, or a band whose edges are off-scale — is dropped with one console warning naming the annotation's kind, the axis and the reason, rather than vanishing without a trace (BACKLOG-0001672).</p>
9670
9726
  <div class="table-wrap">
9671
9727
  <table>
9672
9728
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
@@ -9732,7 +9788,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9732
9788
  <tr><td class="name">fn</td><td class="type">TotalName</td><td class="desc">A reduction name, as the totals row uses. <small>(optional)</small></td></tr>
9733
9789
  <tr><td class="name">type</td><td class="type">'bar' | 'line' | 'area'</td><td class="desc">The mark this measure draws with, on a combo chart. <small>(optional)</small></td></tr>
9734
9790
  <tr><td class="name">axis</td><td class="type">'left' | 'right'</td><td class="desc">Which axis it belongs to, on a combo chart. <small>(optional)</small></td></tr>
9735
- <tr><td class="name">title</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
9791
+ <tr><td class="name">title</td><td class="type">string</td><td class="desc">The series label a combo's legend and axis titles use (BACKLOG-0001672). `label` is read too, as an undeclared alias, for a caller already using it; `title` wins when both are given. Falls back to the measure column's own `title`, then to `col`, when neither is set. <small>(optional)</small></td></tr>
9736
9792
  </tbody>
9737
9793
  </table>
9738
9794
  </div>
@@ -11670,7 +11726,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11670
11726
  <tr><td class="name">edit</td><td class="type">EditConfig | boolean</td><td class="desc">Editing, and how a change is committed and validated. <small>(optional)</small></td></tr>
11671
11727
  <tr><td class="name">pagination</td><td class="type">PaginationConfig | boolean</td><td class="desc">Page the rows rather than scrolling them. <small>(optional)</small></td></tr>
11672
11728
  <tr><td class="name">locale</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
11673
- <tr><td class="name">direction</td><td class="type">'ltr' | 'rtl' | 'auto'</td><td class="desc">Writing direction. Omit it, or say `'auto'`, to settle it from the element's own computed `dir` and then from `locale`: `ar`, `he`, `fa` and the rest resolve to `rtl`. In a right-to-left grid the logical alignments `start`/`end` mirror while the physical `left`/`right` do not (see {@link Align}). <small>(optional)</small></td></tr>
11729
+ <tr><td class="name">direction</td><td class="type">'ltr' | 'rtl' | 'auto'</td><td class="desc">Writing direction. An explicit `'ltr'` or `'rtl'` always wins. Omit it, or say `'auto'`, to settle it from the mount or the nearest ancestor carrying a `dir="ltr"`/`dir="rtl"` attribute, and only then from `locale`: `ar`, `he`, `fa` and the rest resolve to `rtl`. A page-level `dir="ltr"` therefore wins over an RTL locale — a host that wants RTL inside an LTR page sets `direction: 'rtl'` explicitly. In a right-to-left grid the logical alignments `start`/`end` mirror while the physical `left`/`right` do not (see {@link Align}). <small>(optional)</small></td></tr>
11674
11730
  <tr><td class="name">timeZone</td><td class="type">string</td><td class="desc">IANA zone every date column formats in, e.g. 'Europe/London' or 'UTC'. Omit to use each viewer's own zone. A column's own `format.timeZone` wins. <small>(optional)</small></td></tr>
11675
11731
  <tr><td class="name">theme</td><td class="type">Theme</td><td class="desc">The visual theme. <small>(optional)</small></td></tr>
11676
11732
  <tr><td class="name">density</td><td class="type">Density</td><td class="desc">Row height and padding as a named step, rather than pixel by pixel. <small>(optional)</small></td></tr>
@@ -12098,7 +12154,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12098
12154
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12099
12155
  <tbody>
12100
12156
  <tr><td class="name">el</td><td class="type">unknown | null</td><td class="desc"><small>(read-only)</small></td></tr>
12101
- <tr><td class="name">rowKey</td><td class="type">string | ((row: KanbanRow) =&gt; unknown)</td><td class="desc"><small>(read-only)</small></td></tr>
12157
+ <tr><td class="name">rowKey</td><td class="type">string | ((row: KanbanRow) =&gt; string | number)</td><td class="desc">The resolved card identity; see `KanbanConfig.rowKey`. <small>(read-only)</small></td></tr>
12102
12158
  <tr><td class="name">rows</td><td class="type">KanbanRows</td><td class="desc"></td></tr>
12103
12159
  <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>
12104
12160
  <tr><td class="name">columns</td><td class="type">(): KanbanColumn[]</td><td class="desc"></td></tr>
@@ -12233,7 +12289,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12233
12289
  <tbody>
12234
12290
  <tr><td class="name">rows</td><td class="type">KanbanRow[]</td><td class="desc"><small>(optional)</small></td></tr>
12235
12291
  <tr><td class="name">grid</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
12236
- <tr><td class="name">rowKey</td><td class="type">string | ((row: KanbanRow) =&gt; unknown)</td><td class="desc"><small>(optional)</small></td></tr>
12292
+ <tr><td class="name">rowKey</td><td class="type">string | ((row: KanbanRow) =&gt; string | number)</td><td class="desc">Card identity (a field or fn, returning a string or number); default 'id'. Composite (`string[]`) keys are core-grid-only: a board keys its own card identity from one value, so there is nothing for an array to join into here (BACKLOG-0001669). <small>(optional)</small></td></tr>
12237
12293
  <tr><td class="name">columnProperty</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
12238
12294
  <tr><td class="name">columns</td><td class="type">KanbanColumnDef[]</td><td class="desc"><small>(optional)</small></td></tr>
12239
12295
  <tr><td class="name">columnOrder</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
@@ -12421,7 +12477,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12421
12477
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
12422
12478
  <tbody>
12423
12479
  <tr><td class="name">el</td><td class="type">unknown | null</td><td class="desc"><small>(read-only)</small></td></tr>
12424
- <tr><td class="name">rowKey</td><td class="type">string | ((row: KPIRow) =&gt; unknown)</td><td class="desc"><small>(read-only)</small></td></tr>
12480
+ <tr><td class="name">rowKey</td><td class="type">string | ((row: KPIRow) =&gt; string | number)</td><td class="desc">The resolved row identity; see `KPIConfig.rowKey`. <small>(read-only)</small></td></tr>
12425
12481
  <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>
12426
12482
  <tr><td class="name">rows</td><td class="type">KPIRows</td><td class="desc"></td></tr>
12427
12483
  <tr><td class="name">tiles</td><td class="type">(): KPITileModel[]</td><td class="desc"></td></tr>
@@ -12479,14 +12535,14 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12479
12535
  <tbody>
12480
12536
  <tr><td class="name">rows</td><td class="type">KPIRow[]</td><td class="desc"><small>(optional)</small></td></tr>
12481
12537
  <tr><td class="name">grid</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
12482
- <tr><td class="name">rowKey</td><td class="type">string | ((row: KPIRow) =&gt; unknown)</td><td class="desc"><small>(optional)</small></td></tr>
12538
+ <tr><td class="name">rowKey</td><td class="type">string | ((row: KPIRow) =&gt; string | number)</td><td class="desc">Row identity (a field or fn, returning a string or number); default 'id'. Composite (`string[]`) keys are core-grid-only: a panel keys its own tile identity from one value, so there is nothing for an array to join into here (BACKLOG-0001669). <small>(optional)</small></td></tr>
12483
12539
  <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>
12484
12540
  <tr><td class="name">tiles</td><td class="type">KPITile[]</td><td class="desc"><small>(optional)</small></td></tr>
12485
12541
  <tr><td class="name">columns</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
12486
12542
  <tr><td class="name">ariaLabel</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
12487
12543
  <tr><td class="name">nullText</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
12488
12544
  <tr><td class="name">locale</td><td class="type">string</td><td class="desc">The default locale a clock tile formats in when the tile itself declares none (BACKLOG-0001640); falls back to the browser's default. No effect on a stat tile, which takes its own `format.locale`. <small>(optional)</small></td></tr>
12489
- <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>
12545
+ <tr><td class="name">tree</td><td class="type">KPITreeConfig | false</td><td class="desc">Arrange the tiles as a hierarchy; unset or `false` keeps the panel flat. Opt-in (BACKLOG-0001673): a dotted tile id is not a hierarchy until `tree` is set, and warns once while it is not. <small>(optional)</small></td></tr>
12490
12546
  <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>
12491
12547
  <tr><td class="name">onTileClick</td><td class="type">(event: KPIEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
12492
12548
  <tr><td class="name">onTileDblClick</td><td class="type">(event: KPIEvent) =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
@@ -12572,6 +12628,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12572
12628
  <tr><td class="name">format</td><td class="type">KPIFormat</td><td class="desc">Value formatting. <small>(optional)</small></td></tr>
12573
12629
  <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>
12574
12630
  <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>
12631
+ <tr><td class="name">delta</td><td class="type">'absolute' | 'relative' | 'both'</td><td class="desc">What the movement line prints against `baseline` (BACKLOG-0001673, F-FRED-G): `'absolute'` the difference alone, `'relative'` the percentage alone, `'both'` (the default, unchanged) both together. A rate series (4.10 vs 4.30) makes the percentage a percent-of-a-percent and meaningless, so `'absolute'` is how a host keeps the line without it. The arrow and its colour follow the sign of the difference either way. <small>(optional)</small></td></tr>
12575
12632
  <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>
12576
12633
  <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>
12577
12634
  <tr><td class="name">sparkline</td><td class="type">KPISparkline | string</td><td class="desc">A trend sparkline series. <small>(optional)</small></td></tr>
@@ -12610,13 +12667,14 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
12610
12667
  <tr><td class="name">delta</td><td class="type">number | null</td><td class="desc"></td></tr>
12611
12668
  <tr><td class="name">deltaPercent</td><td class="type">number | null</td><td class="desc"></td></tr>
12612
12669
  <tr><td class="name">deltaFormatted</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
12670
+ <tr><td class="name">deltaMode</td><td class="type">'absolute' | 'relative' | 'both'</td><td class="desc">What the movement line prints; see `KPIStatTile.delta`. Always present once `baseline` is. <small>(optional)</small></td></tr>
12613
12671
  <tr><td class="name">count</td><td class="type">number</td><td class="desc"></td></tr>
12614
12672
  <tr><td class="name">sparkline</td><td class="type">number[] | null</td><td class="desc"></td></tr>
12615
12673
  </tbody>
12616
12674
  </table>
12617
12675
  </div>
12618
12676
  <h3 id="type-KPITreeConfig">KPITreeConfig</h3>
12619
- <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>
12677
+ <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. `tree` is an opt-in (BACKLOG-0001673): omitted or `false`, the panel is flat whatever its tile ids look like, and a dotted id seen with `tree` unset is reported once rather than silently turned into a hierarchy. Given any object (`{}` included), 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 even with `tree` set. A tile's `field` is never a source: a dot there already means a nested object property.</p>
12620
12678
  <div class="table-wrap">
12621
12679
  <table>
12622
12680
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
@@ -14848,7 +14906,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
14848
14906
  <!-- END GENERATED TYPE REFERENCE -->
14849
14907
 
14850
14908
  <footer>
14851
- Lattice Grid 1.66.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
14909
+ Lattice Grid 1.68.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
14852
14910
  This document describes the behaviour of the shipped library. Where this guide and the code
14853
14911
  disagree, the code wins: please <a href="https://www.latticegrid.dev">tell us</a>.
14854
14912
  </footer>
@@ -437,7 +437,7 @@
437
437
  <div class="shell">
438
438
  <aside class="rail">
439
439
  <p class="rail__brand">Lattice Grid</p>
440
- <p class="rail__sub">Developer guide · v1.66.0</p>
440
+ <p class="rail__sub">Developer guide · v1.68.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
@@ -554,7 +554,7 @@
554
554
  <a href="API.html">reference tables</a> are the shorter version for when you already know.
555
555
  </p>
556
556
  <p class="chips">
557
- <span class="chip">Version 1.66.0</span>
557
+ <span class="chip">Version 1.68.0</span>
558
558
  <span class="chip">Zero dependencies</span>
559
559
  <span class="chip">No build step</span>
560
560
  </p>
@@ -1492,7 +1492,7 @@ off(); <span class="cmt">// every subscrip
1492
1492
  </p>
1493
1493
  <div class="example">
1494
1494
  <p class="example__label">Which version am I running?</p>
1495
- <pre><code>grid.getVersion(); <span class="cmt">// '1.66.0'</span>
1495
+ <pre><code>grid.getVersion(); <span class="cmt">// '1.68.0'</span>
1496
1496
  LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid to hand</span></code></pre>
1497
1497
  </div>
1498
1498
  <p class="lead-in">
@@ -9035,7 +9035,7 @@ return (`proposals ${proposals.join(',')} | diff ${result.diff.map((d) =&gt; `ol
9035
9035
 
9036
9036
  <h3 id="kpi-config-examples">KPI config keys, executed</h3>
9037
9037
  <p class="lead-in">
9038
- Three examples covering the eleven KPI config keys not already demonstrated elsewhere in this
9038
+ Four examples covering the twelve KPI config keys not already demonstrated elsewhere in this
9039
9039
  guide.
9040
9040
  </p>
9041
9041
 
@@ -9145,6 +9145,41 @@ return (`${el.querySelectorAll('.lat-kpi__node')[0].getAttribute('aria-label')}
9145
9145
  </p>
9146
9146
  </div>
9147
9147
 
9148
+ <div class="example">
9149
+ <p class="example__label">What the movement line prints against a baseline: delta, executed</p>
9150
+ <pre data-run="js" data-expect="both:▼ -0.2 (-4.7%) | absolute:▼ -0.2 | relative:▼ -4.7%" data-covers="config:delta"><code>const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
9151
+ const { createKPI } = await import('../packages/modules/kpi/index.js');
9152
+
9153
+ const dom = createTestDom({});
9154
+ const el = dom.document.createElement('div');
9155
+ // A rate series (4.10 vs a 4.30 baseline a year earlier): the percentage of a
9156
+ // rate is meaningless, so a host names 'absolute' to keep the line without it.
9157
+ const kpi = createKPI(el, {
9158
+ rows: [{ id: 1, rate: 4.10 }], rowKey: 'id',
9159
+ tiles: [
9160
+ { id: 'both', label: 'Default', aggregation: 'max', field: 'rate',
9161
+ baseline: 4.30, format: { decimals: 1 } },
9162
+ { id: 'absolute', label: 'Difference alone', aggregation: 'max', field: 'rate',
9163
+ baseline: 4.30, format: { decimals: 1 }, delta: 'absolute' },
9164
+ { id: 'relative', label: 'Percentage alone', aggregation: 'max', field: 'rate',
9165
+ baseline: 4.30, format: { decimals: 1 }, delta: 'relative' },
9166
+ ],
9167
+ });
9168
+ const lines = ['both', 'absolute', 'relative'].map((id) =&gt; {
9169
+ const fig = [...el.querySelectorAll('.lat-kpi__tile')].find((f) =&gt; f.getAttribute('aria-label').startsWith(
9170
+ id === 'both' ? 'Default' : (id === 'absolute' ? 'Difference' : 'Percentage')));
9171
+ return `${id}:${fig.querySelector('.lat-kpi__delta').textContent}`;
9172
+ });
9173
+ return lines.join(' | ');
9174
+ </code></pre>
9175
+ <p>
9176
+ <code>delta: 'absolute' | 'relative' | 'both'</code> (default <code>'both'</code>, unchanged)
9177
+ chooses what a tile's movement line prints against its <code>baseline</code> &mdash; the
9178
+ difference alone, the percentage alone, or both together, exactly as before this option
9179
+ existed. The arrow and its colour follow the sign of the difference in every mode.
9180
+ </p>
9181
+ </div>
9182
+
9148
9183
  <h3 id="route-options-examples">Data Router route options, executed</h3>
9149
9184
  <div class="example">
9150
9185
  <p class="example__label">A per-route filter and transform, executed</p>
@@ -9651,7 +9686,7 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'<
9651
9686
 
9652
9687
  <footer>
9653
9688
  <p>
9654
- Lattice Grid 1.66.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
9689
+ Lattice Grid 1.68.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
9655
9690
  Written against the shipped source. Where this guide and the code disagree, the code wins,
9656
9691
  please <a href="https://www.latticegrid.dev">tell us</a>.
9657
9692
  </p>
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.66.0, type declarations
2
+ * Lattice Grid 1.68.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -2071,11 +2071,14 @@ export interface GridConfig {
2071
2071
  pagination?: PaginationConfig | boolean;
2072
2072
  locale?: string;
2073
2073
  /**
2074
- * Writing direction. Omit it, or say `'auto'`, to settle it from the
2075
- * element's own computed `dir` and then from `locale`: `ar`, `he`, `fa` and
2076
- * the rest resolve to `rtl`. In a right-to-left grid the logical alignments
2077
- * `start`/`end` mirror while the physical `left`/`right` do not (see
2078
- * {@link Align}).
2074
+ * Writing direction. An explicit `'ltr'` or `'rtl'` always wins. Omit it, or
2075
+ * say `'auto'`, to settle it from the mount or the nearest ancestor
2076
+ * carrying a `dir="ltr"`/`dir="rtl"` attribute, and only then from
2077
+ * `locale`: `ar`, `he`, `fa` and the rest resolve to `rtl`. A page-level
2078
+ * `dir="ltr"` therefore wins over an RTL locale — a host that wants RTL
2079
+ * inside an LTR page sets `direction: 'rtl'` explicitly. In a right-to-left
2080
+ * grid the logical alignments `start`/`end` mirror while the physical
2081
+ * `left`/`right` do not (see {@link Align}).
2079
2082
  */
2080
2083
  direction?: 'ltr' | 'rtl' | 'auto';
2081
2084
  /**
@@ -7555,6 +7558,12 @@ export interface ChartMeasure {
7555
7558
  type?: 'bar' | 'line' | 'area';
7556
7559
  /** Which axis it belongs to, on a combo chart. */
7557
7560
  axis?: 'left' | 'right';
7561
+ /**
7562
+ * The series label a combo's legend and axis titles use.
7563
+ * `label` is read too, as an undeclared alias, for a caller already using
7564
+ * it; `title` wins when both are given. Falls back to the measure column's
7565
+ * own `title`, then to `col`, when neither is set.
7566
+ */
7558
7567
  title?: string;
7559
7568
  }
7560
7569
 
@@ -7667,7 +7676,11 @@ export interface ChartTrend {
7667
7676
  * which on a dual-axis chart is what stops it being placed against the wrong
7668
7677
  * scale, and is written into the accessible table as a sentence — a vertical
7669
7678
  * marker and an event stating the position they sit at, because a screen-reader
7670
- * user needs where and when, not only that a marker exists.
7679
+ * user needs where and when, not only that a marker exists. An annotation the
7680
+ * chart cannot place — an x value it cannot resolve on the axis, or a band
7681
+ * whose edges are off-scale — is dropped with one console warning naming the
7682
+ * annotation's kind, the axis and the reason, rather than vanishing without a
7683
+ * trace.
7671
7684
  */
7672
7685
  export interface ChartAnnotation {
7673
7686
  /**