@pixerate/editor 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +71 -1
  2. package/THIRD_PARTY_NOTICES.md +29 -0
  3. package/dist/agent/index.cjs +10 -9
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +1 -1
  6. package/dist/agent/index.d.ts +1 -1
  7. package/dist/agent/index.js +4 -3
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/canvas/index.cjs +3173 -340
  10. package/dist/canvas/index.cjs.map +1 -1
  11. package/dist/canvas/index.d.cts +617 -4
  12. package/dist/canvas/index.d.ts +617 -4
  13. package/dist/canvas/index.js +3105 -272
  14. package/dist/canvas/index.js.map +1 -1
  15. package/dist/chunk-4BVP64JJ.cjs +1406 -0
  16. package/dist/chunk-4BVP64JJ.cjs.map +1 -0
  17. package/dist/{chunk-XNORY7M6.cjs → chunk-7WISYZFO.cjs} +105 -50
  18. package/dist/chunk-7WISYZFO.cjs.map +1 -0
  19. package/dist/{chunk-OLZJXL4R.cjs → chunk-AL6JMGWW.cjs} +59 -25
  20. package/dist/chunk-AL6JMGWW.cjs.map +1 -0
  21. package/dist/chunk-BVP3TSLH.cjs +69 -0
  22. package/dist/chunk-BVP3TSLH.cjs.map +1 -0
  23. package/dist/{chunk-MCSJFWAO.cjs → chunk-C7P22VIP.cjs} +670 -96
  24. package/dist/chunk-C7P22VIP.cjs.map +1 -0
  25. package/dist/{chunk-PGNZKFQS.js → chunk-ECCCYKGL.js} +76 -21
  26. package/dist/chunk-ECCCYKGL.js.map +1 -0
  27. package/dist/{chunk-3CN2WD3X.js → chunk-FH6F67DN.js} +646 -72
  28. package/dist/chunk-FH6F67DN.js.map +1 -0
  29. package/dist/chunk-HRMEDT2L.js +1406 -0
  30. package/dist/chunk-HRMEDT2L.js.map +1 -0
  31. package/dist/{chunk-ITBSKI26.js → chunk-IXHSYOVY.js} +941 -9
  32. package/dist/chunk-IXHSYOVY.js.map +1 -0
  33. package/dist/{chunk-FODNLX5N.js → chunk-NAXVVS5I.js} +38 -4
  34. package/dist/chunk-NAXVVS5I.js.map +1 -0
  35. package/dist/chunk-O5RYH2PG.js +69 -0
  36. package/dist/chunk-O5RYH2PG.js.map +1 -0
  37. package/dist/{chunk-SBCZ6RN5.cjs → chunk-PO6A6QQH.cjs} +1039 -107
  38. package/dist/chunk-PO6A6QQH.cjs.map +1 -0
  39. package/dist/{chunk-3LV6RRPH.js → chunk-QGYDWPRM.js} +2 -2
  40. package/dist/{chunk-U5KWZU4X.js → chunk-RR7F5GUM.js} +2632 -129
  41. package/dist/chunk-RR7F5GUM.js.map +1 -0
  42. package/dist/{chunk-5S3Z4PPF.js → chunk-UAKDG62Z.js} +232 -25
  43. package/dist/chunk-UAKDG62Z.js.map +1 -0
  44. package/dist/{chunk-SML64VJG.cjs → chunk-V3CHHWOE.cjs} +4 -4
  45. package/dist/{chunk-SML64VJG.cjs.map → chunk-V3CHHWOE.cjs.map} +1 -1
  46. package/dist/chunk-WSQKHXW7.cjs +5726 -0
  47. package/dist/chunk-WSQKHXW7.cjs.map +1 -0
  48. package/dist/{chunk-D27YFC46.cjs → chunk-X4N4WUKB.cjs} +267 -60
  49. package/dist/chunk-X4N4WUKB.cjs.map +1 -0
  50. package/dist/commands/index.cjs +74 -1
  51. package/dist/commands/index.cjs.map +1 -1
  52. package/dist/commands/index.d.cts +34 -1
  53. package/dist/commands/index.d.ts +34 -1
  54. package/dist/commands/index.js +73 -0
  55. package/dist/commands/index.js.map +1 -1
  56. package/dist/compare/index.cjs +3 -3
  57. package/dist/compare/index.d.cts +1 -1
  58. package/dist/compare/index.d.ts +1 -1
  59. package/dist/compare/index.js +2 -2
  60. package/dist/controller-BIJiNoVs.d.cts +1842 -0
  61. package/dist/controller-BIJiNoVs.d.ts +1842 -0
  62. package/dist/{documents-BQjzJNe2.d.ts → documents-CHT1U5Vs.d.ts} +10 -0
  63. package/dist/{documents-KRTPhvst.d.cts → documents-Gp7wb0Me.d.cts} +10 -0
  64. package/dist/extensions/index.cjs +53 -6
  65. package/dist/extensions/index.cjs.map +1 -1
  66. package/dist/extensions/index.d.cts +12 -4
  67. package/dist/extensions/index.d.ts +12 -4
  68. package/dist/extensions/index.js +53 -6
  69. package/dist/generation/index.cjs +1013 -69
  70. package/dist/generation/index.cjs.map +1 -1
  71. package/dist/generation/index.d.cts +474 -13
  72. package/dist/generation/index.d.ts +474 -13
  73. package/dist/generation/index.js +976 -32
  74. package/dist/generation/index.js.map +1 -1
  75. package/dist/image-editor/index.cjs +3385 -1275
  76. package/dist/image-editor/index.cjs.map +1 -1
  77. package/dist/image-editor/index.d.cts +693 -128
  78. package/dist/image-editor/index.d.ts +693 -128
  79. package/dist/image-editor/index.js +3325 -1215
  80. package/dist/image-editor/index.js.map +1 -1
  81. package/dist/{index-fJrlkJCd.d.ts → index-7a6bHTLy.d.ts} +494 -5
  82. package/dist/{index-BhJBSm2d.d.cts → index-Csd5qI8u.d.cts} +494 -5
  83. package/dist/index.cjs +1491 -45
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.d.cts +110 -19
  86. package/dist/index.d.ts +110 -19
  87. package/dist/index.js +1464 -18
  88. package/dist/index.js.map +1 -1
  89. package/dist/mention-text-CDnCFd3c.d.cts +53 -0
  90. package/dist/mention-text-CDnCFd3c.d.ts +53 -0
  91. package/dist/model-C1zApQM8.d.cts +219 -0
  92. package/dist/model-C1zApQM8.d.ts +219 -0
  93. package/dist/persistence/index.d.cts +3 -120
  94. package/dist/persistence/index.d.ts +3 -120
  95. package/dist/{registry-GJI4gToU.d.cts → registry-CEgwXED1.d.cts} +12 -1
  96. package/dist/{registry-GJI4gToU.d.ts → registry-CEgwXED1.d.ts} +12 -1
  97. package/dist/serializers/index.cjs +12 -4
  98. package/dist/serializers/index.cjs.map +1 -1
  99. package/dist/serializers/index.d.cts +39 -12
  100. package/dist/serializers/index.d.ts +39 -12
  101. package/dist/serializers/index.js +12 -4
  102. package/dist/spreadsheet/index.cjs +127 -3
  103. package/dist/spreadsheet/index.cjs.map +1 -1
  104. package/dist/spreadsheet/index.d.cts +431 -11
  105. package/dist/spreadsheet/index.d.ts +431 -11
  106. package/dist/spreadsheet/index.js +127 -3
  107. package/dist/storage-BpFfspHm.d.cts +120 -0
  108. package/dist/storage-BpFfspHm.d.ts +120 -0
  109. package/dist/tokens/index.cjs +115 -0
  110. package/dist/tokens/index.cjs.map +1 -0
  111. package/dist/tokens/index.d.cts +212 -0
  112. package/dist/tokens/index.d.ts +212 -0
  113. package/dist/tokens/index.js +115 -0
  114. package/dist/tokens/index.js.map +1 -0
  115. package/dist/{track-changes-BK5sVf4_.d.cts → track-changes-D_VAPELi.d.cts} +95 -11
  116. package/dist/{track-changes-CDjCrwKe.d.ts → track-changes-Db_fGEvx.d.ts} +95 -11
  117. package/package.json +6 -1
  118. package/dist/chunk-3CN2WD3X.js.map +0 -1
  119. package/dist/chunk-5S3Z4PPF.js.map +0 -1
  120. package/dist/chunk-D27YFC46.cjs.map +0 -1
  121. package/dist/chunk-FODNLX5N.js.map +0 -1
  122. package/dist/chunk-ITBSKI26.js.map +0 -1
  123. package/dist/chunk-MA2F5KHF.cjs +0 -3223
  124. package/dist/chunk-MA2F5KHF.cjs.map +0 -1
  125. package/dist/chunk-MCSJFWAO.cjs.map +0 -1
  126. package/dist/chunk-OLZJXL4R.cjs.map +0 -1
  127. package/dist/chunk-PGNZKFQS.js.map +0 -1
  128. package/dist/chunk-SBCZ6RN5.cjs.map +0 -1
  129. package/dist/chunk-U5KWZU4X.js.map +0 -1
  130. package/dist/chunk-XNORY7M6.cjs.map +0 -1
  131. package/dist/controller-BhLLAoi8.d.cts +0 -1000
  132. package/dist/controller-BhLLAoi8.d.ts +0 -1000
  133. /package/dist/{chunk-3LV6RRPH.js.map → chunk-QGYDWPRM.js.map} +0 -0
@@ -19,30 +19,64 @@ import {
19
19
  indexToColName,
20
20
  isBlank,
21
21
  isFormulaError,
22
+ numberToText,
22
23
  numericValue,
23
24
  parseCellInput,
24
25
  parseFormula,
25
26
  parseIsoDate,
26
27
  searchFormulaFunctions,
28
+ serialFromYmd,
29
+ serialToDate,
27
30
  sortCompareValues,
31
+ toBoolean,
28
32
  toText,
29
33
  tokenizeFormula
30
- } from "./chunk-3CN2WD3X.js";
34
+ } from "./chunk-FH6F67DN.js";
31
35
 
32
36
  // src/spreadsheet/formula/dependency-graph.ts
37
+ function extractNameDependencies(node) {
38
+ const names = [];
39
+ const stack = [node];
40
+ while (stack.length > 0) {
41
+ const curr = stack.pop();
42
+ switch (curr.type) {
43
+ case "Name":
44
+ names.push(curr.sheet === void 0 ? { name: curr.name, call: false } : { name: curr.name, sheet: curr.sheet, call: false });
45
+ break;
46
+ case "FunctionCall":
47
+ names.push({ name: curr.name, call: true });
48
+ for (const arg of curr.args) stack.push(arg);
49
+ break;
50
+ case "Invoke":
51
+ stack.push(curr.callee);
52
+ for (const arg of curr.args) stack.push(arg);
53
+ break;
54
+ case "BinaryOp":
55
+ stack.push(curr.left, curr.right);
56
+ break;
57
+ case "UnaryOp":
58
+ stack.push(curr.argument);
59
+ break;
60
+ default:
61
+ break;
62
+ }
63
+ }
64
+ return names;
65
+ }
33
66
  var columnDependencyKey = (col) => `${col.toUpperCase()}:`;
34
67
  function extractCellDependencies(node) {
35
68
  const deps = [];
36
69
  const visit = (curr) => {
70
+ const sheet = "sheet" in curr && curr.sheet !== void 0 ? "sheetEnd" in curr && curr.sheetEnd !== void 0 ? { sheet: curr.sheet, sheetEnd: curr.sheetEnd } : { sheet: curr.sheet } : {};
37
71
  if (curr.type === "CellRef") {
38
- deps.push({ col: curr.col, row: curr.row });
72
+ deps.push({ col: curr.col, row: curr.row, ...sheet });
39
73
  } else if (curr.type === "SpillRef") {
40
- deps.push({ col: curr.col, row: curr.row });
74
+ deps.push({ col: curr.col, row: curr.row, ...sheet });
41
75
  } else if (curr.type === "Range" && curr.endRow >= WHOLE_COLUMN_END_ROW) {
42
76
  const startC = colNameToIndex(curr.startCol);
43
77
  const endC = colNameToIndex(curr.endCol);
44
78
  for (let c = Math.min(startC, endC); c <= Math.max(startC, endC); c++) {
45
- deps.push({ col: indexToColName(c), row: 0 });
79
+ deps.push({ col: indexToColName(c), row: 0, ...sheet });
46
80
  }
47
81
  } else if (curr.type === "Range") {
48
82
  const startC = colNameToIndex(curr.startCol);
@@ -53,7 +87,7 @@ function extractCellDependencies(node) {
53
87
  const maxR = Math.max(curr.startRow, curr.endRow);
54
88
  for (let c = minC; c <= maxC; c++) {
55
89
  for (let r = minR; r <= maxR; r++) {
56
- deps.push({ col: indexToColName(c), row: r });
90
+ deps.push({ col: indexToColName(c), row: r, ...sheet });
57
91
  }
58
92
  }
59
93
  } else if (curr.type === "BinaryOp") {
@@ -63,6 +97,9 @@ function extractCellDependencies(node) {
63
97
  visit(curr.argument);
64
98
  } else if (curr.type === "FunctionCall") {
65
99
  curr.args.forEach(visit);
100
+ } else if (curr.type === "Invoke") {
101
+ visit(curr.callee);
102
+ curr.args.forEach(visit);
66
103
  }
67
104
  };
68
105
  visit(node);
@@ -81,8 +118,29 @@ var DependencyGraph = class {
81
118
  spillDependents = /* @__PURE__ */ new Map();
82
119
  // anchor -> the edges it registered (so they can be replaced).
83
120
  spillEdges = /* @__PURE__ */ new Map();
84
- setCellFormula(cellKey, formulaStr) {
85
- const key = cellKey.toUpperCase();
121
+ /**
122
+ * Sets the cells `cellKey` depends on directly (keys such as `B2`, or a
123
+ * column key from `columnDependencyKey`), replacing its previous
124
+ * dependencies. Used by spreadsheets whose keys carry more than A1 text
125
+ * (e.g. sheet-qualified keys); `setCellFormula` derives them from a formula.
126
+ */
127
+ setCellDependencies(cellKey2, dependencyKeys) {
128
+ const key = cellKey2.toUpperCase();
129
+ for (const dep of this.dependencies.get(key) ?? []) {
130
+ this.dependents.get(dep)?.delete(key);
131
+ }
132
+ this.dependencies.delete(key);
133
+ const deps = /* @__PURE__ */ new Set();
134
+ for (const raw of dependencyKeys) {
135
+ const dep = raw.toUpperCase();
136
+ deps.add(dep);
137
+ if (!this.dependents.has(dep)) this.dependents.set(dep, /* @__PURE__ */ new Set());
138
+ this.dependents.get(dep).add(key);
139
+ }
140
+ if (deps.size > 0) this.dependencies.set(key, deps);
141
+ }
142
+ setCellFormula(cellKey2, formulaStr) {
143
+ const key = cellKey2.toUpperCase();
86
144
  const oldDeps = this.dependencies.get(key) || /* @__PURE__ */ new Set();
87
145
  for (const dep of oldDeps) {
88
146
  this.dependents.get(dep)?.delete(key);
@@ -146,20 +204,20 @@ var DependencyGraph = class {
146
204
  return (this.spillEdges.get(anchor) ?? []).filter(([, to]) => to === anchor).map(([from]) => from);
147
205
  }
148
206
  /** Formula dependencies plus dynamic-array (spill) dependencies of a cell. */
149
- getDependencies(cellKey) {
150
- const key = cellKey.toUpperCase();
207
+ getDependencies(cellKey2) {
208
+ const key = cellKey2.toUpperCase();
151
209
  return unionOf(this.dependencies.get(key), this.spillDependencies.get(key));
152
210
  }
153
211
  /** Cells depending on `cellKey`, through formulas or spills. */
154
- getDependents(cellKey) {
155
- const key = cellKey.toUpperCase();
212
+ getDependents(cellKey2) {
213
+ const key = cellKey2.toUpperCase();
156
214
  return unionOf(this.dependents.get(key), this.spillDependents.get(key));
157
215
  }
158
216
  /**
159
217
  * Detects if there is a cycle containing cellKey.
160
218
  */
161
- hasCycle(cellKey) {
162
- const key = cellKey.toUpperCase();
219
+ hasCycle(cellKey2) {
220
+ const key = cellKey2.toUpperCase();
163
221
  const visited = /* @__PURE__ */ new Set();
164
222
  const recStack = /* @__PURE__ */ new Set();
165
223
  const dfs = (node) => {
@@ -390,8 +448,11 @@ function rewriteReference(text, change) {
390
448
  const newEnd = { ...end, [key]: startValue <= endValue ? hi : lo };
391
449
  return `${formatPart(newStart)}:${formatPart(newEnd)}`;
392
450
  }
393
- function shiftFormulaReferences(formula, change) {
394
- return mapFormulaReferences(formula, (text) => rewriteReference(text, change));
451
+ function shiftFormulaReferences(formula, change, options = {}) {
452
+ return mapFormulaReferences(
453
+ formula,
454
+ (text, info) => !options.appliesTo || options.appliesTo(info.sheet, info.sheetEnd) ? rewriteReference(text, change) : text
455
+ );
395
456
  }
396
457
  function mapFormulaReferences(formula, rewrite) {
397
458
  if (!formula.startsWith("=")) return formula;
@@ -407,16 +468,20 @@ function mapFormulaReferences(formula, rewrite) {
407
468
  const token = tokens[i];
408
469
  if (token.type !== "CELL_REF" && token.type !== "RANGE" && token.type !== "SPILL_REF") continue;
409
470
  if (tokens[i + 1]?.type === "LPAREN") continue;
410
- let end = token.position;
471
+ const refStart = token.refStart ?? token.position;
472
+ let end = refStart;
411
473
  while (end < formula.length && REF_CHAR.test(formula[end])) end++;
412
- const original = formula.slice(token.position, end);
474
+ const original = formula.slice(refStart, end);
475
+ const info = token.sheet === void 0 ? {} : token.sheetEnd === void 0 ? { sheet: token.sheet } : { sheet: token.sheet, sheetEnd: token.sheetEnd };
476
+ const prefix = formula.slice(token.position, refStart);
413
477
  if (token.type === "SPILL_REF") {
414
- const rewritten = rewrite(original);
415
- result += formula.slice(cursor, token.position) + (rewritten === "#REF!" ? rewritten : `${rewritten}#`);
478
+ const rewritten2 = rewrite(original, info);
479
+ result += formula.slice(cursor, token.position) + (rewritten2 === "#REF!" ? rewritten2 : `${prefix}${rewritten2}#`);
416
480
  cursor = end + 1;
417
481
  continue;
418
482
  }
419
- result += formula.slice(cursor, token.position) + rewrite(original);
483
+ const rewritten = rewrite(original, info);
484
+ result += formula.slice(cursor, token.position) + (rewritten === "#REF!" ? rewritten : prefix + rewritten);
420
485
  cursor = end;
421
486
  }
422
487
  return result + formula.slice(cursor);
@@ -437,8 +502,9 @@ function offsetFormulaReferences(formula, rowOffset, colOffset) {
437
502
  });
438
503
  }
439
504
  var MAX_REMAP_RANGE_CELLS = 1e5;
440
- function remapFormulaReferences(formula, moveCell) {
441
- return mapFormulaReferences(formula, (text) => {
505
+ function remapFormulaReferences(formula, moveCell, options = {}) {
506
+ return mapFormulaReferences(formula, (text, info) => {
507
+ if (options.appliesTo && !options.appliesTo(info.sheet, info.sheetEnd)) return text;
442
508
  const parts = text.split(":").map(parsePart);
443
509
  if (parts.some((p) => p === null)) return text;
444
510
  const [start, end] = parts;
@@ -475,6 +541,101 @@ function remapFormulaReferences(formula, moveCell) {
475
541
  return `${formatPart({ ...start, row: minR, col: minC })}:${formatPart({ ...end, row: maxR, col: maxC })}`;
476
542
  });
477
543
  }
544
+ var CELL_LIKE = /^[A-Za-z]{1,3}\d{1,7}$/;
545
+ var isCellLike = (name) => {
546
+ if (!CELL_LIKE.test(name)) return false;
547
+ const letters = name.match(/^[A-Za-z]+/)[0].toUpperCase();
548
+ const col = Array.from(letters).reduce((n, ch) => n * 26 + ch.charCodeAt(0) - 64, 0);
549
+ const row = Number(name.slice(letters.length));
550
+ return col <= 16384 && row >= 1 && row <= 1048576;
551
+ };
552
+ var R1C1_LIKE = /^[Rr]\d*(?:[Cc]\d*)?$|^[Cc]\d*$/;
553
+ function sheetNameNeedsQuotes(name) {
554
+ if (!/^[A-Za-z_\p{L}][\p{L}\p{N}_.]*$/u.test(name)) return true;
555
+ const upper = name.toUpperCase();
556
+ return isCellLike(name) || R1C1_LIKE.test(name) || upper === "TRUE" || upper === "FALSE";
557
+ }
558
+ function quoteSheetName(name) {
559
+ return sheetNameNeedsQuotes(name) ? `'${name.replace(/'/g, "''")}'` : name;
560
+ }
561
+ function formatSheetPrefix(sheet, sheetEnd) {
562
+ if (sheetEnd === void 0) return `${quoteSheetName(sheet)}!`;
563
+ const quoted = sheetNameNeedsQuotes(sheet) || sheetNameNeedsQuotes(sheetEnd);
564
+ return quoted ? `'${`${sheet}:${sheetEnd}`.replace(/'/g, "''")}'!` : `${sheet}:${sheetEnd}!`;
565
+ }
566
+ function tokensOf(formula) {
567
+ if (!formula.startsWith("=")) return null;
568
+ try {
569
+ return tokenizeFormula(formula);
570
+ } catch {
571
+ return null;
572
+ }
573
+ }
574
+ function rewriteTokens(formula, edit) {
575
+ const tokens = tokensOf(formula);
576
+ if (!tokens) return formula;
577
+ let result = "";
578
+ let cursor = 0;
579
+ for (const token of tokens) {
580
+ const change = edit(token);
581
+ if (!change) continue;
582
+ result += formula.slice(cursor, change.from) + change.text;
583
+ cursor = change.to;
584
+ }
585
+ return cursor === 0 ? formula : result + formula.slice(cursor);
586
+ }
587
+ var sameName = (a, b) => a.toUpperCase() === b.toUpperCase();
588
+ function renameSheetInFormula(formula, oldName, newName) {
589
+ return rewriteTokens(formula, (token) => {
590
+ if (token.sheet === void 0 || token.refStart === void 0) return null;
591
+ const first2 = sameName(token.sheet, oldName);
592
+ const last = token.sheetEnd !== void 0 && sameName(token.sheetEnd, oldName);
593
+ if (!first2 && !last) return null;
594
+ const sheet = first2 ? newName : token.sheet;
595
+ const sheetEnd = token.sheetEnd === void 0 ? void 0 : last ? newName : token.sheetEnd;
596
+ return { from: token.position, to: token.refStart, text: formatSheetPrefix(sheet, sheetEnd) };
597
+ });
598
+ }
599
+ function deleteSheetInFormula(formula, name, order = []) {
600
+ const position = (sheet) => order.findIndex((s) => sameName(s, sheet));
601
+ return rewriteTokens(formula, (token) => {
602
+ if (token.sheet === void 0 || token.refStart === void 0) return null;
603
+ const end = token.end ?? token.refStart;
604
+ if (token.sheetEnd === void 0) {
605
+ return sameName(token.sheet, name) ? { from: token.position, to: end, text: "#REF!" } : null;
606
+ }
607
+ const first2 = sameName(token.sheet, name);
608
+ const last = sameName(token.sheetEnd, name);
609
+ if (!first2 && !last) return null;
610
+ const a = position(token.sheet);
611
+ const b = position(token.sheetEnd);
612
+ if (a < 0 || b < 0 || a === b) return { from: token.position, to: end, text: "#REF!" };
613
+ const inward = (from, to) => order[from < to ? from + 1 : from - 1];
614
+ const newFirst = first2 ? inward(a, b) : token.sheet;
615
+ const newLast = last ? inward(b, a) : token.sheetEnd;
616
+ const prefix = sameName(newFirst, newLast) ? formatSheetPrefix(newFirst) : formatSheetPrefix(newFirst, newLast);
617
+ return { from: token.position, to: token.refStart, text: prefix };
618
+ });
619
+ }
620
+ function renameNameInFormula(formula, oldName, newName, options = {}) {
621
+ return rewriteTokens(formula, (token) => {
622
+ if (token.type !== "IDENTIFIER" || !sameName(token.value, oldName)) return null;
623
+ if (token.sheet !== void 0 ? options.appliesTo && !options.appliesTo(token.sheet) : options.qualifiedOnly) return null;
624
+ const from = token.refStart ?? token.position;
625
+ return { from, to: token.end ?? from + oldName.length, text: newName };
626
+ });
627
+ }
628
+ function formulaIdentifiers(formula) {
629
+ const tokens = tokensOf(formula);
630
+ if (!tokens) return [];
631
+ const out = [];
632
+ tokens.forEach((token, i) => {
633
+ if (token.type !== "IDENTIFIER") return;
634
+ const call = tokens[i + 1]?.type === "LPAREN";
635
+ out.push(token.sheet === void 0 ? { name: token.value, call } : { name: token.value, sheet: token.sheet, call });
636
+ });
637
+ return out;
638
+ }
478
639
 
479
640
  // src/spreadsheet/formula/caret.ts
480
641
  var EMPTY = { prefix: null, call: null };
@@ -544,9 +705,20 @@ function getFormulaHints(input, caret = input.length, options = {}) {
544
705
  let suggestions = [];
545
706
  if (context.prefix) {
546
707
  const query = context.prefix.text.toUpperCase();
547
- suggestions = searchFormulaFunctions(query, { limit: void 0 }).filter((def) => def.name.startsWith(query)).slice(0, options.limit ?? 8).map(toSuggestion);
708
+ const names = (options.names ?? []).filter((n) => n.name.toUpperCase().startsWith(query)).map((n) => ({
709
+ name: n.name,
710
+ signature: n.isLambda ? `${n.name}(\u2026)` : n.name,
711
+ description: n.description ?? "",
712
+ category: "lookup",
713
+ kind: "name",
714
+ ...n.isLambda ? { isLambda: true } : {}
715
+ }));
716
+ suggestions = [
717
+ ...names,
718
+ ...searchFormulaFunctions(query, { limit: void 0 }).filter((def) => def.name.startsWith(query) && !names.some((n) => n.name.toUpperCase() === def.name)).map(toSuggestion)
719
+ ].slice(0, options.limit ?? 8);
548
720
  const followedByParen = input.slice(context.prefix.end).trimStart().startsWith("(");
549
- if (followedByParen || suggestions.length === 1 && suggestions[0].name === query) suggestions = [];
721
+ if (followedByParen || suggestions.length === 1 && suggestions[0].name.toUpperCase() === query) suggestions = [];
550
722
  }
551
723
  let activeCall = null;
552
724
  if (context.call) {
@@ -565,12 +737,20 @@ function getFormulaHints(input, caret = input.length, options = {}) {
565
737
  }
566
738
  return { context, suggestions, activeCall };
567
739
  }
568
- function applyFormulaSuggestion(input, caret, name) {
740
+ function applyFormulaSuggestion(input, caret, suggestion, options = {}) {
569
741
  const context = getFormulaCaretContext(input, caret);
570
742
  const start = context.prefix?.start ?? caret;
571
743
  const end = context.prefix?.end ?? caret;
744
+ const definedName = typeof suggestion !== "string" && suggestion.kind === "name";
745
+ const name = typeof suggestion === "string" ? suggestion : suggestion.name;
746
+ const call = options.call ?? (definedName ? !!suggestion.isLambda : true);
747
+ if (!call) {
748
+ const value2 = input.slice(0, start) + name + input.slice(end);
749
+ return { value: value2, caret: start + name.length };
750
+ }
572
751
  const hasParen = input[end] === "(";
573
- const insert = hasParen ? name.toUpperCase() : `${name.toUpperCase()}(`;
752
+ const spelled = definedName ? name : name.toUpperCase();
753
+ const insert = hasParen ? spelled : `${spelled}(`;
574
754
  const value = input.slice(0, start) + insert + input.slice(end);
575
755
  return { value, caret: start + insert.length + (hasParen ? 1 : 0) };
576
756
  }
@@ -707,6 +887,61 @@ var DataSourceBinder = class {
707
887
  }
708
888
  };
709
889
 
890
+ // src/spreadsheet/workbook.ts
891
+ var MAX_SHEET_NAME_LENGTH = 31;
892
+ var MAX_DEFINED_NAME_LENGTH = 255;
893
+ var sameName2 = (a, b) => a.toUpperCase() === b.toUpperCase();
894
+ function validateSheetName(name, existingNames = []) {
895
+ const length = Array.from(name).length;
896
+ if (length === 0 || name.trim() === "") return "A sheet name can't be blank.";
897
+ if (length > MAX_SHEET_NAME_LENGTH) return `A sheet name can't be longer than ${MAX_SHEET_NAME_LENGTH} characters.`;
898
+ if (/[:\\/?*[\]]/.test(name)) return "A sheet name can't contain any of these characters: : \\ / ? * [ ]";
899
+ if (name.startsWith("'") || name.endsWith("'")) return "A sheet name can't begin or end with an apostrophe.";
900
+ if (sameName2(name, "History")) return '"History" is a reserved name.';
901
+ if (existingNames.some((existing) => sameName2(existing, name))) return "That name is already taken. Try a different one.";
902
+ return null;
903
+ }
904
+ function nextSheetName(existingNames, base = "Sheet") {
905
+ for (let n = 1; ; n++) {
906
+ const candidate = `${base} ${n}`;
907
+ if (!existingNames.some((existing) => sameName2(existing, candidate))) return candidate;
908
+ }
909
+ }
910
+ function copySheetName(name, existingNames) {
911
+ for (let k = 2; ; k++) {
912
+ const suffix = ` (${k})`;
913
+ const keep = Math.max(0, MAX_SHEET_NAME_LENGTH - suffix.length);
914
+ const candidate = `${Array.from(name).slice(0, keep).join("").trimEnd()}${suffix}`;
915
+ if (!existingNames.some((existing) => sameName2(existing, candidate))) return candidate;
916
+ }
917
+ }
918
+ function isR1C1Like(name) {
919
+ let i = 0;
920
+ if (/[Rr]/.test(name[i] ?? "")) {
921
+ i++;
922
+ while (i < name.length && /\d/.test(name[i])) i++;
923
+ }
924
+ if (/[Cc]/.test(name[i] ?? "")) {
925
+ i++;
926
+ while (i < name.length && /\d/.test(name[i])) i++;
927
+ }
928
+ return name.length > 0 && i === name.length;
929
+ }
930
+ function validateDefinedName(name) {
931
+ if (name === "") return "A name can't be blank.";
932
+ if (Array.from(name).length > MAX_DEFINED_NAME_LENGTH) return `A name can't be longer than ${MAX_DEFINED_NAME_LENGTH} characters.`;
933
+ if (!/^[\p{L}_\\][\p{L}\p{N}_.\\?]*$/u.test(name)) {
934
+ return /^[\p{L}_\\]/u.test(name) ? "A name can only contain letters, numbers, periods, underscores, backslashes and question marks." : "A name must start with a letter, an underscore or a backslash.";
935
+ }
936
+ if (/^\$?[A-Za-z]+\$?\d+$/.test(name)) return "A name can't look like a cell reference (such as A1).";
937
+ if (isR1C1Like(name)) return "A name can't look like an R1C1 reference (such as R1C1, R or C).";
938
+ if (sameName2(name, "TRUE") || sameName2(name, "FALSE")) return "A name can't be TRUE or FALSE.";
939
+ return null;
940
+ }
941
+ function isLambdaDefinition(definition) {
942
+ return /^=?\s*LAMBDA\s*\(/i.test(definition.refersTo);
943
+ }
944
+
710
945
  // src/spreadsheet/table.ts
711
946
  function normalizeRange(range) {
712
947
  return {
@@ -1118,6 +1353,988 @@ function rebaseClipboardRows(rows, origin, target) {
1118
1353
  );
1119
1354
  }
1120
1355
 
1356
+ // src/spreadsheet/rule-ranges.ts
1357
+ function adjustRangeForStructuralChange(range, change) {
1358
+ const r = normalizeRange(range);
1359
+ const startKey = change.axis === "row" ? "startRow" : "startCol";
1360
+ const endKey = change.axis === "row" ? "endRow" : "endCol";
1361
+ if (change.type === "insert") {
1362
+ if (r[startKey] >= change.index) r[startKey]++;
1363
+ if (r[endKey] >= change.index) r[endKey]++;
1364
+ return r;
1365
+ }
1366
+ if (r[startKey] === change.index && r[endKey] === change.index) return null;
1367
+ if (r[startKey] > change.index) r[startKey]--;
1368
+ if (r[endKey] >= change.index) r[endKey]--;
1369
+ return r;
1370
+ }
1371
+ function adjustRangesForStructuralChange(ranges, change) {
1372
+ const out = [];
1373
+ for (const range of ranges) {
1374
+ const next = adjustRangeForStructuralChange(range, change);
1375
+ if (next) out.push(next);
1376
+ }
1377
+ return out;
1378
+ }
1379
+ function subtractRange(range, cut) {
1380
+ const r = normalizeRange(range);
1381
+ const c = normalizeRange(cut);
1382
+ if (!rangesIntersect(r, c)) return [r];
1383
+ const out = [];
1384
+ const top = Math.max(r.startRow, c.startRow);
1385
+ const bottom = Math.min(r.endRow, c.endRow);
1386
+ if (r.startRow < c.startRow) out.push({ startRow: r.startRow, startCol: r.startCol, endRow: c.startRow - 1, endCol: r.endCol });
1387
+ if (r.endRow > c.endRow) out.push({ startRow: c.endRow + 1, startCol: r.startCol, endRow: r.endRow, endCol: r.endCol });
1388
+ if (r.startCol < c.startCol) out.push({ startRow: top, startCol: r.startCol, endRow: bottom, endCol: c.startCol - 1 });
1389
+ if (r.endCol > c.endCol) out.push({ startRow: top, startCol: c.endCol + 1, endRow: bottom, endCol: r.endCol });
1390
+ return out;
1391
+ }
1392
+ function subtractRangeFromRanges(ranges, cut) {
1393
+ return ranges.flatMap((range) => subtractRange(range, cut));
1394
+ }
1395
+ function rangesContain(ranges, row, col) {
1396
+ for (const r of ranges) {
1397
+ const minR = Math.min(r.startRow, r.endRow);
1398
+ const maxR = Math.max(r.startRow, r.endRow);
1399
+ const minC = Math.min(r.startCol, r.endCol);
1400
+ const maxC = Math.max(r.startCol, r.endCol);
1401
+ if (row >= minR && row <= maxR && col >= minC && col <= maxC) return true;
1402
+ }
1403
+ return false;
1404
+ }
1405
+ function ruleAnchor(ranges) {
1406
+ const first2 = ranges[0];
1407
+ if (!first2) return { row: 0, col: 0 };
1408
+ return { row: Math.min(first2.startRow, first2.endRow), col: Math.min(first2.startCol, first2.endCol) };
1409
+ }
1410
+ var withEquals = (formula) => formula.trimStart().startsWith("=") ? formula.trimStart() : `=${formula}`;
1411
+ function ruleFormulaAt(formula, anchor, row, col) {
1412
+ return offsetFormulaReferences(withEquals(formula), row - anchor.row, col - anchor.col);
1413
+ }
1414
+ function shiftRuleFormula(formula, change) {
1415
+ const hadEquals = formula.trimStart().startsWith("=");
1416
+ const shifted = shiftFormulaReferences(withEquals(formula), change);
1417
+ return hadEquals ? shifted : shifted.slice(1);
1418
+ }
1419
+ function forEachRangeCell(ranges, rowCount, colCount, fn) {
1420
+ const seen = ranges.length > 1 ? /* @__PURE__ */ new Set() : null;
1421
+ for (const range of ranges) {
1422
+ const r = normalizeRange(range);
1423
+ const endRow = Math.min(r.endRow, rowCount - 1);
1424
+ const endCol = Math.min(r.endCol, colCount - 1);
1425
+ for (let row = Math.max(0, r.startRow); row <= endRow; row++) {
1426
+ for (let col = Math.max(0, r.startCol); col <= endCol; col++) {
1427
+ if (seen) {
1428
+ const key = row * (colCount + 1) + col;
1429
+ if (seen.has(key)) continue;
1430
+ seen.add(key);
1431
+ }
1432
+ fn(row, col);
1433
+ }
1434
+ }
1435
+ }
1436
+ }
1437
+ function formatRuleRanges(ranges) {
1438
+ return ranges.map((range) => {
1439
+ const r = normalizeRange(range);
1440
+ const start = `${indexToColName(r.startCol)}${r.startRow + 1}`;
1441
+ const end = `${indexToColName(r.endCol)}${r.endRow + 1}`;
1442
+ return start === end ? start : `${start}:${end}`;
1443
+ }).join(", ");
1444
+ }
1445
+ var A1_RANGE = /^\$?([A-Za-z]{1,3})\$?(\d{1,7})(?::\$?([A-Za-z]{1,3})\$?(\d{1,7}))?$/;
1446
+ function parseRuleRanges(text) {
1447
+ const parts = text.split(/[,;\s]+/).filter((p) => p !== "");
1448
+ if (parts.length === 0) return null;
1449
+ const out = [];
1450
+ for (const part of parts) {
1451
+ const m = A1_RANGE.exec(part);
1452
+ if (!m) return null;
1453
+ const startRow = Number(m[2]) - 1;
1454
+ const startCol = colNameToIndex(m[1]);
1455
+ const endRow = m[4] ? Number(m[4]) - 1 : startRow;
1456
+ const endCol = m[3] ? colNameToIndex(m[3]) : startCol;
1457
+ if (startRow < 0 || endRow < 0 || startCol < 0 || endCol < 0) return null;
1458
+ out.push(normalizeRange({ startRow, startCol, endRow, endCol }));
1459
+ }
1460
+ return out;
1461
+ }
1462
+
1463
+ // src/spreadsheet/conditional-format/icons.ts
1464
+ var RED = "#d9534f";
1465
+ var YELLOW = "#e0a800";
1466
+ var GREEN = "#2e9d4f";
1467
+ var GRAY = "#7a7f87";
1468
+ var BLACK = "#3b3f45";
1469
+ var BLUE = "#3b73b9";
1470
+ var g = (glyph, color, label) => ({ glyph, color, label });
1471
+ var ICON_SETS = {
1472
+ "3Arrows": [g("\u2193", RED, "Down arrow"), g("\u2192", YELLOW, "Side arrow"), g("\u2191", GREEN, "Up arrow")],
1473
+ "3ArrowsGray": [g("\u2193", GRAY, "Down arrow"), g("\u2192", GRAY, "Side arrow"), g("\u2191", GRAY, "Up arrow")],
1474
+ "3TrafficLights1": [g("\u25CF", RED, "Red light"), g("\u25CF", YELLOW, "Yellow light"), g("\u25CF", GREEN, "Green light")],
1475
+ "3Flags": [g("\u2691", RED, "Red flag"), g("\u2691", YELLOW, "Yellow flag"), g("\u2691", GREEN, "Green flag")],
1476
+ "3Symbols": [g("\u2716", RED, "Cross"), g("!", YELLOW, "Exclamation"), g("\u2714", GREEN, "Check")],
1477
+ "3Stars": [g("\u2606", YELLOW, "Empty star"), g("\u272C", YELLOW, "Half star"), g("\u2605", YELLOW, "Full star")],
1478
+ "4Arrows": [g("\u2193", RED, "Down arrow"), g("\u2198", YELLOW, "Down-right arrow"), g("\u2197", YELLOW, "Up-right arrow"), g("\u2191", GREEN, "Up arrow")],
1479
+ "4Rating": [g("\u2582", BLUE, "Rating 1 of 4"), g("\u2584", BLUE, "Rating 2 of 4"), g("\u2586", BLUE, "Rating 3 of 4"), g("\u2588", BLUE, "Rating 4 of 4")],
1480
+ "4TrafficLights": [g("\u25CF", BLACK, "Black light"), g("\u25CF", RED, "Red light"), g("\u25CF", YELLOW, "Yellow light"), g("\u25CF", GREEN, "Green light")],
1481
+ "5Arrows": [
1482
+ g("\u2193", RED, "Down arrow"),
1483
+ g("\u2198", YELLOW, "Down-right arrow"),
1484
+ g("\u2192", YELLOW, "Side arrow"),
1485
+ g("\u2197", GREEN, "Up-right arrow"),
1486
+ g("\u2191", GREEN, "Up arrow")
1487
+ ],
1488
+ "5Rating": [
1489
+ g("\u2581", BLUE, "Rating 1 of 5"),
1490
+ g("\u2582", BLUE, "Rating 2 of 5"),
1491
+ g("\u2584", BLUE, "Rating 3 of 5"),
1492
+ g("\u2586", BLUE, "Rating 4 of 5"),
1493
+ g("\u2588", BLUE, "Rating 5 of 5")
1494
+ ],
1495
+ "5Quarters": [g("\u25CB", BLACK, "Empty"), g("\u25D4", BLACK, "Quarter"), g("\u25D1", BLACK, "Half"), g("\u25D5", BLACK, "Three quarters"), g("\u25CF", BLACK, "Full")]
1496
+ };
1497
+ var CONDITIONAL_FORMAT_ICON_SETS = Object.keys(ICON_SETS);
1498
+ function conditionalFormatIconCount(set) {
1499
+ const n = Number(set.charAt(0));
1500
+ return Number.isFinite(n) ? Math.min(5, Math.max(3, n)) : 3;
1501
+ }
1502
+ function getConditionalFormatIcon(set, index) {
1503
+ const icons = ICON_SETS[set] ?? ICON_SETS["3TrafficLights1"];
1504
+ return icons[Math.max(0, Math.min(icons.length - 1, index))];
1505
+ }
1506
+
1507
+ // src/spreadsheet/conditional-format/evaluate.ts
1508
+ var DEFAULT_BAR_COLOR = "#638ec6";
1509
+ var isBlank2 = (v) => v === "" || v === null || v === void 0;
1510
+ var ISO_DATE = /^(\d{4})-(\d{1,2})-(\d{1,2})(?: (\d{1,2}):(\d{2})(?::(\d{2}))?)?$/;
1511
+ function conditionalFormatNumber(value) {
1512
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
1513
+ if (typeof value !== "string" || value.length < 8 || value.length > 19) return null;
1514
+ const m = ISO_DATE.exec(value);
1515
+ if (!m) return null;
1516
+ const serial = serialFromYmd(Number(m[1]), Number(m[2]), Number(m[3]));
1517
+ if (serial === null) return null;
1518
+ const time = m[4] ? (Number(m[4]) * 3600 + Number(m[5]) * 60 + Number(m[6] ?? 0)) / 86400 : 0;
1519
+ return serial + time;
1520
+ }
1521
+ function valueText(value) {
1522
+ if (isBlank2(value)) return "";
1523
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
1524
+ if (typeof value === "number") return numberToText(value);
1525
+ return String(value);
1526
+ }
1527
+ function firstValue(value) {
1528
+ return Array.isArray(value) ? value.length ? value[0] : "" : value;
1529
+ }
1530
+ function percentile(sorted, p) {
1531
+ if (sorted.length === 0) return 0;
1532
+ const rank = Math.min(Math.max(p, 0), 100) / 100 * (sorted.length - 1);
1533
+ const lo = Math.floor(rank);
1534
+ const hi = Math.ceil(rank);
1535
+ const a = sorted[lo] ?? 0;
1536
+ const b = sorted[hi] ?? a;
1537
+ return a + (b - a) * (rank - lo);
1538
+ }
1539
+ function parseHexColor(color) {
1540
+ if (!color) return null;
1541
+ let hex = color.trim().replace(/^#/, "");
1542
+ if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
1543
+ if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
1544
+ return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
1545
+ }
1546
+ var toHex = (rgb) => `#${rgb.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
1547
+ function blendConditionalFormatColors(a, b, t) {
1548
+ const ca = parseHexColor(a) ?? [255, 255, 255];
1549
+ const cb = parseHexColor(b) ?? [255, 255, 255];
1550
+ const k = Math.min(Math.max(Number.isFinite(t) ? t : 0, 0), 1);
1551
+ return toHex([0, 1, 2].map((i) => Math.round(ca[i] + (cb[i] - ca[i]) * k)));
1552
+ }
1553
+ function defaultIconThresholds(set) {
1554
+ const k = conditionalFormatIconCount(set);
1555
+ return Array.from({ length: k }, (_, i) => ({ type: "percent", value: Math.round(i * 100 / k) }));
1556
+ }
1557
+ var usesStats = (rule) => rule.type === "colorScale" || rule.type === "dataBar" || rule.type === "iconSet" || rule.type === "top10" || rule.type === "aboveAverage" || rule.type === "duplicate" || rule.type === "unique";
1558
+ var isFormulaText = (text) => !!text && text.trimStart().startsWith("=");
1559
+ function usesFormulas(rule) {
1560
+ switch (rule.type) {
1561
+ case "expression":
1562
+ return true;
1563
+ case "cellIs":
1564
+ return isFormulaText(rule.value) || isFormulaText(rule.value2);
1565
+ case "colorScale":
1566
+ return rule.stops.some((s) => s.value.type === "formula");
1567
+ case "dataBar":
1568
+ return rule.min?.type === "formula" || rule.max?.type === "formula";
1569
+ case "iconSet":
1570
+ return !!rule.thresholds?.some((t) => t.type === "formula");
1571
+ default:
1572
+ return false;
1573
+ }
1574
+ }
1575
+ function layerStyle(out, style) {
1576
+ if (out.fill === void 0 && style.fill) out.fill = style.fill;
1577
+ if (out.color === void 0 && style.color) out.color = style.color;
1578
+ if (style.bold) out.bold = true;
1579
+ if (style.italic) out.italic = true;
1580
+ if (style.underline) out.underline = true;
1581
+ if (style.strikethrough) out.strikethrough = true;
1582
+ }
1583
+ var COL_SPAN = 1048576;
1584
+ var cellKey = (row, col) => row * COL_SPAN + col;
1585
+ var Overlay = class {
1586
+ constructor(cells) {
1587
+ this.cells = cells;
1588
+ }
1589
+ cells;
1590
+ get(row, col) {
1591
+ return this.cells.size === 0 ? void 0 : this.cells.get(cellKey(row, col));
1592
+ }
1593
+ get size() {
1594
+ return this.cells.size;
1595
+ }
1596
+ *entries() {
1597
+ for (const [key, format] of this.cells) {
1598
+ const row = Math.floor(key / COL_SPAN);
1599
+ const col = key - row * COL_SPAN;
1600
+ yield [{ row, col }, format];
1601
+ }
1602
+ }
1603
+ };
1604
+ var EMPTY_CONDITIONAL_FORMAT_OVERLAY = new Overlay(/* @__PURE__ */ new Map());
1605
+ var ConditionalFormatEngine = class {
1606
+ constructor(sheet) {
1607
+ this.sheet = sheet;
1608
+ }
1609
+ sheet;
1610
+ cells = /* @__PURE__ */ new Map();
1611
+ stats = /* @__PURE__ */ new Map();
1612
+ rulesKey = null;
1613
+ current = EMPTY_CONDITIONAL_FORMAT_OVERLAY;
1614
+ /** Replaces the sheet accessor (e.g. after rows or columns were added). */
1615
+ setSheet(sheet) {
1616
+ this.sheet = sheet;
1617
+ }
1618
+ /** The last evaluated overlay. */
1619
+ get overlay() {
1620
+ return this.current;
1621
+ }
1622
+ /** Evaluates every rule over every cell it applies to. */
1623
+ evaluate(rules) {
1624
+ this.rulesKey = JSON.stringify(rules);
1625
+ this.stats.clear();
1626
+ this.cells = /* @__PURE__ */ new Map();
1627
+ const { rowCount, colCount } = this.sheet;
1628
+ const done = /* @__PURE__ */ new Set();
1629
+ for (const rule of rules) {
1630
+ forEachRangeCell(rule.ranges, rowCount, colCount, (row, col) => {
1631
+ const key = cellKey(row, col);
1632
+ if (done.has(key)) return;
1633
+ done.add(key);
1634
+ const look = this.computeLook(rules, row, col);
1635
+ if (look) this.cells.set(key, look);
1636
+ });
1637
+ }
1638
+ this.current = new Overlay(this.cells);
1639
+ return this.current;
1640
+ }
1641
+ /**
1642
+ * Re-evaluates after the values of `changed` cells changed (the edited
1643
+ * cells and the formulas that depend on them). Rules that look at the whole
1644
+ * range (scales, bars, icons, top/bottom, average, duplicates) are
1645
+ * re-evaluated over their range when a changed cell is inside it; formula
1646
+ * rules always are, since their formulas may read any cell; other rules
1647
+ * only at the changed cells. Falls back to `evaluate` when the rules
1648
+ * changed.
1649
+ */
1650
+ update(rules, changed) {
1651
+ const key = JSON.stringify(rules);
1652
+ if (key !== this.rulesKey) return this.evaluate(rules);
1653
+ if (rules.length === 0) return this.current;
1654
+ const changedCells = Array.from(changed);
1655
+ const { rowCount, colCount } = this.sheet;
1656
+ const affected = /* @__PURE__ */ new Map();
1657
+ const addCell = (row, col) => affected.set(cellKey(row, col), { row, col });
1658
+ for (const rule of rules) {
1659
+ const wholeRange = usesFormulas(rule);
1660
+ const touched = changedCells.some((c) => rangesContain(rule.ranges, c.row, c.col));
1661
+ if (usesStats(rule) && touched) this.stats.delete(rule.id);
1662
+ if (wholeRange || usesStats(rule) && touched) {
1663
+ forEachRangeCell(rule.ranges, rowCount, colCount, addCell);
1664
+ } else if (touched) {
1665
+ for (const c of changedCells) {
1666
+ if (c.row < rowCount && c.col < colCount && rangesContain(rule.ranges, c.row, c.col)) addCell(c.row, c.col);
1667
+ }
1668
+ }
1669
+ }
1670
+ if (affected.size === 0) return this.current;
1671
+ const next = new Map(this.cells);
1672
+ for (const [k, { row, col }] of affected) {
1673
+ const look = this.computeLook(rules, row, col);
1674
+ if (look) next.set(k, look);
1675
+ else next.delete(k);
1676
+ }
1677
+ this.cells = next;
1678
+ this.current = new Overlay(next);
1679
+ return this.current;
1680
+ }
1681
+ statsFor(rule) {
1682
+ let st = this.stats.get(rule.id);
1683
+ if (st) return st;
1684
+ const sorted = [];
1685
+ const counts = /* @__PURE__ */ new Map();
1686
+ forEachRangeCell(rule.ranges, this.sheet.rowCount, this.sheet.colCount, (row, col) => {
1687
+ const v = this.sheet.getValue(row, col);
1688
+ const n2 = conditionalFormatNumber(v);
1689
+ if (n2 !== null) sorted.push(n2);
1690
+ if (!isBlank2(v)) {
1691
+ const text = valueText(v).toLowerCase();
1692
+ counts.set(text, (counts.get(text) ?? 0) + 1);
1693
+ }
1694
+ });
1695
+ sorted.sort((a, b) => a - b);
1696
+ const n = Math.max(sorted.length, 1);
1697
+ let sum = 0;
1698
+ for (const x of sorted) sum += x;
1699
+ const avg = sum / n;
1700
+ let sq = 0;
1701
+ for (const x of sorted) sq += (x - avg) * (x - avg);
1702
+ st = { sorted, avg, std: Math.sqrt(sq / n), counts };
1703
+ this.stats.set(rule.id, st);
1704
+ return st;
1705
+ }
1706
+ evalFormula(formula, rule, row, col) {
1707
+ if (!this.sheet.evaluateFormula) return "#NAME?";
1708
+ try {
1709
+ return firstValue(this.sheet.evaluateFormula(ruleFormulaAt(formula, ruleAnchor(rule.ranges), row, col), row, col));
1710
+ } catch {
1711
+ return "#ERROR!";
1712
+ }
1713
+ }
1714
+ threshold(rule, st, k, row, col) {
1715
+ const min = st.sorted[0] ?? 0;
1716
+ const max = st.sorted[st.sorted.length - 1] ?? 0;
1717
+ switch (k.type) {
1718
+ case "min":
1719
+ return min;
1720
+ case "max":
1721
+ return max;
1722
+ case "number":
1723
+ return k.value;
1724
+ case "percent":
1725
+ return min + (max - min) * k.value / 100;
1726
+ case "percentile":
1727
+ return percentile(st.sorted, k.value);
1728
+ case "formula": {
1729
+ const n = conditionalFormatNumber(this.evalFormula(k.formula, rule, row, col));
1730
+ return n ?? 0;
1731
+ }
1732
+ }
1733
+ }
1734
+ /** The operand of a `cellIs` rule: a formula result, or typed input. */
1735
+ operand(text, rule, row, col) {
1736
+ if (text === void 0) return "";
1737
+ if (isFormulaText(text)) return this.evalFormula(text, rule, row, col);
1738
+ return parseCellInput(text).value;
1739
+ }
1740
+ /** Combines every rule that applies to (row, col). */
1741
+ computeLook(rules, row, col) {
1742
+ const out = {};
1743
+ let any = false;
1744
+ let v;
1745
+ let loaded = false;
1746
+ const value = () => {
1747
+ if (!loaded) {
1748
+ v = this.sheet.getValue(row, col);
1749
+ loaded = true;
1750
+ }
1751
+ return v;
1752
+ };
1753
+ for (const rule of rules) {
1754
+ if (!rangesContain(rule.ranges, row, col)) continue;
1755
+ const matched = this.matchRule(rule, value(), row, col, out);
1756
+ if (matched === "look") any = true;
1757
+ if (matched && matched !== "look") {
1758
+ any = true;
1759
+ layerStyle(out, matched);
1760
+ if (rule.stopIfTrue) break;
1761
+ }
1762
+ }
1763
+ return any ? out : void 0;
1764
+ }
1765
+ /**
1766
+ * Checks one rule: returns its style when it matches, 'look' when it set a
1767
+ * scale fill, bar or icon on `out`, or null.
1768
+ */
1769
+ matchRule(rule, v, row, col, out) {
1770
+ const error = isFormulaError(v);
1771
+ switch (rule.type) {
1772
+ case "cellIs": {
1773
+ if (error) return null;
1774
+ const a = this.operand(rule.value, rule, row, col);
1775
+ let ok;
1776
+ if (rule.operator === "between" || rule.operator === "notBetween") {
1777
+ const b = this.operand(rule.value2, rule, row, col);
1778
+ const [lo, hi] = compareValues(a, b) > 0 ? [b, a] : [a, b];
1779
+ const inside = compareValues(v, lo) >= 0 && compareValues(v, hi) <= 0;
1780
+ ok = rule.operator === "between" ? inside : !inside;
1781
+ } else {
1782
+ if (isFormulaError(a)) return null;
1783
+ const cmp = compareValues(v, a);
1784
+ ok = rule.operator === "equal" ? cmp === 0 : rule.operator === "notEqual" ? cmp !== 0 : rule.operator === "greaterThan" ? cmp > 0 : rule.operator === "lessThan" ? cmp < 0 : rule.operator === "greaterThanOrEqual" ? cmp >= 0 : cmp <= 0;
1785
+ }
1786
+ return ok ? rule.style : null;
1787
+ }
1788
+ case "expression": {
1789
+ const result = toBoolean(this.evalFormula(rule.formula, rule, row, col));
1790
+ return result === true ? rule.style : null;
1791
+ }
1792
+ case "containsText":
1793
+ case "notContainsText":
1794
+ case "beginsWith":
1795
+ case "endsWith": {
1796
+ const text = valueText(v).toLowerCase();
1797
+ const t = rule.text.toLowerCase();
1798
+ const ok = rule.type === "containsText" ? text.includes(t) : rule.type === "notContainsText" ? !text.includes(t) : rule.type === "beginsWith" ? text.startsWith(t) : text.endsWith(t);
1799
+ return ok ? rule.style : null;
1800
+ }
1801
+ case "blanks":
1802
+ return isBlank2(v) || valueText(v).trim() === "" ? rule.style : null;
1803
+ case "noBlanks":
1804
+ return !isBlank2(v) && valueText(v).trim() !== "" ? rule.style : null;
1805
+ case "errors":
1806
+ return error ? rule.style : null;
1807
+ case "noErrors":
1808
+ return !error ? rule.style : null;
1809
+ case "duplicate":
1810
+ case "unique": {
1811
+ if (isBlank2(v)) return null;
1812
+ const count = this.statsFor(rule).counts.get(valueText(v).toLowerCase()) ?? 0;
1813
+ return (rule.type === "duplicate" ? count > 1 : count === 1) ? rule.style : null;
1814
+ }
1815
+ case "top10": {
1816
+ const n = conditionalFormatNumber(v);
1817
+ const st = this.statsFor(rule);
1818
+ if (n === null || st.sorted.length === 0) return null;
1819
+ const rank = Math.max(1, Math.floor(rule.rank || 1));
1820
+ let k = rule.percent ? Math.max(1, Math.floor(st.sorted.length * rank / 100)) : rank;
1821
+ k = Math.min(Math.max(k, 1), st.sorted.length);
1822
+ const ok = rule.bottom ? n <= st.sorted[k - 1] : n >= st.sorted[st.sorted.length - k];
1823
+ return ok ? rule.style : null;
1824
+ }
1825
+ case "aboveAverage": {
1826
+ const n = conditionalFormatNumber(v);
1827
+ if (n === null) return null;
1828
+ const st = this.statsFor(rule);
1829
+ const thr = st.avg + (rule.below ? -1 : 1) * st.std * (rule.stdDev ?? 0);
1830
+ const ok = rule.below ? n < thr || !!rule.equal && n === thr : n > thr || !!rule.equal && n === thr;
1831
+ return ok ? rule.style : null;
1832
+ }
1833
+ case "timePeriod": {
1834
+ const n = conditionalFormatNumber(v);
1835
+ if (n === null) return null;
1836
+ return matchesTimePeriod(rule.period, Math.floor(n), this.sheet.today?.() ?? /* @__PURE__ */ new Date()) ? rule.style : null;
1837
+ }
1838
+ case "colorScale": {
1839
+ const n = conditionalFormatNumber(v);
1840
+ if (n === null || out.fill !== void 0) return null;
1841
+ const st = this.statsFor(rule);
1842
+ const pts = rule.stops.map((s) => ({ at: this.threshold(rule, st, s.value, row, col), color: s.color }));
1843
+ let fill;
1844
+ if (pts.length === 2) {
1845
+ const [a, b] = pts;
1846
+ fill = blendConditionalFormatColors(a.color, b.color, b.at > a.at ? (n - a.at) / (b.at - a.at) : 0);
1847
+ } else if (pts.length >= 3) {
1848
+ const [a, m, b] = pts;
1849
+ fill = n <= m.at ? blendConditionalFormatColors(a.color, m.color, m.at > a.at ? (n - a.at) / (m.at - a.at) : 0) : blendConditionalFormatColors(m.color, b.color, b.at > m.at ? (n - m.at) / (b.at - m.at) : 1);
1850
+ }
1851
+ if (!fill) return null;
1852
+ out.fill = fill;
1853
+ return "look";
1854
+ }
1855
+ case "dataBar": {
1856
+ const n = conditionalFormatNumber(v);
1857
+ if (n === null || out.dataBar) return null;
1858
+ const st = this.statsFor(rule);
1859
+ let lo = this.threshold(rule, st, rule.min ?? { type: "min" }, row, col);
1860
+ const hi = this.threshold(rule, st, rule.max ?? { type: "max" }, row, col);
1861
+ if (lo > 0) lo = 0;
1862
+ const frac = hi > lo ? Math.min(Math.max((n - lo) / (hi - lo), 0), 1) : 1;
1863
+ out.dataBar = {
1864
+ // A minimum length keeps small positive values visible.
1865
+ fraction: frac * 0.9 + (frac > 0 ? 0.1 : 0),
1866
+ color: rule.color ?? DEFAULT_BAR_COLOR,
1867
+ gradient: rule.gradient ?? true
1868
+ };
1869
+ if (rule.showValue === false) out.hideValue = true;
1870
+ return "look";
1871
+ }
1872
+ case "iconSet": {
1873
+ const n = conditionalFormatNumber(v);
1874
+ if (n === null || out.icon) return null;
1875
+ const set = rule.set ?? "3TrafficLights1";
1876
+ const k = conditionalFormatIconCount(set);
1877
+ const thresholds = rule.thresholds ?? [];
1878
+ const lower = thresholds.length === k ? thresholds.slice(1) : thresholds.length + 1 === k ? thresholds : defaultIconThresholds(set).slice(1);
1879
+ const st = this.statsFor(rule);
1880
+ let idx = 0;
1881
+ for (const t of lower) if (n >= this.threshold(rule, st, t, row, col)) idx++;
1882
+ if (rule.reverse) idx = k - 1 - Math.min(idx, k - 1);
1883
+ idx = Math.min(idx, k - 1);
1884
+ out.icon = { set, index: idx, id: `${set}:${idx}` };
1885
+ if (rule.showValue === false) out.hideValue = true;
1886
+ return "look";
1887
+ }
1888
+ }
1889
+ return null;
1890
+ }
1891
+ };
1892
+ function matchesTimePeriod(period, d, today) {
1893
+ const t = serialFromYmd(today.getFullYear(), today.getMonth() + 1, today.getDate());
1894
+ if (t === null) return false;
1895
+ const weekStart = t - today.getDay();
1896
+ const date = serialToDate(d);
1897
+ const monthOffset = (k) => {
1898
+ if (!date) return false;
1899
+ const m = today.getMonth() + k;
1900
+ const y = today.getFullYear() + Math.floor(m / 12);
1901
+ const mm = (m % 12 + 12) % 12;
1902
+ return date.getFullYear() === y && date.getMonth() === mm;
1903
+ };
1904
+ switch (period) {
1905
+ case "yesterday":
1906
+ return d === t - 1;
1907
+ case "today":
1908
+ return d === t;
1909
+ case "tomorrow":
1910
+ return d === t + 1;
1911
+ case "last7Days":
1912
+ return d > t - 7 && d <= t;
1913
+ case "lastWeek":
1914
+ return d >= weekStart - 7 && d < weekStart;
1915
+ case "thisWeek":
1916
+ return d >= weekStart && d < weekStart + 7;
1917
+ case "nextWeek":
1918
+ return d >= weekStart + 7 && d < weekStart + 14;
1919
+ case "lastMonth":
1920
+ return monthOffset(-1);
1921
+ case "thisMonth":
1922
+ return monthOffset(0);
1923
+ case "nextMonth":
1924
+ return monthOffset(1);
1925
+ default:
1926
+ return false;
1927
+ }
1928
+ }
1929
+ function evaluateConditionalFormats(sheet, rules) {
1930
+ return new ConditionalFormatEngine(sheet).evaluate(rules);
1931
+ }
1932
+
1933
+ // src/spreadsheet/conditional-format/describe.ts
1934
+ var OPERATOR_TEXT = {
1935
+ between: "between",
1936
+ notBetween: "not between",
1937
+ equal: "=",
1938
+ notEqual: "\u2260",
1939
+ greaterThan: ">",
1940
+ lessThan: "<",
1941
+ greaterThanOrEqual: "\u2265",
1942
+ lessThanOrEqual: "\u2264"
1943
+ };
1944
+ var anchorText = (v, fallback) => {
1945
+ if (!v) return fallback;
1946
+ switch (v.type) {
1947
+ case "min":
1948
+ return "min";
1949
+ case "max":
1950
+ return "max";
1951
+ case "number":
1952
+ return String(v.value);
1953
+ case "percent":
1954
+ return `${v.value}%`;
1955
+ case "percentile":
1956
+ return `P${v.value}`;
1957
+ case "formula":
1958
+ return v.formula;
1959
+ }
1960
+ };
1961
+ function describeConditionalFormat(rule) {
1962
+ switch (rule.type) {
1963
+ case "cellIs":
1964
+ return rule.operator === "between" || rule.operator === "notBetween" ? `Cell value ${OPERATOR_TEXT[rule.operator]} ${rule.value} and ${rule.value2 ?? ""}` : `Cell value ${OPERATOR_TEXT[rule.operator]} ${rule.value}`;
1965
+ case "expression":
1966
+ return `Formula: ${rule.formula.startsWith("=") ? rule.formula : `=${rule.formula}`}`;
1967
+ case "containsText":
1968
+ return `Text contains "${rule.text}"`;
1969
+ case "notContainsText":
1970
+ return `Text does not contain "${rule.text}"`;
1971
+ case "beginsWith":
1972
+ return `Text begins with "${rule.text}"`;
1973
+ case "endsWith":
1974
+ return `Text ends with "${rule.text}"`;
1975
+ case "blanks":
1976
+ return "Blank cells";
1977
+ case "noBlanks":
1978
+ return "Non-blank cells";
1979
+ case "errors":
1980
+ return "Errors";
1981
+ case "noErrors":
1982
+ return "No errors";
1983
+ case "duplicate":
1984
+ return "Duplicate values";
1985
+ case "unique":
1986
+ return "Unique values";
1987
+ case "top10":
1988
+ return `${rule.bottom ? "Bottom" : "Top"} ${rule.rank}${rule.percent ? "%" : ""}`;
1989
+ case "aboveAverage":
1990
+ return `${rule.below ? "Below" : "Above"} average${rule.stdDev ? ` by ${rule.stdDev} std dev` : ""}`;
1991
+ case "timePeriod":
1992
+ return `Date occurring: ${rule.period}`;
1993
+ case "colorScale":
1994
+ return `Color scale (${rule.stops.map((s) => anchorText(s.value, "")).join(" \u2192 ")})`;
1995
+ case "dataBar":
1996
+ return `Data bar (${anchorText(rule.min, "min")} \u2192 ${anchorText(rule.max, "max")})`;
1997
+ case "iconSet":
1998
+ return `Icon set ${rule.set ?? "3TrafficLights1"}${rule.reverse ? " (reversed)" : ""}`;
1999
+ }
2000
+ }
2001
+ var VALIDATION_OPERATOR_TEXT = OPERATOR_TEXT;
2002
+ function describeDataValidation(rule) {
2003
+ const op = rule.operator ?? "between";
2004
+ const bounds = op === "between" || op === "notBetween" ? `${VALIDATION_OPERATOR_TEXT[op]} ${rule.formula1 ?? ""} and ${rule.formula2 ?? ""}` : `${VALIDATION_OPERATOR_TEXT[op]} ${rule.formula1 ?? ""}`;
2005
+ switch (rule.type) {
2006
+ case "any":
2007
+ return "Any value";
2008
+ case "whole":
2009
+ return `Whole number ${bounds}`;
2010
+ case "decimal":
2011
+ return `Decimal ${bounds}`;
2012
+ case "date":
2013
+ return `Date ${bounds}`;
2014
+ case "time":
2015
+ return `Time ${bounds}`;
2016
+ case "textLength":
2017
+ return `Text length ${bounds}`;
2018
+ case "list":
2019
+ return `List: ${rule.formula1 ?? ""}`;
2020
+ case "custom":
2021
+ return `Custom: ${rule.formula1 ?? ""}`;
2022
+ }
2023
+ }
2024
+
2025
+ // src/spreadsheet/conditional-format/presets.ts
2026
+ var CONDITIONAL_FORMAT_STYLE_PRESETS = [
2027
+ { id: "lightRed", label: "Light red fill, dark red text", style: { fill: "#ffc7ce", color: "#9c0006" } },
2028
+ { id: "yellow", label: "Yellow fill, dark yellow text", style: { fill: "#ffeb9c", color: "#9c5700" } },
2029
+ { id: "green", label: "Green fill, dark green text", style: { fill: "#c6efce", color: "#006100" } },
2030
+ { id: "redText", label: "Red text", style: { color: "#9c0006" } },
2031
+ { id: "bold", label: "Bold", style: { bold: true } }
2032
+ ];
2033
+ var DEFAULT_CONDITIONAL_FORMAT_STYLE = CONDITIONAL_FORMAT_STYLE_PRESETS[0].style;
2034
+ var CONDITIONAL_FORMAT_PRESETS = [
2035
+ { kind: "greaterThan", label: "Cell value greater than", fields: ["value"], usesStyle: true },
2036
+ { kind: "lessThan", label: "Cell value less than", fields: ["value"], usesStyle: true },
2037
+ { kind: "between", label: "Cell value between", fields: ["value", "value2"], usesStyle: true },
2038
+ { kind: "equal", label: "Cell value equal to", fields: ["value"], usesStyle: true },
2039
+ { kind: "textContains", label: "Text that contains", fields: ["text"], usesStyle: true },
2040
+ { kind: "dateOccurring", label: "A date occurring", fields: ["period"], usesStyle: true },
2041
+ { kind: "duplicate", label: "Duplicate values", fields: [], usesStyle: true },
2042
+ { kind: "unique", label: "Unique values", fields: [], usesStyle: true },
2043
+ { kind: "top10", label: "Top N items", fields: ["rank"], usesStyle: true },
2044
+ { kind: "bottom10", label: "Bottom N items", fields: ["rank"], usesStyle: true },
2045
+ { kind: "aboveAverage", label: "Above average", fields: [], usesStyle: true },
2046
+ { kind: "belowAverage", label: "Below average", fields: [], usesStyle: true },
2047
+ { kind: "blanks", label: "Blank cells", fields: [], usesStyle: true },
2048
+ { kind: "errors", label: "Errors", fields: [], usesStyle: true },
2049
+ { kind: "formula", label: "Use a formula", fields: ["formula"], usesStyle: true },
2050
+ { kind: "colorScale2", label: "2-color scale", fields: [], usesStyle: false },
2051
+ { kind: "colorScale3", label: "3-color scale", fields: [], usesStyle: false },
2052
+ { kind: "dataBar", label: "Data bars", fields: ["color"], usesStyle: false },
2053
+ { kind: "iconSet", label: "Icon set", fields: ["iconSet"], usesStyle: false }
2054
+ ];
2055
+ var CONDITIONAL_FORMAT_TIME_PERIODS = [
2056
+ { period: "yesterday", label: "Yesterday" },
2057
+ { period: "today", label: "Today" },
2058
+ { period: "tomorrow", label: "Tomorrow" },
2059
+ { period: "last7Days", label: "In the last 7 days" },
2060
+ { period: "lastWeek", label: "Last week" },
2061
+ { period: "thisWeek", label: "This week" },
2062
+ { period: "nextWeek", label: "Next week" },
2063
+ { period: "lastMonth", label: "Last month" },
2064
+ { period: "thisMonth", label: "This month" },
2065
+ { period: "nextMonth", label: "Next month" }
2066
+ ];
2067
+ function createConditionalFormatRule(input) {
2068
+ const style = input.style ?? DEFAULT_CONDITIONAL_FORMAT_STYLE;
2069
+ const base = { ranges: input.ranges, ...input.stopIfTrue ? { stopIfTrue: true } : {} };
2070
+ switch (input.kind) {
2071
+ case "greaterThan":
2072
+ case "lessThan":
2073
+ case "equal":
2074
+ return { ...base, type: "cellIs", operator: input.kind, value: input.value ?? "0", style };
2075
+ case "between":
2076
+ return { ...base, type: "cellIs", operator: "between", value: input.value ?? "0", value2: input.value2 ?? "0", style };
2077
+ case "textContains":
2078
+ return { ...base, type: "containsText", text: input.text ?? "", style };
2079
+ case "dateOccurring":
2080
+ return { ...base, type: "timePeriod", period: input.period ?? "today", style };
2081
+ case "duplicate":
2082
+ case "unique":
2083
+ case "blanks":
2084
+ case "errors":
2085
+ return { ...base, type: input.kind, style };
2086
+ case "top10":
2087
+ case "bottom10":
2088
+ return { ...base, type: "top10", bottom: input.kind === "bottom10", rank: input.rank ?? 10, style };
2089
+ case "aboveAverage":
2090
+ case "belowAverage":
2091
+ return { ...base, type: "aboveAverage", below: input.kind === "belowAverage", style };
2092
+ case "formula":
2093
+ return { ...base, type: "expression", formula: input.formula ?? "=TRUE", style };
2094
+ case "colorScale2":
2095
+ return {
2096
+ ...base,
2097
+ type: "colorScale",
2098
+ stops: [
2099
+ { value: { type: "min" }, color: "#ffffff" },
2100
+ { value: { type: "max" }, color: "#63be7b" }
2101
+ ]
2102
+ };
2103
+ case "colorScale3":
2104
+ return {
2105
+ ...base,
2106
+ type: "colorScale",
2107
+ stops: [
2108
+ { value: { type: "min" }, color: "#f8696b" },
2109
+ { value: { type: "percentile", value: 50 }, color: "#ffeb84" },
2110
+ { value: { type: "max" }, color: "#63be7b" }
2111
+ ]
2112
+ };
2113
+ case "dataBar":
2114
+ return { ...base, type: "dataBar", color: input.color ?? "#638ec6" };
2115
+ case "iconSet":
2116
+ return { ...base, type: "iconSet", set: input.iconSet ?? "3TrafficLights1" };
2117
+ }
2118
+ }
2119
+ var DATA_VALIDATION_TYPE_OPTIONS = [
2120
+ { type: "any", label: "Any value" },
2121
+ { type: "whole", label: "Whole number" },
2122
+ { type: "decimal", label: "Decimal" },
2123
+ { type: "list", label: "List" },
2124
+ { type: "date", label: "Date" },
2125
+ { type: "time", label: "Time" },
2126
+ { type: "textLength", label: "Text length" },
2127
+ { type: "custom", label: "Custom formula" }
2128
+ ];
2129
+ var DATA_VALIDATION_OPERATOR_OPTIONS = [
2130
+ { operator: "between", label: "between" },
2131
+ { operator: "notBetween", label: "not between" },
2132
+ { operator: "equal", label: "equal to" },
2133
+ { operator: "notEqual", label: "not equal to" },
2134
+ { operator: "greaterThan", label: "greater than" },
2135
+ { operator: "lessThan", label: "less than" },
2136
+ { operator: "greaterThanOrEqual", label: "greater than or equal to" },
2137
+ { operator: "lessThanOrEqual", label: "less than or equal to" }
2138
+ ];
2139
+ var DATA_VALIDATION_ERROR_STYLE_OPTIONS = [
2140
+ { style: "stop", label: "Stop" },
2141
+ { style: "warning", label: "Warning" },
2142
+ { style: "information", label: "Information" }
2143
+ ];
2144
+
2145
+ // src/spreadsheet/conditional-format/render.ts
2146
+ function conditionalFormatContrastColor(fill) {
2147
+ let hex = fill.trim().replace(/^#/, "");
2148
+ if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
2149
+ if (!/^[0-9a-fA-F]{6}$/.test(hex)) return void 0;
2150
+ const [r, g2, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
2151
+ return 0.299 * r + 0.587 * g2 + 0.114 * b > 0.55 ? "#1f2328" : "#ffffff";
2152
+ }
2153
+ function getConditionalFormatCss(format) {
2154
+ if (!format) return {};
2155
+ const css = {};
2156
+ if (format.fill) css.background = format.fill;
2157
+ if (format.color) css.color = format.color;
2158
+ else if (format.fill) {
2159
+ const contrast = conditionalFormatContrastColor(format.fill);
2160
+ if (contrast) css.color = contrast;
2161
+ }
2162
+ if (format.bold) css.fontWeight = "700";
2163
+ if (format.italic) css.fontStyle = "italic";
2164
+ const decorations = [format.underline ? "underline" : "", format.strikethrough ? "line-through" : ""].filter(Boolean);
2165
+ if (decorations.length) css.textDecoration = decorations.join(" ");
2166
+ return css;
2167
+ }
2168
+ function getDataBarCss(bar) {
2169
+ const width = `${Math.round(Math.min(Math.max(bar.fraction, 0), 1) * 1e3) / 10}%`;
2170
+ return {
2171
+ position: "absolute",
2172
+ left: "0",
2173
+ top: "2px",
2174
+ bottom: "2px",
2175
+ width,
2176
+ pointerEvents: "none",
2177
+ borderRadius: "1px",
2178
+ background: bar.gradient ? `linear-gradient(90deg, ${bar.color}, color-mix(in srgb, ${bar.color} 20%, transparent))` : bar.color,
2179
+ opacity: bar.gradient ? "0.9" : "0.75"
2180
+ };
2181
+ }
2182
+ var kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
2183
+ function conditionalFormatStyleString(css) {
2184
+ let out = "";
2185
+ for (const [key, value] of Object.entries(css)) {
2186
+ if (value !== void 0 && value !== "") out += `${kebab(key)}: ${value}; `;
2187
+ }
2188
+ return out.trim();
2189
+ }
2190
+
2191
+ // src/spreadsheet/validation/validate.ts
2192
+ var DEFAULT_DATA_VALIDATION_MESSAGE = "This value doesn't match the data validation restrictions defined for this cell.";
2193
+ var DEFAULT_DATA_VALIDATION_TITLE = "Invalid value";
2194
+ var BARE_REFERENCE = /^\$?[A-Za-z]{1,3}\$?\d{1,7}(?::\$?[A-Za-z]{1,3}\$?\d{1,7})?$/;
2195
+ var isBlank3 = (v) => v === "" || v === null || v === void 0;
2196
+ function valueText2(value) {
2197
+ if (isBlank3(value)) return "";
2198
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
2199
+ if (typeof value === "number") return numberToText(value);
2200
+ return String(value);
2201
+ }
2202
+ function findDataValidation(rules, row, col) {
2203
+ if (!rules) return void 0;
2204
+ for (const rule of rules) if (rangesContain(rule.ranges, row, col)) return rule;
2205
+ return void 0;
2206
+ }
2207
+ function evaluate(sheet, rule, formula, row, col, entered) {
2208
+ if (!sheet.evaluateFormula) return "#NAME?";
2209
+ try {
2210
+ return sheet.evaluateFormula(ruleFormulaAt(formula, ruleAnchor(rule.ranges), row, col), row, col, entered);
2211
+ } catch {
2212
+ return "#ERROR!";
2213
+ }
2214
+ }
2215
+ var first = (v) => Array.isArray(v) ? v.length ? v[0] : "" : v;
2216
+ function getValidationListValues(sheet, rule) {
2217
+ const f = (rule.formula1 ?? "").trim();
2218
+ if (f === "") return [];
2219
+ if (f.startsWith("=") || BARE_REFERENCE.test(f)) {
2220
+ const anchor = ruleAnchor(rule.ranges);
2221
+ const result = evaluate(sheet, rule, f, anchor.row, anchor.col);
2222
+ const values = Array.isArray(result) ? result : [result];
2223
+ const out = [];
2224
+ for (const v of values) if (!isBlank3(v) && !isFormulaError(v)) out.push(valueText2(v));
2225
+ return out;
2226
+ }
2227
+ const unquoted = f.length >= 2 && f.startsWith('"') && f.endsWith('"') ? f.slice(1, -1) : f;
2228
+ return unquoted.split(",").map((x) => x.trim()).filter((x) => x !== "");
2229
+ }
2230
+ function compareOp(op, n, a, b) {
2231
+ if (a === null) return false;
2232
+ switch (op) {
2233
+ case "between":
2234
+ return b !== null && n >= Math.min(a, b) && n <= Math.max(a, b);
2235
+ case "notBetween":
2236
+ return b !== null && (n < Math.min(a, b) || n > Math.max(a, b));
2237
+ case "equal":
2238
+ return n === a;
2239
+ case "notEqual":
2240
+ return n !== a;
2241
+ case "greaterThan":
2242
+ return n > a;
2243
+ case "lessThan":
2244
+ return n < a;
2245
+ case "greaterThanOrEqual":
2246
+ return n >= a;
2247
+ case "lessThanOrEqual":
2248
+ return n <= a;
2249
+ }
2250
+ }
2251
+ function bound(sheet, rule, text, row, col, entered) {
2252
+ if (text === void 0 || text.trim() === "") return null;
2253
+ const value = text.trim().startsWith("=") ? first(evaluate(sheet, rule, text, row, col, entered)) : parseCellInput(text.trim()).value;
2254
+ return conditionalFormatNumber(value);
2255
+ }
2256
+ function satisfies(sheet, rule, row, col, value, text, entered) {
2257
+ const op = rule.operator ?? "between";
2258
+ switch (rule.type) {
2259
+ case "any":
2260
+ return true;
2261
+ case "list": {
2262
+ const typed = text.trim().toLowerCase();
2263
+ return getValidationListValues(sheet, rule).some(
2264
+ (item) => item.toLowerCase() === typed || compareValues(parseCellInput(item).value, value) === 0
2265
+ );
2266
+ }
2267
+ case "custom":
2268
+ return toBoolean(first(evaluate(sheet, rule, rule.formula1 ?? "", row, col, entered))) === true;
2269
+ case "textLength":
2270
+ return compareOp(op, Array.from(text).length, bound(sheet, rule, rule.formula1, row, col, entered), bound(sheet, rule, rule.formula2, row, col, entered));
2271
+ case "whole":
2272
+ case "decimal":
2273
+ case "date":
2274
+ case "time": {
2275
+ if (isFormulaError(value)) return false;
2276
+ let n = conditionalFormatNumber(value);
2277
+ if (n === null) return false;
2278
+ if (rule.type === "whole" && !Number.isInteger(n)) return false;
2279
+ if (rule.type === "time" && typeof value === "string") n -= Math.floor(n);
2280
+ return compareOp(op, n, bound(sheet, rule, rule.formula1, row, col, entered), bound(sheet, rule, rule.formula2, row, col, entered));
2281
+ }
2282
+ }
2283
+ }
2284
+ function invalid(rule) {
2285
+ return {
2286
+ valid: false,
2287
+ rule,
2288
+ errorStyle: rule.errorStyle ?? "stop",
2289
+ title: rule.errorTitle || DEFAULT_DATA_VALIDATION_TITLE,
2290
+ message: rule.errorMessage || DEFAULT_DATA_VALIDATION_MESSAGE
2291
+ };
2292
+ }
2293
+ function validateCellInput(sheet, rules, row, col, input) {
2294
+ const rule = findDataValidation(rules, row, col);
2295
+ if (!rule) return { valid: true };
2296
+ if (rule.type === "any" || rule.showErrorAlert === false) return { valid: true, rule };
2297
+ if (input.trim() === "" && (rule.ignoreBlank ?? true)) return { valid: true, rule };
2298
+ let value;
2299
+ let text = input;
2300
+ if (input.startsWith("=")) {
2301
+ value = first(sheet.evaluateFormula ? safeEval(sheet, input, row, col) : "#NAME?");
2302
+ text = valueText2(value);
2303
+ } else {
2304
+ value = parseCellInput(input).value;
2305
+ if (input.startsWith("'")) text = input.slice(1);
2306
+ }
2307
+ const entered = { row, col, value };
2308
+ return satisfies(sheet, rule, row, col, value, text, entered) ? { valid: true, rule } : invalid(rule);
2309
+ }
2310
+ function safeEval(sheet, formula, row, col) {
2311
+ try {
2312
+ return sheet.evaluateFormula(formula, row, col);
2313
+ } catch {
2314
+ return "#ERROR!";
2315
+ }
2316
+ }
2317
+ function findInvalidCells(sheet, rules) {
2318
+ const out = [];
2319
+ if (!rules?.length) return out;
2320
+ const seen = /* @__PURE__ */ new Set();
2321
+ for (const rule of rules) {
2322
+ if (rule.type === "any") continue;
2323
+ forEachRangeCell(rule.ranges, sheet.rowCount, sheet.colCount, (row, col) => {
2324
+ const key = `${row}:${col}`;
2325
+ if (seen.has(key)) return;
2326
+ seen.add(key);
2327
+ if (findDataValidation(rules, row, col) !== rule) return;
2328
+ const value = sheet.getValue(row, col);
2329
+ if (isBlank3(value)) return;
2330
+ const raw = sheet.getInput?.(row, col);
2331
+ const text = raw !== void 0 && raw !== "" && !raw.startsWith("=") ? raw.startsWith("'") ? raw.slice(1) : raw : valueText2(value);
2332
+ if (!satisfies(sheet, rule, row, col, value, text)) out.push({ row, col });
2333
+ });
2334
+ }
2335
+ return out;
2336
+ }
2337
+
1121
2338
  // src/spreadsheet/view.ts
1122
2339
  var DEFAULT_ROW_HEIGHT = 32;
1123
2340
  var DEFAULT_COLUMN_WIDTH = 130;
@@ -1278,11 +2495,19 @@ function buildSpreadsheetView(doc, getValue) {
1278
2495
 
1279
2496
  // src/spreadsheet/controller.ts
1280
2497
  var nextIdSequence = 0;
2498
+ var createRuntime = (doc) => ({
2499
+ doc,
2500
+ spills: /* @__PURE__ */ new Map(),
2501
+ spilledBy: /* @__PURE__ */ new Map(),
2502
+ spillCells: /* @__PURE__ */ new Map(),
2503
+ activeCell: null,
2504
+ selectedRange: null
2505
+ });
2506
+ var sameName3 = (a, b) => a.toUpperCase() === b.toUpperCase();
1281
2507
  var DEFAULT_MAX_HISTORY_BYTES = 64 * 1024 * 1024;
1282
2508
  var MAX_SPILL_PASSES = 25;
1283
2509
  var spillCellId = (rowId, colId) => `${rowId}\0${colId}`;
1284
2510
  var SpreadsheetController = class {
1285
- document;
1286
2511
  activeCell = null;
1287
2512
  selectedRange = null;
1288
2513
  editingCell = null;
@@ -1292,6 +2517,17 @@ var SpreadsheetController = class {
1292
2517
  evaluator;
1293
2518
  primaryDataSourceId;
1294
2519
  onDocumentChange;
2520
+ onWorkbookChange;
2521
+ onActiveSheetChange;
2522
+ // Workbook: sheets in tab order, the active one, and the one being worked
2523
+ // on (the active sheet, or another sheet while one of its cells is read or
2524
+ // evaluated). Everything that uses `this.document` works on `ctx`.
2525
+ runtimes = [];
2526
+ active;
2527
+ ctx;
2528
+ // The sheet bound to the primary data source (the first sheet).
2529
+ boundSheetId = null;
2530
+ names = [];
1295
2531
  onSelectionChange;
1296
2532
  onCellCommit;
1297
2533
  // History stack for undo/redo
@@ -1306,16 +2542,39 @@ var SpreadsheetController = class {
1306
2542
  evaluatingRowIndex = null;
1307
2543
  // Cell being evaluated, for implicit intersection (`=@A:A`).
1308
2544
  evaluatingCell = null;
1309
- // Dynamic arrays. Spilled values are derived state: they live here, not in
1310
- // the document, and every recalculation rebuilds them.
1311
- spills = /* @__PURE__ */ new Map();
1312
- // anchor key -> spill
1313
- spilledBy = /* @__PURE__ */ new Map();
1314
- // spilled cell key -> anchor key
1315
- spillCells = /* @__PURE__ */ new Map();
1316
- // spillCellId -> displayed cell
1317
- // Cells whose spill ownership changed during the current pass.
2545
+ // Dynamic arrays. Spilled values are derived state: they live in the sheet
2546
+ // runtimes, not in the documents, and every recalculation rebuilds them.
2547
+ // Cells (sheet-qualified keys) whose spill ownership changed during the current pass.
1318
2548
  spillChanges = null;
2549
+ /** The active sheet's document (the sheet the editor shows). */
2550
+ get document() {
2551
+ return this.ctx.doc;
2552
+ }
2553
+ set document(doc) {
2554
+ this.ctx.doc = doc;
2555
+ }
2556
+ get spills() {
2557
+ return this.ctx.spills;
2558
+ }
2559
+ get spilledBy() {
2560
+ return this.ctx.spilledBy;
2561
+ }
2562
+ get spillCells() {
2563
+ return this.ctx.spillCells;
2564
+ }
2565
+ // Conditional formatting is evaluated lazily (getConditionalFormatOverlay).
2566
+ // Cell keys whose values changed since, or 'all' to evaluate from scratch.
2567
+ cfEngine = null;
2568
+ cfDirty = "all";
2569
+ // Set while a single cell edit notifies, so the change stays incremental.
2570
+ cfIncremental = false;
2571
+ // Value being entered, seen by validation formulas instead of the cell's value.
2572
+ valueOverride = null;
2573
+ // "Circle invalid data": on/off and the cached cells.
2574
+ circlesShown = false;
2575
+ invalidCircles = null;
2576
+ /** Invalid input waiting for the user's answer (see `resolveValidationPrompt`). */
2577
+ validationPrompt = null;
1319
2578
  constructor(options = {}) {
1320
2579
  const defaultCols = [
1321
2580
  { id: "col_a", key: "A", title: "A", width: 120, type: "freeform" },
@@ -1329,7 +2588,7 @@ var SpreadsheetController = class {
1329
2588
  height: 32,
1330
2589
  type: "freeform"
1331
2590
  }));
1332
- this.document = {
2591
+ const first2 = {
1333
2592
  id: options.document?.id || `sheet_${Date.now()}`,
1334
2593
  name: options.document?.name || "Sheet 1",
1335
2594
  columns: options.document?.columns || defaultCols,
@@ -1339,27 +2598,123 @@ var SpreadsheetController = class {
1339
2598
  updatedAt: options.document?.updatedAt || Date.now(),
1340
2599
  ...options.document?.table ? { table: options.document.table } : {}
1341
2600
  };
2601
+ if (options.workbook && options.workbook.sheets.length > 0) {
2602
+ this.setWorkbookState(options.workbook, false);
2603
+ } else {
2604
+ this.runtimes = [createRuntime(first2)];
2605
+ this.active = this.ctx = this.runtimes[0];
2606
+ }
2607
+ this.boundSheetId = this.runtimes[0].doc.id;
1342
2608
  this.primaryDataSourceId = options.primaryDataSourceId;
1343
2609
  this.maxHistoryBytes = Math.max(0, options.maxHistoryBytes ?? DEFAULT_MAX_HISTORY_BYTES);
1344
2610
  this.onDocumentChange = options.onDocumentChange;
1345
2611
  this.onSelectionChange = options.onSelectionChange;
1346
2612
  this.onCellCommit = options.onCellCommit;
2613
+ this.onWorkbookChange = options.onWorkbookChange;
2614
+ this.onActiveSheetChange = options.onActiveSheetChange;
1347
2615
  if (options.dataSources) {
1348
2616
  options.dataSources.forEach((ds) => this.binder.register(ds));
1349
2617
  }
1350
2618
  this.evaluator = new FormulaEvaluator({
1351
- resolveCell: (colStr, rowNum) => this.resolveCellByCoord(colStr, rowNum),
1352
- resolveRange: (sCol, sRow, eCol, eRow) => this.resolveRangeByCoords(sCol, sRow, eCol, eRow),
2619
+ resolveCell: (colStr, rowNum, sheet) => this.onSheet(sheet, () => this.resolveCellByCoord(colStr, rowNum)),
2620
+ resolveRange: (sCol, sRow, eCol, eRow, sheet) => this.onSheet(sheet, () => this.resolveRangeByCoords(sCol, sRow, eCol, eRow)),
1353
2621
  resolveProperty: (prop) => this.resolveCurrentRowProperty(prop),
1354
- resolveSpill: (colStr, rowNum) => this.resolveSpillByCoord(colStr, rowNum),
2622
+ resolveSpill: (colStr, rowNum, sheet) => this.onSheet(sheet, () => this.resolveSpillByCoord(colStr, rowNum)),
2623
+ resolveSheetSpan: (first3, last) => this.resolveSheetSpan(first3, last),
2624
+ resolveName: (name, sheet) => this.resolveDefinedName(name, sheet),
1355
2625
  getFormulaCell: () => this.evaluatingCell
1356
2626
  });
1357
2627
  this.syncWithDataSource();
1358
- this.rederiveTypedValues();
2628
+ for (const rt of this.runtimes) this.withSheet(rt, () => this.rederiveTypedValues());
1359
2629
  this.recalculateAll();
1360
2630
  }
2631
+ // --- Workbook: sheet runtimes ---
2632
+ get boundRuntime() {
2633
+ return this.boundSheetId === null ? null : this.findRuntimeById(this.boundSheetId) ?? null;
2634
+ }
2635
+ /** Runs `fn` with `rt` as the sheet `this.document` (and spills) refer to. */
2636
+ withSheet(rt, fn) {
2637
+ if (rt === this.ctx) return fn();
2638
+ const saved = this.ctx;
2639
+ this.ctx = rt;
2640
+ try {
2641
+ return fn();
2642
+ } finally {
2643
+ this.ctx = saved;
2644
+ }
2645
+ }
2646
+ /**
2647
+ * Runs `fn` on the sheet named `sheet` (a sheet-qualified reference), or
2648
+ * on the current sheet when undefined. `#REF!` when no sheet has that name.
2649
+ */
2650
+ onSheet(sheet, fn) {
2651
+ if (sheet === void 0) return fn();
2652
+ const rt = this.findRuntimeByName(sheet);
2653
+ return rt ? this.withSheet(rt, fn) : "#REF!";
2654
+ }
2655
+ findRuntimeByName(name) {
2656
+ return this.runtimes.find((rt) => sameName3(rt.doc.name, name));
2657
+ }
2658
+ findRuntimeById(id) {
2659
+ return this.runtimes.find((rt) => rt.doc.id === id) ?? this.runtimes.find((rt) => sameName3(rt.doc.id, id));
2660
+ }
2661
+ /** A sheet by id or name. */
2662
+ findRuntime(idOrName) {
2663
+ return this.runtimes.find((rt) => rt.doc.id === idOrName) ?? this.findRuntimeByName(idOrName);
2664
+ }
2665
+ /** Dependency-graph key of a cell (`A1`) on a sheet: sheet-qualified, so keys are unique in the workbook. */
2666
+ qualify(key, rt = this.ctx) {
2667
+ return `${rt.doc.id}!${key}`.toUpperCase();
2668
+ }
2669
+ /** The sheet and A1 key of a qualified key. */
2670
+ unqualify(qualified) {
2671
+ const bang = qualified.lastIndexOf("!");
2672
+ if (bang < 0) return null;
2673
+ const id = qualified.slice(0, bang);
2674
+ const rt = this.runtimes.find((r) => r.doc.id.toUpperCase() === id);
2675
+ return rt ? { rt, key: qualified.slice(bang + 1) } : null;
2676
+ }
2677
+ resolveSheetSpan(first2, last) {
2678
+ const a = this.runtimes.findIndex((rt) => sameName3(rt.doc.name, first2));
2679
+ const b = this.runtimes.findIndex((rt) => sameName3(rt.doc.name, last));
2680
+ if (a < 0 || b < 0) return "#REF!";
2681
+ return this.runtimes.slice(Math.min(a, b), Math.max(a, b) + 1).map((rt) => rt.doc.name);
2682
+ }
2683
+ /** The defined name `name` visible from `rt`: its sheet-scoped name first, then the workbook one. */
2684
+ findName(name, rt) {
2685
+ return (rt ? this.names.find((n) => n.sheetId === rt.doc.id && sameName3(n.name, name)) : void 0) ?? this.names.find((n) => n.sheetId === void 0 && sameName3(n.name, name));
2686
+ }
2687
+ resolveDefinedName(name, sheet) {
2688
+ const rt = sheet === void 0 ? this.ctx : this.findRuntimeByName(sheet);
2689
+ if (!rt) return null;
2690
+ const def = this.findName(name, rt);
2691
+ if (!def) return null;
2692
+ const scope = def.sheetId !== void 0 ? this.findRuntimeById(def.sheetId) : void 0;
2693
+ return {
2694
+ formula: def.refersTo,
2695
+ ...scope ? { sheet: scope.doc.name } : {},
2696
+ key: `${def.sheetId ?? ""}!${def.name}`
2697
+ };
2698
+ }
2699
+ /**
2700
+ * Replaces the active sheet with `document` (its name becomes the sheet's
2701
+ * name, made unique). Clears undo history.
2702
+ */
1361
2703
  loadDocument(document) {
1362
- this.document = JSON.parse(JSON.stringify(document));
2704
+ const loaded = JSON.parse(JSON.stringify(document));
2705
+ const others = this.runtimes.filter((rt) => rt !== this.active);
2706
+ if (others.some((rt) => rt.doc.id === loaded.id)) loaded.id = this.generateSheetId();
2707
+ const otherNames = others.map((rt) => rt.doc.name);
2708
+ if (!loaded.name || otherNames.some((n) => sameName3(n, loaded.name))) {
2709
+ loaded.name = loaded.name ? copySheetName(loaded.name, otherNames) : nextSheetName(otherNames);
2710
+ }
2711
+ const previousId = this.active.doc.id;
2712
+ this.ctx = this.active;
2713
+ this.document = loaded;
2714
+ if (previousId !== loaded.id) {
2715
+ for (const n of this.names) if (n.sheetId === previousId) n.sheetId = loaded.id;
2716
+ if (this.boundSheetId === previousId) this.boundSheetId = loaded.id;
2717
+ }
1363
2718
  this.activeCell = null;
1364
2719
  this.selectedRange = null;
1365
2720
  this.editingCell = null;
@@ -1371,6 +2726,60 @@ var SpreadsheetController = class {
1371
2726
  this.recalculateAll();
1372
2727
  this.notifyChange();
1373
2728
  }
2729
+ // --- Workbook: loading and saving ---
2730
+ /** A deep copy of the workbook: every sheet, the active sheet and the defined names. */
2731
+ getWorkbook() {
2732
+ return JSON.parse(JSON.stringify(this.workbookSnapshot()));
2733
+ }
2734
+ /** Replaces the whole workbook (sheets, names, active sheet). Clears undo history. */
2735
+ loadWorkbook(workbook) {
2736
+ if (!workbook.sheets.length) return;
2737
+ this.setWorkbookState(JSON.parse(JSON.stringify(workbook)), true);
2738
+ this.boundSheetId = this.runtimes[0].doc.id;
2739
+ this.editingCell = null;
2740
+ this.draftValue = "";
2741
+ this.undoStack = [];
2742
+ this.redoStack = [];
2743
+ this.syncWithDataSource();
2744
+ for (const rt of this.runtimes) this.withSheet(rt, () => this.rederiveTypedValues());
2745
+ this.recalculateAll();
2746
+ this.notifyChange();
2747
+ this.onSelectionChange?.(this.activeCell, this.selectedRange);
2748
+ }
2749
+ workbookSnapshot() {
2750
+ return {
2751
+ sheets: this.runtimes.map((rt) => rt.doc),
2752
+ activeSheetId: this.active.doc.id,
2753
+ names: this.names
2754
+ };
2755
+ }
2756
+ /**
2757
+ * Installs a workbook's sheets and names (taking ownership of the objects).
2758
+ * Duplicate sheet ids and names are made unique.
2759
+ */
2760
+ setWorkbookState(workbook, resetSelection) {
2761
+ const ids = /* @__PURE__ */ new Set();
2762
+ const names = [];
2763
+ const runtimes = workbook.sheets.map((doc) => {
2764
+ if (!doc.id || ids.has(doc.id)) doc.id = this.generateSheetId();
2765
+ ids.add(doc.id);
2766
+ if (!doc.name || names.some((n) => sameName3(n, doc.name))) {
2767
+ doc.name = doc.name ? copySheetName(doc.name, names) : nextSheetName(names);
2768
+ }
2769
+ names.push(doc.name);
2770
+ doc.columns ??= [];
2771
+ doc.rows ??= [];
2772
+ doc.cells ??= {};
2773
+ return createRuntime(doc);
2774
+ });
2775
+ this.runtimes = runtimes;
2776
+ this.active = this.ctx = runtimes.find((rt) => rt.doc.id === workbook.activeSheetId) ?? runtimes[0];
2777
+ this.names = (workbook.names ?? []).filter((n) => n.sheetId === void 0 || ids.has(n.sheetId)).map((n) => ({ ...n, refersTo: n.refersTo.startsWith("=") ? n.refersTo : `=${n.refersTo}` }));
2778
+ if (resetSelection) {
2779
+ this.activeCell = null;
2780
+ this.selectedRange = null;
2781
+ }
2782
+ }
1374
2783
  /**
1375
2784
  * Re-parses the value of every stored typed (non-formula) cell from its
1376
2785
  * `raw` input, so documents saved before typed input was parsed (whose
@@ -1452,9 +2861,12 @@ var SpreadsheetController = class {
1452
2861
  this.recalculateAll();
1453
2862
  }
1454
2863
  syncWithDataSource() {
1455
- if (!this.primaryDataSourceId) return;
1456
- this.document.rows = this.binder.syncRows(this.document.rows, this.primaryDataSourceId);
1457
- this.recomputeRowIndices();
2864
+ if (!this.primaryDataSourceId || !this.boundRuntime) return;
2865
+ const id = this.primaryDataSourceId;
2866
+ this.withSheet(this.boundRuntime, () => {
2867
+ this.document.rows = this.binder.syncRows(this.document.rows, id);
2868
+ this.recomputeRowIndices();
2869
+ });
1458
2870
  }
1459
2871
  recomputeRowIndices() {
1460
2872
  this.document.rows.forEach((r, idx) => {
@@ -1534,18 +2946,24 @@ var SpreadsheetController = class {
1534
2946
  } else {
1535
2947
  cell = { raw, ...this.typedCell(raw, col?.format, col, existing) };
1536
2948
  }
1537
- const cellKey = this.getCellKey(rowId, colId);
1538
- this.depGraph.setCellFormula(cellKey, isFormula ? raw : null);
2949
+ const cellKey2 = this.getCellKey(rowId, colId);
2950
+ this.registerFormula(cellKey2, isFormula ? raw : null);
1539
2951
  this.document.cells[rowId][colId] = cell;
1540
2952
  this.document.updatedAt = Date.now();
1541
2953
  if (this.batching) {
1542
2954
  this.pendingCommits.push([rowId, colId, raw]);
1543
2955
  return;
1544
2956
  }
2957
+ if (this.cfDirty !== "all") this.cfDirty.add(cellKey2);
1545
2958
  this.recalculateCell(rowId, colId);
1546
- this.notifyChange();
1547
- const evaluatedVal = this.document.cells[rowId][colId]?.value;
1548
- this.onCellCommit?.(rowId, colId, raw, evaluatedVal);
2959
+ this.cfIncremental = true;
2960
+ try {
2961
+ this.notifyChange();
2962
+ } finally {
2963
+ this.cfIncremental = false;
2964
+ }
2965
+ const evaluatedVal = this.document.cells[rowId][colId]?.value;
2966
+ this.onCellCommit?.(rowId, colId, raw, evaluatedVal);
1549
2967
  }
1550
2968
  getCellKey(rowId, colId) {
1551
2969
  const row = this.document.rows.find((r) => r.id === rowId);
@@ -1558,9 +2976,66 @@ var SpreadsheetController = class {
1558
2976
  * order. Every cell in a reference cycle is marked `#CYCLE!`.
1559
2977
  */
1560
2978
  recalculateCell(rowId, colId) {
1561
- const cellKey = this.getCellKey(rowId, colId);
1562
- const anchor = this.spilledBy.get(cellKey);
1563
- this.recalculateKeys(anchor ? [cellKey, anchor] : [cellKey], true);
2979
+ const cellKey2 = this.getCellKey(rowId, colId);
2980
+ const anchor = this.spilledBy.get(cellKey2);
2981
+ this.recalculateKeys(anchor ? [this.qualify(cellKey2), this.qualify(anchor)] : [this.qualify(cellKey2)], true);
2982
+ }
2983
+ /**
2984
+ * Registers the dependencies of the formula in `cellKey` (A1 key on the
2985
+ * current sheet), or removes them when `formula` is null: its references
2986
+ * (sheet-qualified ones on their sheet, 3-D ones on every sheet of the
2987
+ * span) and, through defined names, the cells those names refer to.
2988
+ */
2989
+ registerFormula(cellKey2, formula) {
2990
+ const key = this.qualify(cellKey2);
2991
+ if (!formula || !formula.startsWith("=")) {
2992
+ this.depGraph.setCellDependencies(key, []);
2993
+ return;
2994
+ }
2995
+ let ast;
2996
+ try {
2997
+ ast = parseFormula(formula);
2998
+ } catch {
2999
+ this.depGraph.setCellDependencies(key, []);
3000
+ return;
3001
+ }
3002
+ const deps = /* @__PURE__ */ new Set();
3003
+ this.collectDependencies(ast, this.ctx, deps, /* @__PURE__ */ new Set());
3004
+ this.depGraph.setCellDependencies(key, deps);
3005
+ }
3006
+ /** Adds the qualified keys `ast` depends on, with unqualified references on `home`. */
3007
+ collectDependencies(ast, home, out, seenNames) {
3008
+ for (const dep of extractCellDependencies(ast)) {
3009
+ let sheets;
3010
+ if (dep.sheet === void 0) {
3011
+ sheets = [home];
3012
+ } else if (dep.sheetEnd === void 0) {
3013
+ const rt = this.findRuntimeByName(dep.sheet);
3014
+ sheets = rt ? [rt] : [];
3015
+ } else {
3016
+ const span = this.resolveSheetSpan(dep.sheet, dep.sheetEnd);
3017
+ sheets = Array.isArray(span) ? span.map((name) => this.findRuntimeByName(name)) : [];
3018
+ }
3019
+ for (const rt of sheets) {
3020
+ out.add(dep.row === 0 ? columnDependencyKey(`${rt.doc.id}!${dep.col}`) : this.qualify(`${dep.col}${dep.row}`, rt));
3021
+ }
3022
+ }
3023
+ for (const used of extractNameDependencies(ast)) {
3024
+ if (used.call && getFormulaFunction(used.name)) continue;
3025
+ const lookupSheet = used.sheet === void 0 ? home : this.findRuntimeByName(used.sheet);
3026
+ if (!lookupSheet) continue;
3027
+ const def = this.findName(used.name, lookupSheet);
3028
+ if (!def) continue;
3029
+ const scope = def.sheetId !== void 0 ? this.findRuntimeById(def.sheetId) : void 0;
3030
+ const nameHome = scope ?? home;
3031
+ const seenKey = `${def.sheetId ?? ""}!${def.name.toUpperCase()}@${nameHome.doc.id}`;
3032
+ if (seenNames.has(seenKey)) continue;
3033
+ seenNames.add(seenKey);
3034
+ try {
3035
+ this.collectDependencies(parseFormula(def.refersTo), nameHome, out, seenNames);
3036
+ } catch {
3037
+ }
3038
+ }
1564
3039
  }
1565
3040
  /**
1566
3041
  * Recalculates `keys` (and, with `includeDependents`, their dependents) in
@@ -1589,56 +3064,69 @@ var SpreadsheetController = class {
1589
3064
  * in dependency order.
1590
3065
  */
1591
3066
  recalculateAll() {
3067
+ this.cfDirty = "all";
1592
3068
  this.depGraph.clear();
1593
- this.spills.clear();
1594
- this.spilledBy.clear();
1595
- this.spillCells.clear();
1596
3069
  const formulaKeys = [];
1597
- for (const row of this.document.rows) {
1598
- for (const col of this.document.columns) {
1599
- const cell = this.document.cells[row.id]?.[col.id] ?? this.getDerivedCell(row, col);
1600
- if (cell.raw.startsWith("=")) {
1601
- const cellKey = `${col.key}${row.index + 1}`.toUpperCase();
1602
- this.depGraph.setCellFormula(cellKey, cell.raw);
1603
- formulaKeys.push(cellKey);
3070
+ for (const rt of this.runtimes) {
3071
+ this.withSheet(rt, () => {
3072
+ this.spills.clear();
3073
+ this.spilledBy.clear();
3074
+ this.spillCells.clear();
3075
+ for (const row of this.document.rows) {
3076
+ for (const col of this.document.columns) {
3077
+ const cell = this.document.cells[row.id]?.[col.id] ?? this.getDerivedCell(row, col);
3078
+ if (cell.raw.startsWith("=")) {
3079
+ const cellKey2 = `${col.key}${row.index + 1}`.toUpperCase();
3080
+ this.registerFormula(cellKey2, cell.raw);
3081
+ formulaKeys.push(this.qualify(cellKey2));
3082
+ }
3083
+ }
1604
3084
  }
1605
- }
3085
+ });
1606
3086
  }
1607
3087
  this.recalculateKeys(formulaKeys, false);
1608
3088
  }
1609
3089
  runEvaluationPlan(plan) {
1610
- for (const key of plan.order) {
1611
- if (plan.cyclic.has(key)) {
1612
- this.markCycle(key);
1613
- } else {
1614
- this.evalSingleCellByKey(key);
1615
- }
3090
+ for (const qualified of plan.order) {
3091
+ const located = this.unqualify(qualified);
3092
+ if (!located) continue;
3093
+ const onActive = located.rt === this.active;
3094
+ this.withSheet(located.rt, () => {
3095
+ if (onActive) this.markValueChanged(located.key);
3096
+ if (plan.cyclic.has(qualified)) {
3097
+ this.markCycle(located.key);
3098
+ } else {
3099
+ this.evalSingleCellByKey(located.key);
3100
+ }
3101
+ if (onActive) this.markValueChanged(located.key);
3102
+ });
1616
3103
  }
1617
3104
  }
1618
- locateCellKey(cellKey) {
1619
- const match = cellKey.match(/^([A-Za-z]+)(\d+)$/);
3105
+ locateCellKey(cellKey2) {
3106
+ const match = cellKey2.match(/^([A-Za-z]+)(\d+)$/);
1620
3107
  if (!match) return null;
1621
3108
  const col = this.document.columns.find((c) => c.key.toUpperCase() === match[1].toUpperCase());
1622
3109
  const row = this.document.rows[parseInt(match[2], 10) - 1];
1623
3110
  return col && row ? { row, col } : null;
1624
3111
  }
1625
- markCycle(cellKey) {
1626
- const located = this.locateCellKey(cellKey);
3112
+ markCycle(cellKey2) {
3113
+ const located = this.locateCellKey(cellKey2);
1627
3114
  if (!located) return;
1628
3115
  const { row, col } = located;
1629
3116
  const cell = this.getCell(row.id, col.id);
1630
3117
  if (!cell.raw.startsWith("=")) return;
1631
3118
  if (!this.document.cells[row.id]) this.document.cells[row.id] = {};
1632
- const deps = this.depGraph.getDependencies(cellKey);
1633
- const selfReference = deps.includes(cellKey) || deps.includes(`${col.key.toUpperCase()}:`);
1634
- const spilledKeys = this.depGraph.getSpilledKeys(cellKey);
3119
+ const qualified = this.qualify(cellKey2);
3120
+ const deps = this.depGraph.getDependencies(qualified);
3121
+ const selfReference = deps.includes(qualified) || deps.includes(columnDependencyKey(`${this.document.id}!${col.key}`));
3122
+ const spilledKeys = this.depGraph.getSpilledKeys(qualified);
1635
3123
  const ownSpill = deps.find((dep) => spilledKeys.includes(dep));
1636
- 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.";
1637
- this.clearSpill(cellKey.toUpperCase(), true);
3124
+ const errorMessage = selfReference ? "This formula can't reference its own cell." : ownSpill ? `This formula refers to ${ownSpill.slice(ownSpill.lastIndexOf("!") + 1)}, 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.";
3125
+ this.clearSpill(cellKey2.toUpperCase(), true);
1638
3126
  this.document.cells[row.id][col.id] = { ...cell, value: "#CYCLE!", error: "#CYCLE!", errorMessage };
1639
3127
  }
1640
- evalSingleCellByKey(cellKey) {
1641
- const match = cellKey.match(/^([A-Za-z]+)(\d+)$/);
3128
+ evalSingleCellByKey(cellKey2) {
3129
+ const match = cellKey2.match(/^([A-Za-z]+)(\d+)$/);
1642
3130
  if (!match) return;
1643
3131
  const colName = match[1].toUpperCase();
1644
3132
  const rowNum = parseInt(match[2], 10);
@@ -1723,7 +3211,7 @@ var SpreadsheetController = class {
1723
3211
  }
1724
3212
  if (reason) {
1725
3213
  this.clearSpill(anchor);
1726
- this.depGraph.setSpillEdges(anchor, [], blockers);
3214
+ this.depGraph.setSpillEdges(this.qualify(anchor), [], blockers.map((key) => this.qualify(key)));
1727
3215
  return reason;
1728
3216
  }
1729
3217
  const previous = this.spills.get(anchor);
@@ -1749,7 +3237,7 @@ var SpreadsheetController = class {
1749
3237
  this.spillCells.set(spillCellId(rowObj.id, column.id), display);
1750
3238
  if (this.spilledBy.get(key) !== anchor) {
1751
3239
  this.spilledBy.set(key, anchor);
1752
- this.spillChanges?.add(key);
3240
+ this.spillChanges?.add(this.qualify(key));
1753
3241
  }
1754
3242
  cells.push({ key, rowId: rowObj.id, colId: column.id });
1755
3243
  }
@@ -1762,7 +3250,7 @@ var SpreadsheetController = class {
1762
3250
  }
1763
3251
  }
1764
3252
  this.spills.set(anchor, { anchor, row, col, rows, cols, values: array.values, cells });
1765
- this.depGraph.setSpillEdges(anchor, cells.map((cell) => cell.key));
3253
+ this.depGraph.setSpillEdges(this.qualify(anchor), cells.map((cell) => this.qualify(cell.key)));
1766
3254
  return null;
1767
3255
  }
1768
3256
  /** Removes the array spilled from `anchor`. With `keepEdges`, its dependency edges stay. */
@@ -1772,13 +3260,13 @@ var SpreadsheetController = class {
1772
3260
  for (const cell of record.cells) this.forgetSpilledCell(anchor, cell);
1773
3261
  this.spills.delete(anchor);
1774
3262
  }
1775
- if (!keepEdges) this.depGraph.setSpillEdges(anchor, []);
3263
+ if (!keepEdges) this.depGraph.setSpillEdges(this.qualify(anchor), []);
1776
3264
  }
1777
3265
  forgetSpilledCell(anchor, cell) {
1778
3266
  if (this.spilledBy.get(cell.key) !== anchor) return;
1779
3267
  this.spilledBy.delete(cell.key);
1780
3268
  this.spillCells.delete(spillCellId(cell.rowId, cell.colId));
1781
- this.spillChanges?.add(cell.key);
3269
+ this.spillChanges?.add(this.qualify(cell.key));
1782
3270
  }
1783
3271
  resolveSpillByCoord(colStr, rowNum) {
1784
3272
  const record = this.spills.get(`${colStr}${rowNum}`.toUpperCase());
@@ -1832,6 +3320,8 @@ var SpreadsheetController = class {
1832
3320
  const col = this.document.columns.find((c) => c.key.toUpperCase() === colStr.toUpperCase());
1833
3321
  const row = this.document.rows[rowNum - 1];
1834
3322
  if (!col || !row) return 0;
3323
+ const override = this.valueOverride;
3324
+ if (override && override.row === rowNum - 1 && this.document.columns[override.col] === col) return override.value;
1835
3325
  return this.getCell(row.id, col.id).value ?? "";
1836
3326
  }
1837
3327
  resolveRangeByCoords(sCol, sRow, eCol, eRow) {
@@ -1846,9 +3336,14 @@ var SpreadsheetController = class {
1846
3336
  const colName = indexToColName(c);
1847
3337
  const col = this.document.columns.find((colDef) => colDef.key.toUpperCase() === colName);
1848
3338
  if (!col) continue;
3339
+ const override = this.valueOverride && this.document.columns[this.valueOverride.col] === col ? this.valueOverride : null;
1849
3340
  for (let r = minR; r <= maxR; r++) {
1850
3341
  const row = this.document.rows[r - 1];
1851
3342
  if (!row) continue;
3343
+ if (override && override.row === r - 1) {
3344
+ results.push(override.value);
3345
+ continue;
3346
+ }
1852
3347
  results.push(this.getCell(row.id, col.id).value ?? "");
1853
3348
  }
1854
3349
  }
@@ -1915,14 +3410,75 @@ var SpreadsheetController = class {
1915
3410
  const cell = this.getCell(rowObj.id, colObj.id);
1916
3411
  this.draftValue = initialValue !== void 0 ? initialValue : cell.raw;
1917
3412
  }
3413
+ /**
3414
+ * Commits the cell editor's draft (or `valueToCommit`). Input that breaks
3415
+ * the cell's data validation is not committed: the editor stays open and
3416
+ * `validationPrompt` holds the alert until `resolveValidationPrompt`.
3417
+ * Unchanged input is not validated.
3418
+ */
1918
3419
  commitEditing(valueToCommit) {
1919
- if (!this.editingCell) return;
3420
+ if (!this.editingCell) return { status: "none" };
3421
+ if (this.validationPrompt) return { status: "pending", prompt: this.validationPrompt };
1920
3422
  const val = valueToCommit !== void 0 ? valueToCommit : this.draftValue;
1921
- if (this.editingCell.rowId && this.editingCell.colId) {
1922
- this.setCellValue(this.editingCell.rowId, this.editingCell.colId, val);
3423
+ const { rowId, colId, row, col } = this.editingCell;
3424
+ if (rowId && colId) {
3425
+ const failed = this.checkInput(rowId, colId, row, col, val, true);
3426
+ if (failed) return failed;
3427
+ this.setCellValue(rowId, colId, val);
1923
3428
  }
1924
3429
  this.editingCell = null;
1925
3430
  this.draftValue = "";
3431
+ return { status: "committed" };
3432
+ }
3433
+ /**
3434
+ * Commits typed input to a cell (e.g. from the formula bar or a list
3435
+ * dropdown) with data validation, like `commitEditing` without an open
3436
+ * editor. `setCellValue` never validates (paste, import and code bypass
3437
+ * validation, as in Excel).
3438
+ */
3439
+ commitCellInput(rowId, colId, raw) {
3440
+ if (this.validationPrompt) return { status: "pending", prompt: this.validationPrompt };
3441
+ const row = this.document.rows.findIndex((r) => r.id === rowId);
3442
+ const col = this.document.columns.findIndex((c) => c.id === colId);
3443
+ if (row === -1 || col === -1) return { status: "none" };
3444
+ const failed = this.checkInput(rowId, colId, row, col, raw, false);
3445
+ if (failed) return failed;
3446
+ this.setCellValue(rowId, colId, raw);
3447
+ return { status: "committed" };
3448
+ }
3449
+ /** Validates input; when it fails, records and returns the prompt. */
3450
+ checkInput(rowId, colId, row, col, raw, editing) {
3451
+ if (!this.document.table?.dataValidations?.length) return null;
3452
+ if (this.getCell(rowId, colId).raw === raw) return null;
3453
+ const result = this.validateCellInput(row, col, raw);
3454
+ if (result.valid) return null;
3455
+ const prompt = { rowId, colId, row, col, raw, result, editing };
3456
+ this.validationPrompt = prompt;
3457
+ return { status: result.errorStyle === "stop" ? "rejected" : "confirm", prompt };
3458
+ }
3459
+ /**
3460
+ * Answers `validationPrompt`: `accept` keeps the input (warning and
3461
+ * information alerts only; for a stop alert it acts as `retry`), `retry`
3462
+ * returns to editing it, `cancel` discards it.
3463
+ */
3464
+ resolveValidationPrompt(action) {
3465
+ const prompt = this.validationPrompt;
3466
+ if (!prompt) return;
3467
+ this.validationPrompt = null;
3468
+ const editingSameCell = prompt.editing && this.editingCell?.rowId === prompt.rowId && this.editingCell?.colId === prompt.colId;
3469
+ if (action === "accept" && prompt.result.errorStyle !== "stop") {
3470
+ this.setCellValue(prompt.rowId, prompt.colId, prompt.raw);
3471
+ if (editingSameCell) {
3472
+ this.editingCell = null;
3473
+ this.draftValue = "";
3474
+ }
3475
+ } else if (action === "cancel") {
3476
+ if (editingSameCell) this.cancelEditing();
3477
+ } else if (editingSameCell) {
3478
+ this.draftValue = prompt.raw;
3479
+ } else {
3480
+ this.startEditing(prompt.row, prompt.col, prompt.raw);
3481
+ }
1926
3482
  }
1927
3483
  cancelEditing() {
1928
3484
  this.editingCell = null;
@@ -2027,20 +3583,36 @@ var SpreadsheetController = class {
2027
3583
  * column formula) so they keep pointing at the same cells.
2028
3584
  */
2029
3585
  shiftReferences(change) {
2030
- for (const rowCells of Object.values(this.document.cells)) {
2031
- for (const cell of Object.values(rowCells)) {
2032
- if (cell.raw?.startsWith("=")) {
2033
- cell.raw = shiftFormulaReferences(cell.raw, change);
3586
+ const changed = this.ctx;
3587
+ for (const rt of this.runtimes) {
3588
+ const filter = this.referencesTo(changed, rt);
3589
+ for (const rowCells of Object.values(rt.doc.cells)) {
3590
+ for (const cell of Object.values(rowCells)) {
3591
+ if (cell.raw?.startsWith("=")) {
3592
+ cell.raw = shiftFormulaReferences(cell.raw, change, filter);
3593
+ }
2034
3594
  }
2035
3595
  }
2036
- }
2037
- if (change.axis === "col") {
2038
- for (const col of this.document.columns) {
2039
- if (col.formula?.startsWith("=")) {
2040
- col.formula = shiftFormulaReferences(col.formula, change);
3596
+ if (change.axis === "col") {
3597
+ for (const col of rt.doc.columns) {
3598
+ if (col.formula?.startsWith("=")) {
3599
+ col.formula = shiftFormulaReferences(col.formula, change, filter);
3600
+ }
2041
3601
  }
2042
3602
  }
2043
3603
  }
3604
+ for (const n of this.names) {
3605
+ const scope = n.sheetId !== void 0 ? this.findRuntimeById(n.sheetId) : void 0;
3606
+ n.refersTo = shiftFormulaReferences(n.refersTo, change, {
3607
+ appliesTo: (sheet, sheetEnd) => sheetEnd === void 0 && (sheet === void 0 ? scope === changed : sameName3(sheet, changed.doc.name))
3608
+ });
3609
+ }
3610
+ }
3611
+ /** Filter for references, in formulas on sheet `on`, that point at sheet `target`. */
3612
+ referencesTo(target, on) {
3613
+ return {
3614
+ appliesTo: (sheet, sheetEnd) => sheetEnd === void 0 && (sheet === void 0 ? on === target : sameName3(sheet, target.doc.name))
3615
+ };
2044
3616
  }
2045
3617
  setColumnWidth(colId, width) {
2046
3618
  const col = this.document.columns.find((c) => c.id === colId);
@@ -2174,14 +3746,24 @@ var SpreadsheetController = class {
2174
3746
  */
2175
3747
  importFromCsv(csv, options = {}) {
2176
3748
  const matrix = parseCsv(csv, { delimiter: options.delimiter });
2177
- this.writeMatrix(matrix, options.startRow ?? 0, options.startCol ?? 0);
3749
+ this.onTargetSheet(options.sheet, () => this.writeMatrix(matrix, options.startRow ?? 0, options.startCol ?? 0));
3750
+ }
3751
+ /** Runs `fn` on the sheet `sheet` (id or name), default the active one. Throws for an unknown sheet. */
3752
+ onTargetSheet(sheet, fn) {
3753
+ if (sheet === void 0) {
3754
+ fn();
3755
+ return;
3756
+ }
3757
+ const rt = this.findRuntime(sheet);
3758
+ if (!rt) throw new Error(`There is no sheet "${sheet}".`);
3759
+ this.withSheet(rt, fn);
2178
3760
  }
2179
3761
  /**
2180
3762
  * Writes already-parsed rows of raw inputs (e.g. from an import adapter)
2181
3763
  * starting at (startRow, startCol), as a single undo step.
2182
3764
  */
2183
3765
  importRows(rows, options = {}) {
2184
- this.writeMatrix(rows, options.startRow ?? 0, options.startCol ?? 0);
3766
+ this.onTargetSheet(options.sheet, () => this.writeMatrix(rows, options.startRow ?? 0, options.startCol ?? 0));
2185
3767
  }
2186
3768
  /** The cells of `range` (or the whole sheet), row by row. */
2187
3769
  readMatrix(range) {
@@ -2271,10 +3853,14 @@ var SpreadsheetController = class {
2271
3853
  };
2272
3854
  /** Rewrites every formula so references follow cells that moved. */
2273
3855
  remapAllFormulas(moveCell) {
2274
- for (const rowCells of Object.values(this.document.cells)) {
2275
- for (const cell of Object.values(rowCells)) {
2276
- if (cell.raw?.startsWith("=")) {
2277
- cell.raw = remapFormulaReferences(cell.raw, moveCell);
3856
+ const moved = this.ctx;
3857
+ for (const rt of this.runtimes) {
3858
+ const filter = this.referencesTo(moved, rt);
3859
+ for (const rowCells of Object.values(rt.doc.cells)) {
3860
+ for (const cell of Object.values(rowCells)) {
3861
+ if (cell.raw?.startsWith("=")) {
3862
+ cell.raw = remapFormulaReferences(cell.raw, moveCell, filter);
3863
+ }
2278
3864
  }
2279
3865
  }
2280
3866
  }
@@ -2285,6 +3871,7 @@ var SpreadsheetController = class {
2285
3871
  * merges reduced to a single cell are dropped.
2286
3872
  */
2287
3873
  adjustTableForStructuralChange(change) {
3874
+ this.adjustRulesForStructuralChange(change);
2288
3875
  const merges = this.document.table?.merges;
2289
3876
  if (!merges?.length) return;
2290
3877
  const startKey = change.axis === "row" ? "startRow" : "startCol";
@@ -2806,10 +4393,361 @@ var SpreadsheetController = class {
2806
4393
  getViewModel() {
2807
4394
  return buildSpreadsheetView(this.document, this.getCellValueAt);
2808
4395
  }
4396
+ // --- Sheets ---
4397
+ /** The sheets in tab order. */
4398
+ getSheets() {
4399
+ return this.runtimes.map((rt, index) => ({ id: rt.doc.id, name: rt.doc.name, index, active: rt === this.active }));
4400
+ }
4401
+ /** Id of the active sheet (the one `document` is). */
4402
+ get activeSheetId() {
4403
+ return this.active.doc.id;
4404
+ }
4405
+ /** A sheet's document by id or name (live: don't mutate it directly). */
4406
+ getSheetDocument(idOrName) {
4407
+ return this.findRuntime(idOrName)?.doc;
4408
+ }
4409
+ /** Why `name` can't name a sheet (or null): see `validateSheetName`. `exceptSheet` is the sheet being renamed. */
4410
+ validateSheetName(name, exceptSheet) {
4411
+ const except = exceptSheet === void 0 ? void 0 : this.findRuntime(exceptSheet);
4412
+ return validateSheetName(
4413
+ name,
4414
+ this.runtimes.filter((rt) => rt !== except).map((rt) => rt.doc.name)
4415
+ );
4416
+ }
4417
+ /**
4418
+ * Shows another sheet (by id or name). The current edit is committed and
4419
+ * each sheet keeps its own selection. Returns false for an unknown sheet.
4420
+ */
4421
+ setActiveSheet(idOrName) {
4422
+ const rt = this.findRuntime(idOrName);
4423
+ if (!rt) return false;
4424
+ if (rt === this.active) return true;
4425
+ if (this.editingCell) this.commitEditing();
4426
+ this.activateRuntime(rt);
4427
+ this.notifyChange();
4428
+ return true;
4429
+ }
4430
+ activateRuntime(rt) {
4431
+ if (rt === this.active) return;
4432
+ if (this.runtimes.includes(this.active)) {
4433
+ this.active.activeCell = this.activeCell;
4434
+ this.active.selectedRange = this.selectedRange;
4435
+ }
4436
+ this.active = this.ctx = rt;
4437
+ this.activeCell = rt.activeCell;
4438
+ this.selectedRange = rt.selectedRange;
4439
+ this.editingCell = null;
4440
+ this.draftValue = "";
4441
+ this.onSelectionChange?.(this.activeCell, this.selectedRange);
4442
+ this.onActiveSheetChange?.(rt.doc.id);
4443
+ }
4444
+ /**
4445
+ * Adds a sheet (default: `Sheet N`, empty, after the last sheet) and
4446
+ * activates it unless `activate` is false. Returns its id. Throws when
4447
+ * `name` is invalid or taken. Undoable.
4448
+ */
4449
+ addSheet(options = {}) {
4450
+ const existing = this.runtimes.map((rt2) => rt2.doc.name);
4451
+ const name = options.name ?? options.document?.name ?? nextSheetName(existing);
4452
+ const invalid2 = validateSheetName(name, existing);
4453
+ if (invalid2) throw new Error(invalid2);
4454
+ this.recordWorkbookSnapshot();
4455
+ const id = options.document?.id && !this.findRuntimeById(options.document.id) ? options.document.id : this.generateSheetId();
4456
+ const doc = {
4457
+ columns: Array.from({ length: 4 }, (_, i) => ({
4458
+ id: `col_${String.fromCharCode(97 + i)}`,
4459
+ key: indexToColName(i),
4460
+ title: indexToColName(i),
4461
+ width: 120,
4462
+ type: "freeform"
4463
+ })),
4464
+ rows: Array.from({ length: 10 }, (_, i) => ({ id: `row_${i + 1}`, index: i, height: 32, type: "freeform" })),
4465
+ cells: {},
4466
+ createdAt: Date.now(),
4467
+ updatedAt: Date.now(),
4468
+ ...JSON.parse(JSON.stringify(options.document ?? {})),
4469
+ id,
4470
+ name
4471
+ };
4472
+ const rt = createRuntime(doc);
4473
+ const index = Math.max(0, Math.min(options.index ?? this.runtimes.length, this.runtimes.length));
4474
+ this.runtimes.splice(index, 0, rt);
4475
+ this.withSheet(rt, () => {
4476
+ this.recomputeRowIndices();
4477
+ this.rederiveTypedValues();
4478
+ });
4479
+ if (options.activate !== false) this.activateRuntime(rt);
4480
+ this.recalculateAll();
4481
+ this.notifyChange();
4482
+ return id;
4483
+ }
4484
+ /**
4485
+ * Renames a sheet. Formulas on every sheet and defined names that refer to
4486
+ * it follow (`=Old!A1` becomes `='New Name'!A1`). Throws when the name is
4487
+ * invalid or taken. Undoable.
4488
+ */
4489
+ renameSheet(idOrName, newName) {
4490
+ const rt = this.findRuntime(idOrName);
4491
+ if (!rt) throw new Error(`There is no sheet "${idOrName}".`);
4492
+ if (rt.doc.name === newName) return;
4493
+ const invalid2 = this.validateSheetName(newName, rt.doc.id);
4494
+ if (invalid2) throw new Error(invalid2);
4495
+ this.recordWorkbookSnapshot();
4496
+ const oldName = rt.doc.name;
4497
+ this.rewriteAllFormulas((formula) => renameSheetInFormula(formula, oldName, newName));
4498
+ rt.doc.name = newName;
4499
+ rt.doc.updatedAt = Date.now();
4500
+ this.recalculateAll();
4501
+ this.notifyChange();
4502
+ }
4503
+ /** Moves a sheet to position `toIndex` in tab order. Returns false for an unknown sheet. Undoable. */
4504
+ moveSheet(idOrName, toIndex) {
4505
+ const rt = this.findRuntime(idOrName);
4506
+ if (!rt) return false;
4507
+ const from = this.runtimes.indexOf(rt);
4508
+ const to = Math.max(0, Math.min(Math.floor(toIndex), this.runtimes.length - 1));
4509
+ if (from === to) return true;
4510
+ this.recordWorkbookSnapshot();
4511
+ this.runtimes.splice(from, 1);
4512
+ this.runtimes.splice(to, 0, rt);
4513
+ this.recalculateAll();
4514
+ this.notifyChange();
4515
+ return true;
4516
+ }
4517
+ /**
4518
+ * Deletes a sheet and its sheet-scoped names. References to it become
4519
+ * `#REF!` (a 3-D reference losing an end sheet shrinks). The last sheet
4520
+ * can't be deleted (returns false). Undoable.
4521
+ */
4522
+ deleteSheet(idOrName) {
4523
+ const rt = this.findRuntime(idOrName);
4524
+ if (!rt || this.runtimes.length <= 1) return false;
4525
+ this.recordWorkbookSnapshot();
4526
+ const order = this.runtimes.map((r) => r.doc.name);
4527
+ const index = this.runtimes.indexOf(rt);
4528
+ this.runtimes.splice(index, 1);
4529
+ this.names = this.names.filter((n) => n.sheetId !== rt.doc.id);
4530
+ this.rewriteAllFormulas((formula) => deleteSheetInFormula(formula, rt.doc.name, order));
4531
+ if (this.active === rt) {
4532
+ this.ctx = this.active;
4533
+ this.activateRuntime(this.runtimes[Math.min(index, this.runtimes.length - 1)]);
4534
+ } else if (this.ctx === rt) {
4535
+ this.ctx = this.active;
4536
+ }
4537
+ this.recalculateAll();
4538
+ this.notifyChange();
4539
+ return true;
4540
+ }
4541
+ /**
4542
+ * Copies a sheet (default name `Name (2)`, placed after it) with its
4543
+ * sheet-scoped names, which refer to the copy. Activates the copy unless
4544
+ * `activate` is false. Returns the new sheet's id, or null for an unknown
4545
+ * sheet. Undoable.
4546
+ */
4547
+ duplicateSheet(idOrName, options = {}) {
4548
+ const source = this.findRuntime(idOrName);
4549
+ if (!source) return null;
4550
+ const existing = this.runtimes.map((rt2) => rt2.doc.name);
4551
+ const name = options.name ?? copySheetName(source.doc.name, existing);
4552
+ const invalid2 = validateSheetName(name, existing);
4553
+ if (invalid2) throw new Error(invalid2);
4554
+ this.recordWorkbookSnapshot();
4555
+ const doc = JSON.parse(JSON.stringify(source.doc));
4556
+ doc.id = this.generateSheetId();
4557
+ doc.name = name;
4558
+ doc.createdAt = doc.updatedAt = Date.now();
4559
+ const copies = this.names.filter((n) => n.sheetId === source.doc.id).map((n) => ({ ...n, sheetId: doc.id, refersTo: renameSheetInFormula(n.refersTo, source.doc.name, name) }));
4560
+ this.names.push(...copies);
4561
+ const rt = createRuntime(doc);
4562
+ const index = Math.max(0, Math.min(options.index ?? this.runtimes.indexOf(source) + 1, this.runtimes.length));
4563
+ this.runtimes.splice(index, 0, rt);
4564
+ if (options.activate !== false) this.activateRuntime(rt);
4565
+ this.recalculateAll();
4566
+ this.notifyChange();
4567
+ return doc.id;
4568
+ }
4569
+ /** Applies `rewrite` to every formula: cells and column formulas on every sheet, and defined names. */
4570
+ rewriteAllFormulas(rewrite) {
4571
+ for (const rt of this.runtimes) {
4572
+ for (const [rowId, rowCells] of Object.entries(rt.doc.cells)) {
4573
+ for (const [colId, cell] of Object.entries(rowCells)) {
4574
+ if (!cell.raw?.startsWith("=")) continue;
4575
+ const raw = rewrite(cell.raw);
4576
+ if (raw !== cell.raw) rt.doc.cells[rowId][colId] = { ...cell, raw };
4577
+ }
4578
+ }
4579
+ for (const col of rt.doc.columns) {
4580
+ if (col.formula?.startsWith("=")) col.formula = rewrite(col.formula);
4581
+ }
4582
+ }
4583
+ for (const n of this.names) n.refersTo = rewrite(n.refersTo);
4584
+ }
4585
+ generateSheetId() {
4586
+ return `sheet_${Date.now()}_${(nextIdSequence++).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
4587
+ }
4588
+ // --- Defined names (Name Manager) ---
4589
+ /** Every defined name, workbook-scoped and sheet-scoped. */
4590
+ getNames() {
4591
+ return this.names.map((n) => {
4592
+ const scope = n.sheetId !== void 0 ? this.findRuntimeById(n.sheetId) : void 0;
4593
+ return {
4594
+ ...n,
4595
+ ...scope ? { sheetName: scope.doc.name } : {},
4596
+ isLambda: isLambdaDefinition(n)
4597
+ };
4598
+ });
4599
+ }
4600
+ /** The defined name `name` visible from the active sheet (its own sheet-scoped name first), or undefined. */
4601
+ getName(name, scope) {
4602
+ const rt = scope?.sheet !== void 0 ? this.findRuntime(scope.sheet) : void 0;
4603
+ const def = scope?.sheet !== void 0 ? rt ? this.names.find((n) => n.sheetId === rt.doc.id && sameName3(n.name, name)) : void 0 : this.findName(name, this.active);
4604
+ return def ? this.getNames().find((n) => n.name === def.name && n.sheetId === def.sheetId) : void 0;
4605
+ }
4606
+ /** Why `name` can't be a defined name (or null): see `validateDefinedName`. */
4607
+ validateDefinedName(name) {
4608
+ return validateDefinedName(name);
4609
+ }
4610
+ scopeRuntime(scope) {
4611
+ if (scope?.sheet === void 0) return void 0;
4612
+ const rt = this.findRuntime(scope.sheet);
4613
+ if (!rt) throw new Error(`There is no sheet "${scope.sheet}".`);
4614
+ return rt;
4615
+ }
4616
+ /**
4617
+ * Defines (or redefines) a name in the workbook, or on one sheet with
4618
+ * `sheet`. `refersTo` is a formula: a range (`=Data!$A$2:$A$20`), a
4619
+ * constant (`=0.07`), a formula or a LAMBDA (`=LAMBDA(x, x*2)`, called as
4620
+ * `=Double(4)`). Formulas using the name recalculate. Throws when the name
4621
+ * is invalid or the formula can't be read. Undoable.
4622
+ */
4623
+ defineName(name, refersTo, options = {}) {
4624
+ const invalid2 = validateDefinedName(name);
4625
+ if (invalid2) throw new Error(invalid2);
4626
+ const formula = refersTo.trim().startsWith("=") ? refersTo.trim() : `=${refersTo.trim()}`;
4627
+ try {
4628
+ parseFormula(formula);
4629
+ } catch (err) {
4630
+ throw new Error(`There's a problem with this formula: ${err?.message ?? formula}`);
4631
+ }
4632
+ const rt = this.scopeRuntime(options);
4633
+ this.recordWorkbookSnapshot();
4634
+ const sheetId = rt?.doc.id;
4635
+ const index = this.names.findIndex((n) => n.sheetId === sheetId && sameName3(n.name, name));
4636
+ const def = {
4637
+ name,
4638
+ refersTo: formula,
4639
+ ...sheetId !== void 0 ? { sheetId } : {},
4640
+ ...options.comment ? { comment: options.comment } : {}
4641
+ };
4642
+ if (index >= 0) this.names[index] = def;
4643
+ else this.names.push(def);
4644
+ this.recalculateAll();
4645
+ this.notifyChange();
4646
+ }
4647
+ /**
4648
+ * Renames a defined name. Formulas that use it (cells, column formulas and
4649
+ * other names) follow, on the sheets where the old name meant this one.
4650
+ * Throws when the name doesn't exist or the new name is invalid or taken.
4651
+ * Undoable.
4652
+ */
4653
+ renameName(oldName, newName, options = {}) {
4654
+ const rt = this.scopeRuntime(options);
4655
+ const sheetId = rt?.doc.id;
4656
+ const def = this.names.find((n) => n.sheetId === sheetId && sameName3(n.name, oldName));
4657
+ if (!def) throw new Error(`There is no name "${oldName}"${rt ? ` on sheet ${rt.doc.name}` : ""}.`);
4658
+ if (def.name === newName) return;
4659
+ const invalid2 = validateDefinedName(newName);
4660
+ if (invalid2) throw new Error(invalid2);
4661
+ if (!sameName3(oldName, newName) && this.names.some((n) => n.sheetId === sheetId && sameName3(n.name, newName))) {
4662
+ throw new Error("The name entered already exists. Enter a unique name.");
4663
+ }
4664
+ this.recordWorkbookSnapshot();
4665
+ const shadowedOn = new Set(this.names.filter((n) => n.sheetId !== void 0 && sameName3(n.name, oldName)).map((n) => n.sheetId));
4666
+ const appliesOn = (sheet) => sheetId !== void 0 ? sheet?.doc.id === sheetId : !sheet || !shadowedOn.has(sheet.doc.id);
4667
+ const appliesToSheetName = (sheetName) => appliesOn(this.findRuntimeByName(sheetName));
4668
+ for (const sheet of this.runtimes) {
4669
+ const options2 = { appliesTo: appliesToSheetName, qualifiedOnly: !appliesOn(sheet) };
4670
+ for (const [rowId, rowCells] of Object.entries(sheet.doc.cells)) {
4671
+ for (const [colId, cell] of Object.entries(rowCells)) {
4672
+ if (!cell.raw?.startsWith("=")) continue;
4673
+ const raw = renameNameInFormula(cell.raw, oldName, newName, options2);
4674
+ if (raw !== cell.raw) sheet.doc.cells[rowId][colId] = { ...cell, raw };
4675
+ }
4676
+ }
4677
+ for (const col of sheet.doc.columns) {
4678
+ if (col.formula?.startsWith("=")) col.formula = renameNameInFormula(col.formula, oldName, newName, options2);
4679
+ }
4680
+ }
4681
+ for (const n of this.names) {
4682
+ const scope = n.sheetId !== void 0 ? this.findRuntimeById(n.sheetId) : void 0;
4683
+ n.refersTo = renameNameInFormula(n.refersTo, oldName, newName, {
4684
+ appliesTo: appliesToSheetName,
4685
+ qualifiedOnly: scope ? !appliesOn(scope) : sheetId !== void 0
4686
+ });
4687
+ }
4688
+ def.name = newName;
4689
+ this.recalculateAll();
4690
+ this.notifyChange();
4691
+ }
4692
+ /**
4693
+ * Deletes a defined name. Formulas still using it show `#NAME?`. Returns
4694
+ * false when there is no such name. Undoable.
4695
+ */
4696
+ deleteName(name, options = {}) {
4697
+ const rt = this.scopeRuntime(options);
4698
+ const sheetId = rt?.doc.id;
4699
+ const index = this.names.findIndex((n) => n.sheetId === sheetId && sameName3(n.name, name));
4700
+ if (index < 0) return false;
4701
+ this.recordWorkbookSnapshot();
4702
+ this.names.splice(index, 1);
4703
+ this.recalculateAll();
4704
+ this.notifyChange();
4705
+ return true;
4706
+ }
4707
+ /**
4708
+ * Names formula autocomplete can offer on the active sheet: its
4709
+ * sheet-scoped names and the workbook names they don't hide.
4710
+ */
4711
+ getNameSuggestions() {
4712
+ const visible = /* @__PURE__ */ new Map();
4713
+ for (const n of this.names) {
4714
+ if (n.sheetId === this.active.doc.id) visible.set(n.name.toUpperCase(), n);
4715
+ }
4716
+ for (const n of this.names) {
4717
+ if (n.sheetId === void 0 && !visible.has(n.name.toUpperCase())) visible.set(n.name.toUpperCase(), n);
4718
+ }
4719
+ return Array.from(visible.values(), (n) => ({
4720
+ name: n.name,
4721
+ description: n.comment ? `${n.comment} (${n.refersTo})` : n.refersTo,
4722
+ isLambda: isLambdaDefinition(n)
4723
+ })).sort((a, b) => a.name.localeCompare(b.name));
4724
+ }
4725
+ /**
4726
+ * Evaluates a formula (e.g. `=Rate*2`, `=SUM(Sales)`) as if it were in a
4727
+ * cell of `sheet` (id or name; default the active sheet), without storing
4728
+ * it. Used by the Name Manager to show a name's current value.
4729
+ */
4730
+ evaluateFormula(formula, options = {}) {
4731
+ const rt = options.sheet === void 0 ? this.active : this.findRuntime(options.sheet);
4732
+ if (!rt) return { value: "#REF!", error: "#REF!", message: `There is no sheet "${options.sheet}".` };
4733
+ const text = formula.startsWith("=") ? formula : `=${formula}`;
4734
+ return this.withSheet(rt, () => this.evaluator.evaluateWithDiagnostics(text));
4735
+ }
2809
4736
  // --- Undo/Redo Snapshots ---
4737
+ //
4738
+ // Most steps snapshot one sheet (the one changed); sheet and name
4739
+ // operations snapshot the whole workbook. Undoing a sheet's step shows
4740
+ // that sheet.
2810
4741
  recordSnapshot() {
2811
4742
  if (this.isApplyingHistory || this.batching) return;
2812
- this.undoStack.push(JSON.stringify(this.document));
4743
+ this.pushUndo({ kind: "sheet", sheetId: this.ctx.doc.id, data: JSON.stringify(this.ctx.doc) });
4744
+ }
4745
+ recordWorkbookSnapshot() {
4746
+ if (this.isApplyingHistory || this.batching) return;
4747
+ this.pushUndo({ kind: "workbook", data: JSON.stringify(this.workbookSnapshot()) });
4748
+ }
4749
+ pushUndo(entry) {
4750
+ this.undoStack.push(entry);
2813
4751
  if (this.undoStack.length > 50) this.undoStack.shift();
2814
4752
  this.redoStack = [];
2815
4753
  this.trimHistory();
@@ -2817,18 +4755,16 @@ var SpreadsheetController = class {
2817
4755
  /** Drops the oldest snapshots beyond `maxHistoryBytes`, always keeping the newest. */
2818
4756
  trimHistory() {
2819
4757
  if (this.maxHistoryBytes <= 0 || this.undoStack.length <= 1) return;
2820
- let total = 0;
2821
- for (const s of this.undoStack) total += s.length * 2;
2822
- for (const s of this.redoStack) total += s.length * 2;
4758
+ let total = this.getHistoryByteSize();
2823
4759
  while (total > this.maxHistoryBytes && this.undoStack.length > 1) {
2824
- total -= this.undoStack.shift().length * 2;
4760
+ total -= this.undoStack.shift().data.length * 2;
2825
4761
  }
2826
4762
  }
2827
4763
  /** Approximate bytes held by the undo/redo snapshots (UTF-16 size). */
2828
4764
  getHistoryByteSize() {
2829
4765
  let total = 0;
2830
- for (const s of this.undoStack) total += s.length * 2;
2831
- for (const s of this.redoStack) total += s.length * 2;
4766
+ for (const s of this.undoStack) total += s.data.length * 2;
4767
+ for (const s of this.redoStack) total += s.data.length * 2;
2832
4768
  return total;
2833
4769
  }
2834
4770
  /** Number of undo / redo snapshots currently held. */
@@ -2836,32 +4772,340 @@ var SpreadsheetController = class {
2836
4772
  return { undo: this.undoStack.length, redo: this.redoStack.length };
2837
4773
  }
2838
4774
  undo() {
2839
- if (this.undoStack.length === 0) return;
4775
+ this.applyHistory(this.undoStack, this.redoStack);
4776
+ }
4777
+ redo() {
4778
+ this.applyHistory(this.redoStack, this.undoStack);
4779
+ }
4780
+ /** Restores the newest entry of `from`, saving the current state of the same scope on `to`. */
4781
+ applyHistory(from, to) {
4782
+ const entry = from.pop();
4783
+ if (!entry) return;
2840
4784
  this.isApplyingHistory = true;
2841
4785
  try {
2842
- this.redoStack.push(JSON.stringify(this.document));
2843
- this.document = JSON.parse(this.undoStack.pop());
4786
+ if (entry.kind === "sheet") {
4787
+ const rt = this.findRuntimeById(entry.sheetId);
4788
+ if (!rt) return;
4789
+ to.push({ kind: "sheet", sheetId: rt.doc.id, data: JSON.stringify(rt.doc) });
4790
+ rt.doc = JSON.parse(entry.data);
4791
+ if (rt !== this.active) {
4792
+ this.ctx = this.active;
4793
+ this.activateRuntime(rt);
4794
+ }
4795
+ } else {
4796
+ to.push({ kind: "workbook", data: JSON.stringify(this.workbookSnapshot()) });
4797
+ const previousActive = this.active.doc.id;
4798
+ const selection = { activeCell: this.activeCell, selectedRange: this.selectedRange };
4799
+ const saved = new Map(this.runtimes.map((rt) => [rt.doc.id, rt]));
4800
+ this.setWorkbookState(JSON.parse(entry.data), false);
4801
+ for (const rt of this.runtimes) {
4802
+ const old = saved.get(rt.doc.id);
4803
+ if (old) {
4804
+ rt.activeCell = old.activeCell;
4805
+ rt.selectedRange = old.selectedRange;
4806
+ }
4807
+ }
4808
+ if (this.active.doc.id === previousActive) {
4809
+ this.activeCell = selection.activeCell;
4810
+ this.selectedRange = selection.selectedRange;
4811
+ } else {
4812
+ this.activeCell = this.active.activeCell;
4813
+ this.selectedRange = this.active.selectedRange;
4814
+ this.onSelectionChange?.(this.activeCell, this.selectedRange);
4815
+ this.onActiveSheetChange?.(this.active.doc.id);
4816
+ }
4817
+ }
2844
4818
  this.recalculateAll();
2845
4819
  this.notifyChange();
2846
4820
  } finally {
2847
4821
  this.isApplyingHistory = false;
2848
4822
  }
2849
4823
  }
2850
- redo() {
2851
- if (this.redoStack.length === 0) return;
2852
- this.isApplyingHistory = true;
4824
+ notifyChange() {
4825
+ if (!this.cfIncremental) this.cfDirty = "all";
4826
+ this.invalidCircles = null;
4827
+ this.onDocumentChange?.(this.active.doc);
4828
+ if (this.onWorkbookChange) {
4829
+ const snapshot = this.workbookSnapshot();
4830
+ this.onWorkbookChange({ sheets: snapshot.sheets, activeSheetId: snapshot.activeSheetId, names: snapshot.names });
4831
+ }
4832
+ }
4833
+ // --- Conditional formatting and data validation ---
4834
+ /** The sheet as rule evaluation sees it. */
4835
+ ruleSheet() {
4836
+ return {
4837
+ rowCount: this.document.rows.length,
4838
+ colCount: this.document.columns.length,
4839
+ getValue: (row, col) => this.getCellValueAt(row, col) ?? "",
4840
+ getInput: (row, col) => {
4841
+ const r = this.document.rows[row];
4842
+ const c = this.document.columns[col];
4843
+ return r && c ? this.getCell(r.id, c.id).raw : "";
4844
+ },
4845
+ evaluateFormula: (formula, row, col, entered) => this.evaluateFormulaAt(formula, row, col, entered)
4846
+ };
4847
+ }
4848
+ /**
4849
+ * Evaluates `formula` as if it were in (row, col), without storing it:
4850
+ * relative references and `[property]` resolve from that cell. With
4851
+ * `entered`, that cell reads as the given value (input being validated).
4852
+ * Returns the value, or an array's values (column-major) for a range.
4853
+ */
4854
+ evaluateFormulaAt(formula, row, col, entered) {
4855
+ const saved = { rowIndex: this.evaluatingRowIndex, cell: this.evaluatingCell, override: this.valueOverride };
4856
+ this.evaluatingRowIndex = row;
4857
+ this.evaluatingCell = { col: this.document.columns[col]?.key ?? indexToColName(col), row: row + 1 };
4858
+ this.valueOverride = entered ?? null;
2853
4859
  try {
2854
- this.undoStack.push(JSON.stringify(this.document));
2855
- this.document = JSON.parse(this.redoStack.pop());
2856
- this.recalculateAll();
2857
- this.notifyChange();
4860
+ const result = this.evaluator.evaluateWithDiagnostics(formula.startsWith("=") ? formula : `=${formula}`);
4861
+ return result.array ? result.array.values : result.value;
2858
4862
  } finally {
2859
- this.isApplyingHistory = false;
4863
+ this.evaluatingRowIndex = saved.rowIndex;
4864
+ this.evaluatingCell = saved.cell;
4865
+ this.valueOverride = saved.override;
2860
4866
  }
2861
4867
  }
2862
- notifyChange() {
4868
+ /** Records that the value of `cellKey` (and of the array it spills) may have changed. */
4869
+ markValueChanged(cellKey2) {
4870
+ if (this.cfDirty === "all") return;
4871
+ this.cfDirty.add(cellKey2);
4872
+ const spill = this.spills.get(cellKey2);
4873
+ if (spill) for (const cell of spill.cells) this.cfDirty.add(cell.key);
4874
+ }
4875
+ generateRuleId(prefix) {
4876
+ return `${prefix}_${Date.now()}_${(nextIdSequence++).toString(36)}`;
4877
+ }
4878
+ /** Moves rule ranges and rule formulas with inserted / deleted rows and columns. */
4879
+ adjustRulesForStructuralChange(change) {
4880
+ const table = this.document.table;
4881
+ if (!table) return;
4882
+ const shiftIfFormula = (text) => text !== void 0 && text.trimStart().startsWith("=") ? shiftRuleFormula(text, change) : text;
4883
+ const shiftAnchor = (value) => value.type === "formula" ? { ...value, formula: shiftRuleFormula(value.formula, change) } : value;
4884
+ if (table.conditionalFormats?.length) {
4885
+ table.conditionalFormats = table.conditionalFormats.map((rule) => {
4886
+ const next = { ...rule, ranges: adjustRangesForStructuralChange(rule.ranges, change) };
4887
+ if (next.type === "expression") next.formula = shiftRuleFormula(next.formula, change);
4888
+ if (next.type === "cellIs") {
4889
+ next.value = shiftIfFormula(next.value) ?? "";
4890
+ if (next.value2 !== void 0) next.value2 = shiftIfFormula(next.value2);
4891
+ }
4892
+ if (next.type === "colorScale") next.stops = next.stops.map((s) => ({ ...s, value: shiftAnchor(s.value) }));
4893
+ if (next.type === "dataBar") {
4894
+ if (next.min) next.min = shiftAnchor(next.min);
4895
+ if (next.max) next.max = shiftAnchor(next.max);
4896
+ }
4897
+ if (next.type === "iconSet" && next.thresholds) next.thresholds = next.thresholds.map(shiftAnchor);
4898
+ return next;
4899
+ }).filter((rule) => rule.ranges.length > 0);
4900
+ }
4901
+ if (table.dataValidations?.length) {
4902
+ table.dataValidations = table.dataValidations.map((rule) => {
4903
+ const next = { ...rule, ranges: adjustRangesForStructuralChange(rule.ranges, change) };
4904
+ if (rule.formula1 !== void 0) next.formula1 = shiftIfFormula(rule.formula1);
4905
+ if (rule.formula2 !== void 0) next.formula2 = shiftIfFormula(rule.formula2);
4906
+ return next;
4907
+ }).filter((rule) => rule.ranges.length > 0);
4908
+ }
4909
+ }
4910
+ /** Conditional formats of the sheet, highest priority first. */
4911
+ getConditionalFormats() {
4912
+ return this.document.table?.conditionalFormats ?? [];
4913
+ }
4914
+ /**
4915
+ * Adds a conditional format (one undo step). New rules get the highest
4916
+ * priority, as in Excel; pass `priority: 'last'` to add it below the
4917
+ * others. Returns the rule id.
4918
+ */
4919
+ addConditionalFormat(rule, options = {}) {
4920
+ const id = rule.id || this.generateRuleId("cf");
4921
+ const stored = { ...rule, id, ranges: rule.ranges.map((r) => normalizeRange(r)) };
4922
+ this.applyTableChange(() => {
4923
+ const rules = [...this.table.conditionalFormats ?? []];
4924
+ if (options.priority === "last") rules.push(stored);
4925
+ else rules.unshift(stored);
4926
+ this.table.conditionalFormats = rules;
4927
+ });
4928
+ return id;
4929
+ }
4930
+ /** Changes a conditional format (its condition, ranges or stop-if-true). Returns false when not found. */
4931
+ updateConditionalFormat(id, changes) {
4932
+ const rules = this.document.table?.conditionalFormats ?? [];
4933
+ const index = rules.findIndex((r) => r.id === id);
4934
+ if (index === -1) return false;
4935
+ this.applyTableChange(() => {
4936
+ const next = [...rules];
4937
+ const updated = { ...rules[index], ...changes, id };
4938
+ if (changes.ranges) updated.ranges = changes.ranges.map((r) => normalizeRange(r));
4939
+ next[index] = updated;
4940
+ this.table.conditionalFormats = next;
4941
+ });
4942
+ return true;
4943
+ }
4944
+ /** Removes a conditional format. Returns false when not found. */
4945
+ deleteConditionalFormat(id) {
4946
+ const rules = this.document.table?.conditionalFormats ?? [];
4947
+ if (!rules.some((r) => r.id === id)) return false;
4948
+ this.applyTableChange(() => {
4949
+ this.table.conditionalFormats = rules.filter((r) => r.id !== id);
4950
+ });
4951
+ return true;
4952
+ }
4953
+ /**
4954
+ * Moves a conditional format to priority `to` (0 = highest), or by
4955
+ * `{ by: -1 }` (up) / `{ by: 1 }` (down). Returns false when nothing moved.
4956
+ */
4957
+ moveConditionalFormat(id, to) {
4958
+ const rules = this.document.table?.conditionalFormats ?? [];
4959
+ const from = rules.findIndex((r) => r.id === id);
4960
+ if (from === -1) return false;
4961
+ const target = Math.max(0, Math.min(rules.length - 1, typeof to === "number" ? to : from + to.by));
4962
+ if (target === from) return false;
4963
+ this.applyTableChange(() => {
4964
+ const next = [...rules];
4965
+ const [moved] = next.splice(from, 1);
4966
+ next.splice(target, 0, moved);
4967
+ this.table.conditionalFormats = next;
4968
+ });
4969
+ return true;
4970
+ }
4971
+ /**
4972
+ * Removes conditional formatting from the cells of `range` (rules keep
4973
+ * their other cells; rules left without cells are deleted), or every rule
4974
+ * when `range` is null.
4975
+ */
4976
+ clearConditionalFormats(range = null) {
4977
+ const rules = this.document.table?.conditionalFormats ?? [];
4978
+ if (rules.length === 0) return;
4979
+ this.applyTableChange(() => {
4980
+ this.table.conditionalFormats = range ? rules.map((rule) => ({ ...rule, ranges: subtractRangeFromRanges(rule.ranges, range) })).filter((rule) => rule.ranges.length > 0) : [];
4981
+ });
4982
+ }
4983
+ /**
4984
+ * The conditional look of every formatted cell. Evaluated on demand and
4985
+ * cached: after a cell edit only the affected cells are re-evaluated.
4986
+ */
4987
+ getConditionalFormatOverlay() {
4988
+ const rules = this.document.table?.conditionalFormats ?? [];
4989
+ if (rules.length === 0) {
4990
+ this.cfEngine = null;
4991
+ this.cfDirty = "all";
4992
+ return EMPTY_CONDITIONAL_FORMAT_OVERLAY;
4993
+ }
4994
+ const sheet = this.ruleSheet();
4995
+ if (!this.cfEngine || this.cfDirty === "all") {
4996
+ this.cfEngine = new ConditionalFormatEngine(sheet);
4997
+ this.cfEngine.evaluate(rules);
4998
+ } else if (this.cfDirty.size > 0) {
4999
+ const changed = [];
5000
+ for (const key of this.cfDirty) {
5001
+ const located = this.locateCellKey(key);
5002
+ if (located) changed.push({ row: located.row.index, col: this.document.columns.indexOf(located.col) });
5003
+ }
5004
+ this.cfEngine.setSheet(sheet);
5005
+ this.cfEngine.update(rules, changed);
5006
+ }
5007
+ this.cfDirty = /* @__PURE__ */ new Set();
5008
+ return this.cfEngine.overlay;
5009
+ }
5010
+ /** The conditional look of (row, col), if any. */
5011
+ getConditionalFormat(row, col) {
5012
+ return this.getConditionalFormatOverlay().get(row, col);
5013
+ }
5014
+ /** Data validation rules of the sheet. */
5015
+ getDataValidations() {
5016
+ return this.document.table?.dataValidations ?? [];
5017
+ }
5018
+ /** The rule that applies to (row, col), if any. */
5019
+ getDataValidationAt(row, col) {
5020
+ return findDataValidation(this.document.table?.dataValidations, row, col);
5021
+ }
5022
+ /**
5023
+ * Adds a data validation rule (one undo step). Its cells are first
5024
+ * removed from other rules, so each cell has at most one. Returns the id.
5025
+ */
5026
+ addDataValidation(rule) {
5027
+ const id = rule.id || this.generateRuleId("dv");
5028
+ const ranges = rule.ranges.map((r) => normalizeRange(r));
5029
+ this.applyTableChange(() => {
5030
+ let rules = this.table.dataValidations ?? [];
5031
+ for (const cut of ranges) {
5032
+ rules = rules.map((r) => ({ ...r, ranges: subtractRangeFromRanges(r.ranges, cut) })).filter((r) => r.ranges.length > 0);
5033
+ }
5034
+ this.table.dataValidations = [...rules, { ...rule, id, ranges }];
5035
+ });
5036
+ return id;
5037
+ }
5038
+ /** Changes a data validation rule. Returns false when not found. */
5039
+ updateDataValidation(id, changes) {
5040
+ const rules = this.document.table?.dataValidations ?? [];
5041
+ const index = rules.findIndex((r) => r.id === id);
5042
+ if (index === -1) return false;
5043
+ this.applyTableChange(() => {
5044
+ const next = [...rules];
5045
+ const updated = { ...rules[index], ...changes, id };
5046
+ if (changes.ranges) updated.ranges = changes.ranges.map((r) => normalizeRange(r));
5047
+ next[index] = updated;
5048
+ this.table.dataValidations = next;
5049
+ });
5050
+ return true;
5051
+ }
5052
+ /** Removes a data validation rule. Returns false when not found. */
5053
+ deleteDataValidation(id) {
5054
+ const rules = this.document.table?.dataValidations ?? [];
5055
+ if (!rules.some((r) => r.id === id)) return false;
5056
+ this.applyTableChange(() => {
5057
+ this.table.dataValidations = rules.filter((r) => r.id !== id);
5058
+ });
5059
+ return true;
5060
+ }
5061
+ /** Removes validation from the cells of `range`, or every rule when `range` is null. */
5062
+ clearDataValidation(range = null) {
5063
+ const rules = this.document.table?.dataValidations ?? [];
5064
+ if (rules.length === 0) return;
5065
+ this.applyTableChange(() => {
5066
+ this.table.dataValidations = range ? rules.map((r) => ({ ...r, ranges: subtractRangeFromRanges(r.ranges, range) })).filter((r) => r.ranges.length > 0) : [];
5067
+ });
5068
+ }
5069
+ /** Checks input for (row, col) against its data validation, without committing it. */
5070
+ validateCellInput(row, col, raw) {
5071
+ return validateCellInput(this.ruleSheet(), this.document.table?.dataValidations, row, col, raw);
5072
+ }
5073
+ /**
5074
+ * Dropdown items of a list-validated cell, or null when the cell has no
5075
+ * list rule (or its in-cell dropdown is off).
5076
+ */
5077
+ getValidationListValues(row, col) {
5078
+ const rule = this.getDataValidationAt(row, col);
5079
+ if (!rule || rule.type !== "list" || rule.inCellDropdown === false) return null;
5080
+ return getValidationListValues(this.ruleSheet(), rule);
5081
+ }
5082
+ /** Cells whose current value breaks their validation rule. */
5083
+ findInvalidCells() {
5084
+ return findInvalidCells(this.ruleSheet(), this.document.table?.dataValidations);
5085
+ }
5086
+ /**
5087
+ * Turns "circle invalid data" on or off. View state, not part of the
5088
+ * document (not undoable, not saved); listeners are notified so the grid
5089
+ * redraws.
5090
+ */
5091
+ setInvalidDataCircles(show) {
5092
+ if (this.circlesShown === show) return;
5093
+ this.circlesShown = show;
5094
+ this.invalidCircles = null;
2863
5095
  this.onDocumentChange?.(this.document);
2864
5096
  }
5097
+ /** Whether invalid cells are circled. */
5098
+ getInvalidDataCircles() {
5099
+ return this.circlesShown;
5100
+ }
5101
+ /** Whether (row, col) is circled as invalid (always false while circles are off). */
5102
+ isInvalidDataCircled(row, col) {
5103
+ if (!this.circlesShown) return false;
5104
+ if (!this.invalidCircles) {
5105
+ this.invalidCircles = new Set(this.findInvalidCells().map((c) => `${c.row}:${c.col}`));
5106
+ }
5107
+ return this.invalidCircles.has(`${row}:${col}`);
5108
+ }
2865
5109
  };
2866
5110
  function stepInOrder(order, from, delta) {
2867
5111
  if (order.length === 0) return from;
@@ -2875,7 +5119,11 @@ function stepInOrder(order, from, delta) {
2875
5119
  }
2876
5120
 
2877
5121
  // src/spreadsheet/menu.ts
2878
- function runSpreadsheetMenuItem(item, ask) {
5122
+ function runSpreadsheetMenuItem(item, ask, confirm) {
5123
+ if (item.confirm) {
5124
+ const confirmed = confirm ? confirm(item.confirm) : typeof window !== "undefined" && typeof window.confirm === "function" ? window.confirm(item.confirm) : true;
5125
+ if (!confirmed) return false;
5126
+ }
2879
5127
  if (!item.input) {
2880
5128
  item.run();
2881
5129
  return true;
@@ -3067,8 +5315,91 @@ function getSpreadsheetMenuItems(controller, cell) {
3067
5315
  run: () => controller.groupRowsBy(column.id)
3068
5316
  });
3069
5317
  }
5318
+ items.push(...getRuleMenuItems(controller, selection));
3070
5319
  return items;
3071
5320
  }
5321
+ function getRuleMenuItems(controller, selection) {
5322
+ const ranges = [selection];
5323
+ const hasFormats = controller.getConditionalFormats().some((rule) => rule.ranges.some((r) => rangesIntersect(r, selection)));
5324
+ const hasValidation = controller.getDataValidations().some((rule) => rule.ranges.some((r) => rangesIntersect(r, selection)));
5325
+ const add = (input) => {
5326
+ controller.addConditionalFormat(createConditionalFormatRule({ ...input, ranges }));
5327
+ };
5328
+ const circles = controller.getInvalidDataCircles();
5329
+ return [
5330
+ {
5331
+ id: "conditional-format-manage",
5332
+ label: "Conditional formatting\u2026",
5333
+ separatorBefore: true,
5334
+ panel: "conditionalFormatting",
5335
+ run: () => {
5336
+ }
5337
+ },
5338
+ {
5339
+ id: "conditional-format-greater-than",
5340
+ label: "Conditional formatting: Highlight cells greater than\u2026",
5341
+ input: { label: "Highlight cells greater than", defaultValue: "0" },
5342
+ run: (value) => {
5343
+ if (value !== void 0 && value.trim() !== "") add({ kind: "greaterThan", value: value.trim() });
5344
+ }
5345
+ },
5346
+ {
5347
+ id: "conditional-format-duplicates",
5348
+ label: "Conditional formatting: Highlight duplicate values",
5349
+ run: () => add({ kind: "duplicate" })
5350
+ },
5351
+ {
5352
+ id: "conditional-format-color-scale",
5353
+ label: "Conditional formatting: Color scale",
5354
+ run: () => add({ kind: "colorScale3" })
5355
+ },
5356
+ {
5357
+ id: "conditional-format-data-bars",
5358
+ label: "Conditional formatting: Data bars",
5359
+ run: () => add({ kind: "dataBar" })
5360
+ },
5361
+ {
5362
+ id: "conditional-format-icon-set",
5363
+ label: "Conditional formatting: Icon set",
5364
+ run: () => add({ kind: "iconSet", iconSet: "3Arrows" })
5365
+ },
5366
+ {
5367
+ id: "conditional-format-clear",
5368
+ label: "Clear conditional formatting from selection",
5369
+ disabled: !hasFormats,
5370
+ run: () => controller.clearConditionalFormats(selection)
5371
+ },
5372
+ {
5373
+ id: "data-validation-manage",
5374
+ label: "Data validation\u2026",
5375
+ separatorBefore: true,
5376
+ panel: "dataValidation",
5377
+ run: () => {
5378
+ }
5379
+ },
5380
+ {
5381
+ id: "data-validation-list",
5382
+ label: "Data validation: List of values\u2026",
5383
+ input: { label: "Allowed values, separated by commas (or a range such as =$H$1:$H$5)", defaultValue: "" },
5384
+ run: (value) => {
5385
+ if (value === void 0 || value.trim() === "") return;
5386
+ controller.addDataValidation({ ranges, type: "list", formula1: value.trim() });
5387
+ }
5388
+ },
5389
+ {
5390
+ id: "data-validation-clear",
5391
+ label: "Clear data validation from selection",
5392
+ disabled: !hasValidation,
5393
+ run: () => controller.clearDataValidation(selection)
5394
+ },
5395
+ {
5396
+ id: "data-validation-circles",
5397
+ label: circles ? "Clear validation circles" : "Circle invalid data",
5398
+ disabled: !circles && controller.getDataValidations().length === 0,
5399
+ run: () => controller.setInvalidDataCircles(!circles)
5400
+ }
5401
+ ];
5402
+ }
3072
5403
 
3073
5404
  // src/spreadsheet/actions.ts
3074
5405
  var SPREADSHEET_TABLE_ACTIONS = [
@@ -3108,7 +5439,28 @@ var SPREADSHEET_TABLE_ACTIONS = [
3108
5439
  "exportToCsv",
3109
5440
  "importFromCsv",
3110
5441
  "copyToClipboardData",
3111
- "pasteClipboardData"
5442
+ "pasteClipboardData",
5443
+ // Conditional formatting and data validation
5444
+ "getConditionalFormats",
5445
+ "addConditionalFormat",
5446
+ "updateConditionalFormat",
5447
+ "deleteConditionalFormat",
5448
+ "moveConditionalFormat",
5449
+ "clearConditionalFormats",
5450
+ "getConditionalFormatOverlay",
5451
+ "getConditionalFormat",
5452
+ "getDataValidations",
5453
+ "getDataValidationAt",
5454
+ "addDataValidation",
5455
+ "updateDataValidation",
5456
+ "deleteDataValidation",
5457
+ "clearDataValidation",
5458
+ "validateCellInput",
5459
+ "getValidationListValues",
5460
+ "findInvalidCells",
5461
+ "setInvalidDataCircles",
5462
+ "getInvalidDataCircles",
5463
+ "isInvalidDataCircled"
3112
5464
  ];
3113
5465
  function bindSpreadsheetTableActions(controller) {
3114
5466
  const actions = {};
@@ -3156,13 +5508,106 @@ function registerSpreadsheetImportAdapters(registry) {
3156
5508
  return () => unregisters.forEach((unregister) => unregister());
3157
5509
  }
3158
5510
  async function importFileIntoSpreadsheet(controller, input, options) {
3159
- const { registry, startRow, startCol, ...rest } = options;
5511
+ const { registry, startRow, startCol, sheet, ...rest } = options;
3160
5512
  const outcome = await registry.importFile(input, { ...rest, target: "spreadsheet" });
3161
- controller.importRows(outcome.result.rows, { startRow, startCol });
5513
+ controller.importRows(outcome.result.rows, { startRow, startCol, sheet });
3162
5514
  return outcome;
3163
5515
  }
3164
5516
 
5517
+ // src/spreadsheet/sheet-tabs.ts
5518
+ function getSheetTabMenuItems(controller, sheetId, options = {}) {
5519
+ const sheets = controller.getSheets();
5520
+ const sheet = sheets.find((s) => s.id === sheetId);
5521
+ if (!sheet) return [];
5522
+ const guard = (fn) => () => {
5523
+ try {
5524
+ fn();
5525
+ } catch (err) {
5526
+ if (!options.onError) throw err;
5527
+ options.onError(err?.message ?? String(err));
5528
+ }
5529
+ };
5530
+ return [
5531
+ {
5532
+ id: "sheet-insert",
5533
+ label: "Insert sheet",
5534
+ run: guard(() => {
5535
+ controller.addSheet({ index: sheet.index + 1 });
5536
+ })
5537
+ },
5538
+ {
5539
+ id: "sheet-rename",
5540
+ label: "Rename\u2026",
5541
+ input: { label: "Sheet name", defaultValue: sheet.name },
5542
+ run: (name) => guard(() => {
5543
+ const next = (name ?? "").trim();
5544
+ if (next && next !== sheet.name) controller.renameSheet(sheetId, next);
5545
+ })()
5546
+ },
5547
+ {
5548
+ id: "sheet-duplicate",
5549
+ label: "Duplicate",
5550
+ run: guard(() => {
5551
+ controller.duplicateSheet(sheetId);
5552
+ })
5553
+ },
5554
+ {
5555
+ id: "sheet-move-left",
5556
+ label: "Move left",
5557
+ disabled: sheet.index === 0,
5558
+ separatorBefore: true,
5559
+ run: () => {
5560
+ controller.moveSheet(sheetId, sheet.index - 1);
5561
+ }
5562
+ },
5563
+ {
5564
+ id: "sheet-move-right",
5565
+ label: "Move right",
5566
+ disabled: sheet.index === sheets.length - 1,
5567
+ run: () => {
5568
+ controller.moveSheet(sheetId, sheet.index + 1);
5569
+ }
5570
+ },
5571
+ {
5572
+ id: "sheet-delete",
5573
+ label: "Delete",
5574
+ disabled: sheets.length <= 1,
5575
+ separatorBefore: true,
5576
+ confirm: `Delete the sheet "${sheet.name}"? Formulas that refer to it will show #REF!.`,
5577
+ run: () => {
5578
+ controller.deleteSheet(sheetId);
5579
+ }
5580
+ }
5581
+ ];
5582
+ }
5583
+ function sheetDropIndex(fromIndex, targetIndex, dropAfter) {
5584
+ let to = dropAfter ? targetIndex + 1 : targetIndex;
5585
+ if (fromIndex < to) to--;
5586
+ return to;
5587
+ }
5588
+ function getNameManagerRows(controller) {
5589
+ return controller.getNames().map((n) => {
5590
+ let value;
5591
+ if (n.isLambda) {
5592
+ value = "[LAMBDA]";
5593
+ } else {
5594
+ const result = controller.evaluateFormula(`=${n.name}`, n.sheetId ? { sheet: n.sheetId } : {});
5595
+ value = result.array ? `{${result.array.rows}\xD7${result.array.cols} array}` : isFormulaError(result.value) ? String(result.value) : formatCellDisplay({ value: result.value });
5596
+ }
5597
+ return {
5598
+ name: n.name,
5599
+ refersTo: n.refersTo,
5600
+ scopeLabel: n.sheetName ?? "Workbook",
5601
+ ...n.sheetId ? { sheetId: n.sheetId } : {},
5602
+ value,
5603
+ ...n.comment ? { comment: n.comment } : {},
5604
+ isLambda: n.isLambda
5605
+ };
5606
+ }).sort((a, b) => a.name.localeCompare(b.name) || a.scopeLabel.localeCompare(b.scopeLabel));
5607
+ }
5608
+
3165
5609
  export {
5610
+ extractNameDependencies,
3166
5611
  columnDependencyKey,
3167
5612
  extractCellDependencies,
3168
5613
  DependencyGraph,
@@ -3170,6 +5615,13 @@ export {
3170
5615
  mapFormulaReferences,
3171
5616
  offsetFormulaReferences,
3172
5617
  remapFormulaReferences,
5618
+ sheetNameNeedsQuotes,
5619
+ quoteSheetName,
5620
+ formatSheetPrefix,
5621
+ renameSheetInFormula,
5622
+ deleteSheetInFormula,
5623
+ renameNameInFormula,
5624
+ formulaIdentifiers,
3173
5625
  getFormulaCaretContext,
3174
5626
  getFormulaHints,
3175
5627
  applyFormulaSuggestion,
@@ -3181,6 +5633,13 @@ export {
3181
5633
  formatCellDisplay,
3182
5634
  formatValueWithCode,
3183
5635
  DataSourceBinder,
5636
+ MAX_SHEET_NAME_LENGTH,
5637
+ MAX_DEFINED_NAME_LENGTH,
5638
+ validateSheetName,
5639
+ nextSheetName,
5640
+ copySheetName,
5641
+ validateDefinedName,
5642
+ isLambdaDefinition,
3184
5643
  normalizeRange,
3185
5644
  rangeContains,
3186
5645
  rangesIntersect,
@@ -3201,6 +5660,47 @@ export {
3201
5660
  createSpreadsheetClipboardData,
3202
5661
  readSpreadsheetClipboard,
3203
5662
  rebaseClipboardRows,
5663
+ adjustRangeForStructuralChange,
5664
+ adjustRangesForStructuralChange,
5665
+ subtractRange,
5666
+ subtractRangeFromRanges,
5667
+ rangesContain,
5668
+ ruleAnchor,
5669
+ ruleFormulaAt,
5670
+ shiftRuleFormula,
5671
+ forEachRangeCell,
5672
+ formatRuleRanges,
5673
+ parseRuleRanges,
5674
+ CONDITIONAL_FORMAT_ICON_SETS,
5675
+ conditionalFormatIconCount,
5676
+ getConditionalFormatIcon,
5677
+ conditionalFormatNumber,
5678
+ blendConditionalFormatColors,
5679
+ defaultIconThresholds,
5680
+ EMPTY_CONDITIONAL_FORMAT_OVERLAY,
5681
+ ConditionalFormatEngine,
5682
+ matchesTimePeriod,
5683
+ evaluateConditionalFormats,
5684
+ describeConditionalFormat,
5685
+ describeDataValidation,
5686
+ CONDITIONAL_FORMAT_STYLE_PRESETS,
5687
+ DEFAULT_CONDITIONAL_FORMAT_STYLE,
5688
+ CONDITIONAL_FORMAT_PRESETS,
5689
+ CONDITIONAL_FORMAT_TIME_PERIODS,
5690
+ createConditionalFormatRule,
5691
+ DATA_VALIDATION_TYPE_OPTIONS,
5692
+ DATA_VALIDATION_OPERATOR_OPTIONS,
5693
+ DATA_VALIDATION_ERROR_STYLE_OPTIONS,
5694
+ conditionalFormatContrastColor,
5695
+ getConditionalFormatCss,
5696
+ getDataBarCss,
5697
+ conditionalFormatStyleString,
5698
+ DEFAULT_DATA_VALIDATION_MESSAGE,
5699
+ DEFAULT_DATA_VALIDATION_TITLE,
5700
+ findDataValidation,
5701
+ getValidationListValues,
5702
+ validateCellInput,
5703
+ findInvalidCells,
3204
5704
  DEFAULT_ROW_HEIGHT,
3205
5705
  DEFAULT_COLUMN_WIDTH,
3206
5706
  getTableSections,
@@ -3218,6 +5718,9 @@ export {
3218
5718
  tsvImportAdapter,
3219
5719
  SPREADSHEET_IMPORT_ADAPTERS,
3220
5720
  registerSpreadsheetImportAdapters,
3221
- importFileIntoSpreadsheet
5721
+ importFileIntoSpreadsheet,
5722
+ getSheetTabMenuItems,
5723
+ sheetDropIndex,
5724
+ getNameManagerRows
3222
5725
  };
3223
- //# sourceMappingURL=chunk-U5KWZU4X.js.map
5726
+ //# sourceMappingURL=chunk-RR7F5GUM.js.map