@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.cjs +255 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +255 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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] === "_"))
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1556
|
-
|
|
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) =>
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
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 }),
|