@ai-matrx/design-system 0.22.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +151 -0
- package/dist/data-table/cell-undo.cjs +98 -0
- package/dist/data-table/cell-undo.cjs.map +1 -0
- package/dist/data-table/cell-undo.d.cts +85 -0
- package/dist/data-table/cell-undo.d.ts +85 -0
- package/dist/data-table/cell-undo.js +78 -0
- package/dist/data-table/cell-undo.js.map +1 -0
- package/dist/data-table/copy-helpers.d.cts +6 -2
- package/dist/data-table/copy-helpers.d.ts +6 -2
- package/dist/data-table/coverage.cjs +116 -0
- package/dist/data-table/coverage.cjs.map +1 -0
- package/dist/data-table/coverage.d.cts +65 -0
- package/dist/data-table/coverage.d.ts +65 -0
- package/dist/data-table/coverage.js +96 -0
- package/dist/data-table/coverage.js.map +1 -0
- package/dist/data-table/fill.cjs +299 -0
- package/dist/data-table/fill.cjs.map +1 -0
- package/dist/data-table/fill.d.cts +129 -0
- package/dist/data-table/fill.d.ts +129 -0
- package/dist/data-table/fill.js +277 -0
- package/dist/data-table/fill.js.map +1 -0
- package/dist/data-table/filter-engine.d.cts +6 -2
- package/dist/data-table/filter-engine.d.ts +6 -2
- package/dist/data-table/grid-clipboard.cjs +165 -0
- package/dist/data-table/grid-clipboard.cjs.map +1 -0
- package/dist/data-table/grid-clipboard.d.cts +85 -0
- package/dist/data-table/grid-clipboard.d.ts +85 -0
- package/dist/data-table/grid-clipboard.js +145 -0
- package/dist/data-table/grid-clipboard.js.map +1 -0
- package/dist/data-table/grid-selection.cjs +258 -0
- package/dist/data-table/grid-selection.cjs.map +1 -0
- package/dist/data-table/grid-selection.d.cts +166 -0
- package/dist/data-table/grid-selection.d.ts +166 -0
- package/dist/data-table/grid-selection.js +238 -0
- package/dist/data-table/grid-selection.js.map +1 -0
- package/dist/data-table/grouping.cjs +302 -0
- package/dist/data-table/grouping.cjs.map +1 -0
- package/dist/data-table/grouping.d.cts +119 -0
- package/dist/data-table/grouping.d.ts +119 -0
- package/dist/data-table/grouping.js +282 -0
- package/dist/data-table/grouping.js.map +1 -0
- package/dist/data-table/host.d.cts +6 -2
- package/dist/data-table/host.d.ts +6 -2
- package/dist/data-table/index.cjs +2433 -246
- package/dist/data-table/index.cjs.map +1 -1
- package/dist/data-table/index.d.cts +11 -3
- package/dist/data-table/index.d.ts +11 -3
- package/dist/data-table/index.js +2425 -238
- package/dist/data-table/index.js.map +1 -1
- package/dist/data-table/infer-filter.d.cts +6 -2
- package/dist/data-table/infer-filter.d.ts +6 -2
- package/dist/data-table/layered-filters.d.cts +6 -2
- package/dist/data-table/layered-filters.d.ts +6 -2
- package/dist/data-table/query-control.d.cts +6 -2
- package/dist/data-table/query-control.d.ts +6 -2
- package/dist/data-table/spreadsheet.cjs +1014 -0
- package/dist/data-table/spreadsheet.cjs.map +1 -0
- package/dist/data-table/spreadsheet.d.cts +118 -0
- package/dist/data-table/spreadsheet.d.ts +118 -0
- package/dist/data-table/spreadsheet.js +994 -0
- package/dist/data-table/spreadsheet.js.map +1 -0
- package/dist/data-table/types.cjs.map +1 -1
- package/dist/data-table/types.d.cts +6 -2
- package/dist/data-table/types.d.ts +6 -2
- package/dist/data-table/url-state.d.cts +6 -2
- package/dist/data-table/url-state.d.ts +6 -2
- package/dist/data-table/validation.cjs +229 -0
- package/dist/data-table/validation.cjs.map +1 -0
- package/dist/data-table/validation.d.cts +125 -0
- package/dist/data-table/validation.d.ts +125 -0
- package/dist/data-table/validation.js +209 -0
- package/dist/data-table/validation.js.map +1 -0
- package/dist/data-table/virtual-window.cjs +203 -0
- package/dist/data-table/virtual-window.cjs.map +1 -0
- package/dist/data-table/virtual-window.d.cts +84 -0
- package/dist/data-table/virtual-window.d.ts +84 -0
- package/dist/data-table/virtual-window.js +183 -0
- package/dist/data-table/virtual-window.js.map +1 -0
- package/dist/data-table/xlsx.d.cts +6 -2
- package/dist/data-table/xlsx.d.ts +6 -2
- package/dist/field-formats/index.d.cts +2 -2
- package/dist/field-formats/index.d.ts +2 -2
- package/dist/formulas/index.cjs +1041 -0
- package/dist/formulas/index.cjs.map +1 -0
- package/dist/formulas/index.d.cts +389 -0
- package/dist/formulas/index.d.ts +389 -0
- package/dist/formulas/index.js +1019 -0
- package/dist/formulas/index.js.map +1 -0
- package/dist/{types-BdbvlF9-.d.cts → types-CJWU1Gae.d.cts} +1 -1
- package/dist/{types-BdbvlF9-.d.ts → types-CJWU1Gae.d.ts} +1 -1
- package/dist/{types-Sn249eoy.d.cts → types-CmQrOz-T.d.cts} +154 -2
- package/dist/{types-D-flTpK0.d.ts → types-nlLCMiqd.d.ts} +154 -2
- package/package.json +109 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,156 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.25.0 — 2026-09-20
|
|
4
|
+
|
|
5
|
+
**Five defects a real browser found that jsdom could not, and the table's own
|
|
6
|
+
stress page so the next one is found the same way.**
|
|
7
|
+
|
|
8
|
+
Every clause in the component suite was green while all five of these were
|
|
9
|
+
live. They are claims about SCROLLING, about a POINTER, and about what a person
|
|
10
|
+
READS — none of which jsdom has. `demo/` is now where a real browser does that
|
|
11
|
+
work: a hundred thousand rows with grouping, the spreadsheet gestures, the
|
|
12
|
+
coverage sentence and the horizontal window on at once.
|
|
13
|
+
|
|
14
|
+
- **The fill handle dragged and filled nothing.** Two `mouseup` listeners on
|
|
15
|
+
`window`; the one that only cleared the drag flag was registered first, so by
|
|
16
|
+
the time the one that commits the fill ran, there was no drag in progress.
|
|
17
|
+
One handler now reads the flag and then clears it. Proven by a real pointer:
|
|
18
|
+
four cells filled, `"Filled 4 cells down 1 column."`
|
|
19
|
+
- **The scroll re-rendered the whole table on every event.** The window was
|
|
20
|
+
derived from a scroll offset held in state, so a four-thousand-line component
|
|
21
|
+
ran on every scroll whether or not one different row would be drawn: 38ms a
|
|
22
|
+
frame while scrolling EIGHT PIXELS, against the 16.7ms floor the same browser
|
|
23
|
+
hits on a static forty-row table. The scroll handler computes the window off
|
|
24
|
+
the DOM and sets state only when the rendered range actually moves.
|
|
25
|
+
- **The first paint drew every row**, because the window settled in an effect
|
|
26
|
+
and an effect runs after the first render. Render now derives a window when
|
|
27
|
+
state has none, and when a stale one no longer fits the row count.
|
|
28
|
+
- **`bodyEntries` built the whole list before windowing it**, allocating a
|
|
29
|
+
hundred thousand objects per scroll to render forty rows. It slices first.
|
|
30
|
+
- **`cellProps` used `indexOf` per cell** — O(rows) per cell, twenty-four
|
|
31
|
+
million comparisons a frame at 100,000 rows. Index maps.
|
|
32
|
+
- **The sticky header sat UNDER the frozen column.** A frozen body cell is
|
|
33
|
+
`z-20` so it can slide under the columns to its right; the header was `z-10`,
|
|
34
|
+
so the frozen column's cells scrolled over the header they belong to and the
|
|
35
|
+
top data row printed through the column names. Only a table long enough to
|
|
36
|
+
scroll shows it.
|
|
37
|
+
- **`rowHeight` did nothing unless virtualization was engaged** — so a view that
|
|
38
|
+
remembered a row height was ignored on every table short enough to read. It
|
|
39
|
+
applies on its own now, with a clause that pins it.
|
|
40
|
+
- **A group header said its subtotals twice** — once in the label line and again
|
|
41
|
+
in each column's cell — and the cell version was left-aligned under a
|
|
42
|
+
right-aligned money column. The label carries the count; the columns carry
|
|
43
|
+
the subtotals, each aligned with the numbers it totals.
|
|
44
|
+
|
|
45
|
+
Measured on the production demo build, 1440x900 headless Chromium, 100,000 rows:
|
|
46
|
+
**19 of the 40 rendered rows in the document, a 3.3-million-pixel scroll height,
|
|
47
|
+
18.6ms median frame and 23.6ms at the 95th percentile**, zero console errors.
|
|
48
|
+
|
|
49
|
+
## 0.24.0 — 2026-09-20
|
|
50
|
+
|
|
51
|
+
**The window goes sideways too.**
|
|
52
|
+
|
|
53
|
+
0.23.0 windowed the rows. A table can also be forty rows and three hundred
|
|
54
|
+
columns, and that one put every column in the document. `virtualize.columns`
|
|
55
|
+
closes it: the frozen leading run is ALWAYS drawn — a frozen identity column
|
|
56
|
+
that scrolled out of the DOM would leave the table blank exactly when somebody
|
|
57
|
+
scrolled far enough to need it — and a spacer cell on either side of the window
|
|
58
|
+
carries the width of what was left out, in the header, every body row, every
|
|
59
|
+
group header and the footer, so the table is as wide as it says it is.
|
|
60
|
+
|
|
61
|
+
- It engages only past **30** non-frozen columns and only when the consumer
|
|
62
|
+
asks for it, so nothing in the product changes shape today.
|
|
63
|
+
- A windowed column takes a FIXED width, and that is not a style choice: if the
|
|
64
|
+
drawn columns sized themselves to their content while the spacers claimed a
|
|
65
|
+
declared width, the horizontal scroll position would stop agreeing with the
|
|
66
|
+
window index and a person scrolling to column 80 would be shown column 74.
|
|
67
|
+
- Every `colSpan` in the body now counts what is RENDERED, not what is
|
|
68
|
+
declared — a full-width row spanning the declared count would push itself
|
|
69
|
+
wider than its own header.
|
|
70
|
+
|
|
71
|
+
Guard: three clauses in `MatrxDataTable.gaps.test.tsx`, including
|
|
72
|
+
`padStart + drawn + padEnd === the rest of the table`; the red twin fails
|
|
73
|
+
12/12 without the props.
|
|
74
|
+
|
|
75
|
+
## 0.23.1 — 2026-09-20
|
|
76
|
+
|
|
77
|
+
**Ticking a row no longer moves the row under your cursor.**
|
|
78
|
+
|
|
79
|
+
Defect N11, the thirteenth cold walk of the Masterwork pipeline (blocking). Measured on a
|
|
80
|
+
catalogued media Library at 1440x900: before the first click the header checkbox sat at `y 580`
|
|
81
|
+
and rows 1-3 at `y 606 / 647 / 688`; the instant a row's checkbox was ticked the selection bar was
|
|
82
|
+
inserted into the flow ABOVE the table and everything moved down **86px** — two full row heights,
|
|
83
|
+
header checkbox to `y 666`, rows 1-3 to `y 692 / 733 / 774`. The cursor had not moved and was now
|
|
84
|
+
resting on the SELECT-ALL box, so a second click at the same point selected 25 of 2,981 rows. The
|
|
85
|
+
walker hit it twice before he thought to measure it, which is how we know a real person hits it.
|
|
86
|
+
Picking three rows out of a long list meant re-measuring the table between every click.
|
|
87
|
+
|
|
88
|
+
- **The selection bar is now the last child of the body region** — under the rows and under the
|
|
89
|
+
pagination, instead of wedged between the toolbar and the first row. The rows live in a `flex-1`
|
|
90
|
+
box; a flex child appended after `flex-1` can only take height from the bottom of that box and
|
|
91
|
+
cannot change where it STARTS, so no row can move down. It is also where the best in the world
|
|
92
|
+
put it: Gmail, Linear and Airtable all keep the selection bar at the bottom of the list.
|
|
93
|
+
- **The bar carries `data-matrx-table-bulk-bar`**, so a test or a host can address it by what it
|
|
94
|
+
is rather than by where it happens to fall in a flat DOM query.
|
|
95
|
+
- Guard: `MatrxDataTable.selection-never-shifts-rows.test.tsx`, five cases, three of which go red
|
|
96
|
+
the moment the block is moved back above `data-matrx-table-body-region`.
|
|
97
|
+
|
|
98
|
+
**Consumer action:** none. No API changed. Every surface that hands `MatrxDataTable` a `selection`
|
|
99
|
+
inherits the fix. One caveat for test authors: a test that reached the selection bar's copy menu
|
|
100
|
+
as `getAllByRole("button", { name: /Copy, transform or export/ })[1]` was relying on the bar
|
|
101
|
+
rendering above the rows; address it through `[data-matrx-table-bulk-bar]` instead (this
|
|
102
|
+
package's own `default-copy-regressions.test.tsx` was updated the same way).
|
|
103
|
+
|
|
104
|
+
## 0.23.0 — 2026-09-20
|
|
105
|
+
|
|
106
|
+
**The three gaps are closed in the primitive, and the spreadsheet half of `/data` now lives here.**
|
|
107
|
+
|
|
108
|
+
The owner's read of the `/data` grid named three things it could not do that Airtable, Google
|
|
109
|
+
Sheets and Notion all can — and named the third one a lie on screen. All three are answered in
|
|
110
|
+
the shared table, so every surface in the product inherits them instead of the one route that
|
|
111
|
+
asked.
|
|
112
|
+
|
|
113
|
+
- **`grouping`** — "group these rows by Status" with collapsible sections, a count, and a
|
|
114
|
+
per-column subtotal ON its own column, not crammed into a banner. Controlled for the two pieces
|
|
115
|
+
of state a person expects to survive (which column groups, which sections are shut), so a saved
|
|
116
|
+
view carries them like any other reading of the table. A blank value is its own named section
|
|
117
|
+
and sorts last: a section of unanswered rows is not the first thing a person wants to read. The
|
|
118
|
+
model is `./grouping.ts` — pure, with eleven aggregate kinds, every one of which LABELS itself
|
|
119
|
+
("Sum 4,812.5", "12 filled") and says `Sum —` rather than `0` when it has nothing to add up.
|
|
120
|
+
- **`virtualize`** — render the rows a person can see. Off by default; on, it engages only past
|
|
121
|
+
its own threshold (150 rows), so short tables behave exactly as they did. Two spacer rows stand
|
|
122
|
+
in for the height of what the window left out, and the invariant `padStart + rendered + padEnd
|
|
123
|
+
=== total` is asserted over a spread of inputs, because a scrollbar that describes the slice
|
|
124
|
+
instead of the table is its own lie. `./virtual-window.ts` also has the variable-height and
|
|
125
|
+
scroll-to-index math, prefix-summed and binary-searched: O(log n) per scroll, not O(n).
|
|
126
|
+
- **`coverage`** — THE HONEST COUNT. Declare what the current answer actually covers and the
|
|
127
|
+
table says so, in plain English, above the rows: *"This filter searched the first 5,000 rows
|
|
128
|
+
only. 84,312 rows in this table; the rest were not looked at."* A complete answer says nothing.
|
|
129
|
+
A partial count is never presented as exact. The sentences are asserted to contain none of
|
|
130
|
+
"cap", "limit", "fetch", "query", "client" or "source" — a person reading a screen is not
|
|
131
|
+
reading our vocabulary.
|
|
132
|
+
- **`MatrxDataTableProps.rowHeight`** — an explicit body row height, for a view that remembers one.
|
|
133
|
+
|
|
134
|
+
New entry points carrying the rest of the `/data` grid's reusable half, each pure and tested:
|
|
135
|
+
|
|
136
|
+
- **`@ai-matrx/design-system/formulas`** — the 28-function expression language for computed
|
|
137
|
+
columns, moved out of the app verbatim. No `eval`, nothing throws, every failure is an
|
|
138
|
+
`ok: false` with a sentence.
|
|
139
|
+
- **`@ai-matrx/design-system/data-table/grid-selection`** and **`/grid-clipboard`** — the range
|
|
140
|
+
model (shift-click, drag, keyboard, row/column/all) and real Excel/Sheets TSV.
|
|
141
|
+
- **`@ai-matrx/design-system/data-table/fill`** — fill-down and the drag fill handle, with series
|
|
142
|
+
detection: `1, 2` continues to `3, 4, 5`, a single value copies, an inconsistent run copies. A
|
|
143
|
+
column that cannot take a write produces a skip with a sentence, never a silent drop.
|
|
144
|
+
- **`@ai-matrx/design-system/data-table/cell-undo`** — the bounded, PURE undo/redo stack behind
|
|
145
|
+
cell edits.
|
|
146
|
+
- **`@ai-matrx/design-system/data-table/validation`** — the column validation model, so a refusal
|
|
147
|
+
is the same sentence everywhere.
|
|
148
|
+
|
|
149
|
+
One defect this work turned up and fixed in passing: an optional config field named `valueOf` is
|
|
150
|
+
never absent, because every object in JavaScript already has one — `config.valueOf ? … : …` called
|
|
151
|
+
`Object.prototype.valueOf` and grouped every row under the config object itself. The field is
|
|
152
|
+
`readCell` now, in both the config and the model.
|
|
153
|
+
|
|
3
154
|
## 0.22.0 — 2026-09-20
|
|
4
155
|
|
|
5
156
|
**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,14 @@
|
|
|
1
1
|
import { AgentPayloadInput } from './copy-types.cjs';
|
|
2
|
-
import { g as MatrxDataTableCopyConfig, M as MatrxColumnDef } from '../types-
|
|
2
|
+
import { g as MatrxDataTableCopyConfig, M as MatrxColumnDef } from '../types-CmQrOz-T.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-
|
|
7
|
+
import '../types-CJWU1Gae.cjs';
|
|
8
|
+
import './grouping.cjs';
|
|
9
|
+
import './fill.cjs';
|
|
10
|
+
import './spreadsheet.cjs';
|
|
11
|
+
import './grid-selection.cjs';
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* Typed, declared-column capture for MatrxDataTable's built-in Alchemy source.
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { AgentPayloadInput } from './copy-types.js';
|
|
2
|
-
import { g as MatrxDataTableCopyConfig, M as MatrxColumnDef } from '../types-
|
|
2
|
+
import { g as MatrxDataTableCopyConfig, M as MatrxColumnDef } from '../types-nlLCMiqd.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-
|
|
7
|
+
import '../types-CJWU1Gae.js';
|
|
8
|
+
import './grouping.js';
|
|
9
|
+
import './fill.js';
|
|
10
|
+
import './spreadsheet.js';
|
|
11
|
+
import './grid-selection.js';
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* Typed, declared-column capture for MatrxDataTable's built-in Alchemy source.
|