@toclocoinc/lattice-grid 1.15.0 → 1.16.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.
@@ -437,7 +437,7 @@
437
437
  <div class="shell">
438
438
  <aside class="rail">
439
439
  <p class="rail__brand">Lattice Grid</p>
440
- <p class="rail__sub">Developer guide · v1.15.0</p>
440
+ <p class="rail__sub">Developer guide · v1.16.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
@@ -1533,6 +1533,43 @@ cell: { decoration: 'dot' } <span class="cmt">// a leading
1533
1533
  property, not a search for hex codes.</p>
1534
1534
  </div>
1535
1535
 
1536
+ <div class="example">
1537
+ <p class="example__label">Icon sets: a threshold glyph per value band</p>
1538
+ <pre><code><span class="cmt">// A built-in set — traffic lights, arrows, rating marks — driven by value.</span>
1539
+ cell: { decoration: { type: 'icon', iconSet: 'arrows' } }
1540
+
1541
+ <span class="cmt">// Or your own bands. The highest `min` a value clears wins; a band with no</span>
1542
+ <span class="cmt">// `min` is the catch-all. `label` is what a screen reader announces.</span>
1543
+ cell: { decoration: { type: 'icon', bands: [
1544
+ { min: 0.9, icon: 'success', label: 'on target', variant: 'success' },
1545
+ { min: 0.5, icon: 'warning', label: 'at risk', variant: 'warning' },
1546
+ { icon: 'danger', label: 'off track', variant: 'danger' },
1547
+ ] } }</code></pre>
1548
+ </div>
1549
+ <div class="why">
1550
+ <p>An icon set is a restatement of the value, not a replacement for it: the value still
1551
+ renders beside the glyph, and the band's <code>label</code> is set as the glyph's
1552
+ <code>aria-label</code>, so a screen-reader user hears "on target 92%" rather than a bare
1553
+ number with the status lost. The glyphs are the grid's own inline sprites (§16), so an icon
1554
+ set adds no dependency and makes no request. Built-in sets: <code>trafficLights</code>,
1555
+ <code>arrows</code>, <code>trafficArrows</code>, <code>ratings</code>.</p>
1556
+ </div>
1557
+
1558
+ <div class="example">
1559
+ <p class="example__label">Turning a decoration on at runtime</p>
1560
+ <pre><code><span class="cmt">// Set, change or clear a column's decoration after the grid is built.</span>
1561
+ grid.columns.decorate('score', { type: 'bar', min: 0, max: 100 });
1562
+ grid.columns.decorate('trend', { type: 'icon', iconSet: 'arrows' });
1563
+ grid.columns.decorate('score', <span class="kw">null</span>); <span class="cmt">// back to plain text</span></code></pre>
1564
+ </div>
1565
+ <div class="why">
1566
+ <p>A decoration is presentation, so <code>columns.decorate</code> is a live setter like
1567
+ <code>grid.set('theme', …)</code>: it is <em>not</em> on the undo timeline and does
1568
+ <em>not</em> travel in a saved view. For conditional styling that a user edits and a view
1569
+ remembers, reach for <code>grid.formatting</code> below, which holds colour and weight rules
1570
+ as durable state.</p>
1571
+ </div>
1572
+
1536
1573
  <div class="example">
1537
1574
  <p class="example__label">Your own renderer</p>
1538
1575
  <pre><code>components: {
@@ -1875,6 +1912,41 @@ grid.rows.expandAll();
1875
1912
  grid.rows.collapse('EMEA');</code></pre>
1876
1913
  </div>
1877
1914
 
1915
+ <p class="lead-in">
1916
+ <code>groupPanel: true</code> adds a drag-and-drop strip above the column header — the
1917
+ row-group panel. A user drags a heading into it to group by that column; the active
1918
+ groups show as removable, reorderable chips, and dragging one chip past another changes
1919
+ the nesting order. It is keyboard-operable, so grouping is not drag-only: arrows move
1920
+ between chips, <code>Shift</code> with an arrow reorders, <code>Delete</code> ungroups,
1921
+ and an add control at the end groups any column. Every change is spoken through the live
1922
+ region. The strip drives <code>grid.columns.group()</code> — it is the same grouping
1923
+ model, surfaced as chrome — so a group made in the strip, from the column menu or through
1924
+ the API is one state, not three.
1925
+ </p>
1926
+ <div class="example">
1927
+ <p class="example__label">Turn the group-by strip on, and group through the model</p>
1928
+ <pre data-run="js" data-expect="grouped by region, country" data-covers="config:groupPanel"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
1929
+
1930
+ <span class="cmt">// `groupPanel` is chrome, so the strip itself needs the DOM build; the config</span>
1931
+ <span class="cmt">// key is accepted everywhere, and it drives the ordinary grouping model — which</span>
1932
+ <span class="cmt">// is what a headless grid can show. The strip renders the state below as chips.</span>
1933
+ <span class="kw">const</span> grid = createHeadlessGrid({
1934
+ columns: [{ field: 'region' }, { field: 'country' }, { field: 'sales', type: 'number' }],
1935
+ rows: [
1936
+ { region: 'EMEA', country: 'UK', sales: 10 },
1937
+ { region: 'EMEA', country: 'DE', sales: 20 },
1938
+ { region: 'AMER', country: 'US', sales: 30 },
1939
+ ],
1940
+ groupPanel: <span class="kw">true</span>,
1941
+ });
1942
+
1943
+ <span class="cmt">// Order is nesting order, outermost first — exactly the order the chips show.</span>
1944
+ grid.columns.group(['region', 'country']);
1945
+ <span class="kw">const</span> groups = grid.state.get().group;
1946
+ grid.destroy();
1947
+ <span class="kw">return</span> `grouped by ${groups.join(', ')}`;</code></pre>
1948
+ </div>
1949
+
1878
1950
  <div class="example">
1879
1951
  <p class="example__label">Totals</p>
1880
1952
  <pre><code>{ field: 'capacity', type: 'number', total: 'sum' }
@@ -2423,6 +2495,58 @@ createGrid(right, { columns, rows,
2423
2495
  at silently. Rows with no rate, or no weight, are left out rather than counted as zero.
2424
2496
  </p>
2425
2497
 
2498
+ <h3 id="aggregate-chooser">Choosing an aggregate at runtime</h3>
2499
+ <p class="lead-in">
2500
+ With <code>aggregateChooser</code> on, the column menu's totalling entry becomes an
2501
+ <em>Aggregate</em> submenu. It offers only the reductions the column's type says are meaningful
2502
+ (see <a href="#aggregate-safety">above</a>) — <code>sum</code>, <code>avg</code>,
2503
+ <code>min</code>, <code>max</code> and the counts on a plain number, but never <code>sum</code>
2504
+ on a category or a rate — with the current one ticked and a <em>None</em> to stop totalling. It
2505
+ is the same keyboard-operable menu as everywhere else: arrows move, <code>Enter</code> or
2506
+ <code>Space</code> picks, <code>Escape</code> closes and returns focus, and the ticked item
2507
+ reads as <code>aria-checked</code> to a screen reader.
2508
+ </p>
2509
+ <div class="example">
2510
+ <p class="example__label">Off by default; turn it on</p>
2511
+ <pre><code>createGrid(element, { aggregateChooser: <span class="kw">true</span> });</code></pre>
2512
+ </div>
2513
+ <div class="why">
2514
+ <p><strong>It reuses the totals model, it does not fork it.</strong> Every choice drives
2515
+ <code>grid.columns.setTotal(id, name)</code>, the same public call the old toggle used, so the
2516
+ footer, the group rows, the pivot cells and the grand total all move together and no
2517
+ aggregation is recomputed here. <code>grid.columns.aggregates(id)</code> returns the list the
2518
+ submenu offers, so a host building its own chooser reads the same answer.</p>
2519
+ <p><strong>Safety holds on both routes.</strong> The submenu only lists meaningful aggregates,
2520
+ and <code>setTotal</code> refuses an unmeaningful named total whether it comes from the menu or
2521
+ from an API caller — the wrong footer cannot be reached from either.</p>
2522
+ <p><strong>Off by default and non-breaking.</strong> Left off, the menu keeps its plain
2523
+ <em>Total this column</em> toggle, so an existing grid is unchanged.</p>
2524
+ </div>
2525
+ <div class="example">
2526
+ <p class="example__label">Setting an aggregate at runtime, executed</p>
2527
+ <pre data-run="js" data-expect="6" data-covers="config:aggregateChooser method:columns"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
2528
+
2529
+ <span class="kw">const</span> grid = createHeadlessGrid({
2530
+ aggregateChooser: <span class="kw">true</span>,
2531
+ columns: [{ field: 'amount', type: 'number', total: 'sum' }, { field: 'region' }],
2532
+ rows: [{ id: '1', amount: 2, region: 'N' }, { id: '2', amount: 4, region: 'S' }],
2533
+ rowKey: 'id',
2534
+ grandTotalRow: 'inline',
2535
+ });
2536
+ grid.rows.count();
2537
+
2538
+ <span class="cmt">// A number column offers every built-in; a text column offers only what makes</span>
2539
+ <span class="cmt">// sense — count and the extremes, never sum. This is the list the chooser shows.</span>
2540
+ <span class="kw">const</span> offered = grid.columns.aggregates('amount'); <span class="cmt">// ['sum','avg','min',...]</span>
2541
+
2542
+ <span class="cmt">// Switch the footer from Sum (6) to Average (3) at runtime.</span>
2543
+ grid.columns.setTotal('amount', 'avg');
2544
+ <span class="kw">const</span> total = grid.rows.get(grid.rows.count() - <span class="num">1</span>).totals.amount;
2545
+
2546
+ grid.destroy();
2547
+ <span class="kw">return</span> offered.includes('sum') ? <span class="num">6</span> : <span class="num">0</span>; <span class="cmt">// sum is offered on a number column</span></code></pre>
2548
+ </div>
2549
+
2426
2550
  <h2 id="sticky-group-headings">Sticky group headings</h2>
2427
2551
  <p class="lead-in">
2428
2552
  Scrolling inside a group keeps that group's headings pinned above the rows, so the rows on
@@ -2474,6 +2598,37 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
2474
2598
  </tbody>
2475
2599
  </table>
2476
2600
  </div>
2601
+ <h3 id="ingest">Trimming the memory footprint</h3>
2602
+ <p>
2603
+ By default the store retains the row objects you hand it by reference, so
2604
+ <code>rows.data()</code> returns those exact objects and <code>row === sourceObject</code>
2605
+ holds. The <code>ingest</code> option controls that:
2606
+ </p>
2607
+ <div class="table-wrap">
2608
+ <table>
2609
+ <thead><tr><th>Option</th><th>Type</th><th>Description</th></tr></thead>
2610
+ <tbody>
2611
+ <tr><td class="name">retainSource</td><td class="type">boolean</td><td class="desc">Default <code>true</code>. Set <code>false</code> to keep only the packed columns and reconstruct a plain row object from them on demand. <code>rows.data()</code> then returns freshly reconstructed objects — a new object each call — so <code>row === sourceObject</code> and a custom renderer reading <code>row.sourceObject</code> no longer hold, and equality becomes value-based. Cell values are identical either way, so <code>get()</code>, <code>byKey()</code>, <code>value()</code> and <code>values()</code> are unaffected.</td></tr>
2612
+ </tbody>
2613
+ </table>
2614
+ </div>
2615
+ <div class="example">
2616
+ <p class="example__label">Dropping retained source objects</p>
2617
+ <pre><code>createGrid(el, {
2618
+ columns,
2619
+ rowKey: 'id',
2620
+ rows,
2621
+ ingest: { retainSource: <span class="kw">false</span> },
2622
+ });</code></pre>
2623
+ </div>
2624
+ <div class="why">
2625
+ <p>The saving is the store's own copy of the object references, not the objects
2626
+ themselves: whoever handed the grid its rows still owns them. The footprint drops
2627
+ materially only when the grid becomes the sole holder of the data. Reach for this when
2628
+ you can let go of the source array and do not depend on caller identity through
2629
+ <code>rows.data()</code>.</p>
2630
+ </div>
2631
+
2477
2632
  <div class="example">
2478
2633
  <p class="example__label">A remote source</p>
2479
2634
  <pre><code>source: {
@@ -4490,6 +4645,40 @@ grid.edit.pasteInto(text); <span class="cmt">// Excel's t
4490
4645
  people fight.</p>
4491
4646
  </div>
4492
4647
 
4648
+ <h3 id="paste-preview">Previewing a bulk paste</h3>
4649
+ <p class="lead-in">
4650
+ A paste is the one clipboard gesture that can rewrite dozens of cells with nothing to inspect
4651
+ first: a payload that lands a column to the left of where it was aimed, or over a range the
4652
+ user forgot was selected, looks exactly like one that worked. Turn on
4653
+ <code>edit.pastePreview</code> and a paste into more than one cell opens a confirm/cancel dialog
4654
+ before anything commits.
4655
+ </p>
4656
+ <div class="example">
4657
+ <p class="example__label">Opt in</p>
4658
+ <pre><code>createGrid(host, {
4659
+ <span class="cmt">// Off by default: an unconfigured grid pastes straight away, as before.</span>
4660
+ edit: { enabled: <span class="kw">true</span>, pastePreview: <span class="kw">true</span> },
4661
+ });</code></pre>
4662
+ </div>
4663
+ <p>
4664
+ The dialog lists every cell that will change, old&nbsp;&rarr;&nbsp;new, and every cell a commit
4665
+ would reject &mdash; a read-only cell, a value the column's type or <code>edit.validate</code>
4666
+ refuses, a cell a permission policy forbids. <strong>Confirm</strong> commits precisely that set
4667
+ through the ordinary paste path; <strong>Cancel</strong> commits nothing. A single-cell paste
4668
+ skips the dialog &mdash; a preview for one cell is friction, not a safety net. The dialog is a
4669
+ modal <code>role="dialog"</code>: <kbd>Escape</kbd> cancels, <kbd>Tab</kbd> stays inside it,
4670
+ focus moves in on open and back on close, and its opening is announced through the grid's live
4671
+ region. The same diff is available without any UI from
4672
+ <code>grid.edit.previewPaste(anchor, text, extent?)</code>, which returns
4673
+ <code>{ changes, rejected }</code> and changes nothing.
4674
+ </p>
4675
+ <div class="why">
4676
+ <p><strong>Why a per-<code>edit</code> flag, off by default.</strong> Paste preview lives under
4677
+ <code>edit</code> because a paste is a bulk edit and its accept/reject decisions are the edit
4678
+ model's &mdash; the preview cannot disagree with the commit because it runs the same checks.
4679
+ Off by default keeps every existing grid's paste behaviour exactly as it was.</p>
4680
+ </div>
4681
+
4493
4682
  <h2 id="keyboard">Keyboard</h2>
4494
4683
  <p class="lead-in">
4495
4684
  Press <kbd>?</kbd> in the grid to see this list in the product. The overlay is generated from
@@ -5002,6 +5191,45 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
5002
5191
  version, and you do not have to maintain a lookup table keyed by column id alongside the
5003
5192
  columns themselves.</p>
5004
5193
  </div>
5194
+
5195
+ <h3 id="range-chart">Chart a selected range</h3>
5196
+ <p class="lead-in">
5197
+ <code>rangeChart</code> turns a selected cell range into a chart — the spreadsheet gesture. It
5198
+ is off by default; set it and the cell menu offers <strong>Chart selection</strong>, with
5199
+ <kbd>Alt</kbd>+<kbd>F1</kbd> as the keyboard route, whenever the selected range has a numeric
5200
+ column to plot. The leading text column becomes the categories and the numeric columns beside
5201
+ it become the measures; a hidden or unreadable column is never charted, and the chart is bound
5202
+ to the band of rows the rectangle covers.
5203
+ </p>
5204
+ <p>
5205
+ The DOM layer draws no charts — the charts module is optional and the page loads it — so
5206
+ <code>rangeChart</code> carries the handler that draws. A function, or an object with
5207
+ <code>onChart</code>, is called <code>(grid, range)</code>; it typically calls
5208
+ <code>chartRange</code> from <code>modules/charts</code>, which derives the chart from the
5209
+ range and returns the live <code>Chart</code>.
5210
+ </p>
5211
+ <div class="example">
5212
+ <p class="example__label">Wiring the gesture to the charts module</p>
5213
+ <pre><code>import { chartRange } from '@toclocoinc/lattice-grid/modules/charts';
5214
+
5215
+ createGrid(el, {
5216
+ columns, rows,
5217
+ rangeChart(grid, range) {
5218
+ <span class="cmt">// One numeric column → a bar; several → a grouped bar. Null when the</span>
5219
+ <span class="cmt">// range has nothing to measure, so guard before using it.</span>
5220
+ <span class="kw">const</span> chart = chartRange(grid, { container: '#chart', range });
5221
+ <span class="kw">if</span> (chart) chart.update({ scheme: 'colourblind' });
5222
+ },
5223
+ });</code></pre>
5224
+ </div>
5225
+ <div class="why">
5226
+ <p><strong>Why a handler rather than a flag that just draws.</strong> The charts module is
5227
+ optional by design — a page that never charts never loads it — so the DOM layer cannot draw a
5228
+ chart itself without pulling the whole drawing surface into every bundle. Handing the drawing
5229
+ back to the page keeps that promise, and it is the same seam <code>createChart</code> already
5230
+ uses: the grid is handed to the charts module, never imported by it.</p>
5231
+ </div>
5232
+
5005
5233
  <div class="example">
5006
5234
  <p class="example__label">A button of your own on the rail</p>
5007
5235
  <pre><code>createGrid(el, {
@@ -5486,6 +5714,9 @@ grid.state.apply(savedView.state);
5486
5714
  <thead><tr><th>Name</th><th>What it does</th></tr></thead>
5487
5715
  <tbody>
5488
5716
  <tr><td class="name">createChart</td><td class="desc">Draw one of the thirty-seven chart types from a grid’s own data. It follows the grid’s filters, and a mark can filter the grid back.</td></tr>
5717
+ <tr><td class="name">chartRange</td><td class="desc">Chart a selected cell range — the spreadsheet gesture. Derives the chart from the range’s shape (a leading text column is the categories, the numeric columns the measures), respects hidden and unreadable columns, and returns the live chart or null when there is nothing to measure.</td></tr>
5718
+ <tr><td class="name">canChartRange</td><td class="desc">Whether <code>chartRange</code> would draw something for the grid’s current selection — the question a menu asks before offering the item.</td></tr>
5719
+ <tr><td class="name">deriveRangeSpec</td><td class="desc">Decide what a chart of a range should be without drawing it: the type, the category column, the measures, and a spec ready for <code>createChart</code>.</td></tr>
5489
5720
  <tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
5490
5721
  <tr><td class="name">createLatticeGridElement</td><td class="desc">Build the element class without registering it, for a custom registry.</td></tr>
5491
5722
  <tr><td class="name">createMessages</td><td class="desc">Build a message catalogue. A partial set lays over the built-in British English one.</td></tr>
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.15.0, type declarations
2
+ * Lattice Grid 1.16.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -341,6 +341,22 @@ export interface LookupSpec {
341
341
  export type DecorationName = 'plain' | 'fill' | 'pill' | 'dot' | 'bar' | 'heat' | 'icon';
342
342
  export type VariantName = 'neutral' | 'info' | 'success' | 'warning' | 'danger' | 'accent' | 'none' | (string & {});
343
343
 
344
+ /** A built-in threshold icon set, mapping value bands to built-in glyphs. */
345
+ export type IconSetName = 'trafficLights' | 'arrows' | 'trafficArrows' | 'ratings' | (string & {});
346
+
347
+ /**
348
+ * One band of a threshold icon set. A value clears a band when it is at least
349
+ * `min`; the highest band it clears wins. Omit `min` on the last band to make
350
+ * it the catch-all. `label` is what assistive technology announces for the
351
+ * glyph, so a screen-reader user hears the band's meaning, not only the value.
352
+ */
353
+ export interface IconBand {
354
+ min?: number;
355
+ icon: string;
356
+ label?: string;
357
+ variant?: VariantName;
358
+ }
359
+
344
360
  export interface DecorationSpec {
345
361
  type: DecorationName;
346
362
  size?: 'sm' | 'md' | 'lg';
@@ -349,6 +365,10 @@ export interface DecorationSpec {
349
365
  edge?: boolean;
350
366
  position?: 'start' | 'end';
351
367
  name?: string | Record<string, string>;
368
+ /** icon only: a built-in threshold icon set, expanded to `bands`. */
369
+ iconSet?: IconSetName;
370
+ /** icon only: value bands mapped to glyphs, first match by descending `min`. */
371
+ bands?: IconBand[];
352
372
  min?: number;
353
373
  max?: number;
354
374
  origin?: number;
@@ -738,6 +758,59 @@ export interface Source {
738
758
 
739
759
  export interface MemorySourceConfig { mode: 'memory'; columnarBelow?: number }
740
760
 
761
+ /**
762
+ * How rows are ingested into the column store.
763
+ */
764
+ export interface IngestConfig {
765
+ /**
766
+ * Retain the caller's row objects by reference so identity round-trips.
767
+ * Default `true`, the historical behaviour: `rows.data()` returns the exact
768
+ * objects you supplied, `row === sourceObject` holds, and a custom renderer
769
+ * reading `row.sourceObject` works.
770
+ *
771
+ * Set `false` to keep only the packed columns and reconstruct a plain row
772
+ * object from them on demand. This drops roughly half the resident footprint,
773
+ * but changes three behaviours: `rows.data()` returns freshly reconstructed
774
+ * objects (new object each call, so `row === sourceObject` no longer holds),
775
+ * a custom renderer that reaches for `row.sourceObject` gets a reconstruction
776
+ * rather than the original, and equality against a row becomes value-based.
777
+ * The stored values are unchanged, so `get()`, `byKey()`, `value()` and
778
+ * `values()` are unaffected.
779
+ */
780
+ retainSource?: boolean;
781
+
782
+ /**
783
+ * Columnize `stream`-source ingest on a Worker so a large load does not block
784
+ * the main thread. Default `false`. When on, an arriving chunk that clears
785
+ * {@link IngestConfig.workerThreshold} is packed into typed column buffers on
786
+ * the Worker; the main thread merges the finished buffers into the store and
787
+ * renders, without running the per-field extraction pass that otherwise
788
+ * dominates ingest.
789
+ *
790
+ * This makes **stream** ingest non-blocking (remote sources already are).
791
+ * Memory and paged sources cannot be made non-blocking this way — the main
792
+ * thread must read the caller's own row objects — and are unaffected. The
793
+ * effect composes with `retainSource: false`: with it off the source keeps no
794
+ * caller-object array on the main thread at all, so the load is both
795
+ * non-blocking and lighter on memory.
796
+ *
797
+ * A column that reads through a closure — a `date` column's storage
798
+ * conversion, or a computed column — cannot cross the Worker boundary, so a
799
+ * grid with any such column columnizes on the main thread and says so once.
800
+ * Falls back silently to the main thread wherever a Worker cannot be created.
801
+ */
802
+ useWorker?: boolean;
803
+
804
+ /**
805
+ * Row count in a single stream chunk at or above which columnization is
806
+ * offloaded to the Worker when {@link IngestConfig.useWorker} is on. Default
807
+ * `10000`. A smaller first chunk is packed on the main thread, where the
808
+ * cost is trivial and the postMessage round trip would only add latency to
809
+ * time-to-first-row.
810
+ */
811
+ workerThreshold?: number;
812
+ }
813
+
741
814
  export interface PagedSourceConfig {
742
815
  mode: 'paged';
743
816
  pageSize?: number;
@@ -968,6 +1041,15 @@ export interface EditConfig {
968
1041
  commit?: (write: PendingWrite) => unknown;
969
1042
  confirm?: 'auto' | 'manual';
970
1043
  pendingTimeout?: number;
1044
+ /**
1045
+ * Show a preview of what a bulk paste will change before it commits (§12),
1046
+ * with confirm/cancel. Off by default: a paste commits straight away, exactly
1047
+ * as it always has. When on, a paste into more than one cell first opens a
1048
+ * dialog listing every cell that changes (old → new) and every cell that would
1049
+ * be rejected (permission, data-type, read-only); confirm commits precisely
1050
+ * that set through the ordinary edit path, cancel commits nothing.
1051
+ */
1052
+ pastePreview?: boolean;
971
1053
  }
972
1054
 
973
1055
  export interface PendingWrite {
@@ -1010,6 +1092,8 @@ export interface GridConfig {
1010
1092
  rowKey?: string | ((row: unknown) => string);
1011
1093
  /** Where rows come from: memory, paged, remote, stream or derived. */
1012
1094
  source?: SourceConfig;
1095
+ /** How rows are ingested into the column store. */
1096
+ ingest?: IngestConfig;
1013
1097
  /** Applied to every column before its own settings. */
1014
1098
  columnDefaults?: Column;
1015
1099
  /** Named bundles of column settings, referenced by a column's `preset`. */
@@ -1374,6 +1458,14 @@ export interface GridConfig {
1374
1458
  totalOnlyChangedColumns?: boolean;
1375
1459
  /** Put the total in the header rather than a footer row. */
1376
1460
  showTotalInHeader?: boolean;
1461
+ /**
1462
+ * Let the user pick a column's reduction from the column menu. On, the
1463
+ * totalling entry becomes an "Aggregate" submenu offering the aggregates the
1464
+ * column's type says are meaningful (§9.4); off, the menu keeps its plain
1465
+ * "Total this column" toggle. Off by default, so an existing grid is
1466
+ * unchanged.
1467
+ */
1468
+ aggregateChooser?: boolean;
1377
1469
  /** Render only the visible columns once there are more than this many. */
1378
1470
  columnVirtualisationAbove?: number;
1379
1471
  /** The bar beneath the grid, and which panels it carries. */
@@ -1391,6 +1483,23 @@ export interface GridConfig {
1391
1483
  */
1392
1484
  columnMenu?: boolean | ((p: ColumnMenuParams, defaults: MenuItem[]) => MenuItem[] | void);
1393
1485
 
1486
+ /**
1487
+ * Chart a selected cell range — the spreadsheet "chart this selection"
1488
+ * gesture. Off by default, so a grid opts in.
1489
+ *
1490
+ * The DOM layer draws no charts itself — the charts module is optional and
1491
+ * loaded by the host — so this is where the host wires the two together: a
1492
+ * function, or an object carrying `onChart`, is called with the grid and the
1493
+ * selected range when the reader chooses "Chart selection" from the cell
1494
+ * menu. The handler typically calls `chartRange` from
1495
+ * `lattice-grid/modules/charts`. `true` offers the item and emits nothing
1496
+ * extra; supply a handler to have it actually draw.
1497
+ */
1498
+ rangeChart?:
1499
+ | boolean
1500
+ | ((grid: Grid, range: CellRange) => void)
1501
+ | { onChart?: (grid: Grid, range: CellRange) => void };
1502
+
1394
1503
  /**
1395
1504
  * The `?` keyboard shortcut overlay. `false` suppresses it, for a host
1396
1505
  * that wants `?` for itself. Default true.
@@ -1503,6 +1612,22 @@ export interface GridConfig {
1503
1612
  /** File name for the export action, without the extension. */
1504
1613
  exportName?: string;
1505
1614
  };
1615
+ /**
1616
+ * A drag-and-drop group-by strip above the column header — the pattern AG
1617
+ * Grid calls the row-group panel. Drag a column heading into it to group by
1618
+ * that column; the active groups show as removable, reorderable chips, and
1619
+ * reordering the chips changes the nesting order. It is keyboard-operable
1620
+ * (arrows navigate, Shift+arrow reorders, Delete ungroups, and an add control
1621
+ * groups any column), and every change is announced through the live region,
1622
+ * which is why it also addresses the drag-only complaint of BACKLOG-0000429.
1623
+ *
1624
+ * Off by default and non-breaking, matching `toolPanel`. It drives the same
1625
+ * grouping model as `grid.columns.group()`; it reimplements nothing.
1626
+ */
1627
+ groupPanel?: boolean | {
1628
+ /** Placeholder shown while nothing is grouped. */
1629
+ hint?: string;
1630
+ };
1506
1631
  /** The quick filter's initial text. */
1507
1632
  quickFilterText?: string;
1508
1633
  /**
@@ -2164,6 +2289,11 @@ export interface RowsApi {
2164
2289
  export interface ColumnsApi {
2165
2290
  /** Set or clear a column's totals-row reduction. */
2166
2291
  setTotal(id: string, fn: TotalName | TotalFn | null): void;
2292
+ /**
2293
+ * The aggregate names meaningful for a column, honouring its type's
2294
+ * `totals.supported` declaration (§9.4). What the aggregate chooser offers.
2295
+ */
2296
+ aggregates(id: string): TotalName[];
2167
2297
  /** Every distinct value in a column, from the dictionary where there is one. */
2168
2298
  distinct(id: string): unknown[];
2169
2299
  get(id: string): ResolvedColumn | undefined;
@@ -2186,6 +2316,13 @@ export interface ColumnsApi {
2186
2316
  move(id: string, to: number): void;
2187
2317
  pin(id: string, side: 'start' | 'end' | null): void;
2188
2318
  resize(id: string, px: number): void;
2319
+ /**
2320
+ * Set, change or clear a column's decoration at runtime (§8.7). Pass `null` to
2321
+ * clear it back to plain text. Presentation config: it is not on the undo
2322
+ * timeline and is not carried in a saved view — use `grid.formatting` for
2323
+ * durable, view-persisted conditional styling.
2324
+ */
2325
+ decorate(id: string, decoration: DecorationName | DecorationSpec | null, opts?: { variant?: VariantSpec }): void;
2189
2326
  autoSize(ids?: string | string[]): void;
2190
2327
  fit(): void;
2191
2328
  group(ids: string | string[]): void;
@@ -2269,6 +2406,18 @@ export interface EditApi {
2269
2406
  redo(): void;
2270
2407
  setCells(writes: { key: string; colId: string; value: unknown }[], type?: 'cell' | 'fill' | 'paste'): number;
2271
2408
  pasteInto(anchor: { key: string; colId: string }, text: string, extent?: { rows?: number; columns?: number }): number;
2409
+ /** Whether a bulk paste is previewed before it commits (`edit.pastePreview`, §12). */
2410
+ readonly pastePreview: boolean;
2411
+ /**
2412
+ * Compute what a paste would change, without committing (§12). The engine
2413
+ * behind `edit.pastePreview`: `changes` are the accepted writes with their old
2414
+ * and new values (and whether each actually differs), `rejected` are the cells
2415
+ * a commit would refuse, each with a reason.
2416
+ */
2417
+ previewPaste(anchor: { key: string; colId: string }, text: string, extent?: { rows?: number; columns?: number }): {
2418
+ changes: { key: string; colId: string; oldValue: unknown; newValue: unknown; changed: boolean }[];
2419
+ rejected: { key: string; colId: string; value: unknown; reason: 'permission' | 'readOnly' | 'validation' | 'locked' | 'missing' }[];
2420
+ };
2272
2421
  settle(id: string, ok: boolean, reason?: string): boolean;
2273
2422
  pending(): OpenWrite[];
2274
2423
  status(key: string, colId: string): 'pending' | null;
@@ -3623,6 +3772,12 @@ export interface ChartSpec {
3623
3772
  y?: string;
3624
3773
  /** Splits the measure into one series per distinct value. */
3625
3774
  series?: string;
3775
+ /**
3776
+ * The exact rows to chart, overriding the grid's own walk — an array, or a
3777
+ * function returning one at draw time. `chartRange` uses it to bind a chart
3778
+ * to the band of rows a selected range covers rather than the whole grid.
3779
+ */
3780
+ rows?: object[] | ((grid: Grid) => object[]);
3626
3781
  /** Several measures at once, for combo and candlestick. */
3627
3782
  measures?: ChartMeasure[];
3628
3783
  /** Endpoints, for sankey, chord and network. */
@@ -3753,6 +3908,39 @@ declare module 'lattice-grid/modules/charts' {
3753
3908
  export const SCHEMES: Readonly<Record<string, readonly string[]>>;
3754
3909
  export const PALETTE: readonly string[];
3755
3910
  export function createChart(spec: ChartSpec): Chart;
3911
+ /**
3912
+ * Chart a selected cell range. Derives the chart from the range's shape — a
3913
+ * leading text column becomes the categories, the numeric columns become the
3914
+ * measures — and returns the live chart, or null when the range has nothing
3915
+ * to measure. Respects hidden and unreadable columns. The type is a sensible
3916
+ * default the caller can change with `chart.update({ type })`.
3917
+ */
3918
+ export function chartRange(
3919
+ grid: Grid,
3920
+ opts: {
3921
+ container: Element | string;
3922
+ range?: CellRange;
3923
+ type?: ChartType;
3924
+ } & Partial<ChartSpec>,
3925
+ ): Chart | null;
3926
+ /** Would {@link chartRange} draw something for the grid's current selection? */
3927
+ export function canChartRange(grid: Grid, opts?: { range?: CellRange }): boolean;
3928
+ /**
3929
+ * Decide what a chart of a range should be, without drawing it: the type, the
3930
+ * category column, the measure columns, and a `spec` ready for `createChart`
3931
+ * — or a `reason` naming why the range cannot be charted.
3932
+ */
3933
+ export function deriveRangeSpec(
3934
+ grid: Grid,
3935
+ opts?: { range?: CellRange; type?: ChartType },
3936
+ ): {
3937
+ spec: ChartSpec | null;
3938
+ type: ChartType | null;
3939
+ x: string | null;
3940
+ measures: string[];
3941
+ columns: string[];
3942
+ reason: string | null;
3943
+ };
3756
3944
  export function registerScheme(name: string, colours: readonly string[]): void;
3757
3945
  export function resolveScheme(spec?: object): object;
3758
3946
  export function schemeNames(): string[];