@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.d.cts CHANGED
@@ -7,11 +7,15 @@ type CellAddress = string;
7
7
  type CellValue = string | number | boolean | null;
8
8
  /** Text alignment */
9
9
  type TextAlign = "left" | "center" | "right";
10
+ /** Display format for cell values */
11
+ type CellDisplayFormat = "auto" | "text" | "number" | "date" | "datetime" | "percentage" | "currency";
10
12
  /** Cell formatting */
11
13
  interface CellFormat {
12
14
  bold?: boolean;
13
15
  italic?: boolean;
14
16
  textAlign?: TextAlign;
17
+ /** Display format — controls how the value is rendered */
18
+ displayFormat?: CellDisplayFormat;
15
19
  }
16
20
  /** A single cell's complete data */
17
21
  interface CellData {
@@ -116,6 +120,8 @@ interface SpreadsheetContextValue {
116
120
  renameSheet: (sheetId: string, name: string) => void;
117
121
  deleteSheet: (sheetId: string) => void;
118
122
  setActiveSheet: (sheetId: string) => void;
123
+ setFrozenRows: (count: number) => void;
124
+ setFrozenCols: (count: number) => void;
119
125
  undo: () => void;
120
126
  redo: () => void;
121
127
  canUndo: boolean;
package/dist/index.d.ts CHANGED
@@ -7,11 +7,15 @@ type CellAddress = string;
7
7
  type CellValue = string | number | boolean | null;
8
8
  /** Text alignment */
9
9
  type TextAlign = "left" | "center" | "right";
10
+ /** Display format for cell values */
11
+ type CellDisplayFormat = "auto" | "text" | "number" | "date" | "datetime" | "percentage" | "currency";
10
12
  /** Cell formatting */
11
13
  interface CellFormat {
12
14
  bold?: boolean;
13
15
  italic?: boolean;
14
16
  textAlign?: TextAlign;
17
+ /** Display format — controls how the value is rendered */
18
+ displayFormat?: CellDisplayFormat;
15
19
  }
16
20
  /** A single cell's complete data */
17
21
  interface CellData {
@@ -116,6 +120,8 @@ interface SpreadsheetContextValue {
116
120
  renameSheet: (sheetId: string, name: string) => void;
117
121
  deleteSheet: (sheetId: string) => void;
118
122
  setActiveSheet: (sheetId: string) => void;
123
+ setFrozenRows: (count: number) => void;
124
+ setFrozenCols: (count: number) => void;
119
125
  undo: () => void;
120
126
  redo: () => void;
121
127
  canUndo: boolean;
package/dist/index.js CHANGED
@@ -121,12 +121,41 @@ function lexFormula(input) {
121
121
  if (ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_") {
122
122
  const pos = i;
123
123
  i++;
124
- while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9" || input[i] === "_")) i++;
125
- const word = input.slice(pos, i);
124
+ 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] === " ")) {
125
+ if (input[i] === " ") {
126
+ let lookAhead = i + 1;
127
+ while (lookAhead < len && input[lookAhead] === " ") lookAhead++;
128
+ if (lookAhead < len && (input[lookAhead] >= "A" && input[lookAhead] <= "Z" || input[lookAhead] >= "a" && input[lookAhead] <= "z" || input[lookAhead] >= "0" && input[lookAhead] <= "9" || input[lookAhead] === "!")) {
129
+ i++;
130
+ continue;
131
+ }
132
+ break;
133
+ }
134
+ i++;
135
+ }
136
+ let word = input.slice(pos, i).trimEnd();
137
+ i = pos + word.length;
126
138
  if (word.toUpperCase() === "TRUE" || word.toUpperCase() === "FALSE") {
127
139
  tokens.push({ type: "boolean", value: word.toUpperCase(), position: pos });
128
140
  continue;
129
141
  }
142
+ if (i < len && input[i] === "!") {
143
+ const sheetName = word;
144
+ i++;
145
+ const refStart = i;
146
+ while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9")) i++;
147
+ const ref1 = input.slice(refStart, i);
148
+ if (i < len && input[i] === ":") {
149
+ i++;
150
+ const ref2Start = i;
151
+ while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9")) i++;
152
+ const ref2 = input.slice(ref2Start, i);
153
+ tokens.push({ type: "sheetRangeRef", value: sheetName + "!" + ref1 + ":" + ref2, position: pos });
154
+ } else {
155
+ tokens.push({ type: "sheetCellRef", value: sheetName + "!" + ref1.toUpperCase(), position: pos });
156
+ }
157
+ continue;
158
+ }
130
159
  if (i < len && input[i] === ":") {
131
160
  const colonPos = i;
132
161
  i++;
@@ -270,6 +299,19 @@ function parseFormula(tokens) {
270
299
  const parts = t.value.split(":");
271
300
  return { type: "rangeRef", start: parts[0], end: parts[1] };
272
301
  }
302
+ if (t.type === "sheetCellRef") {
303
+ advance();
304
+ const bangIdx = t.value.indexOf("!");
305
+ return { type: "sheetCellRef", sheet: t.value.slice(0, bangIdx), address: t.value.slice(bangIdx + 1).toUpperCase() };
306
+ }
307
+ if (t.type === "sheetRangeRef") {
308
+ advance();
309
+ const bangIdx = t.value.indexOf("!");
310
+ const sheetName = t.value.slice(0, bangIdx);
311
+ const rangePart = t.value.slice(bangIdx + 1);
312
+ const parts = rangePart.split(":");
313
+ return { type: "sheetRangeRef", sheet: sheetName, start: parts[0].toUpperCase(), end: parts[1].toUpperCase() };
314
+ }
273
315
  if (t.type === "function") {
274
316
  const name = advance().value;
275
317
  expect("paren", "(");
@@ -1003,7 +1045,7 @@ registerFunction("TYPE", (args) => {
1003
1045
  });
1004
1046
 
1005
1047
  // src/engine/formula/evaluator.ts
1006
- function evaluateAST(node, getCellValue, getRangeValues) {
1048
+ function evaluateAST(node, getCellValue, getRangeValues, ctx) {
1007
1049
  switch (node.type) {
1008
1050
  case "number":
1009
1051
  return node.value;
@@ -1013,9 +1055,19 @@ function evaluateAST(node, getCellValue, getRangeValues) {
1013
1055
  return node.value;
1014
1056
  case "cellRef":
1015
1057
  return getCellValue(node.address);
1016
- case "rangeRef":
1058
+ case "rangeRef": {
1017
1059
  const vals = getRangeValues(node.start, node.end);
1018
1060
  return vals[0] ?? null;
1061
+ }
1062
+ case "sheetCellRef": {
1063
+ if (!ctx?.getSheetCellValue) return "#REF!";
1064
+ return ctx.getSheetCellValue(node.sheet, node.address);
1065
+ }
1066
+ case "sheetRangeRef": {
1067
+ if (!ctx?.getSheetRangeValues) return "#REF!";
1068
+ const vals = ctx.getSheetRangeValues(node.sheet, node.start, node.end);
1069
+ return vals[0] ?? null;
1070
+ }
1019
1071
  case "functionCall": {
1020
1072
  const entry = getFunction(node.name);
1021
1073
  if (!entry) return `#NAME?`;
@@ -1023,7 +1075,11 @@ function evaluateAST(node, getCellValue, getRangeValues) {
1023
1075
  if (arg.type === "rangeRef") {
1024
1076
  return getRangeValues(arg.start, arg.end);
1025
1077
  }
1026
- const val = evaluateAST(arg, getCellValue, getRangeValues);
1078
+ if (arg.type === "sheetRangeRef") {
1079
+ if (!ctx?.getSheetRangeValues) return ["#REF!"];
1080
+ return ctx.getSheetRangeValues(arg.sheet, arg.start, arg.end);
1081
+ }
1082
+ const val = evaluateAST(arg, getCellValue, getRangeValues, ctx);
1027
1083
  return [val];
1028
1084
  });
1029
1085
  try {
@@ -1033,8 +1089,8 @@ function evaluateAST(node, getCellValue, getRangeValues) {
1033
1089
  }
1034
1090
  }
1035
1091
  case "binaryOp": {
1036
- const left = evaluateAST(node.left, getCellValue, getRangeValues);
1037
- const right = evaluateAST(node.right, getCellValue, getRangeValues);
1092
+ const left = evaluateAST(node.left, getCellValue, getRangeValues, ctx);
1093
+ const right = evaluateAST(node.right, getCellValue, getRangeValues, ctx);
1038
1094
  const lNum = typeof left === "number" ? left : Number(left);
1039
1095
  const rNum = typeof right === "number" ? right : Number(right);
1040
1096
  switch (node.operator) {
@@ -1067,7 +1123,7 @@ function evaluateAST(node, getCellValue, getRangeValues) {
1067
1123
  }
1068
1124
  }
1069
1125
  case "unaryOp": {
1070
- const operand = evaluateAST(node.operand, getCellValue, getRangeValues);
1126
+ const operand = evaluateAST(node.operand, getCellValue, getRangeValues, ctx);
1071
1127
  const num = typeof operand === "number" ? operand : Number(operand);
1072
1128
  if (isNaN(num)) return "#VALUE!";
1073
1129
  return node.operator === "-" ? -num : num;
@@ -1181,7 +1237,7 @@ function getRecalculationOrder(graph) {
1181
1237
  function recalculateWorkbook(workbook) {
1182
1238
  return {
1183
1239
  ...workbook,
1184
- sheets: workbook.sheets.map(recalculateSheet)
1240
+ sheets: workbook.sheets.map((s) => recalculateSheet(s, workbook.sheets))
1185
1241
  };
1186
1242
  }
1187
1243
  function createInitialState(data) {
@@ -1211,7 +1267,7 @@ function pushUndo(state) {
1211
1267
  if (stack.length > 50) stack.shift();
1212
1268
  return { undoStack: stack, redoStack: [] };
1213
1269
  }
1214
- function recalculateSheet(sheet) {
1270
+ function recalculateSheet(sheet, allSheets) {
1215
1271
  const graph = buildDependencyGraph(sheet.cells);
1216
1272
  if (graph.size === 0) return sheet;
1217
1273
  const circular = detectCircularRefs(graph);
@@ -1227,6 +1283,28 @@ function recalculateSheet(sheet) {
1227
1283
  const addresses = expandRange(startAddr, endAddr);
1228
1284
  return addresses.map(getCellValue);
1229
1285
  };
1286
+ const getSheetCellValue = (sheetName, addr) => {
1287
+ if (!allSheets) return "#REF!";
1288
+ const target = allSheets.find((s) => s.name === sheetName || s.id === sheetName);
1289
+ if (!target) return "#REF!";
1290
+ const c = target.cells[addr];
1291
+ if (!c) return null;
1292
+ if (c.formula && c.computedValue !== void 0) return c.computedValue;
1293
+ return c.value;
1294
+ };
1295
+ const getSheetRangeValues = (sheetName, startAddr, endAddr) => {
1296
+ if (!allSheets) return [];
1297
+ const target = allSheets.find((s) => s.name === sheetName || s.id === sheetName);
1298
+ if (!target) return [];
1299
+ const addresses = expandRange(startAddr, endAddr);
1300
+ return addresses.map((a) => {
1301
+ const c = target.cells[a];
1302
+ if (!c) return null;
1303
+ if (c.formula && c.computedValue !== void 0) return c.computedValue;
1304
+ return c.value;
1305
+ });
1306
+ };
1307
+ const ctx = { getSheetCellValue, getSheetRangeValues };
1230
1308
  for (const addr of order) {
1231
1309
  const cell = cells[addr];
1232
1310
  if (!cell?.formula) continue;
@@ -1237,7 +1315,7 @@ function recalculateSheet(sheet) {
1237
1315
  try {
1238
1316
  const tokens = lexFormula(cell.formula);
1239
1317
  const ast = parseFormula(tokens);
1240
- const result = evaluateAST(ast, getCellValue, getRangeValues);
1318
+ const result = evaluateAST(ast, getCellValue, getRangeValues, ctx);
1241
1319
  cells[addr] = { ...cell, computedValue: result };
1242
1320
  } catch {
1243
1321
  cells[addr] = { ...cell, computedValue: "#ERROR!" };
@@ -1262,7 +1340,7 @@ function reducer(state, action) {
1262
1340
  if (existing?.format) cellData.format = existing.format;
1263
1341
  const workbook = updateActiveSheet(state, (s) => {
1264
1342
  const updated = { ...s, cells: { ...s.cells, [action.address]: cellData } };
1265
- return recalculateSheet(updated);
1343
+ return recalculateSheet(updated, state.workbook.sheets);
1266
1344
  });
1267
1345
  return { ...state, workbook, ...history };
1268
1346
  }
@@ -1418,6 +1496,20 @@ function reducer(state, action) {
1418
1496
  selection: { activeCell: "A1", ranges: [{ start: "A1", end: "A1" }] },
1419
1497
  editingCell: null
1420
1498
  };
1499
+ case "SET_FROZEN_ROWS": {
1500
+ const workbook = updateActiveSheet(state, (s) => ({
1501
+ ...s,
1502
+ frozenRows: Math.max(0, action.count)
1503
+ }));
1504
+ return { ...state, workbook };
1505
+ }
1506
+ case "SET_FROZEN_COLS": {
1507
+ const workbook = updateActiveSheet(state, (s) => ({
1508
+ ...s,
1509
+ frozenCols: Math.max(0, action.count)
1510
+ }));
1511
+ return { ...state, workbook };
1512
+ }
1421
1513
  case "UNDO": {
1422
1514
  if (state.undoStack.length === 0) return state;
1423
1515
  const prev = state.undoStack[state.undoStack.length - 1];
@@ -1462,6 +1554,8 @@ function useSpreadsheetStore(initialData) {
1462
1554
  addSheet: () => dispatch({ type: "ADD_SHEET" }),
1463
1555
  renameSheet: (sheetId, name) => dispatch({ type: "RENAME_SHEET", sheetId, name }),
1464
1556
  deleteSheet: (sheetId) => dispatch({ type: "DELETE_SHEET", sheetId }),
1557
+ setFrozenRows: (count) => dispatch({ type: "SET_FROZEN_ROWS", count }),
1558
+ setFrozenCols: (count) => dispatch({ type: "SET_FROZEN_COLS", count }),
1465
1559
  setActiveSheet: (sheetId) => dispatch({ type: "SET_ACTIVE_SHEET", sheetId }),
1466
1560
  undo: () => dispatch({ type: "UNDO" }),
1467
1561
  redo: () => dispatch({ type: "REDO" }),
@@ -1550,11 +1644,49 @@ function RowHeader({ rowIndex }) {
1550
1644
  );
1551
1645
  }
1552
1646
  RowHeader.displayName = "RowHeader";
1647
+ var EXCEL_EPOCH2 = new Date(1899, 11, 30).getTime();
1648
+ function serialToDateStr(serial) {
1649
+ const d = new Date(EXCEL_EPOCH2 + Math.floor(serial) * 864e5);
1650
+ const y = d.getFullYear();
1651
+ const m = String(d.getMonth() + 1).padStart(2, "0");
1652
+ const day = String(d.getDate()).padStart(2, "0");
1653
+ return `${y}-${m}-${day}`;
1654
+ }
1655
+ function serialToDateTimeStr(serial) {
1656
+ const date = serialToDateStr(serial);
1657
+ const fraction = serial % 1;
1658
+ const totalSeconds = Math.round(fraction * 86400);
1659
+ const h = String(Math.floor(totalSeconds / 3600)).padStart(2, "0");
1660
+ const min = String(Math.floor(totalSeconds % 3600 / 60)).padStart(2, "0");
1661
+ const s = String(totalSeconds % 60).padStart(2, "0");
1662
+ return `${date} ${h}:${min}:${s}`;
1663
+ }
1664
+ function isDateFormula(formula) {
1665
+ if (!formula) return false;
1666
+ const f = formula.toUpperCase();
1667
+ return /^(TODAY|NOW|DATE|EDATE)\b/.test(f) || /\b(TODAY|NOW|DATE|EDATE)\s*\(/.test(f);
1668
+ }
1669
+ function formatCellValue(val, cell) {
1670
+ if (val === null || val === void 0) return "";
1671
+ const fmt = cell?.format?.displayFormat;
1672
+ if (typeof val === "number") {
1673
+ if (fmt === "date") return serialToDateStr(val);
1674
+ if (fmt === "datetime") return serialToDateTimeStr(val);
1675
+ if (fmt === "percentage") return (val * 100).toFixed(1) + "%";
1676
+ if (fmt === "currency") return "$" + val.toFixed(2);
1677
+ if (fmt === "auto" || !fmt) {
1678
+ if (cell?.formula && isDateFormula(cell.formula)) {
1679
+ return val % 1 === 0 ? serialToDateStr(val) : serialToDateTimeStr(val);
1680
+ }
1681
+ }
1682
+ }
1683
+ if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
1684
+ return String(val);
1685
+ }
1553
1686
  function getCellDisplayValue2(cell) {
1554
1687
  if (!cell) return "";
1555
- if (cell.formula && cell.computedValue !== void 0) return String(cell.computedValue ?? "");
1556
- if (cell.value === null) return "";
1557
- return String(cell.value);
1688
+ const val = cell.formula && cell.computedValue !== void 0 ? cell.computedValue : cell.value;
1689
+ return formatCellValue(val, cell);
1558
1690
  }
1559
1691
  var Cell = memo(function Cell2({ address, row, col }) {
1560
1692
  const {
@@ -1850,13 +1982,42 @@ function SpreadsheetGrid({ className }) {
1850
1982
  children: [
1851
1983
  /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-10", children: /* @__PURE__ */ jsx(ColumnHeaders, {}) }),
1852
1984
  /* @__PURE__ */ jsxs("div", { className: "relative", children: [
1853
- Array.from({ length: rowCount }, (_, rowIdx) => /* @__PURE__ */ jsxs("div", { className: "flex", children: [
1854
- /* @__PURE__ */ jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsx(RowHeader, { rowIndex: rowIdx }) }),
1855
- Array.from({ length: columnCount }, (_2, colIdx) => {
1856
- const addr = toAddress(rowIdx, colIdx);
1857
- return /* @__PURE__ */ jsx(Cell, { address: addr, row: rowIdx, col: colIdx }, addr);
1858
- })
1859
- ] }, rowIdx)),
1985
+ Array.from({ length: rowCount }, (_, rowIdx) => {
1986
+ const isFrozenRow = rowIdx < activeSheet.frozenRows;
1987
+ return /* @__PURE__ */ jsxs(
1988
+ "div",
1989
+ {
1990
+ className: "flex",
1991
+ style: isFrozenRow ? {
1992
+ position: "sticky",
1993
+ top: rowHeight + rowIdx * rowHeight,
1994
+ zIndex: 8,
1995
+ backgroundColor: "inherit"
1996
+ } : void 0,
1997
+ children: [
1998
+ /* @__PURE__ */ jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsx(RowHeader, { rowIndex: rowIdx }) }),
1999
+ Array.from({ length: columnCount }, (_2, colIdx) => {
2000
+ const addr = toAddress(rowIdx, colIdx);
2001
+ const isFrozenCol = colIdx < activeSheet.frozenCols;
2002
+ return /* @__PURE__ */ jsx(
2003
+ "div",
2004
+ {
2005
+ style: isFrozenCol ? {
2006
+ position: "sticky",
2007
+ left: 48 + Array.from({ length: colIdx }, (_3, c) => getColumnWidth(c)).reduce((a, b) => a + b, 0),
2008
+ zIndex: isFrozenRow ? 9 : 6,
2009
+ backgroundColor: "inherit"
2010
+ } : void 0,
2011
+ children: /* @__PURE__ */ jsx(Cell, { address: addr, row: rowIdx, col: colIdx })
2012
+ },
2013
+ addr
2014
+ );
2015
+ })
2016
+ ]
2017
+ },
2018
+ rowIdx
2019
+ );
2020
+ }),
1860
2021
  /* @__PURE__ */ jsx(SelectionOverlay, {}),
1861
2022
  editorPosition && /* @__PURE__ */ jsx(
1862
2023
  "div",