@ai-matrx/design-system 0.22.0 → 0.26.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/dist/data-table/cell-undo.cjs +98 -0
  3. package/dist/data-table/cell-undo.cjs.map +1 -0
  4. package/dist/data-table/cell-undo.d.cts +85 -0
  5. package/dist/data-table/cell-undo.d.ts +85 -0
  6. package/dist/data-table/cell-undo.js +78 -0
  7. package/dist/data-table/cell-undo.js.map +1 -0
  8. package/dist/data-table/copy-helpers.d.cts +7 -2
  9. package/dist/data-table/copy-helpers.d.ts +7 -2
  10. package/dist/data-table/coverage.cjs +116 -0
  11. package/dist/data-table/coverage.cjs.map +1 -0
  12. package/dist/data-table/coverage.d.cts +65 -0
  13. package/dist/data-table/coverage.d.ts +65 -0
  14. package/dist/data-table/coverage.js +96 -0
  15. package/dist/data-table/coverage.js.map +1 -0
  16. package/dist/data-table/data-row-inspector.cjs.map +1 -1
  17. package/dist/data-table/data-row-inspector.js.map +1 -1
  18. package/dist/data-table/data-row-window.cjs.map +1 -1
  19. package/dist/data-table/data-row-window.js.map +1 -1
  20. package/dist/data-table/facets.cjs +118 -0
  21. package/dist/data-table/facets.cjs.map +1 -0
  22. package/dist/data-table/facets.d.cts +100 -0
  23. package/dist/data-table/facets.d.ts +100 -0
  24. package/dist/data-table/facets.js +98 -0
  25. package/dist/data-table/facets.js.map +1 -0
  26. package/dist/data-table/fill.cjs +299 -0
  27. package/dist/data-table/fill.cjs.map +1 -0
  28. package/dist/data-table/fill.d.cts +129 -0
  29. package/dist/data-table/fill.d.ts +129 -0
  30. package/dist/data-table/fill.js +277 -0
  31. package/dist/data-table/fill.js.map +1 -0
  32. package/dist/data-table/filter-engine.d.cts +7 -2
  33. package/dist/data-table/filter-engine.d.ts +7 -2
  34. package/dist/data-table/grid-clipboard.cjs +165 -0
  35. package/dist/data-table/grid-clipboard.cjs.map +1 -0
  36. package/dist/data-table/grid-clipboard.d.cts +85 -0
  37. package/dist/data-table/grid-clipboard.d.ts +85 -0
  38. package/dist/data-table/grid-clipboard.js +145 -0
  39. package/dist/data-table/grid-clipboard.js.map +1 -0
  40. package/dist/data-table/grid-selection.cjs +258 -0
  41. package/dist/data-table/grid-selection.cjs.map +1 -0
  42. package/dist/data-table/grid-selection.d.cts +166 -0
  43. package/dist/data-table/grid-selection.d.ts +166 -0
  44. package/dist/data-table/grid-selection.js +238 -0
  45. package/dist/data-table/grid-selection.js.map +1 -0
  46. package/dist/data-table/grouping.cjs +302 -0
  47. package/dist/data-table/grouping.cjs.map +1 -0
  48. package/dist/data-table/grouping.d.cts +119 -0
  49. package/dist/data-table/grouping.d.ts +119 -0
  50. package/dist/data-table/grouping.js +282 -0
  51. package/dist/data-table/grouping.js.map +1 -0
  52. package/dist/data-table/host.cjs.map +1 -1
  53. package/dist/data-table/host.d.cts +18 -4
  54. package/dist/data-table/host.d.ts +18 -4
  55. package/dist/data-table/host.js.map +1 -1
  56. package/dist/data-table/index.cjs +3173 -451
  57. package/dist/data-table/index.cjs.map +1 -1
  58. package/dist/data-table/index.d.cts +13 -3
  59. package/dist/data-table/index.d.ts +13 -3
  60. package/dist/data-table/index.js +3174 -451
  61. package/dist/data-table/index.js.map +1 -1
  62. package/dist/data-table/infer-filter.d.cts +7 -2
  63. package/dist/data-table/infer-filter.d.ts +7 -2
  64. package/dist/data-table/layered-filters.d.cts +7 -2
  65. package/dist/data-table/layered-filters.d.ts +7 -2
  66. package/dist/data-table/menu-targets.cjs +77 -0
  67. package/dist/data-table/menu-targets.cjs.map +1 -0
  68. package/dist/data-table/menu-targets.d.cts +108 -0
  69. package/dist/data-table/menu-targets.d.ts +108 -0
  70. package/dist/data-table/menu-targets.js +57 -0
  71. package/dist/data-table/menu-targets.js.map +1 -0
  72. package/dist/data-table/pagination.cjs.map +1 -1
  73. package/dist/data-table/pagination.js.map +1 -1
  74. package/dist/data-table/query-control.d.cts +7 -2
  75. package/dist/data-table/query-control.d.ts +7 -2
  76. package/dist/data-table/spreadsheet.cjs +1062 -0
  77. package/dist/data-table/spreadsheet.cjs.map +1 -0
  78. package/dist/data-table/spreadsheet.d.cts +153 -0
  79. package/dist/data-table/spreadsheet.d.ts +153 -0
  80. package/dist/data-table/spreadsheet.js +1042 -0
  81. package/dist/data-table/spreadsheet.js.map +1 -0
  82. package/dist/data-table/types.cjs.map +1 -1
  83. package/dist/data-table/types.d.cts +7 -2
  84. package/dist/data-table/types.d.ts +7 -2
  85. package/dist/data-table/url-state.d.cts +7 -2
  86. package/dist/data-table/url-state.d.ts +7 -2
  87. package/dist/data-table/uuid-cell.cjs.map +1 -1
  88. package/dist/data-table/uuid-cell.js.map +1 -1
  89. package/dist/data-table/validation.cjs +229 -0
  90. package/dist/data-table/validation.cjs.map +1 -0
  91. package/dist/data-table/validation.d.cts +125 -0
  92. package/dist/data-table/validation.d.ts +125 -0
  93. package/dist/data-table/validation.js +209 -0
  94. package/dist/data-table/validation.js.map +1 -0
  95. package/dist/data-table/virtual-window.cjs +203 -0
  96. package/dist/data-table/virtual-window.cjs.map +1 -0
  97. package/dist/data-table/virtual-window.d.cts +84 -0
  98. package/dist/data-table/virtual-window.d.ts +84 -0
  99. package/dist/data-table/virtual-window.js +183 -0
  100. package/dist/data-table/virtual-window.js.map +1 -0
  101. package/dist/data-table/xlsx.d.cts +7 -2
  102. package/dist/data-table/xlsx.d.ts +7 -2
  103. package/dist/field-formats/index.d.cts +2 -2
  104. package/dist/field-formats/index.d.ts +2 -2
  105. package/dist/formulas/index.cjs +1041 -0
  106. package/dist/formulas/index.cjs.map +1 -0
  107. package/dist/formulas/index.d.cts +389 -0
  108. package/dist/formulas/index.d.ts +389 -0
  109. package/dist/formulas/index.js +1019 -0
  110. package/dist/formulas/index.js.map +1 -0
  111. package/dist/{types-BdbvlF9-.d.cts → types-CJWU1Gae.d.cts} +1 -1
  112. package/dist/{types-BdbvlF9-.d.ts → types-CJWU1Gae.d.ts} +1 -1
  113. package/dist/{types-D-flTpK0.d.ts → types-Cu5_eUG1.d.ts} +268 -4
  114. package/dist/{types-Sn249eoy.d.cts → types-DrJXAAoD.d.cts} +268 -4
  115. package/package.json +129 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,232 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.0 — 2026-09-20
4
+
5
+ **The four things a re-point of the owner's `/data` grid would have lost, now
6
+ first-class in the shared table.**
7
+
8
+ A capability-by-capability walk of that route against this package found four
9
+ gaps. Each is built here, in the shared layer, so every table in the product
10
+ gets it — not one route.
11
+
12
+ - **THE FOUR RIGHT-CLICK LEVELS.** The package exposed exactly ONE context
13
+ descriptor — the row — so a surface wanting a cell, a column or a selection
14
+ menu had to fork the table. It now answers all four. `menu-targets.ts` reads
15
+ which level the pointer was aimed at off the anchors the table renders
16
+ (`data-matrx-header-column-id` is new on every header; body cells already
17
+ carried `data-matrx-table-column-id`), with the precedence a spreadsheet
18
+ person expects: a click inside an extended range is the RANGE, never the one
19
+ cell under it — except a range of one cell, which is a cell.
20
+ `MatrxDataTableContextMenuConfig` gained `resolveCellContext`,
21
+ `resolveColumnContext`, `resolveSelectionContext` and `resolveTableContext`;
22
+ the host's `createDefaultRowContext` became `createDefaultMenuContext`, which
23
+ takes one payload carrying the level and what it is acting on. The package
24
+ still never interprets a menu — that stays the application's.
25
+ - **ENTER TO EDIT, AND TYPE TO REPLACE.** `classifyGridKey` had classified
26
+ `edit` and `editSeeded` since the gestures landed and nothing consumed them.
27
+ `useSpreadsheetGrid` now asks its consumer through `onEditRequest(address,
28
+ seed)` and consumes the keystroke only when an editor actually opened — so a
29
+ read-only column types nothing instead of swallowing the character.
30
+ `MatrxDataTable` performs it through the SAME `editRequest` door the row
31
+ menu's "Edit row" already used, and a seeded editor opens holding the typed
32
+ character rather than the old value.
33
+ - **FILTER VALUE COUNTS.** A "Contains…" box asks the reader to already know
34
+ what is in the column. `facets.ts` counts the real values, blanks included as
35
+ a countable option, and `MatrxDataTableFacetsConfig` decides where the counts
36
+ come from: the browser when it holds every row the counts describe, the
37
+ consumer's `source` when it does not — and when neither can answer for the
38
+ whole table, the popover SAYS the counts cover only the loaded rows. A
39
+ complete answer says nothing at all. Counting happens when someone opens a
40
+ popover, never on first paint.
41
+ - **VALIDATION REFUSALS, IN THE CELL, IN WORDS.** `data-table/validation` was a
42
+ published module the table never called. `edit.validate` now judges a value
43
+ before it enters the draft and a returned sentence is a refusal: the editor
44
+ stays open holding what the person typed and the sentence renders under the
45
+ cell in amber, clearing the moment they change the value. The same judgement
46
+ screens paste, fill and clear through `spreadsheet.validateCell` — a rule a
47
+ paste can walk around is not a rule.
48
+
49
+ **Two defects only a real browser could find**, both in the seam between the
50
+ cell cursor and the editor, both green in jsdom:
51
+
52
+ - **A click selected the cell and then every keystroke went to the document
53
+ body.** `preventDefault` on a mousedown also prevents focus, so the grid
54
+ container never got it — and Enter, a typed character, an arrow and Cmd-Z all
55
+ landed on nothing. A jsdom test dispatches the keydown ON the container by
56
+ hand and can never see it. The cell cursor now focuses the grid.
57
+ - **A click on a string-editable cell dropped straight into a text box**, which
58
+ is THE CLICK LAW broken at the one place the two paths meet: the same click
59
+ starts a range, and an editor opening under the pointer eats the drag and the
60
+ next keystroke. With spreadsheet gestures on, the editor opens the way it does
61
+ in Excel — Enter, F2, the hover pencil, or typing — and an explicit
62
+ `editTrigger: "click"` does not override it.
63
+
64
+ New entry points: `/data-table/facets` and `/data-table/menu-targets`, both
65
+ pure. Suite: 101 files / 1,073 passed. Red twin
66
+ `MatrxDataTable.four-levels.red.tsx`: 13/13 fail. Three absence-asserting
67
+ clauses and one rendering clause are deliberately not twinned, and the red file
68
+ says which and why. Browser proof on a production build, 1440x900 headless
69
+ Chromium, zero console errors: right-click answered `cell · Amount`,
70
+ `column · Status`, `row · Row` and `selection · 16 cells`; Enter opened the cell
71
+ on `Ada` and typing `Z` opened it on `Z`, committing `Zara`; the Status filter
72
+ listed five values at 400 each and filtering to Done left only Done on screen;
73
+ a 500-row table declaring 84,312 said *"These counts cover the 500 rows loaded
74
+ here, not the whole table."* and a complete answer said nothing; a blank Owner
75
+ was refused in the cell with *"Owner cannot be empty. Every job needs someone on
76
+ it."* while the editor stayed open, and the same rule refused a paste
77
+ (`Pasted 0 cells.`) that never touches an editor.
78
+
79
+ ## 0.25.0 — 2026-09-20
80
+
81
+ **Five defects a real browser found that jsdom could not, and the table's own
82
+ stress page so the next one is found the same way.**
83
+
84
+ Every clause in the component suite was green while all five of these were
85
+ live. They are claims about SCROLLING, about a POINTER, and about what a person
86
+ READS — none of which jsdom has. `demo/` is now where a real browser does that
87
+ work: a hundred thousand rows with grouping, the spreadsheet gestures, the
88
+ coverage sentence and the horizontal window on at once.
89
+
90
+ - **The fill handle dragged and filled nothing.** Two `mouseup` listeners on
91
+ `window`; the one that only cleared the drag flag was registered first, so by
92
+ the time the one that commits the fill ran, there was no drag in progress.
93
+ One handler now reads the flag and then clears it. Proven by a real pointer:
94
+ four cells filled, `"Filled 4 cells down 1 column."`
95
+ - **The scroll re-rendered the whole table on every event.** The window was
96
+ derived from a scroll offset held in state, so a four-thousand-line component
97
+ ran on every scroll whether or not one different row would be drawn: 38ms a
98
+ frame while scrolling EIGHT PIXELS, against the 16.7ms floor the same browser
99
+ hits on a static forty-row table. The scroll handler computes the window off
100
+ the DOM and sets state only when the rendered range actually moves.
101
+ - **The first paint drew every row**, because the window settled in an effect
102
+ and an effect runs after the first render. Render now derives a window when
103
+ state has none, and when a stale one no longer fits the row count.
104
+ - **`bodyEntries` built the whole list before windowing it**, allocating a
105
+ hundred thousand objects per scroll to render forty rows. It slices first.
106
+ - **`cellProps` used `indexOf` per cell** — O(rows) per cell, twenty-four
107
+ million comparisons a frame at 100,000 rows. Index maps.
108
+ - **The sticky header sat UNDER the frozen column.** A frozen body cell is
109
+ `z-20` so it can slide under the columns to its right; the header was `z-10`,
110
+ so the frozen column's cells scrolled over the header they belong to and the
111
+ top data row printed through the column names. Only a table long enough to
112
+ scroll shows it.
113
+ - **`rowHeight` did nothing unless virtualization was engaged** — so a view that
114
+ remembered a row height was ignored on every table short enough to read. It
115
+ applies on its own now, with a clause that pins it.
116
+ - **A group header said its subtotals twice** — once in the label line and again
117
+ in each column's cell — and the cell version was left-aligned under a
118
+ right-aligned money column. The label carries the count; the columns carry
119
+ the subtotals, each aligned with the numbers it totals.
120
+
121
+ Measured on the production demo build, 1440x900 headless Chromium, 100,000 rows:
122
+ **19 of the 40 rendered rows in the document, a 3.3-million-pixel scroll height,
123
+ 18.6ms median frame and 23.6ms at the 95th percentile**, zero console errors.
124
+
125
+ ## 0.24.0 — 2026-09-20
126
+
127
+ **The window goes sideways too.**
128
+
129
+ 0.23.0 windowed the rows. A table can also be forty rows and three hundred
130
+ columns, and that one put every column in the document. `virtualize.columns`
131
+ closes it: the frozen leading run is ALWAYS drawn — a frozen identity column
132
+ that scrolled out of the DOM would leave the table blank exactly when somebody
133
+ scrolled far enough to need it — and a spacer cell on either side of the window
134
+ carries the width of what was left out, in the header, every body row, every
135
+ group header and the footer, so the table is as wide as it says it is.
136
+
137
+ - It engages only past **30** non-frozen columns and only when the consumer
138
+ asks for it, so nothing in the product changes shape today.
139
+ - A windowed column takes a FIXED width, and that is not a style choice: if the
140
+ drawn columns sized themselves to their content while the spacers claimed a
141
+ declared width, the horizontal scroll position would stop agreeing with the
142
+ window index and a person scrolling to column 80 would be shown column 74.
143
+ - Every `colSpan` in the body now counts what is RENDERED, not what is
144
+ declared — a full-width row spanning the declared count would push itself
145
+ wider than its own header.
146
+
147
+ Guard: three clauses in `MatrxDataTable.gaps.test.tsx`, including
148
+ `padStart + drawn + padEnd === the rest of the table`; the red twin fails
149
+ 12/12 without the props.
150
+
151
+ ## 0.23.1 — 2026-09-20
152
+
153
+ **Ticking a row no longer moves the row under your cursor.**
154
+
155
+ Defect N11, the thirteenth cold walk of the Masterwork pipeline (blocking). Measured on a
156
+ catalogued media Library at 1440x900: before the first click the header checkbox sat at `y 580`
157
+ and rows 1-3 at `y 606 / 647 / 688`; the instant a row's checkbox was ticked the selection bar was
158
+ inserted into the flow ABOVE the table and everything moved down **86px** — two full row heights,
159
+ header checkbox to `y 666`, rows 1-3 to `y 692 / 733 / 774`. The cursor had not moved and was now
160
+ resting on the SELECT-ALL box, so a second click at the same point selected 25 of 2,981 rows. The
161
+ walker hit it twice before he thought to measure it, which is how we know a real person hits it.
162
+ Picking three rows out of a long list meant re-measuring the table between every click.
163
+
164
+ - **The selection bar is now the last child of the body region** — under the rows and under the
165
+ pagination, instead of wedged between the toolbar and the first row. The rows live in a `flex-1`
166
+ box; a flex child appended after `flex-1` can only take height from the bottom of that box and
167
+ cannot change where it STARTS, so no row can move down. It is also where the best in the world
168
+ put it: Gmail, Linear and Airtable all keep the selection bar at the bottom of the list.
169
+ - **The bar carries `data-matrx-table-bulk-bar`**, so a test or a host can address it by what it
170
+ is rather than by where it happens to fall in a flat DOM query.
171
+ - Guard: `MatrxDataTable.selection-never-shifts-rows.test.tsx`, five cases, three of which go red
172
+ the moment the block is moved back above `data-matrx-table-body-region`.
173
+
174
+ **Consumer action:** none. No API changed. Every surface that hands `MatrxDataTable` a `selection`
175
+ inherits the fix. One caveat for test authors: a test that reached the selection bar's copy menu
176
+ as `getAllByRole("button", { name: /Copy, transform or export/ })[1]` was relying on the bar
177
+ rendering above the rows; address it through `[data-matrx-table-bulk-bar]` instead (this
178
+ package's own `default-copy-regressions.test.tsx` was updated the same way).
179
+
180
+ ## 0.23.0 — 2026-09-20
181
+
182
+ **The three gaps are closed in the primitive, and the spreadsheet half of `/data` now lives here.**
183
+
184
+ The owner's read of the `/data` grid named three things it could not do that Airtable, Google
185
+ Sheets and Notion all can — and named the third one a lie on screen. All three are answered in
186
+ the shared table, so every surface in the product inherits them instead of the one route that
187
+ asked.
188
+
189
+ - **`grouping`** — "group these rows by Status" with collapsible sections, a count, and a
190
+ per-column subtotal ON its own column, not crammed into a banner. Controlled for the two pieces
191
+ of state a person expects to survive (which column groups, which sections are shut), so a saved
192
+ view carries them like any other reading of the table. A blank value is its own named section
193
+ and sorts last: a section of unanswered rows is not the first thing a person wants to read. The
194
+ model is `./grouping.ts` — pure, with eleven aggregate kinds, every one of which LABELS itself
195
+ ("Sum 4,812.5", "12 filled") and says `Sum —` rather than `0` when it has nothing to add up.
196
+ - **`virtualize`** — render the rows a person can see. Off by default; on, it engages only past
197
+ its own threshold (150 rows), so short tables behave exactly as they did. Two spacer rows stand
198
+ in for the height of what the window left out, and the invariant `padStart + rendered + padEnd
199
+ === total` is asserted over a spread of inputs, because a scrollbar that describes the slice
200
+ instead of the table is its own lie. `./virtual-window.ts` also has the variable-height and
201
+ scroll-to-index math, prefix-summed and binary-searched: O(log n) per scroll, not O(n).
202
+ - **`coverage`** — THE HONEST COUNT. Declare what the current answer actually covers and the
203
+ table says so, in plain English, above the rows: *"This filter searched the first 5,000 rows
204
+ only. 84,312 rows in this table; the rest were not looked at."* A complete answer says nothing.
205
+ A partial count is never presented as exact. The sentences are asserted to contain none of
206
+ "cap", "limit", "fetch", "query", "client" or "source" — a person reading a screen is not
207
+ reading our vocabulary.
208
+ - **`MatrxDataTableProps.rowHeight`** — an explicit body row height, for a view that remembers one.
209
+
210
+ New entry points carrying the rest of the `/data` grid's reusable half, each pure and tested:
211
+
212
+ - **`@ai-matrx/design-system/formulas`** — the 28-function expression language for computed
213
+ columns, moved out of the app verbatim. No `eval`, nothing throws, every failure is an
214
+ `ok: false` with a sentence.
215
+ - **`@ai-matrx/design-system/data-table/grid-selection`** and **`/grid-clipboard`** — the range
216
+ model (shift-click, drag, keyboard, row/column/all) and real Excel/Sheets TSV.
217
+ - **`@ai-matrx/design-system/data-table/fill`** — fill-down and the drag fill handle, with series
218
+ detection: `1, 2` continues to `3, 4, 5`, a single value copies, an inconsistent run copies. A
219
+ column that cannot take a write produces a skip with a sentence, never a silent drop.
220
+ - **`@ai-matrx/design-system/data-table/cell-undo`** — the bounded, PURE undo/redo stack behind
221
+ cell edits.
222
+ - **`@ai-matrx/design-system/data-table/validation`** — the column validation model, so a refusal
223
+ is the same sentence everywhere.
224
+
225
+ One defect this work turned up and fixed in passing: an optional config field named `valueOf` is
226
+ never absent, because every object in JavaScript already has one — `config.valueOf ? … : …` called
227
+ `Object.prototype.valueOf` and grouped every row under the config object itself. The field is
228
+ `readCell` now, in both the config and the model.
229
+
3
230
  ## 0.22.0 — 2026-09-20
4
231
 
5
232
  **The data table can paint, format and freeze — so every table in the product can.**
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/data-table/cell-undo.ts
21
+ var cell_undo_exports = {};
22
+ __export(cell_undo_exports, {
23
+ DEFAULT_UNDO_DEPTH: () => DEFAULT_UNDO_DEPTH,
24
+ canRedo: () => canRedo,
25
+ canUndo: () => canUndo,
26
+ createUndoStack: () => createUndoStack,
27
+ popRedo: () => popRedo,
28
+ popUndo: () => popUndo,
29
+ pushUndo: () => pushUndo,
30
+ undoEntryFor: () => undoEntryFor
31
+ });
32
+ module.exports = __toCommonJS(cell_undo_exports);
33
+ var DEFAULT_UNDO_DEPTH = 100;
34
+ function createUndoStack(depth = DEFAULT_UNDO_DEPTH) {
35
+ const bounded = Number.isFinite(depth) && depth > 0 ? Math.floor(depth) : 0;
36
+ return { past: [], future: [], depth: bounded };
37
+ }
38
+ function pushUndo(stack, entry) {
39
+ if (stack.depth <= 0) {
40
+ return { past: [], future: [], depth: stack.depth };
41
+ }
42
+ const past = [...stack.past, entry];
43
+ while (past.length > stack.depth) past.shift();
44
+ return { past, future: [], depth: stack.depth };
45
+ }
46
+ function popUndo(stack) {
47
+ const entry = stack.past[stack.past.length - 1];
48
+ if (entry === void 0) return { stack, entry: null };
49
+ return {
50
+ stack: {
51
+ past: stack.past.slice(0, -1),
52
+ future: [...stack.future, entry],
53
+ depth: stack.depth
54
+ },
55
+ entry
56
+ };
57
+ }
58
+ function popRedo(stack) {
59
+ const entry = stack.future[stack.future.length - 1];
60
+ if (entry === void 0) return { stack, entry: null };
61
+ const past = [...stack.past, entry];
62
+ while (past.length > stack.depth) past.shift();
63
+ return {
64
+ stack: { past, future: stack.future.slice(0, -1), depth: stack.depth },
65
+ entry
66
+ };
67
+ }
68
+ function keyOf(patch) {
69
+ return JSON.stringify([patch.rowId, patch.columnId]);
70
+ }
71
+ function undoEntryFor(before, after, label) {
72
+ const priors = /* @__PURE__ */ new Map();
73
+ for (const patch of before) priors.set(keyOf(patch), patch);
74
+ const undo = after.map((patch) => {
75
+ const prior = priors.get(keyOf(patch));
76
+ return {
77
+ rowId: patch.rowId,
78
+ columnId: patch.columnId,
79
+ value: prior ? prior.value : null
80
+ };
81
+ });
82
+ return {
83
+ label,
84
+ undo,
85
+ redo: after.map((patch) => ({
86
+ rowId: patch.rowId,
87
+ columnId: patch.columnId,
88
+ value: patch.value
89
+ }))
90
+ };
91
+ }
92
+ function canUndo(stack) {
93
+ return stack.depth > 0 && stack.past.length > 0;
94
+ }
95
+ function canRedo(stack) {
96
+ return stack.depth > 0 && stack.future.length > 0;
97
+ }
98
+ //# sourceMappingURL=cell-undo.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/data-table/cell-undo.ts"],"sourcesContent":["/**\n * THE CELL UNDO STACK — the model behind Cmd-Z in a grid.\n *\n * WHY IT SHIPS WITH EASY EDITING, NOT AFTER IT. Lowering the friction to change\n * data raises the rate of accidental change: a click toggles a boolean, a bare\n * keystroke starts an edit, Delete empties a cell, and one drag of the fill\n * handle rewrites a column. Every one of those is a good affordance ONLY if the\n * floor underneath it is recoverable.\n *\n * WHAT THIS FILE IS. A bounded, PURE stack of reversible edits: no React, no\n * network, no toasts. The app's `useCellUndo` hook owned all three plus the\n * stack, which meant the stack's rules — depth, the redo contract, what an undo\n * of an added row means — could only be exercised by mounting a component and\n * mocking a database. Those rules are spreadsheet law, not app trivia, so they\n * belong in a module anyone can test in a millisecond and any client can reuse.\n *\n * WHY IT IS PATCHES, NOT CELL COORDINATES. An entry carries the patches that\n * undo it and the patches that redo it, captured BEFORE the write and never\n * derived after it: re-reading a cell later to discover what to restore races\n * with realtime and with agent writes, and would happily \"undo\" to a value\n * somebody else just set. Patches also make the entry shape indifferent to WHAT\n * changed — one cell, a filled column, or the cells of a row that was added or\n * deleted — which is how row and column operations join the same Cmd-Z the user\n * already trusts instead of living outside it.\n *\n * PURITY IS THE POINT. Every function returns a NEW stack and never mutates the\n * one it was handed, so the stack can live in React state, in a store, or in a\n * ref without the \"my undo history changed under me\" class of bug.\n */\n\nimport type { CellPatch } from \"./fill\";\n\nexport type { CellPatch };\n\nexport interface UndoEntry {\n /** What the user did, in plain English, for the toast: \"Undid the change to Day rate.\" */\n label: string;\n /** The patches that UNDO it. */\n undo: CellPatch[];\n /** The patches that REDO it. */\n redo: CellPatch[];\n}\n\nexport interface UndoStack {\n past: UndoEntry[];\n future: UndoEntry[];\n depth: number;\n}\n\n/**\n * How many steps back we remember. Deliberately generous — the cost is a few\n * kilobytes of session memory, and the failure mode of a too-short stack is a\n * user discovering their mistake is unrecoverable.\n */\nexport const DEFAULT_UNDO_DEPTH = 100;\n\nexport function createUndoStack(depth: number = DEFAULT_UNDO_DEPTH): UndoStack {\n const bounded = Number.isFinite(depth) && depth > 0 ? Math.floor(depth) : 0;\n return { past: [], future: [], depth: bounded };\n}\n\n/**\n * Record an edit that already happened.\n *\n * Pushing CLEARS the redo future — the standard editor contract: you cannot\n * redo into a future you have just diverged from. Past `depth`, the OLDEST\n * entry is dropped, so the stack is bounded memory rather than a leak.\n */\nexport function pushUndo(stack: UndoStack, entry: UndoEntry): UndoStack {\n if (stack.depth <= 0) {\n // Undo is off. Pushing keeps the stack empty rather than silently growing\n // a history nothing will ever read.\n return { past: [], future: [], depth: stack.depth };\n }\n const past = [...stack.past, entry];\n while (past.length > stack.depth) past.shift();\n return { past, future: [], depth: stack.depth };\n}\n\nexport function popUndo(stack: UndoStack): { stack: UndoStack; entry: UndoEntry | null } {\n const entry = stack.past[stack.past.length - 1];\n if (entry === undefined) return { stack, entry: null };\n return {\n stack: {\n past: stack.past.slice(0, -1),\n future: [...stack.future, entry],\n depth: stack.depth,\n },\n entry,\n };\n}\n\nexport function popRedo(stack: UndoStack): { stack: UndoStack; entry: UndoEntry | null } {\n const entry = stack.future[stack.future.length - 1];\n if (entry === undefined) return { stack, entry: null };\n const past = [...stack.past, entry];\n while (past.length > stack.depth) past.shift();\n return {\n stack: { past, future: stack.future.slice(0, -1), depth: stack.depth },\n entry,\n };\n}\n\nfunction keyOf(patch: CellPatch): string {\n // JSON, not a separator character: a row id or column id containing whatever\n // separator we picked would collide two different cells into one key, and the\n // undo would restore the wrong one.\n return JSON.stringify([patch.rowId, patch.columnId]);\n}\n\n/**\n * Build an entry from a before/after pair, so a caller never assembles one by\n * hand — hand-assembled inverses are how an undo ends up restoring the wrong\n * cell.\n *\n * An after-patch with NO matching before is still undoable: its undo sets the\n * value back to `null`. That is the added-row case — a cell that did not exist\n * before the change has no prior value, and \"undo\" for it means empty, not\n * \"leave whatever is there\".\n */\nexport function undoEntryFor(\n before: CellPatch[],\n after: CellPatch[],\n label: string,\n): UndoEntry {\n const priors = new Map<string, CellPatch>();\n for (const patch of before) priors.set(keyOf(patch), patch);\n\n const undo: CellPatch[] = after.map((patch) => {\n const prior = priors.get(keyOf(patch));\n return {\n rowId: patch.rowId,\n columnId: patch.columnId,\n value: prior ? prior.value : null,\n };\n });\n\n return {\n label,\n undo,\n redo: after.map((patch) => ({\n rowId: patch.rowId,\n columnId: patch.columnId,\n value: patch.value,\n })),\n };\n}\n\n/** True when there is a step to take back. */\nexport function canUndo(stack: UndoStack): boolean {\n return stack.depth > 0 && stack.past.length > 0;\n}\n\n/** True when there is a step to reapply. */\nexport function canRedo(stack: UndoStack): boolean {\n return stack.depth > 0 && stack.future.length > 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsDO,IAAM,qBAAqB;AAE3B,SAAS,gBAAgB,QAAgB,oBAA+B;AAC7E,QAAM,UAAU,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAI,KAAK,MAAM,KAAK,IAAI;AAC1E,SAAO,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,OAAO,QAAQ;AAChD;AASO,SAAS,SAAS,OAAkB,OAA6B;AACtE,MAAI,MAAM,SAAS,GAAG;AAGpB,WAAO,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,OAAO,MAAM,MAAM;AAAA,EACpD;AACA,QAAM,OAAO,CAAC,GAAG,MAAM,MAAM,KAAK;AAClC,SAAO,KAAK,SAAS,MAAM,MAAO,MAAK,MAAM;AAC7C,SAAO,EAAE,MAAM,QAAQ,CAAC,GAAG,OAAO,MAAM,MAAM;AAChD;AAEO,SAAS,QAAQ,OAAiE;AACvF,QAAM,QAAQ,MAAM,KAAK,MAAM,KAAK,SAAS,CAAC;AAC9C,MAAI,UAAU,OAAW,QAAO,EAAE,OAAO,OAAO,KAAK;AACrD,SAAO;AAAA,IACL,OAAO;AAAA,MACL,MAAM,MAAM,KAAK,MAAM,GAAG,EAAE;AAAA,MAC5B,QAAQ,CAAC,GAAG,MAAM,QAAQ,KAAK;AAAA,MAC/B,OAAO,MAAM;AAAA,IACf;AAAA,IACA;AAAA,EACF;AACF;AAEO,SAAS,QAAQ,OAAiE;AACvF,QAAM,QAAQ,MAAM,OAAO,MAAM,OAAO,SAAS,CAAC;AAClD,MAAI,UAAU,OAAW,QAAO,EAAE,OAAO,OAAO,KAAK;AACrD,QAAM,OAAO,CAAC,GAAG,MAAM,MAAM,KAAK;AAClC,SAAO,KAAK,SAAS,MAAM,MAAO,MAAK,MAAM;AAC7C,SAAO;AAAA,IACL,OAAO,EAAE,MAAM,QAAQ,MAAM,OAAO,MAAM,GAAG,EAAE,GAAG,OAAO,MAAM,MAAM;AAAA,IACrE;AAAA,EACF;AACF;AAEA,SAAS,MAAM,OAA0B;AAIvC,SAAO,KAAK,UAAU,CAAC,MAAM,OAAO,MAAM,QAAQ,CAAC;AACrD;AAYO,SAAS,aACd,QACA,OACA,OACW;AACX,QAAM,SAAS,oBAAI,IAAuB;AAC1C,aAAW,SAAS,OAAQ,QAAO,IAAI,MAAM,KAAK,GAAG,KAAK;AAE1D,QAAM,OAAoB,MAAM,IAAI,CAAC,UAAU;AAC7C,UAAM,QAAQ,OAAO,IAAI,MAAM,KAAK,CAAC;AACrC,WAAO;AAAA,MACL,OAAO,MAAM;AAAA,MACb,UAAU,MAAM;AAAA,MAChB,OAAO,QAAQ,MAAM,QAAQ;AAAA,IAC/B;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,MAAM,MAAM,IAAI,CAAC,WAAW;AAAA,MAC1B,OAAO,MAAM;AAAA,MACb,UAAU,MAAM;AAAA,MAChB,OAAO,MAAM;AAAA,IACf,EAAE;AAAA,EACJ;AACF;AAGO,SAAS,QAAQ,OAA2B;AACjD,SAAO,MAAM,QAAQ,KAAK,MAAM,KAAK,SAAS;AAChD;AAGO,SAAS,QAAQ,OAA2B;AACjD,SAAO,MAAM,QAAQ,KAAK,MAAM,OAAO,SAAS;AAClD;","names":[]}
@@ -0,0 +1,85 @@
1
+ import { CellPatch } from './fill.cjs';
2
+
3
+ /**
4
+ * THE CELL UNDO STACK — the model behind Cmd-Z in a grid.
5
+ *
6
+ * WHY IT SHIPS WITH EASY EDITING, NOT AFTER IT. Lowering the friction to change
7
+ * data raises the rate of accidental change: a click toggles a boolean, a bare
8
+ * keystroke starts an edit, Delete empties a cell, and one drag of the fill
9
+ * handle rewrites a column. Every one of those is a good affordance ONLY if the
10
+ * floor underneath it is recoverable.
11
+ *
12
+ * WHAT THIS FILE IS. A bounded, PURE stack of reversible edits: no React, no
13
+ * network, no toasts. The app's `useCellUndo` hook owned all three plus the
14
+ * stack, which meant the stack's rules — depth, the redo contract, what an undo
15
+ * of an added row means — could only be exercised by mounting a component and
16
+ * mocking a database. Those rules are spreadsheet law, not app trivia, so they
17
+ * belong in a module anyone can test in a millisecond and any client can reuse.
18
+ *
19
+ * WHY IT IS PATCHES, NOT CELL COORDINATES. An entry carries the patches that
20
+ * undo it and the patches that redo it, captured BEFORE the write and never
21
+ * derived after it: re-reading a cell later to discover what to restore races
22
+ * with realtime and with agent writes, and would happily "undo" to a value
23
+ * somebody else just set. Patches also make the entry shape indifferent to WHAT
24
+ * changed — one cell, a filled column, or the cells of a row that was added or
25
+ * deleted — which is how row and column operations join the same Cmd-Z the user
26
+ * already trusts instead of living outside it.
27
+ *
28
+ * PURITY IS THE POINT. Every function returns a NEW stack and never mutates the
29
+ * one it was handed, so the stack can live in React state, in a store, or in a
30
+ * ref without the "my undo history changed under me" class of bug.
31
+ */
32
+
33
+ interface UndoEntry {
34
+ /** What the user did, in plain English, for the toast: "Undid the change to Day rate." */
35
+ label: string;
36
+ /** The patches that UNDO it. */
37
+ undo: CellPatch[];
38
+ /** The patches that REDO it. */
39
+ redo: CellPatch[];
40
+ }
41
+ interface UndoStack {
42
+ past: UndoEntry[];
43
+ future: UndoEntry[];
44
+ depth: number;
45
+ }
46
+ /**
47
+ * How many steps back we remember. Deliberately generous — the cost is a few
48
+ * kilobytes of session memory, and the failure mode of a too-short stack is a
49
+ * user discovering their mistake is unrecoverable.
50
+ */
51
+ declare const DEFAULT_UNDO_DEPTH = 100;
52
+ declare function createUndoStack(depth?: number): UndoStack;
53
+ /**
54
+ * Record an edit that already happened.
55
+ *
56
+ * Pushing CLEARS the redo future — the standard editor contract: you cannot
57
+ * redo into a future you have just diverged from. Past `depth`, the OLDEST
58
+ * entry is dropped, so the stack is bounded memory rather than a leak.
59
+ */
60
+ declare function pushUndo(stack: UndoStack, entry: UndoEntry): UndoStack;
61
+ declare function popUndo(stack: UndoStack): {
62
+ stack: UndoStack;
63
+ entry: UndoEntry | null;
64
+ };
65
+ declare function popRedo(stack: UndoStack): {
66
+ stack: UndoStack;
67
+ entry: UndoEntry | null;
68
+ };
69
+ /**
70
+ * Build an entry from a before/after pair, so a caller never assembles one by
71
+ * hand — hand-assembled inverses are how an undo ends up restoring the wrong
72
+ * cell.
73
+ *
74
+ * An after-patch with NO matching before is still undoable: its undo sets the
75
+ * value back to `null`. That is the added-row case — a cell that did not exist
76
+ * before the change has no prior value, and "undo" for it means empty, not
77
+ * "leave whatever is there".
78
+ */
79
+ declare function undoEntryFor(before: CellPatch[], after: CellPatch[], label: string): UndoEntry;
80
+ /** True when there is a step to take back. */
81
+ declare function canUndo(stack: UndoStack): boolean;
82
+ /** True when there is a step to reapply. */
83
+ declare function canRedo(stack: UndoStack): boolean;
84
+
85
+ export { CellPatch, DEFAULT_UNDO_DEPTH, type UndoEntry, type UndoStack, canRedo, canUndo, createUndoStack, popRedo, popUndo, pushUndo, undoEntryFor };
@@ -0,0 +1,85 @@
1
+ import { CellPatch } from './fill.js';
2
+
3
+ /**
4
+ * THE CELL UNDO STACK — the model behind Cmd-Z in a grid.
5
+ *
6
+ * WHY IT SHIPS WITH EASY EDITING, NOT AFTER IT. Lowering the friction to change
7
+ * data raises the rate of accidental change: a click toggles a boolean, a bare
8
+ * keystroke starts an edit, Delete empties a cell, and one drag of the fill
9
+ * handle rewrites a column. Every one of those is a good affordance ONLY if the
10
+ * floor underneath it is recoverable.
11
+ *
12
+ * WHAT THIS FILE IS. A bounded, PURE stack of reversible edits: no React, no
13
+ * network, no toasts. The app's `useCellUndo` hook owned all three plus the
14
+ * stack, which meant the stack's rules — depth, the redo contract, what an undo
15
+ * of an added row means — could only be exercised by mounting a component and
16
+ * mocking a database. Those rules are spreadsheet law, not app trivia, so they
17
+ * belong in a module anyone can test in a millisecond and any client can reuse.
18
+ *
19
+ * WHY IT IS PATCHES, NOT CELL COORDINATES. An entry carries the patches that
20
+ * undo it and the patches that redo it, captured BEFORE the write and never
21
+ * derived after it: re-reading a cell later to discover what to restore races
22
+ * with realtime and with agent writes, and would happily "undo" to a value
23
+ * somebody else just set. Patches also make the entry shape indifferent to WHAT
24
+ * changed — one cell, a filled column, or the cells of a row that was added or
25
+ * deleted — which is how row and column operations join the same Cmd-Z the user
26
+ * already trusts instead of living outside it.
27
+ *
28
+ * PURITY IS THE POINT. Every function returns a NEW stack and never mutates the
29
+ * one it was handed, so the stack can live in React state, in a store, or in a
30
+ * ref without the "my undo history changed under me" class of bug.
31
+ */
32
+
33
+ interface UndoEntry {
34
+ /** What the user did, in plain English, for the toast: "Undid the change to Day rate." */
35
+ label: string;
36
+ /** The patches that UNDO it. */
37
+ undo: CellPatch[];
38
+ /** The patches that REDO it. */
39
+ redo: CellPatch[];
40
+ }
41
+ interface UndoStack {
42
+ past: UndoEntry[];
43
+ future: UndoEntry[];
44
+ depth: number;
45
+ }
46
+ /**
47
+ * How many steps back we remember. Deliberately generous — the cost is a few
48
+ * kilobytes of session memory, and the failure mode of a too-short stack is a
49
+ * user discovering their mistake is unrecoverable.
50
+ */
51
+ declare const DEFAULT_UNDO_DEPTH = 100;
52
+ declare function createUndoStack(depth?: number): UndoStack;
53
+ /**
54
+ * Record an edit that already happened.
55
+ *
56
+ * Pushing CLEARS the redo future — the standard editor contract: you cannot
57
+ * redo into a future you have just diverged from. Past `depth`, the OLDEST
58
+ * entry is dropped, so the stack is bounded memory rather than a leak.
59
+ */
60
+ declare function pushUndo(stack: UndoStack, entry: UndoEntry): UndoStack;
61
+ declare function popUndo(stack: UndoStack): {
62
+ stack: UndoStack;
63
+ entry: UndoEntry | null;
64
+ };
65
+ declare function popRedo(stack: UndoStack): {
66
+ stack: UndoStack;
67
+ entry: UndoEntry | null;
68
+ };
69
+ /**
70
+ * Build an entry from a before/after pair, so a caller never assembles one by
71
+ * hand — hand-assembled inverses are how an undo ends up restoring the wrong
72
+ * cell.
73
+ *
74
+ * An after-patch with NO matching before is still undoable: its undo sets the
75
+ * value back to `null`. That is the added-row case — a cell that did not exist
76
+ * before the change has no prior value, and "undo" for it means empty, not
77
+ * "leave whatever is there".
78
+ */
79
+ declare function undoEntryFor(before: CellPatch[], after: CellPatch[], label: string): UndoEntry;
80
+ /** True when there is a step to take back. */
81
+ declare function canUndo(stack: UndoStack): boolean;
82
+ /** True when there is a step to reapply. */
83
+ declare function canRedo(stack: UndoStack): boolean;
84
+
85
+ export { CellPatch, DEFAULT_UNDO_DEPTH, type UndoEntry, type UndoStack, canRedo, canUndo, createUndoStack, popRedo, popUndo, pushUndo, undoEntryFor };
@@ -0,0 +1,78 @@
1
+
2
+ // src/data-table/cell-undo.ts
3
+ var DEFAULT_UNDO_DEPTH = 100;
4
+ function createUndoStack(depth = DEFAULT_UNDO_DEPTH) {
5
+ const bounded = Number.isFinite(depth) && depth > 0 ? Math.floor(depth) : 0;
6
+ return { past: [], future: [], depth: bounded };
7
+ }
8
+ function pushUndo(stack, entry) {
9
+ if (stack.depth <= 0) {
10
+ return { past: [], future: [], depth: stack.depth };
11
+ }
12
+ const past = [...stack.past, entry];
13
+ while (past.length > stack.depth) past.shift();
14
+ return { past, future: [], depth: stack.depth };
15
+ }
16
+ function popUndo(stack) {
17
+ const entry = stack.past[stack.past.length - 1];
18
+ if (entry === void 0) return { stack, entry: null };
19
+ return {
20
+ stack: {
21
+ past: stack.past.slice(0, -1),
22
+ future: [...stack.future, entry],
23
+ depth: stack.depth
24
+ },
25
+ entry
26
+ };
27
+ }
28
+ function popRedo(stack) {
29
+ const entry = stack.future[stack.future.length - 1];
30
+ if (entry === void 0) return { stack, entry: null };
31
+ const past = [...stack.past, entry];
32
+ while (past.length > stack.depth) past.shift();
33
+ return {
34
+ stack: { past, future: stack.future.slice(0, -1), depth: stack.depth },
35
+ entry
36
+ };
37
+ }
38
+ function keyOf(patch) {
39
+ return JSON.stringify([patch.rowId, patch.columnId]);
40
+ }
41
+ function undoEntryFor(before, after, label) {
42
+ const priors = /* @__PURE__ */ new Map();
43
+ for (const patch of before) priors.set(keyOf(patch), patch);
44
+ const undo = after.map((patch) => {
45
+ const prior = priors.get(keyOf(patch));
46
+ return {
47
+ rowId: patch.rowId,
48
+ columnId: patch.columnId,
49
+ value: prior ? prior.value : null
50
+ };
51
+ });
52
+ return {
53
+ label,
54
+ undo,
55
+ redo: after.map((patch) => ({
56
+ rowId: patch.rowId,
57
+ columnId: patch.columnId,
58
+ value: patch.value
59
+ }))
60
+ };
61
+ }
62
+ function canUndo(stack) {
63
+ return stack.depth > 0 && stack.past.length > 0;
64
+ }
65
+ function canRedo(stack) {
66
+ return stack.depth > 0 && stack.future.length > 0;
67
+ }
68
+ export {
69
+ DEFAULT_UNDO_DEPTH,
70
+ canRedo,
71
+ canUndo,
72
+ createUndoStack,
73
+ popRedo,
74
+ popUndo,
75
+ pushUndo,
76
+ undoEntryFor
77
+ };
78
+ //# sourceMappingURL=cell-undo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/data-table/cell-undo.ts"],"sourcesContent":["/**\n * THE CELL UNDO STACK — the model behind Cmd-Z in a grid.\n *\n * WHY IT SHIPS WITH EASY EDITING, NOT AFTER IT. Lowering the friction to change\n * data raises the rate of accidental change: a click toggles a boolean, a bare\n * keystroke starts an edit, Delete empties a cell, and one drag of the fill\n * handle rewrites a column. Every one of those is a good affordance ONLY if the\n * floor underneath it is recoverable.\n *\n * WHAT THIS FILE IS. A bounded, PURE stack of reversible edits: no React, no\n * network, no toasts. The app's `useCellUndo` hook owned all three plus the\n * stack, which meant the stack's rules — depth, the redo contract, what an undo\n * of an added row means — could only be exercised by mounting a component and\n * mocking a database. Those rules are spreadsheet law, not app trivia, so they\n * belong in a module anyone can test in a millisecond and any client can reuse.\n *\n * WHY IT IS PATCHES, NOT CELL COORDINATES. An entry carries the patches that\n * undo it and the patches that redo it, captured BEFORE the write and never\n * derived after it: re-reading a cell later to discover what to restore races\n * with realtime and with agent writes, and would happily \"undo\" to a value\n * somebody else just set. Patches also make the entry shape indifferent to WHAT\n * changed — one cell, a filled column, or the cells of a row that was added or\n * deleted — which is how row and column operations join the same Cmd-Z the user\n * already trusts instead of living outside it.\n *\n * PURITY IS THE POINT. Every function returns a NEW stack and never mutates the\n * one it was handed, so the stack can live in React state, in a store, or in a\n * ref without the \"my undo history changed under me\" class of bug.\n */\n\nimport type { CellPatch } from \"./fill\";\n\nexport type { CellPatch };\n\nexport interface UndoEntry {\n /** What the user did, in plain English, for the toast: \"Undid the change to Day rate.\" */\n label: string;\n /** The patches that UNDO it. */\n undo: CellPatch[];\n /** The patches that REDO it. */\n redo: CellPatch[];\n}\n\nexport interface UndoStack {\n past: UndoEntry[];\n future: UndoEntry[];\n depth: number;\n}\n\n/**\n * How many steps back we remember. Deliberately generous — the cost is a few\n * kilobytes of session memory, and the failure mode of a too-short stack is a\n * user discovering their mistake is unrecoverable.\n */\nexport const DEFAULT_UNDO_DEPTH = 100;\n\nexport function createUndoStack(depth: number = DEFAULT_UNDO_DEPTH): UndoStack {\n const bounded = Number.isFinite(depth) && depth > 0 ? Math.floor(depth) : 0;\n return { past: [], future: [], depth: bounded };\n}\n\n/**\n * Record an edit that already happened.\n *\n * Pushing CLEARS the redo future — the standard editor contract: you cannot\n * redo into a future you have just diverged from. Past `depth`, the OLDEST\n * entry is dropped, so the stack is bounded memory rather than a leak.\n */\nexport function pushUndo(stack: UndoStack, entry: UndoEntry): UndoStack {\n if (stack.depth <= 0) {\n // Undo is off. Pushing keeps the stack empty rather than silently growing\n // a history nothing will ever read.\n return { past: [], future: [], depth: stack.depth };\n }\n const past = [...stack.past, entry];\n while (past.length > stack.depth) past.shift();\n return { past, future: [], depth: stack.depth };\n}\n\nexport function popUndo(stack: UndoStack): { stack: UndoStack; entry: UndoEntry | null } {\n const entry = stack.past[stack.past.length - 1];\n if (entry === undefined) return { stack, entry: null };\n return {\n stack: {\n past: stack.past.slice(0, -1),\n future: [...stack.future, entry],\n depth: stack.depth,\n },\n entry,\n };\n}\n\nexport function popRedo(stack: UndoStack): { stack: UndoStack; entry: UndoEntry | null } {\n const entry = stack.future[stack.future.length - 1];\n if (entry === undefined) return { stack, entry: null };\n const past = [...stack.past, entry];\n while (past.length > stack.depth) past.shift();\n return {\n stack: { past, future: stack.future.slice(0, -1), depth: stack.depth },\n entry,\n };\n}\n\nfunction keyOf(patch: CellPatch): string {\n // JSON, not a separator character: a row id or column id containing whatever\n // separator we picked would collide two different cells into one key, and the\n // undo would restore the wrong one.\n return JSON.stringify([patch.rowId, patch.columnId]);\n}\n\n/**\n * Build an entry from a before/after pair, so a caller never assembles one by\n * hand — hand-assembled inverses are how an undo ends up restoring the wrong\n * cell.\n *\n * An after-patch with NO matching before is still undoable: its undo sets the\n * value back to `null`. That is the added-row case — a cell that did not exist\n * before the change has no prior value, and \"undo\" for it means empty, not\n * \"leave whatever is there\".\n */\nexport function undoEntryFor(\n before: CellPatch[],\n after: CellPatch[],\n label: string,\n): UndoEntry {\n const priors = new Map<string, CellPatch>();\n for (const patch of before) priors.set(keyOf(patch), patch);\n\n const undo: CellPatch[] = after.map((patch) => {\n const prior = priors.get(keyOf(patch));\n return {\n rowId: patch.rowId,\n columnId: patch.columnId,\n value: prior ? prior.value : null,\n };\n });\n\n return {\n label,\n undo,\n redo: after.map((patch) => ({\n rowId: patch.rowId,\n columnId: patch.columnId,\n value: patch.value,\n })),\n };\n}\n\n/** True when there is a step to take back. */\nexport function canUndo(stack: UndoStack): boolean {\n return stack.depth > 0 && stack.past.length > 0;\n}\n\n/** True when there is a step to reapply. */\nexport function canRedo(stack: UndoStack): boolean {\n return stack.depth > 0 && stack.future.length > 0;\n}\n"],"mappings":";;AAsDO,IAAM,qBAAqB;AAE3B,SAAS,gBAAgB,QAAgB,oBAA+B;AAC7E,QAAM,UAAU,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAI,KAAK,MAAM,KAAK,IAAI;AAC1E,SAAO,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,OAAO,QAAQ;AAChD;AASO,SAAS,SAAS,OAAkB,OAA6B;AACtE,MAAI,MAAM,SAAS,GAAG;AAGpB,WAAO,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,OAAO,MAAM,MAAM;AAAA,EACpD;AACA,QAAM,OAAO,CAAC,GAAG,MAAM,MAAM,KAAK;AAClC,SAAO,KAAK,SAAS,MAAM,MAAO,MAAK,MAAM;AAC7C,SAAO,EAAE,MAAM,QAAQ,CAAC,GAAG,OAAO,MAAM,MAAM;AAChD;AAEO,SAAS,QAAQ,OAAiE;AACvF,QAAM,QAAQ,MAAM,KAAK,MAAM,KAAK,SAAS,CAAC;AAC9C,MAAI,UAAU,OAAW,QAAO,EAAE,OAAO,OAAO,KAAK;AACrD,SAAO;AAAA,IACL,OAAO;AAAA,MACL,MAAM,MAAM,KAAK,MAAM,GAAG,EAAE;AAAA,MAC5B,QAAQ,CAAC,GAAG,MAAM,QAAQ,KAAK;AAAA,MAC/B,OAAO,MAAM;AAAA,IACf;AAAA,IACA;AAAA,EACF;AACF;AAEO,SAAS,QAAQ,OAAiE;AACvF,QAAM,QAAQ,MAAM,OAAO,MAAM,OAAO,SAAS,CAAC;AAClD,MAAI,UAAU,OAAW,QAAO,EAAE,OAAO,OAAO,KAAK;AACrD,QAAM,OAAO,CAAC,GAAG,MAAM,MAAM,KAAK;AAClC,SAAO,KAAK,SAAS,MAAM,MAAO,MAAK,MAAM;AAC7C,SAAO;AAAA,IACL,OAAO,EAAE,MAAM,QAAQ,MAAM,OAAO,MAAM,GAAG,EAAE,GAAG,OAAO,MAAM,MAAM;AAAA,IACrE;AAAA,EACF;AACF;AAEA,SAAS,MAAM,OAA0B;AAIvC,SAAO,KAAK,UAAU,CAAC,MAAM,OAAO,MAAM,QAAQ,CAAC;AACrD;AAYO,SAAS,aACd,QACA,OACA,OACW;AACX,QAAM,SAAS,oBAAI,IAAuB;AAC1C,aAAW,SAAS,OAAQ,QAAO,IAAI,MAAM,KAAK,GAAG,KAAK;AAE1D,QAAM,OAAoB,MAAM,IAAI,CAAC,UAAU;AAC7C,UAAM,QAAQ,OAAO,IAAI,MAAM,KAAK,CAAC;AACrC,WAAO;AAAA,MACL,OAAO,MAAM;AAAA,MACb,UAAU,MAAM;AAAA,MAChB,OAAO,QAAQ,MAAM,QAAQ;AAAA,IAC/B;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,MAAM,MAAM,IAAI,CAAC,WAAW;AAAA,MAC1B,OAAO,MAAM;AAAA,MACb,UAAU,MAAM;AAAA,MAChB,OAAO,MAAM;AAAA,IACf,EAAE;AAAA,EACJ;AACF;AAGO,SAAS,QAAQ,OAA2B;AACjD,SAAO,MAAM,QAAQ,KAAK,MAAM,KAAK,SAAS;AAChD;AAGO,SAAS,QAAQ,OAA2B;AACjD,SAAO,MAAM,QAAQ,KAAK,MAAM,OAAO,SAAS;AAClD;","names":[]}
@@ -1,10 +1,15 @@
1
1
  import { AgentPayloadInput } from './copy-types.cjs';
2
- import { g as MatrxDataTableCopyConfig, M as MatrxColumnDef } from '../types-Sn249eoy.cjs';
2
+ import { g as MatrxDataTableCopyConfig, M as MatrxColumnDef } from '../types-DrJXAAoD.cjs';
3
3
  import '../content-transfer.cjs';
4
4
  import 'react';
5
5
  import '@ai-matrx/kit/content-transfer';
6
6
  import '../content-transfer/icon.cjs';
7
- import '../types-BdbvlF9-.cjs';
7
+ import '../types-CJWU1Gae.cjs';
8
+ import './grouping.cjs';
9
+ import './fill.cjs';
10
+ import './facets.cjs';
11
+ import './spreadsheet.cjs';
12
+ import './grid-selection.cjs';
8
13
 
9
14
  /**
10
15
  * Typed, declared-column capture for MatrxDataTable's built-in Alchemy source.
@@ -1,10 +1,15 @@
1
1
  import { AgentPayloadInput } from './copy-types.js';
2
- import { g as MatrxDataTableCopyConfig, M as MatrxColumnDef } from '../types-D-flTpK0.js';
2
+ import { g as MatrxDataTableCopyConfig, M as MatrxColumnDef } from '../types-Cu5_eUG1.js';
3
3
  import '../content-transfer.js';
4
4
  import 'react';
5
5
  import '@ai-matrx/kit/content-transfer';
6
6
  import '../content-transfer/icon.js';
7
- import '../types-BdbvlF9-.js';
7
+ import '../types-CJWU1Gae.js';
8
+ import './grouping.js';
9
+ import './fill.js';
10
+ import './facets.js';
11
+ import './spreadsheet.js';
12
+ import './grid-selection.js';
8
13
 
9
14
  /**
10
15
  * Typed, declared-column capture for MatrxDataTable's built-in Alchemy source.