@toclocoinc/lattice-grid 1.52.0 → 1.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/docs/API.html +78 -1
- package/docs/api-detail.html +3 -2
- package/lattice-grid.d.ts +251 -1
- package/lattice-grid.esm.min.js +48 -14
- package/lattice-grid.min.cjs +48 -14
- package/lattice-grid.min.js +48 -14
- package/modules/ai.esm.min.js +18 -4
- package/modules/ai.min.cjs +18 -4
- package/modules/ai.min.js +18 -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 +83 -5
- package/modules/charts.min.cjs +83 -5
- package/modules/charts.min.js +83 -5
- 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 +63 -4
- package/modules/gantt.min.cjs +63 -4
- package/modules/gantt.min.js +63 -4
- package/modules/htmx.esm.min.js +48 -14
- package/modules/htmx.min.cjs +48 -14
- package/modules/htmx.min.js +48 -14
- 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 +168 -17
- package/modules/kpi.min.cjs +168 -17
- package/modules/kpi.min.js +168 -17
- package/modules/layout.esm.min.js +1825 -0
- package/modules/layout.min.cjs +1828 -0
- package/modules/layout.min.js +1828 -0
- 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 +5 -4
- package/modules/tabs.min.cjs +5 -4
- package/modules/tabs.min.js +5 -4
- 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 +48 -14
- package/modules/webcomponent.min.cjs +48 -14
- package/modules/webcomponent.min.js +48 -14
- 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.53.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -350,6 +350,7 @@ the UMD build or `.cjs` for CommonJS.
|
|
|
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
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`). |
|
|
353
|
+
| layout | `@toclocoinc/lattice-grid/modules/layout` | `modules/layout.min.js` | `LatticeGridLayout` | A reconfigurable dashboard: windows on a cell grid, moved and resized by drag or keyboard (`createLayout`). |
|
|
353
354
|
| ai | `@toclocoinc/lattice-grid/modules/ai` | `modules/ai.min.js` | `LatticeGridAI` | Bring-your-own-model narrative and insights grounded on computed figures (`createAI`). |
|
|
354
355
|
| 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`). |
|
|
355
356
|
| 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
|
@@ -420,6 +420,7 @@
|
|
|
420
420
|
<a href="#quickfilter">Quick filter</a>
|
|
421
421
|
<a href="#units">Units of your own</a>
|
|
422
422
|
<a href="#chartsmodule">The charts module</a>
|
|
423
|
+
<a href="#layout">The dashboard layout</a>
|
|
423
424
|
<a href="#mocksocket">The mock socket</a>
|
|
424
425
|
<a href="#charts">In-cell charts</a>
|
|
425
426
|
<a href="#formulas">Formulas</a>
|
|
@@ -5715,7 +5716,7 @@ const kpi = createKPI(document.querySelector('#kpis'), {
|
|
|
5715
5716
|
<tbody>
|
|
5716
5717
|
<tr><td class="sig">createKPI(el, config)</td><td class="desc">Create a KPI panel. Pass a DOM element to render into, or <code>null</code> for a headless panel that computes the same tile model without a DOM.</td></tr>
|
|
5717
5718
|
<tr><td class="sig">rows.apply({ add, update, remove })</td><td class="desc">The keyed-diff consumer contract a grid shares, so the panel is a drop-in Data Router target and updates each tile incrementally. Also <code>rows.forEach</code> and <code>rows.count</code>.</td></tr>
|
|
5718
|
-
<tr><td class="sig">tiles() / tile(id) / value(id)</td><td class="desc">Every computed tile model, one tile by id (<code>value</code>, <code>formatted</code>, <code>status</code>, <code>delta</code>, <code>deltaPercent</code>, <code>count</code>, <code>sparkline</code>), or a tile's raw value. <code>status</code> is <code>good</code>, <code>warn</code>, <code>critical</code>, <code>unknown</code> (the panel holds no rows) or <code>null</code> (no thresholds configured); <code>value</code> is <code>null</code> whenever the tile measured nothing.</td></tr>
|
|
5719
|
+
<tr><td class="sig">tiles() / tile(id) / value(id)</td><td class="desc">Every computed tile model, one tile by id (<code>value</code>, <code>formatted</code>, <code>status</code>, <code>delta</code>, <code>deltaPercent</code>, <code>count</code>, <code>sparkline</code>, <code>bar</code>), or a tile's raw value. <code>status</code> is <code>good</code>, <code>warn</code>, <code>critical</code>, <code>unknown</code> (the panel holds no rows) or <code>null</code> (no thresholds configured); <code>value</code> is <code>null</code> whenever the tile measured nothing.</td></tr>
|
|
5719
5720
|
<tr><td class="sig">setRows(rows) / refresh()</td><td class="desc">Replace the source rows, or re-seed and recompute.</td></tr>
|
|
5720
5721
|
<tr><td class="sig">nodes() / node(key) / visibleNodes()</td><td class="desc">The hierarchy, when <code>tree</code> resolves one: the top-level nodes with their children, one node by key at any depth, or just the nodes on screen. Each node carries <code>label</code>, <code>level</code>, <code>tile</code> (null on a synthesised level), <code>status</code>, <code>rollup</code> (the worst severity at or below it, never <code>unknown</code>), <code>unknown</code> (how many below it measured nothing) and <code>items</code>. Empty on a flat panel, where <code>kpi.tree</code> is <code>false</code>.</td></tr>
|
|
5721
5722
|
<tr><td class="sig">expand(key) / collapse(key) / toggle(key)</td><td class="desc">Open or close a branch. A key for a branch the panel does not (yet) hold is retained rather than dropped, so a delta that later introduces it finds it already open.</td></tr>
|
|
@@ -5747,6 +5748,9 @@ kpi.expand(kpi.nodes()[0].key); <span class="cmt">// open it</span></code><
|
|
|
5747
5748
|
<p><strong>No value rolls up; severity does.</strong> A parent shows no aggregated number. That is not a simplification: the running accumulators expose <code>add</code>/<code>remove</code>/<code>value</code> and no merge, so <code>avg</code>, <code>countDistinct</code> and a <code>custom</code> reducer cannot be composed from their children without rescanning, and a per-aggregation exception list would be a number that is right for a sum and wrong for an average. A parent that has a tile of its own still shows <em>that tile's</em> reading. What does roll up is the status: <code>rollup</code> is the worst severity at or below the node, across as many levels as you have, and it is what a collapsed branch reports.</p>
|
|
5748
5749
|
<p><strong>Nothing measured is not good news, and it does not win the roll-up either.</strong> A leaf that measured nothing is <code>unknown</code> (see above), and <code>unknown</code> is deliberately excluded from <code>rollup</code>: ranking “not measured” as the worst thing beneath a parent would hide a real warning under it. It is surfaced separately instead — <code>node.unknown</code> counts the descendants that measured nothing, and the node renders it in words (“2 unknown”) and puts it in its accessible name. So neither way of being wrong is available: silence cannot read as green, and it cannot bury an amber.</p>
|
|
5749
5750
|
<p><strong>Accessible by construction.</strong> The rail is a real <code>role="tree"</code> with the APG keyboard model — <kbd>→</kbd> opens a closed branch and otherwise steps into it, <kbd>←</kbd> closes an open one and otherwise steps out to its parent, <kbd>↑</kbd>/<kbd>↓</kbd> walk what is visible, <kbd>Home</kbd>/<kbd>End</kbd> jump to its ends, <kbd>Enter</kbd>/<kbd>Space</kbd> activate — with a roving tabindex, and <code>aria-level</code>, <code>aria-posinset</code> and <code>aria-setsize</code> on every node, because a reader cannot count what a collapsed branch has left out of the DOM. Status carries a <strong>shape</strong> as well as a colour (a filled circle, a triangle, a square, a hollow circle), not one dot in three colours, and a parent's rolled-up status is <em>in its accessible name</em>: “Compute, 6 items, worst status critical”, announced as one string. Every phrase is a catalogue key: pass <code>messages</code> (any <code>{ t(key, params) }</code>, including a grid's own) to translate the panel, and a key your catalogue lacks falls back to English rather than printing the key.</p>
|
|
5751
|
+
<p><strong>Every leaf carries a value <em>and</em> a meter.</strong> A rail exists to be read at a glance, and a column of numbers is not that, so each leaf draws a small fixed-scale bar beside its reading. The scale comes only from what the tile already declares — there is no new configuration key. A <code>bands</code> list states its own ends and is taken at its word; <code>thresholds</code> states two interior cut points and a lone <code>target</code> states one point, so in those the open end is anchored at the <strong>origin</strong>: <code>{ warn: 70, critical: 90 }</code> measures 0–90, and <code>target: 4000</code> measures 0 to the target. The scale is never derived from the data — a bar scaled to the values currently in the panel would mean something different on every refresh — so a tile with no bands, no thresholds and no target gets <strong>no meter at all</strong>, a value and nothing else. The meter is placed by exactly the mapping the grid's own conditional-formatting data bar uses, <code>clamp((x - lo) / span)</code>, so a reading past the top of its scale fills the bar rather than overflowing it, and it is coloured by the leaf's own <code>good</code>/<code>warn</code>/<code>critical</code> status — reinforcing the shape glyph, never replacing it. A leaf that measured nothing draws a <em>dashed empty outline with no fill element</em>, which is deliberately not what a measured zero looks like (a solid track with a fill of no length): those are different facts. Each tile model carries the same numbers as <code>bar</code> (<code>{ lo, hi, percent }</code>, or <code>null</code> when no scale is declared) for a host that would rather draw its own. A row that gets no meter <strong>reserves the width of one</strong>, so the readings stay in a single column whether or not there is a bar beside them — the same reservation a leaf makes on the other side for the <code>+</code>/<code>−</code> control it does not have. A branch draws no meter, because no value rolls up to draw one; and a flat panel of tiles renders exactly the tiles it always did.</p>
|
|
5752
|
+
<p><strong>The panel follows the grid it belongs to, not the page.</strong> Pass <code>grid</code> and the panel takes that grid's <em>resolved</em> type and row rhythm: on a 16px page beside a grid painting its cells at 12.7px, a rail used to draw 16px text. Type reads <code>--lat-kpi-* > --lattice-* > --lat-chrome-* > 13px</code> and row height reads <code>--lat-kpi-row-height > --lat-chrome-row-height > 22px</code>. The <code>--lat-chrome-*</code> rung is mirrored off the mounted grid in JS (<code>modules/shared/chrome.js</code>) because a grid's <code>density</code> lands on the grid's own root, which is a <em>descendant</em> of a panel mounted beside it, and custom properties inherit downward only — no stylesheet can read it. Measured in Chrome: identical type and a 1.00× leaf-row-to-grid-row ratio at <code>compact</code>, <code>comfortable</code> and <code>spacious</code>. It re-mirrors on the grid's <code>config:changed</code>, so <code>grid.set('density', …)</code> moves the panel with it, and <code>destroy()</code> releases everything it wrote. A panel with <strong>no</strong> grid — a plain <code>rows</code> array, or the router-driven panel — mirrors nothing and renders at the module's own defaults, and a host's <code>--lattice-font-size</code> on an ancestor still out-ranks the mirror.</p>
|
|
5753
|
+
<p><strong>A heading says it opens.</strong> Each top-level item carries a <code>+</code> when shut and a <code>−</code> when open, in a small bordered box, on a row with a pointer cursor and a hover state — a disclosure triangle read as decoration rather than as a control. It is purely decoration: <code>aria-expanded</code> on the node is what states the same thing to a screen reader, so the marker is <code>aria-hidden</code> exactly as the status shape is, and the announced name is unchanged. A leaf keeps the marker's width as an empty spacer, so the status shapes and labels stay in one column whether or not the row beside them opens.</p>
|
|
5750
5754
|
<p><strong>A collapsed branch costs data, not paint.</strong> Every node's status is computed whether or not you can see it — that is what makes the rail worth having — while a collapsed branch contributes no DOM at all. It is the same division the grid's grouping already makes between its totals walk and its display walk. Expansion is patched in place, keyed on the node, so a live routed feed does not throw a keyboard user off the node they are standing on.</p>
|
|
5751
5755
|
<h3 id="kpi-tree-example">A collapsed parent reports the red leaf underneath it, executed</h3>
|
|
5752
5756
|
<p class="section-note">Two subsystems from dotted tile ids, one of them in breach, with nothing expanded; then the same
|
|
@@ -5978,6 +5982,79 @@ tabs.activate('breached'); <span class="cmt">// materialises
|
|
|
5978
5982
|
tabs.destroy();
|
|
5979
5983
|
<span class="kw">return</span> counts.join(','); <span class="cmt">// 4,3,2</span></code></pre>
|
|
5980
5984
|
|
|
5985
|
+
<h2 id="layout">The dashboard layout</h2>
|
|
5986
|
+
<p><code>modules/layout</code> is an opt-in <strong>reconfigurable dashboard surface</strong>: a cell grid inside an element, and a set of windows placed on it that a user can move, resize and close — by drag <em>or</em> by keyboard. It is the thing a customer would otherwise reach for GridStack or react-grid-layout to get, which means a second dependency, a second sizing model, and a seam where the viewers in it do not resize properly.</p>
|
|
5987
|
+
<p><strong>It is payload-agnostic, and that is the whole design.</strong> A window body is a <code>div</code> with an <code>id</code>. The module creates it, sizes it, and never reads or writes its contents — it does not import <code>createGrid</code>, does not know what a payload is, and never calls into one. What it does instead is emit <code>window:resized</code> with the measured content box, which is the contract. That rule is what keeps its own code to <strong>11,202 bytes gzipped</strong> (measured: a 75,502-byte bundle over a 62,206-byte fixed floor, of which 2,094 bytes are the two shared module helpers) and what makes it usable for a payload we have not written yet.</p>
|
|
5988
|
+
<pre><code>import { createLayout } from '@toclocoinc/lattice-grid/modules/layout';
|
|
5989
|
+
|
|
5990
|
+
const layout = createLayout(document.querySelector('#dash'), {
|
|
5991
|
+
columns: 12, rows: 8, gap: 8,
|
|
5992
|
+
overflowX: 'static', overflowY: 'scroll', rowHeight: '160px',
|
|
5993
|
+
windows: [
|
|
5994
|
+
{ id: 'pipeline', title: 'Pipeline', xPos: 1, yPos: 1, xSize: 6, ySize: 4,
|
|
5995
|
+
movable: true, resizable: true, closable: true },
|
|
5996
|
+
{ id: 'trend', title: 'Trend', xPos: 7, yPos: 1, xSize: 6, ySize: 4,
|
|
5997
|
+
movable: true, resizable: true },
|
|
5998
|
+
],
|
|
5999
|
+
onWindowResized: ({ id, width, height }) => redraw(id, width, height),
|
|
6000
|
+
});
|
|
6001
|
+
|
|
6002
|
+
<span class="cmt">// The module made the container; you fill it and you own what is inside it.</span>
|
|
6003
|
+
createGrid(layout.payload('pipeline'), { rowKey: 'id', rows, columns });</code></pre>
|
|
6004
|
+
<p><strong>Two independent overflow axes, not one setting.</strong> <code>overflowX</code> and <code>overflowY</code> are each <code>'static'</code> or <code>'scroll'</code>, because a dashboard that scrolls both ways is ordinary and a single enum cannot express it. The difference is what happens to a track's size. A <strong>static</strong> axis divides the mounted element with <code>minmax(0, 1fr)</code> — never a bare <code>1fr</code>, whose implicit <code>auto</code> minimum lets one stubborn payload drag a track past the container. A <strong>scrolling</strong> axis repeats a <em>fixed</em> track (<code>columnWidth</code> / <code>rowHeight</code>) and the canvas extends past the viewport, which then scrolls. That is the owner's "maintaining their sizing", and it is the difference between this and <code>flex-wrap</code>: measured in a real browser, ten 200px columns in a 600px host paint at 200px each over a 2000px canvas, and <em>shrinking the host to 300px leaves the column at 200px</em> and scrolls further.</p>
|
|
6005
|
+
<p><strong>Spacing takes a real CSS length.</strong> <code>gap</code>, <code>padding</code>, <code>columnWidth</code> and <code>rowHeight</code> each accept a number (pixels), or a string: <code>'200px'</code>, <code>'25%'</code>, <code>'1fr'</code>, <code>'2rem'</code>, <code>'10vh'</code>. Percentages that sum past 100 are allowed to overflow and scroll rather than being silently scaled down, which is the honest outcome. Anything outside that vocabulary — including <code>calc()</code> and <code>var()</code> — is refused by name with one warning and replaced by the default, because the value is written into an inline style.</p>
|
|
6006
|
+
<p><strong>Rearrangement.</strong> <code>compact: 'vertical'</code> (the default) pushes displaced windows down and then pulls everything up into whatever space that left, so a window dropped into empty space falls to the top of its column — <code>window:moved</code> carries both <code>to</code> (where it was asked to go) and <code>landed</code> (where it actually ended up). <code>compact: 'none'</code> keeps every window exactly where it is put. There is no horizontal compactor: pushing sideways has no single obviously-correct direction, and getting it wrong silently rearranges a dashboard a user carefully built.</p>
|
|
6007
|
+
<p><strong>Keyboard, to the same standard as the drag.</strong> Every movable and resizable window carries a focusable handle running the full grab / move / drop / cancel model the kanban board established: <kbd>Space</kbd> or <kbd>Enter</kbd> grabs, the arrow keys move a tentative placement, <kbd>Enter</kbd> drops it through the same <code>beforeWindowMove</code> gate the pointer drag uses, and <kbd>Escape</kbd> cancels. A polite live region announces every step — grabbed, each tentative position with its column and row, dropped, cancelled, and <em>reverted</em> when a handler vetoes the drop — and focus returns to the handle afterwards. A window with <code>chrome: false</code> still gets a handle, because a movable window a keyboard user cannot move is not movable.</p>
|
|
6008
|
+
<p><strong>An “Edit layout” button, without rebuilding the dashboard.</strong> <code>closable</code>, <code>movable</code> and <code>resizable</code> also take a <em>layout-level</em> default, so unlocking a twelve-window dashboard is one setting rather than twenty-four, and <code>setInteractive(true|false|{movable, resizable, closable})</code> changes that default at runtime — unlock, let the user rearrange, lock again and save <code>getLayout()</code>. Nothing is destroyed and nothing is rebuilt, so every grid, chart and board mounted in a window survives the toggle untouched. <strong>The asymmetry is deliberate: you can always take a capability away; you can never grant one where the developer said no.</strong> <code>setInteractive(false)</code> locks every window, including one whose own spec says <code>movable: true</code>, so a dashboard hard-locks in a single call without auditing twelve window specs; <code>setInteractive(true)</code> unlocks only the windows that never opted out, so a masthead declared <code>movable: false</code> stays pinned. Both halves of the enforcement move together — the handles a window renders <em>and</em> the checks the pointer and keyboard paths make, because removing a handle stops a mouse while only the gesture check stops a keyboard user already standing on one. <strong><code>config.movable: false</code> and <code>setInteractive(false)</code> are deliberately not the same thing:</strong> the config states the <em>default</em> for windows that declare nothing — and <code>false</code> is already that default, so it takes nothing away from a window that declared <code>movable: true</code> — while <code>setInteractive(false)</code> is an <em>active lock</em> that pins every window whatever its own spec says. <code>getInteractive()</code> reports all three states rather than two: <code>undefined</code> where no layout-level default is in force, <code>true</code>, or <code>false</code> for a lock. Reporting “unset” as <code>false</code> would read correctly and round-trip wrongly, so <code>setInteractive(getInteractive())</code> is a no-op in every state, and a key carrying <code>undefined</code> means “leave this capability alone”. Interactivity is a <em>mode</em>, not part of the arrangement: <code>getLayout()</code> does not carry it, <code>setLayout()</code> does not read it, and no event fires. <strong>A locked layout is not a read-only dashboard:</strong> the module creates the payload container and never reads or writes its contents, so a grid inside a window is made read-only with the grid's own settings — a dashboard that must not be edited is two decisions, not one.</p>
|
|
6009
|
+
<p><strong>Which payloads re-lay-out on <code>window:resized</code>, honestly.</strong> The grid and the chart each own a <code>ResizeObserver</code> and respond correctly; the Gantt does too since 1.52.0; kanban and KPI do no JS work at all on a resize and need none, because they reflow by CSS construction (a kanban column keeps its 280px and the board starts scrolling). Every one of those is measured in <code>test/layout-browser.test.js</code> rather than asserted. <strong>One exception, named rather than softened:</strong> a grid column declared as a <em>percentage</em> (<code>layout: { width: '50%' }</code>) is resolved against the viewport width once and never re-resolved, so halving the window leaves the column wider than the viewport it sits in (800px host → 783px viewport, 391px column; 400px host → 383px viewport, still a 391px column). That is not caused by this module — it reproduces on a plain grid in a plain resized <code>div</code> — and until it is fixed, size grid columns inside a resizable window in pixels or with <code>flex</code>, not with percentages.</p>
|
|
6010
|
+
<p><strong>Idle cost, measured.</strong> A twelve-window dashboard is <strong>indistinguishable from a page with no layout module on it at all</strong>. Over 8 seconds of real Chrome (<code>bench/layout-idle.mjs</code>), twelve windows with empty payloads, twelve <em>independent</em> live grids in them, and a single lone grid with no layout module all sit in the same few-millisecond band — under a tenth of one percent of a core. They are not separated here because they cannot be: seven runs across two machines land between 3.1ms and 9.0ms and the ordering between them inverts run to run, so a stated delta would be reporting the noise floor. The module adds no timer, no frame loop and no polling, and owns exactly one <code>ResizeObserver</code> for the whole layout rather than one per window. <strong>The one figure that is a result rather than noise is not this module's:</strong> twelve grids <em>derived</em> from one shared parent filtered at 20Hz cost <strong>3,155–3,409ms</strong> over the same 8 seconds — several hundred times the quiet band, and stable across every run — because the engine's repaint listener for a derived source's <code>rows:changed</code> calls the renderer directly and bypasses <code>grid.updates.pause()</code>. The bench reports the size of that gap rather than leaving it inferred.</p>
|
|
6011
|
+
<p><strong>Closing a window does not destroy its payload.</strong> <code>window:closed</code> hands the payload container back; whatever you mounted inside it is yours to destroy. Stated plainly because a leaked grid per closed window is the obvious failure, and this module has no way to know that a <code>div</code> contains something with a <code>destroy()</code>.</p>
|
|
6012
|
+
<p><strong>Not in v1:</strong> horizontal compaction; per-frame drag events; nested layouts; window maximise/minimise; tabbed windows (that is <code>modules/tabs</code>); and <strong>responsive breakpoints — a twelve-window dashboard on a phone is unsolved, and this does not pretend otherwise</strong>. Server-side persistence is the host's, with <code>getLayout()</code>.</p>
|
|
6013
|
+
<div class="table-wrap">
|
|
6014
|
+
<table>
|
|
6015
|
+
<thead><tr><th>Member</th><th>Description</th></tr></thead>
|
|
6016
|
+
<tbody>
|
|
6017
|
+
<tr><td class="sig">createLayout(el, config)</td><td class="desc">Create a dashboard layout. <code>columns</code>/<code>rows</code> (default 12/6) divide the element; <code>overflowX</code>/<code>overflowY</code> are each <code>'static'</code> or <code>'scroll'</code>; <code>columnWidth</code>/<code>rowHeight</code> are the fixed track sizes a scrolling axis uses; <code>gap</code> (8px), <code>padding</code> (5px) and <code>compact</code> (<code>'vertical'</code>) complete it. A second mount on the same element is refused by name.</td></tr>
|
|
6018
|
+
<tr><td class="sig">config.windows[]</td><td class="desc">Each window: <code>id</code> (required, unique), <code>xPos</code>/<code>yPos</code>/<code>xSize</code>/<code>ySize</code> in 1-based cells (auto-placed in the first free cell when omitted), <code>title</code>, <code>chrome</code> (default <code>true</code>), and <code>closable</code>/<code>movable</code>/<code>resizable</code> (all default <code>false</code>, so a dashboard the developer wants fixed is fixed without opting out of anything; each also takes a layout-level default of the same name, which a window's own boolean overrides). <code>padding</code> and <code>payloadId</code> (default <code>`${id}-body`</code>) override per window.</td></tr>
|
|
6019
|
+
<tr><td class="sig">payload(id) / window(id) / windows()</td><td class="desc">The payload container for a window — the <code>div</code> carrying its <code>payloadId</code>, which you fill; a copy of a window's current descriptor; every window id in mount order.</td></tr>
|
|
6020
|
+
<tr><td class="sig">add(spec) / move(id, to) / close(id)</td><td class="desc">Add a window after mount (returns its payload container); move or resize one through the same before-events the drag uses; close one through <code>beforeWindowClose</code>. <code>move</code> and <code>close</code> return <code>true</code>/<code>false</code> synchronously with no handler registered, or a <code>Promise<boolean></code> when a handler deferred.</td></tr>
|
|
6021
|
+
<tr><td class="sig">getLayout() / setLayout(snapshot)</td><td class="desc">The full current arrangement as plain JSON (<code>{columns, rows, windows: [{id, xPos, yPos, xSize, ySize}]}</code>), and its restore. <code>setLayout</code> never throws on garbage, and an entry naming a window that does not exist yet is <em>retained</em> and applied when that window is added.</td></tr>
|
|
6022
|
+
<tr><td class="sig">getState() / setState(snapshot)</td><td class="desc">The versioned persistence pair, following core's and the Gantt's shape: no arguments in, one plain JSON-safe object out, and <code>setState</code> survives whatever is handed to it.</td></tr>
|
|
6023
|
+
<tr><td class="sig">setInteractive(value) / getInteractive()</td><td class="desc">Lock or unlock the whole dashboard at runtime, without destroying it. A boolean sets <code>movable</code>, <code>resizable</code> and <code>closable</code> together; an object sets only the keys it carries, and a key carrying <code>undefined</code> is treated as absent; <code>getInteractive()</code> returns the layout-level values as a copy, three-valued (<code>undefined</code> for unset, <code>true</code>, or <code>false</code> for a lock) so that <code>setInteractive(getInteractive())</code> is a no-op in every state. The config keys of the same name state the <em>default</em>; only this method takes a capability away. Locking always wins and unlocking never overrides an opt-out: <code>setInteractive(false)</code> pins a window that declared <code>movable: true</code>, and <code>setInteractive(true)</code> leaves a window that declared <code>movable: false</code> pinned. No event fires and <code>getLayout()</code> is unchanged — a mode is not an arrangement.</td></tr>
|
|
6024
|
+
<tr><td class="sig">refresh()</td><td class="desc">Re-measure every window and emit <code>window:resized</code> for those that changed. Called automatically; exposed for a host that changed something the module cannot observe, such as revealing an ancestor.</td></tr>
|
|
6025
|
+
<tr><td class="sig">on(name, fn) / off(name, fn)</td><td class="desc">Events: <code>window:moved</code>, <code>window:resized</code>, <code>window:closed</code>, <code>layout:changed</code>; the cancellable <code>beforeWindowMove</code>, <code>beforeWindowResize</code> and <code>beforeWindowClose</code> (call <code>preventDefault(reason?)</code> or return <code>false</code>), each paired with <code>windowMove:cancelled</code>, <code>windowResize:cancelled</code> and <code>windowClose:cancelled</code>. <code>'*'</code> subscribes to every past-tense event and is deliberately never delivered a before-event. Config sugar for all ten. Drag progress is <strong>not</strong> emitted per frame.</td></tr>
|
|
6026
|
+
<tr><td class="sig">destroy()</td><td class="desc">Stop observing, drop every listener including any left by a gesture in flight, and remove the DOM the module built. Whatever you mounted in a payload is yours to destroy.</td></tr>
|
|
6027
|
+
</tbody>
|
|
6028
|
+
</table>
|
|
6029
|
+
</div>
|
|
6030
|
+
<h3 id="layout-live-example">Placement, compaction and a saved arrangement, executed</h3>
|
|
6031
|
+
<p class="section-note"><code>createLayout</code> needs a real host element, the same way <code>createGrid</code> does, so this executed example reaches for the same in-tree DOM test double the suite runs the renderer against headlessly (<code>packages/dom/src/renderer/testdom.js</code>). <code>demo/layout.html</code> is the browser version, with a real grid, chart and KPI rail in three windows that you can drag, resize with the keyboard, close, save and restore.</p>
|
|
6032
|
+
<pre data-run="js" data-expect="a@1,1 b@3,1|a@3,1 b@3,2|a@1,1 b@3,1|a-body" data-covers="export:createLayout"><code><span class="kw">const</span> { createTestDom } = <span class="kw">await</span> import('../packages/dom/src/renderer/testdom.js');
|
|
6033
|
+
<span class="kw">const</span> { root } = createTestDom();
|
|
6034
|
+
<span class="kw">const</span> { createLayout } = <span class="kw">await</span> import('../packages/modules/layout/index.js');
|
|
6035
|
+
|
|
6036
|
+
<span class="kw">const</span> layout = createLayout(root, {
|
|
6037
|
+
columns: 4, rows: 4,
|
|
6038
|
+
windows: [
|
|
6039
|
+
{ id: 'a', title: 'A', xPos: 1, yPos: 1, xSize: 2, ySize: 1 },
|
|
6040
|
+
{ id: 'b', title: 'B', xSize: 2, ySize: 1 }, <span class="cmt">// no coordinates: auto-placed</span>
|
|
6041
|
+
],
|
|
6042
|
+
});
|
|
6043
|
+
<span class="kw">const</span> shape = () => layout.getLayout().windows.map((w) => `${w.id}@${w.xPos},${w.yPos}`).join(' ');
|
|
6044
|
+
|
|
6045
|
+
<span class="kw">const</span> placed = shape(); <span class="cmt">// B landed in the first free cell: 3,1</span>
|
|
6046
|
+
<span class="kw">const</span> saved = JSON.parse(JSON.stringify(layout.getLayout()));
|
|
6047
|
+
|
|
6048
|
+
layout.move('a', { xPos: 3, yPos: 1 }); <span class="cmt">// drop A on top of B</span>
|
|
6049
|
+
<span class="kw">const</span> pushed = shape(); <span class="cmt">// B is pushed down to 3,2</span>
|
|
6050
|
+
|
|
6051
|
+
layout.setLayout(saved); <span class="cmt">// the saved arrangement round-trips</span>
|
|
6052
|
+
<span class="kw">const</span> restored = shape();
|
|
6053
|
+
|
|
6054
|
+
<span class="kw">const</span> payload = layout.payload('a').id; <span class="cmt">// the container you fill: 'a-body'</span>
|
|
6055
|
+
layout.destroy();
|
|
6056
|
+
<span class="kw">return</span> [placed, pushed, restored, payload].join('|');</code></pre>
|
|
6057
|
+
|
|
5981
6058
|
<h2 id="mocksocket">The mock socket</h2>
|
|
5982
6059
|
<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>
|
|
5983
6060
|
<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.53.0</p>
|
|
441
441
|
<nav>
|
|
442
442
|
<div class="rail__group">
|
|
443
443
|
<span class="rail__label">Start here</span>
|
|
@@ -6991,7 +6991,8 @@ grid.import.apply(preview);</code></pre>
|
|
|
6991
6991
|
<tr><td class="name">createKPI</td><td class="desc">A KPI / stat-tile view (module <code>kpi</code>) of a dataset as a panel of aggregate tiles — each tile a <code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, <code>countDistinct</code> or a <code>custom</code> reducer over the routed rows, with an optional <code>filter</code> predicate, number <code>format</code> (<code>number</code>/<code>currency</code>/<code>percent</code>/<code>compact</code>), a <code>baseline</code> for a delta, semantic threshold bands (<code>thresholds</code> with two cut points and a direction, or an explicit <code>bands</code> list, giving a <code>good</code>/<code>warn</code>/<code>critical</code> status kept separate from any accent), and an optional <code>sparkline</code> series. It is a dataset viewer like any other: it consumes data through the same keyed-diff <code>rows.apply({ add, update, remove })</code> contract a grid exposes, so a Data Router can <code>attach(value, kpi)</code> and drive a KPI panel beside a grid, a kanban and a chart off one feed. Updates are incremental — a delta adjusts each tile's running accumulator by only the rows it carries (an add contributes, a remove reverses, an update reverses-then-contributes), the two bounded exceptions being a <code>min</code>/<code>max</code> whose current extreme is removed (a rescan of that tile's own value multiset) and a <code>custom</code> reducer (recomputed over the filtered store, an arbitrary function having no inverse). Each tile is a labelled <code><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>
|
|
6992
6992
|
<tr><td class="name">createAI</td><td class="desc">Create an AI narrative / insights controller (module <code>ai</code>) over a live grid. It produces a short, plain-language narrative of the grid's <em>computed</em> figures — a per-KPI / per-chart / per-column <code>explain</code>, or an <code>insights</code> panel over the current filtered view. The grid makes no AI call of its own: <code>createAI</code> imports no provider SDK, holds no key, and makes no network request; it calls one host callback, <code>ask({ system, messages, prompt, tools?, schema?, signal })</code> — your model, your key, your privacy decision — the same philosophy as the data adapters. Two grounding paths feed one guard: where the provider offers tool-use the model is given a curated read-only tool set (<code>getSchema</code>, <code>getProfile</code>, <code>getStatistics</code>, <code>getForecast</code>, <code>runQuery</code>) and the grid's own engine computes what it asks for; otherwise the module builds a facts packet from <code>grid.statistics</code>/the profile/forecasts/view counts and passes it in the prompt. Every figure in the narrative is reconciled against the values the engine produced this render — an ungrounded number is stripped before display (the number-reconciliation guard), so a hallucinated figure never reaches the user. The prompt is constrained to narrate-only; the layer is read-only and never mutates data. <code>redact</code> (a column id, a list, or a predicate) and <code>maxRows</code> bound what the module hands <code>ask()</code>, and the module sends nothing itself; an <code>ask()</code> error surfaces a friendly message and the grid stays fully usable, AI being additive rather than load-bearing. Complementary to <code>grid.ai</code> (the intent/plan skill layer): pass no <code>ask</code> and it adopts the grid's configured <code>ai.ask</code>, running the facts-packet path over it. Pass a headless grid for a DOM-free narrative; <code>facts(target)</code> returns the exact grounded packet without calling <code>ask()</code>. UMD global <code>LatticeGridAI</code>.</td></tr>
|
|
6993
6993
|
<tr><td class="name">createTabs</td><td class="desc">Create a tabbed grid (module <code>tabs</code>): a <code>role="tablist"</code> strip above a stack of <code>role="tabpanel"</code> regions, each hosting its own, independently-configured <code>createGrid</code> instance — "configure each tab as per a normal grid" rather than one grid whose state is swapped (<code>ColumnModel#applyState</code> only repositions/hides/resizes existing columns by id; it carries no field, type or row data, so a state-swap only works when every tab shares one schema). <code>createGrid</code> is injected (<code>createTabs(el, { createGrid, tabs })</code>), the same pattern the React/Vue/Svelte adapters use, so the module imports no engine code and adds nothing to a page that does not load it. A tab that names <code>from: '<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>
|
|
6994
|
-
<tr><td class="name">
|
|
6994
|
+
<tr><td class="name">createLayout</td><td class="desc">Create a reconfigurable dashboard layout (module <code>layout</code>): a cell grid inside an element, and a set of windows on it that a user can move, resize and close by drag <em>or</em> by keyboard — the surface a customer would otherwise reach for GridStack to get. It is <strong>payload-agnostic</strong>: a window body is a <code>div</code> with an <code>id</code> that the module creates, sizes and never reads, so it imports no engine code at all (not even <code>createGrid</code>) and its own code is 11,202 bytes gzipped, measured against a 62,206-byte fixed bundle floor. <code>columns</code>/<code>rows</code> divide the element; <code>overflowX</code> and <code>overflowY</code> are <em>independent</em> axes, each <code>'static'</code> (tracks divide the container with <code>minmax(0, 1fr)</code>) or <code>'scroll'</code> (tracks take a fixed <code>columnWidth</code>/<code>rowHeight</code> and the canvas extends past the viewport, so a column keeps the size it asked for — measured: shrinking a 600px host to 300px leaves a 200px column at 200px). Spacing takes a real CSS length: a number of pixels, or <code>'200px'</code>, <code>'25%'</code>, <code>'1fr'</code>, <code>'2rem'</code>; anything else is refused by name and replaced by the default, because the value reaches an inline style. Windows are placed by 1-based <code>xPos</code>/<code>yPos</code>/<code>xSize</code>/<code>ySize</code>, or auto-placed in the first free cell; <code>chrome</code> defaults on, and <code>closable</code>/<code>movable</code>/<code>resizable</code> all default <em>off</em>, so a fixed dashboard is fixed without opting out. <code>compact: 'vertical'</code> pushes displaced windows down then pulls them up (<code>window:moved</code> carries both <code>to</code> and <code>landed</code>); <code>'none'</code> keeps every window where it is put. <code>closable</code>/<code>movable</code>/<code>resizable</code> each also take a <em>layout-level</em> default of the same name, which a window's own boolean overrides, and <code>setInteractive(true|false|{movable, resizable, closable})</code> changes that default at runtime — the “Edit layout” button — without destroying the layout or any payload in it, with <code>getInteractive()</code> reading it back. Locking always wins and unlocking never overrides an opt-out: <code>setInteractive(false)</code> pins a window that declared <code>movable: true</code>, while <code>setInteractive(true)</code> leaves a window that declared <code>movable: false</code> pinned. The config key and the method are deliberately different: <code>movable: false</code> in the config states the <em>default</em> for windows that declare nothing and takes nothing away from one that opted in, whereas <code>setInteractive(false)</code> is an <em>active lock</em>. <code>getInteractive()</code> is three-valued — <code>undefined</code> for unset, <code>true</code>, or <code>false</code> for a lock — and a key carrying <code>undefined</code> is treated as absent, so <code>setInteractive(getInteractive())</code> is a no-op in every state. It moves both halves of the enforcement, the rendered handles <em>and</em> the pointer and keyboard gesture checks, and fires no event because a mode is not an arrangement — <code>getLayout()</code> neither carries it nor restores it. A locked layout is not a read-only dashboard: what is inside a window is configured with that payload's own settings. Keyboard parity with the drag: a focusable handle per window running the kanban board's grab/move/drop/cancel model, with a polite live region announcing grabbed, every tentative position, dropped, cancelled and reverted. It owns exactly <strong>one</strong> <code>ResizeObserver</code> for the whole layout, over two targets, and tells payloads their new content box through <code>window:resized</code> — it never calls into a payload, because it cannot know what one is. Events: <code>window:moved</code>, <code>window:resized</code>, <code>window:closed</code>, <code>layout:changed</code>, the cancellable <code>beforeWindowMove</code>/<code>beforeWindowResize</code>/<code>beforeWindowClose</code> and their <code>*:cancelled</code> pairs; drag progress is not emitted per frame. <code>getLayout()</code>/<code>setLayout()</code> round-trip the arrangement as plain JSON, and <code>getState()</code>/<code>setState()</code> are the versioned pair. Closing a window does <strong>not</strong> destroy its payload — the container is handed back on <code>window:closed</code> and the host owns that lifecycle. UMD global <code>LatticeGridLayout</code>.</td></tr>
|
|
6995
|
+
<tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
|
|
6995
6996
|
<tr><td class="name">createGantt</td><td class="desc">Create a project-planning Gantt controller (module <code>gantt</code>) over a task list and a dependency list. A CPM engine (<code>computeSchedule</code>) computes each task's early/late start and finish, its slack and the zero-float critical path, honouring the four link types (<code>LINK_TYPES</code>: FS/SS/FF/SF) with lag, and recomputes on every edit — emitting <code>schedule</code> or, on a dependency cycle or bad input, <code>error</code> (a code from <code>SCHEDULE_ERROR</code>). Milestones are zero-duration points; summary (WBS) tasks are derived from their children (earliest start, latest finish, weighted progress) rather than scheduled; <code>findViolations</code> flags a task placed earlier than its predecessors allow, and <code>toISODate</code> maps an engine day-number back to a calendar date.</td></tr>
|
|
6996
6997
|
<tr><td class="name">createLatticeGridElement</td><td class="desc">Build the element class without registering it, for a custom registry.</td></tr>
|
|
6997
6998
|
<tr><td class="name">createMessages</td><td class="desc">Build a message catalogue. A partial set lays over the built-in British English one.</td></tr>
|
package/lattice-grid.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Lattice Grid 1.
|
|
2
|
+
* Lattice Grid 1.53.0, type declarations
|
|
3
3
|
* Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
|
|
4
4
|
* https://latticegrid.dev
|
|
5
5
|
*/
|
|
@@ -9318,3 +9318,253 @@ declare module 'lattice-grid/modules/tabs' {
|
|
|
9318
9318
|
export function createTabs(el: HTMLElement, config: TabsConfig): Tabs;
|
|
9319
9319
|
export default createTabs;
|
|
9320
9320
|
}
|
|
9321
|
+
|
|
9322
|
+
declare module 'lattice-grid/modules/layout' {
|
|
9323
|
+
/**
|
|
9324
|
+
* One window on the cell grid.
|
|
9325
|
+
*
|
|
9326
|
+
* Deliberately **not** named `WindowSpec`: that name is already taken by the
|
|
9327
|
+
* rolling-statistics window (`{ kind: 'count'|'time'|'session', span, size }`)
|
|
9328
|
+
* and reusing it would put `kind: 'session'` next to a dashboard pane.
|
|
9329
|
+
*/
|
|
9330
|
+
interface LayoutWindow {
|
|
9331
|
+
/** A stable, unique id. Required. */
|
|
9332
|
+
id: string;
|
|
9333
|
+
/** The 1-based column the window starts in. Auto-placed when omitted. */
|
|
9334
|
+
xPos?: number;
|
|
9335
|
+
/** The 1-based row the window starts in. Auto-placed when omitted. */
|
|
9336
|
+
yPos?: number;
|
|
9337
|
+
/** How many columns it spans (default 1). */
|
|
9338
|
+
xSize?: number;
|
|
9339
|
+
/** How many rows it spans (default 1). */
|
|
9340
|
+
ySize?: number;
|
|
9341
|
+
/** The title shown in the chrome bar, and the name every control takes. */
|
|
9342
|
+
title?: string;
|
|
9343
|
+
/** Whether to draw the title bar (default `true`). */
|
|
9344
|
+
chrome?: boolean;
|
|
9345
|
+
/** Whether to offer a close button (default `false`). */
|
|
9346
|
+
closable?: boolean;
|
|
9347
|
+
/** Whether the window can be moved by drag or keyboard (default `false`). */
|
|
9348
|
+
movable?: boolean;
|
|
9349
|
+
/** Whether the window can be resized by drag or keyboard (default `false`). */
|
|
9350
|
+
resizable?: boolean;
|
|
9351
|
+
/** Padding inside the window; the layout's `padding` (default `'5px'`) otherwise. */
|
|
9352
|
+
padding?: number | string;
|
|
9353
|
+
/** The `id` given to the payload container (default `` `${id}-body` ``). */
|
|
9354
|
+
payloadId?: string;
|
|
9355
|
+
/** The window's accessible name, when the title alone is not enough context. */
|
|
9356
|
+
ariaLabel?: string;
|
|
9357
|
+
}
|
|
9358
|
+
|
|
9359
|
+
/**
|
|
9360
|
+
* The three capabilities a layout-level default and `setInteractive()` cover.
|
|
9361
|
+
*
|
|
9362
|
+
* These are the layout **defaults**, not the per-window resolution: a window
|
|
9363
|
+
* that declared `movable: false` stays pinned whatever these say.
|
|
9364
|
+
*
|
|
9365
|
+
* Three values, not two. `undefined` means no layout-level default is in force
|
|
9366
|
+
* and each window's own flag decides; `true` unlocks everything that did not
|
|
9367
|
+
* opt out; `false` is an active lock. Reporting `undefined` as `false` would
|
|
9368
|
+
* read correctly and round-trip wrongly, so it is reported as it is.
|
|
9369
|
+
*/
|
|
9370
|
+
interface LayoutInteractive {
|
|
9371
|
+
movable: boolean | undefined;
|
|
9372
|
+
resizable: boolean | undefined;
|
|
9373
|
+
closable: boolean | undefined;
|
|
9374
|
+
}
|
|
9375
|
+
|
|
9376
|
+
/** The plain, JSON-safe arrangement `getLayout()` returns and `setLayout()` takes. */
|
|
9377
|
+
interface LayoutSnapshot {
|
|
9378
|
+
columns: number;
|
|
9379
|
+
rows: number;
|
|
9380
|
+
windows: { id: string; xPos: number; yPos: number; xSize: number; ySize: number }[];
|
|
9381
|
+
}
|
|
9382
|
+
|
|
9383
|
+
/** A cell placement, as carried on the move and resize events. */
|
|
9384
|
+
interface LayoutPlacement {
|
|
9385
|
+
xPos: number;
|
|
9386
|
+
yPos: number;
|
|
9387
|
+
xSize: number;
|
|
9388
|
+
ySize: number;
|
|
9389
|
+
}
|
|
9390
|
+
|
|
9391
|
+
/** The payload of `window:moved`, `beforeWindowMove`, `beforeWindowResize`. */
|
|
9392
|
+
interface LayoutMoveEvent {
|
|
9393
|
+
id: string;
|
|
9394
|
+
from: LayoutPlacement;
|
|
9395
|
+
/** Where the window was asked to go. */
|
|
9396
|
+
to: LayoutPlacement;
|
|
9397
|
+
/** Where it actually ended up, which under `compact: 'vertical'` may differ. */
|
|
9398
|
+
landed?: LayoutPlacement;
|
|
9399
|
+
origin?: 'api' | 'user' | 'init';
|
|
9400
|
+
reason?: string | null;
|
|
9401
|
+
/** Cancel the action (only meaningful on a `before*` event). */
|
|
9402
|
+
preventDefault?: (reason?: string) => void;
|
|
9403
|
+
defaultPrevented?: boolean;
|
|
9404
|
+
}
|
|
9405
|
+
|
|
9406
|
+
/**
|
|
9407
|
+
* The payload of `window:resized` — the measured **content box** of the
|
|
9408
|
+
* payload container, not a cell count. Emitted when the container genuinely
|
|
9409
|
+
* changes size, including on the opening frame; never with a zero box.
|
|
9410
|
+
*/
|
|
9411
|
+
interface LayoutResizeEvent {
|
|
9412
|
+
id: string;
|
|
9413
|
+
payloadId: string;
|
|
9414
|
+
/** The payload container itself, so a host can act on it directly. */
|
|
9415
|
+
payload: HTMLElement;
|
|
9416
|
+
width: number;
|
|
9417
|
+
height: number;
|
|
9418
|
+
xPos: number;
|
|
9419
|
+
yPos: number;
|
|
9420
|
+
xSize: number;
|
|
9421
|
+
ySize: number;
|
|
9422
|
+
}
|
|
9423
|
+
|
|
9424
|
+
/** The payload of `window:closed` and `beforeWindowClose`. */
|
|
9425
|
+
interface LayoutCloseEvent {
|
|
9426
|
+
id: string;
|
|
9427
|
+
payloadId: string;
|
|
9428
|
+
/** The payload container, handed back so the host can destroy what it mounted. */
|
|
9429
|
+
payload?: HTMLElement;
|
|
9430
|
+
origin?: 'api' | 'user';
|
|
9431
|
+
reason?: string | null;
|
|
9432
|
+
preventDefault?: (reason?: string) => void;
|
|
9433
|
+
defaultPrevented?: boolean;
|
|
9434
|
+
}
|
|
9435
|
+
|
|
9436
|
+
/** The payload of `layout:changed`: the whole arrangement, plus what moved it. */
|
|
9437
|
+
interface LayoutChangedEvent extends LayoutSnapshot {
|
|
9438
|
+
cause: string;
|
|
9439
|
+
}
|
|
9440
|
+
|
|
9441
|
+
/** Dashboard layout configuration. */
|
|
9442
|
+
interface LayoutConfig {
|
|
9443
|
+
/** Cell columns across the mounted element (default 12). */
|
|
9444
|
+
columns?: number;
|
|
9445
|
+
/** Cell rows down the mounted element (default 6). */
|
|
9446
|
+
rows?: number;
|
|
9447
|
+
/** Horizontal overflow (default `'static'`). */
|
|
9448
|
+
overflowX?: 'static' | 'scroll';
|
|
9449
|
+
/** Vertical overflow (default `'static'`). */
|
|
9450
|
+
overflowY?: 'static' | 'scroll';
|
|
9451
|
+
/** Fixed column track size, used only when `overflowX` is `'scroll'` (default `'240px'`). */
|
|
9452
|
+
columnWidth?: number | string;
|
|
9453
|
+
/** Fixed row track size, used only when `overflowY` is `'scroll'` (default `'160px'`). */
|
|
9454
|
+
rowHeight?: number | string;
|
|
9455
|
+
/** The gap between cells (default `'8px'`). */
|
|
9456
|
+
gap?: number | string;
|
|
9457
|
+
/** The default padding inside a window (default `'5px'`). */
|
|
9458
|
+
padding?: number | string;
|
|
9459
|
+
/** Rearrangement (default `'vertical'`): push displaced windows down, then pull up. */
|
|
9460
|
+
compact?: 'vertical' | 'none';
|
|
9461
|
+
/**
|
|
9462
|
+
* The default `movable` for every window that does not declare its own
|
|
9463
|
+
* (default `false`). This states a default, so `false` takes nothing away
|
|
9464
|
+
* from a window that declared `movable: true`; `setInteractive(false)` is
|
|
9465
|
+
* the active lock that does.
|
|
9466
|
+
*/
|
|
9467
|
+
movable?: boolean;
|
|
9468
|
+
/** The default `resizable` for windows that declare none (default `false`); see `movable`. */
|
|
9469
|
+
resizable?: boolean;
|
|
9470
|
+
/** The default `closable` for windows that declare none (default `false`); see `movable`. */
|
|
9471
|
+
closable?: boolean;
|
|
9472
|
+
/** The windows, in mount order. */
|
|
9473
|
+
windows?: LayoutWindow[];
|
|
9474
|
+
/** An arrangement to apply at mount, as produced by `getLayout()`. */
|
|
9475
|
+
layout?: LayoutSnapshot;
|
|
9476
|
+
/** The layout region's accessible name. */
|
|
9477
|
+
ariaLabel?: string;
|
|
9478
|
+
/** A message catalogue, e.g. `grid.messages`; built-in English seeds otherwise. */
|
|
9479
|
+
messages?: { t(key: string, params?: Record<string, unknown>): string };
|
|
9480
|
+
onWindowMoved?: (event: LayoutMoveEvent) => void;
|
|
9481
|
+
onWindowResized?: (event: LayoutResizeEvent) => void;
|
|
9482
|
+
onWindowClosed?: (event: LayoutCloseEvent) => void;
|
|
9483
|
+
onLayoutChanged?: (event: LayoutChangedEvent) => void;
|
|
9484
|
+
onBeforeWindowMove?: (event: LayoutMoveEvent) => boolean | void | Promise<boolean>;
|
|
9485
|
+
onBeforeWindowResize?: (event: LayoutMoveEvent) => boolean | void | Promise<boolean>;
|
|
9486
|
+
onBeforeWindowClose?: (event: LayoutCloseEvent) => boolean | void | Promise<boolean>;
|
|
9487
|
+
onWindowMoveCancelled?: (event: LayoutMoveEvent) => void;
|
|
9488
|
+
onWindowResizeCancelled?: (event: LayoutMoveEvent) => void;
|
|
9489
|
+
onWindowCloseCancelled?: (event: LayoutCloseEvent) => void;
|
|
9490
|
+
}
|
|
9491
|
+
|
|
9492
|
+
/**
|
|
9493
|
+
* A reconfigurable dashboard: a cell grid inside an element, and a set of
|
|
9494
|
+
* windows on it that a user can move, resize and close by pointer or by
|
|
9495
|
+
* keyboard (BACKLOG-0001108).
|
|
9496
|
+
*
|
|
9497
|
+
* The module is **payload-agnostic**: a window body is a container with an id,
|
|
9498
|
+
* which this module creates and sizes and never reads. It tells a payload it
|
|
9499
|
+
* was resized by emitting `window:resized`; it never calls into one, because it
|
|
9500
|
+
* cannot know what one is.
|
|
9501
|
+
*/
|
|
9502
|
+
interface Layout {
|
|
9503
|
+
readonly el: HTMLElement;
|
|
9504
|
+
/** The window ids, in mount order. */
|
|
9505
|
+
windows(): string[];
|
|
9506
|
+
/** The payload container for a window, or `null`. */
|
|
9507
|
+
payload(id: string): HTMLElement | null;
|
|
9508
|
+
/** A copy of one window's current descriptor, or `null`. */
|
|
9509
|
+
window(id: string): LayoutWindow | null;
|
|
9510
|
+
/** Add a window after mount; returns its payload container. */
|
|
9511
|
+
add(spec: LayoutWindow): HTMLElement;
|
|
9512
|
+
/** Move or resize a window, through the same before-events the drag uses. */
|
|
9513
|
+
move(id: string, to: Partial<LayoutPlacement>): boolean | Promise<boolean>;
|
|
9514
|
+
/** Close a window through `beforeWindowClose`; the payload is not destroyed. */
|
|
9515
|
+
close(id: string): boolean | Promise<boolean>;
|
|
9516
|
+
/** The full current arrangement. */
|
|
9517
|
+
getLayout(): LayoutSnapshot;
|
|
9518
|
+
/** Restore an arrangement; never throws on garbage. */
|
|
9519
|
+
setLayout(incoming: LayoutSnapshot | LayoutWindow[]): number;
|
|
9520
|
+
/** A versioned snapshot, following core's and gantt's shape. */
|
|
9521
|
+
getState(): { version: number; layout: LayoutSnapshot };
|
|
9522
|
+
/** Restore a `getState()` snapshot; never throws on garbage. */
|
|
9523
|
+
setState(snapshot: unknown): number;
|
|
9524
|
+
/**
|
|
9525
|
+
* Lock or unlock the dashboard at runtime — the "Edit layout" button. A
|
|
9526
|
+
* boolean sets all three capabilities; an object sets only the keys it
|
|
9527
|
+
* carries. Nothing is destroyed, so every payload survives the toggle.
|
|
9528
|
+
*
|
|
9529
|
+
* The asymmetry is deliberate: **you can always take a capability away; you
|
|
9530
|
+
* can never grant one where the developer said no.** `setInteractive(false)`
|
|
9531
|
+
* locks every window, including one whose own spec says `movable: true`;
|
|
9532
|
+
* `setInteractive(true)` unlocks only the windows that never opted out.
|
|
9533
|
+
*
|
|
9534
|
+
* `config.movable: false` and `setInteractive(false)` are deliberately not
|
|
9535
|
+
* the same thing: the config states the *default* for windows that declare
|
|
9536
|
+
* nothing (and `false` is already that default, so it takes nothing away from
|
|
9537
|
+
* a window that opted in), while this is an *active lock*.
|
|
9538
|
+
*
|
|
9539
|
+
* A key carrying `undefined` is treated as absent, so
|
|
9540
|
+
* `setInteractive(getInteractive())` is a no-op in every state.
|
|
9541
|
+
*
|
|
9542
|
+
* A locked layout is not a read-only dashboard: this module never reads or
|
|
9543
|
+
* writes a payload, so a grid inside a window is made read-only with the
|
|
9544
|
+
* grid's own settings.
|
|
9545
|
+
*/
|
|
9546
|
+
setInteractive(value: boolean | Partial<LayoutInteractive>): LayoutInteractive;
|
|
9547
|
+
/**
|
|
9548
|
+
* The layout-level interactivity now in force, as a copy — `undefined` where
|
|
9549
|
+
* no layout-level default is set, so the result round-trips through
|
|
9550
|
+
* `setInteractive`.
|
|
9551
|
+
*/
|
|
9552
|
+
getInteractive(): LayoutInteractive;
|
|
9553
|
+
/** Re-measure every window and emit `window:resized` for those that changed. */
|
|
9554
|
+
refresh(): number;
|
|
9555
|
+
on(
|
|
9556
|
+
name: 'window:moved' | 'window:resized' | 'window:closed' | 'layout:changed'
|
|
9557
|
+
| 'beforeWindowMove' | 'beforeWindowResize' | 'beforeWindowClose'
|
|
9558
|
+
| 'windowMove:cancelled' | 'windowResize:cancelled' | 'windowClose:cancelled'
|
|
9559
|
+
| '*' | string,
|
|
9560
|
+
fn: (event: any) => unknown,
|
|
9561
|
+
): () => void;
|
|
9562
|
+
off(name: string, fn: (event: any) => unknown): void;
|
|
9563
|
+
/** Tear the layout down; whatever the host mounted in a payload is the host's to destroy. */
|
|
9564
|
+
destroy(): void;
|
|
9565
|
+
}
|
|
9566
|
+
|
|
9567
|
+
/** Create a reconfigurable dashboard layout over a host element. */
|
|
9568
|
+
export function createLayout(el: HTMLElement, config?: LayoutConfig): Layout;
|
|
9569
|
+
export default createLayout;
|
|
9570
|
+
}
|