@toclocoinc/lattice-grid 1.63.3 → 1.65.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 +4 -2
  2. package/docs/API.html +408 -25
  3. package/docs/CHART-CODES.md +24 -0
  4. package/docs/api-detail.html +115 -5
  5. package/lattice-grid.d.ts +333 -205
  6. package/lattice-grid.esm.min.js +155 -96
  7. package/lattice-grid.min.cjs +155 -96
  8. package/lattice-grid.min.js +155 -96
  9. package/modules/ai.d.ts +10 -13
  10. package/modules/ai.esm.min.js +3 -6
  11. package/modules/ai.min.cjs +3 -6
  12. package/modules/ai.min.js +3 -6
  13. package/modules/angular.d.ts +2 -2
  14. package/modules/angular.esm.min.js +433 -10
  15. package/modules/angular.min.cjs +433 -10
  16. package/modules/angular.min.js +433 -10
  17. package/modules/chart-alluvial.d.ts +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +29 -0
  66. package/modules/chart-markermap.esm.min.js +313 -0
  67. package/modules/chart-markermap.min.cjs +317 -0
  68. package/modules/chart-markermap.min.js +317 -0
  69. package/modules/chart-parallel.d.ts +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +2 -2
  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 +4 -5
  94. package/modules/charts.esm.min.js +395 -33
  95. package/modules/charts.min.cjs +395 -33
  96. package/modules/charts.min.js +395 -33
  97. package/modules/data-router.d.ts +5 -6
  98. package/modules/data-router.esm.min.js +3 -6
  99. package/modules/data-router.min.cjs +3 -6
  100. package/modules/data-router.min.js +3 -6
  101. package/modules/devtools.d.ts +1 -1
  102. package/modules/devtools.esm.min.js +1 -4
  103. package/modules/devtools.min.cjs +1 -4
  104. package/modules/devtools.min.js +1 -4
  105. package/modules/dhtmlx-compat.d.ts +1 -1
  106. package/modules/dhtmlx-compat.esm.min.js +3 -6
  107. package/modules/dhtmlx-compat.min.cjs +3 -6
  108. package/modules/dhtmlx-compat.min.js +3 -6
  109. package/modules/gantt.d.ts +37 -43
  110. package/modules/gantt.esm.min.js +3 -6
  111. package/modules/gantt.min.cjs +3 -6
  112. package/modules/gantt.min.js +3 -6
  113. package/modules/geo-europe-nuts.d.ts +2 -2
  114. package/modules/geo-europe-nuts.esm.min.js +1 -1
  115. package/modules/geo-uk.d.ts +2 -2
  116. package/modules/geo-uk.esm.min.js +1 -1
  117. package/modules/geo-us-states.d.ts +2 -2
  118. package/modules/geo-us-states.esm.min.js +1 -1
  119. package/modules/geo-world-110m.d.ts +2 -2
  120. package/modules/geo-world-110m.esm.min.js +1 -1
  121. package/modules/geo-world-50m.d.ts +2 -2
  122. package/modules/geo-world-50m.esm.min.js +1 -1
  123. package/modules/htmx.d.ts +2 -2
  124. package/modules/htmx.esm.min.js +155 -96
  125. package/modules/htmx.min.cjs +155 -96
  126. package/modules/htmx.min.js +155 -96
  127. package/modules/kanban.d.ts +14 -14
  128. package/modules/kanban.esm.min.js +3 -6
  129. package/modules/kanban.min.cjs +3 -6
  130. package/modules/kanban.min.js +3 -6
  131. package/modules/kpi.d.ts +57 -4
  132. package/modules/kpi.esm.min.js +202 -10
  133. package/modules/kpi.min.cjs +202 -10
  134. package/modules/kpi.min.js +202 -10
  135. package/modules/layout.d.ts +2 -2
  136. package/modules/layout.esm.min.js +3 -6
  137. package/modules/layout.min.cjs +3 -6
  138. package/modules/layout.min.js +3 -6
  139. package/modules/mock-socket.d.ts +1 -1
  140. package/modules/mock-socket.esm.min.js +1 -4
  141. package/modules/mock-socket.min.cjs +1 -4
  142. package/modules/mock-socket.min.js +1 -4
  143. package/modules/react.d.ts +4 -5
  144. package/modules/react.esm.min.js +3 -6
  145. package/modules/react.min.cjs +3 -6
  146. package/modules/react.min.js +3 -6
  147. package/modules/svelte.d.ts +2 -3
  148. package/modules/svelte.esm.min.js +1 -4
  149. package/modules/svelte.min.cjs +1 -4
  150. package/modules/svelte.min.js +1 -4
  151. package/modules/tabs.d.ts +2 -3
  152. package/modules/tabs.esm.min.js +3 -6
  153. package/modules/tabs.min.cjs +3 -6
  154. package/modules/tabs.min.js +3 -6
  155. package/modules/vue.d.ts +1 -1
  156. package/modules/vue.esm.min.js +1 -4
  157. package/modules/vue.min.cjs +1 -4
  158. package/modules/vue.min.js +1 -4
  159. package/modules/webcomponent.d.ts +2 -2
  160. package/modules/webcomponent.esm.min.js +155 -96
  161. package/modules/webcomponent.min.cjs +155 -96
  162. package/modules/webcomponent.min.js +155 -96
  163. package/package.json +1 -1
@@ -253,6 +253,30 @@ createChart({ grid, container: '#map', type: 'bubblemap', lon: 'lng', lat: 'lat'
253
253
  size: 'sales', shapes: us });
254
254
  ```
255
255
 
256
+ ### Against a marker map
257
+
258
+ `chart-markermap` places a labelled figure per location on the same packs, and
259
+ takes its colour from the **value column's own conditional-formatting rules**
260
+ rather than from anything on the chart. Set the rules once, on the column, and
261
+ the cell and the marker agree; the legend lists the rules that fired. The
262
+ coordinates are `lon`/`lat` in degrees, the name is `label`, and the number is
263
+ `value`, written in the column's own format.
264
+
265
+ ```js
266
+ import '@toclocoinc/lattice-grid/modules/chart-markermap';
267
+ import { pack as world } from '@toclocoinc/lattice-grid/modules/geo-world-110m';
268
+
269
+ grid.formatting.add('availability', {
270
+ when: { op: 'gte', value: 0.999 }, style: { background: '#1b7f3b' }, label: 'Healthy',
271
+ });
272
+ createChart({ grid, container: '#map', type: 'markermap', lon: 'lng', lat: 'lat',
273
+ label: 'site', value: 'availability', shapes: world, selection: true });
274
+ ```
275
+
276
+ A row whose `lon`/`lat` is absent, non-numeric or outside ±180 / ±90 draws no
277
+ marker; the count is on `chart.data().unplaced` and written under the map.
278
+ `labels: false` drops the text and keeps the tooltip.
279
+
256
280
  ---
257
281
 
258
282
  ## Country codes
@@ -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.63.3</p>
440
+ <p class="rail__sub">Developer guide · v1.65.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.63.3</span>
557
+ <span class="chip">Version 1.65.0</span>
558
558
  <span class="chip">Zero dependencies</span>
559
559
  <span class="chip">No build step</span>
560
560
  </p>
@@ -1369,7 +1369,7 @@ off(); <span class="cmt">// every subscrip
1369
1369
  </p>
1370
1370
  <div class="example">
1371
1371
  <p class="example__label">Which version am I running?</p>
1372
- <pre><code>grid.getVersion(); <span class="cmt">// '1.63.3'</span>
1372
+ <pre><code>grid.getVersion(); <span class="cmt">// '1.65.0'</span>
1373
1373
  LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid to hand</span></code></pre>
1374
1374
  </div>
1375
1375
  <p class="lead-in">
@@ -7896,6 +7896,116 @@ grid.diff.before('CIR-100042', 'capacity');</code></pre>
7896
7896
  highlight cannot serve both.
7897
7897
  </p>
7898
7898
 
7899
+ <h3 id="network-map-guide">A network map: icon nodes, links coloured by their value</h3>
7900
+ <p class="lead-in">
7901
+ A <code>network</code> chart draws the rows as a graph &mdash; two columns name the endpoints
7902
+ and a third carries the value on the link. Until 1.64 every node was a plain circle, every
7903
+ edge the same grey, and the layout went wherever the simulation put it, so the picture a
7904
+ network team actually draws &mdash; core on top, regions below, an icon per device, a colour
7905
+ per circuit &mdash; could not be expressed. Three options change that, and every one of them
7906
+ carries a fact that only the host has.
7907
+ </p>
7908
+ <div class="example">
7909
+ <p class="example__label">One <code>nodes</code> list, one rule set, one chart config</p>
7910
+ <pre><code>createGrid(el, {
7911
+ columns: [{ field: 'from' }, { field: 'to' }, { field: 'load', type: 'number' }],
7912
+ rows, <span class="cmt">// one row per circuit</span>
7913
+ selection: 'multiple',
7914
+ <span class="cmt">// The rules live on the column. The cells and the links read the same ones.</span>
7915
+ formatting: {
7916
+ load: [
7917
+ { label: 'Healthy', when: { op: 'lt', value: 40 }, style: { background: '#107c41' } },
7918
+ { label: 'Busy', when: { op: 'lt', value: 80 }, style: { background: '#f0b400' } },
7919
+ { label: 'Saturated', when: { op: 'gte', value: 80 }, style: { background: '#a4262c' } },
7920
+ ],
7921
+ },
7922
+ });
7923
+
7924
+ createChart({
7925
+ grid, container: '#topology', type: 'network',
7926
+ source: 'from', target: 'to', y: { col: 'load', fn: 'sum' },
7927
+ selection: true,
7928
+ nodes: [
7929
+ <span class="cmt">// x/y are fractions of the plot: both given pins the node there.</span>
7930
+ { id: 'core-1', label: 'Core', icon: 'router', x: 0.3, y: 0.15 },
7931
+ { id: 'core-2', label: 'Core', icon: 'router', x: 0.7, y: 0.15 },
7932
+ { id: 'emea', label: 'EMEA', icon: 'hub', x: 0.2, y: 0.8 },
7933
+ { id: 'amer', label: 'AMER', icon: 'hub', x: 0.5, y: 0.8 },
7934
+ { id: 'apac', label: 'APAC', icon: 'hub', x: 0.8, y: 0.8 },
7935
+ ],
7936
+ });</code></pre>
7937
+ </div>
7938
+ <div class="why">
7939
+ <p><strong>The icons come from the grid, not from the chart.</strong> <code>icon</code> is a
7940
+ name in the grid&rsquo;s own sprite registry &mdash; a built-in, or one you registered &mdash;
7941
+ and the chart reads it through <code>grid.icons</code>, the grid it is bound to. That is
7942
+ deliberate and it is the only route that works: the charts module ships as its own bundle, so
7943
+ an <code>import</code> of the registry there would hand the chart a <em>second</em>, empty
7944
+ copy, and a glyph you registered would be invisible to it. One registry, reached through the
7945
+ one object both sides already share. Unknown names warn once and draw a plain disc rather
7946
+ than nothing.</p>
7947
+ <p><strong>Pinning steers the layout without replacing it.</strong> A node with both
7948
+ <code>x</code> and <code>y</code> is a fixed body: it still pushes its neighbours apart and
7949
+ still pulls on its links, and the integration step skips it. Everything unpinned settles
7950
+ around it by the same deterministic relaxation as before &mdash; and pinning one node does
7951
+ not reshuffle the others, because the layout&rsquo;s seeding draws for every node whether it
7952
+ is pinned or not, precisely so that it cannot. Half a position is not a position: a node with
7953
+ only <code>x</code> is laid out.</p>
7954
+ <p><strong>Parallel links are not summed.</strong> Three rows between the same pair are three
7955
+ lines, offset 4&nbsp;px apart, symmetric about the pair&rsquo;s own line, in row order. One
7956
+ line carrying their total would be a number nothing measured &mdash; three circuits at 40% do
7957
+ not make one at 120% &mdash; and the pointer picks out the line you are over rather than the
7958
+ pair, so each one&rsquo;s own value is readable. Links are undirected: no arrowheads, and
7959
+ <code>A,B</code> is the same pair as <code>B,A</code>.</p>
7960
+ <p><strong>There is no chart-level threshold option, on purpose.</strong> The colour comes
7961
+ from <code>grid.formatting.styleFor(col, value)</code> &mdash; <code>background</code>, then
7962
+ <code>backgroundColor</code>, then <code>color</code>; a gradient is not a colour and is not
7963
+ read. A second place to say &ldquo;red above 80&rdquo; is a second place for the chart and the
7964
+ cell to disagree. The legend lists only the rules that fired, with their own labels and
7965
+ swatches, and changing a rule recolours the links on the next frame without the layout
7966
+ re-running, so nothing moves.</p>
7967
+ <p><strong>Clicking acts on rows, because a link is a row.</strong> With
7968
+ <code>selection: true</code>, clicking a link selects its row and clicking a node selects
7969
+ every row it is an end of; the grid&rsquo;s selection then lights those marks and dims the
7970
+ rest. The types whose marks are <em>aggregates</em> are deliberately untouched: selecting the
7971
+ forty rows behind a bar is not what a click on a bar means.</p>
7972
+ </div>
7973
+
7974
+ <p class="lead-in">
7975
+ The glyphs those nodes draw are yours to supply. <code>icons</code> on the grid configuration
7976
+ registers SVG sprites by name, before the first paint, into the same registry the built-in
7977
+ chevrons and sort arrows live in &mdash; so a name you register is usable anywhere a glyph
7978
+ name is: a column&rsquo;s <code>icon</code> decoration, a rail action&rsquo;s
7979
+ <code>icon</code>, a network node&rsquo;s <code>icon</code>. Registering a built-in name
7980
+ overrides it, which is how the expander chevron becomes your own mark. A sprite is a view box
7981
+ and its path data; one filled path on the house 16&times;16 box is the common case, so a bare
7982
+ path string is read as exactly that. <code>grid.icons</code> reads the registry back.
7983
+ </p>
7984
+ <div class="example">
7985
+ <p class="example__label">Registering your own glyphs, executed</p>
7986
+ <pre data-run="js" data-expect="router 2 stroke | hub 1 fill" data-covers="config:icons"><code><span class="kw">const</span> { createTestDom } = <span class="kw">await</span> import('../packages/dom/src/renderer/testdom.js');
7987
+ <span class="kw">const</span> { root } = createTestDom();
7988
+ <span class="kw">const</span> { createGrid } = <span class="kw">await</span> import('../packages/dom/src/index.js');
7989
+
7990
+ <span class="kw">const</span> grid = createGrid(root, {
7991
+ rowKey: 'id',
7992
+ rows: [{ id: 'core-1', load: 41 }],
7993
+ columns: [{ field: 'id' }, { field: 'load', type: 'number' }],
7994
+ <span class="cmt">// Your own sprites, by name, alongside the built-in set.</span>
7995
+ icons: {
7996
+ router: { viewBox: '0 0 16 16', paths: ['M2 6h12v6H2Z', 'M5 6V3h6v3'], paint: 'stroke' },
7997
+ hub: 'M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2Z', <span class="cmt">// one filled path is enough</span>
7998
+ },
7999
+ });
8000
+
8001
+ <span class="cmt">// Read back what the registry holds: the normalised glyph, whichever form registered it.</span>
8002
+ <span class="kw">const</span> router = grid.icons.get('router');
8003
+ <span class="kw">const</span> hub = grid.icons.get('hub');
8004
+ grid.destroy();
8005
+
8006
+ <span class="kw">return</span> `router ${router.paths.length} ${router.paint} | hub ${hub.paths.length} ${hub.paint}`;</code></pre>
8007
+ </div>
8008
+
7899
8009
  <h3 id="removed-rows">Showing what was deleted</h3>
7900
8010
  <div class="example">
7901
8011
  <p class="example__label">A deletion is a change too</p>
@@ -8078,7 +8188,7 @@ grid.import.apply(preview);</code></pre>
8078
8188
  <tr><td class="name">createKPI</td><td class="desc">A KPI / stat-tile view (module <code>kpi</code>) of a dataset as a panel of aggregate tiles — each tile a <code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, <code>countDistinct</code> or a <code>custom</code> reducer over the routed rows, with an optional <code>filter</code> predicate, number <code>format</code> (<code>number</code>/<code>currency</code>/<code>percent</code>/<code>compact</code>), a <code>baseline</code> for a delta, semantic threshold bands (<code>thresholds</code> with two cut points and a direction, or an explicit <code>bands</code> list, giving a <code>good</code>/<code>warn</code>/<code>critical</code> status kept separate from any accent), and an optional <code>sparkline</code> series. It is a dataset viewer like any other: it consumes data through the same keyed-diff <code>rows.apply({ add, update, remove })</code> contract a grid exposes, so a Data Router can <code>attach(value, kpi)</code> and drive a KPI panel beside a grid, a kanban and a chart off one feed. Updates are incremental — a delta adjusts each tile's running accumulator by only the rows it carries (an add contributes, a remove reverses, an update reverses-then-contributes), the two bounded exceptions being a <code>min</code>/<code>max</code> whose current extreme is removed (a rescan of that tile's own value multiset) and a <code>custom</code> reducer (recomputed over the filtered store, an arbitrary function having no inverse). Each tile is a labelled <code>&lt;figure&gt;</code>, focusable and keyboard-activatable, its value announced, the sparkline respecting <code>prefers-reduced-motion</code>; a <code>tile:click</code> event (also from the keyboard) lets a host drill down or filter a routed grid. Pass <code>null</code> as the element for a headless panel that computes the same tile model without a DOM. Not a dashboard layout engine (that is the parked dashboard generator) and no charting beyond the minimal sparkline (that is the charts module).</td></tr>
8079
8189
  <tr><td class="name">createAI</td><td class="desc">Create an AI narrative / insights controller (module <code>ai</code>) over a live grid. It produces a short, plain-language narrative of the grid's <em>computed</em> figures — a per-KPI / per-chart / per-column <code>explain</code>, or an <code>insights</code> panel over the current filtered view. The grid makes no AI call of its own: <code>createAI</code> imports no provider SDK, holds no key, and makes no network request; it calls one host callback, <code>ask({ system, messages, prompt, tools?, schema?, signal })</code> — your model, your key, your privacy decision — the same philosophy as the data adapters. Two grounding paths feed one guard: where the provider offers tool-use the model is given a curated read-only tool set (<code>getSchema</code>, <code>getProfile</code>, <code>getStatistics</code>, <code>getForecast</code>, <code>runQuery</code>) and the grid's own engine computes what it asks for; otherwise the module builds a facts packet from <code>grid.statistics</code>/the profile/forecasts/view counts and passes it in the prompt. Every figure in the narrative is reconciled against the values the engine produced this render — an ungrounded number is stripped before display (the number-reconciliation guard), so a hallucinated figure never reaches the user. The prompt is constrained to narrate-only; the layer is read-only and never mutates data. <code>redact</code> (a column id, a list, or a predicate) and <code>maxRows</code> bound what the module hands <code>ask()</code>, and the module sends nothing itself; an <code>ask()</code> error surfaces a friendly message and the grid stays fully usable, AI being additive rather than load-bearing. Complementary to <code>grid.ai</code> (the intent/plan skill layer): pass no <code>ask</code> and it adopts the grid's configured <code>ai.ask</code>, running the facts-packet path over it. Pass a headless grid for a DOM-free narrative; <code>facts(target)</code> returns the exact grounded packet without calling <code>ask()</code>. UMD global <code>LatticeGridAI</code>.</td></tr>
8080
8190
  <tr><td class="name">createTabs</td><td class="desc">Create a tabbed grid (module <code>tabs</code>): a <code>role="tablist"</code> strip above a stack of <code>role="tabpanel"</code> regions, each hosting its own, independently-configured <code>createGrid</code> instance — "configure each tab as per a normal grid" rather than one grid whose state is swapped (<code>ColumnModel#applyState</code> only repositions/hides/resizes existing columns by id; it carries no field, type or row data, so a state-swap only works when every tab shares one schema). <code>createGrid</code> is injected (<code>createTabs(el, { createGrid, tabs })</code>), the same pattern the React/Vue/Svelte adapters use, so the module imports no engine code and adds nothing to a page that does not load it. A tab that names <code>from: '&lt;tabId&gt;'</code> gets a <code>source: { mode: 'derived', from: &lt;the parent tab’s live grid&gt;, where, group, join, … }</code> wired for it automatically — reusing the shipped derived-source mechanism rather than a new config-inheritance one — and activating a derived tab materialises its whole ancestor chain first; a cyclic <code>from</code> graph is refused (naming the exact cycle) when <code>createTabs</code> is called, not at first click. A tab’s grid mounts on first activation and then stays alive, hidden, so its scroll/selection/filters/sort/grouping/expansion — and an open cell/row editor, left exactly as it was, uncommitted and undiscarded — survive a switch natively; <code>destroy()</code> tears every mounted tab down. The strip is a real tablist with <code>aria-selected</code>, a roving <code>tabindex</code>, and manual-activation keyboard handling (arrows/Home/End move focus, Enter/Space or a click activates). Events: <code>tab:changed</code>, a cancellable <code>beforeTabChange</code> paired with <code>tabChange:cancelled</code>. UMD global <code>LatticeGridTabs</code>.</td></tr>
8081
- <tr><td class="name">createLayout</td><td class="desc">Create a reconfigurable dashboard layout (module <code>layout</code>): a cell grid inside an element, and a set of windows on it that a user can move, resize and close by drag <em>or</em> by keyboard &mdash; the surface a customer would otherwise reach for GridStack to get. It is <strong>payload-agnostic</strong>: a window body is a <code>div</code> with an <code>id</code> that the module creates, sizes and never reads, so it imports no engine code at all (not even <code>createGrid</code>) and its own code is 12,890 bytes gzipped, measured against a 62,206-byte fixed bundle floor. <code>columns</code>/<code>rows</code> divide the element; <code>overflowX</code> and <code>overflowY</code> are <em>independent</em> axes, each <code>'static'</code> (tracks divide the container with <code>minmax(0, 1fr)</code>) or <code>'scroll'</code> (tracks take a fixed <code>columnWidth</code>/<code>rowHeight</code> and the canvas extends past the viewport, so a column keeps the size it asked for &mdash; measured: shrinking a 600px host to 300px leaves a 200px column at 200px). Spacing takes a real CSS length: a number of pixels, or <code>'200px'</code>, <code>'25%'</code>, <code>'1fr'</code>, <code>'2rem'</code>; anything else is refused by name and replaced by the default, because the value reaches an inline style. Windows are placed by 1-based <code>xPos</code>/<code>yPos</code>/<code>xSize</code>/<code>ySize</code>, or auto-placed in the first free cell; <code>chrome</code> defaults on, and <code>closable</code>/<code>movable</code>/<code>resizable</code> all default <em>off</em>, so a fixed dashboard is fixed without opting out. <code>compact: 'vertical'</code> pushes displaced windows down then pulls them up (<code>window:moved</code> carries both <code>to</code> and <code>landed</code>); <code>'none'</code> keeps every window where it is put. <code>closable</code>/<code>movable</code>/<code>resizable</code> each also take a <em>layout-level</em> default of the same name, which a window's own boolean overrides, and <code>setInteractive(true|false|{movable, resizable, closable})</code> changes that default at runtime &mdash; the &ldquo;Edit layout&rdquo; button &mdash; without destroying the layout or any payload in it, with <code>getInteractive()</code> reading it back. Locking always wins and unlocking never overrides an opt-out: <code>setInteractive(false)</code> pins a window that declared <code>movable: true</code>, while <code>setInteractive(true)</code> leaves a window that declared <code>movable: false</code> pinned. The config key and the method are deliberately different: <code>movable: false</code> in the config states the <em>default</em> for windows that declare nothing and takes nothing away from one that opted in, whereas <code>setInteractive(false)</code> is an <em>active lock</em>. <code>getInteractive()</code> is three-valued &mdash; <code>undefined</code> for unset, <code>true</code>, or <code>false</code> for a lock &mdash; and a key carrying <code>undefined</code> is treated as absent, so <code>setInteractive(getInteractive())</code> is a no-op in every state. It moves both halves of the enforcement, the rendered handles <em>and</em> the pointer and keyboard gesture checks, and fires no event because a mode is not an arrangement &mdash; <code>getLayout()</code> neither carries it nor restores it. A locked layout is not a read-only dashboard: what is inside a window is configured with that payload's own settings. <code>maximise(id)</code>, <code>minimise(id)</code> and <code>restore(id)</code> are the display modes, with <code>maximised()</code> and <code>minimised()</code> reading them back: maximise fills the <em>layout host</em> rather than the browser window (no <code>position: fixed</code>, whose containing block is the nearest ancestor with a <code>transform</code> or a <code>contain</code>; no reparenting; nothing that can disturb the page around the dashboard), hides the other windows, runs <strong>no compaction at all</strong> and keeps the payload container as the very same DOM node &mdash; and <kbd>Escape</kbd> restores it from anywhere inside the layout. <code>minimise</code> draws a window as a single row and hides its payload while its chrome stays to carry the way back, so the windows below pull up on screen; in the arrangement nothing moves at all, because the collapse is a projection of the dashboard rather than a change to it, so restoring gives back exactly the arrangement that was there in <em>any</em> order and with any number of other windows still collapsed. A <code>chrome: false</code> window is refused by name. Both controls are opt-in per window (<code>maximisable</code>, <code>minimisable</code>) with a layout-level default of the same name, and <code>setInteractive()</code> deliberately does not touch either: a mode is not an arrangement, so neither appears in <code>getLayout()</code>, which reports the underlying placement in both states. Keyboard parity with the drag: a focusable handle per window running the kanban board's grab/move/drop/cancel model, with a polite live region announcing grabbed, every tentative position, dropped, cancelled and reverted. It owns exactly <strong>one</strong> <code>ResizeObserver</code> for the whole layout, over two targets, and tells payloads their new content box through <code>window:resized</code> &mdash; it never calls into a payload, because it cannot know what one is. Events: <code>window:moved</code>, <code>window:resized</code>, <code>window:closed</code>, <code>layout:changed</code>, the cancellable <code>beforeWindowMove</code>/<code>beforeWindowResize</code>/<code>beforeWindowClose</code> and their <code>*:cancelled</code> pairs; drag progress is not emitted per frame. <code>getLayout()</code>/<code>setLayout()</code> round-trip the arrangement as plain JSON, and <code>getState()</code>/<code>setState()</code> are the versioned pair. Closing a window does <strong>not</strong> destroy its payload &mdash; the container is handed back on <code>window:closed</code> and the host owns that lifecycle. UMD global <code>LatticeGridLayout</code>.</td></tr>
8191
+ <tr><td class="name">createLayout</td><td class="desc">Create a reconfigurable dashboard layout (module <code>layout</code>): a cell grid inside an element, and a set of windows on it that a user can move, resize and close by drag <em>or</em> by keyboard &mdash; the surface a customer would otherwise reach for GridStack to get. It is <strong>payload-agnostic</strong>: a window body is a <code>div</code> with an <code>id</code> that the module creates, sizes and never reads, so it imports no engine code at all (not even <code>createGrid</code>) and the whole module is about 17,500 bytes gzipped on the built bundle, of which roughly 2KB is the shared module runtime. <code>columns</code>/<code>rows</code> divide the element; <code>overflowX</code> and <code>overflowY</code> are <em>independent</em> axes, each <code>'static'</code> (tracks divide the container with <code>minmax(0, 1fr)</code>) or <code>'scroll'</code> (tracks take a fixed <code>columnWidth</code>/<code>rowHeight</code> and the canvas extends past the viewport, so a column keeps the size it asked for &mdash; measured: shrinking a 600px host to 300px leaves a 200px column at 200px). Spacing takes a real CSS length: a number of pixels, or <code>'200px'</code>, <code>'25%'</code>, <code>'1fr'</code>, <code>'2rem'</code>; anything else is refused by name and replaced by the default, because the value reaches an inline style. Windows are placed by 1-based <code>xPos</code>/<code>yPos</code>/<code>xSize</code>/<code>ySize</code>, or auto-placed in the first free cell; <code>chrome</code> defaults on, and <code>closable</code>/<code>movable</code>/<code>resizable</code> all default <em>off</em>, so a fixed dashboard is fixed without opting out. <code>compact: 'vertical'</code> pushes displaced windows down then pulls them up (<code>window:moved</code> carries both <code>to</code> and <code>landed</code>); <code>'none'</code> keeps every window where it is put. <code>closable</code>/<code>movable</code>/<code>resizable</code> each also take a <em>layout-level</em> default of the same name, which a window's own boolean overrides, and <code>setInteractive(true|false|{movable, resizable, closable})</code> changes that default at runtime &mdash; the &ldquo;Edit layout&rdquo; button &mdash; without destroying the layout or any payload in it, with <code>getInteractive()</code> reading it back. Locking always wins and unlocking never overrides an opt-out: <code>setInteractive(false)</code> pins a window that declared <code>movable: true</code>, while <code>setInteractive(true)</code> leaves a window that declared <code>movable: false</code> pinned. The config key and the method are deliberately different: <code>movable: false</code> in the config states the <em>default</em> for windows that declare nothing and takes nothing away from one that opted in, whereas <code>setInteractive(false)</code> is an <em>active lock</em>. <code>getInteractive()</code> is three-valued &mdash; <code>undefined</code> for unset, <code>true</code>, or <code>false</code> for a lock &mdash; and a key carrying <code>undefined</code> is treated as absent, so <code>setInteractive(getInteractive())</code> is a no-op in every state. It moves both halves of the enforcement, the rendered handles <em>and</em> the pointer and keyboard gesture checks, and fires no event because a mode is not an arrangement &mdash; <code>getLayout()</code> neither carries it nor restores it. A locked layout is not a read-only dashboard: what is inside a window is configured with that payload's own settings. <code>maximise(id)</code>, <code>minimise(id)</code> and <code>restore(id)</code> are the display modes, with <code>maximised()</code> and <code>minimised()</code> reading them back: maximise fills the <em>layout host</em> rather than the browser window (no <code>position: fixed</code>, whose containing block is the nearest ancestor with a <code>transform</code> or a <code>contain</code>; no reparenting; nothing that can disturb the page around the dashboard), hides the other windows, runs <strong>no compaction at all</strong> and keeps the payload container as the very same DOM node &mdash; and <kbd>Escape</kbd> restores it from anywhere inside the layout. <code>minimise</code> draws a window as a single row and hides its payload while its chrome stays to carry the way back, so the windows below pull up on screen; in the arrangement nothing moves at all, because the collapse is a projection of the dashboard rather than a change to it, so restoring gives back exactly the arrangement that was there in <em>any</em> order and with any number of other windows still collapsed. A <code>chrome: false</code> window is refused by name. Both controls are opt-in per window (<code>maximisable</code>, <code>minimisable</code>) with a layout-level default of the same name, and <code>setInteractive()</code> deliberately does not touch either: a mode is not an arrangement, so neither appears in <code>getLayout()</code>, which reports the underlying placement in both states. Keyboard parity with the drag: a focusable handle per window running the kanban board's grab/move/drop/cancel model, with a polite live region announcing grabbed, every tentative position, dropped, cancelled and reverted. It owns exactly <strong>one</strong> <code>ResizeObserver</code> for the whole layout, over two targets, and tells payloads their new content box through <code>window:resized</code> &mdash; it never calls into a payload, because it cannot know what one is. Events: <code>window:moved</code>, <code>window:resized</code>, <code>window:closed</code>, <code>layout:changed</code>, the cancellable <code>beforeWindowMove</code>/<code>beforeWindowResize</code>/<code>beforeWindowClose</code> and their <code>*:cancelled</code> pairs; drag progress is not emitted per frame. <code>getLayout()</code>/<code>setLayout()</code> round-trip the arrangement as plain JSON, and <code>getState()</code>/<code>setState()</code> are the versioned pair. Closing a window does <strong>not</strong> destroy its payload &mdash; the container is handed back on <code>window:closed</code> and the host owns that lifecycle. UMD global <code>LatticeGridLayout</code>.</td></tr>
8082
8192
  <tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
8083
8193
  <tr><td class="name">createGantt</td><td class="desc">Create a project-planning Gantt controller (module <code>gantt</code>) over a task list and a dependency list. A CPM engine (<code>computeSchedule</code>) computes each task's early/late start and finish, its slack and the zero-float critical path, honouring the four link types (<code>LINK_TYPES</code>: FS/SS/FF/SF) with lag, and recomputes on every edit — emitting <code>schedule</code> or, on a dependency cycle or bad input, <code>error</code> (a code from <code>SCHEDULE_ERROR</code>). Milestones are zero-duration points; summary (WBS) tasks are derived from their children (earliest start, latest finish, weighted progress) rather than scheduled; <code>findViolations</code> flags a task placed earlier than its predecessors allow, and <code>toISODate</code> maps an engine day-number back to a calendar date.</td></tr>
8084
8194
  <tr><td class="name">createLatticeGridElement</td><td class="desc">Build the element class without registering it, for a custom registry.</td></tr>
@@ -9408,7 +9518,7 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'<
9408
9518
 
9409
9519
  <footer>
9410
9520
  <p>
9411
- Lattice Grid 1.63.3 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
9521
+ Lattice Grid 1.65.0 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
9412
9522
  Written against the shipped source. Where this guide and the code disagree, the code wins,
9413
9523
  please <a href="https://www.latticegrid.dev">tell us</a>.
9414
9524
  </p>