@particle-academy/fancy-sheets 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -123,12 +123,41 @@ function lexFormula(input) {
123
123
  if (ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_") {
124
124
  const pos = i;
125
125
  i++;
126
- while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9" || input[i] === "_")) i++;
127
- const word = input.slice(pos, i);
126
+ while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9" || input[i] === "_" || input[i] === " ")) {
127
+ if (input[i] === " ") {
128
+ let lookAhead = i + 1;
129
+ while (lookAhead < len && input[lookAhead] === " ") lookAhead++;
130
+ if (lookAhead < len && (input[lookAhead] >= "A" && input[lookAhead] <= "Z" || input[lookAhead] >= "a" && input[lookAhead] <= "z" || input[lookAhead] >= "0" && input[lookAhead] <= "9" || input[lookAhead] === "!")) {
131
+ i++;
132
+ continue;
133
+ }
134
+ break;
135
+ }
136
+ i++;
137
+ }
138
+ let word = input.slice(pos, i).trimEnd();
139
+ i = pos + word.length;
128
140
  if (word.toUpperCase() === "TRUE" || word.toUpperCase() === "FALSE") {
129
141
  tokens.push({ type: "boolean", value: word.toUpperCase(), position: pos });
130
142
  continue;
131
143
  }
144
+ if (i < len && input[i] === "!") {
145
+ const sheetName = word;
146
+ i++;
147
+ const refStart = i;
148
+ while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9")) i++;
149
+ const ref1 = input.slice(refStart, i);
150
+ if (i < len && input[i] === ":") {
151
+ i++;
152
+ const ref2Start = i;
153
+ while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9")) i++;
154
+ const ref2 = input.slice(ref2Start, i);
155
+ tokens.push({ type: "sheetRangeRef", value: sheetName + "!" + ref1 + ":" + ref2, position: pos });
156
+ } else {
157
+ tokens.push({ type: "sheetCellRef", value: sheetName + "!" + ref1.toUpperCase(), position: pos });
158
+ }
159
+ continue;
160
+ }
132
161
  if (i < len && input[i] === ":") {
133
162
  const colonPos = i;
134
163
  i++;
@@ -272,6 +301,19 @@ function parseFormula(tokens) {
272
301
  const parts = t.value.split(":");
273
302
  return { type: "rangeRef", start: parts[0], end: parts[1] };
274
303
  }
304
+ if (t.type === "sheetCellRef") {
305
+ advance();
306
+ const bangIdx = t.value.indexOf("!");
307
+ return { type: "sheetCellRef", sheet: t.value.slice(0, bangIdx), address: t.value.slice(bangIdx + 1).toUpperCase() };
308
+ }
309
+ if (t.type === "sheetRangeRef") {
310
+ advance();
311
+ const bangIdx = t.value.indexOf("!");
312
+ const sheetName = t.value.slice(0, bangIdx);
313
+ const rangePart = t.value.slice(bangIdx + 1);
314
+ const parts = rangePart.split(":");
315
+ return { type: "sheetRangeRef", sheet: sheetName, start: parts[0].toUpperCase(), end: parts[1].toUpperCase() };
316
+ }
275
317
  if (t.type === "function") {
276
318
  const name = advance().value;
277
319
  expect("paren", "(");
@@ -1005,7 +1047,7 @@ registerFunction("TYPE", (args) => {
1005
1047
  });
1006
1048
 
1007
1049
  // src/engine/formula/evaluator.ts
1008
- function evaluateAST(node, getCellValue, getRangeValues) {
1050
+ function evaluateAST(node, getCellValue, getRangeValues, ctx) {
1009
1051
  switch (node.type) {
1010
1052
  case "number":
1011
1053
  return node.value;
@@ -1015,9 +1057,19 @@ function evaluateAST(node, getCellValue, getRangeValues) {
1015
1057
  return node.value;
1016
1058
  case "cellRef":
1017
1059
  return getCellValue(node.address);
1018
- case "rangeRef":
1060
+ case "rangeRef": {
1019
1061
  const vals = getRangeValues(node.start, node.end);
1020
1062
  return vals[0] ?? null;
1063
+ }
1064
+ case "sheetCellRef": {
1065
+ if (!ctx?.getSheetCellValue) return "#REF!";
1066
+ return ctx.getSheetCellValue(node.sheet, node.address);
1067
+ }
1068
+ case "sheetRangeRef": {
1069
+ if (!ctx?.getSheetRangeValues) return "#REF!";
1070
+ const vals = ctx.getSheetRangeValues(node.sheet, node.start, node.end);
1071
+ return vals[0] ?? null;
1072
+ }
1021
1073
  case "functionCall": {
1022
1074
  const entry = getFunction(node.name);
1023
1075
  if (!entry) return `#NAME?`;
@@ -1025,7 +1077,11 @@ function evaluateAST(node, getCellValue, getRangeValues) {
1025
1077
  if (arg.type === "rangeRef") {
1026
1078
  return getRangeValues(arg.start, arg.end);
1027
1079
  }
1028
- const val = evaluateAST(arg, getCellValue, getRangeValues);
1080
+ if (arg.type === "sheetRangeRef") {
1081
+ if (!ctx?.getSheetRangeValues) return ["#REF!"];
1082
+ return ctx.getSheetRangeValues(arg.sheet, arg.start, arg.end);
1083
+ }
1084
+ const val = evaluateAST(arg, getCellValue, getRangeValues, ctx);
1029
1085
  return [val];
1030
1086
  });
1031
1087
  try {
@@ -1035,8 +1091,8 @@ function evaluateAST(node, getCellValue, getRangeValues) {
1035
1091
  }
1036
1092
  }
1037
1093
  case "binaryOp": {
1038
- const left = evaluateAST(node.left, getCellValue, getRangeValues);
1039
- const right = evaluateAST(node.right, getCellValue, getRangeValues);
1094
+ const left = evaluateAST(node.left, getCellValue, getRangeValues, ctx);
1095
+ const right = evaluateAST(node.right, getCellValue, getRangeValues, ctx);
1040
1096
  const lNum = typeof left === "number" ? left : Number(left);
1041
1097
  const rNum = typeof right === "number" ? right : Number(right);
1042
1098
  switch (node.operator) {
@@ -1069,7 +1125,7 @@ function evaluateAST(node, getCellValue, getRangeValues) {
1069
1125
  }
1070
1126
  }
1071
1127
  case "unaryOp": {
1072
- const operand = evaluateAST(node.operand, getCellValue, getRangeValues);
1128
+ const operand = evaluateAST(node.operand, getCellValue, getRangeValues, ctx);
1073
1129
  const num = typeof operand === "number" ? operand : Number(operand);
1074
1130
  if (isNaN(num)) return "#VALUE!";
1075
1131
  return node.operator === "-" ? -num : num;
@@ -1183,7 +1239,7 @@ function getRecalculationOrder(graph) {
1183
1239
  function recalculateWorkbook(workbook) {
1184
1240
  return {
1185
1241
  ...workbook,
1186
- sheets: workbook.sheets.map(recalculateSheet)
1242
+ sheets: workbook.sheets.map((s) => recalculateSheet(s, workbook.sheets))
1187
1243
  };
1188
1244
  }
1189
1245
  function createInitialState(data) {
@@ -1213,7 +1269,7 @@ function pushUndo(state) {
1213
1269
  if (stack.length > 50) stack.shift();
1214
1270
  return { undoStack: stack, redoStack: [] };
1215
1271
  }
1216
- function recalculateSheet(sheet) {
1272
+ function recalculateSheet(sheet, allSheets) {
1217
1273
  const graph = buildDependencyGraph(sheet.cells);
1218
1274
  if (graph.size === 0) return sheet;
1219
1275
  const circular = detectCircularRefs(graph);
@@ -1229,6 +1285,28 @@ function recalculateSheet(sheet) {
1229
1285
  const addresses = expandRange(startAddr, endAddr);
1230
1286
  return addresses.map(getCellValue);
1231
1287
  };
1288
+ const getSheetCellValue = (sheetName, addr) => {
1289
+ if (!allSheets) return "#REF!";
1290
+ const target = allSheets.find((s) => s.name === sheetName || s.id === sheetName);
1291
+ if (!target) return "#REF!";
1292
+ const c = target.cells[addr];
1293
+ if (!c) return null;
1294
+ if (c.formula && c.computedValue !== void 0) return c.computedValue;
1295
+ return c.value;
1296
+ };
1297
+ const getSheetRangeValues = (sheetName, startAddr, endAddr) => {
1298
+ if (!allSheets) return [];
1299
+ const target = allSheets.find((s) => s.name === sheetName || s.id === sheetName);
1300
+ if (!target) return [];
1301
+ const addresses = expandRange(startAddr, endAddr);
1302
+ return addresses.map((a) => {
1303
+ const c = target.cells[a];
1304
+ if (!c) return null;
1305
+ if (c.formula && c.computedValue !== void 0) return c.computedValue;
1306
+ return c.value;
1307
+ });
1308
+ };
1309
+ const ctx = { getSheetCellValue, getSheetRangeValues };
1232
1310
  for (const addr of order) {
1233
1311
  const cell = cells[addr];
1234
1312
  if (!cell?.formula) continue;
@@ -1239,7 +1317,7 @@ function recalculateSheet(sheet) {
1239
1317
  try {
1240
1318
  const tokens = lexFormula(cell.formula);
1241
1319
  const ast = parseFormula(tokens);
1242
- const result = evaluateAST(ast, getCellValue, getRangeValues);
1320
+ const result = evaluateAST(ast, getCellValue, getRangeValues, ctx);
1243
1321
  cells[addr] = { ...cell, computedValue: result };
1244
1322
  } catch {
1245
1323
  cells[addr] = { ...cell, computedValue: "#ERROR!" };
@@ -1264,7 +1342,7 @@ function reducer(state, action) {
1264
1342
  if (existing?.format) cellData.format = existing.format;
1265
1343
  const workbook = updateActiveSheet(state, (s) => {
1266
1344
  const updated = { ...s, cells: { ...s.cells, [action.address]: cellData } };
1267
- return recalculateSheet(updated);
1345
+ return recalculateSheet(updated, state.workbook.sheets);
1268
1346
  });
1269
1347
  return { ...state, workbook, ...history };
1270
1348
  }
@@ -1420,6 +1498,20 @@ function reducer(state, action) {
1420
1498
  selection: { activeCell: "A1", ranges: [{ start: "A1", end: "A1" }] },
1421
1499
  editingCell: null
1422
1500
  };
1501
+ case "SET_FROZEN_ROWS": {
1502
+ const workbook = updateActiveSheet(state, (s) => ({
1503
+ ...s,
1504
+ frozenRows: Math.max(0, action.count)
1505
+ }));
1506
+ return { ...state, workbook };
1507
+ }
1508
+ case "SET_FROZEN_COLS": {
1509
+ const workbook = updateActiveSheet(state, (s) => ({
1510
+ ...s,
1511
+ frozenCols: Math.max(0, action.count)
1512
+ }));
1513
+ return { ...state, workbook };
1514
+ }
1423
1515
  case "UNDO": {
1424
1516
  if (state.undoStack.length === 0) return state;
1425
1517
  const prev = state.undoStack[state.undoStack.length - 1];
@@ -1464,6 +1556,8 @@ function useSpreadsheetStore(initialData) {
1464
1556
  addSheet: () => dispatch({ type: "ADD_SHEET" }),
1465
1557
  renameSheet: (sheetId, name) => dispatch({ type: "RENAME_SHEET", sheetId, name }),
1466
1558
  deleteSheet: (sheetId) => dispatch({ type: "DELETE_SHEET", sheetId }),
1559
+ setFrozenRows: (count) => dispatch({ type: "SET_FROZEN_ROWS", count }),
1560
+ setFrozenCols: (count) => dispatch({ type: "SET_FROZEN_COLS", count }),
1467
1561
  setActiveSheet: (sheetId) => dispatch({ type: "SET_ACTIVE_SHEET", sheetId }),
1468
1562
  undo: () => dispatch({ type: "UNDO" }),
1469
1563
  redo: () => dispatch({ type: "REDO" }),
@@ -1552,11 +1646,49 @@ function RowHeader({ rowIndex }) {
1552
1646
  );
1553
1647
  }
1554
1648
  RowHeader.displayName = "RowHeader";
1649
+ var EXCEL_EPOCH2 = new Date(1899, 11, 30).getTime();
1650
+ function serialToDateStr(serial) {
1651
+ const d = new Date(EXCEL_EPOCH2 + Math.floor(serial) * 864e5);
1652
+ const y = d.getFullYear();
1653
+ const m = String(d.getMonth() + 1).padStart(2, "0");
1654
+ const day = String(d.getDate()).padStart(2, "0");
1655
+ return `${y}-${m}-${day}`;
1656
+ }
1657
+ function serialToDateTimeStr(serial) {
1658
+ const date = serialToDateStr(serial);
1659
+ const fraction = serial % 1;
1660
+ const totalSeconds = Math.round(fraction * 86400);
1661
+ const h = String(Math.floor(totalSeconds / 3600)).padStart(2, "0");
1662
+ const min = String(Math.floor(totalSeconds % 3600 / 60)).padStart(2, "0");
1663
+ const s = String(totalSeconds % 60).padStart(2, "0");
1664
+ return `${date} ${h}:${min}:${s}`;
1665
+ }
1666
+ function isDateFormula(formula) {
1667
+ if (!formula) return false;
1668
+ const f = formula.toUpperCase();
1669
+ return /^(TODAY|NOW|DATE|EDATE)\b/.test(f) || /\b(TODAY|NOW|DATE|EDATE)\s*\(/.test(f);
1670
+ }
1671
+ function formatCellValue(val, cell) {
1672
+ if (val === null || val === void 0) return "";
1673
+ const fmt = cell?.format?.displayFormat;
1674
+ if (typeof val === "number") {
1675
+ if (fmt === "date") return serialToDateStr(val);
1676
+ if (fmt === "datetime") return serialToDateTimeStr(val);
1677
+ if (fmt === "percentage") return (val * 100).toFixed(1) + "%";
1678
+ if (fmt === "currency") return "$" + val.toFixed(2);
1679
+ if (fmt === "auto" || !fmt) {
1680
+ if (cell?.formula && isDateFormula(cell.formula)) {
1681
+ return val % 1 === 0 ? serialToDateStr(val) : serialToDateTimeStr(val);
1682
+ }
1683
+ }
1684
+ }
1685
+ if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
1686
+ return String(val);
1687
+ }
1555
1688
  function getCellDisplayValue2(cell) {
1556
1689
  if (!cell) return "";
1557
- if (cell.formula && cell.computedValue !== void 0) return String(cell.computedValue ?? "");
1558
- if (cell.value === null) return "";
1559
- return String(cell.value);
1690
+ const val = cell.formula && cell.computedValue !== void 0 ? cell.computedValue : cell.value;
1691
+ return formatCellValue(val, cell);
1560
1692
  }
1561
1693
  var Cell = react.memo(function Cell2({ address, row, col }) {
1562
1694
  const {
@@ -1852,13 +1984,42 @@ function SpreadsheetGrid({ className }) {
1852
1984
  children: [
1853
1985
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-10", children: /* @__PURE__ */ jsxRuntime.jsx(ColumnHeaders, {}) }),
1854
1986
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
1855
- Array.from({ length: rowCount }, (_, rowIdx) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex", children: [
1856
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsxRuntime.jsx(RowHeader, { rowIndex: rowIdx }) }),
1857
- Array.from({ length: columnCount }, (_2, colIdx) => {
1858
- const addr = toAddress(rowIdx, colIdx);
1859
- return /* @__PURE__ */ jsxRuntime.jsx(Cell, { address: addr, row: rowIdx, col: colIdx }, addr);
1860
- })
1861
- ] }, rowIdx)),
1987
+ Array.from({ length: rowCount }, (_, rowIdx) => {
1988
+ const isFrozenRow = rowIdx < activeSheet.frozenRows;
1989
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1990
+ "div",
1991
+ {
1992
+ className: "flex",
1993
+ style: isFrozenRow ? {
1994
+ position: "sticky",
1995
+ top: rowHeight + rowIdx * rowHeight,
1996
+ zIndex: 8,
1997
+ backgroundColor: "inherit"
1998
+ } : void 0,
1999
+ children: [
2000
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsxRuntime.jsx(RowHeader, { rowIndex: rowIdx }) }),
2001
+ Array.from({ length: columnCount }, (_2, colIdx) => {
2002
+ const addr = toAddress(rowIdx, colIdx);
2003
+ const isFrozenCol = colIdx < activeSheet.frozenCols;
2004
+ return /* @__PURE__ */ jsxRuntime.jsx(
2005
+ "div",
2006
+ {
2007
+ style: isFrozenCol ? {
2008
+ position: "sticky",
2009
+ left: 48 + Array.from({ length: colIdx }, (_3, c) => getColumnWidth(c)).reduce((a, b) => a + b, 0),
2010
+ zIndex: isFrozenRow ? 9 : 6,
2011
+ backgroundColor: "inherit"
2012
+ } : void 0,
2013
+ children: /* @__PURE__ */ jsxRuntime.jsx(Cell, { address: addr, row: rowIdx, col: colIdx })
2014
+ },
2015
+ addr
2016
+ );
2017
+ })
2018
+ ]
2019
+ },
2020
+ rowIdx
2021
+ );
2022
+ }),
1862
2023
  /* @__PURE__ */ jsxRuntime.jsx(SelectionOverlay, {}),
1863
2024
  editorPosition && /* @__PURE__ */ jsxRuntime.jsx(
1864
2025
  "div",