@ai-matrx/design-system 0.21.14 → 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.
Files changed (97) hide show
  1. package/CHANGELOG.md +188 -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 +6 -1
  9. package/dist/data-table/copy-helpers.d.ts +6 -1
  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/fill.cjs +299 -0
  17. package/dist/data-table/fill.cjs.map +1 -0
  18. package/dist/data-table/fill.d.cts +129 -0
  19. package/dist/data-table/fill.d.ts +129 -0
  20. package/dist/data-table/fill.js +277 -0
  21. package/dist/data-table/fill.js.map +1 -0
  22. package/dist/data-table/filter-engine.d.cts +6 -1
  23. package/dist/data-table/filter-engine.d.ts +6 -1
  24. package/dist/data-table/grid-clipboard.cjs +165 -0
  25. package/dist/data-table/grid-clipboard.cjs.map +1 -0
  26. package/dist/data-table/grid-clipboard.d.cts +85 -0
  27. package/dist/data-table/grid-clipboard.d.ts +85 -0
  28. package/dist/data-table/grid-clipboard.js +145 -0
  29. package/dist/data-table/grid-clipboard.js.map +1 -0
  30. package/dist/data-table/grid-selection.cjs +258 -0
  31. package/dist/data-table/grid-selection.cjs.map +1 -0
  32. package/dist/data-table/grid-selection.d.cts +166 -0
  33. package/dist/data-table/grid-selection.d.ts +166 -0
  34. package/dist/data-table/grid-selection.js +238 -0
  35. package/dist/data-table/grid-selection.js.map +1 -0
  36. package/dist/data-table/grouping.cjs +302 -0
  37. package/dist/data-table/grouping.cjs.map +1 -0
  38. package/dist/data-table/grouping.d.cts +119 -0
  39. package/dist/data-table/grouping.d.ts +119 -0
  40. package/dist/data-table/grouping.js +282 -0
  41. package/dist/data-table/grouping.js.map +1 -0
  42. package/dist/data-table/host.d.cts +6 -1
  43. package/dist/data-table/host.d.ts +6 -1
  44. package/dist/data-table/index.cjs +4120 -806
  45. package/dist/data-table/index.cjs.map +1 -1
  46. package/dist/data-table/index.d.cts +99 -3
  47. package/dist/data-table/index.d.ts +99 -3
  48. package/dist/data-table/index.js +3971 -653
  49. package/dist/data-table/index.js.map +1 -1
  50. package/dist/data-table/infer-filter.d.cts +6 -1
  51. package/dist/data-table/infer-filter.d.ts +6 -1
  52. package/dist/data-table/layered-filters.d.cts +6 -1
  53. package/dist/data-table/layered-filters.d.ts +6 -1
  54. package/dist/data-table/query-control.d.cts +6 -1
  55. package/dist/data-table/query-control.d.ts +6 -1
  56. package/dist/data-table/spreadsheet.cjs +1014 -0
  57. package/dist/data-table/spreadsheet.cjs.map +1 -0
  58. package/dist/data-table/spreadsheet.d.cts +118 -0
  59. package/dist/data-table/spreadsheet.d.ts +118 -0
  60. package/dist/data-table/spreadsheet.js +994 -0
  61. package/dist/data-table/spreadsheet.js.map +1 -0
  62. package/dist/data-table/types.cjs.map +1 -1
  63. package/dist/data-table/types.d.cts +6 -1
  64. package/dist/data-table/types.d.ts +6 -1
  65. package/dist/data-table/url-state.d.cts +6 -1
  66. package/dist/data-table/url-state.d.ts +6 -1
  67. package/dist/data-table/validation.cjs +229 -0
  68. package/dist/data-table/validation.cjs.map +1 -0
  69. package/dist/data-table/validation.d.cts +125 -0
  70. package/dist/data-table/validation.d.ts +125 -0
  71. package/dist/data-table/validation.js +209 -0
  72. package/dist/data-table/validation.js.map +1 -0
  73. package/dist/data-table/virtual-window.cjs +203 -0
  74. package/dist/data-table/virtual-window.cjs.map +1 -0
  75. package/dist/data-table/virtual-window.d.cts +84 -0
  76. package/dist/data-table/virtual-window.d.ts +84 -0
  77. package/dist/data-table/virtual-window.js +183 -0
  78. package/dist/data-table/virtual-window.js.map +1 -0
  79. package/dist/data-table/xlsx.d.cts +6 -1
  80. package/dist/data-table/xlsx.d.ts +6 -1
  81. package/dist/field-formats/index.cjs +741 -0
  82. package/dist/field-formats/index.cjs.map +1 -0
  83. package/dist/field-formats/index.d.cts +45 -0
  84. package/dist/field-formats/index.d.ts +45 -0
  85. package/dist/field-formats/index.js +723 -0
  86. package/dist/field-formats/index.js.map +1 -0
  87. package/dist/formulas/index.cjs +1041 -0
  88. package/dist/formulas/index.cjs.map +1 -0
  89. package/dist/formulas/index.d.cts +389 -0
  90. package/dist/formulas/index.d.ts +389 -0
  91. package/dist/formulas/index.js +1019 -0
  92. package/dist/formulas/index.js.map +1 -0
  93. package/dist/types-CJWU1Gae.d.cts +204 -0
  94. package/dist/types-CJWU1Gae.d.ts +204 -0
  95. package/dist/{types-BgqIKbhq.d.cts → types-CmQrOz-T.d.cts} +382 -1
  96. package/dist/{types-emLxze_m.d.ts → types-nlLCMiqd.d.ts} +382 -1
  97. package/package.json +119 -2
@@ -0,0 +1,299 @@
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/fill.ts
21
+ var fill_exports = {};
22
+ __export(fill_exports, {
23
+ detectSeries: () => detectSeries,
24
+ fillDown: () => fillDown,
25
+ fillRange: () => fillRange,
26
+ fillTargetFor: () => fillTargetFor,
27
+ tileIndex: () => tileIndex
28
+ });
29
+ module.exports = __toCommonJS(fill_exports);
30
+
31
+ // src/data-table/grid-selection.ts
32
+ function boundsContain(bounds, rowIndex, colIndex) {
33
+ return rowIndex >= bounds.r0 && rowIndex <= bounds.r1 && colIndex >= bounds.c0 && colIndex <= bounds.c1;
34
+ }
35
+
36
+ // src/data-table/fill.ts
37
+ var NO_SERIES = { kind: "none", step: 0 };
38
+ var MS_PER_DAY = 864e5;
39
+ function plural(count, one, many) {
40
+ return `${count} ${count === 1 ? one : many}`;
41
+ }
42
+ function isValidRect(rect) {
43
+ return Number.isInteger(rect.top) && Number.isInteger(rect.left) && Number.isInteger(rect.bottom) && Number.isInteger(rect.right) && rect.top <= rect.bottom && rect.left <= rect.right;
44
+ }
45
+ function asBounds(rect) {
46
+ return { r0: rect.top, r1: rect.bottom, c0: rect.left, c1: rect.right };
47
+ }
48
+ function directionOf(source, target) {
49
+ if (!isValidRect(source) || !isValidRect(target)) return null;
50
+ if (target.top === source.bottom + 1) return "down";
51
+ if (target.bottom === source.top - 1) return "up";
52
+ if (target.left === source.right + 1) return "right";
53
+ if (target.right === source.left - 1) return "left";
54
+ return null;
55
+ }
56
+ function tileIndex(i, a, b) {
57
+ const n = b - a + 1;
58
+ if (n <= 0) return a;
59
+ if (i >= a && i <= b) return i;
60
+ if (i > b) return a + (i - a) % n;
61
+ return b - (a - 1 - i) % n;
62
+ }
63
+ function isFiniteNumber(v) {
64
+ return typeof v === "number" && Number.isFinite(v);
65
+ }
66
+ var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
67
+ var DATE_ISH = /^\d{4}-\d{2}-\d{2}([T ]|$)/;
68
+ function dateMs(v) {
69
+ if (v instanceof Date) {
70
+ const t = v.getTime();
71
+ return Number.isFinite(t) ? t : null;
72
+ }
73
+ if (typeof v === "string" && DATE_ISH.test(v)) {
74
+ const t = Date.parse(v);
75
+ return Number.isFinite(t) ? t : null;
76
+ }
77
+ return null;
78
+ }
79
+ function detectSeries(values) {
80
+ if (!Array.isArray(values) || values.length < 2) return NO_SERIES;
81
+ if (values.every(isFiniteNumber)) {
82
+ const nums = values;
83
+ const first = nums[0];
84
+ const second = nums[1];
85
+ if (first === void 0 || second === void 0) return NO_SERIES;
86
+ const step = second - first;
87
+ for (let i = 1; i < nums.length; i += 1) {
88
+ const prev = nums[i - 1];
89
+ const cur = nums[i];
90
+ if (prev === void 0 || cur === void 0) return NO_SERIES;
91
+ if (cur - prev !== step) return NO_SERIES;
92
+ }
93
+ return { kind: "number", step };
94
+ }
95
+ const stamps = values.map(dateMs);
96
+ if (stamps.every((t) => t !== null)) {
97
+ const first = stamps[0];
98
+ const second = stamps[1];
99
+ if (first === void 0 || second === void 0) return NO_SERIES;
100
+ const deltaMs = second - first;
101
+ if (deltaMs % MS_PER_DAY !== 0) return NO_SERIES;
102
+ for (let i = 1; i < stamps.length; i += 1) {
103
+ const prev = stamps[i - 1];
104
+ const cur = stamps[i];
105
+ if (prev === void 0 || cur === void 0) return NO_SERIES;
106
+ if (cur - prev !== deltaMs) return NO_SERIES;
107
+ }
108
+ return { kind: "date", step: deltaMs / MS_PER_DAY };
109
+ }
110
+ return NO_SERIES;
111
+ }
112
+ function formatDateLike(ms, sample) {
113
+ const iso = new Date(ms).toISOString();
114
+ if (typeof sample === "string" && DATE_ONLY.test(sample)) return iso.slice(0, 10);
115
+ return iso;
116
+ }
117
+ function extrapolate(line, series, k) {
118
+ const last = line[line.length - 1];
119
+ if (series.kind === "number" && isFiniteNumber(last)) {
120
+ return last + k * series.step;
121
+ }
122
+ if (series.kind === "date") {
123
+ const base = dateMs(last);
124
+ if (base === null) return jsonable(last);
125
+ return formatDateLike(base + k * series.step * MS_PER_DAY, last);
126
+ }
127
+ return jsonable(last);
128
+ }
129
+ function jsonable(value) {
130
+ if (value instanceof Date) {
131
+ const t = value.getTime();
132
+ return Number.isFinite(t) ? value.toISOString() : null;
133
+ }
134
+ if (value === void 0) return null;
135
+ return value;
136
+ }
137
+ function emptyResult(summary) {
138
+ return { patches: [], skipped: [], summary };
139
+ }
140
+ function fillRange(request) {
141
+ const { source, target, columnIds, rowIds } = request;
142
+ const mode = request.mode ?? "series";
143
+ const isWritable = request.isColumnWritable ?? (() => true);
144
+ const direction = directionOf(source, target);
145
+ if (!direction) {
146
+ return emptyResult(
147
+ "Filled 0 cells because the fill area does not touch the selection."
148
+ );
149
+ }
150
+ if (rowIds.length === 0 || columnIds.length === 0) {
151
+ return emptyResult("Filled 0 cells because the table is empty.");
152
+ }
153
+ const read = (r, c) => {
154
+ try {
155
+ return { ok: true, value: request.valueAt(r, c) };
156
+ } catch {
157
+ return { ok: false, value: void 0 };
158
+ }
159
+ };
160
+ const vertical = direction === "down" || direction === "up";
161
+ const forward = direction === "down" || direction === "right";
162
+ const fillStart = vertical ? source.top : source.left;
163
+ const fillEnd = vertical ? source.bottom : source.right;
164
+ const crossStart = vertical ? source.left : source.top;
165
+ const crossEnd = vertical ? source.right : source.bottom;
166
+ const maxFill = (vertical ? rowIds.length : columnIds.length) - 1;
167
+ const maxCross = (vertical ? columnIds.length : rowIds.length) - 1;
168
+ const rawFillFrom = vertical ? target.top : target.left;
169
+ const rawFillTo = vertical ? target.bottom : target.right;
170
+ const rawCrossFrom = vertical ? target.left : target.top;
171
+ const rawCrossTo = vertical ? target.right : target.bottom;
172
+ const fillFrom = Math.max(rawFillFrom, 0);
173
+ const fillTo = Math.min(rawFillTo, maxFill);
174
+ const crossFrom = Math.max(rawCrossFrom, 0);
175
+ const crossTo = Math.min(rawCrossTo, maxCross);
176
+ const requested = Math.max(rawFillTo - rawFillFrom + 1, 0) * Math.max(rawCrossTo - rawCrossFrom + 1, 0);
177
+ const inside = Math.max(fillTo - fillFrom + 1, 0) * Math.max(crossTo - crossFrom + 1, 0);
178
+ const outside = Math.max(requested - inside, 0);
179
+ const sourceBounds = asBounds(source);
180
+ const patches = [];
181
+ const skipped = [];
182
+ const touchedColumns = /* @__PURE__ */ new Set();
183
+ const lines = /* @__PURE__ */ new Map();
184
+ const lineFor = (crossIndex) => {
185
+ const srcCross = tileIndex(crossIndex, crossStart, crossEnd);
186
+ const cached = lines.get(srcCross);
187
+ if (cached) return cached;
188
+ const values = [];
189
+ const reads = [];
190
+ for (let f = fillStart; f <= fillEnd; f += 1) {
191
+ const r = vertical ? f : srcCross;
192
+ const c = vertical ? srcCross : f;
193
+ const got = read(r, c);
194
+ values.push(got.value);
195
+ reads.push(got.ok);
196
+ }
197
+ const series = mode === "series" ? detectSeries(values) : NO_SERIES;
198
+ const line = { values, reads, series };
199
+ lines.set(srcCross, line);
200
+ return line;
201
+ };
202
+ for (let f = fillFrom; f <= fillTo; f += 1) {
203
+ for (let x = crossFrom; x <= crossTo; x += 1) {
204
+ const rowIndex = vertical ? f : x;
205
+ const colIndex = vertical ? x : f;
206
+ const rowId = rowIds[rowIndex];
207
+ const columnId = columnIds[colIndex];
208
+ if (rowId === void 0 || columnId === void 0) continue;
209
+ if (boundsContain(sourceBounds, rowIndex, colIndex)) continue;
210
+ if (!isWritable(columnId)) {
211
+ skipped.push({
212
+ rowId,
213
+ columnId,
214
+ reason: `${columnId} cannot be filled because that column is read-only.`
215
+ });
216
+ continue;
217
+ }
218
+ const line = lineFor(x);
219
+ if (line.series.kind !== "none") {
220
+ const k = forward ? f - fillEnd : fillStart - f;
221
+ patches.push({ rowId, columnId, value: jsonable(extrapolate(line.values, line.series, k)) });
222
+ touchedColumns.add(columnId);
223
+ continue;
224
+ }
225
+ const srcFill = tileIndex(f, fillStart, fillEnd);
226
+ const patternIndex = srcFill - fillStart;
227
+ if (line.reads[patternIndex] !== true) {
228
+ skipped.push({
229
+ rowId,
230
+ columnId,
231
+ reason: `The value to copy into ${columnId} could not be read, so that cell was left alone.`
232
+ });
233
+ continue;
234
+ }
235
+ patches.push({ rowId, columnId, value: jsonable(line.values[patternIndex]) });
236
+ touchedColumns.add(columnId);
237
+ }
238
+ }
239
+ return { patches, skipped, summary: summarize(direction, patches, skipped, touchedColumns, outside) };
240
+ }
241
+ function summarize(direction, patches, skipped, touchedColumns, outside) {
242
+ const parts = [];
243
+ if (patches.length === 0) {
244
+ parts.push("Filled 0 cells because there was nothing to fill.");
245
+ } else if (direction === "down" || direction === "up") {
246
+ const word = direction === "down" ? "down" : "up";
247
+ parts.push(
248
+ `Filled ${plural(patches.length, "cell", "cells")} ${word} ${plural(touchedColumns.size, "column", "columns")}.`
249
+ );
250
+ } else {
251
+ const word = direction === "right" ? "right" : "left";
252
+ parts.push(
253
+ `Filled ${plural(patches.length, "cell", "cells")} ${word} across ${plural(touchedColumns.size, "column", "columns")}.`
254
+ );
255
+ }
256
+ if (skipped.length > 0) {
257
+ parts.push(`Skipped ${plural(skipped.length, "cell", "cells")} that could not be filled.`);
258
+ }
259
+ if (outside > 0) {
260
+ parts.push(`Ignored ${plural(outside, "cell", "cells")} outside the table.`);
261
+ }
262
+ return parts.join(" ");
263
+ }
264
+ function fillDown(request) {
265
+ const { range, ...rest } = request;
266
+ if (!isValidRect(range)) {
267
+ return emptyResult("Filled 0 cells because the selection is not a rectangle.");
268
+ }
269
+ if (range.top === range.bottom) {
270
+ return emptyResult("Filled 0 cells because the selection is a single row.");
271
+ }
272
+ return fillRange({
273
+ ...rest,
274
+ source: { top: range.top, left: range.left, bottom: range.top, right: range.right },
275
+ target: { top: range.top + 1, left: range.left, bottom: range.bottom, right: range.right }
276
+ });
277
+ }
278
+ function fillTargetFor(source, toRow, toColumn) {
279
+ if (!isValidRect(source)) return null;
280
+ if (!Number.isInteger(toRow) || !Number.isInteger(toColumn)) return null;
281
+ const below = toRow - source.bottom;
282
+ const above = source.top - toRow;
283
+ const rightOf = toColumn - source.right;
284
+ const leftOf = source.left - toColumn;
285
+ const vertical = Math.max(below, above, 0);
286
+ const horizontal = Math.max(rightOf, leftOf, 0);
287
+ if (vertical === 0 && horizontal === 0) return null;
288
+ if (vertical >= horizontal) {
289
+ if (below > 0) {
290
+ return { top: source.bottom + 1, bottom: toRow, left: source.left, right: source.right };
291
+ }
292
+ return { top: toRow, bottom: source.top - 1, left: source.left, right: source.right };
293
+ }
294
+ if (rightOf > 0) {
295
+ return { top: source.top, bottom: source.bottom, left: source.right + 1, right: toColumn };
296
+ }
297
+ return { top: source.top, bottom: source.bottom, left: toColumn, right: source.left - 1 };
298
+ }
299
+ //# sourceMappingURL=fill.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/data-table/fill.ts","../../src/data-table/grid-selection.ts"],"sourcesContent":["/**\n * THE FILL MODEL — what a fill-down and a drag of the fill handle produce.\n *\n * WHY THE LITTLE SQUARE EXISTS. Cmd-D fills down, and a keyboard user is happy.\n * But the first thing a spreadsheet person reaches for is the small square in\n * the corner of the selection: they grab it, drag, and the column fills. A grid\n * without that square is a grid they have to be taught to use. This module is\n * the whole answer to \"what does that drag mean\", expressed as data so the same\n * rules hold for the keyboard shortcut, the handle, and anything later.\n *\n * PURE MODULE: no React, no DOM, no network. A fill is a pure function of the\n * rectangles, the current values, and the row/column order — which is what makes\n * every spreadsheet convention below testable without mounting a grid.\n *\n * THE CONVENTIONS THIS ENCODES, AND WHY EACH ONE IS NOT NEGOTIABLE:\n *\n * TILING. A two-cell pattern filling five cells lays down `a b a b a`. Users\n * select a pattern precisely so it repeats; writing the first value five times\n * is the behaviour of a grid that has never been used for data entry.\n *\n * A SINGLE NUMBER IS NOT A SERIES. One cell holding `5` dragged down gives\n * five 5s, not 5, 6, 7, 8, 9. This is the classic bug: it looks clever, and it\n * silently corrupts every column of ids, quantities and prices a user drags.\n * A run needs at least two values agreeing on a step before it is a run, and\n * `1, 2, 4` — inconsistent — is not one either.\n *\n * DIRECTION. A fill upward continues the run upward: `3, 2` dragged up gives\n * `1, 0, -1`. The series is continued, never mirrored.\n *\n * ONE AXIS. A diagonal drag resolves to whichever axis moved further, exactly\n * as every spreadsheet does; a drag that stays inside the selection fills\n * nothing rather than guessing.\n *\n * NOTHING FAILS SILENTLY. A column that cannot take a written value produces\n * no patch AND one `skipped` entry per cell carrying a plain sentence, so the\n * grid can tell the user what did not happen. Nothing here throws: a\n * `valueAt` that blows up is read as blank and its dependent cell is skipped\n * with a reason.\n *\n * The rectangles are index-based because a drag is a geometric gesture over the\n * grid's CURRENT visual order; the RESULT is addressed by `rowId`/`columnId`, so\n * the patches survive the re-sort that follows a write (the same reason\n * `grid-selection.ts` addresses cells by id).\n */\n\nimport { boundsContain, type RangeBounds } from \"./grid-selection\";\n\n/** A rectangle of the grid, as inclusive indexes into the visual order. */\nexport interface FillRect {\n top: number;\n left: number;\n bottom: number;\n right: number;\n}\n\nexport interface FillRequest {\n /** The cells the user had selected — the PATTERN. */\n source: { top: number; left: number; bottom: number; right: number };\n /** The rectangle the handle was dragged to. Must share an edge with source. */\n target: { top: number; left: number; bottom: number; right: number };\n /** Reads the current value of a cell. Column is addressed by index into `columnIds`. */\n valueAt: (rowIndex: number, columnIndex: number) => unknown;\n /** Column ids in visual order, for the returned patches. */\n columnIds: string[];\n /** Row ids in visual order, for the returned patches. */\n rowIds: string[];\n /** Per-column opt-out: a column that cannot take a written value (a formula column, a read-only column). */\n isColumnWritable?: (columnId: string) => boolean;\n /** \"copy\" repeats the pattern; \"series\" continues a detected run. Default \"series\". */\n mode?: \"copy\" | \"series\";\n}\n\nexport interface CellPatch {\n rowId: string;\n columnId: string;\n value: unknown;\n}\n\nexport interface FillSkip {\n rowId: string;\n columnId: string;\n reason: string;\n}\n\nexport interface FillResult {\n patches: CellPatch[];\n /** Cells inside the target that were skipped, and the plain-English reason. */\n skipped: FillSkip[];\n /** What the fill did, for a status line: e.g. \"Filled 24 cells down 4 columns.\" */\n summary: string;\n}\n\nexport type SeriesKind = \"number\" | \"date\" | \"none\";\n\nexport interface SeriesDetection {\n kind: SeriesKind;\n step: number;\n}\n\ntype FillDirection = \"down\" | \"up\" | \"right\" | \"left\";\n\nconst NO_SERIES: SeriesDetection = { kind: \"none\", step: 0 };\nconst MS_PER_DAY = 86_400_000;\n\n// ─── Small English helpers ──────────────────────────────────────────────────\n//\n// A status line a person reads is part of the feature, not decoration: \"Filled\n// 24 cells down 4 columns.\" is the difference between a grid that reports what\n// it did and one that changes 24 values behind your back.\n\nfunction plural(count: number, one: string, many: string): string {\n return `${count} ${count === 1 ? one : many}`;\n}\n\n// ─── Geometry ───────────────────────────────────────────────────────────────\n\nfunction isValidRect(rect: FillRect): boolean {\n return (\n Number.isInteger(rect.top) &&\n Number.isInteger(rect.left) &&\n Number.isInteger(rect.bottom) &&\n Number.isInteger(rect.right) &&\n rect.top <= rect.bottom &&\n rect.left <= rect.right\n );\n}\n\nfunction asBounds(rect: FillRect): RangeBounds {\n return { r0: rect.top, r1: rect.bottom, c0: rect.left, c1: rect.right };\n}\n\n/**\n * Which way does this fill run? Null when the target does not share an edge\n * with the source, which is the only shape a handle drag can produce and the\n * only shape whose tiling is defined.\n */\nfunction directionOf(source: FillRect, target: FillRect): FillDirection | null {\n if (!isValidRect(source) || !isValidRect(target)) return null;\n if (target.top === source.bottom + 1) return \"down\";\n if (target.bottom === source.top - 1) return \"up\";\n if (target.left === source.right + 1) return \"right\";\n if (target.right === source.left - 1) return \"left\";\n return null;\n}\n\n/**\n * Where does index `i` read from, given a pattern occupying `a..b`?\n *\n * This is the tiling rule, per axis: past the end the pattern repeats forward,\n * before the start it repeats backward, so `a b` extended in either direction\n * stays `… a b [a b] a b …`.\n */\nexport function tileIndex(i: number, a: number, b: number): number {\n const n = b - a + 1;\n if (n <= 0) return a;\n if (i >= a && i <= b) return i;\n if (i > b) return a + ((i - a) % n);\n return b - ((a - 1 - i) % n);\n}\n\n// ─── Series detection ───────────────────────────────────────────────────────\n\nfunction isFiniteNumber(v: unknown): v is number {\n return typeof v === \"number\" && Number.isFinite(v);\n}\n\nconst DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst DATE_ISH = /^\\d{4}-\\d{2}-\\d{2}([T ]|$)/;\n\nfunction dateMs(v: unknown): number | null {\n if (v instanceof Date) {\n const t = v.getTime();\n return Number.isFinite(t) ? t : null;\n }\n if (typeof v === \"string\" && DATE_ISH.test(v)) {\n const t = Date.parse(v);\n return Number.isFinite(t) ? t : null;\n }\n return null;\n}\n\n/**\n * Detects a numeric or date run in a list of values. Exported because it is the\n * interesting part — and because the single-value case is where grids get this\n * wrong, so it deserves its own name and its own tests.\n */\nexport function detectSeries(values: unknown[]): SeriesDetection {\n // ONE VALUE IS NEVER A RUN. Two values are the minimum evidence of intent.\n if (!Array.isArray(values) || values.length < 2) return NO_SERIES;\n\n if (values.every(isFiniteNumber)) {\n const nums = values as number[];\n const first = nums[0];\n const second = nums[1];\n if (first === undefined || second === undefined) return NO_SERIES;\n const step = second - first;\n for (let i = 1; i < nums.length; i += 1) {\n const prev = nums[i - 1];\n const cur = nums[i];\n if (prev === undefined || cur === undefined) return NO_SERIES;\n if (cur - prev !== step) return NO_SERIES;\n }\n return { kind: \"number\", step };\n }\n\n const stamps = values.map(dateMs);\n if (stamps.every((t): t is number => t !== null)) {\n const first = stamps[0];\n const second = stamps[1];\n if (first === undefined || second === undefined) return NO_SERIES;\n const deltaMs = second - first;\n // Whole days only, in UTC. A run of month-ends is a different feature and\n // guessing at it would silently write the wrong dates.\n if (deltaMs % MS_PER_DAY !== 0) return NO_SERIES;\n for (let i = 1; i < stamps.length; i += 1) {\n const prev = stamps[i - 1];\n const cur = stamps[i];\n if (prev === undefined || cur === undefined) return NO_SERIES;\n if (cur - prev !== deltaMs) return NO_SERIES;\n }\n return { kind: \"date\", step: deltaMs / MS_PER_DAY };\n }\n\n return NO_SERIES;\n}\n\nfunction formatDateLike(ms: number, sample: unknown): string {\n const iso = new Date(ms).toISOString();\n if (typeof sample === \"string\" && DATE_ONLY.test(sample)) return iso.slice(0, 10);\n return iso;\n}\n\n/**\n * Continue `line` by `k` steps past its END, in reading order. Filling upward\n * passes the same positive `k`: the run is continued, never reflected, which is\n * why `3, 2` filled upward reads 1, 0, -1 away from the selection.\n */\nfunction extrapolate(line: unknown[], series: SeriesDetection, k: number): unknown {\n const last = line[line.length - 1];\n if (series.kind === \"number\" && isFiniteNumber(last)) {\n return last + k * series.step;\n }\n if (series.kind === \"date\") {\n const base = dateMs(last);\n if (base === null) return jsonable(last);\n return formatDateLike(base + k * series.step * MS_PER_DAY, last);\n }\n return jsonable(last);\n}\n\n/** Every value we hand back is plain data — a Date becomes its ISO string. */\nfunction jsonable(value: unknown): unknown {\n if (value instanceof Date) {\n const t = value.getTime();\n return Number.isFinite(t) ? value.toISOString() : null;\n }\n if (value === undefined) return null;\n return value;\n}\n\n// ─── The fill ───────────────────────────────────────────────────────────────\n\nfunction emptyResult(summary: string): FillResult {\n return { patches: [], skipped: [], summary };\n}\n\nexport function fillRange(request: FillRequest): FillResult {\n const { source, target, columnIds, rowIds } = request;\n const mode = request.mode ?? \"series\";\n const isWritable = request.isColumnWritable ?? (() => true);\n\n const direction = directionOf(source, target);\n if (!direction) {\n return emptyResult(\n \"Filled 0 cells because the fill area does not touch the selection.\",\n );\n }\n if (rowIds.length === 0 || columnIds.length === 0) {\n return emptyResult(\"Filled 0 cells because the table is empty.\");\n }\n\n // A read that throws is a blank, never an exception escaping the model.\n const read = (r: number, c: number): { ok: boolean; value: unknown } => {\n try {\n return { ok: true, value: request.valueAt(r, c) };\n } catch {\n return { ok: false, value: undefined };\n }\n };\n\n const vertical = direction === \"down\" || direction === \"up\";\n const forward = direction === \"down\" || direction === \"right\";\n\n const fillStart = vertical ? source.top : source.left;\n const fillEnd = vertical ? source.bottom : source.right;\n const crossStart = vertical ? source.left : source.top;\n const crossEnd = vertical ? source.right : source.bottom;\n\n const maxFill = (vertical ? rowIds.length : columnIds.length) - 1;\n const maxCross = (vertical ? columnIds.length : rowIds.length) - 1;\n\n const rawFillFrom = vertical ? target.top : target.left;\n const rawFillTo = vertical ? target.bottom : target.right;\n const rawCrossFrom = vertical ? target.left : target.top;\n const rawCrossTo = vertical ? target.right : target.bottom;\n\n const fillFrom = Math.max(rawFillFrom, 0);\n const fillTo = Math.min(rawFillTo, maxFill);\n const crossFrom = Math.max(rawCrossFrom, 0);\n const crossTo = Math.min(rawCrossTo, maxCross);\n\n const requested =\n Math.max(rawFillTo - rawFillFrom + 1, 0) *\n Math.max(rawCrossTo - rawCrossFrom + 1, 0);\n const inside =\n Math.max(fillTo - fillFrom + 1, 0) * Math.max(crossTo - crossFrom + 1, 0);\n const outside = Math.max(requested - inside, 0);\n\n const sourceBounds = asBounds(source);\n const patches: CellPatch[] = [];\n const skipped: FillSkip[] = [];\n const touchedColumns = new Set<string>();\n\n // The pattern line for each cross position, read ONCE. Reading it per target\n // cell would call the host's `valueAt` thousands of times for a long drag.\n const lines = new Map<number, { values: unknown[]; reads: boolean[]; series: SeriesDetection }>();\n const lineFor = (crossIndex: number) => {\n const srcCross = tileIndex(crossIndex, crossStart, crossEnd);\n const cached = lines.get(srcCross);\n if (cached) return cached;\n const values: unknown[] = [];\n const reads: boolean[] = [];\n for (let f = fillStart; f <= fillEnd; f += 1) {\n const r = vertical ? f : srcCross;\n const c = vertical ? srcCross : f;\n const got = read(r, c);\n values.push(got.value);\n reads.push(got.ok);\n }\n const series = mode === \"series\" ? detectSeries(values) : NO_SERIES;\n const line = { values, reads, series };\n lines.set(srcCross, line);\n return line;\n };\n\n for (let f = fillFrom; f <= fillTo; f += 1) {\n for (let x = crossFrom; x <= crossTo; x += 1) {\n const rowIndex = vertical ? f : x;\n const colIndex = vertical ? x : f;\n const rowId = rowIds[rowIndex];\n const columnId = columnIds[colIndex];\n if (rowId === undefined || columnId === undefined) continue;\n\n // The pattern is never rewritten with itself.\n if (boundsContain(sourceBounds, rowIndex, colIndex)) continue;\n\n if (!isWritable(columnId)) {\n skipped.push({\n rowId,\n columnId,\n reason: `${columnId} cannot be filled because that column is read-only.`,\n });\n continue;\n }\n\n const line = lineFor(x);\n if (line.series.kind !== \"none\") {\n const k = forward ? f - fillEnd : fillStart - f;\n patches.push({ rowId, columnId, value: jsonable(extrapolate(line.values, line.series, k)) });\n touchedColumns.add(columnId);\n continue;\n }\n\n const srcFill = tileIndex(f, fillStart, fillEnd);\n const patternIndex = srcFill - fillStart;\n if (line.reads[patternIndex] !== true) {\n skipped.push({\n rowId,\n columnId,\n reason: `The value to copy into ${columnId} could not be read, so that cell was left alone.`,\n });\n continue;\n }\n patches.push({ rowId, columnId, value: jsonable(line.values[patternIndex]) });\n touchedColumns.add(columnId);\n }\n }\n\n return { patches, skipped, summary: summarize(direction, patches, skipped, touchedColumns, outside) };\n}\n\nfunction summarize(\n direction: FillDirection,\n patches: CellPatch[],\n skipped: FillSkip[],\n touchedColumns: Set<string>,\n outside: number,\n): string {\n const parts: string[] = [];\n if (patches.length === 0) {\n parts.push(\"Filled 0 cells because there was nothing to fill.\");\n } else if (direction === \"down\" || direction === \"up\") {\n const word = direction === \"down\" ? \"down\" : \"up\";\n parts.push(\n `Filled ${plural(patches.length, \"cell\", \"cells\")} ${word} ${plural(touchedColumns.size, \"column\", \"columns\")}.`,\n );\n } else {\n const word = direction === \"right\" ? \"right\" : \"left\";\n parts.push(\n `Filled ${plural(patches.length, \"cell\", \"cells\")} ${word} across ${plural(touchedColumns.size, \"column\", \"columns\")}.`,\n );\n }\n if (skipped.length > 0) {\n parts.push(`Skipped ${plural(skipped.length, \"cell\", \"cells\")} that could not be filled.`);\n }\n if (outside > 0) {\n parts.push(`Ignored ${plural(outside, \"cell\", \"cells\")} outside the table.`);\n }\n return parts.join(\" \");\n}\n\n/** Cmd-D: fill the top row of the selection into the rest of it. */\nexport function fillDown(\n request: Omit<FillRequest, \"source\" | \"target\"> & { range: FillRequest[\"source\"] },\n): FillResult {\n const { range, ...rest } = request;\n if (!isValidRect(range)) {\n return emptyResult(\"Filled 0 cells because the selection is not a rectangle.\");\n }\n if (range.top === range.bottom) {\n return emptyResult(\"Filled 0 cells because the selection is a single row.\");\n }\n return fillRange({\n ...rest,\n source: { top: range.top, left: range.left, bottom: range.top, right: range.right },\n target: { top: range.top + 1, left: range.left, bottom: range.bottom, right: range.right },\n });\n}\n\n/**\n * The rectangle a drag from a handle at the selection's corner to (row, column)\n * produces — or null when the drag stayed inside the selection and there is\n * nothing to fill.\n *\n * A DIAGONAL DRAG RESOLVES TO ONE AXIS: whichever moved further, with a tie\n * going to the vertical, because filling a column is the gesture people mean.\n */\nexport function fillTargetFor(\n source: FillRequest[\"source\"],\n toRow: number,\n toColumn: number,\n): FillRequest[\"target\"] | null {\n if (!isValidRect(source)) return null;\n if (!Number.isInteger(toRow) || !Number.isInteger(toColumn)) return null;\n\n const below = toRow - source.bottom;\n const above = source.top - toRow;\n const rightOf = toColumn - source.right;\n const leftOf = source.left - toColumn;\n\n const vertical = Math.max(below, above, 0);\n const horizontal = Math.max(rightOf, leftOf, 0);\n if (vertical === 0 && horizontal === 0) return null;\n\n if (vertical >= horizontal) {\n if (below > 0) {\n return { top: source.bottom + 1, bottom: toRow, left: source.left, right: source.right };\n }\n return { top: toRow, bottom: source.top - 1, left: source.left, right: source.right };\n }\n if (rightOf > 0) {\n return { top: source.top, bottom: source.bottom, left: source.right + 1, right: toColumn };\n }\n return { top: source.top, bottom: source.bottom, left: toColumn, right: source.left - 1 };\n}\n","/**\n * Grid selection model for user data tables.\n * @ai-matrx/design-system/data-table/grid-selection\n *\n * This moved here from the app so every table in the product shares one\n * range-selection model.\n *\n * THE THREE STATES. A spreadsheet grid has three, not two, and collapsing them\n * to two is what made this grid feel like a web page instead of a tool:\n *\n * 1. NOTHING SELECTED — the grid at rest.\n * 2. SELECTED — one cell is current. Nothing has changed. You can\n * look, copy, and move with the keyboard.\n * 3. EDITING — a value is being changed.\n *\n * State 2 is the one that was missing, and its absence is why every edit needed\n * a double-click and why arrow keys, Tab, copy and fill-down were all\n * impossible: none of them mean anything without a notion of \"the current cell\".\n *\n * 🚨 THE CLICK LAW — a single click may SELECT a cell, TOGGLE a two-state\n * value, or OPEN a chooser. It may NEVER drop the user into a free-text buffer.\n * Opening a menu is not a mutation and toggling a checkbox is instantly\n * reversible, but landing in a text buffer turns every attempt to select-and-\n * copy into an accidental edit. `directClickKinds()` below is the whole list of\n * what a single click may act on; adding a free-text editor to it is a defect.\n *\n * A CHOOSER OPENS ON THE SECOND CLICK. A choice cell's first click SELECTS it\n * (so Cmd-C, Cmd-X, the arrow keys and the right-click menu all have a current\n * cell); a click on the cell that is ALREADY selected opens the option list —\n * the Airtable gesture. Opening the chooser on the very first click moved\n * focus into its search box, and every keyboard shortcut then went to that box\n * instead of the grid: a choice column could not be copied at all (2026-09-14).\n * Checkboxes and ratings stay one-click: a mis-click on those is instantly\n * undone in place and steals no focus.\n *\n * CLIPBOARD KEYS. Cmd-C / Cmd-X / Cmd-V on a selected cell act on THAT CELL —\n * copy its value, cut it (copy + clear), paste over it. A paste carrying a\n * spreadsheet block (tabs / line breaks) lands as a block from the selected\n * cell downward and rightward (`grid-clipboard.ts`). None of these need an\n * editor open, which is the whole point: a normal table copies what you point\n * at, not only what you are typing into.\n *\n * Addresses are (rowId, fieldName), never (rowIndex, colIndex): the grid\n * reloads after every write, and realtime reorders rows underneath the user. An\n * index-based selection silently jumps to a DIFFERENT cell when that happens —\n * and the next keystroke then edits the wrong row.\n *\n * Pure module: no React, no DOM. Everything here is a function of the current\n * address and the row/field lists, which is what makes the navigation testable\n * without mounting a grid.\n */\n\nexport type CellAddress = { rowId: string; fieldName: string };\n\nexport type GridMove =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"nextCell\"\n | \"prevCell\"\n | \"rowStart\"\n | \"rowEnd\"\n | \"gridStart\"\n | \"gridEnd\";\n\n/** What a keystroke means when a cell is selected but NOT being edited. */\nexport type GridKeyAction =\n | { kind: \"move\"; move: GridMove }\n /** Shift+arrow: grow the range toward `move`, anchor fixed. */\n | { kind: \"extend\"; move: GridMove }\n | { kind: \"selectAll\" }\n | { kind: \"selectRow\" }\n | { kind: \"selectColumn\" }\n | { kind: \"fillDown\" }\n | { kind: \"edit\" }\n | { kind: \"editSeeded\"; seed: string }\n | { kind: \"clearCell\" }\n | { kind: \"copy\" }\n | { kind: \"cut\" }\n | { kind: \"paste\" }\n | { kind: \"escape\" }\n | null;\n\n// ─── Ranges ─────────────────────────────────────────────────────────────────\n//\n// A RANGE is an anchor (where the selection started — the cell with the ring)\n// and a focus (where it was extended to), both by ADDRESS. The rectangle they\n// span is resolved against the grid's current row and column order at the\n// moment it is needed, so a range survives a re-sort or a hidden column the\n// same way a single selection does: by naming cells, not positions.\n\nexport type CellRange = { anchor: CellAddress; focus: CellAddress };\n\n/** The rectangle a range spans, as inclusive indexes into the grid's order. */\nexport type RangeBounds = { r0: number; r1: number; c0: number; c1: number };\n\nexport function rangeBounds(\n range: CellRange,\n rowIds: readonly string[],\n fieldNames: readonly string[],\n): RangeBounds | null {\n const ra = rowIds.indexOf(range.anchor.rowId);\n const rf = rowIds.indexOf(range.focus.rowId);\n const ca = fieldNames.indexOf(range.anchor.fieldName);\n const cf = fieldNames.indexOf(range.focus.fieldName);\n if (ra === -1 || rf === -1 || ca === -1 || cf === -1) return null;\n return {\n r0: Math.min(ra, rf),\n r1: Math.max(ra, rf),\n c0: Math.min(ca, cf),\n c1: Math.max(ca, cf),\n };\n}\n\nexport function rangeSize(bounds: RangeBounds): {\n rows: number;\n cols: number;\n cells: number;\n} {\n const rows = bounds.r1 - bounds.r0 + 1;\n const cols = bounds.c1 - bounds.c0 + 1;\n return { rows, cols, cells: rows * cols };\n}\n\nexport function boundsContain(\n bounds: RangeBounds,\n rowIndex: number,\n colIndex: number,\n): boolean {\n return (\n rowIndex >= bounds.r0 &&\n rowIndex <= bounds.r1 &&\n colIndex >= bounds.c0 &&\n colIndex <= bounds.c1\n );\n}\n\n/** Every cell of the range in reading order (row by row, left to right). */\nexport function cellsInRange(\n range: CellRange,\n rowIds: readonly string[],\n fieldNames: readonly string[],\n): CellAddress[] {\n const b = rangeBounds(range, rowIds, fieldNames);\n if (!b) return [];\n const out: CellAddress[] = [];\n // `rangeBounds` derives every index from `indexOf`, so both lookups below are\n // in range; the guards make that explicit instead of asserting it.\n for (let r = b.r0; r <= b.r1; r += 1) {\n const rowId = rowIds[r];\n if (rowId === undefined) continue;\n for (let c = b.c0; c <= b.c1; c += 1) {\n const fieldName = fieldNames[c];\n if (fieldName === undefined) continue;\n out.push({ rowId, fieldName });\n }\n }\n return out;\n}\n\n/** The range as a 2-D grid of addresses — what a block copy serialises. */\nexport function rangeRows(\n range: CellRange,\n rowIds: readonly string[],\n fieldNames: readonly string[],\n): CellAddress[][] {\n const b = rangeBounds(range, rowIds, fieldNames);\n if (!b) return [];\n const rows: CellAddress[][] = [];\n // Same in-range guarantee as `cellsInRange`.\n for (let r = b.r0; r <= b.r1; r += 1) {\n const rowId = rowIds[r];\n if (rowId === undefined) continue;\n const row: CellAddress[] = [];\n for (let c = b.c0; c <= b.c1; c += 1) {\n const fieldName = fieldNames[c];\n if (fieldName === undefined) continue;\n row.push({ rowId, fieldName });\n }\n rows.push(row);\n }\n return rows;\n}\n\nexport function isSingleCellRange(range: CellRange): boolean {\n return sameCell(range.anchor, range.focus);\n}\n\n/** The whole row, first column to last — what clicking a row's edge selects. */\nexport function rowRange(\n rowId: string,\n fieldNames: readonly string[],\n): CellRange | null {\n const firstField = fieldNames[0];\n const lastField = fieldNames[fieldNames.length - 1];\n // A non-empty list always has both ends; the null here is the same \"no range\n // to make\" answer the empty-list case gives.\n if (firstField === undefined || lastField === undefined) return null;\n return {\n anchor: { rowId, fieldName: firstField },\n focus: { rowId, fieldName: lastField },\n };\n}\n\n/** The whole column across the page — what clicking a header's edge selects. */\nexport function columnRange(\n fieldName: string,\n rowIds: readonly string[],\n): CellRange | null {\n const firstRow = rowIds[0];\n const lastRow = rowIds[rowIds.length - 1];\n if (firstRow === undefined || lastRow === undefined) return null;\n return {\n anchor: { rowId: firstRow, fieldName },\n focus: { rowId: lastRow, fieldName },\n };\n}\n\n/** Every cell on the page. */\nexport function allRange(\n rowIds: readonly string[],\n fieldNames: readonly string[],\n): CellRange | null {\n const firstRow = rowIds[0];\n const lastRow = rowIds[rowIds.length - 1];\n const firstField = fieldNames[0];\n const lastField = fieldNames[fieldNames.length - 1];\n if (\n firstRow === undefined ||\n lastRow === undefined ||\n firstField === undefined ||\n lastField === undefined\n ) {\n return null;\n }\n return {\n anchor: { rowId: firstRow, fieldName: firstField },\n focus: { rowId: lastRow, fieldName: lastField },\n };\n}\n\nexport function sameCell(\n a: CellAddress | null,\n b: CellAddress | null,\n): boolean {\n if (!a || !b) return a === b;\n return a.rowId === b.rowId && a.fieldName === b.fieldName;\n}\n\n/**\n * Where does `move` land from `current`?\n *\n * Returns null when the move is impossible (no selection, empty grid). Vertical\n * moves CLAMP at the edges — running into the top of the grid should park you\n * on the first row, not clear the selection, because a selection that vanishes\n * when you overshoot loses your place. Horizontal Tab moves WRAP to the next or\n * previous row, which is what makes Tab a usable data-entry gesture.\n */\nexport function moveSelection(\n current: CellAddress | null,\n move: GridMove,\n rowIds: readonly string[],\n fieldNames: readonly string[],\n): CellAddress | null {\n if (rowIds.length === 0 || fieldNames.length === 0) return null;\n\n const firstRowId = rowIds[0];\n const firstFieldName = fieldNames[0];\n // Both lists are non-empty by the check above; `null` here is the same \"no\n // move is possible\" answer the empty-grid case gives.\n if (firstRowId === undefined || firstFieldName === undefined) return null;\n const first: CellAddress = { rowId: firstRowId, fieldName: firstFieldName };\n if (!current) return first;\n\n let r = rowIds.indexOf(current.rowId);\n let c = fieldNames.indexOf(current.fieldName);\n // The selected row or column disappeared (deleted, filtered out, re-sorted\n // off the page). Fall back to the start rather than computing from -1, which\n // would quietly resolve to the last element.\n if (r === -1 || c === -1) return first;\n\n const lastRow = rowIds.length - 1;\n const lastCol = fieldNames.length - 1;\n const clamp = (n: number, max: number) => Math.min(Math.max(n, 0), max);\n\n switch (move) {\n case \"up\":\n r = clamp(r - 1, lastRow);\n break;\n case \"down\":\n r = clamp(r + 1, lastRow);\n break;\n case \"left\":\n c = clamp(c - 1, lastCol);\n break;\n case \"right\":\n c = clamp(c + 1, lastCol);\n break;\n case \"rowStart\":\n c = 0;\n break;\n case \"rowEnd\":\n c = lastCol;\n break;\n case \"gridStart\":\n r = 0;\n c = 0;\n break;\n case \"gridEnd\":\n r = lastRow;\n c = lastCol;\n break;\n case \"nextCell\":\n if (c < lastCol) c += 1;\n else if (r < lastRow) {\n r += 1;\n c = 0;\n }\n break;\n case \"prevCell\":\n if (c > 0) c -= 1;\n else if (r > 0) {\n r -= 1;\n c = lastCol;\n }\n break;\n }\n\n // Every branch above clamps into range or leaves r/c at their `indexOf`\n // values, so both reads land; falling back to `first` matches what this\n // function already does when an index cannot be resolved.\n const rowId = rowIds[r];\n const fieldName = fieldNames[c];\n if (rowId === undefined || fieldName === undefined) return first;\n return { rowId, fieldName };\n}\n\n/**\n * A single printable character typed on a selected cell starts an edit seeded\n * with it — the spreadsheet reflex of \"just start typing to replace\".\n *\n * Modifier combinations are excluded so Cmd-C / Ctrl-R never get mistaken for\n * typing, and only genuinely printable single characters qualify: `key` is the\n * character itself for those, and a multi-character name (\"ArrowUp\", \"F3\") for\n * everything else.\n */\nexport function isTypingKey(e: {\n key: string;\n ctrlKey: boolean;\n metaKey: boolean;\n altKey: boolean;\n}): boolean {\n if (e.ctrlKey || e.metaKey || e.altKey) return false;\n return e.key.length === 1 && e.key !== \" \";\n}\n\n/**\n * Translate a keystroke into a grid action. Called ONLY when a cell is selected\n * and not being edited — an editor owns its own keys while it is open.\n */\nexport function classifyGridKey(e: {\n key: string;\n shiftKey: boolean;\n ctrlKey: boolean;\n metaKey: boolean;\n altKey: boolean;\n}): GridKeyAction {\n const mod = e.ctrlKey || e.metaKey;\n\n // Shift+arrow GROWS the selection instead of moving it — the spreadsheet\n // gesture for \"these cells\". Shift+Tab stays a move (previous cell).\n const arrowMove: Record<string, GridMove> = {\n ArrowUp: mod ? \"gridStart\" : \"up\",\n ArrowDown: mod ? \"gridEnd\" : \"down\",\n ArrowLeft: mod ? \"rowStart\" : \"left\",\n ArrowRight: mod ? \"rowEnd\" : \"right\",\n Home: mod ? \"gridStart\" : \"rowStart\",\n End: mod ? \"gridEnd\" : \"rowEnd\",\n };\n // Read the entry rather than testing `in`: `in` also answers true for\n // inherited Object.prototype names (\"constructor\", \"toString\"), which used to\n // produce a move action carrying an undefined move.\n const arrow = arrowMove[e.key];\n if (arrow !== undefined) {\n return { kind: e.shiftKey ? \"extend\" : \"move\", move: arrow };\n }\n\n switch (e.key) {\n case \"Tab\":\n return { kind: \"move\", move: e.shiftKey ? \"prevCell\" : \"nextCell\" };\n case \"Enter\":\n case \"F2\":\n return { kind: \"edit\" };\n case \" \":\n // Shift+Space selects the row and Ctrl/Cmd+Space the column (Excel);\n // plain Space opens the editor without seeding it, so a boolean or\n // choice cell can be operated entirely from the keyboard.\n if (e.shiftKey) return { kind: \"selectRow\" };\n if (mod) return { kind: \"selectColumn\" };\n return { kind: \"edit\" };\n case \"a\":\n case \"A\":\n if (mod) return { kind: \"selectAll\" };\n break;\n case \"d\":\n case \"D\":\n if (mod) return { kind: \"fillDown\" };\n break;\n case \"Escape\":\n return { kind: \"escape\" };\n case \"Delete\":\n case \"Backspace\":\n return { kind: \"clearCell\" };\n case \"c\":\n case \"C\":\n if (mod) return { kind: \"copy\" };\n break;\n case \"x\":\n case \"X\":\n if (mod) return { kind: \"cut\" };\n break;\n case \"v\":\n case \"V\":\n if (mod) return { kind: \"paste\" };\n break;\n }\n\n if (isTypingKey(e)) return { kind: \"editSeeded\", seed: e.key };\n return null;\n}\n\n/**\n * Editor kinds a SINGLE click may operate directly, without first entering edit\n * mode. See THE CLICK LAW at the top of this file — this list is closed sets\n * and two-state values only, where a mis-click is obvious and instantly undone.\n */\nexport function directClickKinds(): readonly string[] {\n return [\"checkbox\", \"rating\", \"select\", \"multiselect\"] as const;\n}\n\nexport function isDirectClickEditor(editorKind: string | undefined): boolean {\n return editorKind !== undefined && directClickKinds().includes(editorKind);\n}\n\n/** Stable DOM key for a cell, so selection can scroll itself into view. */\nexport function cellDomKey(address: CellAddress): string {\n return `${address.rowId}::${address.fieldName}`;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * THE FILL MODEL — what a fill-down and a drag of the fill handle produce.
3
+ *
4
+ * WHY THE LITTLE SQUARE EXISTS. Cmd-D fills down, and a keyboard user is happy.
5
+ * But the first thing a spreadsheet person reaches for is the small square in
6
+ * the corner of the selection: they grab it, drag, and the column fills. A grid
7
+ * without that square is a grid they have to be taught to use. This module is
8
+ * the whole answer to "what does that drag mean", expressed as data so the same
9
+ * rules hold for the keyboard shortcut, the handle, and anything later.
10
+ *
11
+ * PURE MODULE: no React, no DOM, no network. A fill is a pure function of the
12
+ * rectangles, the current values, and the row/column order — which is what makes
13
+ * every spreadsheet convention below testable without mounting a grid.
14
+ *
15
+ * THE CONVENTIONS THIS ENCODES, AND WHY EACH ONE IS NOT NEGOTIABLE:
16
+ *
17
+ * TILING. A two-cell pattern filling five cells lays down `a b a b a`. Users
18
+ * select a pattern precisely so it repeats; writing the first value five times
19
+ * is the behaviour of a grid that has never been used for data entry.
20
+ *
21
+ * A SINGLE NUMBER IS NOT A SERIES. One cell holding `5` dragged down gives
22
+ * five 5s, not 5, 6, 7, 8, 9. This is the classic bug: it looks clever, and it
23
+ * silently corrupts every column of ids, quantities and prices a user drags.
24
+ * A run needs at least two values agreeing on a step before it is a run, and
25
+ * `1, 2, 4` — inconsistent — is not one either.
26
+ *
27
+ * DIRECTION. A fill upward continues the run upward: `3, 2` dragged up gives
28
+ * `1, 0, -1`. The series is continued, never mirrored.
29
+ *
30
+ * ONE AXIS. A diagonal drag resolves to whichever axis moved further, exactly
31
+ * as every spreadsheet does; a drag that stays inside the selection fills
32
+ * nothing rather than guessing.
33
+ *
34
+ * NOTHING FAILS SILENTLY. A column that cannot take a written value produces
35
+ * no patch AND one `skipped` entry per cell carrying a plain sentence, so the
36
+ * grid can tell the user what did not happen. Nothing here throws: a
37
+ * `valueAt` that blows up is read as blank and its dependent cell is skipped
38
+ * with a reason.
39
+ *
40
+ * The rectangles are index-based because a drag is a geometric gesture over the
41
+ * grid's CURRENT visual order; the RESULT is addressed by `rowId`/`columnId`, so
42
+ * the patches survive the re-sort that follows a write (the same reason
43
+ * `grid-selection.ts` addresses cells by id).
44
+ */
45
+ /** A rectangle of the grid, as inclusive indexes into the visual order. */
46
+ interface FillRect {
47
+ top: number;
48
+ left: number;
49
+ bottom: number;
50
+ right: number;
51
+ }
52
+ interface FillRequest {
53
+ /** The cells the user had selected — the PATTERN. */
54
+ source: {
55
+ top: number;
56
+ left: number;
57
+ bottom: number;
58
+ right: number;
59
+ };
60
+ /** The rectangle the handle was dragged to. Must share an edge with source. */
61
+ target: {
62
+ top: number;
63
+ left: number;
64
+ bottom: number;
65
+ right: number;
66
+ };
67
+ /** Reads the current value of a cell. Column is addressed by index into `columnIds`. */
68
+ valueAt: (rowIndex: number, columnIndex: number) => unknown;
69
+ /** Column ids in visual order, for the returned patches. */
70
+ columnIds: string[];
71
+ /** Row ids in visual order, for the returned patches. */
72
+ rowIds: string[];
73
+ /** Per-column opt-out: a column that cannot take a written value (a formula column, a read-only column). */
74
+ isColumnWritable?: (columnId: string) => boolean;
75
+ /** "copy" repeats the pattern; "series" continues a detected run. Default "series". */
76
+ mode?: "copy" | "series";
77
+ }
78
+ interface CellPatch {
79
+ rowId: string;
80
+ columnId: string;
81
+ value: unknown;
82
+ }
83
+ interface FillSkip {
84
+ rowId: string;
85
+ columnId: string;
86
+ reason: string;
87
+ }
88
+ interface FillResult {
89
+ patches: CellPatch[];
90
+ /** Cells inside the target that were skipped, and the plain-English reason. */
91
+ skipped: FillSkip[];
92
+ /** What the fill did, for a status line: e.g. "Filled 24 cells down 4 columns." */
93
+ summary: string;
94
+ }
95
+ type SeriesKind = "number" | "date" | "none";
96
+ interface SeriesDetection {
97
+ kind: SeriesKind;
98
+ step: number;
99
+ }
100
+ /**
101
+ * Where does index `i` read from, given a pattern occupying `a..b`?
102
+ *
103
+ * This is the tiling rule, per axis: past the end the pattern repeats forward,
104
+ * before the start it repeats backward, so `a b` extended in either direction
105
+ * stays `… a b [a b] a b …`.
106
+ */
107
+ declare function tileIndex(i: number, a: number, b: number): number;
108
+ /**
109
+ * Detects a numeric or date run in a list of values. Exported because it is the
110
+ * interesting part — and because the single-value case is where grids get this
111
+ * wrong, so it deserves its own name and its own tests.
112
+ */
113
+ declare function detectSeries(values: unknown[]): SeriesDetection;
114
+ declare function fillRange(request: FillRequest): FillResult;
115
+ /** Cmd-D: fill the top row of the selection into the rest of it. */
116
+ declare function fillDown(request: Omit<FillRequest, "source" | "target"> & {
117
+ range: FillRequest["source"];
118
+ }): FillResult;
119
+ /**
120
+ * The rectangle a drag from a handle at the selection's corner to (row, column)
121
+ * produces — or null when the drag stayed inside the selection and there is
122
+ * nothing to fill.
123
+ *
124
+ * A DIAGONAL DRAG RESOLVES TO ONE AXIS: whichever moved further, with a tie
125
+ * going to the vertical, because filling a column is the gesture people mean.
126
+ */
127
+ declare function fillTargetFor(source: FillRequest["source"], toRow: number, toColumn: number): FillRequest["target"] | null;
128
+
129
+ export { type CellPatch, type FillRect, type FillRequest, type FillResult, type FillSkip, type SeriesDetection, type SeriesKind, detectSeries, fillDown, fillRange, fillTargetFor, tileIndex };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * THE FILL MODEL — what a fill-down and a drag of the fill handle produce.
3
+ *
4
+ * WHY THE LITTLE SQUARE EXISTS. Cmd-D fills down, and a keyboard user is happy.
5
+ * But the first thing a spreadsheet person reaches for is the small square in
6
+ * the corner of the selection: they grab it, drag, and the column fills. A grid
7
+ * without that square is a grid they have to be taught to use. This module is
8
+ * the whole answer to "what does that drag mean", expressed as data so the same
9
+ * rules hold for the keyboard shortcut, the handle, and anything later.
10
+ *
11
+ * PURE MODULE: no React, no DOM, no network. A fill is a pure function of the
12
+ * rectangles, the current values, and the row/column order — which is what makes
13
+ * every spreadsheet convention below testable without mounting a grid.
14
+ *
15
+ * THE CONVENTIONS THIS ENCODES, AND WHY EACH ONE IS NOT NEGOTIABLE:
16
+ *
17
+ * TILING. A two-cell pattern filling five cells lays down `a b a b a`. Users
18
+ * select a pattern precisely so it repeats; writing the first value five times
19
+ * is the behaviour of a grid that has never been used for data entry.
20
+ *
21
+ * A SINGLE NUMBER IS NOT A SERIES. One cell holding `5` dragged down gives
22
+ * five 5s, not 5, 6, 7, 8, 9. This is the classic bug: it looks clever, and it
23
+ * silently corrupts every column of ids, quantities and prices a user drags.
24
+ * A run needs at least two values agreeing on a step before it is a run, and
25
+ * `1, 2, 4` — inconsistent — is not one either.
26
+ *
27
+ * DIRECTION. A fill upward continues the run upward: `3, 2` dragged up gives
28
+ * `1, 0, -1`. The series is continued, never mirrored.
29
+ *
30
+ * ONE AXIS. A diagonal drag resolves to whichever axis moved further, exactly
31
+ * as every spreadsheet does; a drag that stays inside the selection fills
32
+ * nothing rather than guessing.
33
+ *
34
+ * NOTHING FAILS SILENTLY. A column that cannot take a written value produces
35
+ * no patch AND one `skipped` entry per cell carrying a plain sentence, so the
36
+ * grid can tell the user what did not happen. Nothing here throws: a
37
+ * `valueAt` that blows up is read as blank and its dependent cell is skipped
38
+ * with a reason.
39
+ *
40
+ * The rectangles are index-based because a drag is a geometric gesture over the
41
+ * grid's CURRENT visual order; the RESULT is addressed by `rowId`/`columnId`, so
42
+ * the patches survive the re-sort that follows a write (the same reason
43
+ * `grid-selection.ts` addresses cells by id).
44
+ */
45
+ /** A rectangle of the grid, as inclusive indexes into the visual order. */
46
+ interface FillRect {
47
+ top: number;
48
+ left: number;
49
+ bottom: number;
50
+ right: number;
51
+ }
52
+ interface FillRequest {
53
+ /** The cells the user had selected — the PATTERN. */
54
+ source: {
55
+ top: number;
56
+ left: number;
57
+ bottom: number;
58
+ right: number;
59
+ };
60
+ /** The rectangle the handle was dragged to. Must share an edge with source. */
61
+ target: {
62
+ top: number;
63
+ left: number;
64
+ bottom: number;
65
+ right: number;
66
+ };
67
+ /** Reads the current value of a cell. Column is addressed by index into `columnIds`. */
68
+ valueAt: (rowIndex: number, columnIndex: number) => unknown;
69
+ /** Column ids in visual order, for the returned patches. */
70
+ columnIds: string[];
71
+ /** Row ids in visual order, for the returned patches. */
72
+ rowIds: string[];
73
+ /** Per-column opt-out: a column that cannot take a written value (a formula column, a read-only column). */
74
+ isColumnWritable?: (columnId: string) => boolean;
75
+ /** "copy" repeats the pattern; "series" continues a detected run. Default "series". */
76
+ mode?: "copy" | "series";
77
+ }
78
+ interface CellPatch {
79
+ rowId: string;
80
+ columnId: string;
81
+ value: unknown;
82
+ }
83
+ interface FillSkip {
84
+ rowId: string;
85
+ columnId: string;
86
+ reason: string;
87
+ }
88
+ interface FillResult {
89
+ patches: CellPatch[];
90
+ /** Cells inside the target that were skipped, and the plain-English reason. */
91
+ skipped: FillSkip[];
92
+ /** What the fill did, for a status line: e.g. "Filled 24 cells down 4 columns." */
93
+ summary: string;
94
+ }
95
+ type SeriesKind = "number" | "date" | "none";
96
+ interface SeriesDetection {
97
+ kind: SeriesKind;
98
+ step: number;
99
+ }
100
+ /**
101
+ * Where does index `i` read from, given a pattern occupying `a..b`?
102
+ *
103
+ * This is the tiling rule, per axis: past the end the pattern repeats forward,
104
+ * before the start it repeats backward, so `a b` extended in either direction
105
+ * stays `… a b [a b] a b …`.
106
+ */
107
+ declare function tileIndex(i: number, a: number, b: number): number;
108
+ /**
109
+ * Detects a numeric or date run in a list of values. Exported because it is the
110
+ * interesting part — and because the single-value case is where grids get this
111
+ * wrong, so it deserves its own name and its own tests.
112
+ */
113
+ declare function detectSeries(values: unknown[]): SeriesDetection;
114
+ declare function fillRange(request: FillRequest): FillResult;
115
+ /** Cmd-D: fill the top row of the selection into the rest of it. */
116
+ declare function fillDown(request: Omit<FillRequest, "source" | "target"> & {
117
+ range: FillRequest["source"];
118
+ }): FillResult;
119
+ /**
120
+ * The rectangle a drag from a handle at the selection's corner to (row, column)
121
+ * produces — or null when the drag stayed inside the selection and there is
122
+ * nothing to fill.
123
+ *
124
+ * A DIAGONAL DRAG RESOLVES TO ONE AXIS: whichever moved further, with a tie
125
+ * going to the vertical, because filling a column is the gesture people mean.
126
+ */
127
+ declare function fillTargetFor(source: FillRequest["source"], toRow: number, toColumn: number): FillRequest["target"] | null;
128
+
129
+ export { type CellPatch, type FillRect, type FillRequest, type FillResult, type FillSkip, type SeriesDetection, type SeriesKind, detectSeries, fillDown, fillRange, fillTargetFor, tileIndex };