@toclocoinc/lattice-grid 1.11.0 → 1.12.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.
@@ -3,16 +3,16 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <meta name="description" content="Lattice Grid — a high-performance JavaScript data grid with no dependencies and no build step. What every part of the API does, and why.">
6
+ <meta name="description" content="Lattice Grid, a high-performance JavaScript data grid with no dependencies and no build step. What every part of the API does, and why.">
7
7
  <!--
8
- Lattice Grid — developer guide.
8
+ Lattice Grid: developer guide.
9
9
  Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
10
10
 
11
11
  Self-contained: no stylesheet, script or font is fetched, so this opens from
12
12
  disk, from a file share or from behind a firewall with nothing else present.
13
13
  API.html is the reference; this is the explanation.
14
14
  -->
15
- <title>Lattice Grid — Developer Guide</title>
15
+ <title>Lattice Grid: Developer Guide</title>
16
16
  <style>
17
17
  /* ---------------------------------------------------------------
18
18
  Palette lifted from the grid's own theme, so the reference reads as an
@@ -210,7 +210,7 @@
210
210
 
211
211
  a { color: var(--accent); }
212
212
 
213
- /* Inline identifiers — the primary content of the whole document. */
213
+ /* Inline identifiers, the primary content of the whole document. */
214
214
  code {
215
215
  font-family: var(--mono);
216
216
  font-size: 0.885em;
@@ -238,7 +238,7 @@
238
238
  .kw { color: var(--accent); }
239
239
 
240
240
  /* ---------------------------------------------------------------
241
- Reference tables. Dense, hairline, sticky-headed — the grid's own
241
+ Reference tables. Dense, hairline, sticky-headed, the grid's own
242
242
  visual language, and the right density for scanning an API.
243
243
  --------------------------------------------------------------- */
244
244
  .table-wrap {
@@ -387,7 +387,7 @@
387
387
  }
388
388
 
389
389
  /* Why a thing is the way it is. Distinct from .note, which is a
390
- caveat — this is the reasoning, and it is the part a developer
390
+ caveat: this is the reasoning, and it is the part a developer
391
391
  evaluating the product actually reads. */
392
392
  .why {
393
393
  /* Constrained to the reading measure plus its own padding. Left to fill
@@ -553,7 +553,7 @@ createGrid(element, { locale: 'fr-FR', messages: FR_FR });</code></pre>
553
553
 
554
554
  <p>Where <code>locale</code> is not set, the grid takes the language the page declares in its <code>lang</code> attribute. Overrides merge over the default, so translating part of the interface leaves the remainder in English rather than showing raw keys, and a key that is not in the catalogue is ignored with a warning.</p>
555
555
 
556
- <p>Catalogues ship for twenty-one locales: British and American English, French (France and Canada), Italian, Spanish, Brazilian Portuguese, German, Dutch, Swedish, Danish, Norwegian, Finnish, Polish, Czech, Hungarian, Romanian, Ukrainian, Greek, Japanese and Arabic. Each is an export of the package, so importing one does not reduce what is bundled. <code>EN_US</code> is a partial overlay carrying only what differs from British English and merging over it. <code>AR_SA</code> is an alias for <code>AR</code> rather than a separate catalogue — Arabic ships pan-Arabic, and a region appears in a name only where two variants exist. For anything else, <code>resolveCatalogue(tag)</code> resolves a tag to a catalogue and falls back to the base language, so <code>ar-EG</code> and <code>es-MX</code> both find one. <code>FR_CA</code> is a complete catalogue that follows Quebec usage where it differs — a column there is <em>figée</em> rather than <em>épinglée</em>.</p>
556
+ <p>Catalogues ship for twenty-one locales: British and American English, French (France and Canada), Italian, Spanish, Brazilian Portuguese, German, Dutch, Swedish, Danish, Norwegian, Finnish, Polish, Czech, Hungarian, Romanian, Ukrainian, Greek, Japanese and Arabic. Each is an export of the package, so importing one does not reduce what is bundled. <code>EN_US</code> is a partial overlay carrying only what differs from British English and merging over it. <code>AR_SA</code> is an alias for <code>AR</code> rather than a separate catalogue: Arabic ships pan-Arabic, and a region appears in a name only where two variants exist. For anything else, <code>resolveCatalogue(tag)</code> resolves a tag to a catalogue and falls back to the base language, so <code>ar-EG</code> and <code>es-MX</code> both find one. <code>FR_CA</code> is a complete catalogue that follows Quebec usage where it differs, a column there is <em>figée</em> rather than <em>épinglée</em>.</p>
557
557
 
558
558
  <p class="section-note">None of the translations has been reviewed by a native speaker. They are structurally complete and checked for placeholder integrity and plural coverage, but they are a starting point for review rather than finished copy.</p>
559
559
 
@@ -565,17 +565,17 @@ createGrid(element, { locale: 'fr-FR', messages: FR_FR });</code></pre>
565
565
  'count.rows': { one: '{count} rad', other: '{count} rader' },
566
566
  }</code></pre>
567
567
 
568
- <p>Placeholders are named, so word order is yours to choose. The parameter called <code>count</code> selects the plural form, using the categories the language actually has — English needs two, French treats zero as singular, Arabic has six. Numbers are formatted for the locale automatically; do not format them yourself.</p>
568
+ <p>Placeholders are named, so word order is yours to choose. The parameter called <code>count</code> selects the plural form, using the categories the language actually has: English needs two, French treats zero as singular, Arabic has six. Numbers are formatted for the locale automatically; do not format them yourself.</p>
569
569
 
570
570
  <p><code>MESSAGE_KEYS</code> lists every key. <code>auditCatalogue(yours)</code> returns what is missing and what is not a real key, which is the quickest way to check a translation before shipping it.</p>
571
571
 
572
572
  <div class="why">
573
- <p><strong>The grid is checked in the other direction too.</strong> <code>auditCatalogue</code> tells you a catalogue is complete — that a translator covered every key. It cannot tell you the grid only ever renders text that came from a catalogue in the first place, and a string written into the source passes every test, because the tests assert on the English the grid happens to produce.</p>
573
+ <p><strong>The grid is checked in the other direction too.</strong> <code>auditCatalogue</code> tells you a catalogue is complete: that a translator covered every key. It cannot tell you the grid only ever renders text that came from a catalogue in the first place, and a string written into the source passes every test, because the tests assert on the English the grid happens to produce.</p>
574
574
  <p>So the build refuses one. Any literal reaching an element's text, or an announced attribute such as <code>aria-label</code>, <code>title</code> or <code>placeholder</code>, has to come from the catalogue. That is what stops a localised grid drifting back into English one plausible change at a time.</p>
575
575
  </div>
576
576
 
577
577
  <h3>Right-to-left</h3>
578
- <p>The grid lays out right to left. Set <code>direction: 'rtl'</code> outright, or leave it unset and it follows the element's computed <code>dir</code> first and the locale second — so <code>locale: 'ar'</code> renders right to left with no further configuration, and a grid inside a page that has already declared <code>dir="rtl"</code> agrees with it.</p>
578
+ <p>The grid lays out right to left. Set <code>direction: 'rtl'</code> outright, or leave it unset and it follows the element's computed <code>dir</code> first and the locale second, so <code>locale: 'ar'</code> renders right to left with no further configuration, and a grid inside a page that has already declared <code>dir="rtl"</code> agrees with it.</p>
579
579
 
580
580
  <pre><code>createGrid(element, { locale: 'ar' }); <span class="cmt">// direction follows the locale</span>
581
581
  createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so outright</span></code></pre>
@@ -584,7 +584,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
584
584
 
585
585
  <p>Pinned columns swap sides, the header and pinned rows follow the scroll the other way, and column resize and reorder, the fill handle, annotations, the facet band, the comment marker and menu placement all follow the writing direction rather than the physical one.</p>
586
586
 
587
- <p><strong>Not yet exercised with bidirectional text.</strong> The layout is verified, but only with Latin text in a right-to-left grid. Cells, headings and editors holding actual Arabic or Hebrew — particularly mixed with Latin text or numbers — have not been tested.</p>
587
+ <p><strong>Not yet exercised with bidirectional text.</strong> The layout is verified, but only with Latin text in a right-to-left grid. Cells, headings and editors holding actual Arabic or Hebrew (particularly mixed with Latin text or numbers) have not been tested.</p>
588
588
 
589
589
  <h2 id="what">What it is</h2>
590
590
  <p class="lead-in">
@@ -596,7 +596,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
596
596
  <p class="lead-in">
597
597
  A grid of a hundred thousand rows and thirty columns is three million values. Held as row
598
598
  objects, that is three million property lookups per pass and a great deal of memory the
599
- garbage collector has opinions about. Lattice stores each column as one typed array — numbers
599
+ garbage collector has opinions about. Lattice stores each column as one typed array: numbers
600
600
  in a <code>Float64Array</code>, repeated strings as integers into a dictionary, booleans as
601
601
  bits in a bitset.
602
602
  </p>
@@ -614,7 +614,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
614
614
  flatten. Each remembers its result and the inputs it was computed from.
615
615
  </p>
616
616
  <div class="why">
617
- <p>Change a sort and the filter stage is not recomputed — its inputs did not change. Edit a
617
+ <p>Change a sort and the filter stage is not recomputed: its inputs did not change. Edit a
618
618
  cell in a column nobody sorts, filters or groups on and <em>none</em> of the first five run;
619
619
  only the totals move. This is why a grid that is heavily filtered and grouped still feels
620
620
  immediate when you type in a cell.</p>
@@ -629,7 +629,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
629
629
 
630
630
  <h3>No dependencies, and no build step</h3>
631
631
  <p class="lead-in">
632
- Not "few dependencies" — none, at runtime and at build time. No lodash, no date library, no
632
+ Not "few dependencies": none, at runtime and at build time. No lodash, no date library, no
633
633
  virtualisation library, no icon font. The bundler and minifier that produce the distribution
634
634
  are part of the repository. You can drop two files into a page and be finished.
635
635
  </p>
@@ -640,7 +640,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
640
640
  </div>
641
641
 
642
642
  <h2 id="install">Install</h2>
643
- <p class="lead-in">Two files. Nothing is fetched at runtime — no CDN, no font, no sprite sheet — however the two files themselves got onto the page. Four equally valid ways to get them there:</p>
643
+ <p class="lead-in">Two files. Nothing is fetched at runtime (no CDN, no font, no sprite sheet) however the two files themselves got onto the page. Four equally valid ways to get them there:</p>
644
644
 
645
645
  <div class="example">
646
646
  <p class="example__label">npm</p>
@@ -651,7 +651,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
651
651
  </div>
652
652
 
653
653
  <div class="example">
654
- <p class="example__label">jsDelivr — no npm install, no bundler</p>
654
+ <p class="example__label">jsDelivr, no npm install, no bundler</p>
655
655
  <pre><code>&lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.7.1/lattice-grid.min.css"&gt;
656
656
  &lt;script src="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.7.1/lattice-grid.min.js"&gt;&lt;/script&gt;
657
657
 
@@ -678,7 +678,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
678
678
 
679
679
  <div class="note">
680
680
  <p><strong>jsDelivr mirrors every version published to npm</strong> at
681
- <code>cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@&lt;version&gt;/&lt;file&gt;</code> — pin an
681
+ <code>cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@&lt;version&gt;/&lt;file&gt;</code>: pin an
682
682
  exact version, e.g. <code>@1.7.1</code> rather than <code>@latest</code>, so a later release
683
683
  does not change what a page already in production loads. The same convention reaches a
684
684
  module: <code>.../modules/htmx.esm.min.js</code>, <code>.../modules/dhtmlx-compat.esm.min.js</code>,
@@ -694,9 +694,9 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
694
694
  <thead><tr><th>File</th><th>Gzipped</th><th>What it is</th></tr></thead>
695
695
  <tbody>
696
696
  <tr><td class="sig">lattice-grid.min.js</td><td class="desc">The whole product as a UMD build. Defines <code>window.LatticeGrid</code>, and works with AMD and CommonJS loaders.</td></tr>
697
- <tr><td class="sig">lattice-grid.min.css</td><td class="desc">The single stylesheet. Without it the grid is in the DOM and unreadable — no widths, no scrolling, no theme.</td></tr>
697
+ <tr><td class="sig">lattice-grid.min.css</td><td class="desc">The single stylesheet. Without it the grid is in the DOM and unreadable, no widths, no scrolling, no theme.</td></tr>
698
698
  <tr><td class="sig">lattice-grid.esm.min.js</td><td class="desc">The same, as an ES module.</td></tr>
699
- <tr><td class="sig">lattice-core.esm.js</td><td class="desc">Headless core for Node — no renderer. See <a href="#export-guide">server-side export</a>.</td></tr>
699
+ <tr><td class="sig">lattice-core.esm.js</td><td class="desc">Headless core for Node, no renderer. See <a href="#export-guide">server-side export</a>.</td></tr>
700
700
  <tr><td class="sig">lattice-grid.d.ts</td><td class="desc">Type declarations.</td></tr>
701
701
  </tbody>
702
702
  </table>
@@ -724,7 +724,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
724
724
 
725
725
  <p class="lead-in">
726
726
  A few things happened there without being asked for. <code>region</code> got a title of
727
- "Region" — a field name is turned into a readable heading rather than left as-is. The number
727
+ "Region", a field name is turned into a readable heading rather than left as-is. The number
728
728
  column right-aligned itself, because numbers align right and a grid should not need telling.
729
729
  The date column parsed <code>2024-03-11</code> and rendered <code>11 Mar 2024</code>. And
730
730
  <code>total: 'sum'</code> put a figure in the totals row.
@@ -733,7 +733,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
733
733
  <div class="why">
734
734
  <p><strong>On <code>rowKey</code>:</strong> it names the field that identifies a row. Set it
735
735
  if you have one. Without it the grid assigns keys per row object, which is enough for
736
- sorting, filtering, selection and copying within a session — but not across a reload, because
736
+ sorting, filtering, selection and copying within a session, but not across a reload, because
737
737
  new objects are new rows. Change tracking, streaming dedupe, selection persistence and remote
738
738
  reload all want a real key, and the grid warns once, naming them.</p>
739
739
  </div>
@@ -785,7 +785,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
785
785
  deep-comparing a million-row array on every render would cost more than the reload it
786
786
  avoids. Build <code>columns</code> once outside the component, or memoise it; a fresh array
787
787
  literal on each render tells the grid the columns changed and it will rebuild them. Rows are
788
- the same — hand back a new array when the data actually changes, not before.</p>
788
+ the same: hand back a new array when the data actually changes, not before.</p>
789
789
  <p>StrictMode is handled. React 18 deliberately mounts, unmounts and mounts again in
790
790
  development; the effect cleanup destroys the first grid, so the second starts clean and
791
791
  nothing leaks.</p>
@@ -808,8 +808,8 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
808
808
  /&gt;</code></pre>
809
809
  </div>
810
810
  <div class="why">
811
- <p>Events are re-emitted under dashed names — <code>cell:changed</code> becomes
812
- <code>@cell-changed</code> — because a colon in a Vue template is directive syntax and
811
+ <p>Events are re-emitted under dashed names: <code>cell:changed</code> becomes
812
+ <code>@cell-changed</code>, because a colon in a Vue template is directive syntax and
813
813
  cannot be bound. Every event in the table below is declared in <code>emits</code>.</p>
814
814
  </div>
815
815
 
@@ -840,20 +840,20 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
840
840
  </div>
841
841
 
842
842
  <div class="why">
843
- <p><strong>What the adapters do not do.</strong> They add no features and wrap no API — the
843
+ <p><strong>What the adapters do not do.</strong> They add no features and wrap no API: the
844
844
  grid instance is the same object the vanilla examples use, and anything without a prop is
845
845
  reached through it directly (via the ref in React, <code>expose</code> in Vue, or a
846
846
  reference you keep in Svelte). Nothing is proxied, so nothing can lag behind the grid.</p>
847
847
  </div>
848
848
 
849
- <h3>The web component is self-contained &mdash; use it or the API, not both</h3>
849
+ <h3>The web component is self-contained: use it or the API, not both</h3>
850
850
  <p class="lead-in">
851
851
  The custom element carries the grid inside it, rather than being handed one. That is the point
852
852
  of the format: you add the element and it works, with nothing to wire up.
853
853
  </p>
854
854
  <div class="why">
855
855
  <p><strong>Do not load it alongside <code>createGrid</code> in the same page.</strong> You would
856
- get two independent copies of the grid, and the cost is not the download &mdash; it is that
856
+ get two independent copies of the grid, and the cost is not the download, it is that
857
857
  each copy keeps its own registries. A renderer, editor, data type or variant registered
858
858
  through one is invisible to the other, and a licence key validated in one is not validated in
859
859
  the other. Nothing errors; the custom renderer you registered simply never appears.</p>
@@ -865,9 +865,9 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
865
865
  <h2 id="dhtmlx-guide">Coming from dhtmlx Grid</h2>
866
866
  <p class="lead-in">
867
867
  <code>lattice-grid/modules/dhtmlx-compat</code> exposes a <code>Grid</code> class shaped
868
- like dhtmlx's own <code>dhx.Grid</code> &mdash; the same constructor call, the same
868
+ like dhtmlx's own <code>dhx.Grid</code>, the same constructor call, the same
869
869
  <code>.data</code>, <code>.selection</code>, <code>.history</code>, <code>.export</code> and
870
- <code>.events</code> namespaces &mdash; sitting on top of a real Lattice grid underneath.
870
+ <code>.events</code> namespaces: sitting on top of a real Lattice grid underneath.
871
871
  Swap the import and, for the surface below, the calling code does not change.
872
872
  </p>
873
873
 
@@ -884,7 +884,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
884
884
  });
885
885
 
886
886
  grid.events.on('cellClick', (row, column, event) =&gt; {
887
- <span class="cmt">// row and column carry .id, row's own fields sit alongside it &mdash;</span>
887
+ <span class="cmt">// row and column carry .id, row's own fields sit alongside it , </span>
888
888
  <span class="cmt">// the same shape dhtmlx's own IRow/ICol declare.</span>
889
889
  });</code></pre>
890
890
  </div>
@@ -908,19 +908,19 @@ grid.events.on('cellClick', (row, column, event) =&gt; {
908
908
  <code>afterColumnHide</code>, <code>afterColumnShow</code>, <code>afterExpand</code>,
909
909
  <code>afterCollapse</code>, <code>afterSelect</code>, <code>afterUnSelect</code>,
910
910
  <code>afterCopy</code>, <code>afterRowDrop</code> and <code>scroll</code>. Grid-level
911
- <code>dragItem: 'row'</code> becomes <code>rowReorder: true</code> &mdash; same-grid
911
+ <code>dragItem: 'row'</code> becomes <code>rowReorder: true</code>: same-grid
912
912
  drag-to-reorder.</p>
913
913
  <p><code>cellClick</code>, <code>cellDblClick</code>, <code>cellRightClick</code>,
914
914
  <code>afterEditStart</code>, <code>afterEditEnd</code> and <code>afterSort</code> call your
915
- handler with dhtmlx's own positional arguments &mdash; <code>(row, column, event)</code>,
916
- not Lattice's own event object &mdash; because that is dhtmlx's own documented signature for
915
+ handler with dhtmlx's own positional arguments: <code>(row, column, event)</code>,
916
+ not Lattice's own event object, because that is dhtmlx's own documented signature for
917
917
  them. <code>afterRowDrop</code> calls your handler with <code>(data, event)</code>, firing
918
- from either a same-grid reorder settling or a row landing here from another grid &mdash;
918
+ from either a same-grid reorder settling or a row landing here from another grid ,
919
919
  dhtmlx has one event name for what Lattice models as two. Every other mapped event calls your
920
920
  handler with Lattice's own event object, under Lattice's own field names, since a wrong guess
921
921
  at a fabricated positional shape is worse than an honest one.</p>
922
- <p><strong>Index means current display order</strong> &mdash; after sort, filter and
923
- grouping &mdash; everywhere <code>.data</code> takes or returns one. dhtmlx is not fully
922
+ <p><strong>Index means current display order</strong>: after sort, filter and
923
+ grouping: everywhere <code>.data</code> takes or returns one. dhtmlx is not fully
924
924
  consistent about this across its own methods; this is the one meaning, held everywhere.</p>
925
925
  <p><strong>Cross-grid dragging needs an explicit <code>rowTransfer</code>.</strong> dhtmlx
926
926
  lets any two grids with <code>dragItem: 'row'</code> on the same page exchange rows by
@@ -932,15 +932,15 @@ grid.events.on('cellClick', (row, column, event) =&gt; {
932
932
  <div class="why">
933
933
  <p><strong>What is not.</strong> Every <code>before*</code>/<code>can*</code>/<code>cancel*</code>
934
934
  event is unmapped: dhtmlx's convention lets a handler return <code>false</code> to cancel the
935
- action, and Lattice has no cancelable-event model to honour that with &mdash; approximating one
935
+ action, and Lattice has no cancelable-event model to honour that with: approximating one
936
936
  would silently ignore a <code>return false</code> a caller depends on. Row and column
937
- drag-<em>negotiation</em> events &mdash; <code>beforeRowDrag</code>, <code>dragRowOut</code>,
937
+ drag-<em>negotiation</em> events (<code>beforeRowDrag</code>, <code>dragRowOut</code>,
938
938
  <code>canRowDrop</code>, <code>cancelRowDrop</code>, <code>beforeRowDrop</code> and their
939
- column equivalents &mdash; are unmapped for the same reason: each can refuse or steer a drag
939
+ column equivalents) are unmapped for the same reason: each can refuse or steer a drag
940
940
  mid-gesture, which Lattice has no live protocol to offer. <code>export.pdf()</code> and
941
- <code>export.png()</code> throw &mdash; there is no raster export to translate to.
941
+ <code>export.png()</code> throw: there is no raster export to translate to.
942
942
  <code>.rangeSelection</code> is offered on a best-effort basis and its range shape is this
943
- wrapper's own design, not dhtmlx's — a genuine <code>RangeSelection</code> module is a
943
+ wrapper's own design, not dhtmlx's, a genuine <code>RangeSelection</code> module is a
944
944
  separate part of dhtmlx's own product, and this wrapper does not know its exact shape.
945
945
  Classic <code>dhtmlXGridObject</code> (pre-Suite 5, string-configured,
946
946
  index-addressed) is a different product in every respect that matters here and is not covered
@@ -954,7 +954,7 @@ grid.events.on('cellClick', (row, column, event) =&gt; {
954
954
  <p class="lead-in">
955
955
  <code>lattice-grid/modules/htmx</code> lets a grid survive htmx's own DOM swaps,
956
956
  hydrate from a server-rendered <code>&lt;table&gt;</code>, and drive sort, filter and
957
- infinite scroll over plain htmx requests &mdash; the server owns pagination and the
957
+ infinite scroll over plain htmx requests, the server owns pagination and the
958
958
  request lifecycle; this module only wires the grid's own state to it. Importing it
959
959
  is enough for the lifecycle half: it registers itself against
960
960
  <code>document</code> on load.
@@ -977,7 +977,7 @@ autoInit(document);</code></pre>
977
977
 
978
978
  <div class="why">
979
979
  <p><strong>One file, not two.</strong> <code>autoInit</code> comes from
980
- <code>modules/htmx</code> itself here, not from the base package — this module
980
+ <code>modules/htmx</code> itself here, not from the base package: this module
981
981
  already carries a complete, independently-bundled copy of <code>createGrid</code> and
982
982
  everything it depends on, the same way every module built this way does (a bundle
983
983
  inlines what it imports; it has no way to reach across to a copy some other
@@ -986,18 +986,18 @@ autoInit(document);</code></pre>
986
986
  again for the base package's own <code>createGrid</code>. This module re-exports
987
987
  <code>createGrid</code>, <code>autoInit</code>, <code>hydrateTable</code>,
988
988
  <code>readTable</code>, <code>serialiseState</code> and <code>restoreState</code>
989
- precisely so a page never has to choose between the two — it is the complete package
989
+ precisely so a page never has to choose between the two, it is the complete package
990
990
  for anything touching htmx, not an add-on alongside the base one.</p>
991
991
  <p><strong>Surviving a swap.</strong> <code>autoInit(root)</code> builds a grid on
992
992
  every <code>[data-lattice-grid]</code> element under <code>root</code> it has not
993
- already built one for &mdash; idempotent, so calling it again after a swap only picks
993
+ already built one for: idempotent, so calling it again after a swap only picks
994
994
  up what is new. A sibling <code>&lt;script type="application/json"
995
995
  data-lattice-config&gt;</code> supplies columns and options; without one, a
996
996
  <code>&lt;table&gt;</code> element is hydrated instead, reading its header row for
997
997
  columns and its body rows for data, then replacing itself with the grid. Once
998
998
  imported, this module listens for htmx's own <code>htmx:beforeCleanupElement</code>
999
999
  and <code>htmx:load</code> and calls <code>grid.destroy()</code> / <code>autoInit</code>
1000
- at the right moments automatically &mdash; a grid inside a swapped-out subtree is torn
1000
+ at the right moments automatically, a grid inside a swapped-out subtree is torn
1001
1001
  down before htmx detaches it; a grid inside newly-loaded content is built without
1002
1002
  re-scanning the whole page. A page with JavaScript disabled sees the plain
1003
1003
  <code>&lt;table&gt;</code>, still readable, since it is only ever replaced once the
@@ -1015,10 +1015,10 @@ autoInit(document);</code></pre>
1015
1015
  <span class="kw">const</span> grid = createGrid(host, { columns, rows: [], rowKey: 'id' });
1016
1016
  <span class="kw">const</span> COLUMNS = columns.map(c =&gt; ({ field: c.field }));
1017
1017
 
1018
- <span class="cmt">// Replaces the view outright &mdash; fires whenever sort or filter changes.</span>
1018
+ <span class="cmt">// Replaces the view outright: fires whenever sort or filter changes.</span>
1019
1019
  driveServerMode(grid, document.getElementById('query-trigger'), { columns: COLUMNS });
1020
1020
 
1021
- <span class="cmt">// Appends the next chunk &mdash; fires as the grid's own visible rows near the end.</span>
1021
+ <span class="cmt">// Appends the next chunk: fires as the grid's own visible rows near the end.</span>
1022
1022
  driveInfiniteScroll(grid, document.getElementById('sentinel'), { columns: COLUMNS });</code></pre>
1023
1023
  <pre><code>&lt;div id="query-trigger" hx-get="/rows" hx-trigger="lattice:query-changed" hx-swap="none" hidden&gt;&lt;/div&gt;
1024
1024
  &lt;div id="sentinel" hx-get="/rows" hx-trigger="revealed, lattice:scroll-near-end" hx-swap="none" hidden&gt;&lt;/div&gt;</code></pre>
@@ -1026,38 +1026,38 @@ driveInfiniteScroll(grid, document.getElementById('sentinel'), { columns: COLUMN
1026
1026
 
1027
1027
  <div class="why">
1028
1028
  <p><strong>Two triggers, two elements, deliberately.</strong> A sort or filter
1029
- change and a scroll asking for more rows are different operations &mdash; one
1030
- replaces every loaded row, the other appends to them &mdash; and there is no way to
1029
+ change and a scroll asking for more rows are different operations: one
1030
+ replaces every loaded row, the other appends to them, and there is no way to
1031
1031
  tell the two apart once a response has landed if they share a trigger. Each function
1032
1032
  configures the request (<code>offset</code>, <code>limit</code>, <code>sort</code>,
1033
- <code>filters</code> &mdash; a small, stable convention any server-side language can
1033
+ <code>filters</code>, a small, stable convention any server-side language can
1034
1034
  read with a JSON parser and a slice) and reads the response back into the grid
1035
1035
  itself, so <code>hx-swap="none"</code> is required on both: htmx sends the request
1036
1036
  and nothing else, since a grid is not an HTML swap target.</p>
1037
1037
  <p><strong>The sentinel's own trigger names two events for a reason.</strong>
1038
1038
  <code>revealed</code> is htmx's own once-per-element mechanism and gets the very
1039
- first chunk, firing the moment htmx has processed the element &mdash; reliable
1039
+ first chunk, firing the moment htmx has processed the element: reliable
1040
1040
  because it needs nothing from this module's own timing. Every chunk after that fires
1041
1041
  through <code>lattice:scroll-near-end</code>, which <code>driveInfiniteScroll</code>
1042
1042
  dispatches once the grid's own visible row window comes within
1043
1043
  <code>opts.threshold</code> rows (default 20) of what is loaded. That split matters
1044
1044
  for a fixed-height, virtualised grid specifically: nothing about it ever leaves the
1045
- page's own viewport once first revealed &mdash; new rows land inside the grid's own
1046
- scroll area, not the page's &mdash; so a page-scroll-only trigger would fire exactly
1045
+ page's own viewport once first revealed: new rows land inside the grid's own
1046
+ scroll area, not the page's, so a page-scroll-only trigger would fire exactly
1047
1047
  once and then go silent. Reading the grid's own render position instead is what
1048
1048
  makes every later chunk fire on genuine scroll, not on the repaint a successful load
1049
1049
  causes by itself.</p>
1050
1050
  <p><strong>Out-of-band updates.</strong> <code>driveOobUpdates(grid, opts)</code>
1051
1051
  watches for htmx's own out-of-band swaps landing on an element carrying
1052
1052
  <code>data-lattice-row="&lt;key&gt;"</code>, reads the swapped fragment as that row's
1053
- cells, and applies it to the grid in place &mdash; scroll position, selection and
1053
+ cells, and applies it to the grid in place: scroll position, selection and
1054
1054
  filter state are untouched, since nothing about the view is reloaded.</p>
1055
1055
  <p><strong>Browser history.</strong> On <code>htmx:beforeHistorySave</code>, every
1056
- live grid's state (<code>serialiseState</code> &mdash; sort, filters, column order and
1056
+ live grid's state (<code>serialiseState</code>: sort, filters, column order and
1057
1057
  widths, scroll position and selection, base64url-encoded and diffed against defaults
1058
1058
  so an untouched grid costs almost nothing) is written onto its element as
1059
1059
  <code>data-lattice-state</code>, which rides along in htmx's own history snapshot. On
1060
- <code>htmx:historyRestore</code>, it is read back and applied &mdash; browser back
1060
+ <code>htmx:historyRestore</code>, it is read back and applied: browser back
1061
1061
  returns a visitor to the sort, filter and scroll position they had, not a blank
1062
1062
  slate. A cache miss (the page was re-fetched from the server) restores nothing, since
1063
1063
  a fresh response is already the truth.</p>
@@ -1070,13 +1070,13 @@ driveInfiniteScroll(grid, document.getElementById('sentinel'), { columns: COLUMN
1070
1070
 
1071
1071
  <div class="why">
1072
1072
  <p><strong>What this does not do.</strong> It does not call <code>fetch</code> or
1073
- <code>htmx.ajax()</code> anywhere &mdash; htmx owns every request end to end; this
1073
+ <code>htmx.ajax()</code> anywhere: htmx owns every request end to end; this
1074
1074
  only supplies the moment and the parameters, and reads the response back in. It does
1075
1075
  not import htmx: every htmx-specific call goes through <code>globalThis.htmx</code>,
1076
1076
  read at call time, so loading this module never requires htmx to already be on the
1077
1077
  page, only to be present by the time a driven request actually fires. It ships as
1078
1078
  ESM and as a plain <code>&lt;script src&gt;</code> build with no bundler required,
1079
- with zero runtime dependencies beyond the grid itself and, at call time, htmx &mdash;
1079
+ with zero runtime dependencies beyond the grid itself and, at call time, htmx ,
1080
1080
  but because it references the grid's own internals directly rather than the copy
1081
1081
  already on the page, the bundle carries a full copy of the grid core alongside its
1082
1082
  own code, the same trade-off the web component and dhtmlx wrappers already make.</p>
@@ -1084,7 +1084,7 @@ driveInfiniteScroll(grid, document.getElementById('sentinel'), { columns: COLUMN
1084
1084
 
1085
1085
  <h2 id="concepts">How it works</h2>
1086
1086
  <p class="lead-in">
1087
- Four ideas explain most of the API. If you read nothing else, read this section — the rest of
1087
+ Four ideas explain most of the API. If you read nothing else, read this section, the rest of
1088
1088
  the guide assumes it.
1089
1089
  </p>
1090
1090
 
@@ -1092,7 +1092,7 @@ driveInfiniteScroll(grid, document.getElementById('sentinel'), { columns: COLUMN
1092
1092
  <p class="lead-in">
1093
1093
  Your data objects are held by reference and never copied. What the grid hands back is a
1094
1094
  <em>row wrapper</em>: your object under <code>data</code>, plus the identity and position the
1095
- grid needs — <code>key</code>, <code>index</code>, <code>level</code>, whether it is a group
1095
+ grid needs: <code>key</code>, <code>index</code>, <code>level</code>, whether it is a group
1096
1096
  row, whether it is expanded.
1097
1097
  </p>
1098
1098
  <div class="example">
@@ -1111,20 +1111,20 @@ grid.rows.values('r1') <span class="cmt">// every readable column, as an o
1111
1111
 
1112
1112
  <h3>Display index and row key are different things</h3>
1113
1113
  <p class="lead-in">
1114
- A display index is a position — row 0 is whatever is at the top right now, and it changes when
1114
+ A display index is a position: row 0 is whatever is at the top right now, and it changes when
1115
1115
  you sort. A key identifies a record for as long as it exists. Anything that has to survive a
1116
1116
  sort, a filter or a page change is keyed.
1117
1117
  </p>
1118
1118
  <div class="split">
1119
1119
  <div>
1120
- <p class="split__title split__title--no">Position — fragile</p>
1120
+ <p class="split__title split__title--no">Position: fragile</p>
1121
1121
  <pre><code>grid.rows.get(4)
1122
1122
  grid.selection.setRange({
1123
1123
  startRow: 0, endRow: 9, columns: ['cap'],
1124
1124
  })</code></pre>
1125
1125
  </div>
1126
1126
  <div>
1127
- <p class="split__title split__title--yes">Identity — durable</p>
1127
+ <p class="split__title split__title--yes">Identity: durable</p>
1128
1128
  <pre><code>grid.rows.byKey('CIR-100042')
1129
1129
  grid.edit.setCells([
1130
1130
  { key: 'CIR-100042', colId: 'cap', value: 99 },
@@ -1147,7 +1147,7 @@ grid.on('*', e =&gt; console.log(e.type, e)); <span class="cmt">// wildcard, f
1147
1147
  off(); <span class="cmt">// every subscription returns its own unsubscribe</span></code></pre>
1148
1148
  </div>
1149
1149
  <div class="why">
1150
- <p><code>origin</code> tells you where a change came from — <code>'user'</code>,
1150
+ <p><code>origin</code> tells you where a change came from: <code>'user'</code>,
1151
1151
  <code>'api'</code>, <code>'init'</code>, <code>'undo'</code>. It is what stops a feedback
1152
1152
  loop when you persist changes: a handler that writes to a server on
1153
1153
  <code>cell:changed</code> should usually ignore its own <code>'undo'</code> traffic, or at
@@ -1178,14 +1178,14 @@ LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid
1178
1178
  </p>
1179
1179
  <h2 id="columns-guide">Defining columns</h2>
1180
1180
  <p class="lead-in">
1181
- A column is an object with a <code>field</code> — the path to read from your data — and
1181
+ A column is an object with a <code>field</code> (the path to read from your data) and
1182
1182
  whatever else it needs. Everything except <code>field</code> or <code>id</code> is optional.
1183
1183
  </p>
1184
1184
 
1185
1185
  <div class="sig-line">{ field: 'site.address.postcode', title: 'Postcode', type: 'text', width: 110 }</div>
1186
1186
  <p class="lead-in">
1187
1187
  <code>field</code> reads a dot path, so nested data needs no flattening. The column's
1188
- <code>id</code> defaults to the field, which is what you use everywhere else — in
1188
+ <code>id</code> defaults to the field, which is what you use everywhere else: in
1189
1189
  <code>setCells</code>, in filters, in saved state.
1190
1190
  </p>
1191
1191
 
@@ -1222,7 +1222,7 @@ LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid
1222
1222
  },
1223
1223
  }
1224
1224
 
1225
- <span class="cmt">// compute(deps, ctx) — deps holds the resolved values of the columns you</span>
1225
+ <span class="cmt">// compute(deps, ctx): deps holds the resolved values of the columns you</span>
1226
1226
  <span class="cmt">// named; ctx carries { data, row, column, grid, context } when you need more.</span></code></pre>
1227
1227
  </div>
1228
1228
  <div class="why">
@@ -1249,13 +1249,13 @@ LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid
1249
1249
  <code>grid.columns.fit()</code> distributes the viewport width across visible columns, and
1250
1250
  <code>autoSize</code> measures content.
1251
1251
  </p>
1252
- <p>Both are also on the column menu &mdash; Move left, Move right, Move to start, Move to end, and
1253
- a Width submenu &mdash; and bound to the keyboard with a heading focused: <kbd>Alt</kbd> with a
1252
+ <p>Both are also on the column menu: Move left, Move right, Move to start, Move to end, and
1253
+ a Width submenu, and bound to the keyboard with a heading focused: <kbd>Alt</kbd> with a
1254
1254
  left or right arrow resizes, <kbd>Shift</kbd> with one moves the column. Neither operation
1255
1255
  depends on dragging.</p>
1256
1256
  <p>A pinned region holds the edge of the viewport only while there is something to scroll. Where
1257
- the columns are narrower than the grid &mdash; fixed widths, or a <code>flex</code> column that
1258
- has reached its <code>max</code> &mdash; nothing scrolls, so the pinned columns sit directly
1257
+ the columns are narrower than the grid: fixed widths, or a <code>flex</code> column that
1258
+ has reached its <code>max</code>: nothing scrolls, so the pinned columns sit directly
1259
1259
  after the centre ones and the spare width falls beyond them all, at the right of the grid.</p>
1260
1260
  <p>To take that space up rather than leave it, give a column <code>flex</code> and no
1261
1261
  <code>max</code>, or call <code>grid.columns.fit()</code>. Note that removing a column&rsquo;s
@@ -1278,10 +1278,10 @@ LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid
1278
1278
  <tr><td class="name">Temporal</td><td class="desc"><code>datetime</code>, <code>time</code>, <code>duration</code></td></tr>
1279
1279
  <tr><td class="name">Network</td><td class="desc"><code>ipv4</code>, <code>ipv6</code>, <code>cidr</code>, <code>mac</code></td></tr>
1280
1280
  <tr><td class="name">Numeric bases</td><td class="desc"><code>hex</code>, <code>hex8</code>, <code>hex16</code>, <code>hex32</code>, <code>binary</code>, <code>binary8</code>, <code>octal</code></td></tr>
1281
- <tr><td class="name">Units — computing</td><td class="desc"><code>bytes</code>, <code>megabytes</code>, <code>gigabytes</code>, <code>bitrate</code>, <code>gigabits</code></td></tr>
1282
- <tr><td class="name">Units — physical</td><td class="desc"><code>metres</code>, <code>millimetres</code>, <code>kilometres</code>, <code>grams</code>, <code>kilograms</code>, <code>tonnes</code>, <code>seconds</code>, <code>milliseconds</code>, <code>hours</code></td></tr>
1283
- <tr><td class="name">Units — engineering</td><td class="desc"><code>speed</code>, <code>kph</code>, <code>mph</code>, <code>knots</code>, <code>acceleration</code>, <code>area</code>, <code>hectares</code>, <code>volume</code>, <code>cubicMetres</code>, <code>energy</code>, <code>kilowattHours</code>, <code>power</code>, <code>kilowatts</code>, <code>force</code>, <code>pressure</code>, <code>bar</code>, <code>psi</code>, <code>torque</code>, <code>density</code>, <code>flow</code>, <code>litresPerMinute</code>, <code>radians</code>, <code>degrees</code></td></tr>
1284
- <tr><td class="name">Units — electrical and scientific</td><td class="desc"><code>voltage</code>, <code>current</code>, <code>resistance</code>, <code>capacitance</code>, <code>inductance</code>, <code>charge</code>, <code>conductance</code>, <code>fluxDensity</code>, <code>luminousFlux</code>, <code>illuminance</code>, <code>substance</code>, <code>absorbedDose</code>, <code>equivalentDose</code>, <code>radioactivity</code>, <code>luminousIntensity</code>, <code>doseRate</code>, <code>rpm</code>, <code>angularVelocity</code>, <code>ppm</code>, <code>ppb</code>, <code>basisPoints</code>, <code>molarity</code>, <code>massFlow</code>, <code>tonnesPerHour</code>, <code>viscosity</code>, <code>kinematicViscosity</code>, <code>thermalConductivity</code>, <code>specificHeat</code>, <code>frequency</code></td></tr>
1281
+ <tr><td class="name">Units: computing</td><td class="desc"><code>bytes</code>, <code>megabytes</code>, <code>gigabytes</code>, <code>bitrate</code>, <code>gigabits</code></td></tr>
1282
+ <tr><td class="name">Units: physical</td><td class="desc"><code>metres</code>, <code>millimetres</code>, <code>kilometres</code>, <code>grams</code>, <code>kilograms</code>, <code>tonnes</code>, <code>seconds</code>, <code>milliseconds</code>, <code>hours</code></td></tr>
1283
+ <tr><td class="name">Units: engineering</td><td class="desc"><code>speed</code>, <code>kph</code>, <code>mph</code>, <code>knots</code>, <code>acceleration</code>, <code>area</code>, <code>hectares</code>, <code>volume</code>, <code>cubicMetres</code>, <code>energy</code>, <code>kilowattHours</code>, <code>power</code>, <code>kilowatts</code>, <code>force</code>, <code>pressure</code>, <code>bar</code>, <code>psi</code>, <code>torque</code>, <code>density</code>, <code>flow</code>, <code>litresPerMinute</code>, <code>radians</code>, <code>degrees</code></td></tr>
1284
+ <tr><td class="name">Units: electrical and scientific</td><td class="desc"><code>voltage</code>, <code>current</code>, <code>resistance</code>, <code>capacitance</code>, <code>inductance</code>, <code>charge</code>, <code>conductance</code>, <code>fluxDensity</code>, <code>luminousFlux</code>, <code>illuminance</code>, <code>substance</code>, <code>absorbedDose</code>, <code>equivalentDose</code>, <code>radioactivity</code>, <code>luminousIntensity</code>, <code>doseRate</code>, <code>rpm</code>, <code>angularVelocity</code>, <code>ppm</code>, <code>ppb</code>, <code>basisPoints</code>, <code>molarity</code>, <code>massFlow</code>, <code>tonnesPerHour</code>, <code>viscosity</code>, <code>kinematicViscosity</code>, <code>thermalConductivity</code>, <code>specificHeat</code>, <code>frequency</code></td></tr>
1285
1285
  <tr><td class="name">Temperature</td><td class="desc"><code>celsius</code>, <code>fahrenheit</code>, <code>kelvin</code></td></tr>
1286
1286
  <tr><td class="name">Structured</td><td class="desc"><code>json</code>, <code>colour</code>, <code>rating</code>, <code>percent</code></td></tr>
1287
1287
  </tbody>
@@ -1326,14 +1326,14 @@ dataTypes: {
1326
1326
  <div class="why">
1327
1327
  <p><strong>Ambiguous units are refused, not guessed.</strong> A US gallon and an imperial
1328
1328
  gallon differ by about a fifth, and "ton" means three different masses. Each has its own
1329
- symbol — <code>gal (US)</code>, <code>ton (UK)</code> — and the bare word is claimed by all of
1329
+ symbol: <code>gal (US)</code>, <code>ton (UK)</code>, and the bare word is claimed by all of
1330
1330
  them, so typing it is rejected rather than resolved. A <code>gal</code> silently taken as US
1331
1331
  in a UK deployment is data corruption that reads as rounding.</p>
1332
1332
  <p>The same rule catches case: <code>mV</code> and <code>MV</code> are a billion apart, so both
1333
1333
  exact spellings work and the case-folded <code>mv</code> is refused.</p>
1334
1334
  <p><strong>Customary units are accepted but never chosen.</strong> <code>display: 'auto'</code>
1335
1335
  walks the coherent SI ladder only. With the calorie, the BTU and the kilojoule all on one
1336
- ladder, 4,000 J would render as <code>3.79 BTU</code> — auto picks the largest unit that fits,
1336
+ ladder, 4,000 J would render as <code>3.79 BTU</code>: auto picks the largest unit that fits,
1337
1337
  and the BTU happens to be larger than the kilojoule. Ask for a BTU by name and you get one.</p>
1338
1338
  </div>
1339
1339
 
@@ -1348,7 +1348,7 @@ dataTypes: {
1348
1348
 
1349
1349
  <div class="why">
1350
1350
  <p><strong>Angles wrap, so their mean is replaced.</strong> The average of 359° and 1° is 0°,
1351
- and the arithmetic answer — 180° — is a confident, plausible number pointing in exactly the
1351
+ and the arithmetic answer (180°) is a confident, plausible number pointing in exactly the
1352
1352
  wrong direction. A <code>degrees</code> or <code>radians</code> column averages by direction
1353
1353
  instead, and reports nothing where the angles cancel and there is no mean direction to give.
1354
1354
  The <em>sum</em> stays arithmetic, because a total rotation of 720° is two turns and that is a
@@ -1356,8 +1356,8 @@ dataTypes: {
1356
1356
  <p><strong>Temperature is its own type, not a unit.</strong> Every other unit is a
1357
1357
  multiplication; Celsius to Fahrenheit carries an offset, and zero Celsius is not zero
1358
1358
  anything, so no factor converts it. <code>celsius</code>, <code>fahrenheit</code> and
1359
- <code>kelvin</code> convert on input — type <code>72 F</code> into a Celsius column and it
1360
- stores 22.2 — and <strong>refuse to be summed</strong>: twenty degrees plus twenty degrees is
1359
+ <code>kelvin</code> convert on input: type <code>72 F</code> into a Celsius column and it
1360
+ stores 22.2, and <strong>refuse to be summed</strong>: twenty degrees plus twenty degrees is
1361
1361
  not forty degrees, and a footer saying so would be believed.</p>
1362
1362
  </div>
1363
1363
 
@@ -1371,14 +1371,14 @@ dataTypes: {
1371
1371
  <p class="example__label">One key each, and the types arrive</p>
1372
1372
  <pre><code>columns: [
1373
1373
  { field: 'sku' }, <span class="cmt">// text</span>
1374
- { field: 'quantity' }, <span class="cmt">// number — aligned right, numeric filter</span>
1374
+ { field: 'quantity' }, <span class="cmt">// number: aligned right, numeric filter</span>
1375
1375
  { field: 'shipped' }, <span class="cmt">// date</span>
1376
- { field: 'expedited' } <span class="cmt">// boolean — checkbox editor</span>
1376
+ { field: 'expedited' } <span class="cmt">// boolean: checkbox editor</span>
1377
1377
  ]</code></pre>
1378
1378
  </div>
1379
1379
  <p>Sampling happens once, when rows first arrive. A grid built empty and filled later infers on
1380
1380
  that first load, so fetching after construction is no reason to declare types you would otherwise
1381
- leave out. Later loads keep the types already settled on — data that arrives tomorrow cannot
1381
+ leave out. Later loads keep the types already settled on: data that arrives tomorrow cannot
1382
1382
  change a column's type under a formatter or an editor that was configured around it.</p>
1383
1383
  <p>Only the built-in names are ever inferred. Candidates are tried in registration order, so every
1384
1384
  string reaches <code>text</code> and every number reaches <code>number</code> before an extended
@@ -1397,7 +1397,7 @@ dataTypes: {
1397
1397
  parsed back into a value, how the value is formatted, which filter kind the header offers, how
1398
1398
  the column is stored, and what lands in Excel and on the clipboard. That is what inference
1399
1399
  supplies for free on a plain column, and what naming a type explicitly gives you where the data
1400
- cannot say it — a duration, a byte count, a network address.</p>
1400
+ cannot say it, a duration, a byte count, a network address.</p>
1401
1401
  </div>
1402
1402
 
1403
1403
  <h3>Dates are stored as strings, deliberately</h3>
@@ -1413,7 +1413,7 @@ dataTypes: {
1413
1413
  convert.</p>
1414
1414
  <p>It is also faster and smaller: ISO 8601 sorts lexicographically in the same order it sorts
1415
1415
  chronologically, so a date column sorts as text, and repeated dates dictionary-encode well.</p>
1416
- <p>When you genuinely mean an instant — a log timestamp — use <code>datetime</code> and set
1416
+ <p>When you genuinely mean an instant (a log timestamp) use <code>datetime</code> and set
1417
1417
  <code>format.timeZone</code>.</p>
1418
1418
  </div>
1419
1419
 
@@ -1429,14 +1429,14 @@ format: { <span class="cmt">// when the short
1429
1429
  style: 'currency', currency: 'GBP', decimals: 2,
1430
1430
  negative: 'parentheses', <span class="cmt">// (£1,234.50)</span>
1431
1431
  negativeClass: 'is-loss',
1432
- nullDisplay: '—',
1432
+ nullDisplay: ', ',
1433
1433
  }</code></pre>
1434
1434
  </div>
1435
1435
 
1436
1436
  <h3>Lookups</h3>
1437
1437
  <p class="lead-in">
1438
1438
  A lookup column stores an id and shows a label. Sorting, filtering, grouping, copying and
1439
- exporting all use the label, because that is the thing the user is reasoning about — but the
1439
+ exporting all use the label, because that is the thing the user is reasoning about, but the
1440
1440
  data keeps the id.
1441
1441
  </p>
1442
1442
  <div class="example">
@@ -1487,7 +1487,7 @@ columns: [{ field: 'flags', type: 'reg32' }],
1487
1487
  </p>
1488
1488
 
1489
1489
  <div class="example">
1490
- <p class="example__label">Decorations — the common cases, without writing a renderer</p>
1490
+ <p class="example__label">Decorations, the common cases, without writing a renderer</p>
1491
1491
  <pre><code>cell: { decoration: 'pill' } <span class="cmt">// a status chip</span>
1492
1492
  cell: { decoration: 'bar', min: 0, max: 1 } <span class="cmt">// an inline bar</span>
1493
1493
  cell: { decoration: 'heat', ramp: 'redGreen' } <span class="cmt">// a heat fill</span>
@@ -1495,7 +1495,7 @@ cell: { decoration: 'dot' } <span class="cmt">// a leading
1495
1495
  </div>
1496
1496
 
1497
1497
  <div class="example">
1498
- <p class="example__label">Variants — mapping a value to a semantic colour</p>
1498
+ <p class="example__label">Variants: mapping a value to a semantic colour</p>
1499
1499
  <pre><code>cell: {
1500
1500
  decoration: 'pill',
1501
1501
  variant: { when: [
@@ -1505,7 +1505,7 @@ cell: { decoration: 'dot' } <span class="cmt">// a leading
1505
1505
  }</code></pre>
1506
1506
  </div>
1507
1507
  <div class="why">
1508
- <p>Variants are semantic tokens rather than colours — <code>danger</code>, not
1508
+ <p>Variants are semantic tokens rather than colours: <code>danger</code>, not
1509
1509
  <code>#c22b2b</code>. The theme decides what danger looks like, and it looks the same in the
1510
1510
  status pill, the filter chip and the validation message. Changing the palette is one custom
1511
1511
  property, not a search for hex codes.</p>
@@ -1525,7 +1525,7 @@ columns: [{ field: 'history', cell: { render: 'sparkline' } }],</code></pre>
1525
1525
  <div class="why">
1526
1526
  <p><code>refresh</code> returning <code>true</code> is the contract that makes recycling work:
1527
1527
  it means "I updated in place, keep this element". Return <code>false</code> and the grid
1528
- rebuilds the cell. A renderer that only implements <code>render</code> still works — it is
1528
+ rebuilds the cell. A renderer that only implements <code>render</code> still works, it is
1529
1529
  just rebuilt on every reuse.</p>
1530
1530
  </div>
1531
1531
 
@@ -1540,8 +1540,8 @@ cell: { template: '&lt;span&gt;{{{ value }}}&lt;/span&gt;' }</code></pre>
1540
1540
  <div class="why">
1541
1541
  <p><strong>The flag permits markup, not code.</strong> Without it, <code>{{ }}</code> escapes
1542
1542
  and a <code>{{{ }}}</code> segment is refused outright. With it, an interpolated value may
1543
- carry presentational markup — <code>&lt;b&gt;</code>, <code>&lt;a href&gt;</code>, a
1544
- <code>&lt;span&gt;</code> — and everything executable is still stripped from it:
1543
+ carry presentational markup: <code>&lt;b&gt;</code>, <code>&lt;a href&gt;</code>, a
1544
+ <code>&lt;span&gt;</code>, and everything executable is still stripped from it:
1545
1545
  <code>&lt;script&gt;</code>, <code>&lt;iframe&gt;</code>, <code>&lt;style&gt;</code> and the
1546
1546
  other code-bearing tags, every <code>on*</code> handler attribute, and
1547
1547
  <code>javascript:</code> or <code>data:</code> URLs including entity-encoded spellings of
@@ -1562,21 +1562,21 @@ cell: { template: '&lt;span&gt;{{{ value }}}&lt;/span&gt;' }</code></pre>
1562
1562
  </p>
1563
1563
  <div class="example">
1564
1564
  <p class="example__label">By scope</p>
1565
- <pre><code><span class="cmt">// Cells and columns — declared on the column</span>
1565
+ <pre><code><span class="cmt">// Cells and columns: declared on the column</span>
1566
1566
  { field: 'margin', cell: {
1567
1567
  class: 'tabular',
1568
1568
  classWhen: { 'is-loss': (p) =&gt; p.value &lt; 0 },
1569
1569
  style: (p) =&gt; ({ fontWeight: p.value &gt; 1e6 ? 650 : 400 }),
1570
1570
  }}
1571
1571
 
1572
- <span class="cmt">// Rows — on the grid</span>
1572
+ <span class="cmt">// Rows: on the grid</span>
1573
1573
  rowClass: (p) =&gt; p.data.slaBreached ? 'row-breach' : null,
1574
1574
  rowStyle: (p) =&gt; p.data.region === 'AMER' ? { borderLeft: '3px solid #7c3aed' } : null,</code></pre>
1575
1575
  </div>
1576
1576
  <div class="why">
1577
1577
  <p>All of these are re-evaluated on every repaint and remove what they added last time first.
1578
1578
  That is not caution. Rows and cells come from pools, so an element that carried a class for
1579
- one row will later carry a different row — a class written once and left alone smears down
1579
+ one row will later carry a different row, a class written once and left alone smears down
1580
1580
  the grid as the user scrolls.</p>
1581
1581
  </div>
1582
1582
 
@@ -1596,8 +1596,8 @@ rowStyle: (p) =&gt; p.data.region === 'AMER' ? { borderLeft: '3px solid #7c3aed'
1596
1596
  <p class="lead-in">
1597
1597
  Four themes ship. With no <code>theme</code> set, the grid follows the viewer's
1598
1598
  <code>prefers-color-scheme</code> between light and dark; naming one pins it.
1599
- Density is separate — <code>compact</code>, <code>standard</code>, <code>comfortable</code>
1600
- or <code>spacious</code> — and combines with any of them.
1599
+ Density is separate: <code>compact</code>, <code>standard</code>, <code>comfortable</code>
1600
+ or <code>spacious</code>, and combines with any of them.
1601
1601
  </p>
1602
1602
  <div class="example">
1603
1603
  <p class="example__label">Pinning a theme, at build time or at runtime</p>
@@ -1611,14 +1611,14 @@ grid.set('theme', <span class="kw">null</span>); <span class="cmt">// back to
1611
1611
  <tbody>
1612
1612
  <tr><td class="name"><code>light</code></td><td class="desc">The default. Follows <code>prefers-color-scheme</code> when <code>theme</code> is unset.</td></tr>
1613
1613
  <tr><td class="name"><code>dark</code></td><td class="desc">The same palette inverted, with the accent and status hues re-picked for a dark ground rather than reused.</td></tr>
1614
- <tr><td class="name"><code>high-contrast</code></td><td class="desc">Not "dark with more contrast". Text is 21:1 and borders 6.1:1 against the background, where the other themes sit near 1.3:1 on borders — WCAG 1.4.11 asks for 3:1 on the boundaries a user has to find. Cell borders are drawn rather than implied, selected rows carry an outline as well as a fill, and every status pill has a solid border so it does not depend on hue alone.</td></tr>
1614
+ <tr><td class="name"><code>high-contrast</code></td><td class="desc">Not "dark with more contrast". Text is 21:1 and borders 6.1:1 against the background, where the other themes sit near 1.3:1 on borders, WCAG 1.4.11 asks for 3:1 on the boundaries a user has to find. Cell borders are drawn rather than implied, selected rows carry an outline as well as a fill, and every status pill has a solid border so it does not depend on hue alone.</td></tr>
1615
1615
  <tr><td class="name"><code>terminal</code></td><td class="desc">A phosphor console: one hue on near-black, monospaced throughout. Status is carried by brightness rather than colour, so the palette stays a palette.</td></tr>
1616
1616
  </tbody>
1617
1617
  </table>
1618
1618
  </div>
1619
1619
  <h3>Forced colours</h3>
1620
1620
  <p class="lead-in">
1621
- Windows High Contrast Mode replaces the palette outright &mdash; that is the point of it, and
1621
+ Windows High Contrast Mode replaces the palette outright: that is the point of it, and
1622
1622
  no stylesheet should fight it. What the grid does instead is translate every piece of meaning
1623
1623
  it normally carries in a background tint into something the mode preserves.
1624
1624
  </p>
@@ -1626,8 +1626,8 @@ grid.set('theme', <span class="kw">null</span>); <span class="cmt">// back to
1626
1626
  which forced colours do not render, and gains a rule in its place. Status pills, fill
1627
1627
  decorations, progress tracks and histogram bars each gain a border, because a fill with no
1628
1628
  edge is invisible once its colour is discarded. Diff states stop depending on hue altogether:
1629
- added, removed and changed are told apart by border style &mdash; solid, dashed and doubled
1630
- &mdash; since the mode offers no way to keep four distinct colours.</p>
1629
+ added, removed and changed are told apart by border style: solid, dashed and doubled
1630
+ : since the mode offers no way to keep four distinct colours.</p>
1631
1631
  <p>Two things deliberately keep their colour, declared with
1632
1632
  <code>forced-color-adjust</code>: a colour swatch, where the colour <em>is</em> the value being
1633
1633
  shown, and a collaborator's presence colour, which is how one person is told from another.
@@ -1645,21 +1645,21 @@ grid.set('theme', <span class="kw">null</span>); <span class="cmt">// back to
1645
1645
  <p class="lead-in">
1646
1646
  Every selector in the stylesheet is namespaced under <code>.lattice</code>, so the grid cannot
1647
1647
  restyle your page. From 1.4.0 the reverse is also true: the grid gives the elements it builds
1648
- a floor for the properties a page is most likely to set on a bare tag — margin, padding,
1648
+ a floor for the properties a page is most likely to set on a bare tag: margin, padding,
1649
1649
  border, radius, background, shadow, text transform and letter spacing, plus type and colour on
1650
1650
  form controls, which inherit neither.
1651
1651
  </p>
1652
1652
  <div class="why">
1653
1653
  <p><strong>Why this is needed at all.</strong> A grid is mounted inside somebody else's
1654
- stylesheet. A rule as ordinary as <code>section { padding: 5.5rem 0 }</code> — a marketing
1655
- page, a CMS theme, a Tailwind preflight — matches by tag name, and the grid builds parts of
1654
+ stylesheet. A rule as ordinary as <code>section { padding: 5.5rem 0 }</code>, a marketing
1655
+ page, a CMS theme, a Tailwind preflight: matches by tag name, and the grid builds parts of
1656
1656
  its own interface from those tags: the tool panel's filter rows are
1657
1657
  <code>&lt;section&gt;</code> elements. Without the reset, 5.5rem of somebody else's padding
1658
1658
  lands on every one of them.</p>
1659
1659
  <p>The reset uses no <code>!important</code>. It is specificity (0,1,1) and every rule that
1660
1660
  dresses a grid element is (0,2,0) or higher, so the grid's own styling always wins and the
1661
- reset only fills a gap. Yours wins too, on the same terms: a rule aimed at a Lattice class —
1662
- <code>.lattice .lat-cell { … }</code> — outranks it, so overriding the grid deliberately works
1661
+ reset only fills a gap. Yours wins too, on the same terms: a rule aimed at a Lattice class ,
1662
+ <code>.lattice .lat-cell { … }</code>: outranks it, so overriding the grid deliberately works
1663
1663
  exactly as before. Only bare-tag rules are shut out.</p>
1664
1664
  <p>It touches box model and decoration only. Nothing in it sets <code>display</code>,
1665
1665
  <code>position</code> or any dimension: those belong to the renderer, and a reset that reached
@@ -1667,7 +1667,7 @@ grid.set('theme', <span class="kw">null</span>); <span class="cmt">// back to
1667
1667
  </div>
1668
1668
  <h2 id="loading">Loading and updating data</h2>
1669
1669
  <p class="lead-in">
1670
- Data usually arrives after the grid does. Build it empty, then load — the sort, filters,
1670
+ Data usually arrives after the grid does. Build it empty, then load, the sort, filters,
1671
1671
  grouping and column layout you set up in the meantime all survive and apply to the new data.
1672
1672
  </p>
1673
1673
  <div class="example">
@@ -1682,8 +1682,8 @@ grid.overlay.hide();</code></pre>
1682
1682
 
1683
1683
  <h3>Incremental changes</h3>
1684
1684
  <p class="lead-in">
1685
- <code>rows.load</code> replaces everything. When you have a delta — a websocket message, a
1686
- save that returned the updated record — apply just that.
1685
+ <code>rows.load</code> replaces everything. When you have a delta, a websocket message, a
1686
+ save that returned the updated record: apply just that.
1687
1687
  </p>
1688
1688
  <div class="example">
1689
1689
  <p class="example__label">Adds, updates and removals in one call</p>
@@ -1698,15 +1698,15 @@ grid.overlay.hide();</code></pre>
1698
1698
  <div class="why">
1699
1699
  <p><strong>An update is a patch.</strong> Fields absent from it are untouched, so a delta
1700
1700
  arriving from a websocket or coming back from a save can be applied as-is without reading the
1701
- row first. This has to be said explicitly because the opposite &mdash; assigning the patch
1702
- over the row &mdash; looks identical for a caller who happens to send whole rows and silently
1701
+ row first. This has to be said explicitly because the opposite: assigning the patch
1702
+ over the row: looks identical for a caller who happens to send whole rows and silently
1703
1703
  destroys data for one who does not.</p>
1704
1704
  <p><strong>Coalescing merges fields rather than keeping the last message.</strong> A feed
1705
1705
  sending <code>{price}</code> and <code>{volume}</code> as separate messages inside one window
1706
1706
  keeps both. Coalescing may reorder work; it may not lose it.</p>
1707
1707
  <p><strong>Flushing happens on a frame, with a timer behind it.</strong> A queued batch lands
1708
1708
  on a paint boundary, which is what makes "ten thousand updates, one repaint" true rather than
1709
- usually true &mdash; a timer can fire twice between two paints. But
1709
+ usually true, a timer can fire twice between two paints. But
1710
1710
  <code>requestAnimationFrame</code> is not guaranteed to fire at all: a backgrounded tab stops
1711
1711
  firing it entirely. So a frame and a timer are armed together and the first to arrive wins. In
1712
1712
  a foreground tab the frame always wins, at ~16ms against a 50ms fallback; in a hidden tab the
@@ -1715,7 +1715,7 @@ grid.overlay.hide();</code></pre>
1715
1715
  <p><strong>A long flush defers rather than blocks.</strong> <code>updates.budgetMs</code> caps
1716
1716
  how long one flush spends applying; over budget, the remainder returns to the queue and lands
1717
1717
  next frame, and the promise a caller is holding resolves when their rows actually land rather
1718
- than when the first slice does. Slicing is by row and only for updates &mdash; a partially
1718
+ than when the first slice does. Slicing is by row and only for updates, a partially
1719
1719
  applied row is not a state the store should be in, and splitting a structural change would
1720
1720
  re-run the pipeline twice for one batch. <code>stats().deferrals</code> rising steadily means
1721
1721
  the feed is arriving faster than the grid can apply it.</p>
@@ -1727,7 +1727,7 @@ grid.overlay.hide();</code></pre>
1727
1727
  <p>This runs the minimum pipeline. An update touching no sorted, filtered or grouped column
1728
1728
  skips those stages entirely and only the totals and the affected cells refresh. Adds and
1729
1729
  removals are structural and re-run everything.</p>
1730
- <p>Removals tombstone in place rather than compacting, so every existing index stays valid —
1730
+ <p>Removals tombstone in place rather than compacting, so every existing index stays valid ,
1731
1731
  which is what lets selection, expansion state and cached permutations survive a delete.</p>
1732
1732
  </div>
1733
1733
 
@@ -1778,7 +1778,7 @@ grid.rows.forEachAll(r =&gt; { total += r.data.amount }); <span class="cmt">//
1778
1778
  <div class="why">
1779
1779
  <p>A remote or paged source holds the page it has fetched, not the whole set, so there is
1780
1780
  nothing there to walk past the filters. It warns and walks what it has rather than quietly
1781
- returning the filtered rows &mdash; a caller who asked for everything and silently received a
1781
+ returning the filtered rows, a caller who asked for everything and silently received a
1782
1782
  subset gets a number that looks entirely plausible and is wrong.</p>
1783
1783
  </div>
1784
1784
 
@@ -1799,7 +1799,7 @@ grid.sort.clear();</code></pre>
1799
1799
  <p class="lead-in">
1800
1800
  Clicking a header cycles ascending, descending, none; shift-clicking a second header adds to
1801
1801
  the sort rather than replacing it. A column can supply its own <code>compare</code>, and a
1802
- type already has one — dates compare chronologically, IP addresses numerically rather than as
1802
+ type already has one: dates compare chronologically, IP addresses numerically rather than as
1803
1803
  strings, durations by length.
1804
1804
  </p>
1805
1805
 
@@ -1890,7 +1890,7 @@ grid.rows.collapse('EMEA');</code></pre>
1890
1890
  </table>
1891
1891
  </div>
1892
1892
  <p class="lead-in">
1893
- Nothing has to be configured for this, and the reported number is the same either way —
1893
+ Nothing has to be configured for this, and the reported number is the same either way ,
1894
1894
  where a running value cannot be trusted, the column falls back to a full pass rather than
1895
1895
  reporting a value it is unsure of.
1896
1896
  </p>
@@ -1898,7 +1898,7 @@ grid.rows.collapse('EMEA');</code></pre>
1898
1898
  <h3 id="show-total-in-header">showTotalInHeader</h3>
1899
1899
  <p class="lead-in">
1900
1900
  Under grouping or pivot, a totalled column's cells hold an aggregate rather than a row's own
1901
- value. On by default, the heading says which — a small <code>SUM</code> line above
1901
+ value. On by default, the heading says which, a small <code>SUM</code> line above
1902
1902
  <code>Capacity</code>, <code>AVERAGE</code> above <code>Margin</code>. The heading returns to
1903
1903
  the column's own title when grouping and pivot are both off.
1904
1904
  </p>
@@ -1909,7 +1909,7 @@ grid.rows.collapse('EMEA');</code></pre>
1909
1909
  <p class="lead-in">
1910
1910
  The reduction goes on its own line rather than reading <code>Sum of Capacity</code> across
1911
1911
  one. A header cell reserves width for its sort, filter and menu buttons whether or not they
1912
- are showing, so the label gets well under half the column — on a default column, 54px of
1912
+ are showing, so the label gets well under half the column: on a default column, 54px of
1913
1913
  129px. One line truncated to <code>Sum o…</code>, trading the column's identity for its
1914
1914
  reduction. Stacked, it costs no width at all.
1915
1915
  </p>
@@ -1936,7 +1936,7 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
1936
1936
  the near edge, beside the row headings; <code>'after'</code> puts it at the far edge, which is
1937
1937
  where a spreadsheet puts a grand total.</p>
1938
1938
  <p><strong>It costs a column, not a pass.</strong> A pivoted group row still carries its
1939
- reduction over every one of its leaves — which is exactly the total across all pivot values —
1939
+ reduction over every one of its leaves, which is exactly the total across all pivot values ,
1940
1940
  so these columns read a number that has already been computed. They also count towards
1941
1941
  <code>maxColumns</code>, since they are columns like any other.</p>
1942
1942
  <p><strong>Opt in.</strong> Omitted, a pivot has the columns it has always had, so the option
@@ -1950,7 +1950,7 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
1950
1950
  </div>
1951
1951
  <p class="lead-in">
1952
1952
  By default a total describes what is on screen: filter the grid and every total moves with
1953
- it. Setting this to <code>false</code> makes the filter a lens instead — totals report the
1953
+ it. Setting this to <code>false</code> makes the filter a lens instead: totals report the
1954
1954
  whole dataset no matter what is filtered out. Both the grand total and each group total
1955
1955
  follow the setting, so a group row shows the total for every row belonging to that group,
1956
1956
  not only the ones currently visible.
@@ -1958,7 +1958,7 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
1958
1958
  <p class="lead-in">
1959
1959
  A group whose every row the filter removed has no row to appear on, but its rows still count
1960
1960
  toward the totals above it. Editing a hidden row moves the totals, because it is part of the
1961
- dataset they describe — under the default it does not, because it is not part of the view
1961
+ dataset they describe: under the default it does not, because it is not part of the view
1962
1962
  they describe.
1963
1963
  </p>
1964
1964
  <p class="lead-in">
@@ -1981,7 +1981,7 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
1981
1981
  <p class="lead-in">
1982
1982
  Group totals re-reduce every totalled column on every change, including columns the change
1983
1983
  did not touch. Switching this on reduces only the columns whose values actually moved, and
1984
- an update that rewrites a field with the value it already held reduces nothing at all —
1984
+ an update that rewrites a field with the value it already held reduces nothing at all ,
1985
1985
  which is what a feed resending unchanged fields looks like.
1986
1986
  </p>
1987
1987
  <p class="lead-in">
@@ -1993,7 +1993,7 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
1993
1993
  <tbody>
1994
1994
  <tr><td class="name">One of the four columns</td><td class="desc">15.4ms</td><td class="desc">7.3ms</td></tr>
1995
1995
  <tr><td class="name">All four columns</td><td class="desc">15.0ms</td><td class="desc">14.5ms</td></tr>
1996
- <tr><td class="name">Nothing — same values rewritten</td><td class="desc">15.6ms</td><td class="desc">7.3ms</td></tr>
1996
+ <tr><td class="name">Nothing: same values rewritten</td><td class="desc">15.6ms</td><td class="desc">7.3ms</td></tr>
1997
1997
  </tbody>
1998
1998
  </table>
1999
1999
  </div>
@@ -2005,14 +2005,14 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
2005
2005
  <strong>It is off by default because it is an assertion, not just an optimisation.</strong>
2006
2006
  Skipping a column assumes its total depends on nothing but that column's own values. That
2007
2007
  is true of every built-in reduction. It need not be true of a <code>total</code> supplied as
2008
- a function, which also receives the row, the grid and <code>config.context</code> — such a
2008
+ a function, which also receives the row, the grid and <code>config.context</code>: such a
2009
2009
  total is only recomputed when its own column changes, so a function that reads application
2010
2010
  state outside the column will report the value from the last time that column moved. Leave
2011
2011
  the option off if any of your total functions work that way.
2012
2012
  </p>
2013
2013
  <p class="lead-in">
2014
- Anything that changes which rows a total covers — adding or removing rows, filtering,
2015
- sorting, grouping, or changing which columns are totalled — reduces everything again
2014
+ Anything that changes which rows a total covers: adding or removing rows, filtering,
2015
+ sorting, grouping, or changing which columns are totalled: reduces everything again
2016
2016
  regardless of the option.
2017
2017
  </p>
2018
2018
 
@@ -2043,7 +2043,7 @@ grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span
2043
2043
  </div>
2044
2044
 
2045
2045
  <p class="lead-in">
2046
- The objects are yours and are rendered through the ordinary column pipeline — value getters,
2046
+ The objects are yours and are rendered through the ordinary column pipeline: value getters,
2047
2047
  formatters, cell renderers and conditional formatting all run, so a pinned row looks like the
2048
2048
  data it sits against without you rebuilding any of that.
2049
2049
  </p>
@@ -2068,7 +2068,7 @@ grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span
2068
2068
  <thead><tr><th>Point</th><th>Behaviour</th></tr></thead>
2069
2069
  <tbody>
2070
2070
  <tr><td class="name">Order</td><td class="desc">Rows appear in the order of the array. At the bottom edge, the grand total comes first and your rows sit below it.</td></tr>
2071
- <tr><td class="name">Height</td><td class="desc">From <code>rowHeight</code>, including the function form — which is called with the pinned row, so you can measure your own content. The body reserves exactly the strip's height, so no data row hides underneath it.</td></tr>
2071
+ <tr><td class="name">Height</td><td class="desc">From <code>rowHeight</code>, including the function form, which is called with the pinned row, so you can measure your own content. The body reserves exactly the strip's height, so no data row hides underneath it.</td></tr>
2072
2072
  <tr><td class="name">Updating</td><td class="desc">Pass a <em>new</em> array. Array identity is how the grid knows the rows changed; pushing into the array you passed before will not repaint.</td></tr>
2073
2073
  <tr><td class="name">Editing</td><td class="desc">A pinned row has no place in the store to write to, so it is not editable.</td></tr>
2074
2074
  </tbody>
@@ -2078,7 +2078,7 @@ grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span
2078
2078
  <h2 id="full-width-rows">Full-width rows</h2>
2079
2079
  <p class="lead-in">
2080
2080
  A row drawn as a single band across every column instead of being divided into them: a
2081
- section banner, an explanatory note, an empty-group message, a “load more” affordance —
2081
+ section banner, an explanatory note, an empty-group message, a “load more” affordance ,
2082
2082
  anything that belongs between rows and is not itself divided by the columns.
2083
2083
  </p>
2084
2084
 
@@ -2102,7 +2102,7 @@ grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span
2102
2102
 
2103
2103
  <p class="lead-in">
2104
2104
  The band <strong>holds still while the columns scroll under it</strong>, which is what a
2105
- banner is for — text that scrolled sideways out of view with the columns would be a worse
2105
+ banner is for: text that scrolled sideways out of view with the columns would be a worse
2106
2106
  version of a cell. It is drawn over the pinned regions as well as the centre, so it genuinely
2107
2107
  spans every column.
2108
2108
  </p>
@@ -2115,8 +2115,8 @@ grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span
2115
2115
  between them: <em>full-width</em> changes how a row looks, <em>pinned</em> changes whether a
2116
2116
  row is data at all.</p>
2117
2117
  <p>So if your banners must not appear in an export or a row count, they should not be in the
2118
- data. If they are section headings that belong with the records they head — and should sort,
2119
- filter and export alongside them — this is the right tool.</p>
2118
+ data. If they are section headings that belong with the records they head, and should sort,
2119
+ filter and export alongside them: this is the right tool.</p>
2120
2120
  <p>One consequence worth stating plainly: sorting reorders banners along with everything
2121
2121
  else, because the predicate follows the row and not its position. Either do not offer sorting
2122
2122
  on such a grid, or sort on a key that keeps each section together.</p>
@@ -2153,7 +2153,7 @@ grid.on('row:moved', ({ key, from, to }) =&gt; {
2153
2153
  <p><strong>The order is your data, not a view of it.</strong> A move reorders the array you
2154
2154
  gave the grid and tells you it happened; writing it somewhere permanent is yours, because
2155
2155
  only you know where the order lives. A grid that rearranged rows on screen and stopped there
2156
- would look finished and lose the order on the next load — which is worse than not offering
2156
+ would look finished and lose the order on the next load, which is worse than not offering
2157
2157
  the feature.</p>
2158
2158
  <p>If the save fails, move it back: <code>grid.rows.move(key, from)</code>.</p>
2159
2159
  </div>
@@ -2161,7 +2161,7 @@ grid.on('row:moved', ({ key, from, to }) =&gt; {
2161
2161
  <p class="lead-in">
2162
2162
  <strong>It refuses while a sort, filter or grouping is active</strong>, and says why out loud
2163
2163
  rather than springing the row back in silence. The reason is that dropping between two
2164
- visible rows says nothing about where the row belongs in the underlying array — under a
2164
+ visible rows says nothing about where the row belongs in the underlying array: under a
2165
2165
  filter there may be hidden rows between them, and under a sort the displayed order is
2166
2166
  something the grid computed rather than something the data says. Rather than pick an
2167
2167
  interpretation and put the row somewhere you did not ask for, the move is declined.
@@ -2171,7 +2171,7 @@ grid.on('row:moved', ({ key, from, to }) =&gt; {
2171
2171
 
2172
2172
  <h2 id="row-transfer">Moving rows between grids</h2>
2173
2173
  <p class="lead-in">
2174
- A row can be dragged out of one grid and into another — a picker beside a basket, an inbox
2174
+ A row can be dragged out of one grid and into another, a picker beside a basket, an inbox
2175
2175
  beside a queue, an available list beside an assigned one.
2176
2176
  </p>
2177
2177
 
@@ -2210,10 +2210,10 @@ createGrid(right, { columns, rows,
2210
2210
  <p><strong>Off by default, and both ends have to agree.</strong> Rows leaving a grid is a data
2211
2211
  change you have to want: a grid that quietly let its rows be dragged away would lose one to a
2212
2212
  mis-drag, and there is no gesture a user would think to try to get it back. A one-way
2213
- relationship is a declaration on both grids rather than a convention — the sender refuses to
2213
+ relationship is a declaration on both grids rather than a convention, the sender refuses to
2214
2214
  receive, and the receiver never starts a drag.</p>
2215
- <p><strong>The target adds before the source removes.</strong> If the add is refused — a
2216
- duplicate key, most likely — nothing is removed, so a rejected transfer loses no data. The
2215
+ <p><strong>The target adds before the source removes.</strong> If the add is refused: a
2216
+ duplicate key, most likely: nothing is removed, so a rejected transfer loses no data. The
2217
2217
  other order would delete a row and then discover it had nowhere to go.</p>
2218
2218
  <p><strong>The row object is cloned, not shared.</strong> Two grids holding the same object
2219
2219
  would edit each other's rows through it, which is the sort of coupling nobody goes looking for
@@ -2229,7 +2229,7 @@ createGrid(right, { columns, rows,
2229
2229
  <p class="lead-in">
2230
2230
  <strong>Picking up a row shows it, wherever the pointer goes.</strong> The row being dragged
2231
2231
  dims in its own grid, and a small label naming it follows the pointer for as long as the drag
2232
- is held — over the gap between two grids, over one that is about to refuse the drop, anywhere
2232
+ is held: over the gap between two grids, over one that is about to refuse the drop, anywhere
2233
2233
  the row's own dimming cannot reach. Both clear on release, and a handle press never also starts
2234
2234
  a range selection underneath it.
2235
2235
  </p>
@@ -2258,7 +2258,7 @@ createGrid(right, { columns, rows,
2258
2258
  <div class="why">
2259
2259
  <p><strong>Only tagged columns are ever hidden.</strong> That is the rule the whole feature
2260
2260
  turns on. A financial grid with sixty month columns also has an account name, a total and a
2261
- variance, and none of those belong to a year — if filtering hid them the view would be
2261
+ variance, and none of those belong to a year: if filtering hid them the view would be
2262
2262
  useless, and tagging every column merely to keep it visible would be busywork. So "show 2024"
2263
2263
  does not mean "hide everything else"; it means "hide tagged columns that are not 2024".</p>
2264
2264
  <p>The same rule runs the other way: an untagged column you hid yourself stays hidden, because
@@ -2266,7 +2266,7 @@ createGrid(right, { columns, rows,
2266
2266
  </div>
2267
2267
 
2268
2268
  <p class="lead-in">
2269
- A column can carry more than one tag, which gives you a second axis for free — tag each month
2269
+ A column can carry more than one tag, which gives you a second axis for free: tag each month
2270
2270
  with its year <em>and</em> its quarter, and a user can pick either. The dropdown lists tags in
2271
2271
  the order they were declared rather than alphabetically, since they are usually already in a
2272
2272
  meaningful sequence and sorting would put <code>Q10</code> before <code>Q2</code>.
@@ -2287,7 +2287,7 @@ createGrid(right, { columns, rows,
2287
2287
  <h3>Headings without the controls</h3>
2288
2288
  <p class="lead-in">
2289
2289
  A dense grid often wants the heading and nothing else. <code>showColumnFunctions: false</code>
2290
- leaves each heading as its label, with no sort, filter or menu control — they are not drawn
2290
+ leaves each heading as its label, with no sort, filter or menu control, they are not drawn
2291
2291
  rather than hidden, so the label has the whole cell. Sorting, filtering and the column menu
2292
2292
  stay reachable through the API, the keyboard and the tool panel; only the furniture goes. The
2293
2293
  resize grip stays, since dragging a column wider is a view adjustment rather than a function
@@ -2328,7 +2328,7 @@ createGrid(right, { columns, rows,
2328
2328
 
2329
2329
  <div class="why">
2330
2330
  <p><strong>What is not shared is the design.</strong> If sort, filters and selection travelled
2331
- too, this would not be a feature — it would be one grid with extra steps. The reason to have
2331
+ too, this would not be a feature, it would be one grid with extra steps. The reason to have
2332
2332
  two is that the sections hold different data, so each keeps its own view of it.</p>
2333
2333
  <p>Vertical scroll stays independent for the same reason: the grids hold different numbers of
2334
2334
  rows, and yoking them would make the shorter one run out.</p>
@@ -2336,7 +2336,7 @@ createGrid(right, { columns, rows,
2336
2336
 
2337
2337
  <p class="lead-in">
2338
2338
  A column one grid has and another does not is skipped rather than invented, and nothing checks
2339
- that the column sets match — aligning grids with different columns is a caller error that
2339
+ that the column sets match: aligning grids with different columns is a caller error that
2340
2340
  produces a visibly wrong result rather than a silent one. Destroying any grid releases its
2341
2341
  link and leaves the rest working.
2342
2342
  </p>
@@ -2349,8 +2349,8 @@ createGrid(right, { columns, rows,
2349
2349
 
2350
2350
  <div class="why">
2351
2351
  <p><strong>The failure this prevents is a confident wrong number.</strong> You cannot add
2352
- decibels — 90&nbsp;dB and 90&nbsp;dB make 93&nbsp;dB, not 180. The mean of a column of rates
2353
- is not the mean — a 100% conversion on two visits and a 1% conversion on ten thousand average
2352
+ decibels: 90&nbsp;dB and 90&nbsp;dB make 93&nbsp;dB, not 180. The mean of a column of rates
2353
+ is not the mean, a 100% conversion on two visits and a 1% conversion on ten thousand average
2354
2354
  to 1.02%, not 50.5%. Both mistakes produce a plausible figure rather than an error, and a
2355
2355
  footer nobody can check gets used. A missing total gets asked about; a wrong one does not.</p>
2356
2356
  </div>
@@ -2382,7 +2382,7 @@ createGrid(right, { columns, rows,
2382
2382
  <thead><tr><th>Type</th><th>What it does differently</th></tr></thead>
2383
2383
  <tbody>
2384
2384
  <tr><td class="name">decibel</td><td class="desc">Sums and averages in the linear domain and converts back. Power scale, factor 10.</td></tr>
2385
- <tr><td class="name">decibelAmplitude</td><td class="desc">The same, on the field scale — factor 20, for voltage and current.</td></tr>
2385
+ <tr><td class="name">decibelAmplitude</td><td class="desc">The same, on the field scale: factor 20, for voltage and current.</td></tr>
2386
2386
  <tr><td class="name">ratio</td><td class="desc">Averages by weight, using the column named in <code>typeOptions.weight</code>. Refuses <code>sum</code>, since two rates do not add to a rate.</td></tr>
2387
2387
  <tr><td class="name">percentRate</td><td class="desc">As <code>ratio</code>, displayed with a percent sign.</td></tr>
2388
2388
  </tbody>
@@ -2397,7 +2397,7 @@ createGrid(right, { columns, rows,
2397
2397
 
2398
2398
  <p class="lead-in">
2399
2399
  Without a weight column the average returns nothing rather than falling back to the
2400
- unweighted mean — falling back would be the exact mistake the type exists to prevent, arrived
2400
+ unweighted mean: falling back would be the exact mistake the type exists to prevent, arrived
2401
2401
  at silently. Rows with no rate, or no weight, are left out rather than counted as zero.
2402
2402
  </p>
2403
2403
 
@@ -2418,7 +2418,7 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
2418
2418
  often not in the page at all: the grid renders a window of rows, and a heading five hundred
2419
2419
  rows above the viewport was recycled long ago. So the pinned heading is synthesised from
2420
2420
  whichever group the top visible row belongs to, which the grid answers by binary search over
2421
- an index it builds while flattening the rows — the ten-thousandth row of a group costs what
2421
+ an index it builds while flattening the rows, the ten-thousandth row of a group costs what
2422
2422
  the second one does.</p>
2423
2423
  <p>The cap exists because each heading costs a row of viewport. A five-level grouping without
2424
2424
  one would spend a third of the screen describing what the other two thirds contain.</p>
@@ -2432,13 +2432,13 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
2432
2432
 
2433
2433
  <p class="lead-in">
2434
2434
  The same answer is available directly as <code>grid.rows.groupHeadings(index)</code>, which
2435
- returns the enclosing group rows outermost first — for a breadcrumb, or a heading elsewhere on
2435
+ returns the enclosing group rows outermost first, for a breadcrumb, or a heading elsewhere on
2436
2436
  your page.
2437
2437
  </p>
2438
2438
 
2439
2439
  <h2 id="sources">Working with large data</h2>
2440
2440
  <p class="lead-in">
2441
- A million rows in memory is fine — that is what the columnar store is for. Beyond that, or
2441
+ A million rows in memory is fine: that is what the columnar store is for. Beyond that, or
2442
2442
  when the data lives behind an API, a source takes over.
2443
2443
  </p>
2444
2444
  <div class="table-wrap">
@@ -2448,7 +2448,7 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
2448
2448
  <tr><td class="name">memory</td><td class="desc">The default. Everything is present; the grid does all the work.</td></tr>
2449
2449
  <tr><td class="name">paged</td><td class="desc">A page at a time from a server that paginates.</td></tr>
2450
2450
  <tr><td class="name">remote</td><td class="desc">Blocks fetched on demand as the user scrolls, with sort and filter pushed to the server.</td></tr>
2451
- <tr><td class="name">stream</td><td class="desc">Rows arriving over time — a query that streams, a socket. Promotes to memory once complete.</td></tr>
2451
+ <tr><td class="name">stream</td><td class="desc">Rows arriving over time, a query that streams, a socket. Promotes to memory once complete.</td></tr>
2452
2452
  </tbody>
2453
2453
  </table>
2454
2454
  </div>
@@ -2477,7 +2477,7 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
2477
2477
  viewport reaches them and cached; changing the sort or the filter invalidates the cache and
2478
2478
  re-queries.</p>
2479
2479
  <p>The <code>filters</code> your callback receives is the same condition tree documented
2480
- above. You are not handed an opaque object to reverse-engineer — it is the published format,
2480
+ above. You are not handed an opaque object to reverse-engineer, it is the published format,
2481
2481
  and the same shape you would have written by hand.</p>
2482
2482
  </div>
2483
2483
 
@@ -2524,7 +2524,7 @@ columns: [
2524
2524
  <code>gridLines</code> chooses which rules are drawn between cells. <code>'horizontal'</code>
2525
2525
  is the default and is what the grid has always drawn; vertical rules between body cells are
2526
2526
  additive, so the default is unchanged and nothing moves on upgrade. <code>'rows'</code> and
2527
- <code>'columns'</code> are accepted as aliases. Only the rules between data are affected — the
2527
+ <code>'columns'</code> are accepted as aliases. Only the rules between data are affected: the
2528
2528
  header underline and the seams beside pinned columns are structure rather than decoration, and
2529
2529
  removing them would make the pinned regions look detached.
2530
2530
  </p>
@@ -2539,7 +2539,7 @@ columns: [
2539
2539
  <h2 id="cards">Cards, lists and feeds</h2>
2540
2540
  <p class="lead-in">
2541
2541
  <code>rowTemplate</code> draws each row with a layout of your own instead of dividing it into
2542
- columns. A card list, a feed, a search-result list, a message list — any presentation where a
2542
+ columns. A card list, a feed, a search-result list, a message list: any presentation where a
2543
2543
  record is a small piece of layout rather than a line of cells.
2544
2544
  </p>
2545
2545
 
@@ -2559,28 +2559,28 @@ columns: [
2559
2559
  <p><strong>The template compiles; it does not call back.</strong> There is deliberately no
2560
2560
  "here is a container, build what you like for this row" hook. That shape is easy to offer and
2561
2561
  would be used to allocate DOM per row, and at that moment the virtualisation stops paying for
2562
- itself — quietly, and in a way nobody can attribute to a change. A row template is the same
2562
+ itself: quietly, and in a way nobody can attribute to a change. A row template is the same
2563
2563
  declarative string a cell template is: parsed once, built into real DOM the first time an
2564
2564
  element is used, and afterwards updated by writing text into the few nodes the bindings own.
2565
2565
  Scrolling ten thousand records through a hundred pooled cards allocates nothing.</p>
2566
2566
  <p><strong>Everything underneath is unchanged.</strong> Sorting, filtering, grouping,
2567
2567
  selection, permissions, redaction, saved views, undo, export and the remote source all apply
2568
- exactly as they do to a table — only the drawing changes. That is the reason to build a card
2568
+ exactly as they do to a table: only the drawing changes. That is the reason to build a card
2569
2569
  view on a grid rather than beside one.</p>
2570
2570
  </div>
2571
2571
 
2572
2572
  <p class="lead-in">
2573
2573
  <strong>A card is still a row.</strong> It carries the same row identity a table row does, so
2574
2574
  <code>row:clicked</code> and <code>row:dblclicked</code> fire with the same payload, clicking
2575
- selects, the context menu opens, and <code>rowReorder</code> works — with the card itself as
2575
+ selects, the context menu opens, and <code>rowReorder</code> works, with the card itself as
2576
2576
  the drag handle, since there is no cell to put a grip in. None of that is a second
2577
2577
  implementation; it is the same code that serves a table.
2578
2578
  </p>
2579
2579
 
2580
2580
  <div class="why">
2581
2581
  <p><strong>It is announced as a list, not a grid.</strong> A card has no columns, so the
2582
- <code>grid</code> role — which promises columns, <code>gridcell</code> children and a
2583
- two-dimensional keyboard model — would misdescribe it completely. The layer is a
2582
+ <code>grid</code> role, which promises columns, <code>gridcell</code> children and a
2583
+ two-dimensional keyboard model: would misdescribe it completely. The layer is a
2584
2584
  <code>list</code>, each card a <code>listitem</code> carrying its position and the size of the
2585
2585
  whole set, and the column header is not drawn. <code>role</code> and <code>itemRole</code>
2586
2586
  override both, for a presentation that is really a <code>listbox</code>.</p>
@@ -2612,7 +2612,7 @@ columns: [
2612
2612
  filling a small tablet is not. The grid already watches its own element for size changes, so
2613
2613
  the same observer answers this.</p>
2614
2614
  <p><strong>The state a user built survives the switch.</strong> Rotating a phone must not lose
2615
- the sort, the filters, the selection or the scroll position, and it does not — it is one grid
2615
+ the sort, the filters, the selection or the scroll position, and it does not, it is one grid
2616
2616
  throughout, and only the drawing changes. An open cell editor is closed, since the cell it
2617
2617
  belonged to stops existing.</p>
2618
2618
  </div>
@@ -2621,7 +2621,7 @@ columns: [
2621
2621
  <strong>Sorting and filtering need a home</strong> when there are no column headings to click,
2622
2622
  and the tool panel is it: set <code>toolPanel: true</code> and its rail stays available in card
2623
2623
  presentation with the columns and filter panels behind it. <strong>Export is unaffected</strong>
2624
- — the columns are still the data model, so a CSV or an Excel file from a collapsed grid holds
2624
+ , the columns are still the data model, so a CSV or an Excel file from a collapsed grid holds
2625
2625
  every column, including ones the card does not show.
2626
2626
  </p>
2627
2627
 
@@ -2634,7 +2634,7 @@ columns: [
2634
2634
 
2635
2635
  <h3>Showing what the grid shows</h3>
2636
2636
  <p class="lead-in">
2637
- <code>{{cell.<em>column</em>}}</code> is the text the table puts in that cell — the column's
2637
+ <code>{{cell.<em>column</em>}}</code> is the text the table puts in that cell, the column's
2638
2638
  own formatter, data type, number and date settings and lookup label, all of it.
2639
2639
  <code>{{data.<em>field</em>}}</code> is the raw value underneath.
2640
2640
  </p>
@@ -2643,10 +2643,10 @@ columns: [
2643
2643
  <table>
2644
2644
  <thead><tr><th>Binding</th><th>Reads</th></tr></thead>
2645
2645
  <tbody>
2646
- <tr><td class="sig">{{cell.value}}</td><td class="desc">£1,250.50 — the cell's rendered text</td></tr>
2647
- <tr><td class="sig">{{data.value}}</td><td class="desc">1250.5 — the stored number</td></tr>
2648
- <tr><td class="sig">{{cell.stage}}</td><td class="desc">Held — a lookup's label</td></tr>
2649
- <tr><td class="sig">{{data.stage}}</td><td class="desc">2 — the lookup's id</td></tr>
2646
+ <tr><td class="sig">{{cell.value}}</td><td class="desc">£1,250.50, the cell's rendered text</td></tr>
2647
+ <tr><td class="sig">{{data.value}}</td><td class="desc">1250.5, the stored number</td></tr>
2648
+ <tr><td class="sig">{{cell.stage}}</td><td class="desc">Held, a lookup's label</td></tr>
2649
+ <tr><td class="sig">{{data.stage}}</td><td class="desc">2, the lookup's id</td></tr>
2650
2650
  </tbody>
2651
2651
  </table>
2652
2652
  </div>
@@ -2654,7 +2654,7 @@ columns: [
2654
2654
  <p class="lead-in">
2655
2655
  Both are wanted, which is why both exist: a card showing a value to a person wants
2656
2656
  <code>cell</code>, and a template comparing or calculating wants <code>data</code>. A lookup is
2657
- the case that decides it — a card showing <code>2</code> where the table shows
2657
+ the case that decides it, a card showing <code>2</code> where the table shows
2658
2658
  <code>Held</code> is not a formatting preference but a plain bug.
2659
2659
  </p>
2660
2660
 
@@ -2662,7 +2662,7 @@ columns: [
2662
2662
  <p><strong>A protected column cannot be read raw.</strong> Binding
2663
2663
  <code>{{data.password}}</code> on a secret or redacted column would print the value the column
2664
2664
  exists to hide, while the table beside it shows dots. Such a binding reads the masked text
2665
- instead and says once that it did — a card must not become the hole a redaction closes.</p>
2665
+ instead and says once that it did, a card must not become the hole a redaction closes.</p>
2666
2666
  </div>
2667
2667
 
2668
2668
  <h3>Several cards on a line</h3>
@@ -2681,7 +2681,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
2681
2681
  <p class="lead-in">
2682
2682
  <code>cardsPerRow</code> is a count, for a layout that must not reflow. <code>maxCardWidth</code>
2683
2683
  is a ceiling: the grid fits as many whole cards as it can without exceeding it, and they share
2684
- the remaining space rather than leaving a ragged margin — so 900px at a 200px ceiling is four
2684
+ the remaining space rather than leaving a ragged margin, so 900px at a 200px ceiling is four
2685
2685
  cards of 225px, and the count changes with the container. <code>gap</code> sets the space
2686
2686
  between them. Where both are given, <code>cardsPerRow</code> wins, being an instruction rather
2687
2687
  than a preference.
@@ -2690,14 +2690,14 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
2690
2690
  <div class="why">
2691
2691
  <p><strong>The scroll height counts lines, not records.</strong> Four records on a line means
2692
2692
  the content is a quarter as tall as the row model alone would make it, and a scrollbar sized
2693
- per record would be four times too long — the last several screens empty. The tiled layout
2693
+ per record would be four times too long, the last several screens empty. The tiled layout
2694
2694
  works out its own window from the scroll position for the same reason: the window it would
2695
2695
  otherwise be handed counts one record per line and would leave the bottom of the screen bare.
2696
2696
  Pooling is unaffected; scrolling a tiled gallery reuses its elements exactly as a list does.</p>
2697
2697
  </div>
2698
2698
 
2699
2699
  <p class="lead-in">
2700
- Tiles are a fixed height, taken from <code>rowHeight</code> —
2700
+ Tiles are a fixed height, taken from <code>rowHeight</code> ,
2701
2701
  <code>rowHeight: 'auto'</code> measures a rendered row and cannot describe a line holding
2702
2702
  several of different heights. Variable-height tiles flowing into the shortest column is a
2703
2703
  masonry layout, which is a different thing and is not offered.
@@ -2705,14 +2705,14 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
2705
2705
 
2706
2706
  <p class="lead-in">
2707
2707
  Row heights work as they do everywhere else, including <code>rowHeight: 'auto'</code>, which
2708
- measures the rendered card — content-driven card heights need no extra configuration. The
2708
+ measures the rendered card: content-driven card heights need no extra configuration. The
2709
2709
  columns are still declared and still hold the data: they are what sorting, filtering and
2710
2710
  export operate on, and what the bindings read.
2711
2711
  </p>
2712
2712
 
2713
2713
  <h2 id="row-form">Editing a row on a form</h2>
2714
2714
  <p class="lead-in">
2715
- Double-clicking a row opens it in a panel — a right-hand drawer or a centred dialog — with
2715
+ Double-clicking a row opens it in a panel (a right-hand drawer or a centred dialog) with
2716
2716
  one control per field, a Save and a Cancel. It is the shape almost every application built on
2717
2717
  a grid ends up wanting, and until now the shape they had to build themselves.
2718
2718
  </p>
@@ -2729,7 +2729,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
2729
2729
  <p class="lead-in">
2730
2730
  That is the whole of it for the common case: the form is the row, edited with the same
2731
2731
  editors, types, formats and lookups the cells use. Where the record has more to it than the
2732
- grid shows, give a <code>load</code> function — and then say which fields and in what order,
2732
+ grid shows, give a <code>load</code> function, and then say which fields and in what order,
2733
2733
  because nothing in the grid knows the shape of something it has never seen.
2734
2734
  </p>
2735
2735
 
@@ -2750,21 +2750,21 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
2750
2750
 
2751
2751
  <h3>Which editor a field gets</h3>
2752
2752
  <p class="lead-in">
2753
- Every editor is available on a form, including your own from the module registry — the form
2753
+ Every editor is available on a form, including your own from the module registry, the form
2754
2754
  builds its controls through the same call a cell does, so a field gets the same editor, type,
2755
2755
  formatting and lookup its column would have given it. A field named after a column borrows
2756
2756
  that column outright and needs nothing further.
2757
2757
  </p>
2758
2758
  <p class="lead-in">
2759
- A field the grid has never seen — or one you want entered differently from its cell — says so
2759
+ A field the grid has never seen (or one you want entered differently from its cell) says so
2760
2760
  on the field itself. <code>editor</code> names it, and <code>type</code>, <code>props</code>
2761
2761
  and <code>lookup</code> configure it exactly as they would on a column. Overriding the control
2762
2762
  does not change where the value goes: a field still writes back only if it maps to a column.
2763
2763
  </p>
2764
2764
 
2765
2765
  <div class="why">
2766
- <p><strong>A picker opens when it is asked to.</strong> A popup editor — a date, a dropdown, a
2767
- tree, a colour, a code panel — <em>is</em> its panel: in a cell it opens the moment the cell
2766
+ <p><strong>A picker opens when it is asked to.</strong> A popup editor, a date, a dropdown, a
2767
+ tree, a colour, a code panel: <em>is</em> its panel: in a cell it opens the moment the cell
2768
2768
  does, which is right, because the user has just asked to edit that one cell. A form builds
2769
2769
  every field at once, so on a form the field shows the current value on a control and the panel
2770
2770
  opens over it when clicked. Choosing puts the panel away again and updates the control.</p>
@@ -2780,7 +2780,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
2780
2780
  <p><strong>And a load that never answers is a failure too.</strong> A promise that neither
2781
2781
  resolves nor rejects is what a dropped request looks like from the page; left alone it spins
2782
2782
  until the user gives up, which reads as an application that has hung rather than a request
2783
- that failed. After <code>timeout</code> milliseconds — two seconds unless you say otherwise —
2783
+ that failed. After <code>timeout</code> milliseconds, two seconds unless you say otherwise ,
2784
2784
  the form stops waiting and shows the same message and retry as any other failure. Set
2785
2785
  <code>timeout: false</code> to wait indefinitely, which is right only where your own loader
2786
2786
  already has a limit and would rather report that one. A record that turns up after the form
@@ -2791,7 +2791,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
2791
2791
  <p class="lead-in">
2792
2792
  The fields scroll and the heading and buttons do not, so Save stays reachable on a record with
2793
2793
  forty fields. If a validator refuses one of them, the form stays open, the field is marked, and
2794
- it is scrolled into view and focused — on a long form the offending field is otherwise nowhere
2794
+ it is scrolled into view and focused: on a long form the offending field is otherwise nowhere
2795
2795
  near the button that was just pressed.
2796
2796
  </p>
2797
2797
 
@@ -2821,7 +2821,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
2821
2821
 
2822
2822
  <div class="why">
2823
2823
  <p><strong>The form takes the double click.</strong> On an editable grid that gesture also
2824
- opens a cell editor, and the two cannot both own it — a form that quietly did nothing where a
2824
+ opens a cell editor, and the two cannot both own it, a form that quietly did nothing where a
2825
2825
  cell happened to be editable would be worse than no form. So where <code>rowForm</code> is
2826
2826
  configured, double-clicking a row opens the form and the cell editor stays reachable by
2827
2827
  Enter or by typing into the cell. Set <code>trigger: false</code> to leave opening entirely to
@@ -2831,7 +2831,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
2831
2831
  <h3>Putting the form in your own element</h3>
2832
2832
  <p class="lead-in">
2833
2833
  A drawer and a dialog both sit over the grid. Give <code>container</code> an element of your
2834
- own and the form is built there instead — a sidebar beside the grid, a panel below it, a
2834
+ own and the form is built there instead, a sidebar beside the grid, a panel below it, a
2835
2835
  column in a layout you already have. It fills what it is given, so the size and position are
2836
2836
  yours.
2837
2837
  </p>
@@ -2847,7 +2847,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
2847
2847
  <p class="lead-in">
2848
2848
  A selector is resolved when the form <em>opens</em>, not when the grid is configured, because
2849
2849
  a grid is routinely built before the layout around it exists. A container that cannot be found
2850
- falls back to opening over the grid — better a form in the wrong place than a double-click
2850
+ falls back to opening over the grid: better a form in the wrong place than a double-click
2851
2851
  that appears to do nothing.
2852
2852
  </p>
2853
2853
 
@@ -2869,18 +2869,18 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
2869
2869
  <p class="lead-in">
2870
2870
  The grid has always written optimistically without calling it that: an edit lands in the
2871
2871
  model and is painted before anything else happens. What <code>edit.commit</code> adds is
2872
- <em>durability</em> — whether the write reached your server, and what to put back when it
2872
+ <em>durability</em>: whether the write reached your server, and what to put back when it
2873
2873
  did not.
2874
2874
  </p>
2875
2875
  <div class="why">
2876
2876
  <p><strong>Nothing changes unless you ask for it.</strong> With no <code>commit</code> hook
2877
2877
  the grid behaves exactly as before: the value is written, history is recorded,
2878
2878
  <code>cell:changed</code> fires, and there is no pending state to think about. Subscribe to
2879
- <code>cell:changed</code>, fire your request and ignore the result — that keeps working and
2879
+ <code>cell:changed</code>, fire your request and ignore the result: that keeps working and
2880
2880
  costs nothing.</p>
2881
2881
  </div>
2882
2882
  <div class="example">
2883
- <p class="example__label">The usual case — the promise is the answer</p>
2883
+ <p class="example__label">The usual case, the promise is the answer</p>
2884
2884
  <pre><code>edit: {
2885
2885
  enabled: true,
2886
2886
  commit: async ({ key, colId, value }) =&gt; {
@@ -2894,7 +2894,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
2894
2894
  </div>
2895
2895
  <p class="lead-in">
2896
2896
  Resolving confirms the write; throwing rolls it back and fires <code>cell:reverted</code>
2897
- with your error message as <code>reason</code>. A synchronous hook works too — returning
2897
+ with your error message as <code>reason</code>. A synchronous hook works too: returning
2898
2898
  normally confirms, throwing reverts.
2899
2899
  </p>
2900
2900
  <div class="example">
@@ -2918,7 +2918,7 @@ socket.onmessage = (m) =&gt; {
2918
2918
  promise to resolve. <code>confirm: 'manual'</code> says so explicitly. The grid does not infer
2919
2919
  it from what <code>commit</code> returns, because then a synchronous hook that happens to
2920
2920
  return nothing would leave every cell pending for ever with nothing in your code that looks
2921
- wrong. If a write does stay pending, you get a console warning naming the cell — tune the
2921
+ wrong. If a write does stay pending, you get a console warning naming the cell: tune the
2922
2922
  threshold with <code>pendingTimeout</code>.</p>
2923
2923
  </div>
2924
2924
 
@@ -2940,7 +2940,7 @@ socket.onmessage = (m) =&gt; {
2940
2940
  the part that is easy to get wrong by hand. Suppose a cell holding <code>1</code> is edited to
2941
2941
  <code>2</code>, then to <code>3</code>, then to <code>4</code>, all before any answer comes
2942
2942
  back. If the second write fails, restoring “the value before it” would put back
2943
- <code>2</code> — a value the server never held, and one the user has since replaced twice.
2943
+ <code>2</code>, a value the server never held, and one the user has since replaced twice.
2944
2944
  So each cell remembers the newest value a confirmation has actually vouched for, and a write
2945
2945
  that a later edit has superseded reports its failure without writing anything back. You will
2946
2946
  see <code>cell:reverted</code> with <code>applied: false</code> for those.</p>
@@ -2951,8 +2951,8 @@ socket.onmessage = (m) =&gt; {
2951
2951
  <div class="why">
2952
2952
  <p><strong>Two behaviours worth knowing.</strong> An unconfirmed edit goes through the normal
2953
2953
  pipeline, so if it changes a sorted or filtered column the row moves immediately and moves
2954
- back if the write fails. And undo of an in-flight edit issues a <em>compensating write</em> —
2955
- a fresh write back to the previous value, itself tracked — rather than pretending to cancel a
2954
+ back if the write fails. And undo of an in-flight edit issues a <em>compensating write</em> ,
2955
+ a fresh write back to the previous value, itself tracked: rather than pretending to cancel a
2956
2956
  request that has already gone out.</p>
2957
2957
  </div>
2958
2958
 
@@ -2987,28 +2987,28 @@ grid.edit.status('r1', 'cap'); <span class="cmt">// 'pending' | null</span></
2987
2987
  </div>
2988
2988
  <p class="lead-in">
2989
2989
  <strong>Parent-reference</strong> is what a join or a document store produces. Every node is
2990
- a real row. A row whose parent is not in the data — filtered away, not loaded, or simply
2991
- wrong — is an orphan: it goes to the root by default, or into a named bucket. It is never
2990
+ a real row. A row whose parent is not in the data (filtered away, not loaded, or simply
2991
+ wrong) is an orphan: it goes to the root by default, or into a named bucket. It is never
2992
2992
  dropped, because hiding a record over a bad reference loses data the user can see in a flat
2993
2993
  view.
2994
2994
  </p>
2995
2995
  <p class="lead-in">
2996
2996
  <strong>Path-based</strong> rows describe their own place, so intermediate levels may have no
2997
- row at all — <code>EMEA/UK/Colchester</code> with no <code>EMEA/UK</code> row still needs a
2997
+ row at all: <code>EMEA/UK/Colchester</code> with no <code>EMEA/UK</code> row still needs a
2998
2998
  <code>UK</code> node to sit under. Those are synthesised, and render as group rows: a heading
2999
2999
  over the rows beneath it with no record of its own. A real row arriving later for a level
3000
3000
  already synthesised fills that node rather than appearing beside it.
3001
3001
  </p>
3002
3002
  <p class="lead-in">
3003
3003
  A parent cycle is not a hierarchy and cannot be walked. It is reported once and cut, with the
3004
- rows shown at the root — wrong place beats vanished.
3004
+ rows shown at the root: wrong place beats vanished.
3005
3005
  </p>
3006
3006
  <p class="lead-in">
3007
3007
  The grid generates a <strong>tree column</strong> to carry the expander and the indent, on the
3008
3008
  same terms as the auto-group, selection and detail columns: pinned to the start, and absent
3009
3009
  from <code>columns.visible()</code>, saved views, exports and the tool panel. Its text comes
3010
3010
  from <code>tree.label</code>; without one it falls back to your first visible column, which
3011
- then appears twice until you hide it — the grid does not remove a column you did not ask it
3011
+ then appears twice until you hide it, the grid does not remove a column you did not ask it
3012
3012
  to remove.
3013
3013
  </p>
3014
3014
  <h3 id="tree-lazy">Loading a branch on demand</h3>
@@ -3022,7 +3022,7 @@ grid.edit.status('r1', 'cap'); <span class="cmt">// 'pending' | null</span></
3022
3022
  </div>
3023
3023
  <p class="lead-in">
3024
3024
  <code>hasChildren</code> lets a row declare children it does not hold, so the expander is
3025
- there before anything is fetched — without it there is nothing to click and the branch can
3025
+ there before anything is fetched: without it there is nothing to click and the branch can
3026
3026
  never load. Such a node reads as <em>closed</em> even though tree nodes are otherwise expanded
3027
3027
  by default, because an open branch with nothing under it leaves no gesture to load it.
3028
3028
  </p>
@@ -3046,14 +3046,14 @@ grid.edit.status('r1', 'cap'); <span class="cmt">// 'pending' | null</span></
3046
3046
 
3047
3047
  <h2 id="master-detail">Master-detail</h2>
3048
3048
  <p class="lead-in">
3049
- A master row expands to reveal a detail region — by default a nested grid over whatever
3049
+ A master row expands to reveal a detail region: by default a nested grid over whatever
3050
3050
  <code>detail.rows(row)</code> returns, which may be a promise. The grid adds an expander
3051
3051
  column while the feature is on, on the same terms as the auto-group and selection columns:
3052
3052
  pinned to the start, and absent from <code>columns.visible()</code>, saved views, exports and
3053
3053
  the tool panel.
3054
3054
  </p>
3055
3055
  <div class="example">
3056
- <p class="example__label">Inline — a detail row beneath its master</p>
3056
+ <p class="example__label">Inline, a detail row beneath its master</p>
3057
3057
  <pre><code>detail: {
3058
3058
  rows: (row) =&gt; api.lines(row.data.id), <span class="cmt">// array or promise</span>
3059
3059
  config: { columns: [{ field: 'port' }, { field: 'vlan' }] },
@@ -3074,7 +3074,7 @@ grid.detail.closeAll();</code></pre>
3074
3074
 
3075
3075
  <h3 id="detail-target">A detail pane instead of a detail row</h3>
3076
3076
  <div class="example">
3077
- <p class="example__label">Targeted — the list-and-pane layout</p>
3077
+ <p class="example__label">Targeted, the list-and-pane layout</p>
3078
3078
  <pre><code>detail: {
3079
3079
  target: '#detail-pane', <span class="cmt">// a selector or an element</span>
3080
3080
  rows: (row) =&gt; api.lines(row.data.id),
@@ -3097,12 +3097,12 @@ grid.detail.placement(); <span class="cmt">// 'inline' | 'targe
3097
3097
  </p>
3098
3098
  <p class="lead-in">
3099
3099
  A <code>target</code> selector that matches no element is reported once and leaves the
3100
- details unshown — silence there is indistinguishable from a detail that fails to open, and
3100
+ details unshown: silence there is indistinguishable from a detail that fails to open, and
3101
3101
  the cause is not visible from the grid.
3102
3102
  </p>
3103
3103
  <p class="lead-in">
3104
3104
  <strong>The control changes with the placement, because the gesture does.</strong> Inline it
3105
- is a chevron that turns down when the row expands, carrying <code>aria-expanded</code> — the
3105
+ is a chevron that turns down when the row expands, carrying <code>aria-expanded</code>: the
3106
3106
  ordinary disclosure pattern. Targeted, nothing expands: the row is being chosen and its
3107
3107
  detail appears elsewhere, so the control becomes the &ldquo;opens elsewhere&rdquo; glyph and
3108
3108
  a toggle (<code>aria-pressed</code>) rather than a disclosure. A chevron there would promise
@@ -3113,7 +3113,7 @@ grid.detail.placement(); <span class="cmt">// 'inline' | 'targe
3113
3113
 
3114
3114
  <h3 id="detail-editing">An editable detail</h3>
3115
3115
  <p class="lead-in">
3116
- The detail is a whole grid, so it edits like one — put <code>edit</code> in
3116
+ The detail is a whole grid, so it edits like one: put <code>edit</code> in
3117
3117
  <code>detail.config</code> and its cells are editable. The rows it shows are usually a
3118
3118
  sub-array of the master's own record, so an edit there changes the master's data directly;
3119
3119
  there is nothing to copy back.
@@ -3129,16 +3129,16 @@ grid.detail.placement(); <span class="cmt">// 'inline' | 'targe
3129
3129
  }
3130
3130
 
3131
3131
  grid.on('detail:cell:changed', (e) =&gt; {
3132
- e.masterKey; <span class="cmt">// 'C1' — the row the detail belongs to</span>
3133
- e.path; <span class="cmt">// 'ports.1.vlan' — where it lands on the master's record</span>
3132
+ e.masterKey; <span class="cmt">// 'C1' , the row the detail belongs to</span>
3133
+ e.path; <span class="cmt">// 'ports.1.vlan': where it lands on the master's record</span>
3134
3134
  e.value; <span class="cmt">// 999</span>
3135
3135
  e.oldValue; <span class="cmt">// 101</span>
3136
3136
  });</code></pre>
3137
3137
  </div>
3138
3138
  <p class="lead-in">
3139
3139
  A nested grid is created by the grid, not by you, so its own events would otherwise be out of
3140
- reach. The edit lifecycle — <code>detail:edit:started</code>,
3141
- <code>detail:edit:stopped</code>, <code>detail:cell:changed</code> — is re-emitted on the
3140
+ reach. The edit lifecycle (<code>detail:edit:started</code>,
3141
+ <code>detail:edit:stopped</code>, <code>detail:cell:changed</code>) is re-emitted on the
3142
3142
  master, tagged with the master it came from. You never have to hold the nested grid to hear
3143
3143
  about an edit inside it.
3144
3144
  </p>
@@ -3147,7 +3147,7 @@ grid.on('detail:cell:changed', (e) =&gt; {
3147
3147
  a host can persist a detail edit against the master and never think about the nested grid at
3148
3148
  all. It is worked out by identity: <code>rows(row)</code> usually returns an array that is
3149
3149
  already a property of the record, and that property is the prefix. A detail fetched from a
3150
- server is not part of the master's record, so its <code>path</code> is <code>null</code> —
3150
+ server is not part of the master's record, so its <code>path</code> is <code>null</code> ,
3151
3151
  set <code>detail.path</code> to name it yourself when you want one anyway.
3152
3152
  </p>
3153
3153
  <p class="lead-in">
@@ -3173,33 +3173,33 @@ grid.on('detail:cell:changed', (e) =&gt; {
3173
3173
 
3174
3174
  <h2 id="selection-guide">Selection and ranges</h2>
3175
3175
  <p class="lead-in">
3176
- Row selection and cell ranges are separate answers to separate questions —
3176
+ Row selection and cell ranges are separate answers to separate questions ,
3177
3177
  <em>which records</em> versus <em>which values</em>. Dragging across cells does not tick row
3178
3178
  checkboxes, and selecting rows does not build a range.
3179
3179
  </p>
3180
3180
  <div class="note">
3181
3181
  <p><strong>Cell ranges are on by default; row selection is not.</strong> Set
3182
- <code>selection: 'single'</code> or <code>'multiple'</code> to turn rows on — until you do,
3182
+ <code>selection: 'single'</code> or <code>'multiple'</code> to turn rows on: until you do,
3183
3183
  <code>selection.set()</code> accepts the call and <code>keys()</code> comes back empty.</p>
3184
3184
  </div>
3185
3185
  <div class="example">
3186
3186
  <p class="example__label">Both</p>
3187
3187
  <pre><code>selection: 'multiple' <span class="cmt">// rows are off until you ask</span>
3188
3188
 
3189
- <span class="cmt">// Rows — which records</span>
3189
+ <span class="cmt">// Rows, which records</span>
3190
3190
  grid.selection.set(['r1', 'r2']);
3191
3191
  grid.selection.keys(); <span class="cmt">// the selected row keys</span>
3192
3192
  grid.selection.rows(); <span class="cmt">// the row wrappers</span>
3193
3193
  grid.selection.all(); <span class="cmt">// select everything that passes the filter</span>
3194
3194
  grid.selection.clear();
3195
3195
 
3196
- <span class="cmt">// Cells — which values</span>
3196
+ <span class="cmt">// Cells, which values</span>
3197
3197
  grid.selection.setRange({ startRow: 0, endRow: 9, columns: ['cap', 'margin'] });
3198
3198
  grid.selection.cells(); <span class="cmt">// [{ key, colId }, …]</span>
3199
3199
  grid.selection.summary(); <span class="cmt">// count, sum, min, max, avg over the range</span>
3200
3200
  grid.selection.clearRange();
3201
3201
 
3202
- <span class="cmt">// Several blocks at once — ctrl-click, Ctrl+Shift+Arrow, or from the API</span>
3202
+ <span class="cmt">// Several blocks at once: ctrl-click, Ctrl+Shift+Arrow, or from the API</span>
3203
3203
  grid.selection.addRange({ startRow: 20, endRow: 29, columns: ['cap'] });
3204
3204
  grid.selection.extendRange(34, 'cap'); <span class="cmt">// grows the block just added</span></code></pre>
3205
3205
  </div>
@@ -3213,7 +3213,7 @@ grid.selection.extendRange(34, 'cap'); <span class="cmt">// grows the block jus
3213
3213
  pinned so it does not scroll away. <code>headerCheckbox: true</code> puts a select-all box in
3214
3214
  its heading, which shows three states: unchecked when nothing is selected, checked when
3215
3215
  everything is, and the native indeterminate mark when some are. Clicking it selects
3216
- everything when it is not already full, and clears when it is — including from the
3216
+ everything when it is not already full, and clears when it is: including from the
3217
3217
  indeterminate state, where the intent is "select the rest".
3218
3218
  </p>
3219
3219
  <p class="lead-in">
@@ -3239,7 +3239,7 @@ grid.selection.extendRange(34, 'cap'); <span class="cmt">// grows the block jus
3239
3239
  <p><strong>What multiple ranges do and do not support.</strong> Painting, <code>cells()</code>
3240
3240
  and the status-bar summary all work over the union of the selected blocks. Copy is narrower,
3241
3241
  because tab-separated text is a rectangle: blocks stacked over the same columns, or joined
3242
- over the same rows, copy fine — a diagonal pair has no rectangular form, so
3242
+ over the same rows, copy fine, a diagonal pair has no rectangular form, so
3243
3243
  <code>rangeText()</code> returns <code>''</code> and <code>clipboard:copy</code> reports
3244
3244
  <code>reason: 'discontiguous'</code> rather than emitting misaligned rows. Filling is
3245
3245
  narrower still: with more than one block selected there is no single source to extend, so the
@@ -3269,12 +3269,12 @@ selection: { ranges: false } <span class="cmt">// rows only, no drag-selec
3269
3269
  <div class="why">
3270
3270
  <p><strong>Months step as months.</strong> 15 January to 15 February is thirty-one days, and
3271
3271
  continuing in days would land on 18 March and drift further every step. A month step holds
3272
- the date the user picked, and clamps where the month is short — 31 January plus a month is
3272
+ the date the user picked, and clamps where the month is short: 31 January plus a month is
3273
3273
  28 February, not 3 March.</p>
3274
3274
  <p><strong>Month ends are their own case.</strong> <code>31 Jan, 28 Feb</code> is a month
3275
3275
  series whose second value has already been clamped, so the two share no day of month and the
3276
3276
  day-preserving rule cannot see it. Where every source value is the last day of its own month,
3277
- the fill stays on the month end: 31 March, 30 April, 31 May — and 29 February in a leap year.
3277
+ the fill stays on the month end: 31 March, 30 April, 31 May, and 29 February in a leap year.
3278
3278
  Values that share a day of month keep the day-preserving answer, so <code>30 Apr, 30 Jun</code>
3279
3279
  still gives 30 August rather than the 31st.</p>
3280
3280
  <p><strong>One limit worth knowing.</strong> Filling upwards is not supported; the handle
@@ -3287,7 +3287,7 @@ selection: { ranges: false } <span class="cmt">// rows only, no drag-selec
3287
3287
  }</code></pre>
3288
3288
  </div>
3289
3289
  <div class="why">
3290
- <p>A domain series — order codes, fiscal periods, seat numbers — is not something the grid
3290
+ <p>A domain series (order codes, fiscal periods, seat numbers) is not something the grid
3291
3291
  can infer, so <code>selection.fill</code> takes precedence when you supply it. It must return
3292
3292
  one value per target row; anything else is ignored in favour of the built-in detection,
3293
3293
  rather than being partly applied.</p>
@@ -3296,7 +3296,7 @@ selection: { ranges: false } <span class="cmt">// rows only, no drag-selec
3296
3296
  <h2 id="updates-guide">Holding live updates</h2>
3297
3297
  <p class="lead-in">
3298
3298
  A pause button for incoming data. Changes are held and merged while paused, applied when
3299
- play is pressed, and counted throughout — so the coalescing that makes a live grid fast is
3299
+ play is pressed, and counted throughout, so the coalescing that makes a live grid fast is
3300
3300
  finally visible.
3301
3301
  </p>
3302
3302
  <div class="example">
@@ -3318,7 +3318,7 @@ grid.updates.log({ since: Date.now() - 60000 }); <span class="cmt">// what arr
3318
3318
  no invisible state to explain.</p>
3319
3319
  <p><strong>Merging continues while paused</strong>, so a long pause costs one entry per
3320
3320
  changed row rather than one per update. Forty updates to one row are one row of work when
3321
- play is pressed — and <code>coalesced</code> is the thirty-nine, which is the number nobody
3321
+ play is pressed, and <code>coalesced</code> is the thirty-nine, which is the number nobody
3322
3322
  could see before.</p>
3323
3323
  <p><strong>Bounding the rows themselves is separate.</strong> The log bounds change
3324
3324
  <em>history</em>; a streaming source also needs to bound row <em>retention</em>, or a grid
@@ -3327,8 +3327,8 @@ grid.updates.log({ since: Date.now() - 60000 }); <span class="cmt">// what arr
3327
3327
  many it let go through <code>evicted</code> on the progress report.</p>
3328
3328
  <p><strong>The log keeps the raw sequence, not the merged one.</strong> Merging is right for
3329
3329
  applying a backlog quickly and wrong for looking at what happened, because the intermediate
3330
- states are exactly what a time scrubber would move between. It survives the flush —
3331
- <code>pending</code> is what is waiting, the log is what happened — and it is capped, so a
3330
+ states are exactly what a time scrubber would move between. It survives the flush ,
3331
+ <code>pending</code> is what is waiting, the log is what happened, and it is capped, so a
3332
3332
  grid paused over lunch holds the recent past and reports how much it dropped rather than
3333
3333
  taking the tab with it.</p>
3334
3334
  </div>
@@ -3355,29 +3355,29 @@ expect(grid.diagnostics.renders().dom.cellWrites - before).toBeLessThan(200);</c
3355
3355
  reachable from the public API. Exposing what a system already knows is usually a better first
3356
3356
  move than measuring something new.</p>
3357
3357
  <p><strong>Warnings are mostly collection, not detection.</strong> The grid has 160 places
3358
- that warn once per cause, each already carrying a stable de-duplication key — which is
3358
+ that warn once per cause, each already carrying a stable de-duplication key, which is
3359
3359
  exactly the stable identifier a support conversation needs. They went to the console and
3360
3360
  nowhere else. The console interleaves with your own logging, does not survive a reload, and
3361
3361
  cannot be asked what it has already complained about. They are now kept as records too.</p>
3362
3362
  <p><strong>Every warning names values, not just a condition.</strong> "Something is slow" is a
3363
3363
  warning nobody can act on. Each carries the specific numbers, a stable id, and is dismissible
3364
- for the session but not permanently — a permanently dismissible warning is one nobody sees
3364
+ for the session but not permanently, a permanently dismissible warning is one nobody sees
3365
3365
  again after the person who dismissed it leaves.</p>
3366
3366
  <p><strong>The checks are tested for silence as much as for detection.</strong> A clean grid
3367
3367
  must raise nothing. A checker that cries wolf is one developers learn to ignore, and then it
3368
3368
  is worth less than no checker at all. The accessibility checks found two false positives in
3369
- themselves during development — first comparing <code>aria-rowcount</code> against the row
3369
+ themselves during development: first comparing <code>aria-rowcount</code> against the row
3370
3370
  count when ARIA counts header rows too, then counting header row *elements*, of which a
3371
3371
  single-level header has three because the header is built once per pinned region.</p>
3372
3372
  <p><strong>Instrumentation must not change what it measures.</strong> Counters are integers
3373
3373
  incremented where the work already happened. Render phases are four <code>performance.now()</code>
3374
- marks around existing sections. Timings are sampled — a bounded window of recent operations —
3374
+ marks around existing sections. Timings are sampled, a bounded window of recent operations ,
3375
3375
  and every report names which of its figures are sampled, because a number whose provenance is
3376
3376
  unclear is worse than no number. Paint wait is the browser's and is deliberately not claimed.</p>
3377
3377
  <p><strong>Render causes are captured, not inferred.</strong> By the time a paint runs,
3378
3378
  several distinct causes have collapsed into the same dirty flags, so working backwards gives a
3379
3379
  plausible answer rather than a true one. The renderer records the structural reason when the
3380
- invalidation arrives; the semantic one — filter, sort, data — is only knowable from the event
3380
+ invalidation arrives; the semantic one (filter, sort, data) is only knowable from the event
3381
3381
  that preceded it, so the DOM layer supplies that and the structural reason is the fallback.</p>
3382
3382
  <p><strong>Providers are wrapped where they are installed</strong>, not at each call site, so
3383
3383
  one added later is instrumented by construction. The wrapper returns exactly what the original
@@ -3396,7 +3396,7 @@ expect(grid.diagnostics.renders().dom.cellWrites - before).toBeLessThan(200);</c
3396
3396
  because it imports it. A devtools module that imported anything from core would put the whole grid inside a
3397
3397
  bundle whose entire promise is that deployments not using it pay nothing.</p>
3398
3398
  <p><strong>The support bundle carries no row data.</strong> Configuration, query state, timing,
3399
- warnings, provider statistics, version and environment — and nothing from your data. Stated as
3399
+ warnings, provider statistics, version and environment, and nothing from your data. Stated as
3400
3400
  a guarantee because a bundle that had to be inspected for confidential values before sending
3401
3401
  is a bundle that never gets attached to the ticket.</p>
3402
3402
  <p><strong>It observes and never mutates.</strong> A configuration editor in a debug panel is
@@ -3422,13 +3422,13 @@ const providerFor = (id) =&gt; ({
3422
3422
  <p><strong>Presence carries intent, never values.</strong> This is the line that matters most.
3423
3423
  A peer's committed edit reaches the grid as data, through the channel you already use for
3424
3424
  data. Presence is throttled, lossy and ephemeral <em>by design</em>, so a value carried on it
3425
- is a value that can be dropped &mdash; and that is the class of bug that appears once a month
3425
+ is a value that can be dropped, and that is the class of bug that appears once a month
3426
3426
  in production and cannot be reproduced on demand.</p>
3427
3427
  <p><strong>Positions are row keys, resolved against your view at render time.</strong> Peers
3428
3428
  sort and filter independently, so index 12 is a different record on every screen. Publishing
3429
3429
  an index would put a colleague's cursor on an unrelated row the moment either of you sorted.
3430
- The cost of this is real &mdash; the grid resolves a key to a position rather than reading one
3431
- &mdash; and it is the difference between the feature working and the feature lying.</p>
3430
+ The cost of this is real, the grid resolves a key to a position rather than reading one
3431
+ , and it is the difference between the feature working and the feature lying.</p>
3432
3432
  <p><strong>Idle is measured from when a message arrived, not from what it says.</strong>
3433
3433
  Clocks between clients disagree by seconds routinely and by minutes occasionally. Keying idle
3434
3434
  detection on the sender's timestamp means a peer with a fast clock never goes idle and one
@@ -3442,19 +3442,19 @@ const providerFor = (id) =&gt; ({
3442
3442
  roster filling in slowly.</p>
3443
3443
  <p><strong>A peer's cursor is dashed; your focus ring is solid.</strong> The distinction has to
3444
3444
  be in the kind of line, not only its colour. Colour alone fails for anyone who cannot separate
3445
- two hues and fails for everyone at a glance &mdash; and the palette originally contained the
3445
+ two hues and fails for everyone at a glance, and the palette originally contained the
3446
3446
  exact value of <code>--lattice-focus-color</code>, so the first peer assigned drew a cursor
3447
3447
  identical to the local user's own selection. An active edit is solid and tinted, because an
3448
3448
  edit is not a cursor and those two must not be confused with each other either.</p>
3449
3449
  <p><strong>Nothing is inserted into the grid.</strong> Every treatment is an attribute and a
3450
3450
  custom property written onto a cell that already exists, drawn with an outline and a
3451
3451
  pseudo-element. That is what keeps presence from shifting layout, covering an in-cell chart or
3452
- swallowing a click &mdash; none of which survives an implementation that appends overlay
3452
+ swallowing a click: none of which survives an implementation that appends overlay
3453
3453
  elements. The overlay layer is pointer-transparent and presence deliberately does not opt back
3454
3454
  in; only the roster does, because it is a control.</p>
3455
3455
  <p><strong>The roster is the part people use.</strong> More than the cursors, in practice. It
3456
3456
  carries the name as well as the colour, because colour alone is not a signal everyone can
3457
- read, and it reports peers whose rows are not in your view rather than omitting them &mdash;
3457
+ read, and it reports peers whose rows are not in your view rather than omitting them ,
3458
3458
  an absent peer reads as a disconnection that has not happened.</p>
3459
3459
  <p><strong>A parked cursor does not fade.</strong> The label does, after a couple of seconds,
3460
3460
  because permanent labels over a dense grid are unreadable. The border stays, dims at idle, and
@@ -3504,12 +3504,12 @@ const providerFor = (id) =&gt; ({
3504
3504
  <p><strong>Stable row identity is a hard requirement, enforced rather than documented.</strong>
3505
3505
  A comment is keyed on row identity plus field. Row index changes under sort, filter and
3506
3506
  grouping, so a comment keyed on it reattaches to whichever row later occupies that position
3507
- &mdash; and a comment on the wrong row is worse than no comment at all. A grid with no
3507
+ , and a comment on the wrong row is worse than no comment at all. A grid with no
3508
3508
  <code>rowKey</code> disables comments and names them in the same single warning as the other
3509
3509
  identity-dependent features.</p>
3510
3510
  <p><strong>Identity has to survive more than the session.</strong> Comments outlive the page
3511
- that wrote them, so a key that is stable only within one load &mdash; anything derived from
3512
- arrival order, for instance &mdash; is not enough. Reload the data in a different order and
3511
+ that wrote them, so a key that is stable only within one load (anything derived from
3512
+ arrival order, for instance) is not enough. Reload the data in a different order and
3513
3513
  every thread points somewhere else. If you have only used the grid client-side you may never
3514
3514
  have needed a durable identity before; you do now.</p>
3515
3515
  <p><strong>Writes are optimistic, and rejections are taken back.</strong> The author sees
@@ -3523,14 +3523,14 @@ const providerFor = (id) =&gt; ({
3523
3523
  console. Reject in the provider.</p>
3524
3524
  <p><strong>A body is user input that has round-tripped through your storage.</strong> That is
3525
3525
  the exact shape of a stored cross-site script, so the default path sets text and nothing else.
3526
- Turning on <code>markdown</code> buys emphasis, code and links &mdash; three constructs, built
3526
+ Turning on <code>markdown</code> buys emphasis, code and links: three constructs, built
3527
3527
  as elements rather than parsed as markup, with any scheme other than <code>http</code>,
3528
3528
  <code>https</code> and <code>mailto</code> refused. A refused link still shows its label, so
3529
3529
  nothing the author wrote vanishes without trace.</p>
3530
3530
  <p><strong>The marker cannot move the cell's contents.</strong> It is a corner triangle drawn
3531
3531
  with a border on a pseudo-element, so it occupies no space in the layout: no shifted text, no
3532
3532
  rewrapped number, no displaced sparkline. That constraint is why it is a corner rather than a
3533
- badge &mdash; every other position in a cell is already spoken for. Only the corner opens a
3533
+ badge: every other position in a cell is already spoken for. Only the corner opens a
3534
3534
  thread; a click elsewhere belongs to selection, and taking it would make commented cells
3535
3535
  behave unlike every other cell. The hit region is larger than the drawn mark, because at
3536
3536
  compact density the triangle is about seven pixels across.</p>
@@ -3538,7 +3538,7 @@ const providerFor = (id) =&gt; ({
3538
3538
  matches the cell. Without it, a note reading "this looks too high" sits beside a number it
3539
3539
  never described and the reader concludes the comment is wrong. Changing a value never deletes
3540
3540
  or invalidates a comment.</p>
3541
- <p><strong>Filtered-out comments are hidden, not lost — and the grid says so.</strong>
3541
+ <p><strong>Filtered-out comments are hidden, not lost, and the grid says so.</strong>
3542
3542
  <code>hiddenUnresolved()</code> reports what is still outstanding on rows the filter is
3543
3543
  hiding, because a user who filters and sees no markers should not conclude there is nothing
3544
3544
  left to deal with. The status bar carries this: its <code>comments</code> panel reads
@@ -3549,11 +3549,11 @@ const providerFor = (id) =&gt; ({
3549
3549
  <pre><code>statusBar: { panels: ['rowCount', 'comments'] }</code></pre>
3550
3550
  </div>
3551
3551
  <p>The count returns zero rather than a number it cannot stand behind: it is only meaningful
3552
- once the index covers every row, so it stays at zero &mdash; and the panel stays silent &mdash;
3552
+ once the index covers every row, so it stays at zero, and the panel stays silent ,
3553
3553
  until <code>loadAll()</code> has resolved.</p>
3554
3554
  <p><strong>The comments-only filter is refused rather than approximated.</strong> Restricting
3555
3555
  the grid to rows carrying comments needs the index to cover the whole row set, not just what
3556
- has been scrolled past &mdash; a partial answer would hide precisely the rows the user opened
3556
+ has been scrolled past, a partial answer would hide precisely the rows the user opened
3557
3557
  it to find. Call <code>loadAll()</code> first; until <code>complete</code> is true,
3558
3558
  <code>filterToCommented()</code> returns false and does nothing.</p>
3559
3559
  <p><strong>Comments stay available while streaming</strong>, unlike header histograms: a
@@ -3561,7 +3561,7 @@ const providerFor = (id) =&gt; ({
3561
3561
  debounced viewport path. A thread whose row is evicted by a bounded window closes with a
3562
3562
  short explanation rather than hovering over a row that has gone.</p>
3563
3563
  <p><strong>Keyboard and screen reader.</strong> <kbd>Alt</kbd>+<kbd>M</kbd> opens the thread
3564
- on the focused cell &mdash; Alt because the grid binds nearly every unmodified key to
3564
+ on the focused cell: Alt because the grid binds nearly every unmodified key to
3565
3565
  navigation and editing. The panel traps <kbd>Tab</kbd>, which it has to: the grid behind it is
3566
3566
  still there and still focusable, so without the trap a keyboard user would be moving through
3567
3567
  cells with a dialog open over them. Focus returns to the originating cell on close rather than
@@ -3571,7 +3571,7 @@ const providerFor = (id) =&gt; ({
3571
3571
  other than what it holds.</p>
3572
3572
  <p><strong>Not in this release:</strong> mentions, notifications, rich text, attachments,
3573
3573
  reactions, row-level and column-level comments, and export of comments. The grid opens no
3574
- transport of its own &mdash; if your application pushes updates, call <code>refresh()</code>
3574
+ transport of its own: if your application pushes updates, call <code>refresh()</code>
3575
3575
  and the index reloads.</p>
3576
3576
  </div>
3577
3577
 
@@ -3592,11 +3592,11 @@ const providerFor = (id) =&gt; ({
3592
3592
  <p><strong>The column being filtered is not counted against its own filter.</strong> Every
3593
3593
  other active filter applies; that column's own conditions are pruned out of the tree before
3594
3594
  counting. This is the whole of faceted browsing and it is the part that is easy to get subtly
3595
- wrong &mdash; a self-filtered chart collapses to a single bar the moment you click one, and
3595
+ wrong, a self-filtered chart collapses to a single bar the moment you click one, and
3596
3596
  there is then no way to see what you excluded or to widen the selection. Getting it wrong
3597
3597
  does not degrade the feature, it removes it.</p>
3598
3598
  <p><strong>Pruning is not symmetric across operators.</strong> An <code>and</code> group
3599
- narrows with each condition, so dropping one widens the result &mdash; the direction faceting
3599
+ narrows with each condition, so dropping one widens the result, the direction faceting
3600
3600
  wants. An <code>or</code> group widens with each branch, so dropping one would show
3601
3601
  <em>fewer</em> rows than the user's actual filter. There is no partial answer that is correct,
3602
3602
  so a disjunction naming the column is dropped whole.</p>
@@ -3606,7 +3606,7 @@ const providerFor = (id) =&gt; ({
3606
3606
  because the thing you are pointing at would move as you pointed at it.</p>
3607
3607
  <p><strong>Each bar carries two readings.</strong> Its full height is the bucket's share of
3608
3608
  the unfiltered column; the solid fill inside is how much survives the current filters. Either
3609
- alone misleads &mdash; scaling to the filtered maximum draws a full-height chart out of three
3609
+ alone misleads: scaling to the filtered maximum draws a full-height chart out of three
3610
3610
  surviving rows, and scaling everything down together flattens the whole chart into a few
3611
3611
  pixels the moment anyone filters anything.</p>
3612
3612
  <p><strong>The filters are ordinary filters.</strong> They go through the same
@@ -3623,7 +3623,7 @@ const providerFor = (id) =&gt; ({
3623
3623
  than a scan. The first column anyone points this at is a name or an id, and one hairline per
3624
3624
  customer looks like a rendering fault rather than a distribution. Above
3625
3625
  <code>cardinalityLimit</code> the chart is suppressed, or shows a top-N with an aggregated
3626
- remainder if you ask for <code>aboveLimit: 'topN'</code> &mdash; aggregated rather than
3626
+ remainder if you ask for <code>aboveLimit: 'topN'</code>: aggregated rather than
3627
3627
  truncated, because silently dropping the tail would misrepresent the bars it did draw.</p>
3628
3628
  <p><strong>Nulls are never dropped.</strong> They land in a terminal bucket, always last, and
3629
3629
  the counts always sum to the row count. A column where nine thousand of ten thousand rows are
@@ -3633,8 +3633,8 @@ const providerFor = (id) =&gt; ({
3633
3633
  <p><strong>Live streams suppress the charts.</strong> Constantly shifting distributions are
3634
3634
  unreadable, recounting on every batch is wasteful, and a filter control whose buckets move
3635
3635
  under the pointer is actively hostile. Filters already made stay applied, because they are
3636
- ordinary filters. Pausing the stream brings the charts back &mdash; a paused stream is a still
3637
- one &mdash; unless you set <code>whilePaused: false</code>.</p>
3636
+ ordinary filters. Pausing the stream brings the charts back, a paused stream is a still
3637
+ one: unless you set <code>whilePaused: false</code>.</p>
3638
3638
  <p><strong>Counting runs off the main thread above <code>workerThreshold</code>.</strong>
3639
3639
  Distributions are the only work the grid moves to a Worker. Sorting, filtering and grouping
3640
3640
  run on the main thread; nothing waits on a histogram, which is what makes this one
@@ -3643,24 +3643,24 @@ const providerFor = (id) =&gt; ({
3643
3643
  detach the buffer the grid is still rendering from.</p>
3644
3644
  <p><strong>The Worker settings.</strong> <code>useWorker</code> and
3645
3645
  <code>workerThreshold</code> decide whether and when a distribution is offloaded. Two more
3646
- control how the Worker is built, and both are settled when it is constructed &mdash; changing
3646
+ control how the Worker is built, and both are settled when it is constructed: changing
3647
3647
  either discards the running Worker so the next offload builds a new one.</p>
3648
3648
  <div class="table-wrap">
3649
3649
  <table>
3650
3650
  <thead><tr><th>Setting</th><th>What it does</th></tr></thead>
3651
3651
  <tbody>
3652
- <tr><td class="name"><code>workerUrl</code></td><td class="desc">Loads the Worker from a URL you host instead of a <code>blob:</code>. Required under a Content-Security-Policy that forbids <code>blob:</code> workers &mdash; without it the Worker cannot be constructed at all on such a page, and compute stays on the main thread.</td></tr>
3652
+ <tr><td class="name"><code>workerUrl</code></td><td class="desc">Loads the Worker from a URL you host instead of a <code>blob:</code>. Required under a Content-Security-Policy that forbids <code>blob:</code> workers: without it the Worker cannot be constructed at all on such a page, and compute stays on the main thread.</td></tr>
3653
3653
  <tr><td class="name"><code>sharedMemory</code></td><td class="desc">Off by default. Passes columns to the Worker in a <code>SharedArrayBuffer</code> rather than copying them on every message, at the cost of retaining a shared copy of each column that crosses. Needs the page to be cross-origin isolated; where it is not, it falls back to copying and says so once.</td></tr>
3654
3654
  </tbody>
3655
3655
  </table>
3656
3656
  </div>
3657
3657
  <p><code>grid.diagnostics.renders().worker</code> reports what the Worker host is actually
3658
- doing &mdash; whether one was spawned, how many calls ran locally versus remotely, and the
3658
+ doing: whether one was spawned, how many calls ran locally versus remotely, and the
3659
3659
  threshold, <code>sharedMemory</code> and <code>workerUrl</code> it was built with.</p>
3660
3660
  <p><strong>Server-side sources need a <code>provider</code>, and its absence is silent.</strong>
3661
3661
  A grid holding one page of data cannot compute a distribution over the whole set. Supply a
3662
3662
  function and it receives the column, the pruned filter state and the bucketing settings, and
3663
- returns counts. Without one the charts are simply absent &mdash; no error, because most
3663
+ returns counts. Without one the charts are simply absent, no error, because most
3664
3664
  deployments will never supply one. Be clear-eyed about the load: results are cached against
3665
3665
  the filter state, but this is one query per column per filter change, and a grid with eight
3666
3666
  faceted columns asks eight questions every time a filter moves.</p>
@@ -3669,13 +3669,13 @@ const providerFor = (id) =&gt; ({
3669
3669
  toggles, <kbd>Shift</kbd> with arrows extends a range on ordered columns, <kbd>Escape</kbd>
3670
3670
  clears. Selected buckets carry an outline as well as a colour. Beyond per-bucket labels the
3671
3671
  chart carries a sentence describing the distribution's shape, because twenty bucket readings
3672
- do not add up to "most of the mass is at the low end" &mdash; and that shape is the entire
3672
+ do not add up to "most of the mass is at the low end", and that shape is the entire
3673
3673
  value of the chart.</p>
3674
3674
  </div>
3675
3675
 
3676
3676
  <h2 id="timeline-guide">Time scrubber</h2>
3677
3677
  <p class="lead-in">
3678
- Move the grid back through recent data changes — what did this look like a minute ago,
3678
+ Move the grid back through recent data changes: what did this look like a minute ago,
3679
3679
  before that number moved.
3680
3680
  </p>
3681
3681
  <div class="example">
@@ -3691,17 +3691,17 @@ grid.timeline.detach();</code></pre>
3691
3691
  </div>
3692
3692
  <div class="why">
3693
3693
  <p><strong>Attaching puts a control on the grid.</strong> A slider along the bottom with two
3694
- readings beside it — how long ago, and the clock time. Relative answers the question actually
3694
+ readings beside it: how long ago, and the clock time. Relative answers the question actually
3695
3695
  being asked; absolute is what someone reads out to the person next to them. It moves the grid
3696
3696
  while the handle is dragged rather than on release, and it turns accent-coloured the moment
3697
3697
  you are off live, because a grid quietly showing stale data is the failure this control can
3698
3698
  cause. It removes itself on <code>detach()</code>.</p>
3699
3699
  <p><strong>It reads the data, not your actions.</strong> Undo history records what the
3700
- <em>user</em> did — sorts, filters, edits — which is rarely the question. This reads the
3700
+ <em>user</em> did (sorts, filters, edits) which is rarely the question. This reads the
3701
3701
  change log: a bounded, timestamped, deliberately unmerged record of everything that arrived,
3702
3702
  so the intermediate states are all still there to move between.</p>
3703
3703
  <p><strong>Nothing is scrubbable before <code>attach()</code>.</strong> What a value used to
3704
- be is not recoverable after the fact — no other part of the grid remembers it — so recording
3704
+ be is not recoverable after the fact (no other part of the grid remembers it) so recording
3705
3705
  has to be switched on before there is a past to move through. It is off by default because
3706
3706
  reading a row per key on every change is real cost on a hot feed, and paying it for a
3707
3707
  scrubber nobody opened would be the wrong default.</p>
@@ -3711,7 +3711,7 @@ grid.timeline.detach();</code></pre>
3711
3711
  <p><strong>Value changes reverse; row additions and removals do not.</strong> An add would
3712
3712
  need a removal and a remove would need re-insertion at its old position, and neither is
3713
3713
  recoverable from what the log holds. A window containing them scrubs over the value changes
3714
- and leaves the row set alone — stated plainly because the alternative is a scrubber that
3714
+ and leaves the row set alone: stated plainly because the alternative is a scrubber that
3715
3715
  silently half-works.</p>
3716
3716
  <p><strong>What moved is marked.</strong> Seeking compares each affected row before and after
3717
3717
  and marks the cells whose value changed, in <code>--lattice-timeline-changed</code>. Without
@@ -3719,13 +3719,13 @@ grid.timeline.detach();</code></pre>
3719
3719
  number you are hunting for goes past unseen.</p>
3720
3720
  <p><strong>The mark is held, not flashed.</strong> It stays until the next seek clears it.
3721
3721
  Every other transient signal in the grid fades on a timer, and this one deliberately does not
3722
- — a scrub is someone hunting for what changed, and a highlight they can miss while reading the
3722
+ , a scrub is someone hunting for what changed, and a highlight they can miss while reading the
3723
3723
  other end of the row helps nobody. <code>timeline:seeking</code> fires before any change is
3724
3724
  applied, so a five-step drag clears once and marks once rather than strobing per entry.</p>
3725
3725
  <p><strong>It compares column values, not raw fields.</strong> A computed column has no field
3726
3726
  of its own; diffing the source row would leave it silently unmarked while its number visibly
3727
3727
  moved. Reading through the column instead costs a little more and marks what the viewer can
3728
- actually see change — which is the only definition of "changed" that matters here.</p>
3728
+ actually see change, which is the only definition of "changed" that matters here.</p>
3729
3729
  <p><strong>The window is bounded by rows, not only by changes.</strong> A cap on entries
3730
3730
  alone does not bound memory, because an entry is not a fixed size: one carrying a single
3731
3731
  changed cell and one carrying a fifty-thousand-row batch both count as one. So the log holds
@@ -3734,7 +3734,7 @@ grid.timeline.detach();</code></pre>
3734
3734
  here than it looks: the log is what keeps superseded row objects alive after the source has
3735
3735
  swapped in their replacements, so on a feed delivering five thousand rows a batch an
3736
3736
  entry-only cap retains twenty million of them. The one exception is a single change larger
3737
- than the whole cap, which is kept — emptying the log would be worse than being briefly over,
3737
+ than the whole cap, which is kept: emptying the log would be worse than being briefly over,
3738
3738
  and it would drop the newest change rather than the oldest. Watch <code>held</code> against
3739
3739
  <code>heldLimit</code> in <code>grid.updates.stats()</code>; <code>rows</code> is a lifetime
3740
3740
  total and says nothing about memory.</p>
@@ -3766,14 +3766,14 @@ grid.presentation.stop(); <span class="cmt">// or Escape</span
3766
3766
  <thead><tr><th>Keys</th><th>Does</th></tr></thead>
3767
3767
  <tbody>
3768
3768
  <tr><td class="sig">Escape</td><td class="desc">Leave, restoring the grid exactly as it was.</td></tr>
3769
- <tr><td class="sig">Ctrl/Cmd + = / -</td><td class="desc">Enlarge or reduce live — a laptop on a call and a projector at the back of a room are different problems.</td></tr>
3769
+ <tr><td class="sig">Ctrl/Cmd + = / -</td><td class="desc">Enlarge or reduce live, a laptop on a call and a projector at the back of a room are different problems.</td></tr>
3770
3770
  <tr><td class="sig">Ctrl/Cmd + 0</td><td class="desc">Back to the default enlargement.</td></tr>
3771
3771
  </tbody>
3772
3772
  </table>
3773
3773
  </div>
3774
3774
  <div class="why">
3775
3775
  <p><strong>The scale multiplies your density, it does not replace it.</strong> A grid built at
3776
- <code>spacious</code> presented at 1.5x is still recognisably that grid, half as big again —
3776
+ <code>spacious</code> presented at 1.5x is still recognisably that grid, half as big again ,
3777
3777
  which is what makes a presentation look like the product rather than like a different one.
3778
3778
  Virtualisation follows the enlargement, so rows are positioned at the size they are drawn.</p>
3779
3779
  <p><strong>Full-screen is the maximiser, not a second implementation.</strong> A grid the user
@@ -3782,7 +3782,7 @@ grid.presentation.stop(); <span class="cmt">// or Escape</span
3782
3782
  element the host had already hidden is not revealed on exit.</p>
3783
3783
  <p><strong>Events</strong> are <code>presentation:started</code>,
3784
3784
  <code>presentation:ended</code>, <code>presentation:scale</code> and
3785
- <code>presentation:changed</code> — colon-separated like every other grid event rather than
3785
+ <code>presentation:changed</code>: colon-separated like every other grid event rather than
3786
3786
  the camelCase the original brief used, so a host subscribing to them does not have to
3787
3787
  remember which family a name belongs to.</p>
3788
3788
  </div>
@@ -3791,7 +3791,7 @@ grid.presentation.stop(); <span class="cmt">// or Escape</span
3791
3791
  <pre><code>grid.presentation.start({ views: ['escalations', 'at-risk', 'margin-watch'] });
3792
3792
  grid.presentation.step(1); <span class="cmt">// or an arrow key, space, Page Down</span>
3793
3793
  grid.presentation.goTo(0); <span class="cmt">// or Home / End</span>
3794
- grid.presentation.reset(); <span class="cmt">// or R — back to the view as saved</span></code></pre>
3794
+ grid.presentation.reset(); <span class="cmt">// or R: back to the view as saved</span></code></pre>
3795
3795
  </div>
3796
3796
  <div class="table-wrap">
3797
3797
  <table>
@@ -3810,14 +3810,14 @@ grid.presentation.reset(); <span class="cmt">// or R — back to the vi
3810
3810
  ordinary <code>views.apply</code>, as a single undo entry. Nothing about presenting changes
3811
3811
  what a view means.</p>
3812
3812
  <p><strong>The stepping keys only bind when there is a sequence</strong>, and never while
3813
- something is being typed into. Without a deck those keys belong to the grid — a presenter
3814
- with no slides still expects Page Down to scroll — and a quick filter answering a question
3813
+ something is being typed into. Without a deck those keys belong to the grid, a presenter
3814
+ with no slides still expects Page Down to scroll, and a quick filter answering a question
3815
3815
  from the room must not advance the deck on the space bar.</p>
3816
3816
  <p><strong>Stepping past either end sits there.</strong> It does not wrap: a presenter who
3817
3817
  sees the first slide again thinks the deck has restarted.</p>
3818
3818
  <p><strong>Transitions are a cross-fade, not continuous row motion.</strong> Rows are pooled
3819
3819
  and virtualised, so an element holding a row before a view change may hold a different row
3820
- after it — only rows visible in <em>both</em> states could be animated between positions, and
3820
+ after it: only rows visible in <em>both</em> states could be animated between positions, and
3821
3821
  half a movement draws the eye to whichever rows happened to survive rather than to the change
3822
3822
  itself. <code>prefers-reduced-motion</code> removes it; a projected fade is far larger than
3823
3823
  one on a laptop, so someone who asked for less motion meant it.</p>
@@ -3842,7 +3842,7 @@ grid.presentation.start({ chrome: ['statusBar'] });</code></pre>
3842
3842
  the other rows are gone. It is opacity alone, so a dimmed sparkline keeps its colours instead
3843
3843
  of flattening to grey.</p>
3844
3844
  <p><strong>A spotlight does not survive a view change.</strong> It belongs to the point being
3845
- made, not to the deck — carried forward, it leaves the audience looking at a lit row that no
3845
+ made, not to the deck: carried forward, it leaves the audience looking at a lit row that no
3846
3846
  longer means anything.</p>
3847
3847
  <p><strong>Redaction travels in views and undo.</strong> It is part of grid state, so a saved
3848
3848
  view carries its own masking and a view that redacts salary redacts it every time it is
@@ -3869,14 +3869,14 @@ grid.on('presentation:captured', (e) =&gt; {
3869
3869
  <p><strong>Mounting the bar elsewhere needs the grid's class.</strong> Every rule that styles
3870
3870
  the prompt bar is scoped under <code>.lattice</code>, and every colour token is declared
3871
3871
  there, so a bar mounted into your own chrome through <code>ai.element</code> arrives
3872
- unstyled. Add <code>class="lattice"</code> to the container — and the same
3873
- <code>data-theme</code> the grid carries, if you have set one — and it picks up the theme.</p>
3872
+ unstyled. Add <code>class="lattice"</code> to the container, and the same
3873
+ <code>data-theme</code> the grid carries, if you have set one, and it picks up the theme.</p>
3874
3874
  </div>
3875
3875
 
3876
3876
  <div class="why">
3877
3877
  <p><strong>It photographs the browser's own rendering.</strong> The grid is cloned, every
3878
3878
  computed style is inlined onto the clone, and the result is wrapped in an SVG
3879
- <code>foreignObject</code> and drawn to a canvas — so the picture is what the browser drew,
3879
+ <code>foreignObject</code> and drawn to a canvas, so the picture is what the browser drew,
3880
3880
  not a second renderer's guess at it. That matters here more than usual: every decoration,
3881
3881
  sparkline and pill the cell layer produces comes out right without being reimplemented.</p>
3882
3882
  <p><strong>Virtualisation makes it cheap.</strong> Only the rows on screen exist in the DOM,
@@ -3884,7 +3884,7 @@ grid.on('presentation:captured', (e) =&gt; {
3884
3884
  A full-screen capture at <code>scale: 2</code> takes around a second.</p>
3885
3885
  <p><strong>Cross-origin images are refused before the work starts.</strong> They taint the
3886
3886
  canvas, and a tainted canvas fails at the very last step with a <code>SecurityError</code>
3887
- that names nothing — so the check runs first and the error names the offending URL. Serve the
3887
+ that names nothing, so the check runs first and the error names the offending URL. Serve the
3888
3888
  image same-origin, inline it as a <code>data:</code> URL, or hide the column.</p>
3889
3889
  <p><strong>Two further limits</strong>, both inherent to the technique: web fonts need
3890
3890
  embedding to appear (Lattice's default <code>system-ui</code> stack is unaffected), and CSS
@@ -3909,7 +3909,7 @@ grid.annotate.use(null); <span class="cmt">// hand the grid
3909
3909
  content coordinates and redrawn with the scroll offset subtracted, so a circle drawn round a
3910
3910
  cell travels with that cell rather than hanging over whatever scrolled underneath it.</p>
3911
3911
  <p><strong>They are transient.</strong> Marks annotate a moment, so they are cleared when the
3912
- presentation ends. A capture taken while they are on screen includes them — the canvas bitmap
3912
+ presentation ends. A capture taken while they are on screen includes them, the canvas bitmap
3913
3913
  is carried into the still deliberately, because <code>cloneNode</code> copies a canvas element
3914
3914
  and not one pixel of what was drawn on it.</p>
3915
3915
  <p><strong>No tool shortcuts are bound.</strong> The keys a presenter would want are already
@@ -3920,7 +3920,7 @@ grid.annotate.use(null); <span class="cmt">// hand the grid
3920
3920
  <h2 id="accessibility-guide">Accessibility</h2>
3921
3921
  <p class="lead-in">
3922
3922
  The grid is built to WCAG 2.2 level AA. What follows is what it does, what it does not do yet,
3923
- and the keyboard map in full &mdash; stated plainly, because a conformance claim that overstates
3923
+ and the keyboard map in full: stated plainly, because a conformance claim that overstates
3924
3924
  is worth less than one that admits its edges.
3925
3925
  </p>
3926
3926
 
@@ -3986,37 +3986,37 @@ grid.annotate.use(null); <span class="cmt">// hand the grid
3986
3986
  <h3>Colour and contrast</h3>
3987
3987
  <p>No information is carried by hue alone. The <code>high-contrast</code> theme runs text at 21:1
3988
3988
  and borders at 6.1:1. In Windows High Contrast Mode the grid translates its state into borders
3989
- and system colours rather than fighting the palette &mdash; see
3989
+ and system colours rather than fighting the palette: see
3990
3990
  <a href="#theming">theming</a> for what that means in detail.</p>
3991
3991
 
3992
3992
  <h3>How this is checked</h3>
3993
3993
  <p>The grid carries its own accessibility rules, and they run on every build against each
3994
- configuration that differs structurally &mdash; flat, grouped, tree, pinned, editing, paginated
3995
- and with a tool panel &mdash; rather than against one sample grid. The same rules are available
3994
+ configuration that differs structurally: flat, grouped, tree, pinned, editing, paginated
3995
+ and with a tool panel: rather than against one sample grid. The same rules are available
3996
3996
  live from the devtools panel, where they can also read colour and measure targets.</p>
3997
3997
  <p>Be clear about what that proves. These are our own rules covering what a data grid gets wrong,
3998
3998
  not a general-purpose engine, and automated checking of any kind catches a minority of real
3999
- problems. They are a regression net &mdash; they stop a fix being undone silently &mdash; and
3999
+ problems. They are a regression net (they stop a fix being undone silently) and
4000
4000
  not evidence of conformance.</p>
4001
4001
 
4002
4002
  <h3>Bigger targets for touch</h3>
4003
4003
  <p class="lead-in">
4004
4004
  The grid meets the minimum target size on its own. That minimum is a conformance floor, not a
4005
- comfortable size for a finger &mdash; both mobile platforms recommend nearer 44 pixels.
4005
+ comfortable size for a finger: both mobile platforms recommend nearer 44 pixels.
4006
4006
  </p>
4007
4007
  <div class="example">
4008
4008
  <pre><code>targetSize: 'large'</code></pre>
4009
4009
  </div>
4010
4010
  <p>This raises the hit areas and leaves the type where it is, which is the distinction that
4011
4011
  matters: a touch user wants a larger target, and a low-vision user wants larger text. Density
4012
- is the control for the second, and the two combine &mdash; a compact grid with large targets is
4012
+ is the control for the second, and the two combine, a compact grid with large targets is
4013
4013
  a reasonable thing to want on a tablet.</p>
4014
4014
  <p>It applies by itself under a coarse pointer, since the person holding one is both who the
4015
4015
  criterion is for and the least likely to go looking for a setting. Pass
4016
4016
  <code>targetSize: 'default'</code> to opt out of that.</p>
4017
4017
  <div class="why">
4018
4018
  <p>Density alone does not do this. It scales the header, the rows and the type, and leaves the
4019
- affordances inside them exactly as they were &mdash; measured at every preset, the menu button
4019
+ affordances inside them exactly as they were: measured at every preset, the menu button
4020
4020
  stays 24 pixels, the filter 16 and the resize grip 10. A spacious grid has the room going spare
4021
4021
  and controls no larger than a compact one, which is the gap this fills.</p>
4022
4022
  </div>
@@ -4026,7 +4026,7 @@ grid.annotate.use(null); <span class="cmt">// hand the grid
4026
4026
  <ul>
4027
4027
  <li><strong>Two-dimensional scrolling.</strong> A grid scrolls horizontally at narrow widths.
4028
4028
  WCAG 1.4.10 Reflow explicitly permits this for data tables, so it is conforming rather than a
4029
- gap &mdash; but it is worth knowing before you meet it.</li>
4029
+ gap, but it is worth knowing before you meet it.</li>
4030
4030
  <li><strong>Pinned columns do not release at narrow widths.</strong> At around 320 pixels, two
4031
4031
  pinned columns of ordinary width can leave under 60 pixels for the scrolling middle. The grid
4032
4032
  stays operable and nothing is lost, but a layout that pins columns is worth reviewing if you
@@ -4034,7 +4034,7 @@ grid.annotate.use(null); <span class="cmt">// hand the grid
4034
4034
  <li><strong>The filter icon is a small target, deliberately.</strong> It is 16&nbsp;&times;&nbsp;16,
4035
4035
  below the 24-pixel minimum of WCAG 2.5.8, and conforms under that criterion's
4036
4036
  <em>equivalent</em> allowance: filtering is also a column-menu item, and the menu button
4037
- meets the size on its own. Worth knowing if you are pointing at it on a touch screen &mdash;
4037
+ meets the size on its own. Worth knowing if you are pointing at it on a touch screen ,
4038
4038
  the menu is the larger route to the same thing.</li>
4039
4039
  </ul>
4040
4040
 
@@ -4067,9 +4067,9 @@ grid.set('density', 'compact'); <span class="cmt">// live</span></code></pre
4067
4067
  <p><strong>Virtualisation follows the token, not a separate number.</strong> Row positions
4068
4068
  are computed from the resolved <code>--lattice-row-height</code>, so a host that overrides
4069
4069
  that token by hand gets the virtualisation to agree. An explicit <code>rowHeight</code> in
4070
- config outranks both — a host that names a number means it.</p>
4070
+ config outranks both, a host that names a number means it.</p>
4071
4071
  <p><strong>Height alone will not reproduce a modern app listing.</strong> Those designs pair
4072
- generous rows with two-line cells — a bold title over a grey sub-label — and an avatar or
4072
+ generous rows with two-line cells (a bold title over a grey sub-label) and an avatar or
4073
4073
  thumbnail. <code>spacious</code> gives the room, an <a href="#image-guide">image column</a>
4074
4074
  gives the picture, and <a href="#twoline-guide">twoline</a> gives the second line.</p>
4075
4075
  </div>
@@ -4102,7 +4102,7 @@ grid.set('density', 'compact'); <span class="cmt">// live</span></code></pre
4102
4102
  including a <code>data:</code> URL claiming to be anything other than an image. Relative URLs
4103
4103
  pass, since they cannot name a scheme. A grid drawing URLs that arrived in a data feed is
4104
4104
  exactly where a bad one gets through. Note this differs from the <code>link</code> renderer,
4105
- which refuses <code>data:</code> outright — correct for an anchor, wrong for a picture.</p>
4105
+ which refuses <code>data:</code> outright: correct for an anchor, wrong for a picture.</p>
4106
4106
  <p><strong>A missing or broken image never moves the column.</strong> The box is sized from
4107
4107
  the row height whether or not the picture loads, and a failed load leaves it empty rather
4108
4108
  than showing the browser's broken-image glyph, which is a different size in every engine.</p>
@@ -4149,7 +4149,7 @@ props: { secondary: 'user', format: (v) =&gt; v ? `User: ${v}` : '' }</code></pr
4149
4149
  <h2 id="redaction-guide">Redacting a column</h2>
4150
4150
  <p class="lead-in">
4151
4151
  For presenting and screen sharing: obscure the values in a column while everything that
4152
- makes the grid readable — the row count, the sort, the filters, the column layout — stays
4152
+ makes the grid readable (the row count, the sort, the filters, the column layout) stays
4153
4153
  exactly as it was. Right-click a column heading and choose <strong>Redact column</strong>.
4154
4154
  </p>
4155
4155
  <div class="example">
@@ -4162,12 +4162,12 @@ grid.redaction.clear(); <span class="cmt">// when the call ends</span><
4162
4162
  <p><strong>This is not a security control, and the difference matters.</strong> The values
4163
4163
  are still in the model, still in the DOM, still on the clipboard and still in every export.
4164
4164
  Anyone looking at the page can read them from devtools, or by turning off a single CSS rule.
4165
- What redaction defeats is a camera and a screen recorder — which is the threat a presenter
4165
+ What redaction defeats is a camera and a screen recorder, which is the threat a presenter
4166
4166
  actually has, and the only one it claims to answer. For a value that must never reach the
4167
4167
  browser, use <code>permissions</code> with <code>writeOnly</code>: there the value is not
4168
4168
  sent, so there is nothing to reveal.</p>
4169
4169
  <p><strong>Headings stay readable, totals do not.</strong> The column heading is left alone
4170
- deliberately — a redacted column still has to be identifiable, by the presenter who wants to
4170
+ deliberately, a redacted column still has to be identifiable, by the presenter who wants to
4171
4171
  turn it back on and by the audience who need to know what they are not being shown. The
4172
4172
  pinned totals row <em>is</em> redacted, because the sum of a column is not a hint at its
4173
4173
  values: filter to one row and the total is the value.</p>
@@ -4181,7 +4181,7 @@ grid.redaction.clear(); <span class="cmt">// when the call ends</span><
4181
4181
  <h2 id="clipboard">Clipboard</h2>
4182
4182
  <p class="lead-in">
4183
4183
  Copy produces the tab-separated form Excel, Numbers and Sheets all read, so a range pastes as
4184
- cells rather than as one lump of text. Values go through each column's clipboard hook — a
4184
+ cells rather than as one lump of text. Values go through each column's clipboard hook: a
4185
4185
  lookup copies its label, a date copies an unambiguous form.
4186
4186
  </p>
4187
4187
  <div class="example">
@@ -4203,7 +4203,7 @@ grid.edit.pasteInto(text); <span class="cmt">// Excel's t
4203
4203
  <p class="lead-in">
4204
4204
  Press <kbd>?</kbd> in the grid to see this list in the product. The overlay is generated from
4205
4205
  the same bindings the grid implements, so it cannot drift from them, and it shows
4206
- <kbd>Cmd</kbd> rather than <kbd>Ctrl</kbd> on a Mac — the grid reads either, so that is what
4206
+ <kbd>Cmd</kbd> rather than <kbd>Ctrl</kbd> on a Mac, the grid reads either, so that is what
4207
4207
  you will actually press. <kbd>Escape</kbd> closes it and focus returns where it was. Set
4208
4208
  <code>shortcuts: false</code> if you want <kbd>?</kbd> for something else.
4209
4209
  </p>
@@ -4225,7 +4225,7 @@ grid.edit.pasteInto(text); <span class="cmt">// Excel's t
4225
4225
  </table>
4226
4226
  </div>
4227
4227
  <div class="why">
4228
- <p>None of these fire while you are typing into an input — a filter box, an open editor, the
4228
+ <p>None of these fire while you are typing into an input, a filter box, an open editor, the
4229
4229
  view-name field. The grid checks where the keystroke came from before claiming it, which
4230
4230
  sounds obvious and is the sort of thing that is usually wrong.</p>
4231
4231
  </div>
@@ -4251,17 +4251,17 @@ grid.edit.pasteInto(text); <span class="cmt">// Excel's t
4251
4251
  who has built a filter has already learned this, and two vocabularies for one idea is how a
4252
4252
  product ends up explaining itself twice.</p>
4253
4253
  <p><strong>First match wins, by default.</strong> "Red if overdue, amber if due this week"
4254
- reads top to bottom and stops — the spreadsheet convention, and the one people expect.
4254
+ reads top to bottom and stops, the spreadsheet convention, and the one people expect.
4255
4255
  <code>stopIfTrue: false</code> lets rules combine: weight from one, colour from another.</p>
4256
4256
  <p><strong>A blank cell satisfies no comparison.</strong> <code>Number(null)</code> is zero, so
4257
4257
  a naive implementation sweeps every empty cell into "less than 100" and formats half a column
4258
4258
  that has no data in it.</p>
4259
- <p><strong>Text still compares numerically.</strong> A rules panel produces strings — the box
4260
- the user typed into yields <code>"100"</code>, not <code>100</code> — and comparing those as
4259
+ <p><strong>Text still compares numerically.</strong> A rules panel produces strings, the box
4260
+ the user typed into yields <code>"100"</code>, not <code>100</code>, and comparing those as
4261
4261
  text puts <code>"9"</code> above <code>"100"</code>.</p>
4262
4262
  <p><strong>A scale's bounds are given, not derived.</strong> Deriving them means scanning the
4263
4263
  column per cell, and a scale that rescaled as rows were filtered would change a cell's colour
4264
- without its value changing — the opposite of what the colour is for.</p>
4264
+ without its value changing, the opposite of what the colour is for.</p>
4265
4265
  </div>
4266
4266
 
4267
4267
  <h2 id="formatting-guide">Formatting an end user can change</h2>
@@ -4283,8 +4283,8 @@ grid.formatting.clear('margin');</code></pre>
4283
4283
  </div>
4284
4284
  <div class="why">
4285
4285
  <p><strong>Two scopes, one ordered list.</strong> A rule sits on a column id or on
4286
- <code>'*'</code> for every column. Evaluation joins them — grid-wide first, then the column's
4287
- own — so a column rule can override a grid-wide one, and <code>stopIfTrue</code> means the
4286
+ <code>'*'</code> for every column. Evaluation joins them: grid-wide first, then the column's
4287
+ own, so a column rule can override a grid-wide one, and <code>stopIfTrue</code> means the
4288
4288
  same thing across the join as it does within either half.</p>
4289
4289
  <p><strong>Saved views and undo came free.</strong> The rules are a section of
4290
4290
  <code>GridState</code>, and both saved views and the undo timeline are built on that. Nothing
@@ -4303,12 +4303,12 @@ grid.formatting.clear('margin');</code></pre>
4303
4303
  <pre><code>createGrid(el, { toolPanel: { side: 'right', panels: ['columns', 'filters', 'formatting'] } });</code></pre>
4304
4304
  </div>
4305
4305
  <div class="why">
4306
- <p>The panel is a form over that array and nothing more — every control is one call into
4306
+ <p>The panel is a form over that array and nothing more: every control is one call into
4307
4307
  <code>grid.formatting</code>, which is what makes each gesture undoable without the panel
4308
4308
  knowing undo exists. It exposes ordering because ordering is meaning: dragging a rule up can
4309
4309
  change which of two colours a cell takes.</p>
4310
4310
  <p><strong>Not yet in the panel:</strong> icon sets and data bars. Both are column decorations
4311
- rather than cell styles — the <code>bar</code> and <code>icon</code> decorations already
4311
+ rather than cell styles, the <code>bar</code> and <code>icon</code> decorations already
4312
4312
  render them, and driving those from the panel needs a runtime column-decoration API that
4313
4313
  persists and undoes alongside the rules. Building it as a second bar implementation inside
4314
4314
  the rule engine was the alternative, and the wrong one.</p>
@@ -4324,7 +4324,7 @@ grid.formatting.clear('margin');</code></pre>
4324
4324
  grid.filters.quick('crc', { mode: 'fuzzy' }); <span class="cmt">// characters in order</span>
4325
4325
  grid.filters.quick('^CIR-[12]', { mode: 'regex' });
4326
4326
 
4327
- grid.filters.quick('acme'); <span class="cmt">// mode persists — still 'regex' here</span>
4327
+ grid.filters.quick('acme'); <span class="cmt">// mode persists: still 'regex' here</span>
4328
4328
  grid.filters.quickState(); <span class="cmt">// { text, mode }</span></code></pre>
4329
4329
  </div>
4330
4330
  <div class="why">
@@ -4365,7 +4365,7 @@ grid.filters.quickState(); <span class="cmt">// { text, mode }</span></co
4365
4365
  </div>
4366
4366
  <div class="why">
4367
4367
  <p><strong>A cell is a few hundred pixels seen for a second</strong>, so there are no axes, no
4368
- gridlines and no legend. The chart carries one idea — a shape, a share, a comparison — and the
4368
+ gridlines and no legend. The chart carries one idea (a shape, a share, a comparison) and the
4369
4369
  number beside it carries the precision. Hide the number with <code>label: false</code> when the
4370
4370
  column next to it already says the same thing.</p>
4371
4371
  <p><strong>Pin <code>min</code> and <code>max</code> when columns are meant to compare.</strong>
@@ -4373,12 +4373,12 @@ grid.filters.quickState(); <span class="cmt">// { text, mode }</span></co
4373
4373
  by an order of magnitude draw identically. Pinning the scale is what makes the column readable
4374
4374
  down its length rather than only across it.</p>
4375
4375
  <p><strong>A gap is not a zero.</strong> Entries that are not numbers break the line and omit
4376
- the bar, rather than being drawn at the baseline — joining across a missing reading would draw
4376
+ the bar, rather than being drawn at the baseline: joining across a missing reading would draw
4377
4377
  a trend nobody measured, and drawing it at zero invents a dip.</p>
4378
4378
  <p><strong>Cost.</strong> Each chart is one SVG built once, with the paths' <code>d</code>
4379
4379
  attributes the only thing a repaint writes, and a bar chart is two paths rather than one
4380
4380
  element per bar. Rows recycle as you scroll, so this is what keeps a chart column the same
4381
- price as a text one. Nothing measures the DOM — the drawing happens in a fixed coordinate
4381
+ price as a text one. Nothing measures the DOM, the drawing happens in a fixed coordinate
4382
4382
  space that CSS scales.</p>
4383
4383
  <p><strong>Accessibility.</strong> The chart is <code>aria-hidden</code> and the cell carries a
4384
4384
  summary: "12 points, 9 to 20, ending 18". A path cannot be read aloud, and a series announced
@@ -4409,14 +4409,14 @@ grid.filters.quickState(); <span class="cmt">// { text, mode }</span></co
4409
4409
  <em>user</em> typed into a cell. Handing it to the JavaScript engine would let anyone who can
4410
4410
  edit a cell read your cookies, call your API with your credentials, or post the grid's contents
4411
4411
  anywhere. It is a hand-written tokeniser and recursive-descent parser, and the only callable
4412
- things are the built-in functions — <code>constructor</code>, <code>globalThis</code> and
4412
+ things are the built-in functions: <code>constructor</code>, <code>globalThis</code> and
4413
4413
  <code>constructor.constructor("return 1")()</code> all simply fail to resolve.</p>
4414
4414
  <p><strong>The result is stored, not the expression.</strong> A formula is a way of
4415
4415
  <em>entering</em> a value, exactly like <code>1,200</code> or <code>(50)</code> or
4416
4416
  <code>12%</code>. It commits as one undo step, fires one <code>cell:changed</code>, and passes
4417
4417
  through the column's own validation.</p>
4418
4418
  <p><strong>The result is stored, not the formula.</strong> The expression is evaluated once, at
4419
- the moment you commit it, and what lands in the cell is a value like any other — so it does not
4419
+ the moment you commit it, and what lands in the cell is a value like any other, so it does not
4420
4420
  recalculate when a cell it referred to changes later. For a value that must stay in step with
4421
4421
  its inputs, use a computed column, which is re-evaluated whenever its dependencies move.</p>
4422
4422
  </div>
@@ -4436,14 +4436,14 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
4436
4436
  </div>
4437
4437
  <p class="lead-in">
4438
4438
  A formula that cannot be read rejects the edit and the cell keeps its old value, the same as any
4439
- other unparseable text. Failures are returned rather than thrown — this runs on the commit path,
4439
+ other unparseable text. Failures are returned rather than thrown: this runs on the commit path,
4440
4440
  where an exception would abandon the commit half-done.
4441
4441
  </p>
4442
4442
  <div class="why">
4443
4443
  <p><strong>Bare arithmetic is deliberately not a formula.</strong> <code>2-1</code> is a
4444
4444
  plausible product code and <code>1/2</code> a plausible date. A reader that evaluated either on
4445
4445
  a guess would have to guess wrong sometimes, and the wrong answer is not a visible error but a
4446
- plausible number — <code>2*3</code> stored as <strong>23</strong> looks like data. Arithmetic
4446
+ plausible number: <code>2*3</code> stored as <strong>23</strong> looks like data. Arithmetic
4447
4447
  without a leading <code>=</code> is refused outright, so the cell keeps what it had rather than
4448
4448
  taking a number nobody typed.</p>
4449
4449
  </div>
@@ -4474,7 +4474,7 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
4474
4474
  <div class="why">
4475
4475
  <p><strong>Handed the defaults, rather than replacing them.</strong> A builder that had to
4476
4476
  return every item in order to append one would be written once as a copy of the built-ins and
4477
- would then drift from them — the copy keeps the menu it was forked from, and stops gaining
4477
+ would then drift from them, the copy keeps the menu it was forked from, and stops gaining
4478
4478
  whatever the grid adds later. Spreading <code>defaults</code> costs one line and never goes
4479
4479
  stale.</p>
4480
4480
  <p>Return the array you want shown: add, remove, reorder, or replace outright. An empty array
@@ -4485,7 +4485,7 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
4485
4485
  <p class="lead-in">
4486
4486
  <code>columnMenu</code> takes the same function form, for both routes into a column's menu:
4487
4487
  the header's 3-dot button and a right-click on the heading. Its <code>params</code> is
4488
- <code>{ colId, column, grid }</code>, and the same rules apply — spread the defaults, return
4488
+ <code>{ colId, column, grid }</code>, and the same rules apply: spread the defaults, return
4489
4489
  an empty array to suppress, return nothing to leave them alone.
4490
4490
  </p>
4491
4491
  <div class="example">
@@ -4528,7 +4528,7 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
4528
4528
  </div>
4529
4529
  <p class="lead-in">
4530
4530
  <code>title</code> and <code>icon</code> may each be a function, re-read on every repaint, for
4531
- a control whose meaning changes — that is how maximise becomes restore. <code>enabled</code> is
4531
+ a control whose meaning changes: that is how maximise becomes restore. <code>enabled</code> is
4532
4532
  a predicate rather than a flag, so a button that cannot do anything greys itself out instead of
4533
4533
  doing nothing when clicked.
4534
4534
  </p>
@@ -4554,7 +4554,7 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
4554
4554
  </div>
4555
4555
  <div class="why">
4556
4556
  <p><strong>An explicit <code>actions</code> array replaces the default rather than extending
4557
- it.</strong> That is what makes it useful — you choose the set and the order — but it also
4557
+ it.</strong> That is what makes it useful (you choose the set and the order) but it also
4558
4558
  means a config written against an earlier version keeps exactly the buttons it named and
4559
4559
  silently misses anything added since. If you want whatever the current version offers, leave
4560
4560
  the key out.</p>
@@ -4589,19 +4589,19 @@ createGrid(el, { maximise: <span class="kw">false</span> });</code></pre>
4589
4589
  <div class="why">
4590
4590
  <p><strong>The element is moved, not just restyled.</strong> A <code>position: fixed</code>
4591
4591
  element is positioned against the nearest ancestor carrying a <code>transform</code>,
4592
- <code>filter</code>, <code>contain</code> or <code>will-change</code> — which is to say any
4593
- card, any animated panel, any sticky app shell — and against the viewport only when there is
4592
+ <code>filter</code>, <code>contain</code> or <code>will-change</code>, which is to say any
4593
+ card, any animated panel, any sticky app shell, and against the viewport only when there is
4594
4594
  no such ancestor. A class alone would therefore fill the window on one page and land in a
4595
4595
  300px box on the next, and would still be clipped by an <code>overflow: hidden</code> or
4596
4596
  buried by a stacking context. Reparenting to <code>&lt;body&gt;</code> removes every ancestor
4597
4597
  that could do any of that.</p>
4598
4598
  <p>Coming back is a hidden placeholder left in the element's place, not a remembered parent
4599
- and index — an index goes stale the moment your application inserts a sibling while the grid
4599
+ and index, an index goes stale the moment your application inserts a sibling while the grid
4600
4600
  is away, and then silently reinserts in the wrong slot. The placeholder also holds the vacated
4601
4601
  space open at the size the grid had, so the page behind neither reflows nor loses its scroll
4602
4602
  position while you are looking at the grid.</p>
4603
4603
  <p>The geometry is written as inline styles and every displaced property is handed back
4604
- exactly as it was found, because the element being restyled is <em>yours</em> — most often one
4604
+ exactly as it was found, because the element being restyled is <em>yours</em>: most often one
4605
4605
  with an inline <code>height</code> on it, which is the ordinary way a grid gets sized and
4606
4606
  which nothing but an inline style can beat. While maximised, the element carries
4607
4607
  <code>.lat-maximised</code> and <code>&lt;body&gt;</code> carries
@@ -4630,7 +4630,7 @@ grid.highlight.clear();</code></pre>
4630
4630
  </p>
4631
4631
  <h2 id="views-guide">Saved views</h2>
4632
4632
  <p class="lead-in">
4633
- A view is a named grid state — sort, filters, grouping, column order, widths, visibility.
4633
+ A view is a named grid state: sort, filters, grouping, column order, widths, visibility.
4634
4634
  There are two kinds and the picker keeps them apart.
4635
4635
  </p>
4636
4636
  <div class="example">
@@ -4651,14 +4651,14 @@ grid.highlight.clear();</code></pre>
4651
4651
  </div>
4652
4652
  <p class="lead-in">
4653
4653
  Views in <code>saved</code> are <strong>defined views</strong>: part of the application,
4654
- listed under their own heading, and neither renamable nor deletable — refused by the model as
4654
+ listed under their own heading, and neither renamable nor deletable: refused by the model as
4655
4655
  well as hidden in the interface. A view flagged <code>isDefault</code> is applied on load.
4656
4656
  Everything the user saves sits below, with rename, share, make-default and delete.
4657
4657
  </p>
4658
4658
  <div class="why">
4659
4659
  <p><strong>Applying a view is a destination, not a patch.</strong> A view's state names only
4660
4660
  the sections it cares about, so applying one resets to the grid's starting state first. Without
4661
- that, clicking "APAC capacity" after "Commercial" would inherit Commercial's hidden columns —
4661
+ that, clicking "APAC capacity" after "Commercial" would inherit Commercial's hidden columns ,
4662
4662
  the same view giving a different grid depending on what preceded it, which is the one thing a
4663
4663
  named view must not do.</p>
4664
4664
  </div>
@@ -4673,7 +4673,7 @@ grid.highlight.clear();</code></pre>
4673
4673
  <table>
4674
4674
  <thead><tr><th>What changed</th><th>What a saved view does</th></tr></thead>
4675
4675
  <tbody>
4676
- <tr><td class="name">A column was added</td><td class="desc"><strong>It appears</strong>, in the state its definition declares, placed after every column the view names. A view is not a whitelist — it says nothing about columns it has never seen, and silence is not an instruction to hide.</td></tr>
4676
+ <tr><td class="name">A column was added</td><td class="desc"><strong>It appears</strong>, in the state its definition declares, placed after every column the view names. A view is not a whitelist, it says nothing about columns it has never seen, and silence is not an instruction to hide.</td></tr>
4677
4677
  <tr><td class="name">A column was added, and should not appear yet</td><td class="desc">Declare it <code>layout: { hidden: true }</code>. The view does not mention it, so nothing overrides that, and it stays hidden until the user shows it.</td></tr>
4678
4678
  <tr><td class="name">A column was removed</td><td class="desc">The entries naming it are skipped and reported; the rest of the view applies. A sort or grouping on the missing column is dropped rather than left pointing at nothing.</td></tr>
4679
4679
  <tr><td class="name">A column was renamed</td><td class="desc">That is a removal and an addition. The old id is skipped, the new column appears at the end, and any width or pinning the user had set is lost with the old id.</td></tr>
@@ -4682,8 +4682,8 @@ grid.highlight.clear();</code></pre>
4682
4682
  </div>
4683
4683
 
4684
4684
  <div class="why">
4685
- <p><strong>Applying a view never throws and never refuses.</strong> It returns a report —
4686
- <code>{ applied, skipped }</code> — naming each thing it could not use and why. Refusing the
4685
+ <p><strong>Applying a view never throws and never refuses.</strong> It returns a report ,
4686
+ <code>{ applied, skipped }</code>: naming each thing it could not use and why. Refusing the
4687
4687
  whole view because one column has gone would lose a layout the user built deliberately, and
4688
4688
  throwing during a page load would lose the page. So a view degrades to as much of itself as
4689
4689
  still makes sense, and the host decides whether the user needs telling.</p>
@@ -4701,7 +4701,7 @@ grid.highlight.clear();</code></pre>
4701
4701
  <p class="lead-in">
4702
4702
  The consequence worth planning for is the first one: <strong>a column added in a new release
4703
4703
  is visible to everyone, including users with a saved view.</strong> That is usually what you
4704
- want — a new field nobody can see is a field nobody uses — but if a release adds several at
4704
+ want (a new field nobody can see is a field nobody uses) but if a release adds several at
4705
4705
  once, every saved view gains them all at the right-hand end. Ship them hidden if that is not
4706
4706
  the introduction you want.
4707
4707
  </p>
@@ -4733,16 +4733,16 @@ grid.on('view:default', e =&gt; api.patch(`/views/${e.view.id}`, { isDefault: tr
4733
4733
 
4734
4734
  <div class="why">
4735
4735
  <p><strong>The other half of the same seam.</strong> <code>views.storage</code> above is where
4736
- a developer plugs in their own backend — a real server, reached over the network. Not every
4736
+ a developer plugs in their own backend, a real server, reached over the network. Not every
4737
4737
  grid has one to plug in, and a picker offering "Save" that quietly does nothing until a backend
4738
4738
  exists is worse than not offering it. <code>views.local: true</code> is the no-backend answer:
4739
4739
  saved views live in this browser's own <code>localStorage</code>, under a default key shared by
4740
- every grid on the origin unless you pass one of your own —
4741
- <code>views: { local: { key: 'orders-grid-views' } }</code> — to keep two grids' views apart.
4740
+ every grid on the origin unless you pass one of your own ,
4741
+ <code>views: { local: { key: 'orders-grid-views' } }</code>: to keep two grids' views apart.
4742
4742
  Given alongside an explicit <code>storage</code>, the explicit adapter always wins and
4743
- <code>local</code> is silently — well, not silently: it warns once — ignored, so a page cannot
4743
+ <code>local</code> is silently (well, not silently: it warns once) ignored, so a page cannot
4744
4744
  end up writing to both without meaning to. The adapter itself is exported as
4745
- <code>createLocalViewStorage(opts)</code>, for anyone who wants it directly — a custom key
4745
+ <code>createLocalViewStorage(opts)</code>, for anyone who wants it directly, a custom key
4746
4746
  without the shorthand, or a different <code>Storage</code>-shaped backing such as
4747
4747
  <code>sessionStorage</code> for views scoped to one tab rather than persisted across visits.</p>
4748
4748
  </div>
@@ -4750,7 +4750,7 @@ grid.on('view:default', e =&gt; api.patch(`/views/${e.view.id}`, { isDefault: tr
4750
4750
  <h2 id="history-guide">Undo</h2>
4751
4751
  <p class="lead-in">
4752
4752
  Undo covers the whole grid, not only edits. Sorts, filters, column moves, grouping, an applied
4753
- view and a restore all record an entry — and each carries a label written for a button.
4753
+ view and a restore all record an entry, and each carries a label written for a button.
4754
4754
  </p>
4755
4755
  <div class="example">
4756
4756
  <p class="example__label">Naming the action</p>
@@ -4759,7 +4759,7 @@ grid.history.undo();
4759
4759
  grid.history.list(); <span class="cmt">// the timeline, newest first</span></code></pre>
4760
4760
  </div>
4761
4761
  <div class="why">
4762
- <p>A button that says only "Undo" makes the user press it to find out what it does — and
4762
+ <p>A button that says only "Undo" makes the user press it to find out what it does: and
4763
4763
  pressing it is the thing they were unsure about. "Undo sort by Region" is decided before the
4764
4764
  click rather than after.</p>
4765
4765
  </div>
@@ -4779,16 +4779,16 @@ grid.history.list(); <span class="cmt">// the timeline, newest first</sp
4779
4779
 
4780
4780
  <h2 id="permissions-guide">Column permissions</h2>
4781
4781
  <p class="lead-in">
4782
- Four levels, resolved per column from configuration or a callback. They are not a ladder —
4782
+ Four levels, resolved per column from configuration or a callback. They are not a ladder ,
4783
4783
  reading and writing are independent, so they are the four corners of a 2×2.
4784
4784
  </p>
4785
4785
  <div class="table-wrap">
4786
4786
  <table>
4787
4787
  <thead><tr><th>Level</th><th>Visible</th><th>Readable</th><th>Editable</th><th>For</th></tr></thead>
4788
4788
  <tbody>
4789
- <tr><td class="sig">hidden</td><td>—</td><td>—</td><td>—</td><td class="desc">Absent from the grid, the tool panel, exports, the clipboard, saved state and the filter model.</td></tr>
4790
- <tr><td class="sig">read</td><td>yes</td><td>yes</td><td>—</td><td class="desc">No editor opens; paste, fill and clear skip it.</td></tr>
4791
- <tr><td class="sig">writeOnly</td><td>yes</td><td>—</td><td>yes</td><td class="desc">A secret. The cell shows a mask, the editor opens empty.</td></tr>
4789
+ <tr><td class="sig">hidden</td><td>, </td><td>, </td><td>, </td><td class="desc">Absent from the grid, the tool panel, exports, the clipboard, saved state and the filter model.</td></tr>
4790
+ <tr><td class="sig">read</td><td>yes</td><td>yes</td><td>, </td><td class="desc">No editor opens; paste, fill and clear skip it.</td></tr>
4791
+ <tr><td class="sig">writeOnly</td><td>yes</td><td>, </td><td>yes</td><td class="desc">A secret. The cell shows a mask, the editor opens empty.</td></tr>
4792
4792
  <tr><td class="sig">write</td><td>yes</td><td>yes</td><td>yes</td><td class="desc">The default, so the feature is opt-in.</td></tr>
4793
4793
  </tbody>
4794
4794
  </table>
@@ -4811,12 +4811,12 @@ grid.permissions.setContext({ role: 'clerk' }); <span class="cmt">// re-r
4811
4811
  <div class="note note--warn">
4812
4812
  <p><strong>For three of the four this is a usability control, not a security boundary.</strong>
4813
4813
  Anything the grid can render it has already loaded, and devtools reaches it. Hiding a column
4814
- removes it from the interface, not from the process — which is worth a great deal for the way
4814
+ removes it from the interface, not from the process, which is worth a great deal for the way
4815
4815
  data actually leaks, which is an export mailed onward or a shared view carrying a column a
4816
4816
  colleague should not see. It is worth nothing against someone determined.</p>
4817
4817
  <p><code>writeOnly</code> is the exception, and the reason it exists. Nothing in the grid needs
4818
4818
  the value, so your server can send <code>null</code> for that field and the column still
4819
- works — at which point the secret is genuinely not on the page. Enforce everything else
4819
+ works: at which point the secret is genuinely not on the page. Enforce everything else
4820
4820
  server-side; <code>permittedColumns</code> and <code>permittedExport</code> are pure and
4821
4821
  dependency-free so the same policy object runs in Node against a request that arrived over
4822
4822
  the wire.</p>
@@ -4850,17 +4850,17 @@ grid.diff.before('CIR-100042', 'capacity');</code></pre>
4850
4850
  </div>
4851
4851
  <div class="why">
4852
4852
  <p><strong>The row is gone from the data and still in the snapshot.</strong> That is the only
4853
- place it exists, and it is what these rows are built from — so they carry the values they had
4853
+ place it exists, and it is what these rows are built from, so they carry the values they had
4854
4854
  when the snapshot was taken, not any current ones.</p>
4855
4855
  <p><strong>One option answers both questions:</strong> whether to show it, and whether it
4856
4856
  counts. <code>'pinned'</code> puts it beneath the rows, struck through and dimmed, outside the
4857
- row set — so <code>rows.count()</code>, exports and selection all pass over it.
4857
+ row set, so <code>rows.count()</code>, exports and selection all pass over it.
4858
4858
  <code>'data'</code> appends it to the set instead, and all three include it. Omitted, nothing
4859
4859
  is shown and the grid is the one you already had.</p>
4860
4860
  <p><strong>Neither sorts or filters it among the live rows.</strong> A removed row's values
4861
4861
  are yesterday's; ordering them among today's presents two data sets as one, and lets a filter
4862
4862
  written for current values decide the fate of historical ones. Neither permits an edit either
4863
- — a write aimed at a removed row is refused and returns <code>0</code>, rather than being
4863
+ , a write aimed at a removed row is refused and returns <code>0</code>, rather than being
4864
4864
  counted as applied against a record that is not there.</p>
4865
4865
  </div>
4866
4866
 
@@ -4884,15 +4884,15 @@ grid.diff.before('CIR-100042', 'capacity');</code></pre>
4884
4884
  <p class="lead-in">
4885
4885
  That mounts a prompt bar. The user types "EMEA circuits over 500 gigs, biggest first"; the
4886
4886
  grid composes a message including its schema; your callback returns the model's reply; the
4887
- grid validates it and shows what it would do in plain English —
4888
- <em>"Filter Region is EMEA and Capacity is more than 500, sort Capacity descending"</em> —
4887
+ grid validates it and shows what it would do in plain English ,
4888
+ <em>"Filter Region is EMEA and Capacity is more than 500, sort Capacity descending"</em> ,
4889
4889
  with Apply and Discard.
4890
4890
  </p>
4891
4891
  <div class="why">
4892
- <p><strong>Nothing is executed on trust.</strong> The vocabulary is seven actions —
4892
+ <p><strong>Nothing is executed on trust.</strong> The vocabulary is seven actions ,
4893
4893
  <code>setFilters</code>, <code>setSort</code>, <code>groupBy</code>,
4894
4894
  <code>showColumns</code>, <code>hideColumns</code>, <code>setQuick</code>,
4895
- <code>clear</code> — and a reply naming a column that does not exist is rejected with a
4895
+ <code>clear</code>, and a reply naming a column that does not exist is rejected with a
4896
4896
  reason while the valid actions in the same reply are kept. A model cannot be talked into an
4897
4897
  operation the vocabulary does not contain, because there is nothing else to call.</p>
4898
4898
  </div>
@@ -4910,7 +4910,7 @@ grid.export.clipboard({ headers: true, rows: 'range' });
4910
4910
  grid.export.print();</code></pre>
4911
4911
  </div>
4912
4912
  <p class="lead-in">
4913
- Exports follow what the user is looking at — the current filter, sort and column order,
4913
+ Exports follow what the user is looking at, the current filter, sort and column order,
4914
4914
  formatted values, and only the columns they may read.
4915
4915
  </p>
4916
4916
 
@@ -4938,7 +4938,7 @@ grid.state.apply(savedView.state);
4938
4938
  <h2 id="webcomponent-guide">Web component</h2>
4939
4939
  <p class="lead-in">
4940
4940
  <code>&lt;lattice-grid&gt;</code> is the grid as a custom element, shipped as a self-contained
4941
- module bundle. It exists for pages without a bundler — a Rails, Django or Laravel template
4941
+ module bundle. It exists for pages without a bundler, a Rails, Django or Laravel template
4942
4942
  that wants a grid without adopting a front-end build.
4943
4943
  </p>
4944
4944
  <div class="example">
@@ -4951,7 +4951,7 @@ grid.state.apply(savedView.state);
4951
4951
  rows='[{"id":"A","city":"Leeds"},{"id":"B","city":"Cardiff"}]'&gt;&lt;/lattice-grid&gt;</code></pre>
4952
4952
  </div>
4953
4953
  <div class="why">
4954
- <p><strong>Load this or the main bundle, not both.</strong> The module is self-contained — it
4954
+ <p><strong>Load this or the main bundle, not both.</strong> The module is self-contained: it
4955
4955
  carries the grid with it, so a page that also loads <code>lattice-grid.esm.js</code>
4956
4956
  downloads and evaluates the grid twice.</p>
4957
4957
  </div>
@@ -4971,8 +4971,8 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
4971
4971
  <div class="why">
4972
4972
  <p><strong>Assign before appending where you can.</strong> The element builds its grid once,
4973
4973
  at the end of the task in which it connects, so everything set in that task arrives as one
4974
- configuration rather than as a series of updates. Setting properties later still works — they
4975
- go through the live configuration path — but the grid is built empty first and repainted
4974
+ configuration rather than as a series of updates. Setting properties later still works: they
4975
+ go through the live configuration path, but the grid is built empty first and repainted
4976
4976
  after, which is a visible flash on a large set.</p>
4977
4977
  <p>Reading <code>.grid</code> forces the build immediately, so the property is never briefly
4978
4978
  null.</p>
@@ -4995,7 +4995,7 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
4995
4995
  <p><strong>Light DOM, deliberately.</strong> The element renders into itself rather than a
4996
4996
  shadow root, because the grid's generated decoration rules are injected into
4997
4997
  <code>document.head</code> and the theme stylesheet is a <code>&lt;link&gt;</code> the page
4998
- owns — neither crosses a shadow boundary. A shadowed grid would be structurally correct and
4998
+ owns: neither crosses a shadow boundary. A shadowed grid would be structurally correct and
4999
4999
  completely unstyled, and subtly so: the <code>--lattice-*</code> tokens <em>do</em> inherit
5000
5000
  through a shadow root, so the colours would arrive while every pill, bar and heat cell stayed
5001
5001
  bare. Light DOM keeps every documented theming route working unchanged.</p>
@@ -5013,7 +5013,7 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
5013
5013
  <tr><td class="name">ready</td><td class="type">{}</td><td class="desc">First render is done. Fires on a future turn, so you can subscribe on the line after <code>createGrid</code>.</td></tr>
5014
5014
  <tr><td class="name">cell:changed</td><td class="type">{ row, key, colId, value, oldValue, undo }</td><td class="desc">Persisting an edit. <code>undo</code> distinguishes a rollback from a fresh change.</td></tr>
5015
5015
  <tr><td class="name">selection:changed</td><td class="type">{ keys }</td><td class="desc">Enabling a bulk action.</td></tr>
5016
- <tr><td class="name">range:changed</td><td class="type">{ ranges }</td><td class="desc">A status bar showing the sum of what is selected — see <code>selection.summary()</code>.</td></tr>
5016
+ <tr><td class="name">range:changed</td><td class="type">{ ranges }</td><td class="desc">A status bar showing the sum of what is selected: see <code>selection.summary()</code>.</td></tr>
5017
5017
  <tr><td class="name">sort:changed / filter:changed</td><td class="type">{ sort } / { filters }</td><td class="desc">Reflecting the view in the URL.</td></tr>
5018
5018
  <tr><td class="name">history:changed</td><td class="type">{ canUndo, canRedo, undo, redo }</td><td class="desc">Driving your own undo button. Emitted <em>after</em> the entry is pushed, so the label is right.</td></tr>
5019
5019
  <tr><td class="name">history:applied</td><td class="type">{ direction, step }</td><td class="desc">An action was undone or redone, with which and what. <code>history:changed</code> also fires when a new action is pushed, so it cannot distinguish the two.</td></tr>
@@ -5032,8 +5032,8 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
5032
5032
  watermark; that is the whole of what it does.
5033
5033
  </p>
5034
5034
  <p class="lead-in">
5035
- Free to develop against, licensed to deploy. A grid on <strong>localhost</strong> — or any
5036
- loopback host — needs no key at all. On any other domain an unlicensed grid still renders
5035
+ Free to develop against, licensed to deploy. A grid on <strong>localhost</strong>, or any
5036
+ loopback host: needs no key at all. On any other domain an unlicensed grid still renders
5037
5037
  everything and carries a small trial watermark linking to latticegrid.dev.
5038
5038
  </p>
5039
5039
  <div class="table-wrap">
@@ -5052,24 +5052,24 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
5052
5052
  grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'</span></code></pre>
5053
5053
  </div>
5054
5054
  <p class="lead-in">
5055
- Call it once, before creating a grid. Setting a key later still works — the watermark comes
5056
- off and <code>licence:changed</code> fires — but the first frames of the grid will carry it.
5055
+ Call it once, before creating a grid. Setting a key later still works, the watermark comes
5056
+ off and <code>licence:changed</code> fires, but the first frames of the grid will carry it.
5057
5057
  </p>
5058
5058
  <p class="lead-in">
5059
5059
  Keys come from <a href="https://www.latticegrid.dev">latticegrid.dev</a> and are issued per
5060
5060
  deployment rather than per developer or per seat: name the domains the grid will run on and
5061
5061
  one key covers every developer, every build and every user on them. A key names the domains
5062
- it covers as you would expect — <code>*.acme.com</code> matches <code>app.acme.com</code>,
5062
+ it covers as you would expect: <code>*.acme.com</code> matches <code>app.acme.com</code>,
5063
5063
  <code>a.b.acme.com</code> and <code>acme.com</code> itself.
5064
5064
  </p>
5065
5065
  <p class="lead-in">
5066
5066
  Checking a key needs no network. There is no licence server, no call home, and nothing that
5067
- can fail at three in the morning — a key carries its own answer and the grid reads it
5067
+ can fail at three in the morning, a key carries its own answer and the grid reads it
5068
5068
  locally, so a grid on an air-gapped network behaves exactly like one on the open internet.
5069
5069
  </p>
5070
5070
  <div class="why">
5071
5071
  <p><strong>Nothing ever refuses to render, and nothing is ever withheld.</strong> An expired
5072
- key, a wrong domain, a key that will not read — all of them log one warning and show the
5072
+ key, a wrong domain, a key that will not read: all of them log one warning and show the
5073
5073
  watermark. Every feature keeps working. The failure to avoid is a customer's production
5074
5074
  screen going blank because a licence lapsed over a weekend, and a grid that quietly drops a
5075
5075
  feature is the same failure wearing a disguise.</p>
@@ -5127,7 +5127,7 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'<
5127
5127
  <footer>
5128
5128
  <p>
5129
5129
  Lattice Grid 1.7.1 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
5130
- Written against the shipped source. Where this guide and the code disagree, the code wins —
5130
+ Written against the shipped source. Where this guide and the code disagree, the code wins ,
5131
5131
  please <a href="https://www.latticegrid.dev">tell us</a>.
5132
5132
  </p>
5133
5133
  </footer>