@particle-academy/fancy-sheets 0.2.0 → 0.3.1

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 {
@@ -106,6 +110,7 @@ interface SpreadsheetContextValue {
106
110
  setSelection: (cell: string) => void;
107
111
  extendSelection: (cell: string) => void;
108
112
  addSelection: (cell: string) => void;
113
+ selectRange: (start: string, end: string) => void;
109
114
  navigate: (direction: "up" | "down" | "left" | "right", extend?: boolean) => void;
110
115
  startEdit: (value?: string) => void;
111
116
  updateEdit: (value: string) => void;
@@ -116,6 +121,8 @@ interface SpreadsheetContextValue {
116
121
  renameSheet: (sheetId: string, name: string) => void;
117
122
  deleteSheet: (sheetId: string) => void;
118
123
  setActiveSheet: (sheetId: string) => void;
124
+ setFrozenRows: (count: number) => void;
125
+ setFrozenCols: (count: number) => void;
119
126
  undo: () => void;
120
127
  redo: () => void;
121
128
  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 {
@@ -106,6 +110,7 @@ interface SpreadsheetContextValue {
106
110
  setSelection: (cell: string) => void;
107
111
  extendSelection: (cell: string) => void;
108
112
  addSelection: (cell: string) => void;
113
+ selectRange: (start: string, end: string) => void;
109
114
  navigate: (direction: "up" | "down" | "left" | "right", extend?: boolean) => void;
110
115
  startEdit: (value?: string) => void;
111
116
  updateEdit: (value: string) => void;
@@ -116,6 +121,8 @@ interface SpreadsheetContextValue {
116
121
  renameSheet: (sheetId: string, name: string) => void;
117
122
  deleteSheet: (sheetId: string) => void;
118
123
  setActiveSheet: (sheetId: string) => void;
124
+ setFrozenRows: (count: number) => void;
125
+ setFrozenCols: (count: number) => void;
119
126
  undo: () => void;
120
127
  redo: () => void;
121
128
  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" }),
@@ -1505,7 +1599,15 @@ function ColumnResizeHandle({ colIndex }) {
1505
1599
  }
1506
1600
  ColumnResizeHandle.displayName = "ColumnResizeHandle";
1507
1601
  function ColumnHeaders() {
1508
- const { columnCount, rowHeight, getColumnWidth } = useSpreadsheet();
1602
+ const { columnCount, rowCount, rowHeight, getColumnWidth, selectRange } = useSpreadsheet();
1603
+ const handleColumnClick = useCallback(
1604
+ (colIdx) => {
1605
+ const start = toAddress(0, colIdx);
1606
+ const end = toAddress(rowCount - 1, colIdx);
1607
+ selectRange(start, end);
1608
+ },
1609
+ [rowCount, selectRange]
1610
+ );
1509
1611
  return /* @__PURE__ */ jsxs(
1510
1612
  "div",
1511
1613
  {
@@ -1523,8 +1625,9 @@ function ColumnHeaders() {
1523
1625
  Array.from({ length: columnCount }, (_, i) => /* @__PURE__ */ jsxs(
1524
1626
  "div",
1525
1627
  {
1526
- className: "relative flex shrink-0 items-center justify-center border-r border-zinc-300 text-[11px] font-medium text-zinc-500 select-none dark:border-zinc-600 dark:text-zinc-400",
1628
+ className: "relative flex shrink-0 cursor-pointer items-center justify-center border-r border-zinc-300 text-[11px] font-medium text-zinc-500 select-none hover:bg-zinc-200 dark:border-zinc-600 dark:text-zinc-400 dark:hover:bg-zinc-700",
1527
1629
  style: { width: getColumnWidth(i), minWidth: getColumnWidth(i) },
1630
+ onClick: () => handleColumnClick(i),
1528
1631
  children: [
1529
1632
  columnToLetter(i),
1530
1633
  /* @__PURE__ */ jsx(ColumnResizeHandle, { colIndex: i })
@@ -1538,23 +1641,67 @@ function ColumnHeaders() {
1538
1641
  }
1539
1642
  ColumnHeaders.displayName = "ColumnHeaders";
1540
1643
  function RowHeader({ rowIndex }) {
1541
- const { rowHeight } = useSpreadsheet();
1644
+ const { rowHeight, columnCount, selectRange } = useSpreadsheet();
1645
+ const handleClick = useCallback(() => {
1646
+ const start = toAddress(rowIndex, 0);
1647
+ const end = toAddress(rowIndex, columnCount - 1);
1648
+ selectRange(start, end);
1649
+ }, [rowIndex, columnCount, selectRange]);
1542
1650
  return /* @__PURE__ */ jsx(
1543
1651
  "div",
1544
1652
  {
1545
1653
  "data-fancy-sheets-row-header": "",
1546
- className: "flex shrink-0 items-center justify-center border-r border-b border-zinc-300 bg-zinc-100 text-[11px] font-medium text-zinc-500 select-none dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-400",
1654
+ className: "flex shrink-0 cursor-pointer items-center justify-center border-r border-b border-zinc-300 bg-zinc-100 text-[11px] font-medium text-zinc-500 select-none hover:bg-zinc-200 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700",
1547
1655
  style: { width: 48, minWidth: 48, height: rowHeight },
1656
+ onClick: handleClick,
1548
1657
  children: rowIndex + 1
1549
1658
  }
1550
1659
  );
1551
1660
  }
1552
1661
  RowHeader.displayName = "RowHeader";
1662
+ var EXCEL_EPOCH2 = new Date(1899, 11, 30).getTime();
1663
+ function serialToDateStr(serial) {
1664
+ const d = new Date(EXCEL_EPOCH2 + Math.floor(serial) * 864e5);
1665
+ const y = d.getFullYear();
1666
+ const m = String(d.getMonth() + 1).padStart(2, "0");
1667
+ const day = String(d.getDate()).padStart(2, "0");
1668
+ return `${y}-${m}-${day}`;
1669
+ }
1670
+ function serialToDateTimeStr(serial) {
1671
+ const date = serialToDateStr(serial);
1672
+ const fraction = serial % 1;
1673
+ const totalSeconds = Math.round(fraction * 86400);
1674
+ const h = String(Math.floor(totalSeconds / 3600)).padStart(2, "0");
1675
+ const min = String(Math.floor(totalSeconds % 3600 / 60)).padStart(2, "0");
1676
+ const s = String(totalSeconds % 60).padStart(2, "0");
1677
+ return `${date} ${h}:${min}:${s}`;
1678
+ }
1679
+ function isDateFormula(formula) {
1680
+ if (!formula) return false;
1681
+ const f = formula.toUpperCase();
1682
+ return /^(TODAY|NOW|DATE|EDATE)\b/.test(f) || /\b(TODAY|NOW|DATE|EDATE)\s*\(/.test(f);
1683
+ }
1684
+ function formatCellValue(val, cell) {
1685
+ if (val === null || val === void 0) return "";
1686
+ const fmt = cell?.format?.displayFormat;
1687
+ if (typeof val === "number") {
1688
+ if (fmt === "date") return serialToDateStr(val);
1689
+ if (fmt === "datetime") return serialToDateTimeStr(val);
1690
+ if (fmt === "percentage") return (val * 100).toFixed(1) + "%";
1691
+ if (fmt === "currency") return "$" + val.toFixed(2);
1692
+ if (fmt === "auto" || !fmt) {
1693
+ if (cell?.formula && isDateFormula(cell.formula)) {
1694
+ return val % 1 === 0 ? serialToDateStr(val) : serialToDateTimeStr(val);
1695
+ }
1696
+ }
1697
+ }
1698
+ if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
1699
+ return String(val);
1700
+ }
1553
1701
  function getCellDisplayValue2(cell) {
1554
1702
  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);
1703
+ const val = cell.formula && cell.computedValue !== void 0 ? cell.computedValue : cell.value;
1704
+ return formatCellValue(val, cell);
1558
1705
  }
1559
1706
  var Cell = memo(function Cell2({ address, row, col }) {
1560
1707
  const {
@@ -1850,13 +1997,42 @@ function SpreadsheetGrid({ className }) {
1850
1997
  children: [
1851
1998
  /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-10", children: /* @__PURE__ */ jsx(ColumnHeaders, {}) }),
1852
1999
  /* @__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)),
2000
+ Array.from({ length: rowCount }, (_, rowIdx) => {
2001
+ const isFrozenRow = rowIdx < activeSheet.frozenRows;
2002
+ return /* @__PURE__ */ jsxs(
2003
+ "div",
2004
+ {
2005
+ className: "flex",
2006
+ style: isFrozenRow ? {
2007
+ position: "sticky",
2008
+ top: rowHeight + rowIdx * rowHeight,
2009
+ zIndex: 8,
2010
+ backgroundColor: "inherit"
2011
+ } : void 0,
2012
+ children: [
2013
+ /* @__PURE__ */ jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsx(RowHeader, { rowIndex: rowIdx }) }),
2014
+ Array.from({ length: columnCount }, (_2, colIdx) => {
2015
+ const addr = toAddress(rowIdx, colIdx);
2016
+ const isFrozenCol = colIdx < activeSheet.frozenCols;
2017
+ return /* @__PURE__ */ jsx(
2018
+ "div",
2019
+ {
2020
+ style: isFrozenCol ? {
2021
+ position: "sticky",
2022
+ left: 48 + Array.from({ length: colIdx }, (_3, c) => getColumnWidth(c)).reduce((a, b) => a + b, 0),
2023
+ zIndex: isFrozenRow ? 9 : 6,
2024
+ backgroundColor: "inherit"
2025
+ } : void 0,
2026
+ children: /* @__PURE__ */ jsx(Cell, { address: addr, row: rowIdx, col: colIdx })
2027
+ },
2028
+ addr
2029
+ );
2030
+ })
2031
+ ]
2032
+ },
2033
+ rowIdx
2034
+ );
2035
+ }),
1860
2036
  /* @__PURE__ */ jsx(SelectionOverlay, {}),
1861
2037
  editorPosition && /* @__PURE__ */ jsx(
1862
2038
  "div",
@@ -1884,6 +2060,8 @@ function DefaultToolbar() {
1884
2060
  confirmEdit,
1885
2061
  startEdit,
1886
2062
  setCellFormat,
2063
+ setFrozenRows,
2064
+ setFrozenCols,
1887
2065
  undo,
1888
2066
  redo,
1889
2067
  canUndo,
@@ -1955,7 +2133,57 @@ function DefaultToolbar() {
1955
2133
  ] })
1956
2134
  },
1957
2135
  align
1958
- ))
2136
+ )),
2137
+ /* @__PURE__ */ jsx("div", { className: "mx-1 h-4 w-px bg-zinc-200 dark:bg-zinc-700" }),
2138
+ /* @__PURE__ */ jsx(
2139
+ "button",
2140
+ {
2141
+ className: cn(btnClass, activeSheet.frozenRows > 0 && activeBtnClass),
2142
+ onClick: () => {
2143
+ if (activeSheet.frozenRows > 0) {
2144
+ setFrozenRows(0);
2145
+ } else {
2146
+ const row = selection.activeCell.match(/\d+/);
2147
+ setFrozenRows(row ? parseInt(row[0], 10) - 1 || 1 : 1);
2148
+ }
2149
+ },
2150
+ disabled: readOnly,
2151
+ title: activeSheet.frozenRows > 0 ? `Unfreeze rows (${activeSheet.frozenRows} frozen)` : "Freeze rows above current cell",
2152
+ children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
2153
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "9", x2: "21", y2: "9" }),
2154
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "4", x2: "21", y2: "4" }),
2155
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "14", x2: "21", y2: "14", strokeDasharray: "3 3" }),
2156
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "19", x2: "21", y2: "19", strokeDasharray: "3 3" })
2157
+ ] })
2158
+ }
2159
+ ),
2160
+ /* @__PURE__ */ jsx(
2161
+ "button",
2162
+ {
2163
+ className: cn(btnClass, activeSheet.frozenCols > 0 && activeBtnClass),
2164
+ onClick: () => {
2165
+ if (activeSheet.frozenCols > 0) {
2166
+ setFrozenCols(0);
2167
+ } else {
2168
+ const colMatch = selection.activeCell.match(/^([A-Z]+)/);
2169
+ if (colMatch) {
2170
+ const col = colMatch[1].split("").reduce((acc, ch) => acc * 26 + ch.charCodeAt(0) - 64, 0) - 1;
2171
+ setFrozenCols(col || 1);
2172
+ } else {
2173
+ setFrozenCols(1);
2174
+ }
2175
+ }
2176
+ },
2177
+ disabled: readOnly,
2178
+ title: activeSheet.frozenCols > 0 ? `Unfreeze columns (${activeSheet.frozenCols} frozen)` : "Freeze columns left of current cell",
2179
+ children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
2180
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "3", x2: "9", y2: "21" }),
2181
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "3", x2: "4", y2: "21" }),
2182
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "3", x2: "14", y2: "21", strokeDasharray: "3 3" }),
2183
+ /* @__PURE__ */ jsx("line", { x1: "19", y1: "3", x2: "19", y2: "21", strokeDasharray: "3 3" })
2184
+ ] })
2185
+ }
2186
+ )
1959
2187
  ] }),
1960
2188
  /* @__PURE__ */ jsxs("div", { "data-fancy-sheets-formula-bar": "", className: "flex items-center gap-2 border-b border-zinc-200 px-2 py-1 dark:border-zinc-700", children: [
1961
2189
  /* @__PURE__ */ jsx("span", { className: "w-12 shrink-0 text-center text-[11px] font-medium text-zinc-500 dark:text-zinc-400", children: selection.activeCell }),