@toclocoinc/lattice-grid 1.57.0 → 1.58.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 (78) hide show
  1. package/README.md +1 -1
  2. package/docs/API.html +44 -4
  3. package/docs/api-detail.html +129 -4
  4. package/lattice-grid.d.ts +85 -1
  5. package/lattice-grid.esm.min.js +218 -37
  6. package/lattice-grid.min.cjs +218 -37
  7. package/lattice-grid.min.css +1 -1
  8. package/lattice-grid.min.js +218 -37
  9. package/modules/ai.esm.min.js +5 -4
  10. package/modules/ai.min.cjs +5 -4
  11. package/modules/ai.min.js +5 -4
  12. package/modules/angular.esm.min.js +2 -2
  13. package/modules/angular.min.cjs +2 -2
  14. package/modules/angular.min.js +2 -2
  15. package/modules/chart-alluvial.esm.min.js +1 -1
  16. package/modules/chart-arc.esm.min.js +1 -1
  17. package/modules/chart-bubblemap.esm.min.js +1 -1
  18. package/modules/chart-bump.esm.min.js +1 -1
  19. package/modules/chart-calendar.esm.min.js +1 -1
  20. package/modules/chart-decomposition.esm.min.js +1 -1
  21. package/modules/chart-diverging.esm.min.js +1 -1
  22. package/modules/chart-dumbbell.esm.min.js +1 -1
  23. package/modules/chart-fan.esm.min.js +1 -1
  24. package/modules/chart-hexbin.esm.min.js +1 -1
  25. package/modules/chart-hexmap.esm.min.js +1 -1
  26. package/modules/chart-icicle.esm.min.js +1 -1
  27. package/modules/chart-parallel.esm.min.js +1 -1
  28. package/modules/chart-ridgeline.esm.min.js +1 -1
  29. package/modules/chart-roc.esm.min.js +1 -1
  30. package/modules/chart-slope.esm.min.js +1 -1
  31. package/modules/chart-splom.esm.min.js +1 -1
  32. package/modules/chart-waffle.esm.min.js +1 -1
  33. package/modules/charts.esm.min.js +4 -4
  34. package/modules/charts.min.cjs +4 -4
  35. package/modules/charts.min.js +4 -4
  36. package/modules/data-router.esm.min.js +4 -4
  37. package/modules/data-router.min.cjs +4 -4
  38. package/modules/data-router.min.js +4 -4
  39. package/modules/devtools.esm.min.js +2 -2
  40. package/modules/devtools.min.cjs +2 -2
  41. package/modules/devtools.min.js +2 -2
  42. package/modules/dhtmlx-compat.esm.min.js +4 -4
  43. package/modules/dhtmlx-compat.min.cjs +4 -4
  44. package/modules/dhtmlx-compat.min.js +4 -4
  45. package/modules/gantt.esm.min.js +4 -4
  46. package/modules/gantt.min.cjs +4 -4
  47. package/modules/gantt.min.js +4 -4
  48. package/modules/htmx.esm.min.js +218 -37
  49. package/modules/htmx.min.cjs +218 -37
  50. package/modules/htmx.min.js +218 -37
  51. package/modules/kanban.esm.min.js +5 -6
  52. package/modules/kanban.min.cjs +5 -6
  53. package/modules/kanban.min.js +5 -6
  54. package/modules/kpi.esm.min.js +41 -8
  55. package/modules/kpi.min.cjs +41 -8
  56. package/modules/kpi.min.js +41 -8
  57. package/modules/layout.esm.min.js +4 -4
  58. package/modules/layout.min.cjs +4 -4
  59. package/modules/layout.min.js +4 -4
  60. package/modules/mock-socket.esm.min.js +2 -2
  61. package/modules/mock-socket.min.cjs +2 -2
  62. package/modules/mock-socket.min.js +2 -2
  63. package/modules/react.esm.min.js +2 -2
  64. package/modules/react.min.cjs +2 -2
  65. package/modules/react.min.js +2 -2
  66. package/modules/svelte.esm.min.js +2 -2
  67. package/modules/svelte.min.cjs +2 -2
  68. package/modules/svelte.min.js +2 -2
  69. package/modules/tabs.esm.min.js +4 -4
  70. package/modules/tabs.min.cjs +4 -4
  71. package/modules/tabs.min.js +4 -4
  72. package/modules/vue.esm.min.js +2 -2
  73. package/modules/vue.min.cjs +2 -2
  74. package/modules/vue.min.js +2 -2
  75. package/modules/webcomponent.esm.min.js +218 -37
  76. package/modules/webcomponent.min.cjs +218 -37
  77. package/modules/webcomponent.min.js +218 -37
  78. package/package.json +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  dependencies, no build step required. Optional adapters for React, Vue, Svelte
5
5
  and Web Components ship alongside it.
6
6
 
7
- Version 1.57.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
7
+ Version 1.58.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
8
8
 
9
9
  ---
10
10
 
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.57.0</p>
363
+ <p class="rail__sub">API reference · v1.58.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.57.0</span>
446
+ <span class="chip">Version 1.58.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>
@@ -889,6 +889,8 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
889
889
  <tr><td class="name">pinnedTopRows</td><td class="type">object[]</td><td class="dflt">, </td><td class="desc">Rows held above the scrolling body. Rendered through the ordinary column pipeline, but not part of the data: not counted, sorted, filtered, grouped, selectable or exported. See <a href="api-detail.html#pinned-rows">Pinned rows</a>.</td></tr>
890
890
  <tr><td class="name">pinnedBottomRows</td><td class="type">object[]</td><td class="dflt">, </td><td class="desc">As <code>pinnedTopRows</code>, held below the body instead. Sits under the grand total when both are shown.</td></tr>
891
891
  <tr><td class="name">fullWidth</td><td class="type">{ when, render }</td><td class="dflt">, </td><td class="desc">Draw matching rows as one band across every column instead of dividing them into columns, a section banner, a note, a “load more” affordance. <code>when(row)</code> picks them, <code>render(params)</code> fills them. Still ordinary data rows in every other respect. See <a href="api-detail.html#full-width-rows">Full-width rows</a>.</td></tr>
892
+ <tr><td class="name">groupRenderer</td><td class="type">(params) =&gt; string | Node | void</td><td class="dflt">, </td><td class="desc">Draw the group row yourself &mdash; a section header with a chevron, a rollup, a count, a progress bar &mdash; instead of the grid's expander-and-label. The row is drawn as one band across every column and no ordinary cells are mounted underneath it. Return an HTML string, a node, or write into <code>params.element</code>. A string <strong>is</strong> inserted as markup here, unlike <code>fullWidth.render</code>, because a group heading is synthesised by the grid and has no data row: the string can only be your own template, the same contract the board's <code>cardRenderer</code> has. The renderer is handed the group key, the grouped column id, the value, the level, the expanded state, <code>leafCount</code>, the group's <code>totals</code> and <code>leaves()</code> for the rows themselves. Mark any element in your markup <code>data-lat-group-toggle</code> to make it expand and collapse the group. See <a href="api-detail.html#group-row-renderer">Group rows you draw yourself</a>.</td></tr>
893
+ <tr><td class="name">groupDefaultExpanded</td><td class="type">boolean | number | (group) =&gt; boolean</td><td class="dflt">, </td><td class="desc">Which groups start open before anyone has touched one. <code>true</code> (the default) opens every group, <code>false</code> closes every group, a number opens the first N levels (<code>0</code> closes everything, a negative opens every level), and a predicate answers per group &mdash; the current sprint open while the rest start closed. It is handed <code>{ key, column, value, level, path }</code>. Only ever consulted for a group nobody has expanded or collapsed: once the user or your code decides, that decision stands. See <a href="api-detail.html#group-row-renderer">Group rows you draw yourself</a>.</td></tr>
892
894
  <tr><td class="name">groupFooter</td><td class="type">boolean</td><td class="dflt">false</td><td class="desc">A closing total row per group.</td></tr>
893
895
  <tr><td class="name">totalFilteredOnly</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc">Totals reduce the filtered set. <code>false</code> totals the whole dataset, group totals included. See <a href="api-detail.html#total-filtered-only">Grouping, totals and pivot</a>.</td></tr>
894
896
  <tr><td class="name">totalOnlyChangedColumns</td><td class="type">boolean</td><td class="dflt">false</td><td class="desc">Reduce only the totalled columns an edit actually changed. Off by default, it asserts that each total depends on nothing but its own column. See <a href="api-detail.html#grouping">Grouping, totals and pivot</a>.</td></tr>
@@ -1130,7 +1132,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
1130
1132
  <table>
1131
1133
  <thead><tr><th>Member</th><th>Returns</th><th>Description</th></tr></thead>
1132
1134
  <tbody>
1133
- <tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.57.0'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
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>
1134
1136
  <tr><td class="sig">get(key)</td><td class="type">unknown</td><td class="desc">Read any configuration key.</td></tr>
1135
1137
  <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>
1136
1138
  <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>
@@ -9407,6 +9409,8 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9407
9409
  <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>
9408
9410
  <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>
9409
9411
  <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>
9413
+ <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>
9410
9414
  <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>
9411
9415
  <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>
9412
9416
  <tr><td class="name">pinnedBottomRows</td><td class="type">unknown[]</td><td class="desc">Rows pinned below the scrolling body. As `pinnedTopRows`, at the other edge. <small>(optional)</small></td></tr>
@@ -9542,6 +9546,41 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
9542
9546
  </tbody>
9543
9547
  </table>
9544
9548
  </div>
9549
+ <h3 id="type-GroupInfo">GroupInfo</h3>
9550
+ <p class="section-note">Which group `groupDefaultExpanded` is being asked about.</p>
9551
+ <div class="table-wrap">
9552
+ <table>
9553
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9554
+ <tbody>
9555
+ <tr><td class="name">key</td><td class="type">string</td><td class="desc">The group's key, the same string `Row.key` carries and `rows.expand` takes.</td></tr>
9556
+ <tr><td class="name">column</td><td class="type">string</td><td class="desc">The id of the column this level groups on. <small>(optional)</small></td></tr>
9557
+ <tr><td class="name">value</td><td class="type">unknown</td><td class="desc">The value this group stands for. <small>(optional)</small></td></tr>
9558
+ <tr><td class="name">level</td><td class="type">number</td><td class="desc">Depth of the group. Zero is the outermost level. <small>(optional)</small></td></tr>
9559
+ <tr><td class="name">path</td><td class="type">string[]</td><td class="desc">The group path from the root down to this group. <small>(optional)</small></td></tr>
9560
+ </tbody>
9561
+ </table>
9562
+ </div>
9563
+ <h3 id="type-GroupRowParams">GroupRowParams</h3>
9564
+ <p class="section-note">What `groupRenderer` is handed.</p>
9565
+ <div class="table-wrap">
9566
+ <table>
9567
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
9568
+ <tbody>
9569
+ <tr><td class="name">row</td><td class="type">Row</td><td class="desc">The group row itself.</td></tr>
9570
+ <tr><td class="name">key</td><td class="type">string</td><td class="desc">The group's key, as `rows.expand`/`rows.collapse` take it.</td></tr>
9571
+ <tr><td class="name">column</td><td class="type">string</td><td class="desc">The id of the column this level groups on. <small>(optional)</small></td></tr>
9572
+ <tr><td class="name">value</td><td class="type">unknown</td><td class="desc">The value this group stands for.</td></tr>
9573
+ <tr><td class="name">level</td><td class="type">number</td><td class="desc">Depth of the group. Zero is the outermost level.</td></tr>
9574
+ <tr><td class="name">expanded</td><td class="type">boolean</td><td class="desc">Whether the group is currently open.</td></tr>
9575
+ <tr><td class="name">leafCount</td><td class="type">number</td><td class="desc">How many records sit beneath it, at any depth.</td></tr>
9576
+ <tr><td class="name">totals</td><td class="type">Record&lt;string, unknown&gt;</td><td class="desc">The group's own reductions, by column id — whatever `total` asked for. <small>(optional)</small></td></tr>
9577
+ <tr><td class="name">leaves</td><td class="type">(): Row[]</td><td class="desc">The rows beneath this group, computed when you call it. A function rather than an array because a group is unbounded and this runs per paint: a host that only needs the count should read `leafCount` and never call this.</td></tr>
9578
+ <tr><td class="name">toggle</td><td class="type">(): void</td><td class="desc">Expand the group if it is closed, collapse it if it is open.</td></tr>
9579
+ <tr><td class="name">grid</td><td class="type">Grid</td><td class="desc"></td></tr>
9580
+ <tr><td class="name">element</td><td class="type">HTMLElement</td><td class="desc">The element to fill. Write into it directly, or return content instead.</td></tr>
9581
+ </tbody>
9582
+ </table>
9583
+ </div>
9545
9584
  <h3 id="type-Heteroscedasticity">Heteroscedasticity</h3>
9546
9585
  <p class="section-note">The Breusch–Pagan heteroscedasticity test result.</p>
9547
9586
  <div class="table-wrap">
@@ -10613,6 +10652,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
10613
10652
  <tr><td class="name">refresh</td><td class="type">(opts?: { rows?: string[]; columns?: string[]; force?: boolean }): void</td><td class="desc"></td></tr>
10614
10653
  <tr><td class="name">move</td><td class="type">(key: string, to: number): { moved: boolean; from: number; to: number; reason?: string }</td><td class="desc">Move a row to another position in the data. Refuses, with a reason, while a sort, filter or grouping is active.</td></tr>
10615
10654
  <tr><td class="name">groupHeadings</td><td class="type">(index: number): Row[]</td><td class="desc">The group headings enclosing a display row, outermost first. Empty when the grid is not grouped.</td></tr>
10655
+ <tr><td class="name">leavesOf</td><td class="type">(key: string): Row[]</td><td class="desc">The leaf rows beneath a group heading: the members it counts in `leafCount`, as rows, so you can roll up a field the grid was never told to total. Filtered members in display order. Computed per call, so call it when you draw a group row rather than in a loop over every row.</td></tr>
10616
10656
  <tr><td class="name">expand</td><td class="type">(key: string, deep?: boolean): void</td><td class="desc"></td></tr>
10617
10657
  <tr><td class="name">collapse</td><td class="type">(key: string): void</td><td class="desc"></td></tr>
10618
10658
  <tr><td class="name">expandAll</td><td class="type">(): void</td><td class="desc"></td></tr>
@@ -11190,7 +11230,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
11190
11230
  <!-- END GENERATED TYPE REFERENCE -->
11191
11231
 
11192
11232
  <footer>
11193
- Lattice Grid 1.57.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
11233
+ Lattice Grid 1.58.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
11194
11234
  This document describes the behaviour of the shipped library. Where this guide and the code
11195
11235
  disagree, the code wins: please <a href="https://www.latticegrid.dev">tell us</a>.
11196
11236
  </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.57.0</p>
440
+ <p class="rail__sub">Developer guide · v1.58.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
@@ -553,7 +553,7 @@
553
553
  <a href="API.html">reference tables</a> are the shorter version for when you already know.
554
554
  </p>
555
555
  <p class="chips">
556
- <span class="chip">Version 1.57.0</span>
556
+ <span class="chip">Version 1.58.0</span>
557
557
  <span class="chip">Zero dependencies</span>
558
558
  <span class="chip">No build step</span>
559
559
  </p>
@@ -1278,7 +1278,7 @@ off(); <span class="cmt">// every subscrip
1278
1278
  </p>
1279
1279
  <div class="example">
1280
1280
  <p class="example__label">Which version am I running?</p>
1281
- <pre><code>grid.getVersion(); <span class="cmt">// '1.57.0'</span>
1281
+ <pre><code>grid.getVersion(); <span class="cmt">// '1.58.0'</span>
1282
1282
  LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid to hand</span></code></pre>
1283
1283
  </div>
1284
1284
  <p class="lead-in">
@@ -2684,6 +2684,131 @@ grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span
2684
2684
  technology as one <code>role="columnheader"</code> cell with an <code>aria-colspan</code>.</p>
2685
2685
  </div>
2686
2686
 
2687
+ <h2 id="group-row-renderer">Group rows you draw yourself</h2>
2688
+ <p class="lead-in">
2689
+ The grid's own group row is an expander, a label and a count. When the heading has to carry
2690
+ more than that &mdash; a sprint section with a chevron, the section name, the points summed
2691
+ across it, a done/total count and a progress bar &mdash; <code>groupRenderer</code> hands you
2692
+ the whole row. It is drawn as one band across every column, over the pinned regions, and no
2693
+ ordinary cells are mounted underneath it.
2694
+ </p>
2695
+
2696
+ <div class="example">
2697
+ <p class="example__label">A section header with a rollup the grid was never told to compute</p>
2698
+ <pre><code>createGrid(element, {
2699
+ columns,
2700
+ rows,
2701
+ groupRenderer: ({ value, leafCount, expanded, leaves }) =&gt; {
2702
+ <span class="cmt">// `leaves()` is the group's own rows. Roll up whatever you like.</span>
2703
+ <span class="kw">const</span> rows = leaves();
2704
+ <span class="kw">const</span> points = rows.reduce((sum, r) =&gt; sum + r.data.points, 0);
2705
+ <span class="kw">const</span> done = rows.filter((r) =&gt; r.data.done).length;
2706
+ <span class="kw">const</span> pct = Math.round((done / leafCount) * 100);
2707
+ <span class="kw">return</span> `&lt;span data-lat-group-toggle&gt;${expanded ? '▾' : '▸'}&lt;/span&gt;
2708
+ &lt;b&gt;${value}&lt;/b&gt; ${points} pts · ${done}/${leafCount}
2709
+ &lt;progress value="${pct}" max="100"&gt;&lt;/progress&gt;`;
2710
+ },
2711
+ });</code></pre>
2712
+ </div>
2713
+
2714
+ <h3 id="group-row-renderer-params">What the renderer is given</h3>
2715
+ <div class="table-wrap">
2716
+ <table>
2717
+ <thead><tr><th>Field</th><th>What it is</th></tr></thead>
2718
+ <tbody>
2719
+ <tr><td class="name">key</td><td class="desc">The group's key &mdash; the same string <code>rows.expand()</code> and <code>rows.collapse()</code> take, and the one <code>group:toggled</code> carries.</td></tr>
2720
+ <tr><td class="name">column</td><td class="desc">The id of the column <em>this level</em> groups on. It is stamped per rebuild from the grouping, not derived from the row's depth, so it stays correct when an outer grouping is removed and this level becomes the outermost.</td></tr>
2721
+ <tr><td class="name">value</td><td class="desc">The value this group stands for.</td></tr>
2722
+ <tr><td class="name">level</td><td class="desc">Depth of the group. Zero is the outermost.</td></tr>
2723
+ <tr><td class="name">expanded</td><td class="desc">Whether the group is open. Draw your chevron from this; the row is re-rendered when it changes.</td></tr>
2724
+ <tr><td class="name">leafCount</td><td class="desc">How many records sit beneath the heading, at any depth. Read this when the size is all you need.</td></tr>
2725
+ <tr><td class="name">totals</td><td class="desc">The group's own reductions by column id &mdash; whatever <code>total</code> asked for. Unaffected by drawing the row.</td></tr>
2726
+ <tr><td class="name">leaves()</td><td class="desc">The rows beneath the heading, computed <em>when you call it</em>. The members the filters left, in display order, so a rollup agrees with the rows drawn below. Also available on its own as <code>grid.rows.leavesOf(key)</code>.</td></tr>
2727
+ <tr><td class="name">toggle()</td><td class="desc">Expand the group if it is closed, collapse it if it is open.</td></tr>
2728
+ <tr><td class="name">grid, row, element</td><td class="desc">The grid, the group row model, and the element to fill if you would rather write into it than return anything.</td></tr>
2729
+ </tbody>
2730
+ </table>
2731
+ </div>
2732
+
2733
+ <div class="why">
2734
+ <p><strong>A string here is markup, and in <code>fullWidth.render</code> it is text.</strong>
2735
+ That difference is deliberate, and it is the difference between the two features. A
2736
+ <a href="#full-width-rows">full-width row</a> renders one of <em>your data</em> rows, and
2737
+ §8.9 keeps host markup behind <code>allowUnsafeTemplates</code> everywhere a data value
2738
+ reaches the page. A group heading has no data row at all &mdash; the grid synthesised it from
2739
+ the grouping &mdash; so its string can only be a template you wrote in your own source. That
2740
+ is the same footing the board's <code>cardRenderer</code> already stands on, and the header
2741
+ this exists for (a chevron, a bar) is unwritable without it. If you interpolate a value that
2742
+ came from outside your application, escape it yourself, or build a node and return that
2743
+ instead.</p>
2744
+ <p><strong>Your chevron is yours to wire.</strong> The grid's own expander binds a click
2745
+ handler to the button it built; a string cannot carry a handler, so that chevron would be
2746
+ dead. Any element in your markup carrying <code>data-lat-group-toggle</code> expands or
2747
+ collapses the group it sits in, and <code>toggle()</code> does the same from a node you built
2748
+ yourself.</p>
2749
+ <p><strong><code>leaves()</code> is a function on purpose.</strong> A group is unbounded and
2750
+ the renderer runs as rows are painted. Handing every group an array of its members would cost
2751
+ a hundred thousand rows on a heading nobody looked at. Call it when you need the rows; read
2752
+ <code>leafCount</code> when you need the size.</p>
2753
+ </div>
2754
+
2755
+ <h3 id="group-default-expanded">Which groups start open</h3>
2756
+ <p class="lead-in">
2757
+ <code>groupDefaultExpanded</code> decides the state of a group <em>before anyone has touched
2758
+ it</em>: <code>true</code> (the default) opens them all, <code>false</code> closes them all, a
2759
+ number opens the first N levels, and a predicate answers per group. Once the user or your own
2760
+ code expands or collapses a group, that decision stands &mdash; the default is not consulted
2761
+ for it again, so a group cannot spring shut under the user on the next rebuild.
2762
+ </p>
2763
+
2764
+ <div class="example">
2765
+ <p class="example__label">The current sprint open, everything else closed, executed</p>
2766
+ <pre data-run="js" data-expect="Backlog 0/1 8pts closed | Sprint 12 1/2 8pts open" data-covers="config:groupRenderer config:groupDefaultExpanded"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
2767
+
2768
+ <span class="cmt">// The renderer the grid calls for each group row. Called here directly too,</span>
2769
+ <span class="cmt">// with the params the grid builds, so this block shows its actual output.</span>
2770
+ <span class="kw">const</span> groupRenderer = (p) =&gt; {
2771
+ <span class="kw">const</span> rows = p.leaves();
2772
+ <span class="kw">const</span> done = rows.filter((r) =&gt; r.data.done).length;
2773
+ <span class="kw">const</span> points = rows.reduce((sum, r) =&gt; sum + r.data.points, 0);
2774
+ <span class="kw">return</span> `${p.value} ${done}/${p.leafCount} ${points}pts ${p.expanded ? 'open' : 'closed'}`;
2775
+ };
2776
+
2777
+ <span class="kw">const</span> grid = createHeadlessGrid({
2778
+ rowKey: 'id',
2779
+ columns: [{ field: 'section' }, { field: 'points', type: 'number', total: 'sum' }, { field: 'done' }],
2780
+ rows: [
2781
+ { id: 1, section: 'Sprint 12', points: 3, done: <span class="kw">true</span> },
2782
+ { id: 2, section: 'Sprint 12', points: 5, done: <span class="kw">false</span> },
2783
+ { id: 3, section: 'Backlog', points: 8, done: <span class="kw">false</span> },
2784
+ ],
2785
+ groupRenderer,
2786
+ <span class="cmt">// Per group, not merely per depth: only the current sprint starts open.</span>
2787
+ groupDefaultExpanded: (group) =&gt; group.value === 'Sprint 12',
2788
+ });
2789
+ grid.columns.group(['section']);
2790
+
2791
+ <span class="kw">const</span> out = [];
2792
+ <span class="kw">for</span> (<span class="kw">let</span> i = 0; i &lt; grid.rows.count(); i++) {
2793
+ <span class="kw">const</span> row = grid.rows.get(i);
2794
+ <span class="kw">if</span> (!row.group) <span class="kw">continue</span>;
2795
+ out.push(groupRenderer({
2796
+ row, key: row.key, column: row.groupColumn, value: row.groupValue,
2797
+ level: row.level, expanded: row.expanded, leafCount: row.leafCount,
2798
+ totals: row.totals, leaves: () =&gt; grid.rows.leavesOf(row.key),
2799
+ toggle: () =&gt; {}, grid, element: <span class="kw">null</span>,
2800
+ }));
2801
+ }
2802
+ grid.destroy();
2803
+ <span class="kw">return</span> out.join(' | ');</code></pre>
2804
+ </div>
2805
+
2806
+ <p class="lead-in">
2807
+ <code>grid.rows.leavesOf(key)</code> is the same reach on its own, for a breadcrumb, a
2808
+ side panel or a rollup computed away from the row: it returns the leaf rows beneath a group
2809
+ heading, which is what <code>leafCount</code> counts.
2810
+ </p>
2811
+
2687
2812
  <h2 id="row-reorder">Row reorder</h2>
2688
2813
  <p class="lead-in">
2689
2814
  <code>rowReorder: true</code> puts a drag handle in the first visible column and lets a user
@@ -7901,7 +8026,7 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'<
7901
8026
 
7902
8027
  <footer>
7903
8028
  <p>
7904
- Lattice Grid 1.57.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
8029
+ Lattice Grid 1.58.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
7905
8030
  Written against the shipped source. Where this guide and the code disagree, the code wins,
7906
8031
  please <a href="https://www.latticegrid.dev">tell us</a>.
7907
8032
  </p>
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.57.0, type declarations
2
+ * Lattice Grid 1.58.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -2313,6 +2313,36 @@ export interface GridConfig {
2313
2313
  sharedMemory?: boolean;
2314
2314
  /** A totals line at the foot of each group as well as the grid. */
2315
2315
  groupFooter?: boolean;
2316
+ /**
2317
+ * Draw the group row yourself.
2318
+ *
2319
+ * The grid's own group row is an expander, a label and a count. A host that
2320
+ * needs more — a section header with a points rollup, a done/total count and
2321
+ * a progress bar — supplies this instead, and owns the whole row: it is drawn
2322
+ * as one band across every column, and no ordinary cells are mounted for it.
2323
+ *
2324
+ * Return an HTML string, or a node, or write into `params.element` and return
2325
+ * nothing. Unlike `fullWidth.render`, a string here **is** inserted as markup,
2326
+ * on the same footing as the board's `cardRenderer`: this is your own template
2327
+ * for a row the grid synthesised, not a value out of your data.
2328
+ *
2329
+ * The chevron is yours to draw and yours to wire: give any element in your
2330
+ * markup `data-lat-group-toggle` and a click on it expands or collapses the
2331
+ * group, or call `params.toggle()` from a node you built yourself.
2332
+ */
2333
+ groupRenderer?(params: GroupRowParams): string | Node | void;
2334
+ /**
2335
+ * Which groups start expanded, before anyone has opened or closed one.
2336
+ *
2337
+ * `true` (the default) opens every group, `false` closes every group, a
2338
+ * number opens the first N levels (`0` closes everything, a negative opens
2339
+ * every level), and a predicate answers per group — the current sprint's
2340
+ * section open while the rest start closed.
2341
+ *
2342
+ * Only ever consulted for a group nobody has touched: once the user or your
2343
+ * code expands or collapses one, that decision stands.
2344
+ */
2345
+ groupDefaultExpanded?: boolean | number | ((group: GroupInfo) => boolean);
2316
2346
  /**
2317
2347
  * Where the grand total goes.
2318
2348
  *
@@ -4634,6 +4664,13 @@ export interface RowsApi {
4634
4664
  * grid is not grouped.
4635
4665
  */
4636
4666
  groupHeadings(index: number): Row[];
4667
+ /**
4668
+ * The leaf rows beneath a group heading: the members it counts in
4669
+ * `leafCount`, as rows, so you can roll up a field the grid was never told
4670
+ * to total. Filtered members in display order. Computed per call, so call it
4671
+ * when you draw a group row rather than in a loop over every row.
4672
+ */
4673
+ leavesOf(key: string): Row[];
4637
4674
  expand(key: string, deep?: boolean): void;
4638
4675
  collapse(key: string): void;
4639
4676
  expandAll(): void;
@@ -5931,6 +5968,53 @@ export interface CellMenuParams {
5931
5968
  grid: Grid;
5932
5969
  }
5933
5970
 
5971
+ /** Which group `groupDefaultExpanded` is being asked about. */
5972
+ export interface GroupInfo {
5973
+ /** The group's key, the same string `Row.key` carries and `rows.expand` takes. */
5974
+ key: string;
5975
+ /** The id of the column this level groups on. */
5976
+ column?: string;
5977
+ /** The value this group stands for. */
5978
+ value?: unknown;
5979
+ /** Depth of the group. Zero is the outermost level. */
5980
+ level?: number;
5981
+ /** The group path from the root down to this group. */
5982
+ path?: string[];
5983
+ }
5984
+
5985
+ /** What `groupRenderer` is handed. */
5986
+ export interface GroupRowParams {
5987
+ /** The group row itself. */
5988
+ row: Row;
5989
+ /** The group's key, as `rows.expand`/`rows.collapse` take it. */
5990
+ key: string;
5991
+ /** The id of the column this level groups on. */
5992
+ column?: string;
5993
+ /** The value this group stands for. */
5994
+ value: unknown;
5995
+ /** Depth of the group. Zero is the outermost level. */
5996
+ level: number;
5997
+ /** Whether the group is currently open. */
5998
+ expanded: boolean;
5999
+ /** How many records sit beneath it, at any depth. */
6000
+ leafCount: number;
6001
+ /** The group's own reductions, by column id — whatever `total` asked for. */
6002
+ totals?: Record<string, unknown>;
6003
+ /**
6004
+ * The rows beneath this group, computed when you call it.
6005
+ *
6006
+ * A function rather than an array because a group is unbounded and this runs
6007
+ * per paint: a host that only needs the count should read `leafCount` and
6008
+ * never call this.
6009
+ */
6010
+ leaves(): Row[];
6011
+ /** Expand the group if it is closed, collapse it if it is open. */
6012
+ toggle(): void;
6013
+ grid: Grid;
6014
+ /** The element to fill. Write into it directly, or return content instead. */
6015
+ element: HTMLElement;
6016
+ }
6017
+
5934
6018
  /** What `fullWidth.render` is handed. */
5935
6019
  export interface FullWidthParams {
5936
6020
  row: Row;