@pixerate/editor 0.23.0 → 0.24.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 (155) hide show
  1. package/README.md +96 -1
  2. package/THIRD_PARTY_NOTICES.md +51 -0
  3. package/dist/agent/index.cjs +9 -8
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +4 -3
  6. package/dist/agent/index.d.ts +4 -3
  7. package/dist/agent/index.js +3 -2
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/canvas/index.cjs +1210 -17
  10. package/dist/canvas/index.cjs.map +1 -1
  11. package/dist/canvas/index.d.cts +318 -1
  12. package/dist/canvas/index.d.ts +318 -1
  13. package/dist/canvas/index.js +1199 -6
  14. package/dist/canvas/index.js.map +1 -1
  15. package/dist/{chunk-QBJ22YZR.js → chunk-3CN2WD3X.js} +1354 -206
  16. package/dist/chunk-3CN2WD3X.js.map +1 -0
  17. package/dist/{chunk-JJDLPEWL.js → chunk-3LV6RRPH.js} +2 -2
  18. package/dist/{chunk-MJZBNWG7.js → chunk-5S3Z4PPF.js} +78 -557
  19. package/dist/chunk-5S3Z4PPF.js.map +1 -0
  20. package/dist/chunk-664LQYFJ.js +115 -0
  21. package/dist/chunk-664LQYFJ.js.map +1 -0
  22. package/dist/{chunk-AA4ZEVCC.cjs → chunk-D27YFC46.cjs} +101 -580
  23. package/dist/chunk-D27YFC46.cjs.map +1 -0
  24. package/dist/chunk-D6FH2YAX.cjs +131 -0
  25. package/dist/chunk-D6FH2YAX.cjs.map +1 -0
  26. package/dist/{chunk-4SUNJZWB.cjs → chunk-FD4LPFAE.cjs} +48 -200
  27. package/dist/chunk-FD4LPFAE.cjs.map +1 -0
  28. package/dist/chunk-FODNLX5N.js +749 -0
  29. package/dist/chunk-FODNLX5N.js.map +1 -0
  30. package/dist/{chunk-OYXSKQFR.js → chunk-FPENXAEJ.js} +35 -187
  31. package/dist/chunk-FPENXAEJ.js.map +1 -0
  32. package/dist/chunk-HLIMTC6N.js +1 -0
  33. package/dist/chunk-HLIMTC6N.js.map +1 -0
  34. package/dist/{chunk-QGHGJ56O.js → chunk-ITBSKI26.js} +1483 -19
  35. package/dist/chunk-ITBSKI26.js.map +1 -0
  36. package/dist/{chunk-XDMXXC4Z.cjs → chunk-MA2F5KHF.cjs} +350 -227
  37. package/dist/chunk-MA2F5KHF.cjs.map +1 -0
  38. package/dist/{chunk-RIKBJLS2.cjs → chunk-MCSJFWAO.cjs} +1356 -208
  39. package/dist/chunk-MCSJFWAO.cjs.map +1 -0
  40. package/dist/chunk-MKA3AHLV.cjs +256 -0
  41. package/dist/chunk-MKA3AHLV.cjs.map +1 -0
  42. package/dist/chunk-OLZJXL4R.cjs +749 -0
  43. package/dist/chunk-OLZJXL4R.cjs.map +1 -0
  44. package/dist/chunk-OXSV64JX.js +689 -0
  45. package/dist/chunk-OXSV64JX.js.map +1 -0
  46. package/dist/chunk-P3RQ5UJE.cjs +689 -0
  47. package/dist/chunk-P3RQ5UJE.cjs.map +1 -0
  48. package/dist/chunk-P6ODPZ4W.js +256 -0
  49. package/dist/chunk-P6ODPZ4W.js.map +1 -0
  50. package/dist/chunk-PGNZKFQS.js +685 -0
  51. package/dist/chunk-PGNZKFQS.js.map +1 -0
  52. package/dist/chunk-PL4JB3BF.cjs +1 -0
  53. package/dist/chunk-PL4JB3BF.cjs.map +1 -0
  54. package/dist/{chunk-GVSMRPHB.cjs → chunk-SBCZ6RN5.cjs} +1568 -104
  55. package/dist/chunk-SBCZ6RN5.cjs.map +1 -0
  56. package/dist/{chunk-CL37JGFW.cjs → chunk-SML64VJG.cjs} +4 -4
  57. package/dist/{chunk-CL37JGFW.cjs.map → chunk-SML64VJG.cjs.map} +1 -1
  58. package/dist/chunk-TNNJXF46.cjs +115 -0
  59. package/dist/chunk-TNNJXF46.cjs.map +1 -0
  60. package/dist/chunk-U5HZSFCS.js +131 -0
  61. package/dist/chunk-U5HZSFCS.js.map +1 -0
  62. package/dist/{chunk-K3R5NG2D.js → chunk-U5KWZU4X.js} +251 -128
  63. package/dist/chunk-U5KWZU4X.js.map +1 -0
  64. package/dist/chunk-XNORY7M6.cjs +685 -0
  65. package/dist/chunk-XNORY7M6.cjs.map +1 -0
  66. package/dist/commands/index.d.cts +5 -353
  67. package/dist/commands/index.d.ts +5 -353
  68. package/dist/comment-store-BEMgVlAA.d.cts +135 -0
  69. package/dist/comment-store-BEMgVlAA.d.ts +135 -0
  70. package/dist/compare/index.cjs +15 -0
  71. package/dist/compare/index.cjs.map +1 -0
  72. package/dist/compare/index.d.cts +58 -0
  73. package/dist/compare/index.d.ts +58 -0
  74. package/dist/compare/index.js +15 -0
  75. package/dist/compare/index.js.map +1 -0
  76. package/dist/{controller-pePCJLT1.d.cts → controller-BhLLAoi8.d.cts} +45 -4
  77. package/dist/{controller-pePCJLT1.d.ts → controller-BhLLAoi8.d.ts} +45 -4
  78. package/dist/documents-BQjzJNe2.d.ts +76 -0
  79. package/dist/documents-KRTPhvst.d.cts +76 -0
  80. package/dist/extensions/index.cjs +53 -5
  81. package/dist/extensions/index.cjs.map +1 -1
  82. package/dist/extensions/index.d.cts +7 -3
  83. package/dist/extensions/index.d.ts +7 -3
  84. package/dist/extensions/index.js +52 -4
  85. package/dist/generation/index.cjs +1373 -0
  86. package/dist/generation/index.cjs.map +1 -0
  87. package/dist/generation/index.d.cts +554 -0
  88. package/dist/generation/index.d.ts +554 -0
  89. package/dist/generation/index.js +1373 -0
  90. package/dist/generation/index.js.map +1 -0
  91. package/dist/grammar/index.cjs +48 -3
  92. package/dist/grammar/index.cjs.map +1 -1
  93. package/dist/grammar/index.d.cts +322 -100
  94. package/dist/grammar/index.d.ts +322 -100
  95. package/dist/grammar/index.js +54 -9
  96. package/dist/image-editor/index.cjs +1831 -30
  97. package/dist/image-editor/index.cjs.map +1 -1
  98. package/dist/image-editor/index.d.cts +590 -3
  99. package/dist/image-editor/index.d.ts +590 -3
  100. package/dist/image-editor/index.js +1807 -6
  101. package/dist/image-editor/index.js.map +1 -1
  102. package/dist/{index-CCr_z9kF.d.cts → index-BhJBSm2d.d.cts} +370 -4
  103. package/dist/{index-Ck6ajwXh.d.ts → index-fJrlkJCd.d.ts} +370 -4
  104. package/dist/index.cjs +162 -25
  105. package/dist/index.cjs.map +1 -1
  106. package/dist/index.d.cts +14 -8
  107. package/dist/index.d.ts +14 -8
  108. package/dist/index.js +174 -37
  109. package/dist/index.js.map +1 -1
  110. package/dist/migrate-iTlm3a8h.d.cts +112 -0
  111. package/dist/migrate-iTlm3a8h.d.ts +112 -0
  112. package/dist/persistence/index.cjs +676 -0
  113. package/dist/persistence/index.cjs.map +1 -0
  114. package/dist/persistence/index.d.cts +347 -0
  115. package/dist/persistence/index.d.ts +347 -0
  116. package/dist/persistence/index.js +676 -0
  117. package/dist/persistence/index.js.map +1 -0
  118. package/dist/{registry-D5-R90p0.d.cts → registry-BDe-vlv3.d.ts} +2 -40
  119. package/dist/registry-D32LpIc4.d.cts +354 -0
  120. package/dist/{registry-D5-R90p0.d.ts → registry-DnzpgY27.d.cts} +2 -40
  121. package/dist/registry-evKTP4ls.d.ts +354 -0
  122. package/dist/schema-DYDGdYL9.d.cts +41 -0
  123. package/dist/schema-DYDGdYL9.d.ts +41 -0
  124. package/dist/serializers/index.cjs +4 -3
  125. package/dist/serializers/index.cjs.map +1 -1
  126. package/dist/serializers/index.d.cts +2 -1
  127. package/dist/serializers/index.d.ts +2 -1
  128. package/dist/serializers/index.js +3 -2
  129. package/dist/sourcemap-C8AH3MBQ.d.cts +91 -0
  130. package/dist/sourcemap-DA1Evhrv.d.ts +91 -0
  131. package/dist/spreadsheet/index.cjs +37 -3
  132. package/dist/spreadsheet/index.cjs.map +1 -1
  133. package/dist/spreadsheet/index.d.cts +188 -17
  134. package/dist/spreadsheet/index.d.ts +188 -17
  135. package/dist/spreadsheet/index.js +43 -9
  136. package/dist/{tokens-CyLZmIei.d.cts → tokens-D7yXvUU6.d.cts} +15 -2
  137. package/dist/{tokens-CyLZmIei.d.ts → tokens-D7yXvUU6.d.ts} +15 -2
  138. package/dist/{track-changes-BSF58RK0.d.ts → track-changes-BK5sVf4_.d.cts} +18 -136
  139. package/dist/{track-changes-BSF58RK0.d.cts → track-changes-CDjCrwKe.d.ts} +18 -136
  140. package/package.json +16 -1
  141. package/dist/chunk-4SUNJZWB.cjs.map +0 -1
  142. package/dist/chunk-AA4ZEVCC.cjs.map +0 -1
  143. package/dist/chunk-AXV2ZW34.cjs +0 -118
  144. package/dist/chunk-AXV2ZW34.cjs.map +0 -1
  145. package/dist/chunk-B2ZRLN4S.js +0 -118
  146. package/dist/chunk-B2ZRLN4S.js.map +0 -1
  147. package/dist/chunk-GVSMRPHB.cjs.map +0 -1
  148. package/dist/chunk-K3R5NG2D.js.map +0 -1
  149. package/dist/chunk-MJZBNWG7.js.map +0 -1
  150. package/dist/chunk-OYXSKQFR.js.map +0 -1
  151. package/dist/chunk-QBJ22YZR.js.map +0 -1
  152. package/dist/chunk-QGHGJ56O.js.map +0 -1
  153. package/dist/chunk-RIKBJLS2.cjs.map +0 -1
  154. package/dist/chunk-XDMXXC4Z.cjs.map +0 -1
  155. /package/dist/{chunk-JJDLPEWL.js.map → chunk-3LV6RRPH.js.map} +0 -0
@@ -1,15 +1,23 @@
1
+ import {
2
+ parseCsv,
3
+ parseTsv,
4
+ serializeCsv,
5
+ serializeTsv
6
+ } from "./chunk-664LQYFJ.js";
1
7
  import {
2
8
  FormulaEvaluator,
9
+ NumberFormat,
3
10
  WHOLE_COLUMN_END_ROW,
4
11
  colNameToIndex,
5
12
  compareValues,
6
13
  describeFormulaError,
7
- formatCellDisplay,
14
+ formatTextFunction,
8
15
  getFormulaArgumentSpecIndex,
9
16
  getFormulaFunction,
10
17
  getFormulaSignature,
11
18
  getFormulaSignatureParts,
12
19
  indexToColName,
20
+ isBlank,
13
21
  isFormulaError,
14
22
  numericValue,
15
23
  parseCellInput,
@@ -17,8 +25,9 @@ import {
17
25
  parseIsoDate,
18
26
  searchFormulaFunctions,
19
27
  sortCompareValues,
28
+ toText,
20
29
  tokenizeFormula
21
- } from "./chunk-QBJ22YZR.js";
30
+ } from "./chunk-3CN2WD3X.js";
22
31
 
23
32
  // src/spreadsheet/formula/dependency-graph.ts
24
33
  var columnDependencyKey = (col) => `${col.toUpperCase()}:`;
@@ -566,6 +575,68 @@ function applyFormulaSuggestion(input, caret, name) {
566
575
  return { value, caret: start + insert.length + (hasParen ? 1 : 0) };
567
576
  }
568
577
 
578
+ // src/spreadsheet/numfmt/presets.ts
579
+ var NUMBER_FORMAT_PRESETS = [
580
+ { id: "general", label: "General", code: "General" },
581
+ { id: "number", label: "Number (0.00)", code: "0.00" },
582
+ { id: "currency", label: "Currency ($1,234.50)", code: '"$"#,##0.00' },
583
+ { id: "percent", label: "Percent (12.5%)", code: "0.0%" },
584
+ { id: "date", label: "Date (3/14/2026)", code: "m/d/yyyy" },
585
+ { id: "time", label: "Time (1:30 PM)", code: "h:mm AM/PM" }
586
+ ];
587
+ function numberFormatForCellFormat(format) {
588
+ switch (format) {
589
+ case "currency":
590
+ return '"$"#,##0.00';
591
+ case "percentage":
592
+ return "0%";
593
+ case "date":
594
+ return "m/d/yyyy";
595
+ default:
596
+ return void 0;
597
+ }
598
+ }
599
+ function cellFormatForNumberFormat(code) {
600
+ switch (NumberFormat.parseCached(code).kind()) {
601
+ case "number":
602
+ case "fraction":
603
+ case "scientific":
604
+ case "special":
605
+ return "number";
606
+ case "currency":
607
+ case "accounting":
608
+ return "currency";
609
+ case "percentage":
610
+ return "percentage";
611
+ case "date":
612
+ return "date";
613
+ case "text":
614
+ return "text";
615
+ default:
616
+ return void 0;
617
+ }
618
+ }
619
+ var isGeneralNumberFormat = (code) => code === void 0 || NumberFormat.parseCached(code).isGeneral();
620
+
621
+ // src/spreadsheet/formula/format.ts
622
+ function formatCellForDisplay(cell) {
623
+ if (!cell) return { text: "", numeric: false };
624
+ if (cell.error) return { text: cell.error, numeric: false };
625
+ const { value } = cell;
626
+ const code = cell.numberFormat ?? (typeof value === "number" ? numberFormatForCellFormat(cell.format) : void 0);
627
+ if (code !== void 0 && !isGeneralNumberFormat(code) && !isBlank(value) && typeof value !== "boolean") {
628
+ return NumberFormat.parseCached(code).format(value);
629
+ }
630
+ return { text: toText(value), numeric: typeof value === "number" };
631
+ }
632
+ function formatCellDisplay(cell) {
633
+ return formatCellForDisplay(cell).text;
634
+ }
635
+ function formatValueWithCode(value, format) {
636
+ const result = formatTextFunction(value, format);
637
+ return "text" in result ? result.text : null;
638
+ }
639
+
569
640
  // src/spreadsheet/datasource/index.ts
570
641
  var DataSourceBinder = class {
571
642
  dataSources = /* @__PURE__ */ new Map();
@@ -912,113 +983,6 @@ function getFillDirection(source, target) {
912
983
  return null;
913
984
  }
914
985
 
915
- // src/spreadsheet/csv.ts
916
- var BOM = "\uFEFF";
917
- function parseCsv(text, options = {}) {
918
- const delimiter = options.delimiter ?? ",";
919
- if (delimiter.length !== 1 || delimiter === '"' || delimiter === "\n" || delimiter === "\r") {
920
- throw new Error(`Invalid CSV delimiter: ${JSON.stringify(delimiter)}`);
921
- }
922
- const input = text.startsWith(BOM) ? text.slice(1) : text;
923
- const records = [];
924
- let record = [];
925
- let field = "";
926
- let i = 0;
927
- let recordStarted = false;
928
- const endField = () => {
929
- record.push(field);
930
- field = "";
931
- };
932
- const endRecord = () => {
933
- endField();
934
- records.push(record);
935
- record = [];
936
- recordStarted = false;
937
- };
938
- let atFieldStart = true;
939
- while (i < input.length) {
940
- const char = input[i];
941
- if (char === '"' && atFieldStart) {
942
- const quoted = readQuoted(input, i, delimiter);
943
- if (quoted.wellFormed || !options.requireWellFormedQuotes) {
944
- recordStarted = true;
945
- atFieldStart = false;
946
- field += quoted.value;
947
- i = quoted.end;
948
- continue;
949
- }
950
- }
951
- if (char === delimiter) {
952
- recordStarted = true;
953
- endField();
954
- atFieldStart = true;
955
- i++;
956
- continue;
957
- }
958
- if (char === "\r" || char === "\n") {
959
- endRecord();
960
- atFieldStart = true;
961
- i += char === "\r" && input[i + 1] === "\n" ? 2 : 1;
962
- continue;
963
- }
964
- recordStarted = true;
965
- atFieldStart = false;
966
- field += char;
967
- i++;
968
- }
969
- if (recordStarted || field !== "" || record.length > 0) endRecord();
970
- return records;
971
- }
972
- function readQuoted(input, start, delimiter) {
973
- let value = "";
974
- let i = start + 1;
975
- while (i < input.length) {
976
- if (input[i] === '"') {
977
- if (input[i + 1] === '"') {
978
- value += '"';
979
- i += 2;
980
- continue;
981
- }
982
- const next = input[i + 1];
983
- const wellFormed = next === void 0 || next === delimiter || next === "\r" || next === "\n";
984
- return { value, end: i + 1, wellFormed };
985
- }
986
- value += input[i];
987
- i++;
988
- }
989
- return { value, end: i, wellFormed: false };
990
- }
991
- function escapeCsvField(value, delimiter = ",", quoteAll = false) {
992
- if (quoteAll || value.includes(delimiter) || /["\r\n]/.test(value)) {
993
- return `"${value.replace(/"/g, '""')}"`;
994
- }
995
- return value;
996
- }
997
- function serializeCsv(rows, options = {}) {
998
- const delimiter = options.delimiter ?? ",";
999
- const lineEnding = options.lineEnding ?? "\r\n";
1000
- const body = rows.map(
1001
- (row) => row.map((value) => escapeCsvField(value == null ? "" : String(value), delimiter, options.quoteAll)).join(delimiter)
1002
- ).join(lineEnding);
1003
- return options.bom ? BOM + body : body;
1004
- }
1005
- function parseTsv(text) {
1006
- const normalized = text.startsWith(BOM) ? text.slice(1) : text;
1007
- if (!normalized.includes('"')) {
1008
- const body = normalized.replace(/\r\n?/g, "\n").replace(/\n$/, "");
1009
- return body === "" ? [] : body.split("\n").map((line) => line.split(" "));
1010
- }
1011
- return parseCsv(normalized, { delimiter: " ", requireWellFormedQuotes: true });
1012
- }
1013
- function serializeTsv(rows) {
1014
- return rows.map(
1015
- (row) => row.map((value) => {
1016
- const text = value == null ? "" : String(value);
1017
- return /[\t\r\n]/.test(text) || text.startsWith('"') ? `"${text.replace(/"/g, '""')}"` : text;
1018
- }).join(" ")
1019
- ).join("\n");
1020
- }
1021
-
1022
986
  // src/spreadsheet/clipboard.ts
1023
987
  var SPREADSHEET_CLIPBOARD_MIME = "application/x.pixerate.spreadsheet+json";
1024
988
  var displayText = (value) => value === null || value === void 0 ? "" : String(value);
@@ -1422,7 +1386,7 @@ var SpreadsheetController = class {
1422
1386
  const cell = cells[col.id];
1423
1387
  if (!cell || typeof cell.raw !== "string" || cell.raw.startsWith("=")) continue;
1424
1388
  if (col.type === "bound" && col.binding && row.recordId) continue;
1425
- const typed = this.typedCell(cell.raw, cell.format);
1389
+ const typed = this.typedCell(cell.raw, cell.format, col, cell);
1426
1390
  cell.value = typed.value;
1427
1391
  if (typed.numberFormat) cell.numberFormat = typed.numberFormat;
1428
1392
  else delete cell.numberFormat;
@@ -1436,14 +1400,31 @@ var SpreadsheetController = class {
1436
1400
  * `CellFormat` to use: the automatic one (currency, percentage, date,
1437
1401
  * number) when `format` is unset or `text` (the default of new columns).
1438
1402
  */
1439
- typedCell(raw, format) {
1403
+ typedCell(raw, format, col, existing) {
1440
1404
  const parsed = parseCellInput(raw);
1405
+ const fixed = this.fixedNumberFormat(col, existing);
1406
+ const numberFormat = fixed ?? parsed.numberFormat;
1441
1407
  return {
1442
1408
  value: parsed.value,
1443
- ...parsed.numberFormat ? { numberFormat: parsed.numberFormat } : {},
1444
- format: parsed.cellFormat && (!format || format === "text") ? parsed.cellFormat : format
1409
+ ...numberFormat ? { numberFormat } : {},
1410
+ ...existing?.explicitNumberFormat ? { explicitNumberFormat: true } : {},
1411
+ format: fixed === void 0 && parsed.cellFormat && (!format || format === "text") ? parsed.cellFormat : format
1445
1412
  };
1446
1413
  }
1414
+ /**
1415
+ * The number format a cell keeps whatever is typed into it: its explicit
1416
+ * one, else its column's. Undefined lets typed input choose one.
1417
+ */
1418
+ fixedNumberFormat(col, existing) {
1419
+ if (existing?.explicitNumberFormat && existing.numberFormat !== void 0) return existing.numberFormat;
1420
+ return col?.numberFormat && !isGeneralNumberFormat(col.numberFormat) ? col.numberFormat : void 0;
1421
+ }
1422
+ /** `numberFormat` / `explicitNumberFormat` fields for a formula, bound or spilled cell. */
1423
+ fixedNumberFormatFields(col, existing) {
1424
+ const numberFormat = this.fixedNumberFormat(col, existing);
1425
+ if (numberFormat === void 0) return {};
1426
+ return existing?.explicitNumberFormat ? { numberFormat, explicitNumberFormat: true } : { numberFormat };
1427
+ }
1447
1428
  /**
1448
1429
  * Typed input for a bound field, coerced by the field's declared type:
1449
1430
  * number, boolean and date fields get a number, boolean or ISO date when
@@ -1501,10 +1482,13 @@ var SpreadsheetController = class {
1501
1482
  const spilled = this.spillCells.get(spillCellId(rowId, colId));
1502
1483
  if (spilled) return spilled;
1503
1484
  }
1504
- if (existing) return existing;
1485
+ if (existing && !existing.explicitNumberFormat) return existing;
1505
1486
  const col = this.document.columns.find((c) => c.id === colId);
1506
1487
  const row = this.document.rows.find((r) => r.id === rowId);
1507
- return this.getDerivedCell(row, col);
1488
+ const derived = this.getDerivedCell(row, col);
1489
+ if (!existing) return derived;
1490
+ if (derived.raw === "") return existing;
1491
+ return { ...derived, numberFormat: existing.numberFormat, explicitNumberFormat: true };
1508
1492
  }
1509
1493
  /** Value of a cell with no stored data: bound record value, column formula, or blank. */
1510
1494
  getDerivedCell(row, col) {
@@ -1517,17 +1501,19 @@ var SpreadsheetController = class {
1517
1501
  return {
1518
1502
  raw: boundVal !== void 0 ? String(boundVal) : "",
1519
1503
  value: boundVal ?? "",
1520
- format: col.format
1504
+ format: col.format,
1505
+ ...this.fixedNumberFormatFields(col, void 0)
1521
1506
  };
1522
1507
  }
1523
1508
  if (col && col.type === "formula" && col.formula) {
1524
1509
  return {
1525
1510
  raw: col.formula,
1526
1511
  value: "",
1527
- format: col.format
1512
+ format: col.format,
1513
+ ...this.fixedNumberFormatFields(col, void 0)
1528
1514
  };
1529
1515
  }
1530
- return { raw: "", value: "", format: col?.format };
1516
+ return { raw: "", value: "", format: col?.format, ...this.fixedNumberFormatFields(col, void 0) };
1531
1517
  }
1532
1518
  setCellValue(rowId, colId, raw) {
1533
1519
  this.recordSnapshot();
@@ -1537,15 +1523,16 @@ var SpreadsheetController = class {
1537
1523
  const col = this.document.columns.find((c) => c.id === colId);
1538
1524
  const row = this.document.rows.find((r) => r.id === rowId);
1539
1525
  const isFormula = raw.startsWith("=");
1526
+ const existing = this.document.cells[rowId][colId];
1540
1527
  let cell;
1541
1528
  if (col?.type === "bound" && col.binding && row?.recordId) {
1542
1529
  const value = this.boundInputValue(col.binding.dataSource, col.binding.field, raw);
1543
1530
  this.binder.commitBoundValue(col.binding.dataSource, row.recordId, col.binding.field, value);
1544
- cell = { raw, value, format: col.format };
1531
+ cell = { raw, value, format: col.format, ...this.fixedNumberFormatFields(col, existing) };
1545
1532
  } else if (isFormula) {
1546
- cell = { raw, value: raw, format: col?.format };
1533
+ cell = { raw, value: raw, format: col?.format, ...this.fixedNumberFormatFields(col, existing) };
1547
1534
  } else {
1548
- cell = { raw, ...this.typedCell(raw, col?.format) };
1535
+ cell = { raw, ...this.typedCell(raw, col?.format, col, existing) };
1549
1536
  }
1550
1537
  const cellKey = this.getCellKey(rowId, colId);
1551
1538
  this.depGraph.setCellFormula(cellKey, isFormula ? raw : null);
@@ -1748,7 +1735,13 @@ var SpreadsheetController = class {
1748
1735
  const rowObj = this.document.rows[row + r];
1749
1736
  const key = `${column.key}${row + r + 1}`.toUpperCase();
1750
1737
  const value = array.values[c * rows + r] ?? "";
1751
- const display = { raw: "", value, format: column.format, spillAnchor: anchor };
1738
+ const display = {
1739
+ raw: "",
1740
+ value,
1741
+ format: column.format,
1742
+ spillAnchor: anchor,
1743
+ ...this.fixedNumberFormatFields(column, this.document.cells[rowObj.id]?.[column.id])
1744
+ };
1752
1745
  if (isFormulaError(value)) {
1753
1746
  display.error = value;
1754
1747
  display.errorMessage = describeFormulaError(value);
@@ -1973,6 +1966,7 @@ var SpreadsheetController = class {
1973
1966
  binding: config?.binding,
1974
1967
  formula: config?.formula,
1975
1968
  format: config?.format || "text",
1969
+ ...config?.numberFormat ? { numberFormat: config.numberFormat } : {},
1976
1970
  readOnly: config?.readOnly
1977
1971
  };
1978
1972
  this.shiftReferences({ axis: "col", index, type: "insert" });
@@ -2460,6 +2454,94 @@ var SpreadsheetController = class {
2460
2454
  this.table[key] = value;
2461
2455
  });
2462
2456
  }
2457
+ // --- Number formats ---
2458
+ //
2459
+ // Cells are replaced, never mutated in place: framework bindings compare
2460
+ // cell objects by identity to decide what to re-render.
2461
+ /**
2462
+ * Sets an explicit number format code (e.g. `0.00`, `"$"#,##0.00`,
2463
+ * `m/d/yyyy`, `[Red]0;[Blue]-0`) on every cell in `range`, including blank
2464
+ * cells, so values typed later keep it. `null` removes explicit formats: the
2465
+ * cells go back to their column's format or the one chosen for typed input.
2466
+ * Undoable as one step. Returns false when there is no range.
2467
+ */
2468
+ setCellNumberFormat(range, code) {
2469
+ if (!range) return false;
2470
+ const r = normalizeRange(range);
2471
+ this.recordSnapshot();
2472
+ const kind = code === null ? void 0 : cellFormatForNumberFormat(code);
2473
+ for (let ri = r.startRow; ri <= r.endRow; ri++) {
2474
+ const row = this.document.rows[ri];
2475
+ if (!row) continue;
2476
+ for (let ci = r.startCol; ci <= r.endCol; ci++) {
2477
+ const col = this.document.columns[ci];
2478
+ if (!col) continue;
2479
+ const cells = this.document.cells[row.id] ??= {};
2480
+ const existing = cells[col.id];
2481
+ if (code === null) {
2482
+ if (!existing?.explicitNumberFormat) continue;
2483
+ if (existing.raw === "" && existing.value === "" && !existing.error) {
2484
+ delete cells[col.id];
2485
+ } else {
2486
+ const { explicitNumberFormat: _explicit, ...rest } = existing;
2487
+ cells[col.id] = this.withNumberFormat(rest, this.implicitNumberFormat(rest, col, row));
2488
+ }
2489
+ } else {
2490
+ cells[col.id] = {
2491
+ ...existing ?? { raw: "", value: "" },
2492
+ format: kind ?? existing?.format ?? col.format,
2493
+ numberFormat: code,
2494
+ explicitNumberFormat: true
2495
+ };
2496
+ }
2497
+ this.refreshSpilledNumberFormat(row, col);
2498
+ }
2499
+ }
2500
+ this.document.updatedAt = Date.now();
2501
+ this.notifyChange();
2502
+ return true;
2503
+ }
2504
+ /**
2505
+ * Sets the number format code of a column (`null` or `General` clears it).
2506
+ * Its cells without an explicit format of their own use it, including
2507
+ * values typed later; when cleared they go back to the format chosen for
2508
+ * typed input. Undoable.
2509
+ */
2510
+ setColumnNumberFormat(colId, code) {
2511
+ const col = this.getColumn(colId);
2512
+ if (!col) return;
2513
+ this.recordSnapshot();
2514
+ if (code === null || isGeneralNumberFormat(code)) delete col.numberFormat;
2515
+ else col.numberFormat = code;
2516
+ for (const row of this.document.rows) {
2517
+ const cells = this.document.cells[row.id];
2518
+ const cell = cells?.[col.id];
2519
+ if (cells && cell && !cell.explicitNumberFormat) {
2520
+ cells[col.id] = this.withNumberFormat(cell, this.implicitNumberFormat(cell, col, row));
2521
+ }
2522
+ this.refreshSpilledNumberFormat(row, col);
2523
+ }
2524
+ this.document.updatedAt = Date.now();
2525
+ this.notifyChange();
2526
+ }
2527
+ /** The column's format, or the one typed input chose (typed cells only). */
2528
+ implicitNumberFormat(cell, col, row) {
2529
+ const typed = cell.raw !== "" && !cell.raw.startsWith("=") && !(col.type === "bound" && col.binding && row.recordId);
2530
+ return this.fixedNumberFormat(col, void 0) ?? (typed ? parseCellInput(cell.raw).numberFormat : void 0);
2531
+ }
2532
+ /** A copy of `cell` with `numberFormat` set, or removed when undefined. */
2533
+ withNumberFormat(cell, numberFormat) {
2534
+ const { numberFormat: _old, ...rest } = cell;
2535
+ return numberFormat === void 0 ? rest : { ...rest, numberFormat };
2536
+ }
2537
+ /** Re-applies the stored cell's (or the column's) format to a spilled value shown there. */
2538
+ refreshSpilledNumberFormat(row, col) {
2539
+ const id = spillCellId(row.id, col.id);
2540
+ const spilled = this.spillCells.get(id);
2541
+ if (!spilled) return;
2542
+ const { numberFormat: _old, explicitNumberFormat: _explicit, ...rest } = spilled;
2543
+ this.spillCells.set(id, { ...rest, ...this.fixedNumberFormatFields(col, this.document.cells[row.id]?.[col.id]) });
2544
+ }
2463
2545
  // --- Hide / unhide ---
2464
2546
  hideRows(indices) {
2465
2547
  this.setHidden("row", indices, true);
@@ -2793,6 +2875,17 @@ function stepInOrder(order, from, delta) {
2793
2875
  }
2794
2876
 
2795
2877
  // src/spreadsheet/menu.ts
2878
+ function runSpreadsheetMenuItem(item, ask) {
2879
+ if (!item.input) {
2880
+ item.run();
2881
+ return true;
2882
+ }
2883
+ const prompt = ask ?? ((label, value) => typeof window !== "undefined" && window.prompt ? window.prompt(label, value) : null);
2884
+ const text = prompt(item.input.label, item.input.defaultValue ?? "");
2885
+ if (text === null || text === void 0) return false;
2886
+ item.run(text);
2887
+ return true;
2888
+ }
2796
2889
  function clampMenuPosition(point, size, viewport, margin = 4) {
2797
2890
  const place = (start, length, limit) => {
2798
2891
  if (start + length <= limit - margin) return Math.max(margin, start);
@@ -2927,6 +3020,31 @@ function getSpreadsheetMenuItems(controller, cell) {
2927
3020
  }
2928
3021
  }
2929
3022
  ];
3023
+ const currentFormat = cellData.numberFormat;
3024
+ const presetMatch = NUMBER_FORMAT_PRESETS.find(
3025
+ (p) => p.id === "general" ? isGeneralNumberFormat(currentFormat) : p.code === currentFormat
3026
+ );
3027
+ NUMBER_FORMAT_PRESETS.forEach((preset, i) => {
3028
+ items.push({
3029
+ id: `number-format-${preset.id}`,
3030
+ label: `Format cells: ${preset.label}`,
3031
+ separatorBefore: i === 0,
3032
+ checked: presetMatch === preset,
3033
+ run: () => {
3034
+ controller.setCellNumberFormat(selection, preset.code);
3035
+ }
3036
+ });
3037
+ });
3038
+ items.push({
3039
+ id: "number-format-custom",
3040
+ label: "Format cells: Custom\u2026",
3041
+ checked: !presetMatch,
3042
+ input: { label: "Number format code, e.g. #,##0.00;[Red](#,##0.00)", defaultValue: currentFormat ?? "General" },
3043
+ run: (code) => {
3044
+ if (code === void 0) return;
3045
+ controller.setCellNumberFormat(selection, code.trim() === "" ? null : code);
3046
+ }
3047
+ });
2930
3048
  if (grouping) {
2931
3049
  items.push(
2932
3050
  {
@@ -2985,6 +3103,8 @@ var SPREADSHEET_TABLE_ACTIONS = [
2985
3103
  "toggleGroupCollapsed",
2986
3104
  "getGroups",
2987
3105
  "transposeRange",
3106
+ "setCellNumberFormat",
3107
+ "setColumnNumberFormat",
2988
3108
  "exportToCsv",
2989
3109
  "importFromCsv",
2990
3110
  "copyToClipboardData",
@@ -3053,6 +3173,13 @@ export {
3053
3173
  getFormulaCaretContext,
3054
3174
  getFormulaHints,
3055
3175
  applyFormulaSuggestion,
3176
+ NUMBER_FORMAT_PRESETS,
3177
+ numberFormatForCellFormat,
3178
+ cellFormatForNumberFormat,
3179
+ isGeneralNumberFormat,
3180
+ formatCellForDisplay,
3181
+ formatCellDisplay,
3182
+ formatValueWithCode,
3056
3183
  DataSourceBinder,
3057
3184
  normalizeRange,
3058
3185
  rangeContains,
@@ -3068,11 +3195,6 @@ export {
3068
3195
  fillSeries,
3069
3196
  computeFillTarget,
3070
3197
  getFillDirection,
3071
- parseCsv,
3072
- escapeCsvField,
3073
- serializeCsv,
3074
- parseTsv,
3075
- serializeTsv,
3076
3198
  SPREADSHEET_CLIPBOARD_MIME,
3077
3199
  serializeHtmlTable,
3078
3200
  parseHtmlTable,
@@ -3087,6 +3209,7 @@ export {
3087
3209
  computeRowGroups,
3088
3210
  buildSpreadsheetView,
3089
3211
  SpreadsheetController,
3212
+ runSpreadsheetMenuItem,
3090
3213
  clampMenuPosition,
3091
3214
  getSpreadsheetMenuItems,
3092
3215
  SPREADSHEET_TABLE_ACTIONS,
@@ -3097,4 +3220,4 @@ export {
3097
3220
  registerSpreadsheetImportAdapters,
3098
3221
  importFileIntoSpreadsheet
3099
3222
  };
3100
- //# sourceMappingURL=chunk-K3R5NG2D.js.map
3223
+ //# sourceMappingURL=chunk-U5KWZU4X.js.map