@pixerate/editor 0.21.0 → 0.23.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 (114) hide show
  1. package/README.md +100 -13
  2. package/THIRD_PARTY_NOTICES.md +88 -0
  3. package/dist/agent/index.cjs +86 -111
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +69 -211
  6. package/dist/agent/index.d.ts +69 -211
  7. package/dist/agent/index.js +77 -102
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/arrangeShortcuts-B8O3f8-P.d.cts +184 -0
  10. package/dist/arrangeShortcuts-sLlD2rSX.d.ts +184 -0
  11. package/dist/canvas/index.cjs +2112 -60
  12. package/dist/canvas/index.cjs.map +1 -1
  13. package/dist/canvas/index.d.cts +712 -181
  14. package/dist/canvas/index.d.ts +712 -181
  15. package/dist/canvas/index.js +2105 -53
  16. package/dist/canvas/index.js.map +1 -1
  17. package/dist/chunk-26BVFKHU.cjs +453 -0
  18. package/dist/chunk-26BVFKHU.cjs.map +1 -0
  19. package/dist/chunk-2H7ZHPEJ.js +762 -0
  20. package/dist/chunk-2H7ZHPEJ.js.map +1 -0
  21. package/dist/chunk-7DWMJCAT.js +453 -0
  22. package/dist/chunk-7DWMJCAT.js.map +1 -0
  23. package/dist/{chunk-TVRC4KVT.cjs → chunk-AA4ZEVCC.cjs} +845 -90
  24. package/dist/chunk-AA4ZEVCC.cjs.map +1 -0
  25. package/dist/chunk-ARRTP33I.js +101 -0
  26. package/dist/chunk-ARRTP33I.js.map +1 -0
  27. package/dist/{chunk-LA7PEGU3.cjs → chunk-CL37JGFW.cjs} +4 -4
  28. package/dist/{chunk-LA7PEGU3.cjs.map → chunk-CL37JGFW.cjs.map} +1 -1
  29. package/dist/chunk-CPOKFNBG.cjs +101 -0
  30. package/dist/chunk-CPOKFNBG.cjs.map +1 -0
  31. package/dist/chunk-G2I4QJOT.cjs +762 -0
  32. package/dist/chunk-G2I4QJOT.cjs.map +1 -0
  33. package/dist/{chunk-4QPLEUL2.cjs → chunk-GVSMRPHB.cjs} +1046 -54
  34. package/dist/chunk-GVSMRPHB.cjs.map +1 -0
  35. package/dist/{chunk-KYRZDWM4.js → chunk-JJDLPEWL.js} +2 -2
  36. package/dist/{chunk-GVW4P7BG.js → chunk-K3R5NG2D.js} +448 -54
  37. package/dist/chunk-K3R5NG2D.js.map +1 -0
  38. package/dist/{chunk-3TYMNULT.js → chunk-MJZBNWG7.js} +807 -52
  39. package/dist/chunk-MJZBNWG7.js.map +1 -0
  40. package/dist/{chunk-A7DKIA72.js → chunk-QBJ22YZR.js} +1059 -93
  41. package/dist/chunk-QBJ22YZR.js.map +1 -0
  42. package/dist/{chunk-CYFJCKC4.js → chunk-QGHGJ56O.js} +997 -5
  43. package/dist/chunk-QGHGJ56O.js.map +1 -0
  44. package/dist/{chunk-JYRIOFEN.cjs → chunk-RIKBJLS2.cjs} +1064 -98
  45. package/dist/chunk-RIKBJLS2.cjs.map +1 -0
  46. package/dist/{chunk-U6NDQFOK.cjs → chunk-XDMXXC4Z.cjs} +569 -175
  47. package/dist/chunk-XDMXXC4Z.cjs.map +1 -0
  48. package/dist/commands/index.cjs +860 -0
  49. package/dist/commands/index.cjs.map +1 -0
  50. package/dist/commands/index.d.cts +592 -0
  51. package/dist/commands/index.d.ts +592 -0
  52. package/dist/commands/index.js +860 -0
  53. package/dist/commands/index.js.map +1 -0
  54. package/dist/{controller-OUdxPY-X.d.ts → controller-pePCJLT1.d.cts} +216 -11
  55. package/dist/{controller-OUdxPY-X.d.cts → controller-pePCJLT1.d.ts} +216 -11
  56. package/dist/extensions/index.cjs +14 -4
  57. package/dist/extensions/index.cjs.map +1 -1
  58. package/dist/extensions/index.d.cts +3 -3
  59. package/dist/extensions/index.d.ts +3 -3
  60. package/dist/extensions/index.js +13 -3
  61. package/dist/history/index.cjs +12 -2
  62. package/dist/history/index.cjs.map +1 -1
  63. package/dist/history/index.d.cts +211 -7
  64. package/dist/history/index.d.ts +211 -7
  65. package/dist/history/index.js +13 -3
  66. package/dist/image-editor/index.cjs +418 -13
  67. package/dist/image-editor/index.cjs.map +1 -1
  68. package/dist/image-editor/index.d.cts +154 -3
  69. package/dist/image-editor/index.d.ts +154 -3
  70. package/dist/image-editor/index.js +417 -12
  71. package/dist/image-editor/index.js.map +1 -1
  72. package/dist/{index-k3mFbPx_.d.cts → index-CCr_z9kF.d.cts} +3 -3
  73. package/dist/{index-Ng1pG7JE.d.ts → index-Ck6ajwXh.d.ts} +3 -3
  74. package/dist/index.cjs +275 -83
  75. package/dist/index.cjs.map +1 -1
  76. package/dist/index.d.cts +296 -47
  77. package/dist/index.d.ts +296 -47
  78. package/dist/index.js +266 -74
  79. package/dist/index.js.map +1 -1
  80. package/dist/registry-D5-R90p0.d.cts +209 -0
  81. package/dist/registry-D5-R90p0.d.ts +209 -0
  82. package/dist/{registry-iAHbqkYt.d.cts → registry-GJI4gToU.d.cts} +15 -4
  83. package/dist/{registry-iAHbqkYt.d.ts → registry-GJI4gToU.d.ts} +15 -4
  84. package/dist/serializers/index.cjs +3 -3
  85. package/dist/serializers/index.d.cts +1 -1
  86. package/dist/serializers/index.d.ts +1 -1
  87. package/dist/serializers/index.js +2 -2
  88. package/dist/spreadsheet/index.cjs +23 -3
  89. package/dist/spreadsheet/index.cjs.map +1 -1
  90. package/dist/spreadsheet/index.d.cts +97 -10
  91. package/dist/spreadsheet/index.d.ts +97 -10
  92. package/dist/spreadsheet/index.js +22 -2
  93. package/dist/{track-changes-G8JcWB8b.d.cts → track-changes-BSF58RK0.d.cts} +92 -9
  94. package/dist/{track-changes-G8JcWB8b.d.ts → track-changes-BSF58RK0.d.ts} +92 -9
  95. package/dist/{types-DMe0vFB1.d.cts → types-DX4Mh4SL.d.cts} +1 -1
  96. package/dist/{types-DMe0vFB1.d.ts → types-DX4Mh4SL.d.ts} +1 -1
  97. package/dist/types-GT5a5YAX.d.cts +86 -0
  98. package/dist/types-GT5a5YAX.d.ts +86 -0
  99. package/package.json +8 -2
  100. package/dist/chunk-2TWGLC5K.cjs +0 -262
  101. package/dist/chunk-2TWGLC5K.cjs.map +0 -1
  102. package/dist/chunk-3TYMNULT.js.map +0 -1
  103. package/dist/chunk-4QPLEUL2.cjs.map +0 -1
  104. package/dist/chunk-A7DKIA72.js.map +0 -1
  105. package/dist/chunk-CYFJCKC4.js.map +0 -1
  106. package/dist/chunk-GVW4P7BG.js.map +0 -1
  107. package/dist/chunk-JYRIOFEN.cjs.map +0 -1
  108. package/dist/chunk-KAADNAPF.js +0 -262
  109. package/dist/chunk-KAADNAPF.js.map +0 -1
  110. package/dist/chunk-TVRC4KVT.cjs.map +0 -1
  111. package/dist/chunk-U6NDQFOK.cjs.map +0 -1
  112. package/dist/types-BZgcDsC5.d.cts +0 -42
  113. package/dist/types-BZgcDsC5.d.ts +0 -42
  114. /package/dist/{chunk-KYRZDWM4.js.map → chunk-JJDLPEWL.js.map} +0 -0
@@ -2,15 +2,23 @@ import {
2
2
  FormulaEvaluator,
3
3
  WHOLE_COLUMN_END_ROW,
4
4
  colNameToIndex,
5
+ compareValues,
6
+ describeFormulaError,
7
+ formatCellDisplay,
5
8
  getFormulaArgumentSpecIndex,
6
9
  getFormulaFunction,
7
10
  getFormulaSignature,
8
11
  getFormulaSignatureParts,
9
12
  indexToColName,
13
+ isFormulaError,
14
+ numericValue,
15
+ parseCellInput,
10
16
  parseFormula,
17
+ parseIsoDate,
11
18
  searchFormulaFunctions,
19
+ sortCompareValues,
12
20
  tokenizeFormula
13
- } from "./chunk-A7DKIA72.js";
21
+ } from "./chunk-QBJ22YZR.js";
14
22
 
15
23
  // src/spreadsheet/formula/dependency-graph.ts
16
24
  var columnDependencyKey = (col) => `${col.toUpperCase()}:`;
@@ -19,6 +27,8 @@ function extractCellDependencies(node) {
19
27
  const visit = (curr) => {
20
28
  if (curr.type === "CellRef") {
21
29
  deps.push({ col: curr.col, row: curr.row });
30
+ } else if (curr.type === "SpillRef") {
31
+ deps.push({ col: curr.col, row: curr.row });
22
32
  } else if (curr.type === "Range" && curr.endRow >= WHOLE_COLUMN_END_ROW) {
23
33
  const startC = colNameToIndex(curr.startCol);
24
34
  const endC = colNameToIndex(curr.endCol);
@@ -54,6 +64,14 @@ var DependencyGraph = class {
54
64
  dependencies = /* @__PURE__ */ new Map();
55
65
  // Map of cellKey (e.g. "A1") -> Set of cellKeys that depend ON this cell (dependents)
56
66
  dependents = /* @__PURE__ */ new Map();
67
+ // Dynamic-array edges, set by the controller after evaluating an anchor:
68
+ // spilled cells depend on their anchor, and a blocked (#SPILL!) anchor
69
+ // depends on the cells blocking it. Kept apart from formula dependencies,
70
+ // so re-entering a formula doesn't drop them.
71
+ spillDependencies = /* @__PURE__ */ new Map();
72
+ spillDependents = /* @__PURE__ */ new Map();
73
+ // anchor -> the edges it registered (so they can be replaced).
74
+ spillEdges = /* @__PURE__ */ new Map();
57
75
  setCellFormula(cellKey, formulaStr) {
58
76
  const key = cellKey.toUpperCase();
59
77
  const oldDeps = this.dependencies.get(key) || /* @__PURE__ */ new Set();
@@ -84,12 +102,49 @@ var DependencyGraph = class {
84
102
  clear() {
85
103
  this.dependencies.clear();
86
104
  this.dependents.clear();
105
+ this.spillDependencies.clear();
106
+ this.spillDependents.clear();
107
+ this.spillEdges.clear();
87
108
  }
109
+ /**
110
+ * Records the dynamic-array edges of the anchor `anchorKey`, replacing its
111
+ * previous ones: every key in `spilledKeys` (cells holding its spilled
112
+ * values) depends on the anchor, and the anchor depends on every key in
113
+ * `blockingKeys` (cells that stop it spilling). Pass empty lists to remove
114
+ * the anchor's edges.
115
+ */
116
+ setSpillEdges(anchorKey, spilledKeys, blockingKeys = []) {
117
+ const anchor = anchorKey.toUpperCase();
118
+ for (const [from, to] of this.spillEdges.get(anchor) ?? []) {
119
+ this.spillDependencies.get(from)?.delete(to);
120
+ this.spillDependents.get(to)?.delete(from);
121
+ }
122
+ const edges = [];
123
+ for (const key of spilledKeys) edges.push([key.toUpperCase(), anchor]);
124
+ for (const key of blockingKeys) edges.push([anchor, key.toUpperCase()]);
125
+ for (const [from, to] of edges) {
126
+ if (!this.spillDependencies.has(from)) this.spillDependencies.set(from, /* @__PURE__ */ new Set());
127
+ this.spillDependencies.get(from).add(to);
128
+ if (!this.spillDependents.has(to)) this.spillDependents.set(to, /* @__PURE__ */ new Set());
129
+ this.spillDependents.get(to).add(from);
130
+ }
131
+ if (edges.length > 0) this.spillEdges.set(anchor, edges);
132
+ else this.spillEdges.delete(anchor);
133
+ }
134
+ /** The spilled cells registered for `anchorKey` by `setSpillEdges`. */
135
+ getSpilledKeys(anchorKey) {
136
+ const anchor = anchorKey.toUpperCase();
137
+ return (this.spillEdges.get(anchor) ?? []).filter(([, to]) => to === anchor).map(([from]) => from);
138
+ }
139
+ /** Formula dependencies plus dynamic-array (spill) dependencies of a cell. */
88
140
  getDependencies(cellKey) {
89
- return Array.from(this.dependencies.get(cellKey.toUpperCase()) || []);
141
+ const key = cellKey.toUpperCase();
142
+ return unionOf(this.dependencies.get(key), this.spillDependencies.get(key));
90
143
  }
144
+ /** Cells depending on `cellKey`, through formulas or spills. */
91
145
  getDependents(cellKey) {
92
- return Array.from(this.dependents.get(cellKey.toUpperCase()) || []);
146
+ const key = cellKey.toUpperCase();
147
+ return unionOf(this.dependents.get(key), this.spillDependents.get(key));
93
148
  }
94
149
  /**
95
150
  * Detects if there is a cycle containing cellKey.
@@ -101,7 +156,7 @@ var DependencyGraph = class {
101
156
  const dfs = (node) => {
102
157
  visited.add(node);
103
158
  recStack.add(node);
104
- const deps = this.dependencies.get(node) || /* @__PURE__ */ new Set();
159
+ const deps = this.getDependencies(node);
105
160
  for (const neighbor of deps) {
106
161
  if (!visited.has(neighbor)) {
107
162
  if (dfs(neighbor)) return true;
@@ -180,6 +235,7 @@ var DependencyGraph = class {
180
235
  const node = queue.pop();
181
236
  const direct = [
182
237
  ...this.dependents.get(node) ?? [],
238
+ ...this.spillDependents.get(node) ?? [],
183
239
  ...this.dependents.get(columnDependencyKey(node.replace(/\d+$/, ""))) ?? []
184
240
  ];
185
241
  for (const dep of direct) {
@@ -192,7 +248,7 @@ var DependencyGraph = class {
192
248
  }
193
249
  const neighbors = (node) => {
194
250
  const result = [];
195
- for (const dep of this.dependencies.get(node) ?? []) {
251
+ for (const dep of this.getDependencies(node)) {
196
252
  if (dep.endsWith(":")) {
197
253
  const col = dep.slice(0, -1);
198
254
  for (const candidate of nodes) {
@@ -258,6 +314,11 @@ var DependencyGraph = class {
258
314
  return { order, cyclic };
259
315
  }
260
316
  };
317
+ function unionOf(a, b) {
318
+ if (!b || b.size === 0) return Array.from(a ?? []);
319
+ if (!a || a.size === 0) return Array.from(b);
320
+ return Array.from(/* @__PURE__ */ new Set([...a, ...b]));
321
+ }
261
322
 
262
323
  // src/spreadsheet/formula/references.ts
263
324
  var CELL_PART = /^(\$?)([A-Za-z]+)(\$?)(\d+)$/;
@@ -335,11 +396,17 @@ function mapFormulaReferences(formula, rewrite) {
335
396
  let cursor = 0;
336
397
  for (let i = 0; i < tokens.length; i++) {
337
398
  const token = tokens[i];
338
- if (token.type !== "CELL_REF" && token.type !== "RANGE") continue;
399
+ if (token.type !== "CELL_REF" && token.type !== "RANGE" && token.type !== "SPILL_REF") continue;
339
400
  if (tokens[i + 1]?.type === "LPAREN") continue;
340
401
  let end = token.position;
341
402
  while (end < formula.length && REF_CHAR.test(formula[end])) end++;
342
403
  const original = formula.slice(token.position, end);
404
+ if (token.type === "SPILL_REF") {
405
+ const rewritten = rewrite(original);
406
+ result += formula.slice(cursor, token.position) + (rewritten === "#REF!" ? rewritten : `${rewritten}#`);
407
+ cursor = end + 1;
408
+ continue;
409
+ }
343
410
  result += formula.slice(cursor, token.position) + rewrite(original);
344
411
  cursor = end;
345
412
  }
@@ -617,28 +684,41 @@ function compareCellValues(a, b, direction = "asc") {
617
684
  const aBlank = isBlankValue(a);
618
685
  const bBlank = isBlankValue(b);
619
686
  if (aBlank || bBlank) return aBlank === bBlank ? 0 : aBlank ? 1 : -1;
620
- const aNum = toNumber(a);
621
- const bNum = toNumber(b);
622
687
  let result;
623
- if (aNum !== null && bNum !== null) result = aNum - bNum;
624
- else if (aNum !== null) result = -1;
625
- else if (bNum !== null) result = 1;
626
- else result = compareText(String(a), String(b));
688
+ if (isPlainText(a) && isPlainText(b) && !(parseIsoDate(a) && parseIsoDate(b))) {
689
+ result = compareText(a, b);
690
+ } else {
691
+ result = sortCompareValues(a, b);
692
+ }
627
693
  if (result === 0) return 0;
628
694
  return direction === "desc" ? -Math.sign(result) : Math.sign(result);
629
695
  }
696
+ var isPlainText = (value) => typeof value === "string" && !isFormulaError(value);
697
+ function filterValue(value) {
698
+ if (isBlankValue(value) || isFormulaError(value)) return null;
699
+ if (typeof value === "number") return Number.isFinite(value) ? { value, kind: "number" } : null;
700
+ if (typeof value === "boolean") return { value, kind: "boolean" };
701
+ const num = numericValue(value);
702
+ if (num !== null) return { value: num, kind: "number" };
703
+ const text = String(value);
704
+ return { value: text, kind: parseIsoDate(text) ? "number" : "text" };
705
+ }
706
+ function filterOperand(operand) {
707
+ if (typeof operand !== "string") return filterValue(operand);
708
+ if (operand.trim() === "") return null;
709
+ const parsed = parseCellInput(operand.trim());
710
+ if (parsed.type === "number" || parsed.type === "date" || parsed.type === "boolean") return filterValue(parsed.value);
711
+ return { value: operand, kind: "text" };
712
+ }
630
713
  function compareForFilter(value, operand) {
631
- if (isBlankValue(value) || isBlankValue(operand)) return null;
632
- const a = toNumber(value);
633
- const b = toNumber(operand);
634
- if (a !== null && b !== null) return a - b;
635
- if (a !== null || b !== null) return null;
636
- return compareText(String(value), String(operand));
714
+ const a = filterValue(value);
715
+ const b = filterOperand(operand);
716
+ if (!a || !b || a.kind !== b.kind) return null;
717
+ return compareValues(a.value, b.value);
637
718
  }
638
719
  function looselyEquals(value, operand) {
639
- const a = toNumber(value);
640
- const b = toNumber(operand);
641
- if (a !== null && b !== null) return a === b;
720
+ const c = compareForFilter(value, operand);
721
+ if (c !== null) return c === 0;
642
722
  return String(value ?? "").trim().toLowerCase() === String(operand ?? "").trim().toLowerCase();
643
723
  }
644
724
  function matchesFilterRule(value, rule) {
@@ -713,7 +793,8 @@ function classifyFillValue(raw) {
713
793
  if (raw.trim() === "") return { kind: "blank" };
714
794
  const num = toNumber(raw);
715
795
  if (num !== null) return { kind: "number", value: num };
716
- const date = ISO_DATE_PATTERN.exec(raw.trim());
796
+ const typed = parseCellInput(raw);
797
+ const date = ISO_DATE_PATTERN.exec(typed.type === "date" ? String(typed.value) : raw.trim());
717
798
  if (date) {
718
799
  const [year, month, day] = [Number(date[1]), Number(date[2]), Number(date[3])];
719
800
  const time = Date.UTC(year, month - 1, day);
@@ -1010,11 +1091,16 @@ function parseHtmlTable(html) {
1010
1091
  return rows.length > 0 ? rows : null;
1011
1092
  }
1012
1093
  function createSpreadsheetClipboardData(cells, origin) {
1013
- const values = cells.map((row) => row.map((cell) => cell.value));
1014
- const payload = { type: "pixerate-spreadsheet", version: 1, origin, cells };
1094
+ const texts = cells.map((row) => row.map((cell) => cell.text ?? displayText(cell.value)));
1095
+ const payload = {
1096
+ type: "pixerate-spreadsheet",
1097
+ version: 1,
1098
+ origin,
1099
+ cells: cells.map((row) => row.map(({ raw, value }) => ({ raw, value })))
1100
+ };
1015
1101
  return {
1016
- "text/plain": serializeTsv(values.map((row) => row.map(displayText))),
1017
- "text/html": serializeHtmlTable(values),
1102
+ "text/plain": serializeTsv(texts),
1103
+ "text/html": serializeHtmlTable(texts),
1018
1104
  [SPREADSHEET_CLIPBOARD_MIME]: JSON.stringify(payload)
1019
1105
  };
1020
1106
  }
@@ -1228,6 +1314,9 @@ function buildSpreadsheetView(doc, getValue) {
1228
1314
 
1229
1315
  // src/spreadsheet/controller.ts
1230
1316
  var nextIdSequence = 0;
1317
+ var DEFAULT_MAX_HISTORY_BYTES = 64 * 1024 * 1024;
1318
+ var MAX_SPILL_PASSES = 25;
1319
+ var spillCellId = (rowId, colId) => `${rowId}\0${colId}`;
1231
1320
  var SpreadsheetController = class {
1232
1321
  document;
1233
1322
  activeCell = null;
@@ -1245,11 +1334,24 @@ var SpreadsheetController = class {
1245
1334
  undoStack = [];
1246
1335
  redoStack = [];
1247
1336
  isApplyingHistory = false;
1337
+ maxHistoryBytes;
1248
1338
  // Set while importing TSV: history and recalculation are done once at the end.
1249
1339
  batching = false;
1250
1340
  pendingCommits = [];
1251
1341
  // Row being evaluated, so [property] references resolve per row.
1252
1342
  evaluatingRowIndex = null;
1343
+ // Cell being evaluated, for implicit intersection (`=@A:A`).
1344
+ evaluatingCell = null;
1345
+ // Dynamic arrays. Spilled values are derived state: they live here, not in
1346
+ // the document, and every recalculation rebuilds them.
1347
+ spills = /* @__PURE__ */ new Map();
1348
+ // anchor key -> spill
1349
+ spilledBy = /* @__PURE__ */ new Map();
1350
+ // spilled cell key -> anchor key
1351
+ spillCells = /* @__PURE__ */ new Map();
1352
+ // spillCellId -> displayed cell
1353
+ // Cells whose spill ownership changed during the current pass.
1354
+ spillChanges = null;
1253
1355
  constructor(options = {}) {
1254
1356
  const defaultCols = [
1255
1357
  { id: "col_a", key: "A", title: "A", width: 120, type: "freeform" },
@@ -1274,6 +1376,7 @@ var SpreadsheetController = class {
1274
1376
  ...options.document?.table ? { table: options.document.table } : {}
1275
1377
  };
1276
1378
  this.primaryDataSourceId = options.primaryDataSourceId;
1379
+ this.maxHistoryBytes = Math.max(0, options.maxHistoryBytes ?? DEFAULT_MAX_HISTORY_BYTES);
1277
1380
  this.onDocumentChange = options.onDocumentChange;
1278
1381
  this.onSelectionChange = options.onSelectionChange;
1279
1382
  this.onCellCommit = options.onCellCommit;
@@ -1283,9 +1386,12 @@ var SpreadsheetController = class {
1283
1386
  this.evaluator = new FormulaEvaluator({
1284
1387
  resolveCell: (colStr, rowNum) => this.resolveCellByCoord(colStr, rowNum),
1285
1388
  resolveRange: (sCol, sRow, eCol, eRow) => this.resolveRangeByCoords(sCol, sRow, eCol, eRow),
1286
- resolveProperty: (prop) => this.resolveCurrentRowProperty(prop)
1389
+ resolveProperty: (prop) => this.resolveCurrentRowProperty(prop),
1390
+ resolveSpill: (colStr, rowNum) => this.resolveSpillByCoord(colStr, rowNum),
1391
+ getFormulaCell: () => this.evaluatingCell
1287
1392
  });
1288
1393
  this.syncWithDataSource();
1394
+ this.rederiveTypedValues();
1289
1395
  this.recalculateAll();
1290
1396
  }
1291
1397
  loadDocument(document) {
@@ -1297,9 +1403,63 @@ var SpreadsheetController = class {
1297
1403
  this.undoStack = [];
1298
1404
  this.redoStack = [];
1299
1405
  this.syncWithDataSource();
1406
+ this.rederiveTypedValues();
1300
1407
  this.recalculateAll();
1301
1408
  this.notifyChange();
1302
1409
  }
1410
+ /**
1411
+ * Re-parses the value of every stored typed (non-formula) cell from its
1412
+ * `raw` input, so documents saved before typed input was parsed (whose
1413
+ * values are text such as `"120"`) compare and sort as numbers. Cells of
1414
+ * bound columns in record rows keep their value: it belongs to the data
1415
+ * source.
1416
+ */
1417
+ rederiveTypedValues() {
1418
+ for (const row of this.document.rows) {
1419
+ const cells = this.document.cells[row.id];
1420
+ if (!cells) continue;
1421
+ for (const col of this.document.columns) {
1422
+ const cell = cells[col.id];
1423
+ if (!cell || typeof cell.raw !== "string" || cell.raw.startsWith("=")) continue;
1424
+ if (col.type === "bound" && col.binding && row.recordId) continue;
1425
+ const typed = this.typedCell(cell.raw, cell.format);
1426
+ cell.value = typed.value;
1427
+ if (typed.numberFormat) cell.numberFormat = typed.numberFormat;
1428
+ else delete cell.numberFormat;
1429
+ if (typed.format) cell.format = typed.format;
1430
+ }
1431
+ }
1432
+ }
1433
+ /**
1434
+ * Value of typed (non-formula) input: a number, boolean, ISO date, error
1435
+ * or text (see `parseCellInput`), its automatic number format, and the
1436
+ * `CellFormat` to use: the automatic one (currency, percentage, date,
1437
+ * number) when `format` is unset or `text` (the default of new columns).
1438
+ */
1439
+ typedCell(raw, format) {
1440
+ const parsed = parseCellInput(raw);
1441
+ return {
1442
+ value: parsed.value,
1443
+ ...parsed.numberFormat ? { numberFormat: parsed.numberFormat } : {},
1444
+ format: parsed.cellFormat && (!format || format === "text") ? parsed.cellFormat : format
1445
+ };
1446
+ }
1447
+ /**
1448
+ * Typed input for a bound field, coerced by the field's declared type:
1449
+ * number, boolean and date fields get a number, boolean or ISO date when
1450
+ * the input parses as one; string and enum fields (and unknown fields)
1451
+ * keep the text exactly as typed.
1452
+ */
1453
+ boundInputValue(dataSourceId, field, raw) {
1454
+ if (raw.startsWith("=")) return raw;
1455
+ const type = this.binder.getDataSource(dataSourceId)?.getFields().find((f) => f.key === field)?.type;
1456
+ if (type !== "number" && type !== "boolean" && type !== "date") return raw;
1457
+ const parsed = parseCellInput(raw);
1458
+ if (type === "number" && parsed.type === "number") return parsed.value;
1459
+ if (type === "boolean" && parsed.type === "boolean") return parsed.value;
1460
+ if (type === "date" && parsed.type === "date") return parsed.value;
1461
+ return raw;
1462
+ }
1303
1463
  registerDataSource(ds) {
1304
1464
  this.binder.register(ds);
1305
1465
  this.syncWithDataSource();
@@ -1336,6 +1496,11 @@ var SpreadsheetController = class {
1336
1496
  }
1337
1497
  getCell(rowId, colId) {
1338
1498
  const existing = this.document.cells[rowId]?.[colId];
1499
+ if (existing && existing.raw !== "") return existing;
1500
+ if (this.spillCells.size > 0) {
1501
+ const spilled = this.spillCells.get(spillCellId(rowId, colId));
1502
+ if (spilled) return spilled;
1503
+ }
1339
1504
  if (existing) return existing;
1340
1505
  const col = this.document.columns.find((c) => c.id === colId);
1341
1506
  const row = this.document.rows.find((r) => r.id === rowId);
@@ -1371,21 +1536,20 @@ var SpreadsheetController = class {
1371
1536
  }
1372
1537
  const col = this.document.columns.find((c) => c.id === colId);
1373
1538
  const row = this.document.rows.find((r) => r.id === rowId);
1539
+ const isFormula = raw.startsWith("=");
1540
+ let cell;
1374
1541
  if (col?.type === "bound" && col.binding && row?.recordId) {
1375
- this.binder.commitBoundValue(
1376
- col.binding.dataSource,
1377
- row.recordId,
1378
- col.binding.field,
1379
- raw
1380
- );
1542
+ const value = this.boundInputValue(col.binding.dataSource, col.binding.field, raw);
1543
+ this.binder.commitBoundValue(col.binding.dataSource, row.recordId, col.binding.field, value);
1544
+ cell = { raw, value, format: col.format };
1545
+ } else if (isFormula) {
1546
+ cell = { raw, value: raw, format: col?.format };
1547
+ } else {
1548
+ cell = { raw, ...this.typedCell(raw, col?.format) };
1381
1549
  }
1382
1550
  const cellKey = this.getCellKey(rowId, colId);
1383
- this.depGraph.setCellFormula(cellKey, raw.startsWith("=") ? raw : null);
1384
- this.document.cells[rowId][colId] = {
1385
- raw,
1386
- value: raw,
1387
- format: col?.format
1388
- };
1551
+ this.depGraph.setCellFormula(cellKey, isFormula ? raw : null);
1552
+ this.document.cells[rowId][colId] = cell;
1389
1553
  this.document.updatedAt = Date.now();
1390
1554
  if (this.batching) {
1391
1555
  this.pendingCommits.push([rowId, colId, raw]);
@@ -1408,7 +1572,30 @@ var SpreadsheetController = class {
1408
1572
  */
1409
1573
  recalculateCell(rowId, colId) {
1410
1574
  const cellKey = this.getCellKey(rowId, colId);
1411
- this.runEvaluationPlan(this.depGraph.getEvaluationPlan([cellKey], { includeDependents: true }));
1575
+ const anchor = this.spilledBy.get(cellKey);
1576
+ this.recalculateKeys(anchor ? [cellKey, anchor] : [cellKey], true);
1577
+ }
1578
+ /**
1579
+ * Recalculates `keys` (and, with `includeDependents`, their dependents) in
1580
+ * dependency order. Spill ranges are only known after evaluating their
1581
+ * anchors, so when a pass changes which cells hold spilled values (and so
1582
+ * the dependency graph), the dependents of those cells are recalculated in
1583
+ * another pass, until nothing changes.
1584
+ */
1585
+ recalculateKeys(keys, includeDependents) {
1586
+ let pending = Array.from(keys);
1587
+ let withDependents = includeDependents;
1588
+ for (let pass = 0; pass < MAX_SPILL_PASSES && pending.length > 0; pass++) {
1589
+ const changed = /* @__PURE__ */ new Set();
1590
+ this.spillChanges = changed;
1591
+ try {
1592
+ this.runEvaluationPlan(this.depGraph.getEvaluationPlan(pending, { includeDependents: withDependents }));
1593
+ } finally {
1594
+ this.spillChanges = null;
1595
+ }
1596
+ pending = Array.from(changed);
1597
+ withDependents = true;
1598
+ }
1412
1599
  }
1413
1600
  /**
1414
1601
  * Rebuilds the dependency graph from scratch and recalculates every formula
@@ -1416,6 +1603,9 @@ var SpreadsheetController = class {
1416
1603
  */
1417
1604
  recalculateAll() {
1418
1605
  this.depGraph.clear();
1606
+ this.spills.clear();
1607
+ this.spilledBy.clear();
1608
+ this.spillCells.clear();
1419
1609
  const formulaKeys = [];
1420
1610
  for (const row of this.document.rows) {
1421
1611
  for (const col of this.document.columns) {
@@ -1427,7 +1617,7 @@ var SpreadsheetController = class {
1427
1617
  }
1428
1618
  }
1429
1619
  }
1430
- this.runEvaluationPlan(this.depGraph.getEvaluationPlan(formulaKeys));
1620
+ this.recalculateKeys(formulaKeys, false);
1431
1621
  }
1432
1622
  runEvaluationPlan(plan) {
1433
1623
  for (const key of plan.order) {
@@ -1454,7 +1644,10 @@ var SpreadsheetController = class {
1454
1644
  if (!this.document.cells[row.id]) this.document.cells[row.id] = {};
1455
1645
  const deps = this.depGraph.getDependencies(cellKey);
1456
1646
  const selfReference = deps.includes(cellKey) || deps.includes(`${col.key.toUpperCase()}:`);
1457
- const errorMessage = selfReference ? "This formula can't reference its own cell." : "This formula is part of a circular reference: it depends on its own result through other cells.";
1647
+ const spilledKeys = this.depGraph.getSpilledKeys(cellKey);
1648
+ const ownSpill = deps.find((dep) => spilledKeys.includes(dep));
1649
+ const errorMessage = selfReference ? "This formula can't reference its own cell." : ownSpill ? `This formula refers to ${ownSpill}, which holds its own spilled results (a circular reference).` : "This formula is part of a circular reference: it depends on its own result through other cells.";
1650
+ this.clearSpill(cellKey.toUpperCase(), true);
1458
1651
  this.document.cells[row.id][col.id] = { ...cell, value: "#CYCLE!", error: "#CYCLE!", errorMessage };
1459
1652
  }
1460
1653
  evalSingleCellByKey(cellKey) {
@@ -1471,25 +1664,176 @@ var SpreadsheetController = class {
1471
1664
  cell.error = void 0;
1472
1665
  cell.errorMessage = void 0;
1473
1666
  }
1667
+ this.clearSpill(`${colName}${rowNum}`);
1474
1668
  return;
1475
1669
  }
1476
1670
  try {
1477
1671
  this.evaluatingRowIndex = rowNum - 1;
1672
+ this.evaluatingCell = { col: colName, row: rowNum };
1478
1673
  const result = this.evaluator.evaluateWithDiagnostics(cell.raw);
1479
1674
  if (!this.document.cells[row.id]) {
1480
1675
  this.document.cells[row.id] = {};
1481
1676
  }
1677
+ let { value, error, message } = result;
1678
+ if (result.array) {
1679
+ const blocked = this.placeSpill(`${colName}${rowNum}`, rowNum - 1, this.document.columns.indexOf(col), result.array);
1680
+ if (blocked) {
1681
+ value = "#SPILL!";
1682
+ error = "#SPILL!";
1683
+ message = blocked;
1684
+ }
1685
+ } else {
1686
+ this.clearSpill(`${colName}${rowNum}`);
1687
+ }
1482
1688
  this.document.cells[row.id][col.id] = {
1483
1689
  ...cell,
1484
- value: result.value,
1690
+ value,
1485
1691
  // `#ERROR!` (malformed formula, wrong argument count) keeps its
1486
1692
  // message in `error`, as before; other errors store their code.
1487
- error: result.error === "#ERROR!" ? result.message : result.error,
1488
- errorMessage: result.message
1693
+ error: error === "#ERROR!" ? message : error,
1694
+ errorMessage: message
1489
1695
  };
1490
1696
  } finally {
1491
1697
  this.evaluatingRowIndex = null;
1698
+ this.evaluatingCell = null;
1699
+ }
1700
+ }
1701
+ // --- Dynamic arrays (spilling) ---
1702
+ /**
1703
+ * Spills `array` from the anchor at (row, col) into the cells below and to
1704
+ * the right. Returns why it can't (the anchor then shows `#SPILL!`), or
1705
+ * null once placed. The grid never grows to fit an array, as in Excel.
1706
+ */
1707
+ placeSpill(anchor, row, col, array) {
1708
+ const { rows, cols } = array;
1709
+ const area = { startRow: row, startCol: col, endRow: row + rows - 1, endCol: col + cols - 1 };
1710
+ const label = `${indexToColName(area.startCol)}${area.startRow + 1}:${indexToColName(area.endCol)}${area.endRow + 1}`;
1711
+ let reason = null;
1712
+ const blockers = [];
1713
+ if (area.endRow >= this.document.rows.length || area.endCol >= this.document.columns.length) {
1714
+ reason = `The ${rows}\xD7${cols} result needs ${label}, which extends past the edge of the sheet.`;
1715
+ } else if (this.getMerges().some((m) => rangesIntersect(m, area))) {
1716
+ reason = `The result can't spill into merged cells (it needs ${label}).`;
1717
+ } else {
1718
+ for (let c = area.startCol; c <= area.endCol; c++) {
1719
+ const column = this.document.columns[c];
1720
+ for (let r = area.startRow; r <= area.endRow; r++) {
1721
+ if (r === row && c === col) continue;
1722
+ const rowObj = this.document.rows[r];
1723
+ const key = `${column.key}${r + 1}`.toUpperCase();
1724
+ const stored = this.document.cells[rowObj.id]?.[column.id];
1725
+ const raw = stored ? stored.raw : this.getDerivedCell(rowObj, column).raw;
1726
+ const owner = this.spilledBy.get(key);
1727
+ if (raw !== "") {
1728
+ blockers.push(key);
1729
+ reason ??= `The result needs ${label}, but ${key} isn't empty.`;
1730
+ } else if (owner && owner !== anchor) {
1731
+ blockers.push(key);
1732
+ reason ??= `The result needs ${label}, but ${key} already holds values spilled from ${owner}.`;
1733
+ }
1734
+ }
1735
+ }
1736
+ }
1737
+ if (reason) {
1738
+ this.clearSpill(anchor);
1739
+ this.depGraph.setSpillEdges(anchor, [], blockers);
1740
+ return reason;
1741
+ }
1742
+ const previous = this.spills.get(anchor);
1743
+ const cells = [];
1744
+ for (let c = 0; c < cols; c++) {
1745
+ const column = this.document.columns[col + c];
1746
+ for (let r = 0; r < rows; r++) {
1747
+ if (r === 0 && c === 0) continue;
1748
+ const rowObj = this.document.rows[row + r];
1749
+ const key = `${column.key}${row + r + 1}`.toUpperCase();
1750
+ const value = array.values[c * rows + r] ?? "";
1751
+ const display = { raw: "", value, format: column.format, spillAnchor: anchor };
1752
+ if (isFormulaError(value)) {
1753
+ display.error = value;
1754
+ display.errorMessage = describeFormulaError(value);
1755
+ }
1756
+ this.spillCells.set(spillCellId(rowObj.id, column.id), display);
1757
+ if (this.spilledBy.get(key) !== anchor) {
1758
+ this.spilledBy.set(key, anchor);
1759
+ this.spillChanges?.add(key);
1760
+ }
1761
+ cells.push({ key, rowId: rowObj.id, colId: column.id });
1762
+ }
1763
+ }
1764
+ if (previous) {
1765
+ const kept = new Set(cells.map((cell) => cell.key));
1766
+ for (const old of previous.cells) {
1767
+ if (kept.has(old.key)) continue;
1768
+ this.forgetSpilledCell(anchor, old);
1769
+ }
1770
+ }
1771
+ this.spills.set(anchor, { anchor, row, col, rows, cols, values: array.values, cells });
1772
+ this.depGraph.setSpillEdges(anchor, cells.map((cell) => cell.key));
1773
+ return null;
1774
+ }
1775
+ /** Removes the array spilled from `anchor`. With `keepEdges`, its dependency edges stay. */
1776
+ clearSpill(anchor, keepEdges = false) {
1777
+ const record = this.spills.get(anchor);
1778
+ if (record) {
1779
+ for (const cell of record.cells) this.forgetSpilledCell(anchor, cell);
1780
+ this.spills.delete(anchor);
1781
+ }
1782
+ if (!keepEdges) this.depGraph.setSpillEdges(anchor, []);
1783
+ }
1784
+ forgetSpilledCell(anchor, cell) {
1785
+ if (this.spilledBy.get(cell.key) !== anchor) return;
1786
+ this.spilledBy.delete(cell.key);
1787
+ this.spillCells.delete(spillCellId(cell.rowId, cell.colId));
1788
+ this.spillChanges?.add(cell.key);
1789
+ }
1790
+ resolveSpillByCoord(colStr, rowNum) {
1791
+ const record = this.spills.get(`${colStr}${rowNum}`.toUpperCase());
1792
+ if (record) return { rows: record.rows, cols: record.cols, values: record.values };
1793
+ const value = this.resolveCellByCoord(colStr, rowNum);
1794
+ return isFormulaError(value) ? value : "#REF!";
1795
+ }
1796
+ toSpill(record) {
1797
+ const row = this.document.rows[record.row];
1798
+ const col = this.document.columns[record.col];
1799
+ return {
1800
+ key: record.anchor,
1801
+ anchor: { row: record.row, col: record.col },
1802
+ range: {
1803
+ startRow: record.row,
1804
+ startCol: record.col,
1805
+ endRow: record.row + record.rows - 1,
1806
+ endCol: record.col + record.cols - 1
1807
+ },
1808
+ formula: row && col ? this.getCell(row.id, col.id).raw : ""
1809
+ };
1810
+ }
1811
+ /**
1812
+ * The dynamic array covering (row, col), whether it is the anchor (the cell
1813
+ * holding the formula) or a spilled cell; null when there is none.
1814
+ */
1815
+ getSpillAt(row, col) {
1816
+ if (this.spills.size === 0) return null;
1817
+ const key = `${indexToColName(col)}${row + 1}`;
1818
+ const record = this.spills.get(key) ?? this.spills.get(this.spilledBy.get(key) ?? "");
1819
+ return record ? this.toSpill(record) : null;
1820
+ }
1821
+ /** Every dynamic array currently spilled. */
1822
+ getSpills() {
1823
+ return Array.from(this.spills.values(), (record) => this.toSpill(record));
1824
+ }
1825
+ /** Whether `range` overlaps a spilled array (its anchor or spilled cells). */
1826
+ intersectsSpill(range) {
1827
+ for (const record of this.spills.values()) {
1828
+ const area = {
1829
+ startRow: record.row,
1830
+ startCol: record.col,
1831
+ endRow: record.row + record.rows - 1,
1832
+ endCol: record.col + record.cols - 1
1833
+ };
1834
+ if (rangesIntersect(area, range)) return true;
1492
1835
  }
1836
+ return false;
1493
1837
  }
1494
1838
  resolveCellByCoord(colStr, rowNum) {
1495
1839
  const col = this.document.columns.find((c) => c.key.toUpperCase() === colStr.toUpperCase());
@@ -1721,7 +2065,7 @@ var SpreadsheetController = class {
1721
2065
  let maxLen = (col.title || "").length + (col.key ? col.key.length + 3 : 0);
1722
2066
  for (const row of this.document.rows) {
1723
2067
  const cell = this.getCell(row.id, col.id);
1724
- const valStr = cell.value != null ? String(cell.value) : "";
2068
+ const valStr = formatCellDisplay(cell);
1725
2069
  if (valStr.length > maxLen) {
1726
2070
  maxLen = valStr.length;
1727
2071
  }
@@ -1745,8 +2089,7 @@ var SpreadsheetController = class {
1745
2089
  ) : this.document.columns;
1746
2090
  return rows.map(
1747
2091
  (row) => columns.map((col) => {
1748
- const cell = this.getCell(row.id, col.id);
1749
- return String(cell.value ?? "").replace(/\t/g, " ");
2092
+ return formatCellDisplay(this.getCell(row.id, col.id)).replace(/\t/g, " ");
1750
2093
  }).join(" ")
1751
2094
  ).join("\n");
1752
2095
  }
@@ -1823,7 +2166,7 @@ var SpreadsheetController = class {
1823
2166
  */
1824
2167
  exportToCsv(options = {}) {
1825
2168
  const matrix = this.readMatrix(options.range ?? null).map(
1826
- (row) => row.map((cell) => options.formulas ? cell.raw : String(cell.value ?? ""))
2169
+ (row) => row.map((cell) => options.formulas ? cell.raw : formatCellDisplay(cell))
1827
2170
  );
1828
2171
  return serializeCsv(matrix, {
1829
2172
  delimiter: options.delimiter,
@@ -1861,7 +2204,16 @@ var SpreadsheetController = class {
1861
2204
  */
1862
2205
  copyToClipboardData(range = this.selectedRange) {
1863
2206
  const r = range ? normalizeRange(range) : { startRow: 0, startCol: 0, endRow: this.document.rows.length - 1, endCol: this.document.columns.length - 1 };
1864
- const cells = this.readMatrix(r).map((row) => row.map((cell) => ({ raw: cell.raw, value: cell.error || cell.value })));
2207
+ const cells = this.readMatrix(r).map(
2208
+ (row) => row.map((cell) => {
2209
+ const value = cell.error || cell.value;
2210
+ const text = formatCellDisplay(cell);
2211
+ if (cell.spillAnchor && !this.spillAnchorInRange(cell.spillAnchor, r)) {
2212
+ return { raw: text, value, text };
2213
+ }
2214
+ return { raw: cell.raw, value, text };
2215
+ })
2216
+ );
1865
2217
  return createSpreadsheetClipboardData(cells, { row: r.startRow, col: r.startCol });
1866
2218
  }
1867
2219
  /**
@@ -1884,6 +2236,10 @@ var SpreadsheetController = class {
1884
2236
  this.selectRange(r);
1885
2237
  return true;
1886
2238
  }
2239
+ spillAnchorInRange(anchor, range) {
2240
+ const record = this.spills.get(anchor);
2241
+ return !!record && rangeContains(range, record.row, record.col);
2242
+ }
1887
2243
  getSelectionTopLeft() {
1888
2244
  if (this.selectedRange) {
1889
2245
  const r = normalizeRange(this.selectedRange);
@@ -1979,6 +2335,7 @@ var SpreadsheetController = class {
1979
2335
  if (keyCols.length === 0 || keyCols.some((k) => k.index < startCol || k.index > endCol)) return false;
1980
2336
  const area = { startRow, startCol, endRow, endCol };
1981
2337
  if ((this.document.table?.merges ?? []).some((m) => rangesIntersect(m, area))) return false;
2338
+ if (this.intersectsSpill(area)) return false;
1982
2339
  const sourceRows = [];
1983
2340
  for (let r = startRow; r <= endRow; r++) sourceRows.push(r);
1984
2341
  const sorted = sourceRows.map((r) => ({ r, values: keyCols.map((k) => this.getCellValueAt(r, k.index)) })).sort((a, b) => {
@@ -2186,6 +2543,7 @@ var SpreadsheetController = class {
2186
2543
  }
2187
2544
  const merges = this.getMerges();
2188
2545
  if (merges.some((m) => rangesIntersect(m, r) && !rangeContainsRange(r, m))) return false;
2546
+ if (this.intersectsSpill(r)) return false;
2189
2547
  this.applyTableChange(() => {
2190
2548
  for (let row = r.startRow; row <= r.endRow; row++) {
2191
2549
  const rowId = this.document.rows[row].id;
@@ -2207,7 +2565,7 @@ var SpreadsheetController = class {
2207
2565
  if (remaining.length === merges.length) return false;
2208
2566
  this.applyTableChange(() => {
2209
2567
  this.table.merges = remaining;
2210
- });
2568
+ }, { recalculate: true });
2211
2569
  return true;
2212
2570
  }
2213
2571
  // --- Autofill ---
@@ -2328,6 +2686,7 @@ var SpreadsheetController = class {
2328
2686
  const width = s.endCol - s.startCol + 1;
2329
2687
  const t = { startRow: s.startRow, startCol: s.startCol, endRow: s.startRow + width - 1, endCol: s.startCol + height - 1 };
2330
2688
  if (this.getMerges().some((m) => rangesIntersect(m, s) || rangesIntersect(m, t))) return false;
2689
+ if (this.intersectsSpill(s) || this.intersectsSpill(t)) return false;
2331
2690
  this.applyTableChange(() => {
2332
2691
  this.growGrid(t.endRow, t.endCol);
2333
2692
  this.remapAllFormulas(({ row, col }) => {
@@ -2371,6 +2730,28 @@ var SpreadsheetController = class {
2371
2730
  this.undoStack.push(JSON.stringify(this.document));
2372
2731
  if (this.undoStack.length > 50) this.undoStack.shift();
2373
2732
  this.redoStack = [];
2733
+ this.trimHistory();
2734
+ }
2735
+ /** Drops the oldest snapshots beyond `maxHistoryBytes`, always keeping the newest. */
2736
+ trimHistory() {
2737
+ if (this.maxHistoryBytes <= 0 || this.undoStack.length <= 1) return;
2738
+ let total = 0;
2739
+ for (const s of this.undoStack) total += s.length * 2;
2740
+ for (const s of this.redoStack) total += s.length * 2;
2741
+ while (total > this.maxHistoryBytes && this.undoStack.length > 1) {
2742
+ total -= this.undoStack.shift().length * 2;
2743
+ }
2744
+ }
2745
+ /** Approximate bytes held by the undo/redo snapshots (UTF-16 size). */
2746
+ getHistoryByteSize() {
2747
+ let total = 0;
2748
+ for (const s of this.undoStack) total += s.length * 2;
2749
+ for (const s of this.redoStack) total += s.length * 2;
2750
+ return total;
2751
+ }
2752
+ /** Number of undo / redo snapshots currently held. */
2753
+ getHistoryDepth() {
2754
+ return { undo: this.undoStack.length, redo: this.redoStack.length };
2374
2755
  }
2375
2756
  undo() {
2376
2757
  if (this.undoStack.length === 0) return;
@@ -2412,6 +2793,17 @@ function stepInOrder(order, from, delta) {
2412
2793
  }
2413
2794
 
2414
2795
  // src/spreadsheet/menu.ts
2796
+ function clampMenuPosition(point, size, viewport, margin = 4) {
2797
+ const place = (start, length, limit) => {
2798
+ if (start + length <= limit - margin) return Math.max(margin, start);
2799
+ const flipped = start - length;
2800
+ return Math.max(margin, flipped >= margin ? flipped : limit - margin - length);
2801
+ };
2802
+ return {
2803
+ x: place(point.x, size.width, viewport.width),
2804
+ y: place(point.y, size.height, viewport.height)
2805
+ };
2806
+ }
2415
2807
  var quote = (value) => {
2416
2808
  const text = String(value ?? "");
2417
2809
  return text.length > 24 ? `"${text.slice(0, 23)}\u2026"` : `"${text}"`;
@@ -2423,7 +2815,8 @@ function getSpreadsheetMenuItems(controller, cell) {
2423
2815
  if (!column || !row) return [];
2424
2816
  const selection = controller.selectedRange && rangeContains(controller.selectedRange, cell.row, cell.col) ? normalizeRange(controller.selectedRange) : { startRow: cell.row, startCol: cell.col, endRow: cell.row, endCol: cell.col };
2425
2817
  const colName = indexToColName(cell.col);
2426
- const value = controller.getCell(row.id, column.id).value;
2818
+ const cellData = controller.getCell(row.id, column.id);
2819
+ const value = cellData.value;
2427
2820
  const table = doc.table ?? {};
2428
2821
  const filter = controller.getFilterState();
2429
2822
  const grouping = table.grouping;
@@ -2445,7 +2838,7 @@ function getSpreadsheetMenuItems(controller, cell) {
2445
2838
  },
2446
2839
  {
2447
2840
  id: "filter-equals",
2448
- label: `Show only rows where ${colName} = ${quote(value)}`,
2841
+ label: `Show only rows where ${colName} = ${quote(formatCellDisplay(cellData))}`,
2449
2842
  separatorBefore: true,
2450
2843
  run: () => controller.addFilterRule({ colId: column.id, operator: "equals", value: String(value ?? "") })
2451
2844
  },
@@ -2694,6 +3087,7 @@ export {
2694
3087
  computeRowGroups,
2695
3088
  buildSpreadsheetView,
2696
3089
  SpreadsheetController,
3090
+ clampMenuPosition,
2697
3091
  getSpreadsheetMenuItems,
2698
3092
  SPREADSHEET_TABLE_ACTIONS,
2699
3093
  bindSpreadsheetTableActions,
@@ -2703,4 +3097,4 @@ export {
2703
3097
  registerSpreadsheetImportAdapters,
2704
3098
  importFileIntoSpreadsheet
2705
3099
  };
2706
- //# sourceMappingURL=chunk-GVW4P7BG.js.map
3100
+ //# sourceMappingURL=chunk-K3R5NG2D.js.map