@particle-academy/fancy-sheets 0.1.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 ADDED
@@ -0,0 +1,1646 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var reactFancy = require('@particle-academy/react-fancy');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/components/Spreadsheet/Spreadsheet.tsx
8
+ var SpreadsheetContext = react.createContext(null);
9
+ function useSpreadsheet() {
10
+ const ctx = react.useContext(SpreadsheetContext);
11
+ if (!ctx) {
12
+ throw new Error("useSpreadsheet must be used within a <Spreadsheet> component");
13
+ }
14
+ return ctx;
15
+ }
16
+
17
+ // src/types/sheet.ts
18
+ function createEmptySheet(id, name) {
19
+ return {
20
+ id,
21
+ name,
22
+ cells: {},
23
+ columnWidths: {},
24
+ mergedRegions: [],
25
+ columnFilters: {},
26
+ frozenRows: 0,
27
+ frozenCols: 0
28
+ };
29
+ }
30
+ function createEmptyWorkbook() {
31
+ const sheet = createEmptySheet("sheet-1", "Sheet 1");
32
+ return {
33
+ sheets: [sheet],
34
+ activeSheetId: sheet.id
35
+ };
36
+ }
37
+
38
+ // src/engine/cell-utils.ts
39
+ function columnToLetter(col) {
40
+ let result = "";
41
+ let c = col;
42
+ while (c >= 0) {
43
+ result = String.fromCharCode(c % 26 + 65) + result;
44
+ c = Math.floor(c / 26) - 1;
45
+ }
46
+ return result;
47
+ }
48
+ function letterToColumn(letters) {
49
+ let result = 0;
50
+ for (let i = 0; i < letters.length; i++) {
51
+ result = result * 26 + (letters.charCodeAt(i) - 64);
52
+ }
53
+ return result - 1;
54
+ }
55
+ function parseAddress(addr) {
56
+ const match = addr.match(/^([A-Z]+)(\d+)$/);
57
+ if (!match) return { row: 0, col: 0 };
58
+ return {
59
+ col: letterToColumn(match[1]),
60
+ row: parseInt(match[2], 10) - 1
61
+ };
62
+ }
63
+ function toAddress(row, col) {
64
+ return columnToLetter(col) + (row + 1);
65
+ }
66
+ function expandRange(startAddr, endAddr) {
67
+ const s = parseAddress(startAddr);
68
+ const e = parseAddress(endAddr);
69
+ const minRow = Math.min(s.row, e.row);
70
+ const maxRow = Math.max(s.row, e.row);
71
+ const minCol = Math.min(s.col, e.col);
72
+ const maxCol = Math.max(s.col, e.col);
73
+ const addresses = [];
74
+ for (let r = minRow; r <= maxRow; r++) {
75
+ for (let c = minCol; c <= maxCol; c++) {
76
+ addresses.push(toAddress(r, c));
77
+ }
78
+ }
79
+ return addresses;
80
+ }
81
+ function normalizeRange(startAddr, endAddr) {
82
+ const s = parseAddress(startAddr);
83
+ const e = parseAddress(endAddr);
84
+ return {
85
+ start: toAddress(Math.min(s.row, e.row), Math.min(s.col, e.col)),
86
+ end: toAddress(Math.max(s.row, e.row), Math.max(s.col, e.col))
87
+ };
88
+ }
89
+
90
+ // src/engine/formula/lexer.ts
91
+ function lexFormula(input) {
92
+ const tokens = [];
93
+ const len = input.length;
94
+ let i = 0;
95
+ while (i < len) {
96
+ const ch = input[i];
97
+ if (ch === " " || ch === " ") {
98
+ i++;
99
+ continue;
100
+ }
101
+ if (ch >= "0" && ch <= "9" || ch === "." && i + 1 < len && input[i + 1] >= "0" && input[i + 1] <= "9") {
102
+ const pos = i;
103
+ while (i < len && (input[i] >= "0" && input[i] <= "9" || input[i] === ".")) i++;
104
+ if (i < len && (input[i] === "e" || input[i] === "E")) {
105
+ i++;
106
+ if (i < len && (input[i] === "+" || input[i] === "-")) i++;
107
+ while (i < len && input[i] >= "0" && input[i] <= "9") i++;
108
+ }
109
+ tokens.push({ type: "number", value: input.slice(pos, i), position: pos });
110
+ continue;
111
+ }
112
+ if (ch === '"') {
113
+ const pos = i;
114
+ i++;
115
+ while (i < len && input[i] !== '"') {
116
+ if (input[i] === "\\") i++;
117
+ i++;
118
+ }
119
+ i++;
120
+ tokens.push({ type: "string", value: input.slice(pos + 1, i - 1), position: pos });
121
+ continue;
122
+ }
123
+ if (ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_") {
124
+ const pos = i;
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);
128
+ if (word.toUpperCase() === "TRUE" || word.toUpperCase() === "FALSE") {
129
+ tokens.push({ type: "boolean", value: word.toUpperCase(), position: pos });
130
+ continue;
131
+ }
132
+ if (i < len && input[i] === ":") {
133
+ const colonPos = i;
134
+ i++;
135
+ const rangeStart = i;
136
+ while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9")) i++;
137
+ if (i > rangeStart) {
138
+ tokens.push({ type: "rangeRef", value: word + ":" + input.slice(rangeStart, i), position: pos });
139
+ continue;
140
+ }
141
+ i = colonPos;
142
+ }
143
+ let j = i;
144
+ while (j < len && input[j] === " ") j++;
145
+ if (j < len && input[j] === "(") {
146
+ tokens.push({ type: "function", value: word.toUpperCase(), position: pos });
147
+ continue;
148
+ }
149
+ if (/^[A-Z]+\d+$/i.test(word)) {
150
+ tokens.push({ type: "cellRef", value: word.toUpperCase(), position: pos });
151
+ continue;
152
+ }
153
+ tokens.push({ type: "cellRef", value: word.toUpperCase(), position: pos });
154
+ continue;
155
+ }
156
+ if (ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "^" || ch === "&") {
157
+ tokens.push({ type: "operator", value: ch, position: i });
158
+ i++;
159
+ continue;
160
+ }
161
+ if (ch === "=" || ch === "<" || ch === ">") {
162
+ const pos = i;
163
+ i++;
164
+ if (i < len && (input[i] === "=" || input[i] === ">")) i++;
165
+ tokens.push({ type: "operator", value: input.slice(pos, i), position: pos });
166
+ continue;
167
+ }
168
+ if (ch === "(" || ch === ")") {
169
+ tokens.push({ type: "paren", value: ch, position: i });
170
+ i++;
171
+ continue;
172
+ }
173
+ if (ch === ",") {
174
+ tokens.push({ type: "comma", value: ",", position: i });
175
+ i++;
176
+ continue;
177
+ }
178
+ i++;
179
+ }
180
+ return tokens;
181
+ }
182
+
183
+ // src/engine/formula/parser.ts
184
+ function parseFormula(tokens) {
185
+ let pos = 0;
186
+ function peek() {
187
+ return tokens[pos];
188
+ }
189
+ function advance() {
190
+ return tokens[pos++];
191
+ }
192
+ function expect(type, value) {
193
+ const t = advance();
194
+ if (!t || t.type !== type || value !== void 0 && t.value !== value) {
195
+ throw new Error(`Expected ${type}${value ? ` '${value}'` : ""} at position ${t?.position ?? pos}`);
196
+ }
197
+ return t;
198
+ }
199
+ function parseExpression() {
200
+ return parseComparison();
201
+ }
202
+ function parseComparison() {
203
+ let left = parseConcatenation();
204
+ while (peek() && peek().type === "operator" && ["=", "<>", "<", ">", "<=", ">="].includes(peek().value)) {
205
+ const op = advance().value;
206
+ const right = parseConcatenation();
207
+ left = { type: "binaryOp", operator: op, left, right };
208
+ }
209
+ return left;
210
+ }
211
+ function parseConcatenation() {
212
+ let left = parseAddition();
213
+ while (peek() && peek().type === "operator" && peek().value === "&") {
214
+ advance();
215
+ const right = parseAddition();
216
+ left = { type: "binaryOp", operator: "&", left, right };
217
+ }
218
+ return left;
219
+ }
220
+ function parseAddition() {
221
+ let left = parseMultiplication();
222
+ while (peek() && peek().type === "operator" && (peek().value === "+" || peek().value === "-")) {
223
+ const op = advance().value;
224
+ const right = parseMultiplication();
225
+ left = { type: "binaryOp", operator: op, left, right };
226
+ }
227
+ return left;
228
+ }
229
+ function parseMultiplication() {
230
+ let left = parseExponentiation();
231
+ while (peek() && peek().type === "operator" && (peek().value === "*" || peek().value === "/")) {
232
+ const op = advance().value;
233
+ const right = parseExponentiation();
234
+ left = { type: "binaryOp", operator: op, left, right };
235
+ }
236
+ return left;
237
+ }
238
+ function parseExponentiation() {
239
+ let left = parseUnary();
240
+ while (peek() && peek().type === "operator" && peek().value === "^") {
241
+ advance();
242
+ const right = parseUnary();
243
+ left = { type: "binaryOp", operator: "^", left, right };
244
+ }
245
+ return left;
246
+ }
247
+ function parseUnary() {
248
+ if (peek() && peek().type === "operator" && (peek().value === "-" || peek().value === "+")) {
249
+ const op = advance().value;
250
+ const operand = parseUnary();
251
+ return { type: "unaryOp", operator: op, operand };
252
+ }
253
+ return parseAtom();
254
+ }
255
+ function parseAtom() {
256
+ const t = peek();
257
+ if (!t) throw new Error("Unexpected end of formula");
258
+ if (t.type === "number") {
259
+ advance();
260
+ return { type: "number", value: parseFloat(t.value) };
261
+ }
262
+ if (t.type === "string") {
263
+ advance();
264
+ return { type: "string", value: t.value };
265
+ }
266
+ if (t.type === "boolean") {
267
+ advance();
268
+ return { type: "boolean", value: t.value === "TRUE" };
269
+ }
270
+ if (t.type === "rangeRef") {
271
+ advance();
272
+ const parts = t.value.split(":");
273
+ return { type: "rangeRef", start: parts[0], end: parts[1] };
274
+ }
275
+ if (t.type === "function") {
276
+ const name = advance().value;
277
+ expect("paren", "(");
278
+ const args = [];
279
+ if (peek() && !(peek().type === "paren" && peek().value === ")")) {
280
+ args.push(parseExpression());
281
+ while (peek() && peek().type === "comma") {
282
+ advance();
283
+ args.push(parseExpression());
284
+ }
285
+ }
286
+ expect("paren", ")");
287
+ return { type: "functionCall", name, args };
288
+ }
289
+ if (t.type === "cellRef") {
290
+ advance();
291
+ return { type: "cellRef", address: t.value };
292
+ }
293
+ if (t.type === "paren" && t.value === "(") {
294
+ advance();
295
+ const expr = parseExpression();
296
+ expect("paren", ")");
297
+ return expr;
298
+ }
299
+ throw new Error(`Unexpected token '${t.value}' at position ${t.position}`);
300
+ }
301
+ const ast = parseExpression();
302
+ return ast;
303
+ }
304
+
305
+ // src/engine/formula/functions/registry.ts
306
+ var functionRegistry = /* @__PURE__ */ new Map();
307
+ function registerFunction(name, fn) {
308
+ functionRegistry.set(name.toUpperCase(), { fn });
309
+ }
310
+ function getFunction(name) {
311
+ return functionRegistry.get(name.toUpperCase());
312
+ }
313
+
314
+ // src/engine/formula/functions/math.ts
315
+ function toNumbers(args) {
316
+ const nums = [];
317
+ for (const group of args) {
318
+ for (const v of group) {
319
+ if (typeof v === "number") nums.push(v);
320
+ else if (typeof v === "string" && v !== "" && !isNaN(Number(v))) nums.push(Number(v));
321
+ }
322
+ }
323
+ return nums;
324
+ }
325
+ registerFunction("SUM", (args) => {
326
+ return toNumbers(args).reduce((a, b) => a + b, 0);
327
+ });
328
+ registerFunction("AVERAGE", (args) => {
329
+ const nums = toNumbers(args);
330
+ if (nums.length === 0) return 0;
331
+ return nums.reduce((a, b) => a + b, 0) / nums.length;
332
+ });
333
+ registerFunction("MIN", (args) => {
334
+ const nums = toNumbers(args);
335
+ if (nums.length === 0) return 0;
336
+ return Math.min(...nums);
337
+ });
338
+ registerFunction("MAX", (args) => {
339
+ const nums = toNumbers(args);
340
+ if (nums.length === 0) return 0;
341
+ return Math.max(...nums);
342
+ });
343
+ registerFunction("COUNT", (args) => {
344
+ return toNumbers(args).length;
345
+ });
346
+ registerFunction("ROUND", (args) => {
347
+ const flat = args.flat();
348
+ const num = typeof flat[0] === "number" ? flat[0] : Number(flat[0]);
349
+ const decimals = typeof flat[1] === "number" ? flat[1] : flat[1] != null ? Number(flat[1]) : 0;
350
+ if (isNaN(num)) return "#VALUE!";
351
+ return Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals);
352
+ });
353
+ registerFunction("ABS", (args) => {
354
+ const val = args.flat()[0];
355
+ const num = typeof val === "number" ? val : Number(val);
356
+ if (isNaN(num)) return "#VALUE!";
357
+ return Math.abs(num);
358
+ });
359
+
360
+ // src/engine/formula/functions/text.ts
361
+ registerFunction("UPPER", (args) => {
362
+ const val = args.flat()[0];
363
+ return val != null ? String(val).toUpperCase() : "";
364
+ });
365
+ registerFunction("LOWER", (args) => {
366
+ const val = args.flat()[0];
367
+ return val != null ? String(val).toLowerCase() : "";
368
+ });
369
+ registerFunction("LEN", (args) => {
370
+ const val = args.flat()[0];
371
+ return val != null ? String(val).length : 0;
372
+ });
373
+ registerFunction("TRIM", (args) => {
374
+ const val = args.flat()[0];
375
+ return val != null ? String(val).trim() : "";
376
+ });
377
+ registerFunction("CONCAT", (args) => {
378
+ return args.flat().map((v) => v != null ? String(v) : "").join("");
379
+ });
380
+
381
+ // src/engine/formula/functions/logic.ts
382
+ function toBool(v) {
383
+ if (typeof v === "boolean") return v;
384
+ if (typeof v === "number") return v !== 0;
385
+ if (typeof v === "string") return v.toUpperCase() === "TRUE";
386
+ return false;
387
+ }
388
+ registerFunction("IF", (args) => {
389
+ const flat = args.flat();
390
+ const condition = toBool(flat[0]);
391
+ const trueVal = flat[1] ?? true;
392
+ const falseVal = flat[2] ?? false;
393
+ return condition ? trueVal : falseVal;
394
+ });
395
+ registerFunction("AND", (args) => {
396
+ return args.flat().every(toBool);
397
+ });
398
+ registerFunction("OR", (args) => {
399
+ return args.flat().some(toBool);
400
+ });
401
+ registerFunction("NOT", (args) => {
402
+ return !toBool(args.flat()[0]);
403
+ });
404
+
405
+ // src/engine/formula/evaluator.ts
406
+ function evaluateAST(node, getCellValue, getRangeValues) {
407
+ switch (node.type) {
408
+ case "number":
409
+ return node.value;
410
+ case "string":
411
+ return node.value;
412
+ case "boolean":
413
+ return node.value;
414
+ case "cellRef":
415
+ return getCellValue(node.address);
416
+ case "rangeRef":
417
+ const vals = getRangeValues(node.start, node.end);
418
+ return vals[0] ?? null;
419
+ case "functionCall": {
420
+ const entry = getFunction(node.name);
421
+ if (!entry) return `#NAME?`;
422
+ const argValues = node.args.map((arg) => {
423
+ if (arg.type === "rangeRef") {
424
+ return getRangeValues(arg.start, arg.end);
425
+ }
426
+ const val = evaluateAST(arg, getCellValue, getRangeValues);
427
+ return [val];
428
+ });
429
+ try {
430
+ return entry.fn(argValues);
431
+ } catch {
432
+ return "#ERROR!";
433
+ }
434
+ }
435
+ case "binaryOp": {
436
+ const left = evaluateAST(node.left, getCellValue, getRangeValues);
437
+ const right = evaluateAST(node.right, getCellValue, getRangeValues);
438
+ const lNum = typeof left === "number" ? left : Number(left);
439
+ const rNum = typeof right === "number" ? right : Number(right);
440
+ switch (node.operator) {
441
+ case "+":
442
+ return isNaN(lNum) || isNaN(rNum) ? "#VALUE!" : lNum + rNum;
443
+ case "-":
444
+ return isNaN(lNum) || isNaN(rNum) ? "#VALUE!" : lNum - rNum;
445
+ case "*":
446
+ return isNaN(lNum) || isNaN(rNum) ? "#VALUE!" : lNum * rNum;
447
+ case "/":
448
+ return rNum === 0 ? "#DIV/0!" : isNaN(lNum) || isNaN(rNum) ? "#VALUE!" : lNum / rNum;
449
+ case "^":
450
+ return isNaN(lNum) || isNaN(rNum) ? "#VALUE!" : Math.pow(lNum, rNum);
451
+ case "&":
452
+ return String(left ?? "") + String(right ?? "");
453
+ case "=":
454
+ return left === right;
455
+ case "<>":
456
+ return left !== right;
457
+ case "<":
458
+ return lNum < rNum;
459
+ case ">":
460
+ return lNum > rNum;
461
+ case "<=":
462
+ return lNum <= rNum;
463
+ case ">=":
464
+ return lNum >= rNum;
465
+ default:
466
+ return "#ERROR!";
467
+ }
468
+ }
469
+ case "unaryOp": {
470
+ const operand = evaluateAST(node.operand, getCellValue, getRangeValues);
471
+ const num = typeof operand === "number" ? operand : Number(operand);
472
+ if (isNaN(num)) return "#VALUE!";
473
+ return node.operator === "-" ? -num : num;
474
+ }
475
+ default:
476
+ return "#ERROR!";
477
+ }
478
+ }
479
+
480
+ // src/engine/formula/references.ts
481
+ function extractReferences(node) {
482
+ const refs = [];
483
+ function walk(n) {
484
+ switch (n.type) {
485
+ case "cellRef":
486
+ refs.push(n.address);
487
+ break;
488
+ case "rangeRef": {
489
+ const s = parseAddress(n.start);
490
+ const e = parseAddress(n.end);
491
+ const minRow = Math.min(s.row, e.row);
492
+ const maxRow = Math.max(s.row, e.row);
493
+ const minCol = Math.min(s.col, e.col);
494
+ const maxCol = Math.max(s.col, e.col);
495
+ for (let r = minRow; r <= maxRow; r++) {
496
+ for (let c = minCol; c <= maxCol; c++) {
497
+ refs.push(toAddress(r, c));
498
+ }
499
+ }
500
+ break;
501
+ }
502
+ case "functionCall":
503
+ n.args.forEach(walk);
504
+ break;
505
+ case "binaryOp":
506
+ walk(n.left);
507
+ walk(n.right);
508
+ break;
509
+ case "unaryOp":
510
+ walk(n.operand);
511
+ break;
512
+ }
513
+ }
514
+ walk(node);
515
+ return refs;
516
+ }
517
+
518
+ // src/engine/formula/dependency-graph.ts
519
+ function buildDependencyGraph(cells) {
520
+ const graph = /* @__PURE__ */ new Map();
521
+ for (const [addr, cell] of Object.entries(cells)) {
522
+ if (!cell.formula) continue;
523
+ try {
524
+ const tokens = lexFormula(cell.formula);
525
+ const ast = parseFormula(tokens);
526
+ const refs = extractReferences(ast);
527
+ graph.set(addr, new Set(refs));
528
+ } catch {
529
+ graph.set(addr, /* @__PURE__ */ new Set());
530
+ }
531
+ }
532
+ return graph;
533
+ }
534
+ function detectCircularRefs(graph) {
535
+ const circular = /* @__PURE__ */ new Set();
536
+ const visited = /* @__PURE__ */ new Set();
537
+ const inStack = /* @__PURE__ */ new Set();
538
+ function dfs(node) {
539
+ if (inStack.has(node)) return true;
540
+ if (visited.has(node)) return false;
541
+ visited.add(node);
542
+ inStack.add(node);
543
+ const deps = graph.get(node);
544
+ if (deps) {
545
+ for (const dep of deps) {
546
+ if (dfs(dep)) {
547
+ circular.add(node);
548
+ return true;
549
+ }
550
+ }
551
+ }
552
+ inStack.delete(node);
553
+ return false;
554
+ }
555
+ for (const node of graph.keys()) {
556
+ dfs(node);
557
+ }
558
+ return circular;
559
+ }
560
+ function getRecalculationOrder(graph) {
561
+ const visited = /* @__PURE__ */ new Set();
562
+ const order = [];
563
+ function visit(node) {
564
+ if (visited.has(node)) return;
565
+ visited.add(node);
566
+ const deps = graph.get(node);
567
+ if (deps) {
568
+ for (const dep of deps) {
569
+ visit(dep);
570
+ }
571
+ }
572
+ order.push(node);
573
+ }
574
+ for (const node of graph.keys()) {
575
+ visit(node);
576
+ }
577
+ return order;
578
+ }
579
+
580
+ // src/hooks/use-spreadsheet-store.ts
581
+ function createInitialState(data) {
582
+ return {
583
+ workbook: data ?? createEmptyWorkbook(),
584
+ selection: { activeCell: "A1", ranges: [{ start: "A1", end: "A1" }] },
585
+ editingCell: null,
586
+ editValue: "",
587
+ undoStack: [],
588
+ redoStack: []
589
+ };
590
+ }
591
+ function getActiveSheet(state) {
592
+ return state.workbook.sheets.find((s) => s.id === state.workbook.activeSheetId);
593
+ }
594
+ function updateActiveSheet(state, updater) {
595
+ return {
596
+ ...state.workbook,
597
+ sheets: state.workbook.sheets.map(
598
+ (s) => s.id === state.workbook.activeSheetId ? updater(s) : s
599
+ )
600
+ };
601
+ }
602
+ function pushUndo(state) {
603
+ const stack = [...state.undoStack, state.workbook];
604
+ if (stack.length > 50) stack.shift();
605
+ return { undoStack: stack, redoStack: [] };
606
+ }
607
+ function recalculateSheet(sheet) {
608
+ const graph = buildDependencyGraph(sheet.cells);
609
+ if (graph.size === 0) return sheet;
610
+ const circular = detectCircularRefs(graph);
611
+ const order = getRecalculationOrder(graph);
612
+ const cells = { ...sheet.cells };
613
+ const getCellValue = (addr) => {
614
+ const c = cells[addr];
615
+ if (!c) return null;
616
+ if (c.formula && c.computedValue !== void 0) return c.computedValue;
617
+ return c.value;
618
+ };
619
+ const getRangeValues = (startAddr, endAddr) => {
620
+ const addresses = expandRange(startAddr, endAddr);
621
+ return addresses.map(getCellValue);
622
+ };
623
+ for (const addr of order) {
624
+ const cell = cells[addr];
625
+ if (!cell?.formula) continue;
626
+ if (circular.has(addr)) {
627
+ cells[addr] = { ...cell, computedValue: "#CIRC!" };
628
+ continue;
629
+ }
630
+ try {
631
+ const tokens = lexFormula(cell.formula);
632
+ const ast = parseFormula(tokens);
633
+ const result = evaluateAST(ast, getCellValue, getRangeValues);
634
+ cells[addr] = { ...cell, computedValue: result };
635
+ } catch {
636
+ cells[addr] = { ...cell, computedValue: "#ERROR!" };
637
+ }
638
+ }
639
+ return { ...sheet, cells };
640
+ }
641
+ function getCellDisplayValue(cell) {
642
+ if (!cell) return "";
643
+ if (cell.computedValue !== void 0) return String(cell.computedValue);
644
+ if (cell.value === null) return "";
645
+ return String(cell.value);
646
+ }
647
+ function reducer(state, action) {
648
+ switch (action.type) {
649
+ case "SET_CELL_VALUE": {
650
+ const history = pushUndo(state);
651
+ const isFormula = action.value.startsWith("=");
652
+ const cellData = isFormula ? { value: action.value, formula: action.value.slice(1), computedValue: null } : { value: isNaN(Number(action.value)) || action.value === "" ? action.value : Number(action.value) };
653
+ const sheet = getActiveSheet(state);
654
+ const existing = sheet.cells[action.address];
655
+ if (existing?.format) cellData.format = existing.format;
656
+ const workbook = updateActiveSheet(state, (s) => {
657
+ const updated = { ...s, cells: { ...s.cells, [action.address]: cellData } };
658
+ return recalculateSheet(updated);
659
+ });
660
+ return { ...state, workbook, ...history };
661
+ }
662
+ case "SET_CELL_FORMAT": {
663
+ const history = pushUndo(state);
664
+ const workbook = updateActiveSheet(state, (s) => {
665
+ const cells = { ...s.cells };
666
+ for (const addr of action.addresses) {
667
+ const existing = cells[addr] ?? { value: null };
668
+ cells[addr] = { ...existing, format: { ...existing.format, ...action.format } };
669
+ }
670
+ return { ...s, cells };
671
+ });
672
+ return { ...state, workbook, ...history };
673
+ }
674
+ case "SET_SELECTION":
675
+ return {
676
+ ...state,
677
+ selection: {
678
+ activeCell: action.cell,
679
+ ranges: [{ start: action.cell, end: action.cell }]
680
+ },
681
+ editingCell: null
682
+ };
683
+ case "EXTEND_SELECTION":
684
+ return {
685
+ ...state,
686
+ selection: {
687
+ ...state.selection,
688
+ ranges: [
689
+ { start: state.selection.activeCell, end: action.cell },
690
+ ...state.selection.ranges.slice(1)
691
+ ]
692
+ }
693
+ };
694
+ case "ADD_SELECTION":
695
+ return {
696
+ ...state,
697
+ selection: {
698
+ activeCell: action.cell,
699
+ ranges: [...state.selection.ranges, { start: action.cell, end: action.cell }]
700
+ }
701
+ };
702
+ case "SELECT_RANGE":
703
+ return {
704
+ ...state,
705
+ selection: {
706
+ activeCell: action.start,
707
+ ranges: [{ start: action.start, end: action.end }]
708
+ }
709
+ };
710
+ case "NAVIGATE": {
711
+ const { row, col } = parseAddress(state.selection.activeCell);
712
+ let newRow = row;
713
+ let newCol = col;
714
+ switch (action.direction) {
715
+ case "up":
716
+ newRow = Math.max(0, row - 1);
717
+ break;
718
+ case "down":
719
+ newRow = row + 1;
720
+ break;
721
+ case "left":
722
+ newCol = Math.max(0, col - 1);
723
+ break;
724
+ case "right":
725
+ newCol = col + 1;
726
+ break;
727
+ }
728
+ const newAddr = toAddress(newRow, newCol);
729
+ if (action.extend) {
730
+ return {
731
+ ...state,
732
+ selection: {
733
+ ...state.selection,
734
+ ranges: [
735
+ { start: state.selection.activeCell, end: newAddr },
736
+ ...state.selection.ranges.slice(1)
737
+ ]
738
+ }
739
+ };
740
+ }
741
+ return {
742
+ ...state,
743
+ selection: { activeCell: newAddr, ranges: [{ start: newAddr, end: newAddr }] },
744
+ editingCell: null
745
+ };
746
+ }
747
+ case "START_EDIT": {
748
+ const sheet = getActiveSheet(state);
749
+ const cell = sheet.cells[state.selection.activeCell];
750
+ const initialValue = action.value ?? (cell?.formula ? "=" + cell.formula : getCellDisplayValue(cell));
751
+ return { ...state, editingCell: state.selection.activeCell, editValue: initialValue };
752
+ }
753
+ case "UPDATE_EDIT":
754
+ return { ...state, editValue: action.value };
755
+ case "CONFIRM_EDIT": {
756
+ if (!state.editingCell) return state;
757
+ const newState = reducer(state, { type: "SET_CELL_VALUE", address: state.editingCell, value: state.editValue });
758
+ const { row, col } = parseAddress(state.editingCell);
759
+ const nextAddr = toAddress(row + 1, col);
760
+ return {
761
+ ...newState,
762
+ editingCell: null,
763
+ editValue: "",
764
+ selection: { activeCell: nextAddr, ranges: [{ start: nextAddr, end: nextAddr }] }
765
+ };
766
+ }
767
+ case "CANCEL_EDIT":
768
+ return { ...state, editingCell: null, editValue: "" };
769
+ case "RESIZE_COLUMN": {
770
+ const workbook = updateActiveSheet(state, (s) => ({
771
+ ...s,
772
+ columnWidths: { ...s.columnWidths, [action.col]: Math.max(30, action.width) }
773
+ }));
774
+ return { ...state, workbook };
775
+ }
776
+ case "ADD_SHEET": {
777
+ const id = `sheet-${Date.now()}`;
778
+ const num = state.workbook.sheets.length + 1;
779
+ const sheet = createEmptySheet(id, `Sheet ${num}`);
780
+ return {
781
+ ...state,
782
+ workbook: {
783
+ sheets: [...state.workbook.sheets, sheet],
784
+ activeSheetId: id
785
+ }
786
+ };
787
+ }
788
+ case "RENAME_SHEET":
789
+ return {
790
+ ...state,
791
+ workbook: {
792
+ ...state.workbook,
793
+ sheets: state.workbook.sheets.map(
794
+ (s) => s.id === action.sheetId ? { ...s, name: action.name } : s
795
+ )
796
+ }
797
+ };
798
+ case "DELETE_SHEET": {
799
+ if (state.workbook.sheets.length <= 1) return state;
800
+ const remaining = state.workbook.sheets.filter((s) => s.id !== action.sheetId);
801
+ const activeId = state.workbook.activeSheetId === action.sheetId ? remaining[0].id : state.workbook.activeSheetId;
802
+ return {
803
+ ...state,
804
+ workbook: { sheets: remaining, activeSheetId: activeId }
805
+ };
806
+ }
807
+ case "SET_ACTIVE_SHEET":
808
+ return {
809
+ ...state,
810
+ workbook: { ...state.workbook, activeSheetId: action.sheetId },
811
+ selection: { activeCell: "A1", ranges: [{ start: "A1", end: "A1" }] },
812
+ editingCell: null
813
+ };
814
+ case "UNDO": {
815
+ if (state.undoStack.length === 0) return state;
816
+ const prev = state.undoStack[state.undoStack.length - 1];
817
+ return {
818
+ ...state,
819
+ workbook: prev,
820
+ undoStack: state.undoStack.slice(0, -1),
821
+ redoStack: [...state.redoStack, state.workbook]
822
+ };
823
+ }
824
+ case "REDO": {
825
+ if (state.redoStack.length === 0) return state;
826
+ const next = state.redoStack[state.redoStack.length - 1];
827
+ return {
828
+ ...state,
829
+ workbook: next,
830
+ undoStack: [...state.undoStack, state.workbook],
831
+ redoStack: state.redoStack.slice(0, -1)
832
+ };
833
+ }
834
+ case "SET_WORKBOOK":
835
+ return { ...state, workbook: action.workbook };
836
+ default:
837
+ return state;
838
+ }
839
+ }
840
+ function useSpreadsheetStore(initialData) {
841
+ const [state, dispatch] = react.useReducer(reducer, initialData, (data) => createInitialState(data));
842
+ const actions = react.useMemo(() => ({
843
+ setCellValue: (address, value) => dispatch({ type: "SET_CELL_VALUE", address, value }),
844
+ setCellFormat: (addresses, format) => dispatch({ type: "SET_CELL_FORMAT", addresses, format }),
845
+ setSelection: (cell) => dispatch({ type: "SET_SELECTION", cell }),
846
+ extendSelection: (cell) => dispatch({ type: "EXTEND_SELECTION", cell }),
847
+ addSelection: (cell) => dispatch({ type: "ADD_SELECTION", cell }),
848
+ selectRange: (start, end) => dispatch({ type: "SELECT_RANGE", start, end }),
849
+ navigate: (direction, extend) => dispatch({ type: "NAVIGATE", direction, extend }),
850
+ startEdit: (value) => dispatch({ type: "START_EDIT", value }),
851
+ updateEdit: (value) => dispatch({ type: "UPDATE_EDIT", value }),
852
+ confirmEdit: () => dispatch({ type: "CONFIRM_EDIT" }),
853
+ cancelEdit: () => dispatch({ type: "CANCEL_EDIT" }),
854
+ resizeColumn: (col, width) => dispatch({ type: "RESIZE_COLUMN", col, width }),
855
+ addSheet: () => dispatch({ type: "ADD_SHEET" }),
856
+ renameSheet: (sheetId, name) => dispatch({ type: "RENAME_SHEET", sheetId, name }),
857
+ deleteSheet: (sheetId) => dispatch({ type: "DELETE_SHEET", sheetId }),
858
+ setActiveSheet: (sheetId) => dispatch({ type: "SET_ACTIVE_SHEET", sheetId }),
859
+ undo: () => dispatch({ type: "UNDO" }),
860
+ redo: () => dispatch({ type: "REDO" }),
861
+ setWorkbook: (workbook) => dispatch({ type: "SET_WORKBOOK", workbook })
862
+ }), []);
863
+ return { state, actions };
864
+ }
865
+ function ColumnResizeHandle({ colIndex }) {
866
+ const { resizeColumn, getColumnWidth } = useSpreadsheet();
867
+ const startX = react.useRef(0);
868
+ const startWidth = react.useRef(0);
869
+ const handlePointerDown = react.useCallback(
870
+ (e) => {
871
+ e.preventDefault();
872
+ e.stopPropagation();
873
+ startX.current = e.clientX;
874
+ startWidth.current = getColumnWidth(colIndex);
875
+ const target = e.currentTarget;
876
+ target.setPointerCapture(e.pointerId);
877
+ const handlePointerMove = (ev) => {
878
+ const delta = ev.clientX - startX.current;
879
+ resizeColumn(colIndex, startWidth.current + delta);
880
+ };
881
+ const handlePointerUp = () => {
882
+ target.removeEventListener("pointermove", handlePointerMove);
883
+ target.removeEventListener("pointerup", handlePointerUp);
884
+ };
885
+ target.addEventListener("pointermove", handlePointerMove);
886
+ target.addEventListener("pointerup", handlePointerUp);
887
+ },
888
+ [colIndex, getColumnWidth, resizeColumn]
889
+ );
890
+ return /* @__PURE__ */ jsxRuntime.jsx(
891
+ "div",
892
+ {
893
+ "data-fancy-sheets-resize-handle": "",
894
+ className: "absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-blue-500/50",
895
+ onPointerDown: handlePointerDown
896
+ }
897
+ );
898
+ }
899
+ ColumnResizeHandle.displayName = "ColumnResizeHandle";
900
+ function ColumnHeaders() {
901
+ const { columnCount, rowHeight, getColumnWidth } = useSpreadsheet();
902
+ return /* @__PURE__ */ jsxRuntime.jsxs(
903
+ "div",
904
+ {
905
+ "data-fancy-sheets-column-headers": "",
906
+ className: "flex border-b border-zinc-300 bg-zinc-100 dark:border-zinc-600 dark:bg-zinc-800",
907
+ style: { height: rowHeight },
908
+ children: [
909
+ /* @__PURE__ */ jsxRuntime.jsx(
910
+ "div",
911
+ {
912
+ className: "flex shrink-0 items-center justify-center border-r border-zinc-300 bg-zinc-100 text-[11px] font-medium text-zinc-400 dark:border-zinc-600 dark:bg-zinc-800",
913
+ style: { width: 48, minWidth: 48 }
914
+ }
915
+ ),
916
+ Array.from({ length: columnCount }, (_, i) => /* @__PURE__ */ jsxRuntime.jsxs(
917
+ "div",
918
+ {
919
+ 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",
920
+ style: { width: getColumnWidth(i), minWidth: getColumnWidth(i) },
921
+ children: [
922
+ columnToLetter(i),
923
+ /* @__PURE__ */ jsxRuntime.jsx(ColumnResizeHandle, { colIndex: i })
924
+ ]
925
+ },
926
+ i
927
+ ))
928
+ ]
929
+ }
930
+ );
931
+ }
932
+ ColumnHeaders.displayName = "ColumnHeaders";
933
+ function RowHeader({ rowIndex }) {
934
+ const { rowHeight } = useSpreadsheet();
935
+ return /* @__PURE__ */ jsxRuntime.jsx(
936
+ "div",
937
+ {
938
+ "data-fancy-sheets-row-header": "",
939
+ 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",
940
+ style: { width: 48, minWidth: 48, height: rowHeight },
941
+ children: rowIndex + 1
942
+ }
943
+ );
944
+ }
945
+ RowHeader.displayName = "RowHeader";
946
+ function getCellDisplayValue2(cell) {
947
+ if (!cell) return "";
948
+ if (cell.formula && cell.computedValue !== void 0) return String(cell.computedValue ?? "");
949
+ if (cell.value === null) return "";
950
+ return String(cell.value);
951
+ }
952
+ var Cell = react.memo(function Cell2({ address, row, col }) {
953
+ const {
954
+ activeSheet,
955
+ selection,
956
+ editingCell,
957
+ readOnly,
958
+ setSelection,
959
+ extendSelection,
960
+ addSelection,
961
+ startEdit,
962
+ rowHeight,
963
+ getColumnWidth,
964
+ isCellSelected,
965
+ isCellActive
966
+ } = useSpreadsheet();
967
+ const cell = activeSheet.cells[address];
968
+ const isActive = isCellActive(address);
969
+ const isSelected = isCellSelected(address);
970
+ const isEditing = editingCell === address;
971
+ const displayValue = getCellDisplayValue2(cell);
972
+ const width = getColumnWidth(col);
973
+ const handleMouseDown = react.useCallback(
974
+ (e) => {
975
+ if (e.shiftKey) {
976
+ extendSelection(address);
977
+ } else if (e.ctrlKey || e.metaKey) {
978
+ addSelection(address);
979
+ } else {
980
+ setSelection(address);
981
+ }
982
+ },
983
+ [address, setSelection, extendSelection, addSelection]
984
+ );
985
+ const handleDoubleClick = react.useCallback(() => {
986
+ if (readOnly) return;
987
+ startEdit();
988
+ }, [readOnly, startEdit]);
989
+ const formatStyle = {};
990
+ if (cell?.format?.bold) formatStyle.fontWeight = "bold";
991
+ if (cell?.format?.italic) formatStyle.fontStyle = "italic";
992
+ if (cell?.format?.textAlign) formatStyle.textAlign = cell.format.textAlign;
993
+ return /* @__PURE__ */ jsxRuntime.jsx(
994
+ "div",
995
+ {
996
+ "data-fancy-sheets-cell": "",
997
+ "data-selected": isSelected || void 0,
998
+ "data-active": isActive || void 0,
999
+ role: "gridcell",
1000
+ className: reactFancy.cn(
1001
+ "relative flex items-center truncate border-r border-b border-zinc-200 px-1.5 text-[13px] dark:border-zinc-700",
1002
+ isActive && "ring-2 ring-inset ring-blue-500",
1003
+ isSelected && !isActive && "bg-blue-500/10"
1004
+ ),
1005
+ style: { width, minWidth: width, height: rowHeight, ...formatStyle },
1006
+ onMouseDown: handleMouseDown,
1007
+ onDoubleClick: handleDoubleClick,
1008
+ children: !isEditing && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: displayValue })
1009
+ }
1010
+ );
1011
+ });
1012
+ Cell.displayName = "Cell";
1013
+ function CellEditor() {
1014
+ const {
1015
+ editingCell,
1016
+ editValue,
1017
+ updateEdit,
1018
+ confirmEdit,
1019
+ cancelEdit,
1020
+ getColumnWidth,
1021
+ rowHeight,
1022
+ activeSheet
1023
+ } = useSpreadsheet();
1024
+ const inputRef = react.useRef(null);
1025
+ react.useEffect(() => {
1026
+ if (editingCell && inputRef.current) {
1027
+ inputRef.current.focus();
1028
+ inputRef.current.select();
1029
+ }
1030
+ }, [editingCell]);
1031
+ if (!editingCell) return null;
1032
+ const { row, col } = parseAddress(editingCell);
1033
+ const width = getColumnWidth(col);
1034
+ const handleKeyDown = (e) => {
1035
+ if (e.key === "Enter") {
1036
+ e.preventDefault();
1037
+ confirmEdit();
1038
+ } else if (e.key === "Escape") {
1039
+ e.preventDefault();
1040
+ cancelEdit();
1041
+ } else if (e.key === "Tab") {
1042
+ e.preventDefault();
1043
+ confirmEdit();
1044
+ }
1045
+ };
1046
+ return /* @__PURE__ */ jsxRuntime.jsx(
1047
+ "input",
1048
+ {
1049
+ ref: inputRef,
1050
+ "data-fancy-sheets-cell-editor": "",
1051
+ className: "absolute z-20 border-2 border-blue-500 bg-white px-1 text-[13px] outline-none dark:bg-zinc-800 dark:text-zinc-100",
1052
+ style: { width: Math.max(width, 60), height: rowHeight },
1053
+ value: editValue,
1054
+ onChange: (e) => updateEdit(e.target.value),
1055
+ onKeyDown: handleKeyDown,
1056
+ onBlur: confirmEdit
1057
+ }
1058
+ );
1059
+ }
1060
+ CellEditor.displayName = "CellEditor";
1061
+ function SelectionOverlay() {
1062
+ const { selection, getColumnWidth, rowHeight } = useSpreadsheet();
1063
+ const rects = react.useMemo(() => {
1064
+ return selection.ranges.map((range, i) => {
1065
+ const norm = normalizeRange(range.start, range.end);
1066
+ const s = parseAddress(norm.start);
1067
+ const e = parseAddress(norm.end);
1068
+ let left = 48;
1069
+ for (let c = 0; c < s.col; c++) left += getColumnWidth(c);
1070
+ let width = 0;
1071
+ for (let c = s.col; c <= e.col; c++) width += getColumnWidth(c);
1072
+ const top = rowHeight + s.row * rowHeight;
1073
+ const height = (e.row - s.row + 1) * rowHeight;
1074
+ return { left, top, width, height, isPrimary: i === 0 };
1075
+ });
1076
+ }, [selection.ranges, getColumnWidth, rowHeight]);
1077
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: rects.map((rect, i) => /* @__PURE__ */ jsxRuntime.jsx(
1078
+ "div",
1079
+ {
1080
+ "data-fancy-sheets-selection": "",
1081
+ className: "pointer-events-none absolute border-2 border-blue-500",
1082
+ style: {
1083
+ left: rect.left,
1084
+ top: rect.top,
1085
+ width: rect.width,
1086
+ height: rect.height,
1087
+ backgroundColor: rect.isPrimary ? "rgba(59, 130, 246, 0.08)" : "rgba(59, 130, 246, 0.05)"
1088
+ }
1089
+ },
1090
+ i
1091
+ )) });
1092
+ }
1093
+ SelectionOverlay.displayName = "SelectionOverlay";
1094
+
1095
+ // src/engine/clipboard.ts
1096
+ function cellsToTSV(cells, range) {
1097
+ const norm = normalizeRange(range.start, range.end);
1098
+ const s = parseAddress(norm.start);
1099
+ const e = parseAddress(norm.end);
1100
+ const rows = [];
1101
+ for (let r = s.row; r <= e.row; r++) {
1102
+ const cols = [];
1103
+ for (let c = s.col; c <= e.col; c++) {
1104
+ const addr = toAddress(r, c);
1105
+ const cell = cells[addr];
1106
+ if (!cell) {
1107
+ cols.push("");
1108
+ } else if (cell.computedValue !== void 0 && cell.computedValue !== null) {
1109
+ cols.push(String(cell.computedValue));
1110
+ } else if (cell.value !== null) {
1111
+ cols.push(String(cell.value));
1112
+ } else {
1113
+ cols.push("");
1114
+ }
1115
+ }
1116
+ rows.push(cols.join(" "));
1117
+ }
1118
+ return rows.join("\n");
1119
+ }
1120
+ function tsvToCells(tsv) {
1121
+ const lines = tsv.split("\n");
1122
+ const values = lines.map((line) => line.split(" "));
1123
+ const rows = values.length;
1124
+ const cols = Math.max(...values.map((v) => v.length));
1125
+ return { values, rows, cols };
1126
+ }
1127
+ function SpreadsheetGrid({ className }) {
1128
+ const {
1129
+ columnCount,
1130
+ rowCount,
1131
+ rowHeight,
1132
+ getColumnWidth,
1133
+ selection,
1134
+ editingCell,
1135
+ readOnly,
1136
+ activeSheet,
1137
+ navigate,
1138
+ startEdit,
1139
+ confirmEdit,
1140
+ cancelEdit,
1141
+ setCellValue,
1142
+ undo,
1143
+ redo
1144
+ } = useSpreadsheet();
1145
+ const containerRef = react.useRef(null);
1146
+ const handleKeyDown = react.useCallback(
1147
+ (e) => {
1148
+ if (editingCell) return;
1149
+ if (e.key === "ArrowUp") {
1150
+ e.preventDefault();
1151
+ navigate("up", e.shiftKey);
1152
+ return;
1153
+ }
1154
+ if (e.key === "ArrowDown") {
1155
+ e.preventDefault();
1156
+ navigate("down", e.shiftKey);
1157
+ return;
1158
+ }
1159
+ if (e.key === "ArrowLeft") {
1160
+ e.preventDefault();
1161
+ navigate("left", e.shiftKey);
1162
+ return;
1163
+ }
1164
+ if (e.key === "ArrowRight") {
1165
+ e.preventDefault();
1166
+ navigate("right", e.shiftKey);
1167
+ return;
1168
+ }
1169
+ if (e.key === "Tab") {
1170
+ e.preventDefault();
1171
+ navigate(e.shiftKey ? "left" : "right");
1172
+ return;
1173
+ }
1174
+ if (e.key === "Enter") {
1175
+ e.preventDefault();
1176
+ if (!readOnly) startEdit();
1177
+ return;
1178
+ }
1179
+ if ((e.ctrlKey || e.metaKey) && e.key === "c") {
1180
+ e.preventDefault();
1181
+ const range = selection.ranges[0];
1182
+ if (range) {
1183
+ const tsv = cellsToTSV(activeSheet.cells, range);
1184
+ navigator.clipboard.writeText(tsv);
1185
+ }
1186
+ return;
1187
+ }
1188
+ if ((e.ctrlKey || e.metaKey) && e.key === "v") {
1189
+ e.preventDefault();
1190
+ navigator.clipboard.readText().then((text) => {
1191
+ if (!text) return;
1192
+ const { values } = tsvToCells(text);
1193
+ const { row: startRow, col: startCol } = parseAddress(selection.activeCell);
1194
+ for (let r = 0; r < values.length; r++) {
1195
+ for (let c = 0; c < values[r].length; c++) {
1196
+ const addr = toAddress(startRow + r, startCol + c);
1197
+ setCellValue(addr, values[r][c]);
1198
+ }
1199
+ }
1200
+ });
1201
+ return;
1202
+ }
1203
+ if ((e.ctrlKey || e.metaKey) && e.key === "z") {
1204
+ e.preventDefault();
1205
+ undo();
1206
+ return;
1207
+ }
1208
+ if ((e.ctrlKey || e.metaKey) && e.key === "y") {
1209
+ e.preventDefault();
1210
+ redo();
1211
+ return;
1212
+ }
1213
+ if (!readOnly && !e.ctrlKey && !e.metaKey && !e.altKey && e.key.length === 1) {
1214
+ startEdit(e.key);
1215
+ }
1216
+ if (!readOnly && (e.key === "Delete" || e.key === "Backspace")) {
1217
+ e.preventDefault();
1218
+ startEdit("");
1219
+ setTimeout(() => confirmEdit(), 0);
1220
+ }
1221
+ },
1222
+ [editingCell, readOnly, navigate, startEdit, confirmEdit, undo, redo]
1223
+ );
1224
+ const editorPosition = editingCell ? (() => {
1225
+ const { row, col } = parseAddress(editingCell);
1226
+ let left = 48;
1227
+ for (let c = 0; c < col; c++) left += getColumnWidth(c);
1228
+ const top = rowHeight + row * rowHeight;
1229
+ return { left, top };
1230
+ })() : null;
1231
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1232
+ "div",
1233
+ {
1234
+ ref: containerRef,
1235
+ "data-fancy-sheets-grid": "",
1236
+ className: reactFancy.cn("relative flex-1 overflow-auto bg-white focus:outline-none dark:bg-zinc-900", className),
1237
+ tabIndex: 0,
1238
+ onKeyDown: handleKeyDown,
1239
+ children: [
1240
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-10", children: /* @__PURE__ */ jsxRuntime.jsx(ColumnHeaders, {}) }),
1241
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
1242
+ Array.from({ length: rowCount }, (_, rowIdx) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex", children: [
1243
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsxRuntime.jsx(RowHeader, { rowIndex: rowIdx }) }),
1244
+ Array.from({ length: columnCount }, (_2, colIdx) => {
1245
+ const addr = toAddress(rowIdx, colIdx);
1246
+ return /* @__PURE__ */ jsxRuntime.jsx(Cell, { address: addr, row: rowIdx, col: colIdx }, addr);
1247
+ })
1248
+ ] }, rowIdx)),
1249
+ /* @__PURE__ */ jsxRuntime.jsx(SelectionOverlay, {}),
1250
+ editorPosition && /* @__PURE__ */ jsxRuntime.jsx(
1251
+ "div",
1252
+ {
1253
+ className: "absolute z-20",
1254
+ style: { left: editorPosition.left, top: editorPosition.top },
1255
+ children: /* @__PURE__ */ jsxRuntime.jsx(CellEditor, {})
1256
+ }
1257
+ )
1258
+ ] })
1259
+ ]
1260
+ }
1261
+ );
1262
+ }
1263
+ SpreadsheetGrid.displayName = "SpreadsheetGrid";
1264
+ var btnClass = "inline-flex items-center justify-center rounded px-2 py-1 text-[12px] font-medium text-zinc-600 transition-colors hover:bg-zinc-100 disabled:opacity-40 dark:text-zinc-300 dark:hover:bg-zinc-800";
1265
+ var activeBtnClass = "bg-zinc-200 dark:bg-zinc-700";
1266
+ function DefaultToolbar() {
1267
+ const {
1268
+ selection,
1269
+ activeSheet,
1270
+ editingCell,
1271
+ editValue,
1272
+ updateEdit,
1273
+ confirmEdit,
1274
+ startEdit,
1275
+ setCellFormat,
1276
+ undo,
1277
+ redo,
1278
+ canUndo,
1279
+ canRedo,
1280
+ readOnly
1281
+ } = useSpreadsheet();
1282
+ const cell = activeSheet.cells[selection.activeCell];
1283
+ const isBold = cell?.format?.bold ?? false;
1284
+ const isItalic = cell?.format?.italic ?? false;
1285
+ const textAlign = cell?.format?.textAlign ?? "left";
1286
+ const selectedAddresses = [selection.activeCell];
1287
+ const handleFormulaBarChange = (e) => {
1288
+ if (editingCell) {
1289
+ updateEdit(e.target.value);
1290
+ } else {
1291
+ startEdit(e.target.value);
1292
+ }
1293
+ };
1294
+ const handleFormulaBarKeyDown = (e) => {
1295
+ if (e.key === "Enter") {
1296
+ e.preventDefault();
1297
+ confirmEdit();
1298
+ }
1299
+ };
1300
+ const formulaBarValue = editingCell ? editValue : cell?.formula ? "=" + cell.formula : cell?.value != null ? String(cell.value) : "";
1301
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1302
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 border-b border-zinc-200 px-1.5 py-1 dark:border-zinc-700", children: [
1303
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: btnClass, onClick: undo, disabled: !canUndo || readOnly, title: "Undo (Ctrl+Z)", children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1304
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "1 4 1 10 7 10" }),
1305
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })
1306
+ ] }) }),
1307
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: btnClass, onClick: redo, disabled: !canRedo || readOnly, title: "Redo (Ctrl+Y)", children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1308
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "23 4 23 10 17 10" }),
1309
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20.49 15a9 9 0 1 1-2.12-9.36L23 10" })
1310
+ ] }) }),
1311
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-1 h-4 w-px bg-zinc-200 dark:bg-zinc-700" }),
1312
+ /* @__PURE__ */ jsxRuntime.jsx(
1313
+ "button",
1314
+ {
1315
+ className: reactFancy.cn(btnClass, isBold && activeBtnClass),
1316
+ onClick: () => setCellFormat(selectedAddresses, { bold: !isBold }),
1317
+ disabled: readOnly,
1318
+ title: "Bold",
1319
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-bold", children: "B" })
1320
+ }
1321
+ ),
1322
+ /* @__PURE__ */ jsxRuntime.jsx(
1323
+ "button",
1324
+ {
1325
+ className: reactFancy.cn(btnClass, isItalic && activeBtnClass),
1326
+ onClick: () => setCellFormat(selectedAddresses, { italic: !isItalic }),
1327
+ disabled: readOnly,
1328
+ title: "Italic",
1329
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "italic", children: "I" })
1330
+ }
1331
+ ),
1332
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-1 h-4 w-px bg-zinc-200 dark:bg-zinc-700" }),
1333
+ ["left", "center", "right"].map((align) => /* @__PURE__ */ jsxRuntime.jsx(
1334
+ "button",
1335
+ {
1336
+ className: reactFancy.cn(btnClass, textAlign === align && activeBtnClass),
1337
+ onClick: () => setCellFormat(selectedAddresses, { textAlign: align }),
1338
+ disabled: readOnly,
1339
+ title: `Align ${align}`,
1340
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
1341
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "6", x2: "21", y2: "6" }),
1342
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: align === "left" ? "3" : align === "center" ? "6" : "9", y1: "12", x2: align === "left" ? "15" : align === "center" ? "18" : "21", y2: "12" }),
1343
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "18", x2: "21", y2: "18" })
1344
+ ] })
1345
+ },
1346
+ align
1347
+ ))
1348
+ ] }),
1349
+ /* @__PURE__ */ jsxRuntime.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: [
1350
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-12 shrink-0 text-center text-[11px] font-medium text-zinc-500 dark:text-zinc-400", children: selection.activeCell }),
1351
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-px bg-zinc-200 dark:bg-zinc-700" }),
1352
+ /* @__PURE__ */ jsxRuntime.jsx(
1353
+ "input",
1354
+ {
1355
+ className: "flex-1 bg-transparent text-[13px] outline-none dark:text-zinc-100",
1356
+ value: formulaBarValue,
1357
+ onChange: handleFormulaBarChange,
1358
+ onKeyDown: handleFormulaBarKeyDown,
1359
+ readOnly,
1360
+ placeholder: "Enter value or formula (=SUM(A1:A5))"
1361
+ }
1362
+ )
1363
+ ] })
1364
+ ] });
1365
+ }
1366
+ function SpreadsheetToolbar({ children, className }) {
1367
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-fancy-sheets-toolbar": "", className: reactFancy.cn("", className), children: children ?? /* @__PURE__ */ jsxRuntime.jsx(DefaultToolbar, {}) });
1368
+ }
1369
+ SpreadsheetToolbar.displayName = "SpreadsheetToolbar";
1370
+ function SpreadsheetSheetTabs({ className }) {
1371
+ const { workbook, setActiveSheet, addSheet, renameSheet, deleteSheet, readOnly } = useSpreadsheet();
1372
+ const [renamingId, setRenamingId] = react.useState(null);
1373
+ const [renameValue, setRenameValue] = react.useState("");
1374
+ const handleDoubleClick = react.useCallback(
1375
+ (sheetId, name) => {
1376
+ if (readOnly) return;
1377
+ setRenamingId(sheetId);
1378
+ setRenameValue(name);
1379
+ },
1380
+ [readOnly]
1381
+ );
1382
+ const handleRenameConfirm = react.useCallback(() => {
1383
+ if (renamingId && renameValue.trim()) {
1384
+ renameSheet(renamingId, renameValue.trim());
1385
+ }
1386
+ setRenamingId(null);
1387
+ }, [renamingId, renameValue, renameSheet]);
1388
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1389
+ "div",
1390
+ {
1391
+ "data-fancy-sheets-tabs": "",
1392
+ className: reactFancy.cn(
1393
+ "flex items-center gap-0.5 border-t border-zinc-200 bg-zinc-50 px-2 py-1 dark:border-zinc-700 dark:bg-zinc-900/50",
1394
+ className
1395
+ ),
1396
+ children: [
1397
+ workbook.sheets.map((sheet) => {
1398
+ const isActive = sheet.id === workbook.activeSheetId;
1399
+ const isRenaming = renamingId === sheet.id;
1400
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center", children: [
1401
+ isRenaming ? /* @__PURE__ */ jsxRuntime.jsx(
1402
+ "input",
1403
+ {
1404
+ className: "rounded border border-blue-500 bg-white px-2 py-0.5 text-[12px] outline-none dark:bg-zinc-800 dark:text-zinc-100",
1405
+ value: renameValue,
1406
+ onChange: (e) => setRenameValue(e.target.value),
1407
+ onBlur: handleRenameConfirm,
1408
+ onKeyDown: (e) => {
1409
+ if (e.key === "Enter") handleRenameConfirm();
1410
+ if (e.key === "Escape") setRenamingId(null);
1411
+ },
1412
+ autoFocus: true
1413
+ }
1414
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1415
+ "button",
1416
+ {
1417
+ className: reactFancy.cn(
1418
+ "rounded px-3 py-1 text-[12px] font-medium transition-colors",
1419
+ isActive ? "bg-white text-zinc-900 shadow-sm dark:bg-zinc-800 dark:text-zinc-100" : "text-zinc-500 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800"
1420
+ ),
1421
+ onClick: () => setActiveSheet(sheet.id),
1422
+ onDoubleClick: () => handleDoubleClick(sheet.id, sheet.name),
1423
+ children: sheet.name
1424
+ }
1425
+ ),
1426
+ !readOnly && workbook.sheets.length > 1 && isActive && !isRenaming && /* @__PURE__ */ jsxRuntime.jsx(
1427
+ "button",
1428
+ {
1429
+ className: "ml-0.5 rounded p-0.5 text-zinc-400 hover:bg-zinc-200 hover:text-zinc-600 dark:hover:bg-zinc-700 dark:hover:text-zinc-300",
1430
+ onClick: () => deleteSheet(sheet.id),
1431
+ title: "Delete sheet",
1432
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", children: [
1433
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1434
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1435
+ ] })
1436
+ }
1437
+ )
1438
+ ] }, sheet.id);
1439
+ }),
1440
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
1441
+ "button",
1442
+ {
1443
+ className: "rounded px-2 py-1 text-[12px] font-medium text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300",
1444
+ onClick: addSheet,
1445
+ title: "Add sheet",
1446
+ children: "+"
1447
+ }
1448
+ )
1449
+ ]
1450
+ }
1451
+ );
1452
+ }
1453
+ SpreadsheetSheetTabs.displayName = "SpreadsheetSheetTabs";
1454
+ function SpreadsheetRoot({
1455
+ children,
1456
+ className,
1457
+ data,
1458
+ defaultData,
1459
+ onChange,
1460
+ columnCount = 26,
1461
+ rowCount = 100,
1462
+ defaultColumnWidth = 100,
1463
+ rowHeight = 28,
1464
+ readOnly = false
1465
+ }) {
1466
+ const { state, actions } = useSpreadsheetStore(data ?? defaultData);
1467
+ react.useEffect(() => {
1468
+ if (data && data !== state.workbook) {
1469
+ actions.setWorkbook(data);
1470
+ }
1471
+ }, [data]);
1472
+ react.useEffect(() => {
1473
+ onChange?.(state.workbook);
1474
+ }, [state.workbook]);
1475
+ const activeSheet = react.useMemo(
1476
+ () => state.workbook.sheets.find((s) => s.id === state.workbook.activeSheetId),
1477
+ [state.workbook]
1478
+ );
1479
+ const getColumnWidth = react.useCallback(
1480
+ (col) => activeSheet.columnWidths[col] ?? defaultColumnWidth,
1481
+ [activeSheet.columnWidths, defaultColumnWidth]
1482
+ );
1483
+ const isCellSelected = react.useCallback(
1484
+ (address) => {
1485
+ const target = parseAddress(address);
1486
+ return state.selection.ranges.some((range) => {
1487
+ const norm = normalizeRange(range.start, range.end);
1488
+ const s = parseAddress(norm.start);
1489
+ const e = parseAddress(norm.end);
1490
+ return target.row >= s.row && target.row <= e.row && target.col >= s.col && target.col <= e.col;
1491
+ });
1492
+ },
1493
+ [state.selection.ranges]
1494
+ );
1495
+ const isCellActive = react.useCallback(
1496
+ (address) => state.selection.activeCell === address,
1497
+ [state.selection.activeCell]
1498
+ );
1499
+ const ctx = react.useMemo(
1500
+ () => ({
1501
+ workbook: state.workbook,
1502
+ activeSheet,
1503
+ columnCount,
1504
+ rowCount,
1505
+ defaultColumnWidth,
1506
+ rowHeight,
1507
+ readOnly,
1508
+ selection: state.selection,
1509
+ editingCell: state.editingCell,
1510
+ editValue: state.editValue,
1511
+ ...actions,
1512
+ canUndo: state.undoStack.length > 0,
1513
+ canRedo: state.redoStack.length > 0,
1514
+ getColumnWidth,
1515
+ isCellSelected,
1516
+ isCellActive
1517
+ }),
1518
+ [state, activeSheet, columnCount, rowCount, defaultColumnWidth, rowHeight, readOnly, actions, getColumnWidth, isCellSelected, isCellActive]
1519
+ );
1520
+ return /* @__PURE__ */ jsxRuntime.jsx(SpreadsheetContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
1521
+ "div",
1522
+ {
1523
+ "data-fancy-sheets": "",
1524
+ className: reactFancy.cn("flex flex-col overflow-hidden", className),
1525
+ children
1526
+ }
1527
+ ) });
1528
+ }
1529
+ SpreadsheetRoot.displayName = "Spreadsheet";
1530
+ var Spreadsheet = Object.assign(SpreadsheetRoot, {
1531
+ Toolbar: SpreadsheetToolbar,
1532
+ Grid: SpreadsheetGrid,
1533
+ SheetTabs: SpreadsheetSheetTabs
1534
+ });
1535
+
1536
+ // src/engine/csv.ts
1537
+ function parseCSV(text) {
1538
+ const rows = [];
1539
+ let current = "";
1540
+ let inQuotes = false;
1541
+ let row = [];
1542
+ for (let i = 0; i < text.length; i++) {
1543
+ const ch = text[i];
1544
+ if (inQuotes) {
1545
+ if (ch === '"' && text[i + 1] === '"') {
1546
+ current += '"';
1547
+ i++;
1548
+ } else if (ch === '"') {
1549
+ inQuotes = false;
1550
+ } else {
1551
+ current += ch;
1552
+ }
1553
+ } else {
1554
+ if (ch === '"') {
1555
+ inQuotes = true;
1556
+ } else if (ch === ",") {
1557
+ row.push(current);
1558
+ current = "";
1559
+ } else if (ch === "\n" || ch === "\r" && text[i + 1] === "\n") {
1560
+ row.push(current);
1561
+ current = "";
1562
+ rows.push(row);
1563
+ row = [];
1564
+ if (ch === "\r") i++;
1565
+ } else {
1566
+ current += ch;
1567
+ }
1568
+ }
1569
+ }
1570
+ row.push(current);
1571
+ if (row.some((c) => c !== "")) rows.push(row);
1572
+ return rows;
1573
+ }
1574
+ function stringifyCSV(data) {
1575
+ return data.map(
1576
+ (row) => row.map((cell) => {
1577
+ if (cell.includes(",") || cell.includes('"') || cell.includes("\n")) {
1578
+ return '"' + cell.replace(/"/g, '""') + '"';
1579
+ }
1580
+ return cell;
1581
+ }).join(",")
1582
+ ).join("\n");
1583
+ }
1584
+ function csvToWorkbook(csv, sheetName = "Sheet 1") {
1585
+ const data = parseCSV(csv);
1586
+ const sheet = createEmptySheet("sheet-1", sheetName);
1587
+ for (let r = 0; r < data.length; r++) {
1588
+ for (let c = 0; c < data[r].length; c++) {
1589
+ const val = data[r][c];
1590
+ if (val === "") continue;
1591
+ const addr = toAddress(r, c);
1592
+ const numVal = Number(val);
1593
+ sheet.cells[addr] = { value: isNaN(numVal) || val === "" ? val : numVal };
1594
+ }
1595
+ }
1596
+ return { sheets: [sheet], activeSheetId: sheet.id };
1597
+ }
1598
+ function workbookToCSV(workbook, sheetId) {
1599
+ const sheet = sheetId ? workbook.sheets.find((s) => s.id === sheetId) : workbook.sheets.find((s) => s.id === workbook.activeSheetId);
1600
+ if (!sheet) return "";
1601
+ let maxRow = 0;
1602
+ let maxCol = 0;
1603
+ for (const addr of Object.keys(sheet.cells)) {
1604
+ const match = addr.match(/^([A-Z]+)(\d+)$/);
1605
+ if (!match) continue;
1606
+ const col = match[1].split("").reduce((acc, ch) => acc * 26 + ch.charCodeAt(0) - 64, 0) - 1;
1607
+ const row = parseInt(match[2], 10) - 1;
1608
+ maxRow = Math.max(maxRow, row);
1609
+ maxCol = Math.max(maxCol, col);
1610
+ }
1611
+ const data = [];
1612
+ for (let r = 0; r <= maxRow; r++) {
1613
+ const row = [];
1614
+ for (let c = 0; c <= maxCol; c++) {
1615
+ const addr = toAddress(r, c);
1616
+ const cell = sheet.cells[addr];
1617
+ if (!cell) {
1618
+ row.push("");
1619
+ } else if (cell.computedValue !== void 0 && cell.computedValue !== null) {
1620
+ row.push(String(cell.computedValue));
1621
+ } else if (cell.value !== null) {
1622
+ row.push(String(cell.value));
1623
+ } else {
1624
+ row.push("");
1625
+ }
1626
+ }
1627
+ data.push(row);
1628
+ }
1629
+ return stringifyCSV(data);
1630
+ }
1631
+
1632
+ exports.Spreadsheet = Spreadsheet;
1633
+ exports.columnToLetter = columnToLetter;
1634
+ exports.createEmptySheet = createEmptySheet;
1635
+ exports.createEmptyWorkbook = createEmptyWorkbook;
1636
+ exports.csvToWorkbook = csvToWorkbook;
1637
+ exports.letterToColumn = letterToColumn;
1638
+ exports.parseAddress = parseAddress;
1639
+ exports.parseCSV = parseCSV;
1640
+ exports.registerFunction = registerFunction;
1641
+ exports.stringifyCSV = stringifyCSV;
1642
+ exports.toAddress = toAddress;
1643
+ exports.useSpreadsheet = useSpreadsheet;
1644
+ exports.workbookToCSV = workbookToCSV;
1645
+ //# sourceMappingURL=index.cjs.map
1646
+ //# sourceMappingURL=index.cjs.map