@toclocoinc/lattice-grid 1.49.0 → 1.50.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.
- package/README.md +2 -1
- package/docs/API.html +67 -1
- package/docs/api-detail.html +3 -2
- package/lattice-grid.d.ts +122 -1
- package/lattice-grid.esm.min.js +173 -12
- package/lattice-grid.min.cjs +173 -12
- package/lattice-grid.min.js +173 -12
- package/modules/ai.esm.min.js +6 -4
- package/modules/ai.min.cjs +6 -4
- package/modules/ai.min.js +6 -4
- package/modules/angular.esm.min.js +2 -2
- package/modules/angular.min.cjs +2 -2
- package/modules/angular.min.js +2 -2
- package/modules/chart-alluvial.esm.min.js +1 -1
- package/modules/chart-arc.esm.min.js +1 -1
- package/modules/chart-bubblemap.esm.min.js +1 -1
- package/modules/chart-bump.esm.min.js +1 -1
- package/modules/chart-calendar.esm.min.js +1 -1
- package/modules/chart-decomposition.esm.min.js +1 -1
- package/modules/chart-diverging.esm.min.js +1 -1
- package/modules/chart-dumbbell.esm.min.js +1 -1
- package/modules/chart-fan.esm.min.js +1 -1
- package/modules/chart-hexbin.esm.min.js +1 -1
- package/modules/chart-hexmap.esm.min.js +1 -1
- package/modules/chart-icicle.esm.min.js +1 -1
- package/modules/chart-parallel.esm.min.js +1 -1
- package/modules/chart-ridgeline.esm.min.js +1 -1
- package/modules/chart-roc.esm.min.js +1 -1
- package/modules/chart-slope.esm.min.js +1 -1
- package/modules/chart-splom.esm.min.js +1 -1
- package/modules/chart-waffle.esm.min.js +1 -1
- package/modules/charts.esm.min.js +4 -4
- package/modules/charts.min.cjs +4 -4
- package/modules/charts.min.js +4 -4
- package/modules/data-router.esm.min.js +4 -4
- package/modules/data-router.min.cjs +4 -4
- package/modules/data-router.min.js +4 -4
- package/modules/devtools.esm.min.js +2 -2
- package/modules/devtools.min.cjs +2 -2
- package/modules/devtools.min.js +2 -2
- package/modules/dhtmlx-compat.esm.min.js +4 -4
- package/modules/dhtmlx-compat.min.cjs +4 -4
- package/modules/dhtmlx-compat.min.js +4 -4
- package/modules/gantt.esm.min.js +78 -4
- package/modules/gantt.min.cjs +77 -4
- package/modules/gantt.min.js +77 -4
- package/modules/htmx.esm.min.js +173 -12
- package/modules/htmx.min.cjs +173 -12
- package/modules/htmx.min.js +173 -12
- package/modules/kanban.esm.min.js +4 -4
- package/modules/kanban.min.cjs +4 -4
- package/modules/kanban.min.js +4 -4
- package/modules/kpi.esm.min.js +4 -4
- package/modules/kpi.min.cjs +4 -4
- package/modules/kpi.min.js +4 -4
- package/modules/mock-socket.esm.min.js +2 -2
- package/modules/mock-socket.min.cjs +2 -2
- package/modules/mock-socket.min.js +2 -2
- package/modules/react.esm.min.js +2 -2
- package/modules/react.min.cjs +2 -2
- package/modules/react.min.js +2 -2
- package/modules/svelte.esm.min.js +2 -2
- package/modules/svelte.min.cjs +2 -2
- package/modules/svelte.min.js +2 -2
- package/modules/tabs.esm.min.js +870 -0
- package/modules/tabs.min.cjs +873 -0
- package/modules/tabs.min.js +873 -0
- package/modules/vue.esm.min.js +2 -2
- package/modules/vue.min.cjs +2 -2
- package/modules/vue.min.js +2 -2
- package/modules/webcomponent.esm.min.js +173 -12
- package/modules/webcomponent.min.cjs +173 -12
- package/modules/webcomponent.min.js +173 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
dependencies, no build step required. Optional adapters for React, Vue, Svelte
|
|
5
5
|
and Web Components ship alongside it.
|
|
6
6
|
|
|
7
|
-
Version 1.
|
|
7
|
+
Version 1.50.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -349,6 +349,7 @@ the UMD build or `.cjs` for CommonJS.
|
|
|
349
349
|
| kanban | `@toclocoinc/lattice-grid/modules/kanban` | `modules/kanban.min.js` | `LatticeGridKanban` | Board view: grid rows as cards grouped into columns (`createKanban`). |
|
|
350
350
|
| gantt | `@toclocoinc/lattice-grid/modules/gantt` | `modules/gantt.min.js` | `LatticeGridGantt` | Editable, dependency-aware project plan with a computed critical path (`createGantt`). |
|
|
351
351
|
| kpi | `@toclocoinc/lattice-grid/modules/kpi` | `modules/kpi.min.js` | `LatticeGridKPI` | A grid of stat tiles, each an aggregate over a dataset (`createKPI`). |
|
|
352
|
+
| tabs | `@toclocoinc/lattice-grid/modules/tabs` | `modules/tabs.min.js` | `LatticeGridTabs` | A tab strip where each tab is its own full grid, optionally derived from another (`createTabs`). |
|
|
352
353
|
| ai | `@toclocoinc/lattice-grid/modules/ai` | `modules/ai.min.js` | `LatticeGridAI` | Bring-your-own-model narrative and insights grounded on computed figures (`createAI`). |
|
|
353
354
|
| mock-socket | `@toclocoinc/lattice-grid/modules/mock-socket` | `modules/mock-socket.min.js` | `LatticeGridMockSocket` | A serverless stand-in for a live WebSocket feed (`MockWebSocket`, `opsFeed`). |
|
|
354
355
|
| devtools | `@toclocoinc/lattice-grid/modules/devtools` | `modules/devtools.min.js` | `LatticeGrid` (extends it) | The in-page diagnostic panel, including the accessibility checks (`createDevtools`). |
|
package/docs/API.html
CHANGED
|
@@ -2129,7 +2129,7 @@ createGrid(el, {
|
|
|
2129
2129
|
<tr><td class="sig">delta</td><td class="desc">Current value minus the baseline.</td></tr>
|
|
2130
2130
|
<tr><td class="sig">deltaPercent</td><td class="desc">The same as a percentage. A change from nothing has no percentage and reads null rather than infinity.</td></tr>
|
|
2131
2131
|
<tr><td class="sig">rate</td><td class="desc">Change per second, from the last two readings.</td></tr>
|
|
2132
|
-
<tr><td class="sig">history</td><td class="desc">The recent readings, oldest first. <code>depth</code> sets how many; the default is 20.</td></tr>
|
|
2132
|
+
<tr><td class="sig">history</td><td class="desc">The recent readings, oldest first. <code>depth</code> sets how many; the default is 20. Counts <em>changes</em> by default — see the time-windowed form below for a real time series.</td></tr>
|
|
2133
2133
|
<tr><td class="sig">firstValue</td><td class="desc">The baseline itself.</td></tr>
|
|
2134
2134
|
<tr><td class="sig">streak</td><td class="desc">Consecutive moves in one direction, signed. It resets on a turn, because "seven rises" means something and "seven changes" does not.</td></tr>
|
|
2135
2135
|
</tbody>
|
|
@@ -2164,6 +2164,10 @@ createGrid(el, {
|
|
|
2164
2164
|
]</code></pre>
|
|
2165
2165
|
<p>Computed in one pass over the display rows and cached against that ordering, so a hundred thousand rows are walked once per sort rather than once per cell. A running column is not sortable. Sorting on one asks the sort to depend on its own output (the value is defined by the display order) so the column does not offer a sort unless its definition asks for one, and the query layer refuses such a sort with a warning rather than computing it. Sort by the column it runs over instead. Group headings and totals rows are skipped, a running total that counted a subtotal would double everything below it, and a row with no value carries the figure forward unchanged rather than resetting it.</p>
|
|
2166
2166
|
|
|
2167
|
+
<h4 id="history-window">History has two clocks: a count of changes, or a span of time (BACKLOG-0001043)</h4>
|
|
2168
|
+
<p>Plain <code>history</code> (above) is an <em>event</em> series: it records a reading only when the value changes, so a row that sits still never advances it. Bound to a <code>cell.render</code> sparkline (<code>'line'</code>, <code>'area'</code>, <code>'column'</code>, <code>'winloss'</code>) that means a static row's sparkline freezes, then jumps when a change finally lands — while a title like "last 60s" keeps claiming a span the column never measured. Add a <em>time</em> <code>window</code> to make it a real time series instead:</p>
|
|
2169
|
+
<pre><code>{ id: 'spark', title: 'Last 60s', shadow: { of: 'price', kind: 'history', window: { kind: 'time', span: 60_000 }, depth: 20 } }</code></pre>
|
|
2170
|
+
<p>This is the same <code>window: { kind, span }</code> shape the rolling kinds below already accept — not a second spelling of it — and it changes what <code>history</code> means rather than adding a new kind: <code>depth</code> (20 here) is now how many buckets the <code>span</code> divides into, so this is sixty seconds as twenty three-second buckets. Each bucket is sampled once, at its close, as the row's <em>last known value</em> at that moment — carried forward from the previous bucket when nothing changed in between. A static row therefore draws a flat line that keeps advancing, one sample per bucket, and a change lands in the bucket it actually happened in rather than being appended at the end. The bucket clock runs on its own low-frequency timer (never a render loop), so the series keeps moving even while no data event ever reaches the column. <code>window: { kind: 'count' }</code> and <code>{ kind: 'session' }</code> are refused for <code>history</code> — a plain count is already what <code>depth</code> means without a window, and a session has no fixed span to divide into buckets — and a caller who never sets <code>window</code> gets the original count-based behaviour, unchanged.</p>
|
|
2167
2171
|
<div class="note"><p>Positional kinds rank over every <em>tracked</em> row, not over the filtered set: a rank that changed as you filtered would make "the top ten movers" depend on what happened to be on screen, and the column would disagree with itself between two views of the same data. Pass <code>scope: 'filtered'</code> on the shadow spec to rank within what the filters left instead: both answers are legitimate, which is why it is a choice rather than a default.</p></div>
|
|
2168
2172
|
<div class="note"><p>Shadow state is keyed by row key, never by index: after any sort an index-keyed history would report one row's past against another row's present, and the wrong number would be <em>sortable</em>. Memory is capped at 200,000 tracked rows per column; past that the oldest are dropped and <code>tracking().forgotten</code> says how many, rather than a smaller number being reported as though it were the truth.</p></div>
|
|
2169
2173
|
|
|
@@ -5044,6 +5048,7 @@ plan.applyEdit({ id: 'design', duration: 7 }); // recomputes; the critical path
|
|
|
5044
5048
|
<tr><td class="sig">importMSPDI(xml, { hoursPerDay? })</td><td class="desc">Import a Microsoft Project (MSPDI) <code>.xml</code> document into a <code>{ tasks, dependencies, resources, projectStart, calendar }</code> model ready for <code>createGantt</code>: the task tree, typed dependencies with lag, constraints, baseline, %complete, resources with capacity and the resource assignments.</td></tr>
|
|
5045
5049
|
<tr><td class="sig">exportMSPDI(model, { hoursPerDay?, projectName? })</td><td class="desc">Serialise a Gantt model (optionally a scheduled one) back to Microsoft Project (MSPDI) XML — the same fields, round-tripping with <code>importMSPDI</code>. The controller offers <code>gantt.toMSPDI()</code> as a shortcut over the current plan.</td></tr>
|
|
5046
5050
|
<tr><td class="sig">computeEarnedValue(tasks, schedule, { statusDate?, costField?, actualCostField? })</td><td class="desc">Earned-value management (EVM) from the baseline and %complete at a status date: Planned Value (PV/BCWS), Earned Value (EV/BCWP), Actual Cost (AC/ACWP, from a per-task <code>actualCost</code>), plus Schedule Variance (EV−PV), Cost Variance (EV−AC), SPI (EV/PV) and CPI (EV/AC) — per task, rolled up to summaries and the project. Budget (BAC) is the task's <code>cost</code>, or its duration when no cost is given. The controller exposes <code>gantt.earnedValue({ statusDate })</code> as the shortcut; the split view surfaces the metrics through <code>kind: 'evm'</code> columns.</td></tr>
|
|
5051
|
+
<tr><td class="sig">getState() / setState(snapshot)</td><td class="desc">Serialise and restore the Gantt's own VIEWER state (BACKLOG-0001042) — which view is mounted (<code>'plain'</code>/<code>'split'</code>/<code>null</code>), zoom, the arrow/progress/baseline toggles, calendar/non-working shading, the plain view's swimlane grouping, the split view's left-panel width and its collapsed summary rows. Not the task data (already covered by <code>setTasks</code>/<code>rows.apply</code>). Versioned, JSON-safe, and <code>setState</code> tolerates an unknown, wrong-typed or newer-version snapshot without throwing, applying only what it recognises.</td></tr>
|
|
5047
5052
|
</tbody>
|
|
5048
5053
|
</table>
|
|
5049
5054
|
</div>
|
|
@@ -5526,6 +5531,67 @@ grid.destroy();
|
|
|
5526
5531
|
<span class="cmt">// Two incomplete tasks (Build, Ship) are on the critical path — at risk.</span>
|
|
5527
5532
|
<span class="kw">return</span> `${by['risk.atRisk'].display} at risk | SPI ${by['risk.spi'].display} | ${by['risk.sla.breaches'].display} breaches`;</code></pre>
|
|
5528
5533
|
|
|
5534
|
+
<h2 id="tabs">The tabbed grid</h2>
|
|
5535
|
+
<p><code>modules/tabs</code> is an opt-in top-of-grid tab strip where <strong>each tab is its own full, independently-configured grid instance</strong> — "configure each tab as per a normal grid" rather than one grid whose state is swapped. That is a deliberate rejection of the cheaper alternative: <code>grid.state.get()</code>/<code>.apply()</code> only repositions, hides, resizes and sorts <strong>existing</strong> columns by id (no field, type, editor or row data), so a state-swap only works when every tab shares one column schema and one source — strictly less than the ask. A tab may instead declare <code>from: '<tabId>'</code> plus a narrowing (<code>where</code>, <code>group</code>, <code>join</code>, …), and the module wires a <code>source: { mode: 'derived', from: <the parent tab's live grid>, … }</code> for it — the shipped derived-source mechanism, not a new config-inheritance one. <code>createGrid</code> is <strong>injected</strong> (the same pattern the React/Vue/Svelte adapters use), so the module imports no engine code regardless of how it is loaded — its own minified ESM build (<code>tabs.esm.min.js</code>) is ~65KB gzipped, the same size class as the KPI and framework-adapter modules (~61–66KB), rather than the ~700KB a module that inlines the whole engine (the web component, htmx) ships.</p>
|
|
5536
|
+
<pre><code>import { createGrid } from '@toclocoinc/lattice-grid';
|
|
5537
|
+
import { createTabs } from '@toclocoinc/lattice-grid/modules/tabs';
|
|
5538
|
+
|
|
5539
|
+
const tabs = createTabs(document.querySelector('#tabs'), {
|
|
5540
|
+
createGrid, <span class="cmt">// injected -- see below</span>
|
|
5541
|
+
tabs: [
|
|
5542
|
+
{ id: 'all', label: 'All', config: { rowKey: 'id', rows, columns } },
|
|
5543
|
+
{ id: 'open', label: 'Open', from: 'all', where: (r) => r.stage === 'Open',
|
|
5544
|
+
follow: 'filtered', refresh: 'live', config: { columns } },
|
|
5545
|
+
{ id: 'breached', label: 'Breached', from: 'open', <span class="cmt">// derives from Open, not All -- a chain</span>
|
|
5546
|
+
where: (r) => r.daysOverdue > 0, config: { columns } },
|
|
5547
|
+
],
|
|
5548
|
+
onBeforeTabChange: ({ id }) => !hasUnsavedEdit(), <span class="cmt">// veto a switch</span>
|
|
5549
|
+
});</code></pre>
|
|
5550
|
+
<p><strong>Lifecycle.</strong> A tab's grid mounts on <em>first activation</em>, not up front, and then stays alive — hidden, never destroyed — until the whole strip is. Per-tab scroll, selection, filters, sort, grouping, expansion — and an open cell/row editor — therefore survive a switch away and back natively, by simply not touching that grid instance, rather than through a lossy serialise/restore round-trip: leave a tab mid-edit, switch away, switch back, and the editor is exactly as it was left, uncommitted and undiscarded. Activating a derived tab materialises its whole ancestor chain first (mounted, hidden), and a cyclic <code>from</code> graph is refused — naming the exact cycle — when <code>createTabs</code> is called, not at first click.</p>
|
|
5551
|
+
<p><strong>A hidden tab costs nothing this module can spend.</strong> An inactive panel carries the <code>hidden</code> attribute (<code>display:none</code>); the module runs no timer, observer or repaint of its own against it. Measured with a real browser (<code>bench/tabs-idle.mjs</code>): several mounted-but-hidden, untouched tabs cost the same idle CPU as none at all. The one honestly-reported exception is not this module's: a <em>derived</em> tab's row model still re-derives on every change to its (possibly hidden) parent — by design, so reactivating it is instant rather than a stale flash — and the engine's own repaint listener for a derived source's <code>rows:changed</code> calls the renderer directly, bypassing <code>grid.updates.pause()</code> (which only holds the streaming-ingestion path). A hidden derived tab therefore still runs a read/compute/write pass on every parent change, even though the write phase paints a zero-size viewport; the bench measures and reports the size of that gap rather than leaving it inferred.</p>
|
|
5552
|
+
<p><strong>Accessibility.</strong> A real <code>role="tablist"</code>/<code>"tab"</code>/<code>"tabpanel"</code> with <code>aria-selected</code> and a roving <code>tabindex</code>, imitating the grid's own column-header keyboard model rather than the tool panel's tablist (which has the roles but no arrow-key handling). This is <strong>manual activation</strong>: <kbd>←</kbd>/<kbd>→</kbd> and <kbd>Home</kbd>/<kbd>End</kbd> move the roving tab stop without switching the panel or mounting a grid; <kbd>Enter</kbd>/<kbd>Space</kbd>, or a click, activates. The newly active tab's label is announced through a polite live region.</p>
|
|
5553
|
+
<div class="table-wrap">
|
|
5554
|
+
<table>
|
|
5555
|
+
<thead><tr><th>Member</th><th>Description</th></tr></thead>
|
|
5556
|
+
<tbody>
|
|
5557
|
+
<tr><td class="sig">createTabs(el, config)</td><td class="desc">Create a tabbed grid. <code>config.createGrid</code> is required (injected, not imported); <code>config.tabs</code> is a non-empty array of tab descriptors, each an <code>id</code>, a <code>label</code>, a grid <code>config</code>, and optionally <code>from</code> plus the derivation narrowing (<code>where</code>, <code>group</code>, <code>groupBy</code>, <code>bucket</code>, <code>join</code>, <code>unnest</code>, <code>refresh</code>, <code>crossFilter</code>, <code>follow</code>, <code>limit</code>, <code>sort</code>, <code>profile</code>) forwarded onto the derived source built for it.</td></tr>
|
|
5558
|
+
<tr><td class="sig">tabs() / tab(id) / isMounted(id)</td><td class="desc">The configured tab ids, in order; a tab's live grid instance (or <code>null</code> before its first activation); whether a tab has been materialised yet.</td></tr>
|
|
5559
|
+
<tr><td class="sig">activate(id, opts)</td><td class="desc">Switch the active tab, gated by <code>beforeTabChange</code>. Returns <code>true</code>/<code>false</code> synchronously with no handler registered, or a <code>Promise<boolean></code> when a handler deferred.</td></tr>
|
|
5560
|
+
<tr><td class="sig">on(name, fn) / off(name, fn)</td><td class="desc">Events: <code>tab:changed</code>, the cancellable <code>beforeTabChange</code> (call <code>preventDefault(reason?)</code> or return <code>false</code> to veto), and its paired <code>tabChange:cancelled</code>. Config sugar: <code>onTabChange</code>, <code>onBeforeTabChange</code>, <code>onTabChangeCancelled</code>.</td></tr>
|
|
5561
|
+
<tr><td class="sig">destroy()</td><td class="desc">Tear the whole strip down; destroys every mounted tab's grid (each isolated, so one throwing does not strand the rest).</td></tr>
|
|
5562
|
+
</tbody>
|
|
5563
|
+
</table>
|
|
5564
|
+
</div>
|
|
5565
|
+
<h3 id="tabs-live-example">All / Open / Breached, a two-deep derivation chain, executed</h3>
|
|
5566
|
+
<p class="section-note"><code>createTabs</code> deliberately has no headless mode — it requires a real host element, the same way <code>createGrid</code> itself does — so this executed example reaches for the same in-tree DOM test double the suite itself runs the renderer against headlessly (<code>packages/dom/src/renderer/testdom.js</code>), rather than a real browser. <code>demo/tabs.html</code> is the browser version of the same chain, with buttons that edit All directly and let Open and Breached follow.</p>
|
|
5567
|
+
<pre data-run="js" data-expect="4,3,2" data-covers="export:createTabs"><code><span class="kw">const</span> { createTestDom } = <span class="kw">await</span> import('../packages/dom/src/renderer/testdom.js');
|
|
5568
|
+
<span class="kw">const</span> { root } = createTestDom();
|
|
5569
|
+
<span class="kw">const</span> { createGrid } = <span class="kw">await</span> import('../packages/dom/src/index.js');
|
|
5570
|
+
<span class="kw">const</span> { createTabs } = <span class="kw">await</span> import('../packages/modules/tabs/index.js');
|
|
5571
|
+
|
|
5572
|
+
<span class="kw">const</span> rows = [
|
|
5573
|
+
{ id: 1, stage: 'Open', daysOverdue: 0 },
|
|
5574
|
+
{ id: 2, stage: 'Open', daysOverdue: 5 },
|
|
5575
|
+
{ id: 3, stage: 'Won', daysOverdue: 0 },
|
|
5576
|
+
{ id: 4, stage: 'Open', daysOverdue: 2 },
|
|
5577
|
+
];
|
|
5578
|
+
<span class="kw">const</span> columns = [{ field: 'id' }, { field: 'stage' }, { field: 'daysOverdue' }];
|
|
5579
|
+
|
|
5580
|
+
<span class="kw">const</span> tabs = createTabs(root, {
|
|
5581
|
+
createGrid,
|
|
5582
|
+
tabs: [
|
|
5583
|
+
{ id: 'all', label: 'All', config: { rowKey: 'id', rows, columns } },
|
|
5584
|
+
{ id: 'open', label: 'Open', from: 'all', where: (r) => r.stage === 'Open', config: { columns } },
|
|
5585
|
+
<span class="cmt">// derives from Open, not All -- a two-deep chain</span>
|
|
5586
|
+
{ id: 'breached', label: 'Breached', from: 'open', where: (r) => r.daysOverdue > 0, config: { columns } },
|
|
5587
|
+
],
|
|
5588
|
+
});
|
|
5589
|
+
|
|
5590
|
+
tabs.activate('breached'); <span class="cmt">// materialises 'open' too, automatically</span>
|
|
5591
|
+
<span class="kw">const</span> counts = ['all', 'open', 'breached'].map((id) => tabs.tab(id).rows.count());
|
|
5592
|
+
tabs.destroy();
|
|
5593
|
+
<span class="kw">return</span> counts.join(','); <span class="cmt">// 4,3,2</span></code></pre>
|
|
5594
|
+
|
|
5529
5595
|
<h2 id="mocksocket">The mock socket</h2>
|
|
5530
5596
|
<p><code>modules/mock-socket</code> is a serverless stand-in for a live <code>WebSocket</code> feed, for building and demonstrating a real-time UI with <strong>no backend</strong>. <code>MockWebSocket</code> presents the same surface as the browser's <code>WebSocket</code> — the same <code>readyState</code> and state constants, the same <code>onopen</code>, <code>onmessage</code>, <code>onclose</code> and <code>onerror</code>, <code>addEventListener</code>, <code>send</code> and <code>close</code> — so the code that reads it does not change when it is swapped for a real one. It fires an initial snapshot the moment it opens, then a stream of deltas on a timer, all from a generator you hand it. It is a dev and test utility: optional, imports nothing from the grid, and is never pulled into the core bundle. It pairs naturally with the data router (one mock stream, partitioned to many grids), but depends on it no more than a real socket does.</p>
|
|
5531
5597
|
<pre><code>import { MockWebSocket, opsFeed } from '@toclocoinc/lattice-grid/modules/mock-socket';
|
package/docs/api-detail.html
CHANGED
|
@@ -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.
|
|
440
|
+
<p class="rail__sub">Developer guide · v1.50.0</p>
|
|
441
441
|
<nav>
|
|
442
442
|
<div class="rail__group">
|
|
443
443
|
<span class="rail__label">Start here</span>
|
|
@@ -6643,7 +6643,8 @@ grid.import.apply(preview);</code></pre>
|
|
|
6643
6643
|
<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×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>
|
|
6644
6644
|
<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><figure></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>
|
|
6645
6645
|
<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>
|
|
6646
|
-
|
|
6646
|
+
<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: '<tabId>'</code> gets a <code>source: { mode: 'derived', from: <the parent tab’s live grid>, 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>
|
|
6647
|
+
<tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
|
|
6647
6648
|
<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>
|
|
6648
6649
|
<tr><td class="name">createLatticeGridElement</td><td class="desc">Build the element class without registering it, for a custom registry.</td></tr>
|
|
6649
6650
|
<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>
|
package/lattice-grid.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Lattice Grid 1.
|
|
2
|
+
* Lattice Grid 1.50.0, type declarations
|
|
3
3
|
* Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
|
|
4
4
|
* https://latticegrid.dev
|
|
5
5
|
*/
|
|
@@ -729,6 +729,12 @@ export interface Column {
|
|
|
729
729
|
shadow?: ShadowKind | {
|
|
730
730
|
of?: string;
|
|
731
731
|
kind: ShadowKind;
|
|
732
|
+
/**
|
|
733
|
+
* For `kind: 'history'`, how many past readings to keep (20 by default). With
|
|
734
|
+
* a time `window` (BACKLOG-0001043) this is instead how many buckets the span
|
|
735
|
+
* divides into — `window: {kind: 'time', span: 60_000}, depth: 20` is sixty
|
|
736
|
+
* one-second buckets. Ignored by every other kind.
|
|
737
|
+
*/
|
|
732
738
|
depth?: number;
|
|
733
739
|
/**
|
|
734
740
|
* For a positional kind, what to rank against. `'all'` (the default) uses
|
|
@@ -768,6 +774,16 @@ export interface Column {
|
|
|
768
774
|
* (`time`), or everything so far (`session`). The first rows of a series
|
|
769
775
|
* carry a partial window, stamped by a `windowCoverage` companion rather than
|
|
770
776
|
* dressed as full.
|
|
777
|
+
*
|
|
778
|
+
* For `kind: 'history'` (BACKLOG-0001043), only `{kind: 'time', span}` (or
|
|
779
|
+
* `minutes`) applies, and it changes what `history` means rather than what it
|
|
780
|
+
* aggregates: the `depth` buckets that span divides into are read once each,
|
|
781
|
+
* carrying the row's last known value forward into any bucket in which it did
|
|
782
|
+
* not change, so a static row still draws a flat, advancing line instead of
|
|
783
|
+
* freezing — the plain count-based history (no `window`) is a count of
|
|
784
|
+
* *changes* and stays exactly as it was. `count` and `session` are refused
|
|
785
|
+
* here: a plain count is already what `depth` means, and a session has no
|
|
786
|
+
* fixed span to divide into buckets.
|
|
771
787
|
*/
|
|
772
788
|
window?: {
|
|
773
789
|
kind: 'count' | 'time' | 'session';
|
|
@@ -8808,3 +8824,108 @@ declare module 'lattice-grid/modules/ai' {
|
|
|
8808
8824
|
|
|
8809
8825
|
export default createAI;
|
|
8810
8826
|
}
|
|
8827
|
+
|
|
8828
|
+
declare module 'lattice-grid/modules/tabs' {
|
|
8829
|
+
/**
|
|
8830
|
+
* One tab: an id, a display label, a grid config, and — for a derived tab —
|
|
8831
|
+
* the parent tab id plus the narrowing forwarded onto the derived source
|
|
8832
|
+
* built for it (`source: { mode: 'derived', from: <parent's grid>, ... }`).
|
|
8833
|
+
* The derivation keys are the ones `packages/core/src/source/derive.js`
|
|
8834
|
+
* already understands; this module invents none of its own.
|
|
8835
|
+
*/
|
|
8836
|
+
interface TabDescriptor {
|
|
8837
|
+
/** A stable, unique id. Required. */
|
|
8838
|
+
id: string;
|
|
8839
|
+
/** The tab button's text. Defaults to `id`. */
|
|
8840
|
+
label?: string;
|
|
8841
|
+
/** The grid config passed to `createGrid` for this tab (merged with the derived `source`, when `from` is set). */
|
|
8842
|
+
config?: object;
|
|
8843
|
+
/** The parent tab id to derive from. When set, `config.source` is built for you and any of your own is replaced (with a warning). */
|
|
8844
|
+
from?: string;
|
|
8845
|
+
/** Row predicate forwarded to the derived source. */
|
|
8846
|
+
where?: (row: unknown) => boolean;
|
|
8847
|
+
/** Group-by forwarded to the derived source. */
|
|
8848
|
+
group?: unknown;
|
|
8849
|
+
groupBy?: unknown;
|
|
8850
|
+
/** Time-bucketing forwarded to the derived source. */
|
|
8851
|
+
bucket?: unknown;
|
|
8852
|
+
/** Join spec forwarded to the derived source. */
|
|
8853
|
+
join?: unknown;
|
|
8854
|
+
/** Array-field unnesting forwarded to the derived source. */
|
|
8855
|
+
unnest?: unknown;
|
|
8856
|
+
/** `'live' | 'idle' | 'manual' | number` forwarded to the derived source. */
|
|
8857
|
+
refresh?: 'live' | 'idle' | 'manual' | number;
|
|
8858
|
+
/** Cross-filter wiring forwarded to the derived source. */
|
|
8859
|
+
crossFilter?: unknown;
|
|
8860
|
+
/** Which slice of the parent's rows to derive from: `'filtered' | 'all' | 'selected' | 'grouped'`. */
|
|
8861
|
+
follow?: 'filtered' | 'all' | 'selected' | 'grouped';
|
|
8862
|
+
/** Row limit forwarded to the derived source. */
|
|
8863
|
+
limit?: number;
|
|
8864
|
+
/** Sort forwarded to the derived source. */
|
|
8865
|
+
sort?: unknown;
|
|
8866
|
+
/** Statistical-profile derivation, forwarded to the derived source. */
|
|
8867
|
+
profile?: unknown;
|
|
8868
|
+
/** This tab's panel's own `aria-label`, when the label alone is not enough context. */
|
|
8869
|
+
ariaLabel?: string;
|
|
8870
|
+
}
|
|
8871
|
+
|
|
8872
|
+
/** The payload every tab-change event carries. */
|
|
8873
|
+
interface TabChangeEvent {
|
|
8874
|
+
id: string;
|
|
8875
|
+
previousId: string | null;
|
|
8876
|
+
origin?: 'api' | 'user' | 'init';
|
|
8877
|
+
reason?: string | null;
|
|
8878
|
+
/** Cancel the switch (only meaningful on `beforeTabChange`). */
|
|
8879
|
+
preventDefault?: (reason?: string) => void;
|
|
8880
|
+
defaultPrevented?: boolean;
|
|
8881
|
+
}
|
|
8882
|
+
|
|
8883
|
+
/** Tabbed-grid configuration. */
|
|
8884
|
+
interface TabsConfig {
|
|
8885
|
+
/** The grid factory to mount each tab with, e.g. `import { createGrid } from 'lattice-grid'`. Required. */
|
|
8886
|
+
createGrid: (el: HTMLElement, config: object) => unknown;
|
|
8887
|
+
/** The tabs, in display order. Required, at least one. */
|
|
8888
|
+
tabs: TabDescriptor[];
|
|
8889
|
+
/** The initially active tab id. Defaults to the first tab. */
|
|
8890
|
+
active?: string;
|
|
8891
|
+
/** The tablist landmark's accessible name. */
|
|
8892
|
+
ariaLabel?: string;
|
|
8893
|
+
/** An explicit message-catalogue override; otherwise a mounted tab's own `grid.messages` is used. */
|
|
8894
|
+
messages?: { t(key: string, params?: Record<string, unknown>): string };
|
|
8895
|
+
onTabChange?: (event: TabChangeEvent) => void;
|
|
8896
|
+
onBeforeTabChange?: (event: TabChangeEvent) => boolean | void | Promise<boolean>;
|
|
8897
|
+
onTabChangeCancelled?: (event: TabChangeEvent) => void;
|
|
8898
|
+
}
|
|
8899
|
+
|
|
8900
|
+
/**
|
|
8901
|
+
* A tabbed grid: a `role="tablist"` strip above a stack of `role="tabpanel"`
|
|
8902
|
+
* regions, each hosting its own, independently-configured grid instance
|
|
8903
|
+
* (BACKLOG-0001039). A tab's grid mounts on first activation and is kept
|
|
8904
|
+
* alive, hidden, until `destroy()`.
|
|
8905
|
+
*/
|
|
8906
|
+
interface Tabs {
|
|
8907
|
+
readonly el: HTMLElement;
|
|
8908
|
+
/** The currently active tab id. */
|
|
8909
|
+
readonly activeId: string;
|
|
8910
|
+
/** The configured tab ids, in order. */
|
|
8911
|
+
tabs(): string[];
|
|
8912
|
+
/** The live grid instance for a tab, or `null` before it has been materialised. */
|
|
8913
|
+
tab(id: string): unknown | null;
|
|
8914
|
+
/** Whether a tab's grid has been created yet. */
|
|
8915
|
+
isMounted(id: string): boolean;
|
|
8916
|
+
/** Switch the active tab, gated by `beforeTabChange`. */
|
|
8917
|
+
activate(id: string, opts?: { origin?: 'api' | 'user' }): boolean | Promise<boolean>;
|
|
8918
|
+
on(name: 'beforeTabChange' | 'tab:changed' | 'tabChange:cancelled' | string, fn: (event: TabChangeEvent) => void): () => void;
|
|
8919
|
+
off(name: string, fn: (event: TabChangeEvent) => void): void;
|
|
8920
|
+
/** Tear the whole strip down; destroys every mounted tab's grid. */
|
|
8921
|
+
destroy(): void;
|
|
8922
|
+
}
|
|
8923
|
+
|
|
8924
|
+
/**
|
|
8925
|
+
* Create a tabbed grid over a host element. Each tab is a full,
|
|
8926
|
+
* independently-configured grid instance; a tab may derive from another via
|
|
8927
|
+
* `from`, reusing the shipped `source: { mode: 'derived' }` mechanism.
|
|
8928
|
+
*/
|
|
8929
|
+
export function createTabs(el: HTMLElement, config: TabsConfig): Tabs;
|
|
8930
|
+
export default createTabs;
|
|
8931
|
+
}
|