@toclocoinc/lattice-grid 1.22.0 → 1.24.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.22.0</p>
440
+ <p class="rail__sub">Developer guide · v1.24.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
@@ -1305,6 +1305,7 @@ LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid
1305
1305
  <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>
1306
1306
  <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>
1307
1307
  <tr><td class="name">Temperature</td><td class="desc"><code>celsius</code>, <code>fahrenheit</code>, <code>kelvin</code></td></tr>
1308
+ <tr><td class="name">Currency</td><td class="desc"><code>currency</code>, <code>usd</code>, <code>eur</code>, <code>gbp</code>, <code>jpy</code></td></tr>
1308
1309
  <tr><td class="name">Structured</td><td class="desc"><code>json</code>, <code>colour</code>, <code>rating</code>, <code>percent</code></td></tr>
1309
1310
  </tbody>
1310
1311
  </table>
@@ -1392,6 +1393,15 @@ dataTypes: {
1392
1393
  <code>kelvin</code> convert on input: type <code>72 F</code> into a Celsius column and it
1393
1394
  stores 22.2, and <strong>refuse to be summed</strong>: twenty degrees plus twenty degrees is
1394
1395
  not forty degrees, and a footer saying so would be believed.</p>
1396
+ <p><strong>Currency is its own type, not a unit either.</strong> A currency's
1397
+ &ldquo;factor&rdquo; is an exchange rate that moves, so a value carries an amount
1398
+ <em>and</em> a code and never a fixed factor. The grid ships and fetches no rates: pass a
1399
+ rate source through <code>createCurrencyType({ display, rates })</code>, and a rate that is
1400
+ needed but absent renders as a loud marker (<code>missingRate</code>), never as zero. A
1401
+ column totalling in a display currency refuses to add unlike currencies until every value
1402
+ can reach that currency. The shipped <code>currency</code>, <code>usd</code>,
1403
+ <code>eur</code>, <code>gbp</code> and <code>jpy</code> types cover the single-currency case;
1404
+ a rate table is denominated in a base you can state with <code>rateBase</code>.</p>
1395
1405
  </div>
1396
1406
 
1397
1407
  <h3>Types read from the data</h3>
@@ -3191,6 +3201,22 @@ columns: [
3191
3201
  protected columns exactly as every other card does. Like the others it is presentation only.
3192
3202
  </p>
3193
3203
 
3204
+ <p class="lead-in">
3205
+ <code>pivotView</code> is the fourth shape: the grid drawn as a pivot &mdash; a cross-tab
3206
+ matrix. The grid's <code>group</code> dimensions run down the left gutter, its <code>pivot</code>
3207
+ dimensions run across the top, and each totalled column fills a cell with its reduction, with a
3208
+ subtotal down every row, across every column, and the grand total in the corner. Those numbers are
3209
+ the grid's own: every cell is the same aggregate kernel the totals row uses, run over the rows that
3210
+ feed the cell, so a pivot subtotal equals the grid's group total for that set by construction rather
3211
+ than being re-derived &mdash; an average subtotal is the average of the rows, never an average of
3212
+ cell averages. Both axes expand and collapse and both are virtualised through the shared row-template
3213
+ layer, so a wide, deep matrix draws only the cells on screen. Clicking a body cell emits
3214
+ <code>pivot:drill</code> with the keys of the contributing rows, the pivot's answer to &ldquo;what is
3215
+ behind this number&rdquo;. The collapse state rides in a saved view, and because a matrix cannot be
3216
+ read on a phone, at or below <code>maxWidth</code> the pivot degrades to a card list, exactly as the
3217
+ table does under <code>responsive</code>. Like the others it is presentation only.
3218
+ </p>
3219
+
3194
3220
  <div class="example">
3195
3221
  <p class="example__label">The shorthands, and the config a grid reports back</p>
3196
3222
  <pre data-run="js" data-expect="recordCard true, gallery 240px, board 300px" data-covers="config:recordCard config:gallery config:board"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
@@ -3214,6 +3240,33 @@ columns: [
3214
3240
  <span class="kw">return</span> `recordCard ${c.recordCard === <span class="kw">true</span>}, gallery ${c.gallery.tileWidth}px, board ${c.board.laneWidth}px`;</code></pre>
3215
3241
  </div>
3216
3242
 
3243
+ <div class="example">
3244
+ <p class="example__label">A pivot: group down, pivot across, and the drill event a cell click fires</p>
3245
+ <pre data-run="js" data-expect="pivotView true, collapsed 0, drills 0" data-covers="config:pivotView method:pivotView event:pivot:drill"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
3246
+
3247
+ <span class="kw">const</span> grid = createHeadlessGrid({
3248
+ columns: [{ field: 'region' }, { field: 'product' }, { field: 'amount', type: 'number', total: 'sum' }],
3249
+ rows: [
3250
+ { id: '1', region: 'EMEA', product: 'Widget', amount: 10 },
3251
+ { id: '2', region: 'APAC', product: 'Gadget', amount: 5 },
3252
+ ],
3253
+ rowKey: 'id',
3254
+ <span class="cmt">// Draw the grid as a pivot: the group down, the pivot across, a measure per cell.</span>
3255
+ pivotView: <span class="kw">true</span>,
3256
+ });
3257
+ grid.columns.group(['region']);
3258
+ grid.columns.pivot(['product']);
3259
+
3260
+ <span class="cmt">// A body-cell click drills to its rows through this event.</span>
3261
+ <span class="kw">let</span> drills = 0;
3262
+ grid.on('pivot:drill', () => { drills += 1; });
3263
+
3264
+ <span class="cmt">// The collapse state a saved view carries.</span>
3265
+ <span class="kw">const</span> state = grid.pivotView.state();
3266
+
3267
+ <span class="kw">return</span> `pivotView ${grid.config().pivotView === <span class="kw">true</span>}, collapsed ${state.rowsCollapsed.length}, drills ${drills}`;</code></pre>
3268
+ </div>
3269
+
3217
3270
  <div class="example">
3218
3271
  <p class="example__label">A card list</p>
3219
3272
  <pre><code>createGrid(element, {
@@ -3601,6 +3654,7 @@ socket.onmessage = (m) =&gt; {
3601
3654
  <tr><td class="sig">pending</td><td class="desc">Applied and painted, not yet acknowledged.</td><td class="desc"><code>cell:pending</code></td></tr>
3602
3655
  <tr><td class="sig">confirmed</td><td class="desc">The server accepted it. Nothing is written back.</td><td class="desc"><code>cell:confirmed</code></td></tr>
3603
3656
  <tr><td class="sig">reverted</td><td class="desc">The server refused it; the cell is rolled back.</td><td class="desc"><code>cell:reverted</code></td></tr>
3657
+ <tr><td class="sig">conflict</td><td class="desc">The write was accepted but the server row had moved underneath it. Last-write-wins: your value stands and the server's truth is surfaced so you can reconcile it.</td><td class="desc"><code>cell:conflict</code></td></tr>
3604
3658
  <tr><td class="sig">superseded</td><td class="desc">A newer edit replaced it while it was in flight.</td><td class="desc">either, with <code>superseded: true</code></td></tr>
3605
3659
  </tbody>
3606
3660
  </table>
@@ -6137,6 +6191,7 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
6137
6191
  <tr><td class="name">cell:changed</td><td class="desc">A committed edit reached the data. undo distinguishes a rollback.</td></tr>
6138
6192
  <tr><td class="name">cell:clicked</td><td class="desc">A cell was clicked. Announcement only: nothing is consumed, so editing and selection behave unchanged.</td></tr>
6139
6193
  <tr><td class="name">cell:confirmed</td><td class="desc">The write reached the server.</td></tr>
6194
+ <tr><td class="name">cell:conflict</td><td class="desc">The write was accepted but the server row had moved underneath it. Last-write-wins with the divergence surfaced: your value stands and serverRow carries the server's truth.</td></tr>
6140
6195
  <tr><td class="name">cell:contextmenu</td><td class="desc">Right-click on a cell.</td></tr>
6141
6196
  <tr><td class="name">cell:dblclicked</td><td class="desc">A cell was double-clicked. Carries the row, column, value and text.</td></tr>
6142
6197
  <tr><td class="name">cell:edit:end</td><td class="desc">It closed: committed or cancelled.</td></tr>
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.22.0, type declarations
2
+ * Lattice Grid 1.24.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -48,6 +48,9 @@ export type TypeName =
48
48
  | 'viscosity' | 'kinematicViscosity' | 'thermalConductivity' | 'specificHeat'
49
49
  // Temperature, which is affine rather than multiplicative
50
50
  | 'celsius' | 'fahrenheit' | 'kelvin'
51
+ // Currency, whose "factor" is a moving exchange rate, so it carries an amount
52
+ // and a code rather than joining the fixed-factor unit factory.
53
+ | 'currency' | 'usd' | 'eur' | 'gbp' | 'jpy'
51
54
  | (string & {});
52
55
 
53
56
  /**
@@ -430,10 +433,10 @@ export type RendererName =
430
433
  | 'winloss' | (string & {});
431
434
 
432
435
  export type EditorName =
433
- | 'checkbox' | 'code' | 'colour' | 'date' | 'datetime' | 'duration' | 'iconPicker'
434
- | 'ipaddress' | 'multiSelect' | 'number' | 'objectPicker' | 'password' | 'radix'
435
- | 'rating' | 'segmented' | 'select' | 'slider' | 'temperature' | 'text' | 'textarea'
436
- | 'time' | 'treeSelect' | 'unit' | (string & {});
436
+ | 'checkbox' | 'code' | 'colour' | 'currency' | 'date' | 'datetime' | 'duration'
437
+ | 'iconPicker' | 'ipaddress' | 'multiSelect' | 'number' | 'objectPicker' | 'password'
438
+ | 'radix' | 'rating' | 'segmented' | 'select' | 'slider' | 'temperature' | 'text'
439
+ | 'textarea' | 'time' | 'treeSelect' | 'unit' | (string & {});
437
440
 
438
441
  export interface EditorParams extends CellParams {
439
442
  stop(cancel?: boolean): void;
@@ -1141,6 +1144,68 @@ export interface OpenWrite {
1141
1144
  age: number;
1142
1145
  }
1143
1146
 
1147
+ /**
1148
+ * What an adapter can persist back to its source — the write-back capability
1149
+ * (§4.1), declared on `AdapterCapabilities.mutate`. `false` (the default) is
1150
+ * read-only by declaration; a resolved block turns every kind off unless the
1151
+ * adapter opts in.
1152
+ */
1153
+ export interface MutateCapability {
1154
+ /** The adapter can insert new rows. Wave 1: declared, not yet bridged. */
1155
+ append?: boolean;
1156
+ /** The adapter can patch existing rows. Wave 1: the wired kind (§4.3 Option A). */
1157
+ update?: boolean;
1158
+ /** The adapter can remove rows. Wave 1: declared, not yet bridged. */
1159
+ delete?: boolean;
1160
+ /**
1161
+ * The reconcile contract — what the server hands back after a successful
1162
+ * mutation (§5.1). `'row'`: the authoritative row (id, computed columns,
1163
+ * timestamps), reconciled before confirm. `'key'`: only the assigned key.
1164
+ * `'none'` (the default): nothing — the optimistic value stands
1165
+ * (last-write-wins).
1166
+ */
1167
+ returning?: 'row' | 'key' | 'none';
1168
+ }
1169
+
1170
+ /**
1171
+ * One mutation handed to `adapter.mutate(op, request)` (§4.2). Cell-scoped
1172
+ * `update` is the only kind wave 1 synthesises; `append`/`delete` are part of
1173
+ * the shape so it survives into a later structural build (card 770).
1174
+ */
1175
+ export interface MutationOp {
1176
+ kind: 'append' | 'update' | 'delete';
1177
+ /** append: the new rows (may lack a server-assigned key). */
1178
+ rows?: unknown[];
1179
+ /** update: the row key. */
1180
+ key?: string;
1181
+ /** update: the changed columns only, matching `PendingWrite` semantics. */
1182
+ patch?: Record<string, unknown>;
1183
+ /** delete: the row key(s). */
1184
+ keys?: string[];
1185
+ /** Provenance, carried through for auth / audit. */
1186
+ origin?: string;
1187
+ /** Stable id for idempotent retry / dedupe. Reserved; retry is a non-goal in wave 1. */
1188
+ requestId?: string;
1189
+ }
1190
+
1191
+ /**
1192
+ * The result of a mutation (§4.2) — the reconcile payload. A cell-update commit
1193
+ * flows this back through `PendingWrites`: `ok: false` reverts and surfaces
1194
+ * `reason`; `rows` (`returning: 'row'`) reconciles server truth before confirm;
1195
+ * `conflict` surfaces a last-write-wins divergence via `cell:conflict`.
1196
+ */
1197
+ export interface MutationResult {
1198
+ ok: boolean;
1199
+ /** `returning: 'row'` — the authoritative row(s) to reconcile to. */
1200
+ rows?: unknown[];
1201
+ /** `returning: 'key'` — server-assigned key(s) for appended rows, in order. */
1202
+ keys?: string[];
1203
+ /** On rejection — surfaced on `cell:reverted`, never swallowed. */
1204
+ reason?: string;
1205
+ /** The server's current value, for a surfaced last-write-wins conflict. */
1206
+ conflict?: { key: string; serverRow?: unknown };
1207
+ }
1208
+
1144
1209
  export interface PaginationConfig {
1145
1210
  enabled?: boolean;
1146
1211
  pageSize?: number;
@@ -1400,6 +1465,46 @@ export interface GridConfig {
1400
1465
  itemRole?: string;
1401
1466
  };
1402
1467
 
1468
+ /**
1469
+ * Present the grid as a pivot — a cross-tab drawn as a matrix (§10,
1470
+ * BACKLOG-0000738).
1471
+ *
1472
+ * The row dimensions (the grid's `group`) go down the left gutter, the column
1473
+ * dimensions (the grid's `pivot`) go across the top, and each totalled column
1474
+ * fills a cell with its reduction. `true` draws the matrix with the default
1475
+ * geometry; an object sizes the cells and gutter or names the breakpoint below
1476
+ * which it degrades to cards.
1477
+ *
1478
+ * **The numbers are the grid's own.** Every cell — body, subtotal, grand total
1479
+ * — is the same aggregate kernel the totals row uses, run over the rows that
1480
+ * feed the cell, so a pivot subtotal equals the grid's group total for that
1481
+ * set by construction rather than being re-derived. Both axes expand and
1482
+ * collapse, both are virtualised, and a cell click emits `pivot:drill` with the
1483
+ * keys of the contributing rows.
1484
+ *
1485
+ * **Narrow-screen fallback.** A matrix cannot be read on a phone, so at or
1486
+ * below `maxWidth` (the container width, not the viewport) the pivot degrades
1487
+ * to a card list — the record card by default — exactly as the table does under
1488
+ * `responsive`. Presentation only: sort, filter, group, pivot and the data
1489
+ * pipeline are unchanged.
1490
+ */
1491
+ pivotView?: boolean | {
1492
+ /** How wide one value column is, in pixels. 120 by default. */
1493
+ cellWidth?: number;
1494
+ /** How tall one body row is, in pixels. 32 by default. */
1495
+ cellHeight?: number;
1496
+ /** How wide the row-label gutter is, in pixels. 200 by default. */
1497
+ headerWidth?: number;
1498
+ /** Degrade to cards at or below this container width. 640 by default. */
1499
+ maxWidth?: number;
1500
+ /** The card layout the narrow fallback uses. The record card when omitted. */
1501
+ fallbackTemplate?: string | object;
1502
+ /** A class of your own on the pivot root, alongside the grid's. */
1503
+ className?: string;
1504
+ /** The pivot's role. `grid` by default — a pivot is a grid of cells. */
1505
+ role?: string;
1506
+ };
1507
+
1403
1508
  /**
1404
1509
  * Present rows as cards when the grid's container is too narrow to be a
1405
1510
  * table honestly, a phone, or a narrow panel on a wide screen.
@@ -1956,6 +2061,12 @@ export interface GridState {
1956
2061
  sort?: SortEntry[];
1957
2062
  group?: string[];
1958
2063
  pivot?: { enabled: boolean; columns: string[] };
2064
+ /**
2065
+ * The pivot presentation's collapse state (§10, BACKLOG-0000738): which
2066
+ * row-axis and column-axis nodes are collapsed. Absent when the matrix is
2067
+ * fully expanded, and tolerated as "expand all" when applied.
2068
+ */
2069
+ pivotView?: { rowsCollapsed: string[]; columnsCollapsed: string[] };
1959
2070
  formatting?: Record<string, FormattingRule[]>;
1960
2071
  expanded?: string[];
1961
2072
  selection?: string[];
@@ -2067,6 +2178,12 @@ export interface PushdownCapabilities {
2067
2178
  total?: boolean;
2068
2179
  /** Whether it can group and aggregate. */
2069
2180
  group?: boolean;
2181
+ /**
2182
+ * What the adapter can persist back — the write-back contract (§4.1). `false`
2183
+ * (the default) is read-only by declaration. A declared block opts kinds in;
2184
+ * `capabilitiesOf` resolves it to a full `MutateCapability` (or `false`).
2185
+ */
2186
+ mutate?: false | MutateCapability;
2070
2187
  }
2071
2188
 
2072
2189
  /** An engine the grid can query, and what it is able to answer. */
@@ -2077,6 +2194,13 @@ export interface PushdownAdapter {
2077
2194
  /** Run the part of the query the adapter declared it could handle. */
2078
2195
  execute(query: RemoteRequest, request?: RemoteRequest):
2079
2196
  Promise<{ rows: unknown[]; total?: number }>;
2197
+ /**
2198
+ * Persist one mutation (§4.2). Present only when `capabilities.mutate` opts
2199
+ * in. `createPushdownSource` synthesises an `edit.commit` that calls this for
2200
+ * cell updates (§4.3 Option A); `request` threads the abort signal through the
2201
+ * way `execute` receives it, and auth already lives on the adapter.
2202
+ */
2203
+ mutate?(op: MutationOp, request?: RemoteRequest): Promise<MutationResult>;
2080
2204
  }
2081
2205
 
2082
2206
  /** How one request was divided between the engine and the grid. */
@@ -2317,6 +2441,38 @@ export interface StatisticsApi {
2317
2441
  * its exact tier and, when one exists, the approximate alternative and bound.
2318
2442
  */
2319
2443
  maintenanceTier(fn: string): MaintenanceTier;
2444
+ /**
2445
+ * A windowed aggregate — "the average lately" (BACKLOG-0000654) — over one
2446
+ * column, stamped with the window it covers (`over`), so a windowed figure is
2447
+ * never read without its window. Exact over the values inside the window.
2448
+ *
2449
+ * `kind: 'count'` takes the last `span` values in arrival order. `kind:
2450
+ * 'time'` takes the values within the last `span` ms (or `minutes`) and `kind:
2451
+ * 'session'` takes every value; both need a timestamp column, so `by` is
2452
+ * required for them and never guessed. Returns null when the column, or the
2453
+ * `by` column, is unknown.
2454
+ */
2455
+ windowed(colId: string, fn: WindowedFn, opts: {
2456
+ kind: 'count' | 'time' | 'session';
2457
+ /** N ticks for a count window, or N ms for a time window. */
2458
+ span?: number;
2459
+ /** N minutes for a time window, converted to ms. */
2460
+ minutes?: number;
2461
+ /** A timestamp column; required for a time or session window. */
2462
+ by?: string;
2463
+ }): WindowedResult | null;
2464
+ }
2465
+
2466
+ /** A named aggregate a windowed reduction can return. */
2467
+ export type WindowedFn =
2468
+ | 'sum' | 'avg' | 'mean' | 'min' | 'max' | 'count' | 'variance' | 'stddev';
2469
+
2470
+ /** One windowed figure and the window it covers. */
2471
+ export interface WindowedResult {
2472
+ /** The reduction, or null when the window held no usable values. */
2473
+ value: number | null;
2474
+ /** The window the figure was computed over — always stated. */
2475
+ over: WindowSpec;
2320
2476
  }
2321
2477
 
2322
2478
  /** How an approximate reduction's error bound holds, and what it measures. */
@@ -2359,6 +2515,40 @@ export interface WindowSpec {
2359
2515
  size: number;
2360
2516
  }
2361
2517
 
2518
+ /** The three window kinds a caller may ask for. */
2519
+ export const WINDOW_KINDS: readonly ('count' | 'time' | 'session')[];
2520
+
2521
+ /**
2522
+ * A sliding window over a stream of timestamped values (BACKLOG-0000654). Holds
2523
+ * the values currently in the window and re-reduces them on demand; the reduction
2524
+ * is exact over the values in the window. `grid.statistics.windowed(...)` drives
2525
+ * one of these over a column; a host can also drive one live, tick by tick.
2526
+ */
2527
+ export class Window {
2528
+ constructor(kind: 'count' | 'time' | 'session', span?: number, now?: () => number);
2529
+ /** How many values are in the window right now. */
2530
+ readonly size: number;
2531
+ /** Add one value at an explicit or current timestamp. */
2532
+ push(v: number, t?: number): void;
2533
+ /** The window descriptor as it stands now. */
2534
+ spec(): WindowSpec;
2535
+ /** The values currently in the window, oldest first. */
2536
+ values(): number[];
2537
+ /** Every windowed aggregate at once, each stamped with the window. */
2538
+ aggregate(): {
2539
+ over: WindowSpec; count: number; sum: number | null; mean: number | null;
2540
+ min: number | null; max: number | null; variance: number | null; stddev: number | null;
2541
+ };
2542
+ /** One named aggregate over the window, stamped with the window it covers. */
2543
+ reduce(fn: WindowedFn): WindowedResult;
2544
+ }
2545
+
2546
+ /** Build a window from a caller's spec: last N ticks, last N minutes/ms, or the session. */
2547
+ export function openWindow(
2548
+ opts: { kind: 'count' | 'time' | 'session'; span?: number; minutes?: number },
2549
+ now?: () => number,
2550
+ ): Window;
2551
+
2362
2552
  export type ShadowKind =
2363
2553
  | 'updates' | 'updatedAt' | 'sinceUpdate' | 'delta' | 'deltaPercent'
2364
2554
  | 'rate' | 'history' | 'firstValue' | 'streak'
@@ -2615,7 +2805,7 @@ export type EventName =
2615
2805
  | 'rows:paused' | 'rows:resumed' | 'row:received' | 'row:sent' | 'row:copied'
2616
2806
  | 'row:moved' | 'source:error' | 'stream:chunk' | 'stream:end' | 'stream:evicted'
2617
2807
  /* Cells and editing */
2618
- | 'cell:changed' | 'cell:pending' | 'cell:confirmed' | 'cell:reverted'
2808
+ | 'cell:changed' | 'cell:pending' | 'cell:confirmed' | 'cell:reverted' | 'cell:conflict'
2619
2809
  | 'cell:clicked' | 'cell:dblclicked' | 'cell:contextmenu'
2620
2810
  | 'cell:edit:start' | 'cell:edit:end' | 'row:edit:start' | 'row:edit:end'
2621
2811
  | 'row:clicked' | 'row:dblclicked'
@@ -2626,6 +2816,7 @@ export type EventName =
2626
2816
  /* Columns */
2627
2817
  | 'column:moved' | 'column:resized' | 'column:visible' | 'column:pinned'
2628
2818
  | 'column:grouped' | 'column:pivoted' | 'column:filter:open' | 'column:menu:open'
2819
+ | 'pivot:drill'
2629
2820
  | 'columns:changed' | 'columns:tagged' | 'header:contextmenu'
2630
2821
  /* Selection and view */
2631
2822
  | 'selection:changed' | 'range:changed' | 'clipboard:copy'
@@ -2960,7 +3151,20 @@ export interface EditApi {
2960
3151
  changes: { key: string; colId: string; oldValue: unknown; newValue: unknown; changed: boolean }[];
2961
3152
  rejected: { key: string; colId: string; value: unknown; reason: 'permission' | 'readOnly' | 'validation' | 'locked' | 'missing' }[];
2962
3153
  };
2963
- settle(id: string, ok: boolean, reason?: string): boolean;
3154
+ /**
3155
+ * Report the outcome of an in-flight write (§18.3; §5.1-5.2 reconcile).
3156
+ *
3157
+ * `reconcile` carries server truth on a successful settle: `value` is a
3158
+ * server-authoritative value written back before `cell:confirmed`
3159
+ * (`returning: 'row'`); `conflict.serverRow` surfaces a last-write-wins
3160
+ * conflict via `cell:conflict`. Omit both to keep the optimistic value.
3161
+ */
3162
+ settle(
3163
+ id: string,
3164
+ ok: boolean,
3165
+ reason?: string,
3166
+ reconcile?: { value?: unknown; conflict?: { serverRow?: unknown } },
3167
+ ): boolean;
2964
3168
  pending(): OpenWrite[];
2965
3169
  status(key: string, colId: string): 'pending' | null;
2966
3170
  }
@@ -3444,6 +3648,22 @@ export interface PresentationApi {
3444
3648
  setSpotlight(target?: { keys?: string[]; colIds?: string[] } | null): boolean;
3445
3649
  }
3446
3650
 
3651
+ /**
3652
+ * Controls for the pivot presentation (§10, BACKLOG-0000738): expand or collapse
3653
+ * an axis node, and read the collapse state a saved view carries. Every method
3654
+ * is a no-op on a headless grid, which has no matrix to collapse.
3655
+ */
3656
+ export interface PivotViewApi {
3657
+ /** Expand a collapsed node on the row or column axis. */
3658
+ expand(axis: 'row' | 'column', path: string): void;
3659
+ /** Collapse a node on the row or column axis, hiding its descendants. */
3660
+ collapse(axis: 'row' | 'column', path: string): void;
3661
+ /** Toggle a node's collapse on the row or column axis. */
3662
+ toggle(axis: 'row' | 'column', path: string): void;
3663
+ /** The collapsed row-axis and column-axis paths, as a saved view carries them. */
3664
+ state(): { rowsCollapsed: string[]; columnsCollapsed: string[] };
3665
+ }
3666
+
3447
3667
  /**
3448
3668
  * Redaction obscures a column's values on screen. It is presentational: the
3449
3669
  * values stay in the model, the DOM, the clipboard and every export. Use
@@ -3750,6 +3970,8 @@ export interface Grid {
3750
3970
  annotate?: AnnotationApi;
3751
3971
  /** Full screen, scaling and chrome suppression. */
3752
3972
  readonly presentation: PresentationApi;
3973
+ /** Expand and collapse the pivot presentation's axes; the state a view carries. */
3974
+ readonly pivotView: PivotViewApi;
3753
3975
  /** The live feed: pausing it, flushing it, and what it has done. */
3754
3976
  readonly updates: UpdatesApi;
3755
3977
  /** Replaying the changes the grid has seen. */
@@ -3872,6 +4094,66 @@ export function parseUnit(text: string | number, opts?: UnitConfig): number | nu
3872
4094
  export function formatUnit(value: number | null | undefined, opts?: UnitConfig): string;
3873
4095
  export const UNIT_SYSTEMS: Record<string, readonly UnitDescriptor[]>;
3874
4096
 
4097
+ /**
4098
+ * A stored currency value: an amount in a named currency. `{amount:10,code:'USD'}`
4099
+ * is a different value from `{amount:10,code:'EUR'}` — currency is a real type,
4100
+ * not a display format, so the code rides on every cell.
4101
+ */
4102
+ export interface Money {
4103
+ amount: number;
4104
+ code: string;
4105
+ }
4106
+
4107
+ /**
4108
+ * A caller-supplied exchange-rate source. The grid ships and fetches no rates.
4109
+ * Either a function `(from, to) => rate|null`, or a table of rates per unit of a
4110
+ * common base (the base being whichever code maps to 1, or `rateBase`). A source
4111
+ * that cannot answer returns `null`, which is surfaced loudly, never as zero.
4112
+ */
4113
+ export type RateSource =
4114
+ | ((from: string, to: string) => number | null)
4115
+ | Record<string, number>;
4116
+
4117
+ export interface CurrencyConfig {
4118
+ /** The default currency code for bare numeric input, e.g. `'USD'`. */
4119
+ code?: string;
4120
+ /** The currency to render and aggregate in. Omit to keep each cell's own. */
4121
+ display?: string;
4122
+ /** The caller's rate source: a `(from,to)=>rate|null` fn or a rate table. */
4123
+ rates?: RateSource;
4124
+ /** The code a rate *table* is denominated in, when not the one mapping to 1. */
4125
+ rateBase?: string;
4126
+ /** Fixed fraction digits; omit for the code's own convention. */
4127
+ decimals?: number;
4128
+ /** The locale for number formatting. */
4129
+ locale?: string;
4130
+ /** Text for a null cell. */
4131
+ nullDisplay?: string;
4132
+ /** The loud marker rendered when a needed rate is missing. */
4133
+ missingRate?: string;
4134
+ /** An Excel number-format override. */
4135
+ excel?: string;
4136
+ /** The code list a currency editor's picker offers. */
4137
+ codes?: string[];
4138
+ }
4139
+
4140
+ /** Build a currency `DataType` (amount + code), aggregate-safe across currencies. */
4141
+ export function createCurrencyType(config?: CurrencyConfig): DataType;
4142
+ /** Parse edited/pasted text into a `Money`, taking the config's `code` for bare numbers. */
4143
+ export function parseMoney(text: string | number, cfg?: CurrencyConfig): Money | null;
4144
+ /** Render a `Money`, in the display currency when set; loud marker when a rate is missing. */
4145
+ export function formatMoney(value: unknown, cfg?: CurrencyConfig): string;
4146
+ /** Convert a `Money` into a target code via the resolved rate fn; `null` when no rate. */
4147
+ export function convertMoney(
4148
+ money: Money,
4149
+ to: string,
4150
+ rate: (from: string, to: string) => number | null,
4151
+ ): number | null;
4152
+ /** Resolve a caller's rate source into a `(from,to)=>rate|null` function. */
4153
+ export function rateFunction(rates?: RateSource, base?: string): (from: string, to: string) => number | null;
4154
+ /** The loud marker text a missing rate renders as. */
4155
+ export const MISSING_RATE: string;
4156
+
3875
4157
  /** How a statistic block finds the number it reports. */
3876
4158
  export interface StatValueSpec {
3877
4159
  /** The column to reduce, as a field name or a dotted path. Omit for `count`. */
@@ -4024,10 +4306,18 @@ export const NO_CAPABILITIES: Readonly<Required<PushdownCapabilities>>;
4024
4306
  * per condition per query, and membership on an array is a scan. The declared
4025
4307
  * form and the resolved form differ, which is why this is its own type.
4026
4308
  */
4027
- export type ResolvedCapabilities = Omit<Required<PushdownCapabilities>, 'operators'> & {
4309
+ export type ResolvedCapabilities = Omit<Required<PushdownCapabilities>, 'operators' | 'mutate'> & {
4028
4310
  operators: ReadonlySet<string>;
4311
+ /** Resolved by `resolveMutate`: `false`, or every kind and `returning` present. */
4312
+ mutate: false | Required<MutateCapability>;
4029
4313
  };
4030
4314
 
4315
+ /**
4316
+ * Resolve an adapter's declared `mutate` block against the defaults (§4.1).
4317
+ * `false` (or anything falsy) stays `false` — read-only by declaration.
4318
+ */
4319
+ export function resolveMutate(declared?: boolean | MutateCapability): false | Required<MutateCapability>;
4320
+
4031
4321
  export function capabilitiesOf(declared?: PushdownCapabilities): ResolvedCapabilities;
4032
4322
 
4033
4323
  /**
@@ -4235,6 +4525,12 @@ export const LatticeGrid: {
4235
4525
  parseUnit: typeof parseUnit;
4236
4526
  formatUnit: typeof formatUnit;
4237
4527
  UNIT_SYSTEMS: typeof UNIT_SYSTEMS;
4528
+ createCurrencyType: typeof createCurrencyType;
4529
+ parseMoney: typeof parseMoney;
4530
+ formatMoney: typeof formatMoney;
4531
+ convertMoney: typeof convertMoney;
4532
+ rateFunction: typeof rateFunction;
4533
+ MISSING_RATE: typeof MISSING_RATE;
4238
4534
  };
4239
4535
 
4240
4536
  export default LatticeGrid;
@@ -4337,6 +4633,65 @@ export interface ChartMeasure {
4337
4633
  title?: string;
4338
4634
  }
4339
4635
 
4636
+ /** One axis's configuration. A bare string is the title. */
4637
+ export interface ChartAxis {
4638
+ title?: string;
4639
+ /** Fix the axis rather than taking its extent from the data. */
4640
+ min?: number;
4641
+ max?: number;
4642
+ /** A tick count, or the exact values to tick. */
4643
+ ticks?: number | unknown[];
4644
+ /** A format mask, or a function of the value. */
4645
+ format?: string | ((value: unknown) => string);
4646
+ /** Draw the gridlines this axis owns. Default true for the measure axis. */
4647
+ grid?: boolean;
4648
+ /** Draw the tick labels. */
4649
+ labels?: boolean;
4650
+ /** Show every nth category label, on a crowded category axis. */
4651
+ every?: number;
4652
+ /** Force the category labels' rotation rather than deciding it. */
4653
+ rotate?: boolean | 'auto';
4654
+ }
4655
+
4656
+ /**
4657
+ * One declarative annotation (BACKLOG-0000744).
4658
+ *
4659
+ * A reference or target line, a shaded band, or a callout. Its value is a
4660
+ * constant `value` (or `from`/`to` for a band), or a `compute` reduction of the
4661
+ * data it annotates — `mean`, `median`, `min`, `max`, or `p95` for a
4662
+ * percentile — so it follows the data as the grid is filtered. Every annotation
4663
+ * names the axis it reads, which on a dual-axis chart is what stops it being
4664
+ * placed against the wrong scale, and is written into the accessible table as a
4665
+ * sentence.
4666
+ */
4667
+ export interface ChartAnnotation {
4668
+ /** The default is a reference line. */
4669
+ kind?: 'line' | 'target' | 'band' | 'callout';
4670
+ /** A constant value, for a line, target or callout's measure position. */
4671
+ value?: number;
4672
+ /** A reduction of the annotated data instead of a constant. */
4673
+ compute?: 'mean' | 'avg' | 'median' | 'min' | 'max' | string;
4674
+ /** A band's two edges, each a constant or (with `fromCompute`/`toCompute`) computed. */
4675
+ from?: number;
4676
+ to?: number;
4677
+ fromCompute?: string;
4678
+ toCompute?: string;
4679
+ /** A vertical line's or callout's x position: a category or a number. */
4680
+ x?: unknown;
4681
+ at?: unknown;
4682
+ /** Force a line vertical rather than horizontal. */
4683
+ orient?: 'horizontal' | 'vertical';
4684
+ /** Which measure axis the annotation reads. */
4685
+ axis?: 'left' | 'right' | 'y2';
4686
+ /** Restrict a `compute` to one series, by its key. */
4687
+ series?: string;
4688
+ label?: string;
4689
+ colour?: string;
4690
+ /** A band's fill opacity; the default is 0.12. */
4691
+ opacity?: number;
4692
+ className?: string;
4693
+ }
4694
+
4340
4695
  /** Data labels beside each mark. */
4341
4696
  export interface ChartLabels {
4342
4697
  position?: 'outside' | 'inside' | 'auto';
@@ -4383,7 +4738,28 @@ export interface ChartSpec {
4383
4738
  scheme?: string | string[];
4384
4739
  legend?: boolean | { position?: 'top' | 'bottom' | 'left' | 'right'; isolate?: boolean };
4385
4740
  labels?: boolean | ChartLabels;
4386
- axis?: object;
4741
+ /**
4742
+ * Per-axis configuration. Each side is a title string or an object of
4743
+ * `{ title, min, max, ticks, format, grid, labels }`. `y2` (or `right`)
4744
+ * configures the second measure axis of a dual-axis or combo chart
4745
+ * (BACKLOG-0000743); a dual-axis chart labels both axes by default so it
4746
+ * cannot silently mislead.
4747
+ */
4748
+ axis?: {
4749
+ x?: string | ChartAxis;
4750
+ y?: string | ChartAxis;
4751
+ y2?: string | ChartAxis;
4752
+ right?: string | ChartAxis;
4753
+ };
4754
+ /**
4755
+ * Dragging across the plot. `true` or `'filter'` writes a range condition into
4756
+ * the grid; `'zoom'` changes only this chart's own domain; `'select'` selects
4757
+ * the rows under the drag. The object form names which axis the drag acts on —
4758
+ * `axis: 'y'` or `'y2'` brushes a value axis, which on a dual-axis chart must
4759
+ * say which one it means (BACKLOG-0000743).
4760
+ */
4761
+ brush?: boolean | 'filter' | 'zoom' | 'select'
4762
+ | { mode: 'filter' | 'zoom' | 'select'; axis?: 'x' | 'y' | 'y2' };
4387
4763
  font?: object;
4388
4764
  margin?: number | { top?: number; right?: number; bottom?: number; left?: number };
4389
4765
  /** Horizontal reference lines. */
@@ -4402,7 +4778,20 @@ export interface ChartSpec {
4402
4778
  * from another column instead.
4403
4779
  */
4404
4780
  error?: boolean | { of?: string; confidence?: number };
4405
- reference?: { value: number; label?: string }[];
4781
+ /**
4782
+ * Horizontal reference lines. On a dual-axis bar or line chart (see
4783
+ * {@link ChartMeasure.axis}) a line naming `axis: 'right'` is placed on the
4784
+ * right-hand scale, so it means what the right axis says rather than landing
4785
+ * at the same number on the scale it does not belong to.
4786
+ */
4787
+ reference?: { value: number; label?: string; axis?: 'left' | 'right' }[];
4788
+ /**
4789
+ * The declarative annotation layer: reference and target lines, shaded bands
4790
+ * and callouts, each naming the axis it reads and each described into the
4791
+ * accessible table as a sentence. A value may be a constant or `compute`d from
4792
+ * the data it annotates, so it follows the chart as the grid is filtered.
4793
+ */
4794
+ annotations?: ChartAnnotation[];
4406
4795
  /** Bins for a histogram; the default is twelve. */
4407
4796
  buckets?: number;
4408
4797
  /** A diverging colour ramp, for heatmap and geomap. */