@toclocoinc/lattice-grid 1.14.0 → 1.15.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 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.14.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
7
+ Version 1.15.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
8
8
 
9
9
  ---
10
10
 
@@ -250,7 +250,10 @@ imports this and never the base package as well.
250
250
 
251
251
  **dhtmlx.** `modules/dhtmlx-compat` exposes a dhtmlx Grid-shaped API over
252
252
  Lattice, for moving an existing integration across a piece at a time rather
253
- than rewriting it in one go.
253
+ than rewriting it in one go. It shares the core the page already loads rather
254
+ than carrying its own, so load `lattice-grid` alongside it — a bundler wires
255
+ the shared import for you, and a `<script src>` page loads the global build
256
+ first — and a licence set on that core applies to these grids too.
254
257
 
255
258
  ### TypeScript
256
259
 
package/docs/API.html CHANGED
@@ -2494,6 +2494,142 @@ createGrid(host, { source, columns: [...] });</code></pre>
2494
2494
  </table>
2495
2495
  </div>
2496
2496
 
2497
+ <h4 id="adapter-options">What each adapter takes</h4>
2498
+ <p class="section-note">
2499
+ Every adapter is a function of one options object. The tables below list what each accepts, the
2500
+ type, the default where it is not obvious, and what it means. The defaults are the load-bearing
2501
+ part: an adapter is designed to work when handed almost nothing, so most of what you can set is
2502
+ about telling it what your endpoint <em>cannot</em> do rather than switching features on.
2503
+ </p>
2504
+
2505
+ <h5 id="odata-options"><code>odataAdapter</code></h5>
2506
+ <div class="table-wrap">
2507
+ <table>
2508
+ <thead><tr><th>Option</th><th>Type</th><th>Default</th><th>Meaning</th></tr></thead>
2509
+ <tbody>
2510
+ <tr><td class="name">url</td><td class="type">string</td><td class="type">—</td><td class="desc">The entity-set endpoint, e.g. <code>https://api.example.com/Orders</code>. Required.</td></tr>
2511
+ <tr><td class="name">headers</td><td class="type">Record&lt;string, string&gt;</td><td class="type">{}</td><td class="desc">Sent on every request, merged over <code>Accept: application/json</code>. This is where a fixed bearer token or an API key goes. See <a href="#adapter-auth">authenticating</a>.</td></tr>
2512
+ <tr><td class="name">fetch</td><td class="type">typeof fetch</td><td class="type">the global <code>fetch</code></td><td class="desc">Your own fetch, for a token that expires, a proxy, or a non-browser runtime. The adapter bundles no HTTP client. See <a href="#adapter-auth">authenticating</a>.</td></tr>
2513
+ <tr><td class="name">count</td><td class="type">boolean</td><td class="type">true</td><td class="desc">Whether to ask for <code>$count=true</code> and read <code>@odata.count</code>. On by default because the grid sizes its scrollbar from the total; set <code>false</code> for a server that does not support it.</td></tr>
2514
+ <tr><td class="name">search</td><td class="type">boolean</td><td class="type">false</td><td class="desc">Whether the server implements <code>$search</code>. Off by default, so quick-filter text stays with the grid until you confirm the endpoint honours it; <code>true</code> pushes it as <code>$search</code>.</td></tr>
2515
+ </tbody>
2516
+ </table>
2517
+ </div>
2518
+
2519
+ <h5 id="rest-options"><code>restAdapter</code></h5>
2520
+ <p class="section-note">
2521
+ <strong>The parameter names are yours, and the defaults are not zero.</strong> Paging and
2522
+ sorting are assumed present; filtering is assumed <em>absent</em> until you declare
2523
+ <code>operators</code>, because an adapter that claims to filter when the endpoint ignores it
2524
+ returns the wrong rows silently. The query-string names default to
2525
+ <code>offset</code>, <code>limit</code>, <code>sort</code>, <code>order</code>,
2526
+ <code>filter</code> and <code>q</code> (search); <code>params</code> overrides any of them.
2527
+ </p>
2528
+ <div class="table-wrap">
2529
+ <table>
2530
+ <thead><tr><th>Option</th><th>Type</th><th>Default</th><th>Meaning</th></tr></thead>
2531
+ <tbody>
2532
+ <tr><td class="name">url</td><td class="type">string</td><td class="type">—</td><td class="desc">The endpoint, e.g. <code>/api/orders</code>. Required.</td></tr>
2533
+ <tr><td class="name">headers</td><td class="type">Record&lt;string, string&gt;</td><td class="type">{}</td><td class="desc">Sent on every request, merged over <code>Accept: application/json</code>. Where a fixed token or key goes. See <a href="#adapter-auth">authenticating</a>.</td></tr>
2534
+ <tr><td class="name">fetch</td><td class="type">typeof fetch</td><td class="type">the global <code>fetch</code></td><td class="desc">Your own fetch, for an expiring token, a proxy or a non-browser runtime. See <a href="#adapter-auth">authenticating</a>.</td></tr>
2535
+ <tr><td class="name">params</td><td class="type">Partial&lt;Record&lt;'offset'|'limit'|'sort'|'order'|'filter'|'search', string&gt;&gt;</td><td class="type">{ offset:'offset', limit:'limit', sort:'sort', order:'order', filter:'filter', search:'q' }</td><td class="desc">Renames the query-string keys to whatever your endpoint already reads. Only the keys you name change; the rest keep the defaults above.</td></tr>
2536
+ <tr><td class="name">capabilities</td><td class="type">PushdownCapabilities</td><td class="type">{ range:true, total:true, sort:'multi', filter:false, quick:false }</td><td class="desc">What the endpoint can answer, merged over the defaults. Declaring <code>operators</code> is the usual way to turn filtering on; reach for this to switch off paging or sorting an endpoint cannot do.</td></tr>
2537
+ <tr><td class="name">operators</td><td class="type">string[]</td><td class="type">— (filtering off)</td><td class="desc">The comparisons the endpoint genuinely applies, e.g. <code>['eq','gt','lt','contains']</code>. Setting it turns filtering on as a <code>tree</code>; a condition using any other operator stays with the grid.</td></tr>
2538
+ <tr><td class="name">encodeFilter</td><td class="type">(filters: object) =&gt; string</td><td class="type"><code>JSON.stringify</code></td><td class="desc">How the pushed condition tree becomes the <code>filter</code> parameter's value. Override it to emit whatever query language your service parses instead of JSON.</td></tr>
2539
+ <tr><td class="name">rows</td><td class="type">(body: unknown) =&gt; unknown[]</td><td class="type">body itself if an array, else <code>body.rows</code> then <code>body.data</code></td><td class="desc">Pulls the row array out of the response body, for an envelope that nests it somewhere else.</td></tr>
2540
+ <tr><td class="name">total</td><td class="type">(body: unknown, rows: unknown[]) =&gt; number</td><td class="type"><code>body.total</code> then <code>body.count</code>, else the page length</td><td class="desc">Reads the count of <em>all</em> matching rows, not the page. The grid sizes its scrollbar from it, so a page-sized total makes a large result look like one page.</td></tr>
2541
+ </tbody>
2542
+ </table>
2543
+ </div>
2544
+
2545
+ <h5 id="duckdb-options"><code>duckdbAdapter</code></h5>
2546
+ <div class="table-wrap">
2547
+ <table>
2548
+ <thead><tr><th>Option</th><th>Type</th><th>Default</th><th>Meaning</th></tr></thead>
2549
+ <tbody>
2550
+ <tr><td class="name">connection</td><td class="type">object</td><td class="type">—</td><td class="desc">A live connection exposing <code>query</code>, and ideally <code>prepare</code>. Required. A connection without <code>prepare</code> is used only for unfiltered queries, because interpolating a user's filter into SQL is worse than not filtering.</td></tr>
2551
+ <tr><td class="name">from</td><td class="type">string</td><td class="type">—</td><td class="desc">A table, a view, or any FROM expression. Required. <code>read_parquet('s3://bucket/*.parquet')</code> is as valid as a table name.</td></tr>
2552
+ <tr><td class="name">fields</td><td class="type">string[]</td><td class="type">everything (<code>SELECT *</code>)</td><td class="desc">The columns to select. Name them to narrow the projection when the grid shows a subset of a wide table.</td></tr>
2553
+ </tbody>
2554
+ </table>
2555
+ </div>
2556
+
2557
+ <h5 id="dfql-options"><code>dfqlAdapter</code></h5>
2558
+ <div class="table-wrap">
2559
+ <table>
2560
+ <thead><tr><th>Option</th><th>Type</th><th>Default</th><th>Meaning</th></tr></thead>
2561
+ <tbody>
2562
+ <tr><td class="name">entity</td><td class="type">string</td><td class="type">—</td><td class="desc">The DemandFlow entity to query. Required.</td></tr>
2563
+ <tr><td class="name">token</td><td class="type">string</td><td class="type">—</td><td class="desc">A personal access token, sent as the bearer credential. Required. Never commit one; read it from configuration at runtime.</td></tr>
2564
+ <tr><td class="name">url</td><td class="type">string</td><td class="type"><code>https://rest.demandflow.com</code></td><td class="desc">The API base, for a non-default region or a self-hosted deployment.</td></tr>
2565
+ <tr><td class="name">comboKey</td><td class="type">'comboKey' | 'comboKey2' | 'comboKey3'</td><td class="type"><code>comboKey</code></td><td class="desc">The name of the key attribute to match on. <code>comboKey</code> is the standard hierarchy.</td></tr>
2566
+ <tr><td class="name">query</td><td class="type">string</td><td class="type"><code>SUB</code></td><td class="desc">The prefix matched against the key attribute. <code>SUB</code> alone means every record of the entity in the tenant.</td></tr>
2567
+ <tr><td class="name">load</td><td class="type">string[]</td><td class="type">everything</td><td class="desc">Fields to project, which saves bandwidth but not query cost.</td></tr>
2568
+ <tr><td class="name">limit</td><td class="type">number</td><td class="type">server default</td><td class="desc">Caps rows <em>scanned</em>, not matched — which is why every request also sends <code>countOnly</code> to reveal the true match count.</td></tr>
2569
+ <tr><td class="name">headers</td><td class="type">Record&lt;string, string&gt;</td><td class="type">{}</td><td class="desc">Extra headers merged over the bearer token, for a gateway that needs its own.</td></tr>
2570
+ <tr><td class="name">fetch</td><td class="type">typeof fetch</td><td class="type">the global <code>fetch</code></td><td class="desc">Your own fetch, for a proxy or a non-browser runtime.</td></tr>
2571
+ </tbody>
2572
+ </table>
2573
+ </div>
2574
+
2575
+ <h4 id="adapter-auth">Authenticating a remote adapter</h4>
2576
+ <p class="section-note">
2577
+ Two shapes cover almost every endpoint. A <strong>fixed credential</strong> — an API key or a
2578
+ long-lived token — goes in <code>headers</code>, which <code>odataAdapter</code> and
2579
+ <code>restAdapter</code> send on every request. A credential that <strong>expires</strong> —
2580
+ a short-lived bearer token you refresh — goes in a custom <code>fetch</code>, which is the one
2581
+ place that can mint a fresh value per request. <code>dfqlAdapter</code> takes its
2582
+ <code>token</code> directly, and <code>headers</code> for anything a gateway adds on top.
2583
+ </p>
2584
+ <p class="section-note">
2585
+ <strong>A fixed token in <code>headers</code>.</strong> The map is sent on every request, so an
2586
+ <code>Authorization</code> header authenticates the whole grid. Below, a custom
2587
+ <code>fetch</code> stands in for the network only so the example can prove the header arrived:
2588
+ </p>
2589
+ <pre data-run="js" data-expect="Bearer static-token-123" data-covers="export:odataAdapter"><code><span class="kw">const</span> { odataAdapter } = <span class="kw">await</span> import('../packages/core/src/index.js');
2590
+
2591
+ <span class="kw">let</span> seen;
2592
+ <span class="kw">const</span> adapter = odataAdapter({
2593
+ url: 'https://api.example.com/Orders',
2594
+ <span class="cmt">// A fixed credential authenticates every request.</span>
2595
+ headers: { Authorization: 'Bearer static-token-123' },
2596
+ <span class="cmt">// Only here to capture what the adapter sent; in a browser, omit it.</span>
2597
+ fetch: <span class="kw">async</span> (url, init) =&gt; {
2598
+ seen = init.headers.Authorization;
2599
+ <span class="kw">return</span> { ok: <span class="kw">true</span>, json: <span class="kw">async</span> () =&gt; ({ value: [], '@odata.count': 0 }) };
2600
+ },
2601
+ });
2602
+
2603
+ <span class="kw">await</span> adapter.execute({ range: { start: 0, end: 20 } }, {});
2604
+ <span class="kw">return</span> seen; <span class="cmt">// the header reached the request</span></code></pre>
2605
+
2606
+ <p class="section-note">
2607
+ <strong>An expiring token in a custom <code>fetch</code>.</strong> A token with a lifetime cannot
2608
+ sit in a fixed map, because the map is read once and the token outlives no request that matters.
2609
+ A custom <code>fetch</code> is called afresh for every request, so it is where you refresh the
2610
+ credential and set the header on the outgoing call:
2611
+ </p>
2612
+ <pre data-run="js" data-expect="Bearer token-2" data-covers="export:restAdapter"><code><span class="kw">const</span> { restAdapter } = <span class="kw">await</span> import('../packages/core/src/index.js');
2613
+
2614
+ <span class="cmt">// Stands in for a token service that hands out a new value each time.</span>
2615
+ <span class="kw">let</span> issued = 0;
2616
+ <span class="kw">const</span> freshToken = <span class="kw">async</span> () =&gt; `token-${++issued}`;
2617
+
2618
+ <span class="kw">let</span> lastAuth;
2619
+ <span class="kw">const</span> adapter = restAdapter({
2620
+ url: '/api/orders',
2621
+ fetch: <span class="kw">async</span> (url, init) =&gt; {
2622
+ <span class="cmt">// Refreshed per request, then merged over whatever headers the adapter set.</span>
2623
+ <span class="kw">const</span> headers = { ...init.headers, Authorization: `Bearer ${<span class="kw">await</span> freshToken()}` };
2624
+ lastAuth = headers.Authorization;
2625
+ <span class="kw">return</span> { ok: <span class="kw">true</span>, json: <span class="kw">async</span> () =&gt; ({ rows: [], total: 0 }) };
2626
+ },
2627
+ });
2628
+
2629
+ <span class="kw">await</span> adapter.execute({ range: { start: 0, end: 20 } }, {}); <span class="cmt">// token-1</span>
2630
+ <span class="kw">await</span> adapter.execute({ range: { start: 20, end: 40 } }, {}); <span class="cmt">// token-2</span>
2631
+ <span class="kw">return</span> lastAuth; <span class="cmt">// a fresh token on the second request</span></code></pre>
2632
+
2497
2633
  <h4 id="pushdown-cookbook">Wiring it to the API you already have</h4>
2498
2634
  <p class="section-note">
2499
2635
  Most data sits behind a service someone on your team wrote. The adapter below sends four
@@ -5427,7 +5563,7 @@ grid.destroy();
5427
5563
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
5428
5564
  <tbody>
5429
5565
  <tr><td class="name">name</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
5430
- <tr><td class="name">icon</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
5566
+ <tr><td class="name">icon</td><td class="type">string</td><td class="desc">An icon shown in the slot before the label. Three forms, told apart without a second option so existing definitions keep working: a registered sprite name (`'download'`), a single character or emoji (`'↑'`), or author-trusted element markup (`'&lt;i class="fa-light fa-download"&gt;&lt;/i&gt;'`), which is rendered as an element rather than shown as text. Markup is inserted into the icon slot only — never the label — at the same trust as `action`. <small>(optional)</small></td></tr>
5431
5567
  <tr><td class="name">shortcut</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
5432
5568
  <tr><td class="name">action</td><td class="type">() =&gt; void</td><td class="desc"><small>(optional)</small></td></tr>
5433
5569
  <tr><td class="name">disabled</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
@@ -6205,6 +6341,7 @@ grid.destroy();
6205
6341
  <tr><td class="name">grid</td><td class="type">Grid</td><td class="desc"><small>(optional)</small></td></tr>
6206
6342
  <tr><td class="name">container</td><td class="type">HTMLElement | string</td><td class="desc">An element, or a CSS selector resolved against the grid's document.</td></tr>
6207
6343
  <tr><td class="name">title</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
6344
+ <tr><td class="name">icon</td><td class="type">string</td><td class="desc">An optional leading icon beside the title and value, using the same value contract as a menu item: a registered sprite name, a single character or emoji, or author-trusted element markup (`'&lt;i class="fa-light fa-bolt"&gt; &lt;/i&gt;'`, an `&lt;img&gt;`). It lays out to the side without disturbing the change indicator, threshold bands or confidence interval; omit it for the plain tile layout. <small>(optional)</small></td></tr>
6208
6345
  <tr><td class="name">value</td><td class="type">unknown | StatValueSpec | ((grid: Grid) =&gt; unknown)</td><td class="desc">A literal value, a spec to reduce, or a function of the grid. <small>(optional)</small></td></tr>
6209
6346
  <tr><td class="name">footer</td><td class="type">string | ((value: unknown, grid: Grid) =&gt; string)</td><td class="desc">Text under the value, or a function of it. <small>(optional)</small></td></tr>
6210
6347
  <tr><td class="name">baseline</td><td class="type">number | ((grid: Grid) =&gt; number)</td><td class="desc">What the value is compared against, for the change indicator. <small>(optional)</small></td></tr>
@@ -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.14.0</p>
440
+ <p class="rail__sub">Developer guide · v1.15.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
@@ -884,6 +884,14 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
884
884
  <code>.events</code> namespaces: sitting on top of a real Lattice grid underneath.
885
885
  Swap the import and, for the surface below, the calling code does not change.
886
886
  </p>
887
+ <p class="lead-in">
888
+ The wrapper shares the core the page already loads rather than carrying its own,
889
+ so load <code>@toclocoinc/lattice-grid</code> alongside it: a bundler wires the shared
890
+ import automatically (it dedupes against the core your app already imports), and a
891
+ plain <code>&lt;script src&gt;</code> page loads the global build first. The pay-off is
892
+ that a licence set on that one core applies to these grids too, and the module is a
893
+ few kilobytes of translation rather than a second copy of the grid.
894
+ </p>
887
895
 
888
896
  <div class="example">
889
897
  <p class="example__label">A drop-in constructor</p>
@@ -4813,12 +4821,22 @@ createStat({
4813
4821
  grid,
4814
4822
  container: tile,
4815
4823
  title: 'Total capacity',
4824
+ <span class="cmt">// A leading icon beside the title and value. Same three forms as a menu</span>
4825
+ <span class="cmt">// item: a sprite name, a character/emoji, or your own markup.</span>
4826
+ icon: '&lt;i class="fa-light fa-gauge-high"&gt;&lt;/i&gt;',
4816
4827
  value: { of: 'capacity', fn: 'sum' },
4817
4828
  baseline: (g) =&gt; lastMonth,
4818
4829
  bands: { good: 5000, warn: 3000, direction: 'up' },
4819
4830
  interval: (v, g) =&gt; g.statistics.interval('capacity'),
4820
4831
  });</code></pre>
4821
4832
  </div>
4833
+ <p class="lead-in">
4834
+ <code>icon</code> is optional and lays out to the side without disturbing the change indicator,
4835
+ threshold bands or confidence interval; omit it for the plain tile. It uses the same
4836
+ <a href="#custom-menu">icon contract</a> as a menu item — a sprite name, a single character or
4837
+ emoji, or author-supplied markup such as a Font Awesome glyph or an
4838
+ <code>&lt;img&gt;</code>.
4839
+ </p>
4822
4840
  <div class="why">
4823
4841
  <p><strong><code>bands</code> and <code>goodWhen</code> judge different things.</strong>
4824
4842
  <code>goodWhen</code> says whether a rise is good news, and colours the change indicator.
@@ -4919,6 +4937,33 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
4919
4937
  <code>{ key, colId, value, row, data, column, index, grid }</code>. <code>data</code> is your
4920
4938
  original row object, so an item can reach fields the grid never displayed.
4921
4939
  </p>
4940
+ <div class="example">
4941
+ <p class="example__label">An item with your own icon</p>
4942
+ <pre><code>createGrid(el, {
4943
+ contextMenu: (params, defaults) =&gt; [
4944
+ ...defaults,
4945
+ { separator: <span class="kw">true</span> },
4946
+ <span class="cmt">// A registered sprite name — the built-in items use these.</span>
4947
+ { name: 'Download', icon: 'download', action: () =&gt; save(params.data) },
4948
+ <span class="cmt">// A single character or emoji, rendered as text.</span>
4949
+ { name: 'Star', icon: '★', action: () =&gt; star(params.data) },
4950
+ <span class="cmt">// Your own markup — a Font Awesome glyph, an inline SVG, an image.</span>
4951
+ <span class="cmt">// It is inserted into the icon slot as an element, at the same trust</span>
4952
+ <span class="cmt">// as the item's action, and never into the label.</span>
4953
+ { name: 'Export', icon: '&lt;i class="fa-light fa-file-export"&gt;&lt;/i&gt;', action: exportRow },
4954
+ ],
4955
+ });</code></pre>
4956
+ </div>
4957
+ <p class="lead-in">
4958
+ A <code>MenuItem</code>'s <code>icon</code> accepts three forms, told apart automatically so
4959
+ existing definitions keep working: a registered sprite <strong>name</strong>
4960
+ (<code>'download'</code>), a single <strong>character</strong> or emoji (<code>'↑'</code>),
4961
+ or author-supplied element <strong>markup</strong>
4962
+ (<code>'&lt;i class="fa-light fa-download"&gt;&lt;/i&gt;'</code>). Markup is rendered as an
4963
+ element rather than shown as text — the misbehaviour it replaces — and is written only into
4964
+ the icon slot, so a definition can never inject markup into the label. It is trusted like the
4965
+ item's <code>action</code>: a menu definition is code you wrote, not user data.
4966
+ </p>
4922
4967
  <div class="why">
4923
4968
  <p><strong>Handed the defaults, rather than replacing them.</strong> A builder that had to
4924
4969
  return every item in order to append one would be written once as a copy of the built-ins and
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.14.0, type declarations
2
+ * Lattice Grid 1.15.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -1592,6 +1592,14 @@ export type PermissionPolicy =
1592
1592
 
1593
1593
  export interface MenuItem {
1594
1594
  name?: string;
1595
+ /**
1596
+ * An icon shown in the slot before the label. Three forms, told apart without
1597
+ * a second option so existing definitions keep working: a registered sprite
1598
+ * name (`'download'`), a single character or emoji (`'↑'`), or author-trusted
1599
+ * element markup (`'<i class="fa-light fa-download"></i>'`), which is rendered
1600
+ * as an element rather than shown as text. Markup is inserted into the icon
1601
+ * slot only — never the label — at the same trust as `action`.
1602
+ */
1595
1603
  icon?: string;
1596
1604
  shortcut?: string;
1597
1605
  action?: () => void;
@@ -3193,6 +3201,15 @@ export interface StatConfig extends StatValueSpec {
3193
3201
  /** An element, or a CSS selector resolved against the grid's document. */
3194
3202
  container: HTMLElement | string;
3195
3203
  title?: string;
3204
+ /**
3205
+ * An optional leading icon beside the title and value, using the same value
3206
+ * contract as a menu item: a registered sprite name, a single character or
3207
+ * emoji, or author-trusted element markup (`'<i class="fa-light fa-bolt">
3208
+ * </i>'`, an `<img>`). It lays out to the side without disturbing the change
3209
+ * indicator, threshold bands or confidence interval; omit it for the plain
3210
+ * tile layout.
3211
+ */
3212
+ icon?: string;
3196
3213
  /** A literal value, a spec to reduce, or a function of the grid. */
3197
3214
  value?: unknown | StatValueSpec | ((grid: Grid) => unknown);
3198
3215
  /** Text under the value, or a function of it. */
@@ -3824,7 +3841,16 @@ declare module 'lattice-grid/modules/htmx' {
3824
3841
  }
3825
3842
 
3826
3843
  declare module 'lattice-grid/modules/dhtmlx-compat' {
3827
- /** A dhtmlx Grid-shaped API over Lattice, for migrating a piece at a time. */
3844
+ /**
3845
+ * A dhtmlx Grid-shaped API over Lattice, for migrating a piece at a time.
3846
+ *
3847
+ * The module shares the page's one core rather than bundling its own: the
3848
+ * grid it builds comes from the `lattice-grid` package the app already loads
3849
+ * (or the `LatticeGrid` global a script tag publishes), so a licence set on
3850
+ * that core applies to these grids too. Load the core alongside this module —
3851
+ * a bundler wires the peer import for you; a `<script src>` page loads the
3852
+ * global build first.
3853
+ */
3828
3854
  export class Grid {
3829
3855
  constructor(container: Element | string, config?: object);
3830
3856
  }