@toclocoinc/lattice-grid 1.34.1 → 1.36.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 (68) hide show
  1. package/README.md +1 -1
  2. package/docs/API.html +350 -2
  3. package/docs/api-detail.html +88 -2
  4. package/lattice-grid.d.ts +331 -13
  5. package/lattice-grid.esm.min.js +557 -98
  6. package/lattice-grid.min.cjs +556 -98
  7. package/lattice-grid.min.js +556 -98
  8. package/modules/angular.esm.min.js +12 -3
  9. package/modules/angular.min.cjs +12 -3
  10. package/modules/angular.min.js +12 -3
  11. package/modules/chart-alluvial.esm.min.js +1 -1
  12. package/modules/chart-arc.esm.min.js +1 -1
  13. package/modules/chart-bubblemap.esm.min.js +1 -1
  14. package/modules/chart-bump.esm.min.js +1 -1
  15. package/modules/chart-calendar.esm.min.js +1 -1
  16. package/modules/chart-decomposition.esm.min.js +1 -1
  17. package/modules/chart-diverging.esm.min.js +1 -1
  18. package/modules/chart-dumbbell.esm.min.js +1 -1
  19. package/modules/chart-fan.esm.min.js +1 -1
  20. package/modules/chart-hexbin.esm.min.js +1 -1
  21. package/modules/chart-hexmap.esm.min.js +1 -1
  22. package/modules/chart-icicle.esm.min.js +1 -1
  23. package/modules/chart-parallel.esm.min.js +1 -1
  24. package/modules/chart-ridgeline.esm.min.js +1 -1
  25. package/modules/chart-roc.esm.min.js +1 -1
  26. package/modules/chart-slope.esm.min.js +1 -1
  27. package/modules/chart-splom.esm.min.js +1 -1
  28. package/modules/chart-waffle.esm.min.js +1 -1
  29. package/modules/charts.esm.min.js +4 -4
  30. package/modules/charts.min.cjs +4 -4
  31. package/modules/charts.min.js +4 -4
  32. package/modules/data-router.esm.min.js +30 -11
  33. package/modules/data-router.min.cjs +30 -11
  34. package/modules/data-router.min.js +30 -11
  35. package/modules/devtools.esm.min.js +2 -2
  36. package/modules/devtools.min.cjs +2 -2
  37. package/modules/devtools.min.js +2 -2
  38. package/modules/dhtmlx-compat.esm.min.js +4 -4
  39. package/modules/dhtmlx-compat.min.cjs +4 -4
  40. package/modules/dhtmlx-compat.min.js +4 -4
  41. package/modules/gantt.esm.min.js +1144 -51
  42. package/modules/gantt.min.cjs +1144 -51
  43. package/modules/gantt.min.js +1144 -51
  44. package/modules/htmx.esm.min.js +555 -98
  45. package/modules/htmx.min.cjs +555 -98
  46. package/modules/htmx.min.js +555 -98
  47. package/modules/kanban.esm.min.js +281 -28
  48. package/modules/kanban.min.cjs +281 -28
  49. package/modules/kanban.min.js +281 -28
  50. package/modules/kpi.esm.min.js +978 -0
  51. package/modules/kpi.min.cjs +981 -0
  52. package/modules/kpi.min.js +981 -0
  53. package/modules/mock-socket.esm.min.js +2 -2
  54. package/modules/mock-socket.min.cjs +2 -2
  55. package/modules/mock-socket.min.js +2 -2
  56. package/modules/react.esm.min.js +12 -3
  57. package/modules/react.min.cjs +12 -3
  58. package/modules/react.min.js +12 -3
  59. package/modules/svelte.esm.min.js +12 -3
  60. package/modules/svelte.min.cjs +12 -3
  61. package/modules/svelte.min.js +12 -3
  62. package/modules/vue.esm.min.js +12 -3
  63. package/modules/vue.min.cjs +12 -3
  64. package/modules/vue.min.js +12 -3
  65. package/modules/webcomponent.esm.min.js +556 -98
  66. package/modules/webcomponent.min.cjs +556 -98
  67. package/modules/webcomponent.min.js +556 -98
  68. 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.34.1</p>
440
+ <p class="rail__sub">Developer guide · v1.36.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
@@ -1869,6 +1869,38 @@ grid.rows.load(data);
1869
1869
  grid.overlay.hide();</code></pre>
1870
1870
  </div>
1871
1871
 
1872
+ <h3 id="url-source-guide">Straight from a URL, with <code>createUrlSource</code></h3>
1873
+ <p class="lead-in">
1874
+ When the data is a file at a URL you do not have to fetch it yourself.
1875
+ <code>createUrlSource(url, opts)</code> loads a JSON file or streams an NDJSON/JSONL file directly,
1876
+ and you pass it as the <code>source</code>. See the
1877
+ <a href="API.html#url-source">Sources reference</a> for every option.
1878
+ </p>
1879
+ <div class="example">
1880
+ <p class="example__label">A JSON file, and a streamed NDJSON file</p>
1881
+ <pre><code><span class="cmt">// A JSON file: a top-level array, or nested via rowsPath / map.</span>
1882
+ createGrid(el, { columns, rowKey: 'id', source: createUrlSource('/data/circuits.json') });
1883
+
1884
+ <span class="cmt">// An NDJSON file: rows stream in as they parse, first rows first.</span>
1885
+ createGrid(el, { columns, rowKey: 'id', source: createUrlSource('/data/events.ndjson', { batchSize: 500 }) });
1886
+
1887
+ <span class="cmt">// Auth, a nested array, and a 30s refresh:</span>
1888
+ createGrid(el, {
1889
+ columns, rowKey: 'id',
1890
+ source: createUrlSource('/api/rows', {
1891
+ headers: { Authorization: 'Bearer …' },
1892
+ rowsPath: 'result.items',
1893
+ poll: 30000,
1894
+ }),
1895
+ });</code></pre>
1896
+ </div>
1897
+ <p>
1898
+ The format is inferred from the extension, then the <code>Content-Type</code>, then a sniff &mdash;
1899
+ override it with <code>format: 'json' | 'ndjson'</code>. A non-2xx response, a network error or a
1900
+ malformed NDJSON line becomes a <code>source:error</code> event rather than an exception, and
1901
+ <code>lenient: true</code> skips a bad NDJSON line instead of failing the whole stream.
1902
+ </p>
1903
+
1872
1904
  <h3>Incremental changes</h3>
1873
1905
  <p class="lead-in">
1874
1906
  <code>rows.load</code> replaces everything. When you have a delta, a websocket message, a
@@ -6238,8 +6270,9 @@ grid.state.apply(savedView.state);
6238
6270
  <tr><td class="name">canChartRange</td><td class="desc">Whether <code>chartRange</code> would draw something for the grid’s current selection — the question a menu asks before offering the item.</td></tr>
6239
6271
  <tr><td class="name">deriveRangeSpec</td><td class="desc">Decide what a chart of a range should be without drawing it: the type, the category column, the measures, and a spec ready for <code>createChart</code>.</td></tr>
6240
6272
  <tr><td class="name">regressionPlots</td><td class="desc">Turn a fitted regression model into diagnostic chart specs ready for <code>createChart</code>: the fit line with its confidence band, residuals-vs-fitted, a QQ plot of the residuals, and a multicollinearity correlogram with the model’s VIF. The plots that need a per-row or per-coefficient quantity the grid has no column for (scale-location, residuals-vs-leverage, the coefficient forest) are returned as a null spec carrying the reason rather than dropped.</td></tr>
6241
- <tr><td class="name">createDataRouter</td><td class="desc">Split one arriving stream or dataset across many grids by what each record is — a property or a predicate — driving each grid through the public keyed <code>rows.apply</code> path so a snapshot is a diff, a delta is applied in place, a moved partition moves the row rather than duplicating it, and an unmatched record is counted, sunk and never dropped. v2 adds cross-grid selection filtering: <code>link(source, target, relation)</code> makes a selection in one grid filter what another receives — by a key map or a predicate function, multi-select as an IN set, debounced — re-pushed through the same keyed-diff path so the target stays dumb. v5 adds wedge-conversion primitives: <code>subscribe(value, handler)</code> routes a slice to any non-grid view (KPI tile, detail pane, map, form) as the same keyed diff a grid gets; <code>alert(value, condition, handler)</code> evaluates a condition over a slice and emits (edge-triggered, debounced) rather than rendering; and <code>configure(spec)</code> (or <code>createDataRouter({ config })</code>) takes the whole routing graph as one declarative data spec that desugars to the imperative API and composes with it. Detaches its grids on <code>destroy</code>; the host owns them.</td></tr>
6273
+ <tr><td class="name">createDataRouter</td><td class="desc">Split one arriving stream or dataset across many grids by what each record is — a property or a predicate — driving each grid through the public keyed <code>rows.apply</code> path so a snapshot is a diff, a delta is applied in place, a moved partition moves the row rather than duplicating it, and an unmatched record is counted, sunk and never dropped. By default routing is first-match-wins (<code>overlap: false</code>); to fan one partition value to several viewers at once (a grid <em>and</em> a KPI panel <em>and</em> a chart off one feed) create the router with <code>overlap: true</code> — with the default, a second viewer on the same value receives nothing and the router emits a one-time dev warning naming the clash. v2 adds cross-grid selection filtering: <code>link(source, target, relation)</code> makes a selection in one grid filter what another receives — by a key map or a predicate function, multi-select as an IN set, debounced — re-pushed through the same keyed-diff path so the target stays dumb. v5 adds wedge-conversion primitives: <code>subscribe(value, handler)</code> routes a slice to any non-grid view (KPI tile, detail pane, map, form) as the same keyed diff a grid gets; <code>alert(value, condition, handler)</code> evaluates a condition over a slice and emits (edge-triggered, debounced) rather than rendering; and <code>configure(spec)</code> (or <code>createDataRouter({ config })</code>) takes the whole routing graph as one declarative data spec that desugars to the imperative API and composes with it. v3 also adds per-route reshaping — <code>transform</code>/<code>filter</code>/<code>sort</code> and <code>rollup</code> ({ groupBy, aggregate }) summaries — a relationship graph (<code>relate(edges)</code>: multi-hop, several-into-one AND, and mutual edges) that scales v2's pairwise <code>link</code>, and stream hygiene: a <code>seq</code>/version orders and de-duplicates a feed (stale/duplicate deltas dropped, counted in <code>dropped</code>), <code>push</code> with a <code>batch</code>/<code>coalesce</code> buffers a high-frequency feed (<code>flushStream</code> for a deterministic point), and <code>lastSeq</code>/<code>checkpoint</code>/<code>seenThrough</code> resume precisely after a dropped socket. v4 adds time-travel: <code>buffer({ window, max })</code> records the ordered stream into a bounded ring over a moving base, so <code>scrubTo</code> reconstructs a past point, <code>replay</code> (with <code>pause</code>/<code>resume</code>) walks a range, and <code>live</code> returns to the head — every state pushed by the same keyed diff, <code>traveling</code>/<code>buffered</code> reporting the state. v6 adds cross-tab sync: <code>broadcast({ channel })</code> mirrors the ordered, de-duplicated deltas to other tabs/windows over a BroadcastChannel with no echo loop, a popped-out grid joining the same feed with no second socket and resyncing mid-stream via the reconnect path. v7 adds query-slice routing: <code>query(adapter, request)</code> sources the router from a DFQL/DuckDB (or any pushdown) adapter, partitioning one result across the routes; a route-level <code>where</code> is pushed down where the adapter's capabilities allow and the residual finished client-side, with <code>lastQueryPlan</code> reporting the split. v8 adds write-back: a <code>writable</code> route captures the grid's committed edits off its public edit surface and routes them to <code>onWrite(change, ctx)</code> (per-route or router-global), reverting on reject, re-entering an accepted write as a delta, and surfacing a last-write-wins <code>onConflict</code>; a derived route cannot be writable. v9 adds fan-in: <code>addSource(feed, { map, key })</code> returns a per-feed handle (<code>load</code>/<code>apply</code>/<code>push</code>/<code>remove</code>) whose rows are normalized and namespaced so many feeds merge into one keyed store without id collisions, <code>removeSource</code> dropping exactly a feed's rows and <code>sources</code> listing them. v10 adds observability: <code>metrics()</code> is a cheap snapshot of per-route/per-source counts and throughput plus the global unrouted/dropped/buffered/lag figures, <code>on('metrics')</code> drives a periodic emit (off unless a listener is registered), and <code>mountDevtools(el)</code> renders a live panel from the module's own DOM file. Detaches its grids on <code>destroy</code>; the host owns them.</td></tr>
6242
6274
  <tr><td class="name">createKanban</td><td class="desc">A board (kanban) view of rows as cards, grouped into columns by a configurable property (a status, a stage, a state field), with per-column card count and an optional points sum, a WIP over-limit flag, configured columns shown even when empty, granular readonly (whole board / per column / per card), field-mapped card templates that reuse the grid’s own column formatters when a grid is bound, and the core pointer events (<code>card:click</code>, <code>card:dblclick</code>, <code>card:contextmenu</code>). 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, board)</code> and drive a kanban beside a grid and a chart off one feed. Accessibility is built in from the start — a labelled group of labelled column lists, cards in a roving-tabindex focus ring with arrow-key navigation, and a polite live region. Every structural property is named in config (<code>columnProperty</code>, <code>pointsProperty</code>, <code>orderProperty</code>, <code>swimlaneProperty</code>, <code>sprintProperty</code>, <code>epicProperty</code>) so it maps DemandFlow and any customer schema without code change. Pass <code>null</code> as the element for a headless board that computes the same model without a DOM. Cards drag between columns (writing the column property) and within a column into a position (writing the order property with fractional ranking), and the same move is keyboard-accessible — <kbd>Space</kbd> to grab, arrows to choose a target, <kbd>Space</kbd>/<kbd>Enter</kbd> to drop, <kbd>Escape</kbd> to cancel — announced on the live region; multi-select drags every selected card. A move calls <code>onBeforeMove(card, from, to, index)</code> first (return <code>false</code> to veto) and then persists through the grid’s shipped write-back path — grid-bound via <code>grid.edit.setCells</code> (the same public edit-commit path inline editing uses, so the grid’s pipeline owns optimistic apply / confirm / revert), standalone with a revert when <code>onCardMove</code> rejects — emitting <code>card:move</code>. A configurable per-card <code>contextMenu</code> replaces the <code>card:contextmenu</code> event when present. With <code>swimlanes: true</code> it renders a 2D lane&times;column grid grouped by <code>swimlaneProperty</code> (per-lane count/points, columns aligned across lanes, a cross-lane drag writing the swimlane property); columns and lanes collapse (state surviving a keyed-diff update), columns reorder by header drag, and <code>setQuickFilter</code>/<code>setFilter</code>/<code>facets</code> drive search and faceting. <code>setSprint</code>/<code>showBacklog</code>/<code>sprints</code> give a sprint view, switcher and backlog; <code>setEpic</code>/<code>epicRollup</code>/<code>rollup</code> give an epic view and rollups (count, points, progress toward the <code>done</code> columns). A card can pop out a nested grid of its children (an epic's stories, a story's tasks, recursively) via a <code>childrenProperty</code> and/or <code>loadChildren(card)</code>: the child is a full composed <code>createGrid</code> (or, with <code>asBoard</code>, a nested board) opened in a drawer/modal/inline container — reuse by composition, no grid-core coupling — emitting <code>card:expand</code>/<code>card:drill</code>. Live updates arrive through the same keyed-diff contract a grid uses, so a Data Router drives the board directly (<code>attach(board, predicate)</code>); a live <code>rows.apply</code> re-renders preserving scroll, focus, selection, collapse and any open pop-out. <code>virtualize</code> renders only a scroll window of a tall column; <code>getState</code>/<code>setState</code> (and <code>config.state</code>) save and restore collapse, order, filter and sprint/epic selection; <code>setLoading</code>/<code>setError</code> give loading and error states. The move is fully keyboard-driven — Space to grab, arrows for column/position, Alt+Up/Down across swimlanes, Space/Enter to drop, Escape to cancel — announced on a live region. A field opted in with <code>card: { title: { field, edit: true } }</code> edits inline (double-click or <code>editCard</code>): grid-bound through the grid's own field editor via its public edit path, standalone through a host editor factory or a default input with an <code>onCardEdit</code> revert; a per-column add-card (<code>config.addCard</code>/<code>onAddCard</code>, or <code>grid.edit.addRow</code>) creates a card and opens it in edit. The module imports nothing from the grid's DOM package.</td></tr>
6275
+ <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>
6243
6276
  <tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
6244
6277
  <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>
6245
6278
  <tr><td class="name">createLatticeGridElement</td><td class="desc">Build the element class without registering it, for a custom registry.</td></tr>
@@ -6492,6 +6525,59 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
6492
6525
  </tbody>
6493
6526
  </table>
6494
6527
  </div>
6528
+ <h4>Cancellable before-events (BACKLOG-0000943)</h4>
6529
+ <p>Every user-initiated mutation has a paired cancellable <code>before</code> event. The handler
6530
+ receives a <code>BeforeEvent</code> carrying the action context plus <code>preventDefault(reason?)</code>,
6531
+ <code>defaultPrevented</code> and <code>reason</code>. Calling <code>preventDefault()</code> — or
6532
+ returning <code>false</code>, the legacy kanban <code>onBeforeMove</code> idiom — cancels the action.
6533
+ A handler may be <code>async</code>; the mutation is held until every registered before-handler
6534
+ settles, so a confirm dialog or a server check genuinely gates the write. Any one handler
6535
+ preventing cancels it (veto wins), and a handler that throws is treated as a cancel and surfaced.
6536
+ On a veto the paired <code>&lt;action&gt;:cancelled</code> event fires carrying the reason.</p>
6537
+ <p>These fire for <em>user</em> actions only. Host/API writes (for example
6538
+ <code>grid.edit.setCells</code>) and remote/router-applied deltas (<code>rows.apply</code>,
6539
+ origin&nbsp;!==&nbsp;<code>'user'</code>) do not fire them — remote truth is not a user gesture and
6540
+ does not self-veto. The <code>origin</code> field carried on each before-event lets a host
6541
+ deduplicate a module-initiated write (a kanban or Gantt move that re-enters core) from a genuine
6542
+ user gesture. If a handler was <code>async</code> and the underlying state moved during the await
6543
+ (a row removed, a value changed by a live delta), the gate re-validates and cancels with reason
6544
+ <code>'stale'</code> rather than applying against state that has moved. With no before-handler
6545
+ registered every mutation stays synchronous and behaves exactly as before.</p>
6546
+ <div class="table-wrap">
6547
+ <table>
6548
+ <thead><tr><th>Event</th><th>Fires when</th></tr></thead>
6549
+ <tbody>
6550
+ <tr><td class="name">beforeEdit</td><td class="desc">Before a validated cell/row commit applies. Carries row, key, mode, changes, origin. Validation (edit.validate) is separate and runs first. Paired with edit:cancelled.</td></tr>
6551
+ <tr><td class="name">beforeSort</td><td class="desc">Before a sort is set. Paired with sort:cancelled.</td></tr>
6552
+ <tr><td class="name">beforeFilter</td><td class="desc">Before a structured or quick filter is set (kind tells them apart). Paired with filter:cancelled.</td></tr>
6553
+ <tr><td class="name">beforeColumnMove</td><td class="desc">Before a column reorder applies, earlier than the post-mutation column change. Paired with columnMove:cancelled.</td></tr>
6554
+ <tr><td class="name">beforeColumnResize</td><td class="desc">Before a column width change applies. Paired with columnResize:cancelled.</td></tr>
6555
+ <tr><td class="name">beforeColumnHide</td><td class="desc">Before one or more columns are hidden. Paired with columnHide:cancelled.</td></tr>
6556
+ <tr><td class="name">beforeSelect</td><td class="desc">Before a user selection change applies; a veto snaps back to the last announced selection. Paired with selection:cancelled.</td></tr>
6557
+ <tr><td class="name">beforeRowAdd</td><td class="desc">Before an optimistic row append applies. Paired with rowAdd:cancelled.</td></tr>
6558
+ <tr><td class="name">beforeDelete</td><td class="desc">Before an optimistic row delete applies — the canonical confirm-before-delete hook. Paired with delete:cancelled.</td></tr>
6559
+ <tr><td class="name">beforeRowMove</td><td class="desc">Before a row reorder applies. Paired with rowMove:cancelled.</td></tr>
6560
+ <tr><td class="name">beforeGroup</td><td class="desc">Before a group/tree expand or collapse applies. Paired with group:cancelled.</td></tr>
6561
+ <tr><td class="name">edit:cancelled</td><td class="desc">A beforeEdit was vetoed; reason is 'stale' when a live delta moved the cell during an async gate.</td></tr>
6562
+ <tr><td class="name">sort:cancelled</td><td class="desc">A beforeSort was vetoed.</td></tr>
6563
+ <tr><td class="name">filter:cancelled</td><td class="desc">A beforeFilter was vetoed.</td></tr>
6564
+ <tr><td class="name">columnMove:cancelled</td><td class="desc">A beforeColumnMove was vetoed.</td></tr>
6565
+ <tr><td class="name">columnResize:cancelled</td><td class="desc">A beforeColumnResize was vetoed.</td></tr>
6566
+ <tr><td class="name">columnHide:cancelled</td><td class="desc">A beforeColumnHide was vetoed.</td></tr>
6567
+ <tr><td class="name">selection:cancelled</td><td class="desc">A beforeSelect was vetoed; the selection snapped back.</td></tr>
6568
+ <tr><td class="name">rowAdd:cancelled</td><td class="desc">A beforeRowAdd was vetoed.</td></tr>
6569
+ <tr><td class="name">delete:cancelled</td><td class="desc">A beforeDelete was vetoed; reason is 'stale' when the row was already gone.</td></tr>
6570
+ <tr><td class="name">rowMove:cancelled</td><td class="desc">A beforeRowMove was vetoed; reason is 'stale' when the row had moved.</td></tr>
6571
+ <tr><td class="name">group:cancelled</td><td class="desc">A beforeGroup was vetoed.</td></tr>
6572
+ <tr><td class="name">export:request</td><td class="desc">A remote export was requested. Past-tense notification.</td></tr>
6573
+ <tr><td class="name">export:done</td><td class="desc">A remote export completed. Past-tense notification.</td></tr>
6574
+ <tr><td class="name">shortcuts:opened</td><td class="desc">The keyboard-shortcuts help overlay opened. Past-tense notification.</td></tr>
6575
+ <tr><td class="name">shortcuts:closed</td><td class="desc">The keyboard-shortcuts help overlay closed. Past-tense notification.</td></tr>
6576
+ <tr><td class="name">print:before</td><td class="desc">Print mode is about to snapshot. Past-tense notification, not cancellable (BACKLOG-0000941).</td></tr>
6577
+ <tr><td class="name">print:after</td><td class="desc">Print mode restored the grid, even if the browser cancelled the print (BACKLOG-0000941).</td></tr>
6578
+ </tbody>
6579
+ </table>
6580
+ </div>
6495
6581
  <h4>Presentation and formatting</h4>
6496
6582
  <div class="table-wrap">
6497
6583
  <table>
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.34.1, type declarations
2
+ * Lattice Grid 1.36.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -3442,7 +3442,21 @@ export type EventName =
3442
3442
  /* Annotations */
3443
3443
  | 'annotation:changed'
3444
3444
  /* Export */
3445
- | 'export:progress'
3445
+ | 'export:progress' | 'export:request' | 'export:done'
3446
+ /* Keyboard help overlay (past-tense notifications) */
3447
+ | 'shortcuts:opened' | 'shortcuts:closed'
3448
+ /* Print (past-tense notifications, BACKLOG-0000941) */
3449
+ | 'print:before' | 'print:after'
3450
+ /* Cancellable before-events (BACKLOG-0000943). Delivered through the async
3451
+ * before-dispatch path with a {@link BeforeEvent} carrying preventDefault. */
3452
+ | 'beforeEdit' | 'beforeSort' | 'beforeFilter'
3453
+ | 'beforeColumnMove' | 'beforeColumnResize' | 'beforeColumnHide'
3454
+ | 'beforeSelect' | 'beforeRowAdd' | 'beforeDelete' | 'beforeRowMove' | 'beforeGroup'
3455
+ /* Their cancellation notifications (past-tense, non-cancellable). */
3456
+ | 'edit:cancelled' | 'sort:cancelled' | 'filter:cancelled'
3457
+ | 'columnMove:cancelled' | 'columnResize:cancelled' | 'columnHide:cancelled'
3458
+ | 'selection:cancelled' | 'rowAdd:cancelled' | 'delete:cancelled'
3459
+ | 'rowMove:cancelled' | 'group:cancelled'
3446
3460
  /* Every event at once, for logging and debugging. */
3447
3461
  | '*';
3448
3462
 
@@ -3453,6 +3467,31 @@ export interface GridEvent {
3453
3467
  [key: string]: unknown;
3454
3468
  }
3455
3469
 
3470
+ /**
3471
+ * A cancellable *before*-event (BACKLOG-0000943), delivered to `on('beforeX')`
3472
+ * handlers before a user-initiated mutation is applied.
3473
+ *
3474
+ * A handler cancels the pending action by calling `preventDefault(reason?)`; the
3475
+ * mutation is then abandoned and a past-tense `<action>:cancelled` event carries
3476
+ * the reason. A handler may be `async` (or return a Promise): the grid awaits
3477
+ * every before-handler before deciding, so a confirm dialog or a server check
3478
+ * can gate the write. Any one handler preventing cancels the action.
3479
+ *
3480
+ * The action-specific fields (the edited cells, the target index, the affected
3481
+ * rows) are spread alongside these, so a handler decides without reaching into
3482
+ * grid internals. `origin` distinguishes a genuine user gesture from a
3483
+ * host/module-driven or remote write, which is how a module whose move re-enters
3484
+ * core is deduplicated by the host.
3485
+ */
3486
+ export interface BeforeEvent extends GridEvent {
3487
+ /** Cancel the pending action; the optional reason is surfaced on the cancellation event. */
3488
+ preventDefault(reason?: string): void;
3489
+ /** True once any handler has called `preventDefault` or returned false. */
3490
+ defaultPrevented: boolean;
3491
+ /** The reason given to `preventDefault`, or null; `'stale'` when re-validation failed. */
3492
+ reason: string | null;
3493
+ }
3494
+
3456
3495
  export type EventHandler = (e: GridEvent) => void;
3457
3496
  export type Unsubscribe = () => void;
3458
3497
 
@@ -4997,6 +5036,53 @@ export function createPushdownSource(
4997
5036
  }>;
4998
5037
  };
4999
5038
 
5039
+ /**
5040
+ * Load a JSON or NDJSON file from a URL (BACKLOG-0000944).
5041
+ *
5042
+ * Returns a `StreamSourceConfig` for `createGrid(el, { source: createUrlSource(url, opts) })`.
5043
+ * A JSON file (a top-level array, or a nested array selected by `rowsPath`/`map`)
5044
+ * is read whole and handed over as rows; an NDJSON/JSONL file (one JSON value per
5045
+ * line) is streamed in incrementally in batches. Format is resolved from an
5046
+ * explicit `format`, else the URL extension, else the `Content-Type`, else a
5047
+ * sniff of the first bytes or a clear error. Errors — a non-2xx status, a network
5048
+ * failure, a bad body, a malformed line — surface as `source:error`, never as an
5049
+ * uncaught throw. Zero new dependencies: `fetch`, `response.body.getReader()` and
5050
+ * `TextDecoder`.
5051
+ *
5052
+ * @param url the file URL
5053
+ * @param opts loading options
5054
+ * @returns a stream source config
5055
+ */
5056
+ export function createUrlSource(
5057
+ url: string,
5058
+ opts?: {
5059
+ /** Explicit format; wins over inference. */
5060
+ format?: 'json' | 'ndjson';
5061
+ /** Dot path to the array inside a wrapped JSON body (JSON only). */
5062
+ rowsPath?: string;
5063
+ /** Extract the array from the parsed JSON (JSON only); runs after `rowsPath`. */
5064
+ map?: (parsed: unknown) => unknown[];
5065
+ /** Transport override for auth, headers or a proxy; default `globalThis.fetch`. */
5066
+ fetch?: typeof fetch;
5067
+ /** Headers merged into the request. */
5068
+ headers?: Record<string, string>;
5069
+ /** Re-fetch on this interval in milliseconds; each pass replaces the rows. */
5070
+ poll?: number;
5071
+ /** NDJSON rows per emitted chunk, to avoid render thrash; default 500. */
5072
+ batchSize?: number;
5073
+ /** NDJSON: skip a malformed line with a warning rather than failing the stream. */
5074
+ lenient?: boolean;
5075
+ /** Start fetching on construction; default true. */
5076
+ autoStart?: boolean;
5077
+ /** Sliding-window bound passed through to the stream. */
5078
+ maxRows?: number;
5079
+ /** Render-coalescing window in ms, passed through to the stream. */
5080
+ coalesceMs?: number;
5081
+ /** Promote to a memory source below this row count, passed through to the stream. */
5082
+ promoteToMemoryBelow?: number;
5083
+ },
5084
+ ): StreamSourceConfig;
5085
+
5000
5086
  /**
5001
5087
  * The pushdown map (BACKLOG-0000730 Part B): one published record per statistic
5002
5088
  * giving whether the engine can express it, the DuckDB aggregate SQL it emits,
@@ -6276,21 +6362,43 @@ declare module 'lattice-grid/modules/gantt' {
6276
6362
  /** One of the four dependency link types (finish-to-start, start-to-start, finish-to-finish, start-to-finish). */
6277
6363
  export type GanttLinkType = 'FS' | 'SS' | 'FF' | 'SF';
6278
6364
 
6365
+ /** A scheduling constraint: pin the start, pin the finish, or schedule as late as possible. */
6366
+ export type GanttConstraintType =
6367
+ | 'must-start-on' | 'must-finish-on' | 'as-late-as-possible' | 'MSO' | 'MFO' | 'ALAP';
6368
+
6369
+ /** A working-time calendar: a Monday–Friday preset, or explicit working weekdays and holidays. */
6370
+ export type GanttCalendar =
6371
+ | 'weekends'
6372
+ | { workdays?: number[]; holidays?: Array<string | number | Date> };
6373
+
6279
6374
  /**
6280
- * A task in a Gantt plan. Give a `duration` or a numeric `start`+`end` (one is
6281
- * derived from the other). `milestone: true` (or `duration: 0`) is a
6282
- * zero-duration point. `parent` nests a task under a summary task, whose window
6283
- * and progress are DERIVED from its children rather than scheduled.
6375
+ * A task in a Gantt plan. Give a `duration` or a `start`+`end` (a day-number,
6376
+ * ISO date string or `Date`; one is derived from the other). `milestone: true`
6377
+ * (or `duration: 0`) is a zero-duration point. `parent` nests a task under a
6378
+ * summary, whose window and progress are DERIVED from its children.
6379
+ * `baselineStart`/`baselineEnd` (host-stored) drive planned-vs-actual variance;
6380
+ * `constraint` pins or pulls the task; `assignee` and `height` feed the split
6381
+ * view's grid panel.
6284
6382
  */
6285
6383
  export interface GanttTask {
6286
6384
  id: string | number;
6287
6385
  name?: string;
6288
- start?: number;
6289
- end?: number;
6386
+ start?: number | string | Date;
6387
+ end?: number | string | Date;
6290
6388
  duration?: number;
6291
6389
  percentComplete?: number;
6292
6390
  milestone?: boolean;
6293
6391
  parent?: string | number;
6392
+ baselineStart?: number | string | Date;
6393
+ baselineEnd?: number | string | Date;
6394
+ baseline?: { start?: number | string | Date; end?: number | string | Date };
6395
+ constraint?: GanttConstraintType;
6396
+ constraintDate?: number | string | Date;
6397
+ assignee?: string | string[];
6398
+ assignees?: string[];
6399
+ owner?: string;
6400
+ /** An explicit row height (px) for the split view; applied to both panels. */
6401
+ height?: number;
6294
6402
  }
6295
6403
 
6296
6404
  /**
@@ -6320,6 +6428,21 @@ declare module 'lattice-grid/modules/gantt' {
6320
6428
  isSummary: boolean;
6321
6429
  isMilestone: boolean;
6322
6430
  children: string[];
6431
+ /** The planned (baseline) window, present only when the task carries a baseline. */
6432
+ baselineStart?: number | null;
6433
+ baselineEnd?: number | null;
6434
+ /** Variance vs the baseline (actual − planned, day-numbers); a positive value is a slip. */
6435
+ startVariance?: number | null;
6436
+ finishVariance?: number | null;
6437
+ durationVariance?: number | null;
6438
+ }
6439
+
6440
+ /** An unhonourable scheduling constraint, reported rather than obeyed. */
6441
+ interface GanttConflict {
6442
+ id: string;
6443
+ type: string;
6444
+ at: number | null;
6445
+ earliestFeasible: number;
6323
6446
  }
6324
6447
 
6325
6448
  /** A CPM schedule result: per-task dates/float and the critical path, or an error. */
@@ -6333,6 +6456,10 @@ declare module 'lattice-grid/modules/gantt' {
6333
6456
  projectStart?: number;
6334
6457
  projectFinish?: number;
6335
6458
  projectDuration?: number;
6459
+ /** Constraints a predecessor made infeasible (empty when all are satisfied). */
6460
+ conflicts?: GanttConflict[];
6461
+ /** Whether a working-time calendar was applied. */
6462
+ calendar?: boolean;
6336
6463
  }
6337
6464
 
6338
6465
  /** A placement violation flagged by `findViolations`. */
@@ -6355,7 +6482,7 @@ declare module 'lattice-grid/modules/gantt' {
6355
6482
  * and the zero-float critical path, with summaries derived from their children,
6356
6483
  * milestones scheduled as points, and dependency cycles refused (never looped).
6357
6484
  */
6358
- export function computeSchedule(tasks: GanttTask[], deps?: GanttDependency[], options?: { projectStart?: number; deadline?: number }): GanttSchedule;
6485
+ export function computeSchedule(tasks: GanttTask[], deps?: GanttDependency[], options?: { projectStart?: number | string | Date; deadline?: number | string | Date; calendar?: GanttCalendar | null }): GanttSchedule;
6359
6486
 
6360
6487
  /** The tasks placed earlier than their earliest feasible start (manual validation). */
6361
6488
  export function findViolations(tasks: GanttTask[], schedule: GanttSchedule): GanttViolation[];
@@ -6369,6 +6496,8 @@ declare module 'lattice-grid/modules/gantt' {
6369
6496
  readonly dependencies: GanttDependency[];
6370
6497
  readonly schedule: GanttSchedule | null;
6371
6498
  readonly critical: string[];
6499
+ /** Constraints the latest schedule could not honour (empty when all are satisfied). */
6500
+ readonly conflicts: GanttConflict[];
6372
6501
  readonly autoSchedule: boolean;
6373
6502
  readonly grid: unknown;
6374
6503
  setTasks(tasks: GanttTask[]): GanttSchedule;
@@ -6423,6 +6552,36 @@ declare module 'lattice-grid/modules/gantt' {
6423
6552
  /** Days a keyboard arrow moves/resizes a task (default 1). */
6424
6553
  moveStep?: number;
6425
6554
  }): unknown;
6555
+ /**
6556
+ * Mount the JOINED split view (BACKLOG-0000938): one continuous, row-aligned
6557
+ * surface with a left task-grid panel (Task Name tree with expand/collapse,
6558
+ * assignee avatars, a circular % ring, plus any host columns) and the right
6559
+ * timeline, sharing a single vertical scroll so every grid row lines up
6560
+ * exactly with its bar row. The timeline scrolls horizontally on its own.
6561
+ * Composes the controller's schedule; makes no change to grid core.
6562
+ */
6563
+ mountSplit(container: unknown, options?: {
6564
+ height?: number;
6565
+ rowHeight?: number;
6566
+ headerHeight?: number;
6567
+ gridWidth?: number;
6568
+ indent?: number;
6569
+ zoom?: 'day' | 'week' | 'month' | 'quarter' | number;
6570
+ today?: number;
6571
+ nonWorking?: 'weekends' | ((day: number) => boolean);
6572
+ calendar?: GanttCalendar | null;
6573
+ showArrows?: boolean;
6574
+ showProgress?: boolean;
6575
+ showBaseline?: boolean;
6576
+ barLabel?: 'name' | 'percent' | 'dates' | 'none' | ((task: GanttScheduledTask) => string);
6577
+ columns?: Array<{ key: string; title?: string; width?: number; kind?: 'name' | 'assignee' | 'progress'; render?: (task: GanttScheduledTask, ctx: { rawTask: GanttTask; depth: number }) => unknown }>;
6578
+ }): unknown;
6579
+ /**
6580
+ * Capture a baseline (planned) snapshot of the current schedule as HOST data
6581
+ * (this does not mutate the tasks). Store it and feed it back as
6582
+ * `baselineStart`/`baselineEnd` task fields to get variance and ghost bars.
6583
+ */
6584
+ captureBaseline(): Array<{ id: string; baselineStart: number; baselineEnd: number; baselineDuration: number }>;
6426
6585
  /** Detach the mounted view, if any. The host still owns the container. */
6427
6586
  unmount(): void;
6428
6587
  /** The mounted view, or null. */
@@ -6440,9 +6599,12 @@ declare module 'lattice-grid/modules/gantt' {
6440
6599
  export function createGantt(opts?: {
6441
6600
  tasks?: GanttTask[];
6442
6601
  dependencies?: GanttDependency[];
6443
- projectStart?: number;
6444
- /** A project deadline (day-number); tasks that cannot meet it get negative float. */
6445
- deadline?: number;
6602
+ /** The schedule anchor: a day-number, ISO date string or Date. It only sets the floor a task with no predecessor starts on; it does not change how the schedule is computed. */
6603
+ projectStart?: number | string | Date;
6604
+ /** A project deadline (a day-number, ISO string or Date); tasks that cannot meet it get negative float. */
6605
+ deadline?: number | string | Date;
6606
+ /** A working-time calendar: skip weekends/holidays, durations in working days. */
6607
+ calendar?: GanttCalendar | null;
6446
6608
  autoSchedule?: boolean;
6447
6609
  grid?: unknown;
6448
6610
  /** Map task fields to grid column ids to enable drag write-back. */
@@ -6789,8 +6951,16 @@ declare module 'lattice-grid/modules/kanban' {
6789
6951
  swimlanes?: boolean;
6790
6952
  /** Explicit lane definitions; otherwise lanes come from the distinct swimlane values. */
6791
6953
  lanes?: (string | { id: string; title?: string })[];
6954
+ /** An explicit lane order by id (also set by a lane-header-drag reorder). */
6955
+ laneOrder?: string[];
6956
+ /** Enforce `wipLimit` as a hard gate: a move that would exceed it is refused (default false). */
6957
+ enforceWip?: boolean;
6958
+ /** A custom card template: return an HTML string or a DOM node to own the whole card body. */
6959
+ cardRenderer?: (card: KanbanCard, ctx: { column: KanbanColumn; readonly: boolean; el: HTMLElement; doc: Document }) => string | Node | void;
6792
6960
  sprintProperty?: string;
6793
6961
  epicProperty?: string;
6962
+ /** A configurable sprint dataset: the canonical sprint list (order + titles), shown even when empty. */
6963
+ sprints?: (string | { id: unknown; title?: string })[];
6794
6964
  /** The initially selected sprint id, `Kanban.BACKLOG`, or undefined for all. */
6795
6965
  sprint?: unknown;
6796
6966
  /** The initially selected epic id, or undefined for all. */
@@ -6933,6 +7103,10 @@ declare module 'lattice-grid/modules/kanban' {
6933
7103
  reorderColumns(order: string[]): Kanban;
6934
7104
  /** Move one column before another (or to the end); emits `column:reorder`. */
6935
7105
  moveColumn(id: string, beforeId: string | null): Kanban;
7106
+ /** Reorder the swimlanes to the given id order (emits `swimlane:reorder`). */
7107
+ reorderLanes(order: string[]): Kanban;
7108
+ /** Move one swimlane before another (or to the end); emits `swimlane:reorder`. */
7109
+ moveLane(id: string, beforeId: string | null): Kanban;
6936
7110
  /** Set a predicate filter over cards, or clear it with null. */
6937
7111
  setFilter(fn: ((row: KanbanRow, card: KanbanCard) => boolean) | null): Kanban;
6938
7112
  /** Set the quick-filter text matched across card fields. */
@@ -6947,8 +7121,10 @@ declare module 'lattice-grid/modules/kanban' {
6947
7121
  showBacklog(): Kanban;
6948
7122
  /** Select the shown epic (undefined for all); emits `epic:changed`. */
6949
7123
  setEpic(epic: unknown): Kanban;
6950
- /** The distinct sprint values (the switcher's options). */
7124
+ /** The distinct sprint values (the switcher's options); a configured `sprints` dataset pins the order. */
6951
7125
  sprints(): unknown[];
7126
+ /** The sprint dataset as `{ id, title }` descriptors — the configured list plus any data-only sprint. */
7127
+ sprintDefs(): { id: unknown; title: string }[];
6952
7128
  /** The distinct epic values. */
6953
7129
  epics(): unknown[];
6954
7130
  /** Roll rows up by a property: per-bucket count, points, done and progress. */
@@ -6990,3 +7166,145 @@ declare module 'lattice-grid/modules/kanban' {
6990
7166
  export function createKanban(el: HTMLElement | null, config?: KanbanConfig): Kanban;
6991
7167
  export default createKanban;
6992
7168
  }
7169
+
7170
+ declare module 'lattice-grid/modules/kpi' {
7171
+ /** A row backing a KPI aggregate: any object. Its identity comes from `rowKey`. */
7172
+ type KPIRow = Record<string, unknown>;
7173
+
7174
+ /** The aggregation kinds a tile can compute. `custom` is a host reducer over the rows. */
7175
+ type KPIAggregation = 'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct' | 'custom';
7176
+
7177
+ /** Number formatting for a tile value. `percent` treats the value as a ratio (0.42 → 42%). */
7178
+ type KPIFormat =
7179
+ | 'number' | 'currency' | 'percent' | 'compact'
7180
+ | { type?: 'number' | 'currency' | 'percent' | 'compact'; decimals?: number; currency?: string; locale?: string };
7181
+
7182
+ /**
7183
+ * A semantic threshold: two cut points and a direction. `higherIsBetter` (the
7184
+ * default) makes a value at/above `warn` good, at/above `critical` a warning,
7185
+ * below it critical; `lowerIsBetter` mirrors it. Colour is a host concern.
7186
+ */
7187
+ interface KPIThresholds {
7188
+ warn: number;
7189
+ critical: number;
7190
+ direction?: 'higherIsBetter' | 'lowerIsBetter';
7191
+ }
7192
+
7193
+ /** An explicit band: the `status` of the first band whose half-open `[min, max)` contains the value. */
7194
+ interface KPIBand {
7195
+ min?: number;
7196
+ max?: number;
7197
+ status: 'good' | 'warn' | 'critical';
7198
+ }
7199
+
7200
+ /** An optional sparkline series: the `y` field plotted in order of the `x` field (or insertion). */
7201
+ interface KPISparkline {
7202
+ x?: string;
7203
+ y: string | ((row: KPIRow) => unknown);
7204
+ }
7205
+
7206
+ /** One tile: an aggregate over the routed rows, with optional filter, format, threshold and trend. */
7207
+ interface KPITile {
7208
+ /** A stable identity for the tile (defaults to the label, then the index). */
7209
+ id?: string;
7210
+ /** The tile's accessible label. */
7211
+ label?: string;
7212
+ /** The aggregation kind, or a reducer `(rows, tile) => value` for a custom tile. */
7213
+ aggregation?: KPIAggregation | ((rows: KPIRow[], tile: object) => unknown);
7214
+ /** The reducer for a `custom` aggregation, when `aggregation` is the string `'custom'`. */
7215
+ compute?: (rows: KPIRow[], tile: object) => unknown;
7216
+ /** The field the aggregation reads (a path or accessor). Ignored by `count`. */
7217
+ field?: string | ((row: KPIRow) => unknown);
7218
+ /** A predicate limiting the rows this tile aggregates. */
7219
+ filter?: (row: KPIRow) => boolean;
7220
+ /** Value formatting. */
7221
+ format?: KPIFormat;
7222
+ /** A comparison target rendered alongside the value. */
7223
+ target?: number;
7224
+ /** A baseline the tile's delta is measured against. */
7225
+ baseline?: number;
7226
+ /** Threshold bands, either two cut points or an explicit band list. */
7227
+ thresholds?: KPIThresholds;
7228
+ /** Explicit status bands (an alternative to `thresholds`). */
7229
+ bands?: KPIBand[];
7230
+ /** A trend sparkline series. */
7231
+ sparkline?: KPISparkline | string;
7232
+ }
7233
+
7234
+ /** A computed tile, as it appears in the model. */
7235
+ interface KPITileModel {
7236
+ id: string;
7237
+ label: string;
7238
+ aggregation: string;
7239
+ field?: string;
7240
+ value: unknown;
7241
+ formatted: string;
7242
+ status: 'good' | 'warn' | 'critical' | null;
7243
+ target?: number;
7244
+ baseline?: number;
7245
+ delta: number | null;
7246
+ deltaPercent: number | null;
7247
+ deltaFormatted?: string;
7248
+ count: number;
7249
+ sparkline: number[] | null;
7250
+ }
7251
+
7252
+ /** The payload every tile event carries. */
7253
+ interface KPIEvent {
7254
+ tile: KPITileModel;
7255
+ id: string;
7256
+ originalEvent?: unknown;
7257
+ }
7258
+
7259
+ /** KPI panel configuration. */
7260
+ interface KPIConfig {
7261
+ rows?: KPIRow[];
7262
+ grid?: unknown;
7263
+ rowKey?: string | ((row: KPIRow) => unknown);
7264
+ tiles?: KPITile[];
7265
+ columns?: number;
7266
+ ariaLabel?: string;
7267
+ nullText?: string;
7268
+ onTileClick?: (event: KPIEvent) => void;
7269
+ onTileDblClick?: (event: KPIEvent) => void;
7270
+ onTileContextMenu?: (event: KPIEvent) => void;
7271
+ onChange?: (event: { model: { tiles: KPITileModel[] } }) => void;
7272
+ }
7273
+
7274
+ /** The keyed-diff consumer surface a KPI panel shares with a grid, so a Data Router routes to it directly. */
7275
+ interface KPIRows {
7276
+ apply(change: { add?: KPIRow[]; update?: KPIRow[]; remove?: unknown[] }): void;
7277
+ forEach(fn: (row: KPIRow, key: unknown) => void): void;
7278
+ readonly count: number;
7279
+ }
7280
+
7281
+ /**
7282
+ * A KPI / stat-tile panel: a grid of aggregate tiles over a dataset. It
7283
+ * consumes data through the same keyed-diff `rows.apply` contract a grid
7284
+ * exposes, so `dataRouter.attach(value, kpi)` drives it like any other viewer,
7285
+ * updating each tile incrementally from the routed delta.
7286
+ */
7287
+ interface KPI {
7288
+ readonly el: unknown | null;
7289
+ readonly rowKey: string | ((row: KPIRow) => unknown);
7290
+ rows: KPIRows;
7291
+ tiles(): KPITileModel[];
7292
+ tile(id: string): KPITileModel | undefined;
7293
+ value(id: string): unknown;
7294
+ setRows(rows: KPIRow[]): KPI;
7295
+ refresh(): KPI;
7296
+ getState(): object;
7297
+ setState(snapshot: object): KPI;
7298
+ on(name: string, fn: (event: KPIEvent) => void): () => void;
7299
+ off(name: string, fn: (event: KPIEvent) => void): void;
7300
+ destroy(): void;
7301
+ }
7302
+
7303
+ /**
7304
+ * Create a KPI / stat-tile panel over rows or a bound grid. Pass a DOM element
7305
+ * to render into, or `null` for a headless panel that computes the same tile
7306
+ * model without a DOM.
7307
+ */
7308
+ export function createKPI(el: HTMLElement | null, config?: KPIConfig): KPI;
7309
+ export default createKPI;
7310
+ }