@toclocoinc/lattice-grid 1.51.0 → 1.53.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 (77) hide show
  1. package/README.md +2 -1
  2. package/docs/API.html +259 -5
  3. package/docs/api-detail.html +121 -5
  4. package/lattice-grid.d.ts +501 -5
  5. package/lattice-grid.esm.min.js +288 -17
  6. package/lattice-grid.min.cjs +288 -17
  7. package/lattice-grid.min.js +288 -17
  8. package/modules/ai.esm.min.js +18 -4
  9. package/modules/ai.min.cjs +18 -4
  10. package/modules/ai.min.js +18 -4
  11. package/modules/angular.esm.min.js +2 -2
  12. package/modules/angular.min.cjs +2 -2
  13. package/modules/angular.min.js +2 -2
  14. package/modules/chart-alluvial.esm.min.js +1 -1
  15. package/modules/chart-arc.esm.min.js +1 -1
  16. package/modules/chart-bubblemap.esm.min.js +1 -1
  17. package/modules/chart-bump.esm.min.js +1 -1
  18. package/modules/chart-calendar.esm.min.js +1 -1
  19. package/modules/chart-decomposition.esm.min.js +1 -1
  20. package/modules/chart-diverging.esm.min.js +1 -1
  21. package/modules/chart-dumbbell.esm.min.js +1 -1
  22. package/modules/chart-fan.esm.min.js +1 -1
  23. package/modules/chart-hexbin.esm.min.js +1 -1
  24. package/modules/chart-hexmap.esm.min.js +1 -1
  25. package/modules/chart-icicle.esm.min.js +1 -1
  26. package/modules/chart-parallel.esm.min.js +1 -1
  27. package/modules/chart-ridgeline.esm.min.js +1 -1
  28. package/modules/chart-roc.esm.min.js +1 -1
  29. package/modules/chart-slope.esm.min.js +1 -1
  30. package/modules/chart-splom.esm.min.js +1 -1
  31. package/modules/chart-waffle.esm.min.js +1 -1
  32. package/modules/charts.esm.min.js +255 -83
  33. package/modules/charts.min.cjs +255 -83
  34. package/modules/charts.min.js +255 -83
  35. package/modules/data-router.esm.min.js +4 -4
  36. package/modules/data-router.min.cjs +4 -4
  37. package/modules/data-router.min.js +4 -4
  38. package/modules/devtools.esm.min.js +2 -2
  39. package/modules/devtools.min.cjs +2 -2
  40. package/modules/devtools.min.js +2 -2
  41. package/modules/dhtmlx-compat.esm.min.js +4 -4
  42. package/modules/dhtmlx-compat.min.cjs +4 -4
  43. package/modules/dhtmlx-compat.min.js +4 -4
  44. package/modules/gantt.esm.min.js +359 -62
  45. package/modules/gantt.min.cjs +359 -62
  46. package/modules/gantt.min.js +359 -62
  47. package/modules/htmx.esm.min.js +288 -17
  48. package/modules/htmx.min.cjs +288 -17
  49. package/modules/htmx.min.js +288 -17
  50. package/modules/kanban.esm.min.js +4 -4
  51. package/modules/kanban.min.cjs +4 -4
  52. package/modules/kanban.min.js +4 -4
  53. package/modules/kpi.esm.min.js +886 -41
  54. package/modules/kpi.min.cjs +886 -41
  55. package/modules/kpi.min.js +886 -41
  56. package/modules/layout.esm.min.js +1825 -0
  57. package/modules/layout.min.cjs +1828 -0
  58. package/modules/layout.min.js +1828 -0
  59. package/modules/mock-socket.esm.min.js +2 -2
  60. package/modules/mock-socket.min.cjs +2 -2
  61. package/modules/mock-socket.min.js +2 -2
  62. package/modules/react.esm.min.js +2 -2
  63. package/modules/react.min.cjs +2 -2
  64. package/modules/react.min.js +2 -2
  65. package/modules/svelte.esm.min.js +2 -2
  66. package/modules/svelte.min.cjs +2 -2
  67. package/modules/svelte.min.js +2 -2
  68. package/modules/tabs.esm.min.js +89 -14
  69. package/modules/tabs.min.cjs +89 -14
  70. package/modules/tabs.min.js +89 -14
  71. package/modules/vue.esm.min.js +2 -2
  72. package/modules/vue.min.cjs +2 -2
  73. package/modules/vue.min.js +2 -2
  74. package/modules/webcomponent.esm.min.js +288 -17
  75. package/modules/webcomponent.min.cjs +288 -17
  76. package/modules/webcomponent.min.js +288 -17
  77. package/package.json +1 -1
@@ -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.51.0</p>
440
+ <p class="rail__sub">Developer guide · v1.53.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
@@ -3396,11 +3396,126 @@ createGrid(right, {
3396
3396
  (<code>idle</code>, or a debounce), and prefer fewer, larger sources over many small ones where
3397
3397
  the shape of your data allows it.</p>
3398
3398
  <p><strong>Not supported alongside a union.</strong> <code>crossFilter</code> has no single
3399
- target once there is more than one parent; <code>profile</code> reduces one grid's own columns.
3400
- Both are refused with a warning rather than guessed at, and the top-level <code>follow</code> is
3401
- ignored in favour of each source's own.</p>
3399
+ target once there is more than one parent; <code>profile</code> and <code>statistics</code>
3400
+ reduce one grid's own columns. All three are refused with a warning rather than guessed at, and
3401
+ the top-level <code>follow</code> is ignored in favour of each source's own.</p>
3402
3402
  </div>
3403
3403
 
3404
+ <h2 id="derived-statistics">The relational statistics, as rows</h2>
3405
+ <p class="lead-in">
3406
+ One grid is the data; a second <em>is</em> the analysis of it. <code>statistics</code> projects
3407
+ the figures that need two or more columns &mdash; or a second grid &mdash; into rows you can
3408
+ sort, filter, chart and export like any others.
3409
+ </p>
3410
+ <p><strong>You probably do not need it for a single-column statistic.</strong> Those already have
3411
+ a route: a derived <code>select</code> reduces a group with any kernel the totals row uses, and
3412
+ that table is a superset of the statistics one. <code>select: { p95: { of: 'amount', fn: 'p95'
3413
+ } }</code> works today, and so do <code>median</code>, <code>stddev</code>, <code>gini</code>,
3414
+ <code>iqr</code>, <code>entropy</code>, <code>trimmedMean</code> and the rest.
3415
+ <code>statistics</code> is for what <code>select</code> structurally cannot reach.</p>
3416
+ <div class="example">
3417
+ <p class="example__label">Which columns move together</p>
3418
+ <pre><code>source: {
3419
+ mode: <span class="str">'derived'</span>,
3420
+ from: trades,
3421
+ statistics: { fn: <span class="str">'correlation'</span>, columns: [<span class="str">'price'</span>, <span class="str">'volume'</span>, <span class="str">'spread'</span>] },
3422
+ <span class="cmt">// -&gt; one row per PAIR: { a, b, coefficient, n }</span>
3423
+ }</code></pre>
3424
+ </div>
3425
+ <p><strong>Three statistics in this release.</strong> Each has one row shape, and the shape is the
3426
+ contract:</p>
3427
+ <ul>
3428
+ <li><code>{ fn: 'correlation', columns, orient? }</code> &mdash; Pearson's r across N columns.
3429
+ <code>orient: 'pairs'</code> (the default) gives <strong>one row per unordered pair</strong>,
3430
+ <code>{ a, b, coefficient, n }</code>. Long form by default because that is what
3431
+ a grid sorts, filters and charts well &mdash; "the three most correlated pairs" is then a
3432
+ sort and a <code>limit</code> on the derived grid. Only the upper triangle is emitted: r is
3433
+ symmetric, so <code>(a,b)</code> and <code>(b,a)</code> are one finding, and a column against
3434
+ itself is 1 by definition. <code>orient: 'matrix'</code> gives the classic square instead,
3435
+ one row per column with a field per other column, for a heat map.</li>
3436
+ <li><code>{ fn: 'series', of, by, periodsPerYear? }</code> &mdash; <strong>one row per
3437
+ metric</strong>, <code>{ metric, value, n }</code>. Note <em>per metric</em>, not
3438
+ per point: <code>grid.statistics.series</code> returns a summary &mdash; <code>n</code>,
3439
+ <code>first</code>, <code>last</code>, <code>change</code>, <code>changePercent</code>,
3440
+ <code>volatility</code>, <code>annualisedVolatility</code>, <code>growth</code>,
3441
+ <code>maxDrawdown</code>, <code>maxDrawdownFrom</code>, <code>maxDrawdownTo</code>,
3442
+ <code>autocorrelation</code>, <code>upDays</code>, <code>downDays</code> &mdash; and not a
3443
+ value per row. The shape is the one <code>profile</code>'s <code>orient: 'metrics'</code>
3444
+ already emits, deliberately, rather than a third convention for the same idea.
3445
+ <code>by</code> is required and never guessed, because kernels see rows in arrival order and
3446
+ that is not the grid's sort.</li>
3447
+ <li><code>{ fn: 'datasetVsDataset', with, columns? }</code> &mdash; <strong>one row per compared
3448
+ column</strong>, largest difference first: <code>{ column, measure, magnitude, distance,
3449
+ direction, nA, nB, reliable, unmatched }</code>. Both sides are read over their
3450
+ <em>filtered</em> rows, and <strong>the peer is watched</strong> &mdash; an edit or a filter
3451
+ on it re-derives the comparison, because a comparison whose other side has moved is wrong
3452
+ rather than merely late. A column present on only one side cannot be compared and is still
3453
+ reported, with a null <code>magnitude</code> and <code>unmatched</code> set to
3454
+ <code>'A'</code> or <code>'B'</code>, so you see that it was skipped and why.</li>
3455
+ </ul>
3456
+ <p><strong>Every row says how much it saw.</strong> <code>n</code> is the rows the figure
3457
+ covered, and it is on the row because a derived statistic travels: a coefficient exported to
3458
+ CSV or bound to a chart has left every bit of its context behind, and &ldquo;r = 0.98 over
3459
+ eleven rows&rdquo; is a different claim from the same number over eleven thousand.</p>
3460
+ <p><strong>What the row does <em>not</em> tell you: whether the source was windowed.</strong> A
3461
+ statistic over a source holding fewer rows than match its filters is computed on the loaded
3462
+ window rather than the whole set. The grid detects that from the <em>source's</em> own
3463
+ counters and says so in a console warning
3464
+ (<code>[lattice] correlation on &hellip; computed over N of M matching rows</code>) &mdash; and
3465
+ that remains the signal to watch. A derived source cannot reach those counters: a grid's public
3466
+ <code>rows.matchCount()</code> reports the <em>loaded</em> matches, so on a bounded stream
3467
+ evicted to 200 of 2,000 rows it returns 200 and agrees exactly with <code>rows.count()</code>.
3468
+ Rather than ship a flag that could never be true, no such flag is emitted; <code>n</code> says
3469
+ what the figure actually saw and nothing more is claimed.</p>
3470
+ <p><strong>A terminal producer, not a pipeline stage.</strong> A correlation is one row per pair,
3471
+ a series summary one row per metric, a comparison one row per column &mdash; none of which is
3472
+ one row per group, so there is no position in
3473
+ <code>unnest &rarr; where &rarr; bucket &rarr; groupBy &rarr; select &rarr; sort &rarr;
3474
+ limit</code> for <code>statistics</code> to occupy. It replaces the pipeline, exactly as
3475
+ <code>profile</code> does. Those keys are now <strong>ignored with a warning that names
3476
+ them</strong> rather than discarded in silence, for both producers. Sort, filter or limit the
3477
+ derived grid itself, or chain a second derived grid whose <code>from</code> is this one.
3478
+ <code>profile</code> and <code>statistics</code> are mutually exclusive and declaring both is
3479
+ refused, by name, when the source is built.</p>
3480
+
3481
+ <h3 id="derived-producer-cost">What a terminal producer costs</h3>
3482
+ <p><strong>No terminal producer patches incrementally &mdash; know this before you point one at a
3483
+ live feed.</strong> The grouped pipeline maintains its grouping across changes: an edit that
3484
+ names the rows it touched re-reduces only the groups those rows belong to. A producer has no
3485
+ grouping to maintain, so <em>every</em> change on the parent re-derives its whole output. This
3486
+ has always been true of <code>profile</code> and was not previously written down; it is written
3487
+ down here now, and it applies to <code>statistics</code> in the same way.</p>
3488
+ <p>Measured on a synthetic 200,000-row grid, five changes after a warm-up, one derived grid
3489
+ attached (machine-dependent &mdash; run <code>node bench/derived-producers.mjs</code> against
3490
+ your own shape):</p>
3491
+ <table>
3492
+ <thead><tr><th>Derived shape</th><th>Per change at 200k rows</th></tr></thead>
3493
+ <tbody>
3494
+ <tr><td>grouped, no <code>where</code> &mdash; <em>the patching pipeline</em></td><td>~1&nbsp;ms</td></tr>
3495
+ <tr><td><code>profile</code>, one column</td><td>~20&nbsp;ms</td></tr>
3496
+ <tr><td><code>statistics</code> <code>correlation</code>, 2 columns (1 pair)</td><td>~5&nbsp;ms</td></tr>
3497
+ <tr><td><code>statistics</code> <code>correlation</code>, 6 columns (15 pairs)</td><td>~32&nbsp;ms</td></tr>
3498
+ <tr><td><code>statistics</code> <code>series</code></td><td>~23&nbsp;ms</td></tr>
3499
+ <tr><td><code>statistics</code> <code>datasetVsDataset</code>, two grids</td><td>~36&nbsp;ms</td></tr>
3500
+ <tr><td><code>where</code>, no <code>groupBy</code> &mdash; <em>a pipeline shape that also never patches</em></td><td>~700&nbsp;ms</td></tr>
3501
+ </tbody>
3502
+ </table>
3503
+ <p><strong>Correlation is quadratic in its column count.</strong> It scans the rows once per pair,
3504
+ so N columns cost N&middot;(N&minus;1)/2 passes: six columns is fifteen passes, twenty columns is
3505
+ a hundred and ninety. Correlate the columns you mean rather than every numeric column you
3506
+ have.</p>
3507
+ <p><strong>The costs of several panels add up.</strong> Every derived grid attached to a parent
3508
+ re-derives on the same change, so three analysis panels over one grid cost the sum of the
3509
+ three, not the largest. This is BACKLOG-0001044's known gap &mdash; a hidden derived grid still
3510
+ does full read and compute work &mdash; with a larger constant behind it; a hidden analysis tab
3511
+ recomputing a correlation matrix on every tick is exactly that cost.</p>
3512
+ <p><strong>The escape hatch is <code>refresh</code>, and it already exists.</strong>
3513
+ <code>'idle'</code> is the default and coalesces a burst of changes into one derivation on the
3514
+ next frame; a <strong>number</strong> is a debounce in milliseconds; <code>'live'</code>
3515
+ derives on every change and is the one to avoid for an expensive analysis over a ticking feed;
3516
+ <code>'manual'</code> stops automatic derivation entirely, leaving the host to drive the
3517
+ source. Below a few tens of thousands of rows none of this matters.</p>
3518
+
3404
3519
  <h2 id="cross-filter">Cross-filtering</h2>
3405
3520
  <p class="lead-in">
3406
3521
  A derived panel can filter the grid it summarises. Click a region in the summary and the
@@ -6876,7 +6991,8 @@ grid.import.apply(preview);</code></pre>
6876
6991
  <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>
6877
6992
  <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>
6878
6993
  <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>
6879
- <tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
6994
+ <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 11,202 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. 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>
6995
+ <tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
6880
6996
  <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>
6881
6997
  <tr><td class="name">createLatticeGridElement</td><td class="desc">Build the element class without registering it, for a custom registry.</td></tr>
6882
6998
  <tr><td class="name">createMessages</td><td class="desc">Build a message catalogue. A partial set lays over the built-in British English one.</td></tr>