@pixerate/editor-react 0.16.0 → 0.18.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.
Files changed (46) hide show
  1. package/README.md +4 -2
  2. package/dist/canvas/index.cjs +1094 -143
  3. package/dist/canvas/index.cjs.map +1 -1
  4. package/dist/canvas/index.d.cts +277 -3
  5. package/dist/canvas/index.d.ts +277 -3
  6. package/dist/canvas/index.js +976 -0
  7. package/dist/canvas/index.js.map +1 -1
  8. package/dist/chunk-34HEZX7J.js +2144 -0
  9. package/dist/chunk-34HEZX7J.js.map +1 -0
  10. package/dist/chunk-I3E4KSA2.js +515 -0
  11. package/dist/chunk-I3E4KSA2.js.map +1 -0
  12. package/dist/{chunk-XTFR3IMK.js → chunk-ZU54BMDC.js} +473 -160
  13. package/dist/chunk-ZU54BMDC.js.map +1 -0
  14. package/dist/generation/index.cjs +290 -0
  15. package/dist/generation/index.cjs.map +1 -1
  16. package/dist/generation/index.d.cts +66 -2
  17. package/dist/generation/index.d.ts +66 -2
  18. package/dist/generation/index.js +7 -1
  19. package/dist/image-editor/index.cjs +490 -187
  20. package/dist/image-editor/index.cjs.map +1 -1
  21. package/dist/image-editor/index.d.cts +34 -2
  22. package/dist/image-editor/index.d.ts +34 -2
  23. package/dist/image-editor/index.js +5 -1
  24. package/dist/index.cjs +2792 -823
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +188 -7
  27. package/dist/index.d.ts +188 -7
  28. package/dist/index.js +600 -158
  29. package/dist/index.js.map +1 -1
  30. package/dist/spreadsheet/index.cjs +1099 -142
  31. package/dist/spreadsheet/index.cjs.map +1 -1
  32. package/dist/spreadsheet/index.d.cts +156 -8
  33. package/dist/spreadsheet/index.d.ts +156 -8
  34. package/dist/spreadsheet/index.js +15 -3
  35. package/dist/tokens/index.cjs +395 -0
  36. package/dist/tokens/index.cjs.map +1 -0
  37. package/dist/tokens/index.d.cts +54 -0
  38. package/dist/tokens/index.d.ts +54 -0
  39. package/dist/tokens/index.js +375 -0
  40. package/dist/tokens/index.js.map +1 -0
  41. package/package.json +10 -2
  42. package/dist/chunk-7SPRQZ6H.js +0 -216
  43. package/dist/chunk-7SPRQZ6H.js.map +0 -1
  44. package/dist/chunk-FYIPXHAE.js +0 -1169
  45. package/dist/chunk-FYIPXHAE.js.map +0 -1
  46. package/dist/chunk-XTFR3IMK.js.map +0 -1
@@ -0,0 +1,2144 @@
1
+ // src/spreadsheet/useSpreadsheetEditor.ts
2
+ import { useState, useRef, useCallback, useMemo } from "react";
3
+ import {
4
+ SpreadsheetController,
5
+ bindSpreadsheetTableActions
6
+ } from "@pixerate/editor/spreadsheet";
7
+ function useSpreadsheetEditor(options = {}) {
8
+ const optionsRef = useRef(options);
9
+ optionsRef.current = options;
10
+ const settersRef = useRef({});
11
+ const [controller] = useState(
12
+ () => new SpreadsheetController({
13
+ ...options,
14
+ onDocumentChange: (updated) => {
15
+ settersRef.current.setDoc?.({ ...updated });
16
+ optionsRef.current.onDocumentChange?.(updated);
17
+ },
18
+ onSelectionChange: (active, range) => {
19
+ settersRef.current.setActiveCell?.(active);
20
+ settersRef.current.setSelectedRange?.(range);
21
+ optionsRef.current.onSelectionChange?.(active, range);
22
+ },
23
+ onCellCommit: (rowId, colId, raw, value) => {
24
+ optionsRef.current.onCellCommit?.(rowId, colId, raw, value);
25
+ }
26
+ })
27
+ );
28
+ const [doc, setDoc] = useState(() => ({ ...controller.document }));
29
+ const [activeCell, setActiveCell] = useState(controller.activeCell);
30
+ const [selectedRange, setSelectedRange] = useState(controller.selectedRange);
31
+ const [editingCell, setEditingCell] = useState(null);
32
+ const [draftValue, setDraftState] = useState("");
33
+ const [validationPrompt, setValidationPrompt] = useState(null);
34
+ settersRef.current = { setDoc, setActiveCell, setSelectedRange };
35
+ const setDraftValue = useCallback((value) => {
36
+ controller.draftValue = value;
37
+ setDraftState(value);
38
+ }, [controller]);
39
+ const setCellValue = useCallback((rowId, colId, raw) => {
40
+ controller.setCellValue(rowId, colId, raw);
41
+ setDoc({ ...controller.document });
42
+ }, [controller]);
43
+ const selectCell = useCallback((row, col, extendSelection = false) => {
44
+ controller.selectCell(row, col, extendSelection);
45
+ setActiveCell(controller.activeCell);
46
+ setSelectedRange(controller.selectedRange);
47
+ }, [controller]);
48
+ const selectRange = useCallback((range) => {
49
+ controller.selectRange(range);
50
+ setSelectedRange(controller.selectedRange);
51
+ }, [controller]);
52
+ const startEditing = useCallback((row, col, initialVal) => {
53
+ controller.startEditing(row, col, initialVal);
54
+ setActiveCell(controller.activeCell);
55
+ setEditingCell(controller.editingCell);
56
+ setDraftValue(controller.draftValue);
57
+ }, [controller]);
58
+ const syncEditing = useCallback(() => {
59
+ setEditingCell(controller.editingCell);
60
+ setDraftValue(controller.draftValue);
61
+ setValidationPrompt(controller.validationPrompt);
62
+ setActiveCell(controller.activeCell);
63
+ setSelectedRange(controller.selectedRange);
64
+ setDoc({ ...controller.document });
65
+ }, [controller]);
66
+ const commitEditing = useCallback((val) => {
67
+ const result = controller.commitEditing(val);
68
+ syncEditing();
69
+ return result;
70
+ }, [controller, syncEditing]);
71
+ const commitCellInput = useCallback((rowId, colId, raw) => {
72
+ const result = controller.commitCellInput(rowId, colId, raw);
73
+ syncEditing();
74
+ return result;
75
+ }, [controller, syncEditing]);
76
+ const resolveValidationPrompt = useCallback((action) => {
77
+ controller.resolveValidationPrompt(action);
78
+ syncEditing();
79
+ }, [controller, syncEditing]);
80
+ const cancelEditing = useCallback(() => {
81
+ controller.cancelEditing();
82
+ setEditingCell(null);
83
+ setDraftValue("");
84
+ }, [controller]);
85
+ const moveCursor = useCallback((dir, extend = false) => {
86
+ controller.moveCursor(dir, extend);
87
+ setActiveCell(controller.activeCell);
88
+ setSelectedRange(controller.selectedRange);
89
+ }, [controller]);
90
+ const insertColumn = useCallback((index, config) => {
91
+ controller.insertColumn(index, config);
92
+ setDoc({ ...controller.document });
93
+ }, [controller]);
94
+ const deleteColumn = useCallback((colId) => {
95
+ controller.deleteColumn(colId);
96
+ setDoc({ ...controller.document });
97
+ }, [controller]);
98
+ const insertRow = useCallback((index, config) => {
99
+ controller.insertRow(index, config);
100
+ setDoc({ ...controller.document });
101
+ }, [controller]);
102
+ const deleteRow = useCallback((rowId) => {
103
+ controller.deleteRow(rowId);
104
+ setDoc({ ...controller.document });
105
+ }, [controller]);
106
+ const setColumnWidth = useCallback((colId, width) => {
107
+ controller.setColumnWidth(colId, width);
108
+ setDoc({ ...controller.document });
109
+ }, [controller]);
110
+ const autoFitColumnWidth = useCallback((colId, customWidth) => {
111
+ controller.autoFitColumnWidth(colId, customWidth);
112
+ setDoc({ ...controller.document });
113
+ }, [controller]);
114
+ const registerDataSource = useCallback((ds) => {
115
+ controller.registerDataSource(ds);
116
+ setDoc({ ...controller.document });
117
+ }, [controller]);
118
+ const setPrimaryDataSource = useCallback((id) => {
119
+ controller.setPrimaryDataSource(id);
120
+ setDoc({ ...controller.document });
121
+ }, [controller]);
122
+ const recalculateAll = useCallback(() => {
123
+ controller.recalculateAll();
124
+ setDoc({ ...controller.document });
125
+ }, [controller]);
126
+ const undo = useCallback(() => {
127
+ controller.undo();
128
+ setDoc({ ...controller.document });
129
+ setActiveCell(controller.activeCell);
130
+ setSelectedRange(controller.selectedRange);
131
+ }, [controller]);
132
+ const redo = useCallback(() => {
133
+ controller.redo();
134
+ setDoc({ ...controller.document });
135
+ setActiveCell(controller.activeCell);
136
+ setSelectedRange(controller.selectedRange);
137
+ }, [controller]);
138
+ const tableActions = useMemo(() => bindSpreadsheetTableActions(controller), [controller]);
139
+ const viewModel = useMemo(() => controller.getViewModel(), [controller, doc]);
140
+ const sheets = useMemo(() => controller.getSheets(), [controller, doc]);
141
+ const names = useMemo(() => controller.getNames(), [controller, doc]);
142
+ const afterSheetSwitch = useCallback(() => {
143
+ setEditingCell(controller.editingCell);
144
+ setDraftValue(controller.draftValue);
145
+ setActiveCell(controller.activeCell);
146
+ setSelectedRange(controller.selectedRange);
147
+ setDoc({ ...controller.document });
148
+ }, [controller, setDraftValue]);
149
+ const workbookActions = useMemo(
150
+ () => ({
151
+ setActiveSheet: (idOrName) => {
152
+ const changed = controller.setActiveSheet(idOrName);
153
+ afterSheetSwitch();
154
+ return changed;
155
+ },
156
+ addSheet: (options2) => {
157
+ const id = controller.addSheet(options2);
158
+ afterSheetSwitch();
159
+ return id;
160
+ },
161
+ renameSheet: (idOrName, newName) => controller.renameSheet(idOrName, newName),
162
+ moveSheet: (idOrName, toIndex) => controller.moveSheet(idOrName, toIndex),
163
+ deleteSheet: (idOrName) => {
164
+ const deleted = controller.deleteSheet(idOrName);
165
+ afterSheetSwitch();
166
+ return deleted;
167
+ },
168
+ duplicateSheet: (idOrName, options2) => {
169
+ const id = controller.duplicateSheet(idOrName, options2);
170
+ afterSheetSwitch();
171
+ return id;
172
+ },
173
+ defineName: (name, refersTo, options2) => controller.defineName(name, refersTo, options2),
174
+ renameName: (oldName, newName, options2) => controller.renameName(oldName, newName, options2),
175
+ deleteName: (name, options2) => controller.deleteName(name, options2)
176
+ }),
177
+ [controller, afterSheetSwitch]
178
+ );
179
+ const conditionalFormatOverlay = useMemo(() => controller.getConditionalFormatOverlay(), [controller, doc]);
180
+ return {
181
+ document: doc,
182
+ activeCell,
183
+ selectedRange,
184
+ editingCell,
185
+ draftValue,
186
+ setDraftValue,
187
+ validationPrompt,
188
+ resolveValidationPrompt,
189
+ commitCellInput,
190
+ controller,
191
+ getCell: (rowId, colId) => controller.getCell(rowId, colId),
192
+ setCellValue,
193
+ selectCell,
194
+ selectRange,
195
+ startEditing,
196
+ commitEditing,
197
+ cancelEditing,
198
+ moveCursor,
199
+ insertColumn,
200
+ deleteColumn,
201
+ insertRow,
202
+ deleteRow,
203
+ setColumnWidth,
204
+ autoFitColumnWidth,
205
+ loadDocument: (document2) => {
206
+ controller.loadDocument(document2);
207
+ setDoc({ ...controller.document });
208
+ setActiveCell(null);
209
+ setSelectedRange(null);
210
+ setEditingCell(null);
211
+ setDraftValue("");
212
+ },
213
+ syncWithDataSource: () => {
214
+ controller.syncWithDataSource();
215
+ controller.recalculateAll();
216
+ setDoc({ ...controller.document });
217
+ },
218
+ registerDataSource,
219
+ setPrimaryDataSource,
220
+ recalculateAll,
221
+ exportToTsv: (range) => controller.exportToTsv(range),
222
+ importFromTsv: (data) => {
223
+ controller.importFromTsv(data);
224
+ setDoc({ ...controller.document });
225
+ },
226
+ ...tableActions,
227
+ viewModel,
228
+ /** The sheets in tab order (see `SpreadsheetController.getSheets`). */
229
+ sheets,
230
+ activeSheetId: controller.activeSheetId,
231
+ /** Defined names (see `SpreadsheetController.getNames`). */
232
+ names,
233
+ ...workbookActions,
234
+ conditionalFormatOverlay,
235
+ undo,
236
+ redo
237
+ };
238
+ }
239
+
240
+ // src/spreadsheet/SpreadsheetEditor.tsx
241
+ import { useRef as useRef8, useState as useState8, useMemo as useMemo5, useEffect as useEffect6, useCallback as useCallback4 } from "react";
242
+ import {
243
+ indexToColName,
244
+ getSpreadsheetMenuItems,
245
+ normalizeRange as normalizeRange2,
246
+ rangeContains,
247
+ formatCellForDisplay,
248
+ numberFormatColorToCss,
249
+ getConditionalFormatCss as getConditionalFormatCss2,
250
+ getConditionalFormatIcon,
251
+ getDataBarCss
252
+ } from "@pixerate/editor/spreadsheet";
253
+
254
+ // src/spreadsheet/SpreadsheetContextMenu.tsx
255
+ import React, { useEffect, useLayoutEffect, useRef as useRef2, useState as useState2 } from "react";
256
+ import { clampMenuPosition, runSpreadsheetMenuItem } from "@pixerate/editor/spreadsheet";
257
+ import { jsx, jsxs } from "react/jsx-runtime";
258
+ var VIEWPORT_MARGIN = 4;
259
+ var SpreadsheetContextMenu = ({ items, x, y, onClose, onOpenPanel, ariaLabel = "Table actions" }) => {
260
+ const menuRef = useRef2(null);
261
+ const onCloseRef = useRef2(onClose);
262
+ onCloseRef.current = onClose;
263
+ const [position, setPosition] = useState2({ x, y });
264
+ useLayoutEffect(() => {
265
+ const menu = menuRef.current;
266
+ if (!menu || typeof window === "undefined") return;
267
+ const { width, height } = menu.getBoundingClientRect();
268
+ const next = clampMenuPosition(
269
+ { x, y },
270
+ { width, height },
271
+ { width: window.innerWidth, height: window.innerHeight },
272
+ VIEWPORT_MARGIN
273
+ );
274
+ setPosition((prev) => prev.x === next.x && prev.y === next.y ? prev : next);
275
+ }, [x, y, items]);
276
+ useEffect(() => {
277
+ const handlePointer = (e) => {
278
+ if (!menuRef.current?.contains(e.target)) onCloseRef.current();
279
+ };
280
+ const handleKey = (e) => {
281
+ if (e.key === "Escape") onCloseRef.current();
282
+ };
283
+ window.addEventListener("mousedown", handlePointer);
284
+ window.addEventListener("keydown", handleKey);
285
+ return () => {
286
+ window.removeEventListener("mousedown", handlePointer);
287
+ window.removeEventListener("keydown", handleKey);
288
+ };
289
+ }, []);
290
+ return /* @__PURE__ */ jsx(
291
+ "div",
292
+ {
293
+ ref: menuRef,
294
+ role: "menu",
295
+ "aria-label": ariaLabel,
296
+ className: "fixed z-50 min-w-[14rem] rounded-md border border-border bg-popover py-1 text-xs text-popover-foreground shadow-lg",
297
+ style: {
298
+ left: position.x,
299
+ top: position.y,
300
+ maxHeight: `calc(100vh - ${VIEWPORT_MARGIN * 2}px)`,
301
+ overflowY: "auto",
302
+ background: "var(--popover, var(--background, #fff))"
303
+ },
304
+ onContextMenu: (e) => e.preventDefault(),
305
+ onKeyDown: (e) => e.stopPropagation(),
306
+ children: items.map((item) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
307
+ item.separatorBefore && /* @__PURE__ */ jsx("div", { role: "separator", className: "my-1 h-px bg-border" }),
308
+ /* @__PURE__ */ jsx(
309
+ "button",
310
+ {
311
+ type: "button",
312
+ role: "menuitem",
313
+ "data-action": item.id,
314
+ "data-checked": item.checked ? "" : void 0,
315
+ disabled: item.disabled,
316
+ className: `block w-full px-3 py-1 text-left hover:bg-muted disabled:cursor-default disabled:opacity-40 disabled:hover:bg-transparent ${item.checked ? "font-semibold" : ""}`,
317
+ onClick: () => {
318
+ onClose();
319
+ if (item.panel) onOpenPanel?.(item.panel);
320
+ else runSpreadsheetMenuItem(item);
321
+ },
322
+ children: item.label
323
+ }
324
+ )
325
+ ] }, item.id))
326
+ }
327
+ );
328
+ };
329
+
330
+ // src/spreadsheet/SpreadsheetGroupRow.tsx
331
+ import React2 from "react";
332
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
333
+ var formatSummary = (value) => value === null ? "\u2013" : String(value);
334
+ var SpreadsheetGroupRow = ({
335
+ group,
336
+ columns,
337
+ groupColId,
338
+ onToggle,
339
+ trailingCell,
340
+ labelColumns = columns,
341
+ leadingColumns,
342
+ leftSpace = 0,
343
+ rightSpace = 0
344
+ }) => {
345
+ const labelColId = labelColumns.some((c) => c.column.id === groupColId) ? groupColId : labelColumns[0]?.column.id;
346
+ const spacer = (key) => /* @__PURE__ */ jsx2("td", { "aria-hidden": "true", "data-spacer": "", style: { padding: 0 } }, key);
347
+ return /* @__PURE__ */ jsxs2("tr", { className: "border-b border-border bg-muted/40 font-semibold", "data-group-key": group.key, style: { height: 28 }, children: [
348
+ /* @__PURE__ */ jsx2("td", { className: "w-12 min-w-[3rem] sticky left-0 z-10 bg-muted border-r border-border text-center", children: /* @__PURE__ */ jsx2(
349
+ "button",
350
+ {
351
+ type: "button",
352
+ "aria-label": `${group.collapsed ? "Expand" : "Collapse"} group ${group.label}`,
353
+ "aria-expanded": !group.collapsed,
354
+ className: "h-5 w-5 rounded text-[10px] text-muted-foreground hover:bg-muted",
355
+ onClick: (e) => {
356
+ e.stopPropagation();
357
+ onToggle(group.key);
358
+ },
359
+ children: group.collapsed ? "\u25B8" : "\u25BE"
360
+ }
361
+ ) }),
362
+ columns.map(({ column }, i) => {
363
+ const summary = group.summaries[column.id];
364
+ const before = leadingColumns !== void 0 && i === leadingColumns && leftSpace > 0 ? spacer("space:left") : null;
365
+ return /* @__PURE__ */ jsxs2(React2.Fragment, { children: [
366
+ before,
367
+ /* @__PURE__ */ jsx2("td", { className: "border-r border-border px-2 py-1 truncate max-w-0 text-foreground", style: { maxWidth: 0, overflow: "hidden" }, children: column.id === labelColId ? /* @__PURE__ */ jsxs2("span", { className: "truncate", children: [
368
+ group.label,
369
+ " ",
370
+ /* @__PURE__ */ jsxs2("span", { className: "font-normal text-muted-foreground", children: [
371
+ "(",
372
+ group.rowIndices.length,
373
+ ")"
374
+ ] })
375
+ ] }) : summary !== void 0 ? /* @__PURE__ */ jsx2("span", { className: "truncate", "data-summary": column.id, children: formatSummary(summary) }) : null })
376
+ ] }, column.id);
377
+ }),
378
+ leadingColumns !== void 0 && leadingColumns >= columns.length && leftSpace > 0 && spacer("space:left"),
379
+ rightSpace > 0 && spacer("space:right"),
380
+ trailingCell && /* @__PURE__ */ jsx2("td", { className: "w-10 min-w-[2.5rem] bg-muted/10 border-r border-border" })
381
+ ] });
382
+ };
383
+
384
+ // src/spreadsheet/useSpreadsheetTableInteractions.ts
385
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef3, useState as useState3 } from "react";
386
+ import {
387
+ computeFillTarget,
388
+ normalizeRange
389
+ } from "@pixerate/editor/spreadsheet";
390
+ function gridHasFocus(grid) {
391
+ if (!grid || typeof document === "undefined") return false;
392
+ const active = document.activeElement;
393
+ if (!active || !grid.contains(active)) return false;
394
+ return !(active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement);
395
+ }
396
+ function useSpreadsheetClipboard(gridRef, state, readOnly) {
397
+ const stateRef = useRef3(state);
398
+ stateRef.current = state;
399
+ const readOnlyRef = useRef3(readOnly);
400
+ readOnlyRef.current = readOnly;
401
+ const copiedRef = useRef3(false);
402
+ useEffect2(() => {
403
+ if (typeof document === "undefined") return;
404
+ const handleCopy = (e) => {
405
+ const s = stateRef.current;
406
+ if (!gridHasFocus(gridRef.current) || s.editingCell || !e.clipboardData || !s.selectedRange) return;
407
+ for (const [type, data] of Object.entries(s.copyToClipboardData(s.selectedRange))) {
408
+ e.clipboardData.setData(type, data);
409
+ }
410
+ copiedRef.current = true;
411
+ e.preventDefault();
412
+ };
413
+ const handlePaste = (e) => {
414
+ const s = stateRef.current;
415
+ if (readOnlyRef.current || !gridHasFocus(gridRef.current) || s.editingCell || !e.clipboardData) return;
416
+ if (s.pasteClipboardData(e.clipboardData)) e.preventDefault();
417
+ };
418
+ document.addEventListener("copy", handleCopy);
419
+ document.addEventListener("paste", handlePaste);
420
+ return () => {
421
+ document.removeEventListener("copy", handleCopy);
422
+ document.removeEventListener("paste", handlePaste);
423
+ };
424
+ }, [gridRef]);
425
+ const copyShortcutFallback = useCallback2(() => {
426
+ copiedRef.current = false;
427
+ setTimeout(() => {
428
+ if (copiedRef.current) return;
429
+ const s = stateRef.current;
430
+ Promise.resolve(navigator.clipboard?.writeText(s.exportToTsv(s.selectedRange))).catch(() => {
431
+ });
432
+ }, 0);
433
+ }, []);
434
+ return { copyShortcutFallback };
435
+ }
436
+ function useFillHandle(state) {
437
+ const [drag, setDrag] = useState3(null);
438
+ const dragRef = useRef3(drag);
439
+ dragRef.current = drag;
440
+ const stateRef = useRef3(state);
441
+ stateRef.current = state;
442
+ const dragging = drag !== null;
443
+ useEffect2(() => {
444
+ if (!dragging) return;
445
+ const handleMouseUp = () => {
446
+ const current = dragRef.current;
447
+ setDrag(null);
448
+ if (current?.target) stateRef.current.autofill(current.source, current.target);
449
+ };
450
+ window.addEventListener("mouseup", handleMouseUp);
451
+ return () => window.removeEventListener("mouseup", handleMouseUp);
452
+ }, [dragging]);
453
+ const startFill = useCallback2((e) => {
454
+ e.preventDefault();
455
+ e.stopPropagation();
456
+ const range = stateRef.current.selectedRange;
457
+ if (range) setDrag({ source: normalizeRange(range), target: null });
458
+ }, []);
459
+ const extendFill = useCallback2((row, col) => {
460
+ const current = dragRef.current;
461
+ if (!current) return;
462
+ setDrag({ source: current.source, target: computeFillTarget(current.source, { row, col }) });
463
+ }, []);
464
+ return { fillTarget: drag?.target ?? null, isFilling: dragging, startFill, extendFill };
465
+ }
466
+
467
+ // src/spreadsheet/SpreadsheetValidationDialog.tsx
468
+ import { useEffect as useEffect3, useRef as useRef4 } from "react";
469
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
470
+ var SpreadsheetValidationDialog = ({ prompt, onResolve }) => {
471
+ const style = prompt.result.errorStyle ?? "stop";
472
+ const firstButton = useRef4(null);
473
+ useEffect3(() => {
474
+ firstButton.current?.focus();
475
+ }, [prompt]);
476
+ const buttons = style === "stop" ? [
477
+ { label: "Retry", action: "retry" },
478
+ { label: "Cancel", action: "cancel" }
479
+ ] : style === "warning" ? [
480
+ { label: "Yes", action: "accept" },
481
+ { label: "No", action: "retry" },
482
+ { label: "Cancel", action: "cancel" }
483
+ ] : [
484
+ { label: "OK", action: "accept" },
485
+ { label: "Cancel", action: "cancel" }
486
+ ];
487
+ const icon = style === "stop" ? "\u26D4" : style === "warning" ? "\u26A0" : "\u2139";
488
+ return /* @__PURE__ */ jsx3(
489
+ "div",
490
+ {
491
+ className: "fixed inset-0 z-[60] flex items-center justify-center bg-black/30",
492
+ onMouseDown: (e) => e.preventDefault(),
493
+ onKeyDown: (e) => {
494
+ e.stopPropagation();
495
+ if (e.key === "Escape") {
496
+ e.preventDefault();
497
+ onResolve("cancel");
498
+ }
499
+ },
500
+ children: /* @__PURE__ */ jsxs3(
501
+ "div",
502
+ {
503
+ role: "alertdialog",
504
+ "aria-modal": "true",
505
+ "aria-labelledby": "spreadsheet-validation-title",
506
+ "aria-describedby": "spreadsheet-validation-message",
507
+ "data-error-style": style,
508
+ className: "w-[22rem] max-w-[calc(100vw-2rem)] rounded-md border border-border bg-popover p-4 text-xs text-popover-foreground shadow-xl",
509
+ style: { background: "var(--popover, var(--background, #fff))" },
510
+ children: [
511
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-start gap-3", children: [
512
+ /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", className: "text-lg leading-none", children: icon }),
513
+ /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
514
+ /* @__PURE__ */ jsx3("h2", { id: "spreadsheet-validation-title", className: "mb-1 text-sm font-semibold", children: prompt.result.title }),
515
+ /* @__PURE__ */ jsx3("p", { id: "spreadsheet-validation-message", className: "whitespace-pre-wrap", children: prompt.result.message }),
516
+ style === "warning" && /* @__PURE__ */ jsx3("p", { className: "mt-2", children: "Continue?" })
517
+ ] })
518
+ ] }),
519
+ /* @__PURE__ */ jsx3("div", { className: "mt-4 flex justify-end gap-2", children: buttons.map((button, i) => /* @__PURE__ */ jsx3(
520
+ "button",
521
+ {
522
+ ref: i === 0 ? firstButton : void 0,
523
+ type: "button",
524
+ "data-action": button.action,
525
+ className: `rounded border border-border px-3 py-1 ${i === 0 ? "bg-primary text-primary-foreground" : "hover:bg-muted"}`,
526
+ onClick: () => onResolve(button.action),
527
+ children: button.label
528
+ },
529
+ button.action
530
+ )) })
531
+ ]
532
+ }
533
+ )
534
+ }
535
+ );
536
+ };
537
+
538
+ // src/spreadsheet/SpreadsheetRulePanels.tsx
539
+ import { useMemo as useMemo2, useState as useState4 } from "react";
540
+ import {
541
+ CONDITIONAL_FORMAT_ICON_SETS,
542
+ CONDITIONAL_FORMAT_PRESETS,
543
+ CONDITIONAL_FORMAT_STYLE_PRESETS,
544
+ CONDITIONAL_FORMAT_TIME_PERIODS,
545
+ DATA_VALIDATION_ERROR_STYLE_OPTIONS,
546
+ DATA_VALIDATION_OPERATOR_OPTIONS,
547
+ DATA_VALIDATION_TYPE_OPTIONS,
548
+ createConditionalFormatRule,
549
+ describeConditionalFormat,
550
+ describeDataValidation,
551
+ formatRuleRanges,
552
+ getConditionalFormatCss,
553
+ parseRuleRanges
554
+ } from "@pixerate/editor/spreadsheet";
555
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
556
+ var inputClass = "w-full rounded border border-border bg-background px-1.5 py-0.5 text-xs text-foreground";
557
+ var buttonClass = "rounded border border-border px-2 py-0.5 hover:bg-muted disabled:opacity-40";
558
+ function selectionText(range) {
559
+ return range ? formatRuleRanges([range]) : "A1";
560
+ }
561
+ var PanelFrame = ({
562
+ title,
563
+ label,
564
+ onClose,
565
+ children
566
+ }) => /* @__PURE__ */ jsxs4(
567
+ "div",
568
+ {
569
+ role: "dialog",
570
+ "aria-label": label,
571
+ className: "fixed right-4 top-16 z-40 flex max-h-[calc(100vh-5rem)] w-80 flex-col overflow-y-auto rounded-md border border-border bg-popover p-3 text-xs text-popover-foreground shadow-lg",
572
+ style: { background: "var(--popover, var(--background, #fff))" },
573
+ onKeyDown: (e) => {
574
+ e.stopPropagation();
575
+ if (e.key === "Escape") onClose();
576
+ },
577
+ onContextMenu: (e) => e.stopPropagation(),
578
+ children: [
579
+ /* @__PURE__ */ jsxs4("div", { className: "mb-2 flex items-center justify-between", children: [
580
+ /* @__PURE__ */ jsx4("h2", { className: "text-sm font-semibold", children: title }),
581
+ /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "Close", className: "px-1 text-muted-foreground hover:text-foreground", onClick: onClose, children: "\u2715" })
582
+ ] }),
583
+ children
584
+ ]
585
+ }
586
+ );
587
+ function RuleSwatch({ rule }) {
588
+ if (rule.type === "colorScale") {
589
+ return /* @__PURE__ */ jsx4(
590
+ "span",
591
+ {
592
+ "aria-hidden": "true",
593
+ className: "inline-block h-3 w-6 shrink-0 rounded-sm border border-border",
594
+ style: { background: `linear-gradient(90deg, ${rule.stops.map((s) => s.color).join(", ")})` }
595
+ }
596
+ );
597
+ }
598
+ if (rule.type === "dataBar") {
599
+ return /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", className: "inline-block h-3 w-6 shrink-0 rounded-sm", style: { background: rule.color ?? "#638ec6" } });
600
+ }
601
+ if (rule.type === "iconSet") return /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", className: "shrink-0", children: "\u25CF" });
602
+ const css = getConditionalFormatCss(rule.style);
603
+ return /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", className: "inline-flex h-4 w-6 shrink-0 items-center justify-center rounded-sm border border-border", style: css, children: "Aa" });
604
+ }
605
+ var SpreadsheetConditionalFormatPanel = ({ state, onClose }) => {
606
+ const rules = state.document.table?.conditionalFormats ?? [];
607
+ const [kind, setKind] = useState4("greaterThan");
608
+ const [appliesTo, setAppliesTo] = useState4(() => selectionText(state.selectedRange));
609
+ const [value, setValue] = useState4("");
610
+ const [value2, setValue2] = useState4("");
611
+ const [text, setText] = useState4("");
612
+ const [formula, setFormula] = useState4("=");
613
+ const [rank, setRank] = useState4("10");
614
+ const [period, setPeriod] = useState4("today");
615
+ const [iconSet, setIconSet] = useState4("3TrafficLights1");
616
+ const [color, setColor] = useState4("#638ec6");
617
+ const [styleId, setStyleId] = useState4(CONDITIONAL_FORMAT_STYLE_PRESETS[0].id);
618
+ const preset = useMemo2(() => CONDITIONAL_FORMAT_PRESETS.find((p) => p.kind === kind), [kind]);
619
+ const ranges = parseRuleRanges(appliesTo);
620
+ const add = () => {
621
+ if (!ranges) return;
622
+ state.addConditionalFormat(
623
+ createConditionalFormatRule({
624
+ kind,
625
+ ranges,
626
+ value,
627
+ value2,
628
+ text,
629
+ formula,
630
+ rank: Number(rank) || 10,
631
+ period,
632
+ iconSet,
633
+ color,
634
+ style: CONDITIONAL_FORMAT_STYLE_PRESETS.find((s) => s.id === styleId)?.style
635
+ })
636
+ );
637
+ };
638
+ return /* @__PURE__ */ jsxs4(PanelFrame, { title: "Conditional formatting", label: "Conditional formatting rules", onClose, children: [
639
+ /* @__PURE__ */ jsxs4("ol", { className: "mb-3 flex flex-col gap-1", "aria-label": "Rules in priority order", children: [
640
+ rules.length === 0 && /* @__PURE__ */ jsx4("li", { className: "text-muted-foreground", children: "No rules on this sheet." }),
641
+ rules.map((rule, i) => /* @__PURE__ */ jsxs4("li", { "data-rule-id": rule.id, className: "flex items-center gap-1.5 rounded border border-border px-1.5 py-1", children: [
642
+ /* @__PURE__ */ jsx4(RuleSwatch, { rule }),
643
+ /* @__PURE__ */ jsxs4("div", { className: "min-w-0 flex-1", children: [
644
+ /* @__PURE__ */ jsx4("div", { className: "truncate", title: describeConditionalFormat(rule), children: describeConditionalFormat(rule) }),
645
+ /* @__PURE__ */ jsx4("div", { className: "truncate font-mono text-[10px] text-muted-foreground", children: formatRuleRanges(rule.ranges) })
646
+ ] }),
647
+ /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-0.5 text-[10px]", title: "Stop if true", children: [
648
+ /* @__PURE__ */ jsx4(
649
+ "input",
650
+ {
651
+ type: "checkbox",
652
+ "aria-label": "Stop if true",
653
+ checked: !!rule.stopIfTrue,
654
+ onChange: (e) => state.updateConditionalFormat(rule.id, { stopIfTrue: e.target.checked })
655
+ }
656
+ ),
657
+ "Stop"
658
+ ] }),
659
+ /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "Move up", className: buttonClass, disabled: i === 0, onClick: () => state.moveConditionalFormat(rule.id, { by: -1 }), children: "\u25B2" }),
660
+ /* @__PURE__ */ jsx4(
661
+ "button",
662
+ {
663
+ type: "button",
664
+ "aria-label": "Move down",
665
+ className: buttonClass,
666
+ disabled: i === rules.length - 1,
667
+ onClick: () => state.moveConditionalFormat(rule.id, { by: 1 }),
668
+ children: "\u25BC"
669
+ }
670
+ ),
671
+ /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "Delete rule", className: buttonClass, onClick: () => state.deleteConditionalFormat(rule.id), children: "\u2715" })
672
+ ] }, rule.id))
673
+ ] }),
674
+ /* @__PURE__ */ jsxs4("fieldset", { className: "flex flex-col gap-1.5 border-t border-border pt-2", children: [
675
+ /* @__PURE__ */ jsx4("legend", { className: "sr-only", children: "New rule" }),
676
+ /* @__PURE__ */ jsxs4("label", { className: "flex flex-col gap-0.5", children: [
677
+ "Format cells",
678
+ /* @__PURE__ */ jsx4("select", { "aria-label": "Rule type", className: inputClass, value: kind, onChange: (e) => setKind(e.target.value), children: CONDITIONAL_FORMAT_PRESETS.map((p) => /* @__PURE__ */ jsx4("option", { value: p.kind, children: p.label }, p.kind)) })
679
+ ] }),
680
+ preset.fields.includes("value") && /* @__PURE__ */ jsx4("input", { "aria-label": "Value", placeholder: "Value or =formula", className: inputClass, value, onChange: (e) => setValue(e.target.value) }),
681
+ preset.fields.includes("value2") && /* @__PURE__ */ jsx4("input", { "aria-label": "Second value", placeholder: "and", className: inputClass, value: value2, onChange: (e) => setValue2(e.target.value) }),
682
+ preset.fields.includes("text") && /* @__PURE__ */ jsx4("input", { "aria-label": "Text", placeholder: "Text", className: inputClass, value: text, onChange: (e) => setText(e.target.value) }),
683
+ preset.fields.includes("formula") && /* @__PURE__ */ jsx4("input", { "aria-label": "Formula", placeholder: "=$A1>10", className: inputClass, value: formula, onChange: (e) => setFormula(e.target.value) }),
684
+ preset.fields.includes("rank") && /* @__PURE__ */ jsx4("input", { "aria-label": "Rank", type: "number", min: 1, className: inputClass, value: rank, onChange: (e) => setRank(e.target.value) }),
685
+ preset.fields.includes("period") && /* @__PURE__ */ jsx4("select", { "aria-label": "Period", className: inputClass, value: period, onChange: (e) => setPeriod(e.target.value), children: CONDITIONAL_FORMAT_TIME_PERIODS.map((p) => /* @__PURE__ */ jsx4("option", { value: p.period, children: p.label }, p.period)) }),
686
+ preset.fields.includes("iconSet") && /* @__PURE__ */ jsx4("select", { "aria-label": "Icon set", className: inputClass, value: iconSet, onChange: (e) => setIconSet(e.target.value), children: CONDITIONAL_FORMAT_ICON_SETS.map((set) => /* @__PURE__ */ jsx4("option", { value: set, children: set }, set)) }),
687
+ preset.fields.includes("color") && /* @__PURE__ */ jsx4("input", { "aria-label": "Bar colour", type: "color", className: "h-6 w-12", value: color, onChange: (e) => setColor(e.target.value) }),
688
+ preset.usesStyle && /* @__PURE__ */ jsx4("select", { "aria-label": "Style", className: inputClass, value: styleId, onChange: (e) => setStyleId(e.target.value), children: CONDITIONAL_FORMAT_STYLE_PRESETS.map((s) => /* @__PURE__ */ jsx4("option", { value: s.id, children: s.label }, s.id)) }),
689
+ /* @__PURE__ */ jsxs4("label", { className: "flex flex-col gap-0.5", children: [
690
+ "Applies to",
691
+ /* @__PURE__ */ jsx4("input", { "aria-label": "Applies to", className: `${inputClass} font-mono`, value: appliesTo, onChange: (e) => setAppliesTo(e.target.value) })
692
+ ] }),
693
+ /* @__PURE__ */ jsxs4("div", { className: "flex justify-between gap-2", children: [
694
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass, onClick: () => setAppliesTo(selectionText(state.selectedRange)), children: "Use selection" }),
695
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass, disabled: !ranges, onClick: add, children: "Add rule" })
696
+ ] })
697
+ ] })
698
+ ] });
699
+ };
700
+ var SpreadsheetDataValidationPanel = ({ state, onClose }) => {
701
+ const rules = state.document.table?.dataValidations ?? [];
702
+ const [type, setType] = useState4("list");
703
+ const [operator, setOperator] = useState4("between");
704
+ const [formula1, setFormula1] = useState4("");
705
+ const [formula2, setFormula2] = useState4("");
706
+ const [errorStyle, setErrorStyle] = useState4("stop");
707
+ const [errorMessage, setErrorMessage] = useState4("");
708
+ const [inputMessage, setInputMessage] = useState4("");
709
+ const [ignoreBlank, setIgnoreBlank] = useState4(true);
710
+ const [appliesTo, setAppliesTo] = useState4(() => selectionText(state.selectedRange));
711
+ const ranges = parseRuleRanges(appliesTo);
712
+ const usesOperator = type === "whole" || type === "decimal" || type === "date" || type === "time" || type === "textLength";
713
+ const needsTwo = usesOperator && (operator === "between" || operator === "notBetween");
714
+ const circles = state.controller.getInvalidDataCircles();
715
+ const apply = () => {
716
+ if (!ranges) return;
717
+ state.addDataValidation({
718
+ ranges,
719
+ type,
720
+ ...usesOperator ? { operator } : {},
721
+ ...formula1 ? { formula1 } : {},
722
+ ...needsTwo && formula2 ? { formula2 } : {},
723
+ errorStyle,
724
+ ...errorMessage ? { errorMessage } : {},
725
+ ...inputMessage ? { inputMessage } : {},
726
+ ignoreBlank
727
+ });
728
+ };
729
+ return /* @__PURE__ */ jsxs4(PanelFrame, { title: "Data validation", label: "Data validation rules", onClose, children: [
730
+ /* @__PURE__ */ jsxs4("ul", { className: "mb-3 flex flex-col gap-1", "aria-label": "Validation rules", children: [
731
+ rules.length === 0 && /* @__PURE__ */ jsx4("li", { className: "text-muted-foreground", children: "No validation on this sheet." }),
732
+ rules.map((rule) => /* @__PURE__ */ jsxs4("li", { "data-rule-id": rule.id, className: "flex items-center gap-1.5 rounded border border-border px-1.5 py-1", children: [
733
+ /* @__PURE__ */ jsxs4("div", { className: "min-w-0 flex-1", children: [
734
+ /* @__PURE__ */ jsx4("div", { className: "truncate", title: describeDataValidation(rule), children: describeDataValidation(rule) }),
735
+ /* @__PURE__ */ jsxs4("div", { className: "truncate font-mono text-[10px] text-muted-foreground", children: [
736
+ formatRuleRanges(rule.ranges),
737
+ " \xB7 ",
738
+ rule.errorStyle ?? "stop"
739
+ ] })
740
+ ] }),
741
+ /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "Delete rule", className: buttonClass, onClick: () => state.deleteDataValidation(rule.id), children: "\u2715" })
742
+ ] }, rule.id))
743
+ ] }),
744
+ /* @__PURE__ */ jsxs4("fieldset", { className: "flex flex-col gap-1.5 border-t border-border pt-2", children: [
745
+ /* @__PURE__ */ jsx4("legend", { className: "sr-only", children: "New validation" }),
746
+ /* @__PURE__ */ jsxs4("label", { className: "flex flex-col gap-0.5", children: [
747
+ "Allow",
748
+ /* @__PURE__ */ jsx4("select", { "aria-label": "Allow", className: inputClass, value: type, onChange: (e) => setType(e.target.value), children: DATA_VALIDATION_TYPE_OPTIONS.map((o) => /* @__PURE__ */ jsx4("option", { value: o.type, children: o.label }, o.type)) })
749
+ ] }),
750
+ usesOperator && /* @__PURE__ */ jsx4("select", { "aria-label": "Operator", className: inputClass, value: operator, onChange: (e) => setOperator(e.target.value), children: DATA_VALIDATION_OPERATOR_OPTIONS.map((o) => /* @__PURE__ */ jsx4("option", { value: o.operator, children: o.label }, o.operator)) }),
751
+ type !== "any" && /* @__PURE__ */ jsx4(
752
+ "input",
753
+ {
754
+ "aria-label": type === "list" ? "Source" : type === "custom" ? "Formula" : needsTwo ? "Minimum" : "Value",
755
+ placeholder: type === "list" ? "Low,Medium,High or =$H$1:$H$5" : type === "custom" ? "=A1>0" : "Value or =formula",
756
+ className: inputClass,
757
+ value: formula1,
758
+ onChange: (e) => setFormula1(e.target.value)
759
+ }
760
+ ),
761
+ needsTwo && /* @__PURE__ */ jsx4("input", { "aria-label": "Maximum", placeholder: "Maximum", className: inputClass, value: formula2, onChange: (e) => setFormula2(e.target.value) }),
762
+ /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1", children: [
763
+ /* @__PURE__ */ jsx4("input", { type: "checkbox", checked: ignoreBlank, onChange: (e) => setIgnoreBlank(e.target.checked) }),
764
+ " Ignore blank"
765
+ ] }),
766
+ /* @__PURE__ */ jsx4("input", { "aria-label": "Input message", placeholder: "Input message (optional)", className: inputClass, value: inputMessage, onChange: (e) => setInputMessage(e.target.value) }),
767
+ /* @__PURE__ */ jsx4("div", { className: "flex gap-1", children: /* @__PURE__ */ jsx4("select", { "aria-label": "Error style", className: inputClass, value: errorStyle, onChange: (e) => setErrorStyle(e.target.value), children: DATA_VALIDATION_ERROR_STYLE_OPTIONS.map((o) => /* @__PURE__ */ jsx4("option", { value: o.style, children: o.label }, o.style)) }) }),
768
+ /* @__PURE__ */ jsx4("input", { "aria-label": "Error message", placeholder: "Error message (optional)", className: inputClass, value: errorMessage, onChange: (e) => setErrorMessage(e.target.value) }),
769
+ /* @__PURE__ */ jsxs4("label", { className: "flex flex-col gap-0.5", children: [
770
+ "Applies to",
771
+ /* @__PURE__ */ jsx4("input", { "aria-label": "Applies to", className: `${inputClass} font-mono`, value: appliesTo, onChange: (e) => setAppliesTo(e.target.value) })
772
+ ] }),
773
+ /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap justify-between gap-2", children: [
774
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass, onClick: () => setAppliesTo(selectionText(state.selectedRange)), children: "Use selection" }),
775
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass, disabled: !ranges, onClick: () => ranges && ranges.forEach((r) => state.clearDataValidation(r)), children: "Clear" }),
776
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass, disabled: !ranges, onClick: apply, children: "Apply" })
777
+ ] }),
778
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass, "aria-pressed": circles, onClick: () => state.setInvalidDataCircles(!circles), children: circles ? "Clear validation circles" : "Circle invalid data" })
779
+ ] })
780
+ ] });
781
+ };
782
+
783
+ // src/spreadsheet/useSpreadsheetViewport.ts
784
+ import { useCallback as useCallback3, useEffect as useEffect4, useLayoutEffect as useLayoutEffect2, useMemo as useMemo3, useRef as useRef5, useState as useState5 } from "react";
785
+ import {
786
+ fullSpreadsheetViewWindow,
787
+ getSpreadsheetScrollToCell,
788
+ getSpreadsheetViewWindow,
789
+ measureSpreadsheetView,
790
+ sameSpreadsheetViewWindow,
791
+ shouldVirtualizeSpreadsheet
792
+ } from "@pixerate/editor/spreadsheet";
793
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect2 : useEffect4;
794
+ function useSpreadsheetViewport(scrollRef, view, options = {}) {
795
+ const metrics = useMemo3(() => measureSpreadsheetView(view), [view]);
796
+ const virtual = shouldVirtualizeSpreadsheet(view, options.virtualize ?? "auto");
797
+ const [viewport, setViewport] = useState5({ scrollTop: 0, scrollLeft: 0, width: 0, height: 0 });
798
+ const windowOptions = useMemo3(
799
+ () => ({ merges: options.merges, overscanRows: options.overscanRows, overscanColumns: options.overscanColumns }),
800
+ [options.merges, options.overscanRows, options.overscanColumns]
801
+ );
802
+ const win = useMemo3(
803
+ () => virtual ? getSpreadsheetViewWindow(metrics, viewport, windowOptions) : fullSpreadsheetViewWindow(metrics),
804
+ [virtual, metrics, viewport, windowOptions]
805
+ );
806
+ const latest = useRef5({ metrics, viewport, virtual, windowOptions });
807
+ latest.current = { metrics, viewport, virtual, windowOptions };
808
+ const readViewport = useCallback3(() => {
809
+ const el = scrollRef.current;
810
+ if (!el) return null;
811
+ return { scrollTop: el.scrollTop, scrollLeft: el.scrollLeft, width: el.clientWidth, height: el.clientHeight };
812
+ }, [scrollRef]);
813
+ const updateViewport = useCallback3((next) => {
814
+ setViewport((prev) => {
815
+ const { metrics: m, virtual: v, windowOptions: o } = latest.current;
816
+ if (!v) return prev;
817
+ const same = prev.width === next.width && prev.height === next.height && sameSpreadsheetViewWindow(getSpreadsheetViewWindow(m, prev, o), getSpreadsheetViewWindow(m, next, o));
818
+ return same ? prev : next;
819
+ });
820
+ }, []);
821
+ const onScroll = useCallback3(() => {
822
+ const next = readViewport();
823
+ if (next) updateViewport(next);
824
+ }, [readViewport, updateViewport]);
825
+ useIsomorphicLayoutEffect(() => {
826
+ if (!virtual) return;
827
+ onScroll();
828
+ const el = scrollRef.current;
829
+ if (!el || typeof ResizeObserver === "undefined") return;
830
+ const observer = new ResizeObserver(() => onScroll());
831
+ observer.observe(el);
832
+ return () => observer.disconnect();
833
+ }, [virtual, onScroll, scrollRef]);
834
+ const activeRow = options.activeCell?.row;
835
+ const activeCol = options.activeCell?.col;
836
+ useIsomorphicLayoutEffect(() => {
837
+ if (!virtual || activeRow === void 0 || activeCol === void 0) return;
838
+ const el = scrollRef.current;
839
+ const current = latest.current.viewport;
840
+ const measured = readViewport();
841
+ const base = measured ? { ...measured, scrollTop: Math.max(measured.scrollTop, 0), scrollLeft: Math.max(measured.scrollLeft, 0) } : current;
842
+ const target = getSpreadsheetScrollToCell(latest.current.metrics, base, { row: activeRow, col: activeCol });
843
+ if (!target) return;
844
+ const next = { ...base, ...target };
845
+ if (el) {
846
+ if (target.scrollTop !== void 0) el.scrollTop = target.scrollTop;
847
+ if (target.scrollLeft !== void 0) el.scrollLeft = target.scrollLeft;
848
+ }
849
+ updateViewport(next);
850
+ }, [virtual, activeRow, activeCol]);
851
+ return { metrics, window: win, onScroll };
852
+ }
853
+
854
+ // src/spreadsheet/FormulaHints.tsx
855
+ import { useEffect as useEffect5, useLayoutEffect as useLayoutEffect3, useMemo as useMemo4, useRef as useRef6, useState as useState6 } from "react";
856
+ import { applyFormulaSuggestion, getFormulaHints } from "@pixerate/editor/spreadsheet";
857
+ import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
858
+ var FormulaHints = ({ inputRef, value, onAccept, limit = 8, names }) => {
859
+ const [caret, setCaret] = useState6(null);
860
+ const [focused, setFocused] = useState6(false);
861
+ const [selected, setSelected] = useState6(0);
862
+ const [dismissed, setDismissed] = useState6(false);
863
+ const [position, setPosition] = useState6(null);
864
+ const pendingCaret = useRef6(null);
865
+ const hints = useMemo4(
866
+ () => getFormulaHints(value, caret ?? value.length, { limit, names }),
867
+ [value, caret, limit, names]
868
+ );
869
+ const suggestions = dismissed ? [] : hints.suggestions;
870
+ const selectedIndex = Math.max(0, Math.min(selected, suggestions.length - 1));
871
+ const visible = focused && value.trimStart().startsWith("=") && (suggestions.length > 0 || !!hints.activeCall);
872
+ const latest = useRef6({ suggestions, selected: selectedIndex, value, caret, onAccept });
873
+ latest.current = { suggestions, selected: selectedIndex, value, caret, onAccept };
874
+ const accept = (index) => {
875
+ const { suggestions: list, value: current, caret: at, onAccept: commit } = latest.current;
876
+ const suggestion = list[index];
877
+ if (!suggestion) return;
878
+ const next = applyFormulaSuggestion(current, at ?? current.length, suggestion);
879
+ pendingCaret.current = next.caret;
880
+ commit(next.value);
881
+ };
882
+ useEffect5(() => {
883
+ const input = inputRef.current;
884
+ if (!input) return;
885
+ const syncCaret = () => setCaret(input.selectionStart ?? input.value.length);
886
+ const onInput = () => {
887
+ setDismissed(false);
888
+ setSelected(0);
889
+ syncCaret();
890
+ };
891
+ const onFocus = () => {
892
+ setFocused(true);
893
+ syncCaret();
894
+ };
895
+ const onBlur = () => setFocused(false);
896
+ const onKeyDown = (e) => {
897
+ const { suggestions: list } = latest.current;
898
+ if (list.length === 0) return;
899
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
900
+ e.preventDefault();
901
+ e.stopPropagation();
902
+ const step = e.key === "ArrowDown" ? 1 : -1;
903
+ setSelected((i) => (i + step + list.length) % list.length);
904
+ } else if (e.key === "Tab" && !e.shiftKey) {
905
+ e.preventDefault();
906
+ e.stopPropagation();
907
+ accept(latest.current.selected);
908
+ } else if (e.key === "Escape") {
909
+ e.preventDefault();
910
+ e.stopPropagation();
911
+ setDismissed(true);
912
+ }
913
+ };
914
+ input.addEventListener("input", onInput);
915
+ input.addEventListener("keyup", syncCaret);
916
+ input.addEventListener("click", syncCaret);
917
+ input.addEventListener("select", syncCaret);
918
+ input.addEventListener("focus", onFocus);
919
+ input.addEventListener("blur", onBlur);
920
+ input.addEventListener("keydown", onKeyDown);
921
+ if (typeof document !== "undefined" && document.activeElement === input) onFocus();
922
+ return () => {
923
+ input.removeEventListener("input", onInput);
924
+ input.removeEventListener("keyup", syncCaret);
925
+ input.removeEventListener("click", syncCaret);
926
+ input.removeEventListener("select", syncCaret);
927
+ input.removeEventListener("focus", onFocus);
928
+ input.removeEventListener("blur", onBlur);
929
+ input.removeEventListener("keydown", onKeyDown);
930
+ };
931
+ }, [inputRef]);
932
+ useLayoutEffect3(() => {
933
+ const input = inputRef.current;
934
+ const at = pendingCaret.current;
935
+ if (!input || at === null || input.value !== value) return;
936
+ pendingCaret.current = null;
937
+ input.setSelectionRange(at, at);
938
+ setCaret(at);
939
+ setSelected(0);
940
+ }, [value, inputRef]);
941
+ useLayoutEffect3(() => {
942
+ const input = inputRef.current;
943
+ if (!visible || !input) return;
944
+ const rect = input.getBoundingClientRect();
945
+ setPosition({ top: rect.bottom + 2, left: rect.left, width: rect.width });
946
+ }, [visible, value, inputRef]);
947
+ if (!visible) return null;
948
+ const { activeCall } = hints;
949
+ return /* @__PURE__ */ jsxs5(
950
+ "div",
951
+ {
952
+ "data-formula-hints": "",
953
+ className: "rounded-md border border-border bg-popover text-popover-foreground shadow-md text-xs font-sans overflow-hidden",
954
+ style: {
955
+ position: "fixed",
956
+ top: position?.top ?? 0,
957
+ left: position?.left ?? 0,
958
+ minWidth: Math.max(260, position?.width ?? 0),
959
+ maxWidth: 440,
960
+ zIndex: 50,
961
+ visibility: position ? "visible" : "hidden"
962
+ },
963
+ onMouseDown: (e) => e.preventDefault(),
964
+ children: [
965
+ activeCall && /* @__PURE__ */ jsxs5("div", { "data-formula-signature": "", className: "px-2 py-1.5 border-b border-border bg-muted/30", children: [
966
+ /* @__PURE__ */ jsx5("div", { className: "font-mono", children: activeCall.parts.map((part, i) => /* @__PURE__ */ jsx5(
967
+ "span",
968
+ {
969
+ "data-active": part.active ? "" : void 0,
970
+ className: part.active ? "font-bold text-primary underline" : "",
971
+ children: part.text
972
+ },
973
+ i
974
+ )) }),
975
+ /* @__PURE__ */ jsx5("div", { className: "mt-0.5 text-muted-foreground", children: activeCall.argument ? /* @__PURE__ */ jsxs5(Fragment, { children: [
976
+ /* @__PURE__ */ jsx5("span", { className: "font-semibold", children: activeCall.argument.name }),
977
+ ": ",
978
+ activeCall.argument.description
979
+ ] }) : activeCall.description })
980
+ ] }),
981
+ suggestions.length > 0 && /* @__PURE__ */ jsx5("ul", { role: "listbox", "aria-label": "Formula suggestions", className: "max-h-64 overflow-auto py-1 m-0 list-none p-0", children: suggestions.map((s, i) => /* @__PURE__ */ jsxs5(
982
+ "li",
983
+ {
984
+ role: "option",
985
+ "aria-selected": i === selectedIndex,
986
+ className: `cursor-pointer px-2 py-1 ${i === selectedIndex ? "bg-primary/10" : "hover:bg-muted/50"}`,
987
+ onMouseEnter: () => setSelected(i),
988
+ onMouseDown: (e) => {
989
+ e.preventDefault();
990
+ accept(i);
991
+ },
992
+ children: [
993
+ /* @__PURE__ */ jsx5("div", { className: "font-mono font-semibold text-foreground", children: s.signature }),
994
+ /* @__PURE__ */ jsx5("div", { className: "text-muted-foreground truncate", children: s.description })
995
+ ]
996
+ },
997
+ `${s.kind ?? "fn"}:${s.name}`
998
+ )) })
999
+ ]
1000
+ }
1001
+ );
1002
+ };
1003
+
1004
+ // src/spreadsheet/SheetTabs.tsx
1005
+ import { useRef as useRef7, useState as useState7 } from "react";
1006
+ import { getSheetTabMenuItems, sheetDropIndex } from "@pixerate/editor/spreadsheet";
1007
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1008
+ var SheetTabs = ({ state, readOnly = false, className = "" }) => {
1009
+ const { controller, sheets } = state;
1010
+ const [renaming, setRenaming] = useState7(null);
1011
+ const [error, setError] = useState7(null);
1012
+ const [menu, setMenu] = useState7(null);
1013
+ const dragFrom = useRef7(null);
1014
+ const [dropTarget, setDropTarget] = useState7(null);
1015
+ const commitRename = (cancelOnError) => {
1016
+ if (!renaming) return;
1017
+ const sheet = sheets.find((s) => s.id === renaming.id);
1018
+ const name = renaming.draft.trim();
1019
+ if (!sheet || !name || name === sheet.name) {
1020
+ setRenaming(null);
1021
+ setError(null);
1022
+ return;
1023
+ }
1024
+ try {
1025
+ state.renameSheet(renaming.id, name);
1026
+ setRenaming(null);
1027
+ setError(null);
1028
+ } catch (err) {
1029
+ setError(err?.message ?? String(err));
1030
+ if (cancelOnError) setRenaming(null);
1031
+ }
1032
+ };
1033
+ const dropAfter = (e) => {
1034
+ const rect = e.currentTarget.getBoundingClientRect();
1035
+ return e.clientX > rect.left + rect.width / 2;
1036
+ };
1037
+ return /* @__PURE__ */ jsxs6("div", { className: `flex flex-col ${className}`, "data-sheet-tabs": "", children: [
1038
+ /* @__PURE__ */ jsxs6(
1039
+ "div",
1040
+ {
1041
+ role: "tablist",
1042
+ "aria-label": "Sheets",
1043
+ className: "flex items-center gap-0.5 overflow-x-auto border-t border-border bg-muted px-1 py-1 text-xs",
1044
+ children: [
1045
+ sheets.map(
1046
+ (sheet, index) => renaming?.id === sheet.id ? /* @__PURE__ */ jsx6(
1047
+ "input",
1048
+ {
1049
+ "aria-label": "Sheet name",
1050
+ autoFocus: true,
1051
+ className: "w-32 rounded border border-primary bg-background px-2 py-0.5 text-xs outline-none",
1052
+ value: renaming.draft,
1053
+ onChange: (e) => setRenaming({ id: sheet.id, draft: e.target.value }),
1054
+ onFocus: (e) => e.currentTarget.select(),
1055
+ onKeyDown: (e) => {
1056
+ e.stopPropagation();
1057
+ if (e.key === "Enter") commitRename(false);
1058
+ else if (e.key === "Escape") {
1059
+ setRenaming(null);
1060
+ setError(null);
1061
+ }
1062
+ },
1063
+ onBlur: () => commitRename(true)
1064
+ },
1065
+ sheet.id
1066
+ ) : /* @__PURE__ */ jsx6(
1067
+ "button",
1068
+ {
1069
+ type: "button",
1070
+ role: "tab",
1071
+ "aria-selected": sheet.active,
1072
+ "data-sheet-id": sheet.id,
1073
+ draggable: !readOnly,
1074
+ title: readOnly ? sheet.name : `${sheet.name} \u2014 double-click to rename, drag to move`,
1075
+ className: `whitespace-nowrap rounded px-3 py-0.5 ${sheet.active ? "bg-background font-semibold text-foreground shadow-sm" : "text-muted-foreground hover:bg-background/60"} ${dropTarget?.index === index ? dropTarget.after ? "border-r-2 border-primary" : "border-l-2 border-primary" : ""}`,
1076
+ onClick: () => state.setActiveSheet(sheet.id),
1077
+ onDoubleClick: () => {
1078
+ if (readOnly) return;
1079
+ setError(null);
1080
+ setRenaming({ id: sheet.id, draft: sheet.name });
1081
+ },
1082
+ onContextMenu: (e) => {
1083
+ if (readOnly) return;
1084
+ e.preventDefault();
1085
+ setMenu({ x: e.clientX, y: e.clientY, items: getSheetTabMenuItems(controller, sheet.id, { onError: setError }) });
1086
+ },
1087
+ onDragStart: (e) => {
1088
+ dragFrom.current = index;
1089
+ e.dataTransfer?.setData("text/plain", sheet.id);
1090
+ if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
1091
+ },
1092
+ onDragOver: (e) => {
1093
+ if (dragFrom.current === null) return;
1094
+ e.preventDefault();
1095
+ const after = dropAfter(e);
1096
+ setDropTarget((prev) => prev?.index === index && prev.after === after ? prev : { index, after });
1097
+ },
1098
+ onDragLeave: () => setDropTarget(null),
1099
+ onDrop: (e) => {
1100
+ e.preventDefault();
1101
+ const from = dragFrom.current;
1102
+ dragFrom.current = null;
1103
+ setDropTarget(null);
1104
+ if (from === null) return;
1105
+ state.moveSheet(sheets[from].id, sheetDropIndex(from, index, dropAfter(e)));
1106
+ },
1107
+ onDragEnd: () => {
1108
+ dragFrom.current = null;
1109
+ setDropTarget(null);
1110
+ },
1111
+ children: sheet.name
1112
+ },
1113
+ sheet.id
1114
+ )
1115
+ ),
1116
+ !readOnly && /* @__PURE__ */ jsx6(
1117
+ "button",
1118
+ {
1119
+ type: "button",
1120
+ "aria-label": "Add sheet",
1121
+ title: "Add sheet",
1122
+ className: "rounded px-2 py-0.5 text-muted-foreground hover:bg-background/60",
1123
+ onClick: () => {
1124
+ setError(null);
1125
+ state.addSheet();
1126
+ },
1127
+ children: "+"
1128
+ }
1129
+ )
1130
+ ]
1131
+ }
1132
+ ),
1133
+ error && /* @__PURE__ */ jsx6("div", { role: "alert", className: "px-2 py-1 text-xs text-destructive", children: error }),
1134
+ menu && /* @__PURE__ */ jsx6(
1135
+ SpreadsheetContextMenu,
1136
+ {
1137
+ items: menu.items,
1138
+ x: menu.x,
1139
+ y: menu.y,
1140
+ ariaLabel: "Sheet actions",
1141
+ onClose: () => setMenu(null)
1142
+ }
1143
+ )
1144
+ ] });
1145
+ };
1146
+
1147
+ // src/spreadsheet/SpreadsheetEditor.tsx
1148
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1149
+ var COLUMN_HEADER_HEIGHT = 28;
1150
+ var ROW_HEADER_WIDTH = 48;
1151
+ function frozenCellStyle(viewRow, viewCol) {
1152
+ if (!viewRow.frozen && !viewCol.frozen) return void 0;
1153
+ return {
1154
+ position: "sticky",
1155
+ ...viewRow.frozen ? { top: COLUMN_HEADER_HEIGHT + viewRow.stickyTop } : {},
1156
+ ...viewCol.frozen ? { left: ROW_HEADER_WIDTH + viewCol.stickyLeft } : {},
1157
+ zIndex: viewRow.frozen && viewCol.frozen ? 14 : viewRow.frozen ? 12 : 11,
1158
+ background: "var(--background, #fff)"
1159
+ };
1160
+ }
1161
+ var coordKey = (cell) => cell ? `${indexToColName(cell.col)}${cell.row + 1}` : "";
1162
+ function spillBorderStyle(range, row, col) {
1163
+ const line = "1px solid color-mix(in srgb, var(--primary, #6366f1) 60%, transparent)";
1164
+ return {
1165
+ position: "absolute",
1166
+ inset: 0,
1167
+ pointerEvents: "none",
1168
+ zIndex: 5,
1169
+ borderTop: row === range.startRow ? line : void 0,
1170
+ borderBottom: row === range.endRow ? line : void 0,
1171
+ borderLeft: col === range.startCol ? line : void 0,
1172
+ borderRight: col === range.endCol ? line : void 0
1173
+ };
1174
+ }
1175
+ function CellText({ cell, conditional }) {
1176
+ const shown = formatCellForDisplay(cell);
1177
+ const color = conditional?.color ?? (shown.color ? numberFormatColorToCss(shown.color) : void 0);
1178
+ const decorated = !!conditional?.dataBar || !!conditional?.icon;
1179
+ const text = /* @__PURE__ */ jsx7(
1180
+ "span",
1181
+ {
1182
+ className: `truncate block max-w-full ${cell.error ? "text-destructive font-semibold" : ""}`,
1183
+ title: cell.error ? cell.errorMessage ?? cell.error : void 0,
1184
+ "data-numeric": shown.numeric ? "" : void 0,
1185
+ style: shown.numeric || color || decorated ? {
1186
+ ...decorated ? { position: "relative" } : {},
1187
+ ...conditional?.icon ? { flex: 1, minWidth: 0 } : {},
1188
+ ...shown.numeric ? { textAlign: "right" } : {},
1189
+ ...color ? { color } : {}
1190
+ } : void 0,
1191
+ children: conditional?.hideValue ? "" : shown.text
1192
+ }
1193
+ );
1194
+ if (!conditional || !decorated) return text;
1195
+ const icon = conditional.icon ? getConditionalFormatIcon(conditional.icon.set, conditional.icon.index) : null;
1196
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
1197
+ conditional.dataBar && /* @__PURE__ */ jsx7("span", { "aria-hidden": "true", className: "spreadsheet-data-bar", style: getDataBarCss(conditional.dataBar) }),
1198
+ icon ? /* @__PURE__ */ jsxs7("span", { className: "flex items-center gap-1 min-w-0", style: { position: "relative" }, children: [
1199
+ /* @__PURE__ */ jsx7("span", { className: "spreadsheet-cf-icon shrink-0", role: "img", "aria-label": icon.label, "data-icon": conditional.icon.id, style: { color: icon.color }, children: icon.glyph }),
1200
+ text
1201
+ ] }) : text
1202
+ ] });
1203
+ }
1204
+ var SpreadsheetEditor = ({
1205
+ state: externalState,
1206
+ readOnly = false,
1207
+ customCellRenderer,
1208
+ onAddColumnClick,
1209
+ onColumnHeaderClick,
1210
+ onColumnResize,
1211
+ showSheetTabs = false,
1212
+ virtualize = "auto"
1213
+ }) => {
1214
+ const defaultState = useSpreadsheetEditor();
1215
+ const state = externalState || defaultState;
1216
+ const gridRef = useRef8(null);
1217
+ const scrollRef = useRef8(null);
1218
+ const formulaBarRef = useRef8(null);
1219
+ const cellInputRef = useRef8(null);
1220
+ const [resizingColId, setResizingColId] = useState8(null);
1221
+ const [hoveredColId, setHoveredColId] = useState8(null);
1222
+ const [resizeStartX, setResizeStartX] = useState8(0);
1223
+ const [resizeStartWidth, setResizeStartWidth] = useState8(0);
1224
+ const view = state.viewModel;
1225
+ const { window: viewWindow, onScroll: handleGridScroll } = useSpreadsheetViewport(scrollRef, view, {
1226
+ virtualize,
1227
+ merges: state.document.table?.merges,
1228
+ activeCell: state.activeCell
1229
+ });
1230
+ const columnSlots = useMemo5(() => {
1231
+ const col = (viewCol) => ({ kind: "col", viewCol });
1232
+ if (!viewWindow.virtual) return view.columns.map(col);
1233
+ const slots = view.columns.slice(0, viewWindow.frozenColumns).map(col);
1234
+ if (viewWindow.leftSpace > 0) slots.push({ kind: "space", key: "space:left", width: viewWindow.leftSpace });
1235
+ for (let i = viewWindow.colStart; i < viewWindow.colEnd; i++) slots.push(col(view.columns[i]));
1236
+ if (viewWindow.rightSpace > 0) slots.push({ kind: "space", key: "space:right", width: viewWindow.rightSpace });
1237
+ return slots;
1238
+ }, [view, viewWindow]);
1239
+ const renderedColumns = useMemo5(
1240
+ () => columnSlots.flatMap((slot) => slot.kind === "col" ? [slot.viewCol] : []),
1241
+ [columnSlots]
1242
+ );
1243
+ const rowSlots = useMemo5(() => {
1244
+ const row = (viewRow) => ({ kind: "row", viewRow });
1245
+ if (!viewWindow.virtual) return view.rows.map(row);
1246
+ const slots = view.rows.slice(0, viewWindow.frozenRows).map(row);
1247
+ if (viewWindow.topSpace > 0) slots.push({ kind: "space", key: "space:top", height: viewWindow.topSpace });
1248
+ for (let i = viewWindow.rowStart; i < viewWindow.rowEnd; i++) slots.push(row(view.rows[i]));
1249
+ if (viewWindow.bottomSpace > 0) slots.push({ kind: "space", key: "space:bottom", height: viewWindow.bottomSpace });
1250
+ return slots;
1251
+ }, [view, viewWindow]);
1252
+ const { copyShortcutFallback } = useSpreadsheetClipboard(gridRef, state, readOnly);
1253
+ const { fillTarget, startFill, extendFill } = useFillHandle(state);
1254
+ const [menu, setMenu] = useState8(null);
1255
+ const [panel, setPanel] = useState8(null);
1256
+ const [listMenu, setListMenu] = useState8(null);
1257
+ const overlay = state.conditionalFormatOverlay;
1258
+ const filter = state.document.table?.filter;
1259
+ const filteredColIds = useMemo5(
1260
+ () => new Set(filter?.enabled ? filter.rules.map((r) => r.colId) : []),
1261
+ [filter]
1262
+ );
1263
+ const handleCellContextMenu = useCallback4((rIdx, cIdx, e) => {
1264
+ if (readOnly) return;
1265
+ e.preventDefault();
1266
+ if (state.editingCell) state.commitEditing();
1267
+ if (!state.selectedRange || !rangeContains(state.selectedRange, rIdx, cIdx)) {
1268
+ state.selectCell(rIdx, cIdx);
1269
+ }
1270
+ setMenu({ x: e.clientX, y: e.clientY, items: getSpreadsheetMenuItems(state.controller, { row: rIdx, col: cIdx }) });
1271
+ }, [readOnly, state]);
1272
+ const nameSuggestions = useMemo5(
1273
+ () => state.controller.getNameSuggestions(),
1274
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1275
+ [state.controller, state.document]
1276
+ );
1277
+ const activeRawValue = useMemo5(() => {
1278
+ if (!state.activeCell) return "";
1279
+ const row = state.document.rows[state.activeCell.row];
1280
+ const col = state.document.columns[state.activeCell.col];
1281
+ if (!row || !col) return "";
1282
+ return state.getCell(row.id, col.id).raw;
1283
+ }, [state.activeCell, state.document]);
1284
+ const activeSpill = useMemo5(
1285
+ () => state.activeCell ? state.controller.getSpillAt(state.activeCell.row, state.activeCell.col) : null,
1286
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1287
+ [state.controller, state.activeCell, state.document]
1288
+ );
1289
+ const spillFormula = activeSpill && !activeRawValue && activeSpill.key !== coordKey(state.activeCell) ? activeSpill.formula : "";
1290
+ const coordLabel = useMemo5(() => {
1291
+ if (!state.activeCell) return "--";
1292
+ const colName = indexToColName(state.activeCell.col);
1293
+ return `${colName}${state.activeCell.row + 1}`;
1294
+ }, [state.activeCell]);
1295
+ const [formulaDraft, setFormulaDraft] = useState8(null);
1296
+ const activeColumn = state.activeCell ? state.document.columns[state.activeCell.col] : void 0;
1297
+ const formulaBarReadOnly = readOnly || !state.activeCell || !!activeColumn?.readOnly;
1298
+ useEffect6(() => {
1299
+ setFormulaDraft(null);
1300
+ }, [state.activeCell?.row, state.activeCell?.col]);
1301
+ const commitFormulaBar = () => {
1302
+ if (state.editingCell) {
1303
+ state.commitEditing();
1304
+ return;
1305
+ }
1306
+ if (formulaDraft === null || !state.activeCell) return;
1307
+ const row = state.document.rows[state.activeCell.row];
1308
+ const col = state.document.columns[state.activeCell.col];
1309
+ if (row && col && !formulaBarReadOnly && formulaDraft !== activeRawValue) {
1310
+ state.commitCellInput(row.id, col.id, formulaDraft);
1311
+ }
1312
+ setFormulaDraft(null);
1313
+ };
1314
+ const handleCellClick = useCallback4((rIdx, cIdx, e) => {
1315
+ if (state.editingCell && (state.editingCell.row !== rIdx || state.editingCell.col !== cIdx)) {
1316
+ state.commitEditing();
1317
+ }
1318
+ state.selectCell(rIdx, cIdx, e.shiftKey);
1319
+ }, [state]);
1320
+ const handleCellDblClick = useCallback4((rIdx, cIdx) => {
1321
+ if (readOnly) return;
1322
+ const col = state.document.columns[cIdx];
1323
+ if (col?.readOnly) return;
1324
+ state.startEditing(rIdx, cIdx);
1325
+ }, [readOnly, state]);
1326
+ const handleKeyDown = useCallback4((e) => {
1327
+ if (state.editingCell) {
1328
+ if (e.key === "Enter") {
1329
+ e.preventDefault();
1330
+ if (state.commitEditing().status !== "committed") return;
1331
+ state.moveCursor("down");
1332
+ } else if (e.key === "Escape") {
1333
+ e.preventDefault();
1334
+ state.cancelEditing();
1335
+ } else if (e.key === "Tab") {
1336
+ e.preventDefault();
1337
+ if (state.commitEditing().status !== "committed") return;
1338
+ state.moveCursor(e.shiftKey ? "left" : "right");
1339
+ } else {
1340
+ return;
1341
+ }
1342
+ gridRef.current?.focus();
1343
+ return;
1344
+ }
1345
+ if (!state.activeCell) return;
1346
+ if (e.altKey && e.key === "ArrowDown") {
1347
+ e.preventDefault();
1348
+ openListMenu(state.activeCell.row, state.activeCell.col);
1349
+ } else if (e.key === "ArrowUp") {
1350
+ e.preventDefault();
1351
+ state.moveCursor("up", e.shiftKey);
1352
+ } else if (e.key === "ArrowDown") {
1353
+ e.preventDefault();
1354
+ state.moveCursor("down", e.shiftKey);
1355
+ } else if (e.key === "ArrowLeft") {
1356
+ e.preventDefault();
1357
+ state.moveCursor("left", e.shiftKey);
1358
+ } else if (e.key === "ArrowRight") {
1359
+ e.preventDefault();
1360
+ state.moveCursor("right", e.shiftKey);
1361
+ } else if (e.key === "Tab") {
1362
+ e.preventDefault();
1363
+ state.moveCursor(e.shiftKey ? "left" : "right");
1364
+ } else if (e.key === "Enter") {
1365
+ e.preventDefault();
1366
+ const col = state.document.columns[state.activeCell.col];
1367
+ if (!readOnly && !col?.readOnly) {
1368
+ state.startEditing();
1369
+ }
1370
+ } else if (e.key === "Backspace" || e.key === "Delete") {
1371
+ if (readOnly) return;
1372
+ const row = state.document.rows[state.activeCell.row];
1373
+ const col = state.document.columns[state.activeCell.col];
1374
+ if (row && col && !col.readOnly) {
1375
+ e.preventDefault();
1376
+ state.setCellValue(row.id, col.id, "");
1377
+ }
1378
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
1379
+ e.preventDefault();
1380
+ if (e.shiftKey) {
1381
+ state.redo();
1382
+ } else {
1383
+ state.undo();
1384
+ }
1385
+ } else if (e.ctrlKey && !e.metaKey && e.key.toLowerCase() === "y") {
1386
+ e.preventDefault();
1387
+ state.redo();
1388
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "c") {
1389
+ copyShortcutFallback();
1390
+ } else if (!readOnly && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
1391
+ const col = state.document.columns[state.activeCell.col];
1392
+ if (!col?.readOnly) {
1393
+ e.preventDefault();
1394
+ state.startEditing(state.activeCell.row, state.activeCell.col, e.key);
1395
+ }
1396
+ }
1397
+ }, [readOnly, state, copyShortcutFallback]);
1398
+ const activeListValues = useMemo5(
1399
+ () => state.activeCell && !readOnly ? state.controller.getValidationListValues(state.activeCell.row, state.activeCell.col) : null,
1400
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1401
+ [state.controller, state.activeCell, state.document, readOnly]
1402
+ );
1403
+ const activeRule = useMemo5(
1404
+ () => state.activeCell ? state.controller.getDataValidationAt(state.activeCell.row, state.activeCell.col) : void 0,
1405
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1406
+ [state.controller, state.activeCell, state.document]
1407
+ );
1408
+ const inputMessage = activeRule && activeRule.showInputMessage !== false && (activeRule.inputTitle || activeRule.inputMessage) ? activeRule : null;
1409
+ function openListMenu(row, col, anchor) {
1410
+ if (readOnly || state.document.columns[col]?.readOnly) return;
1411
+ const values = state.controller.getValidationListValues(row, col);
1412
+ if (!values) return;
1413
+ const td = anchor ?? gridRef.current?.querySelector(`td[data-row="${row}"][data-col="${col}"]`);
1414
+ const rect = td?.getBoundingClientRect();
1415
+ setListMenu({ x: rect ? rect.left : 0, y: rect ? rect.bottom : 0, row, col, values });
1416
+ }
1417
+ useEffect6(() => {
1418
+ if (!listMenu) return;
1419
+ const close = (e) => {
1420
+ if (!e.target?.closest?.('[aria-label="Allowed values"], .spreadsheet-list-arrow')) setListMenu(null);
1421
+ };
1422
+ window.addEventListener("mousedown", close);
1423
+ return () => window.removeEventListener("mousedown", close);
1424
+ }, [listMenu]);
1425
+ const chooseListValue = (value) => {
1426
+ if (!listMenu) return;
1427
+ const row = state.document.rows[listMenu.row];
1428
+ const col = state.document.columns[listMenu.col];
1429
+ setListMenu(null);
1430
+ if (row && col) state.commitCellInput(row.id, col.id, value);
1431
+ gridRef.current?.focus();
1432
+ };
1433
+ const resolveValidation = (action) => {
1434
+ state.resolveValidationPrompt(action);
1435
+ requestAnimationFrame(() => {
1436
+ if (cellInputRef.current) cellInputRef.current.focus();
1437
+ else gridRef.current?.focus();
1438
+ });
1439
+ };
1440
+ const stateRef = useRef8(state);
1441
+ stateRef.current = state;
1442
+ const onColumnResizeRef = useRef8(onColumnResize);
1443
+ onColumnResizeRef.current = onColumnResize;
1444
+ useEffect6(() => {
1445
+ if (!resizingColId) return;
1446
+ if (typeof document !== "undefined") {
1447
+ document.body.style.cursor = "col-resize";
1448
+ document.body.style.userSelect = "none";
1449
+ }
1450
+ let rafId = null;
1451
+ let pendingWidth = resizeStartWidth;
1452
+ let hasDragged = false;
1453
+ const handleMouseMove = (e) => {
1454
+ const delta = e.clientX - resizeStartX;
1455
+ if (Math.abs(delta) > 2) {
1456
+ hasDragged = true;
1457
+ }
1458
+ pendingWidth = Math.max(30, resizeStartWidth + delta);
1459
+ if (rafId === null) {
1460
+ rafId = requestAnimationFrame(() => {
1461
+ rafId = null;
1462
+ stateRef.current.setColumnWidth(resizingColId, pendingWidth);
1463
+ });
1464
+ }
1465
+ };
1466
+ const handleMouseUp = () => {
1467
+ if (rafId !== null) {
1468
+ cancelAnimationFrame(rafId);
1469
+ rafId = null;
1470
+ }
1471
+ if (hasDragged) {
1472
+ stateRef.current.setColumnWidth(resizingColId, pendingWidth);
1473
+ onColumnResizeRef.current?.(resizingColId, pendingWidth);
1474
+ }
1475
+ setResizingColId(null);
1476
+ if (typeof document !== "undefined") {
1477
+ document.body.style.cursor = "";
1478
+ document.body.style.userSelect = "";
1479
+ }
1480
+ };
1481
+ window.addEventListener("mousemove", handleMouseMove);
1482
+ window.addEventListener("mouseup", handleMouseUp);
1483
+ return () => {
1484
+ if (rafId !== null) {
1485
+ cancelAnimationFrame(rafId);
1486
+ }
1487
+ if (typeof document !== "undefined") {
1488
+ document.body.style.cursor = "";
1489
+ document.body.style.userSelect = "";
1490
+ }
1491
+ window.removeEventListener("mousemove", handleMouseMove);
1492
+ window.removeEventListener("mouseup", handleMouseUp);
1493
+ };
1494
+ }, [resizingColId, resizeStartX, resizeStartWidth]);
1495
+ const measureElementContentWidth = useCallback4((el, container) => {
1496
+ if (typeof document === "undefined") return 0;
1497
+ let maxW = el.scrollWidth || 0;
1498
+ const firstChild = el.firstElementChild;
1499
+ if (firstChild && firstChild.scrollWidth > maxW) {
1500
+ maxW = firstChild.scrollWidth;
1501
+ }
1502
+ const truncated = el.querySelectorAll(".truncate, [title], span, div");
1503
+ truncated.forEach((node) => {
1504
+ if (node.scrollWidth > maxW) {
1505
+ maxW = node.scrollWidth;
1506
+ }
1507
+ });
1508
+ try {
1509
+ const clone = firstChild ? firstChild.cloneNode(true) : el.cloneNode(true);
1510
+ clone.style.cssText = `
1511
+ position: absolute !important;
1512
+ visibility: hidden !important;
1513
+ width: max-content !important;
1514
+ min-width: max-content !important;
1515
+ max-width: none !important;
1516
+ overflow: visible !important;
1517
+ white-space: nowrap !important;
1518
+ pointer-events: none !important;
1519
+ top: -9999px !important;
1520
+ left: -9999px !important;
1521
+ `;
1522
+ const allNodes = clone.querySelectorAll("*");
1523
+ allNodes.forEach((node) => {
1524
+ node.style.maxWidth = "none";
1525
+ node.style.overflow = "visible";
1526
+ node.style.textOverflow = "clip";
1527
+ node.style.whiteSpace = "nowrap";
1528
+ if (node.classList.contains("truncate") || node.classList.contains("min-w-0") || node.classList.contains("w-full") || node.classList.contains("max-w-0") || node.classList.contains("flex-1")) {
1529
+ node.style.width = "max-content";
1530
+ node.style.minWidth = "max-content";
1531
+ }
1532
+ });
1533
+ container.appendChild(clone);
1534
+ const rect = clone.getBoundingClientRect();
1535
+ container.removeChild(clone);
1536
+ if (rect.width > maxW) {
1537
+ maxW = rect.width;
1538
+ }
1539
+ } catch {
1540
+ }
1541
+ return maxW;
1542
+ }, []);
1543
+ const autoFitColumn = useCallback4((colId, colIndex) => {
1544
+ const col = state.document.columns.find((c) => c.id === colId);
1545
+ if (!col) return;
1546
+ let calculatedWidth = 0;
1547
+ if (gridRef.current && typeof document !== "undefined") {
1548
+ const table = gridRef.current.querySelector("table");
1549
+ if (table) {
1550
+ const th = table.querySelector(`thead th[data-col="${colIndex}"]`);
1551
+ if (th) {
1552
+ const headerInner = th.firstElementChild || th;
1553
+ const headerW = measureElementContentWidth(headerInner, gridRef.current);
1554
+ calculatedWidth = Math.max(calculatedWidth, headerW + 28);
1555
+ }
1556
+ const rows = table.querySelectorAll("tbody tr");
1557
+ const sampleLimit = Math.min(rows.length, 150);
1558
+ for (let r = 0; r < sampleLimit; r++) {
1559
+ const tr = rows[r];
1560
+ const td = tr.querySelector(`td[data-col="${colIndex}"][colspan="1"]`);
1561
+ if (td) {
1562
+ const cellW = measureElementContentWidth(td, gridRef.current);
1563
+ calculatedWidth = Math.max(calculatedWidth, cellW + 20);
1564
+ }
1565
+ }
1566
+ }
1567
+ }
1568
+ if (calculatedWidth > 0) {
1569
+ const finalWidth = Math.min(600, Math.max(50, Math.ceil(calculatedWidth)));
1570
+ state.setColumnWidth(colId, finalWidth);
1571
+ } else {
1572
+ state.autoFitColumnWidth?.(colId);
1573
+ }
1574
+ const updatedCol = state.document.columns.find((c) => c.id === colId);
1575
+ if (updatedCol && onColumnResize) {
1576
+ onColumnResize(updatedCol.id, updatedCol.width || 130);
1577
+ }
1578
+ }, [state, measureElementContentWidth, onColumnResize]);
1579
+ const handleResizeDblClick = useCallback4((colId, colIndex, e) => {
1580
+ e.preventDefault();
1581
+ e.stopPropagation();
1582
+ autoFitColumn(colId, colIndex);
1583
+ }, [autoFitColumn]);
1584
+ const handleHeaderDblClick = useCallback4((colId, colIndex, e) => {
1585
+ const target = e.currentTarget;
1586
+ if (target) {
1587
+ const rect = target.getBoundingClientRect();
1588
+ if (rect.right - e.clientX <= 12) {
1589
+ e.preventDefault();
1590
+ e.stopPropagation();
1591
+ autoFitColumn(colId, colIndex);
1592
+ }
1593
+ }
1594
+ }, [autoFitColumn]);
1595
+ const isCellSelected = (rIdx, cIdx) => {
1596
+ if (!state.selectedRange) return false;
1597
+ const minR = Math.min(state.selectedRange.startRow, state.selectedRange.endRow);
1598
+ const maxR = Math.max(state.selectedRange.startRow, state.selectedRange.endRow);
1599
+ const minC = Math.min(state.selectedRange.startCol, state.selectedRange.endCol);
1600
+ const maxC = Math.max(state.selectedRange.startCol, state.selectedRange.endCol);
1601
+ return rIdx >= minR && rIdx <= maxR && cIdx >= minC && cIdx <= maxC;
1602
+ };
1603
+ const isActiveCell = (rIdx, cIdx) => {
1604
+ return state.activeCell?.row === rIdx && state.activeCell?.col === cIdx;
1605
+ };
1606
+ const grid = /* @__PURE__ */ jsxs7(
1607
+ "div",
1608
+ {
1609
+ ref: gridRef,
1610
+ className: "flex flex-col h-full w-full overflow-hidden border border-border bg-background select-none outline-none text-foreground font-sans text-xs",
1611
+ tabIndex: 0,
1612
+ onKeyDown: handleKeyDown,
1613
+ role: "grid",
1614
+ "aria-label": "Spreadsheet",
1615
+ children: [
1616
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 border-b border-border bg-muted/20 px-3 py-1.5 text-xs font-mono", children: [
1617
+ /* @__PURE__ */ jsx7("div", { className: "flex h-6 min-w-[3rem] items-center justify-center rounded border border-border bg-background px-1.5 font-semibold text-foreground", children: coordLabel }),
1618
+ /* @__PURE__ */ jsx7("div", { className: "flex items-center text-muted-foreground select-none", children: /* @__PURE__ */ jsx7("span", { className: "italic font-serif font-bold text-sm text-primary", children: "fx" }) }),
1619
+ /* @__PURE__ */ jsx7(
1620
+ "input",
1621
+ {
1622
+ ref: formulaBarRef,
1623
+ type: "text",
1624
+ className: "flex-1 bg-transparent px-2 py-0.5 outline-none font-sans text-xs text-foreground placeholder:text-muted-foreground/60 focus:bg-background focus:ring-1 focus:ring-primary rounded",
1625
+ placeholder: "Enter a value or formula (e.g. =SUM(A1:A5))",
1626
+ "aria-label": "Formula bar",
1627
+ readOnly: formulaBarReadOnly,
1628
+ "data-spill-formula": !state.editingCell && formulaDraft === null && spillFormula ? "true" : void 0,
1629
+ style: !state.editingCell && formulaDraft === null && spillFormula ? { color: "var(--muted-foreground, #94a3b8)" } : void 0,
1630
+ value: state.editingCell ? state.draftValue : formulaDraft ?? (activeRawValue || spillFormula),
1631
+ onFocus: () => {
1632
+ if (!state.editingCell) setFormulaDraft(activeRawValue);
1633
+ },
1634
+ onChange: (e) => {
1635
+ if (state.editingCell) {
1636
+ state.setDraftValue(e.target.value);
1637
+ } else if (state.activeCell) {
1638
+ setFormulaDraft(e.target.value);
1639
+ }
1640
+ },
1641
+ onBlur: () => commitFormulaBar(),
1642
+ onKeyDown: (e) => {
1643
+ e.stopPropagation();
1644
+ if (e.key === "Enter") {
1645
+ e.preventDefault();
1646
+ commitFormulaBar();
1647
+ } else if (e.key === "Escape") {
1648
+ e.preventDefault();
1649
+ setFormulaDraft(null);
1650
+ if (state.editingCell) state.cancelEditing();
1651
+ }
1652
+ }
1653
+ }
1654
+ ),
1655
+ !formulaBarReadOnly && /* @__PURE__ */ jsx7(
1656
+ FormulaHints,
1657
+ {
1658
+ inputRef: formulaBarRef,
1659
+ value: state.editingCell ? state.draftValue : formulaDraft ?? activeRawValue,
1660
+ onAccept: (next) => state.editingCell ? state.setDraftValue(next) : setFormulaDraft(next),
1661
+ names: nameSuggestions
1662
+ }
1663
+ )
1664
+ ] }),
1665
+ inputMessage && /* @__PURE__ */ jsxs7("div", { role: "note", "data-validation-input-message": "", className: "border-b border-border bg-muted/30 px-3 py-1 text-[11px] text-muted-foreground", children: [
1666
+ inputMessage.inputTitle && /* @__PURE__ */ jsx7("span", { className: "mr-1 font-semibold text-foreground", children: inputMessage.inputTitle }),
1667
+ inputMessage.inputMessage
1668
+ ] }),
1669
+ /* @__PURE__ */ jsx7("div", { ref: scrollRef, className: "relative flex-1 overflow-auto", onScroll: handleGridScroll, children: /* @__PURE__ */ jsxs7("table", { className: "border-separate border-spacing-0 table-fixed w-max min-w-full [&>*>tr:not([data-spacer])>*]:border-b [&>*>tr>*]:border-border", children: [
1670
+ /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsxs7("tr", { className: "h-7 bg-muted sticky top-0 z-20 border-b border-border shadow-xs", children: [
1671
+ /* @__PURE__ */ jsx7("th", { className: "w-12 min-w-[3rem] sticky left-0 z-30 bg-muted border-r border-border text-center text-[10px] text-muted-foreground font-medium", children: "#" }),
1672
+ columnSlots.map((slot) => slot.kind === "space" ? /* @__PURE__ */ jsx7(
1673
+ "th",
1674
+ {
1675
+ "aria-hidden": "true",
1676
+ "data-spacer": "",
1677
+ className: "p-0",
1678
+ style: { width: slot.width, minWidth: slot.width, maxWidth: slot.width, padding: 0 }
1679
+ },
1680
+ slot.key
1681
+ ) : (({ column: col, index: cIdx, frozen, stickyLeft }) => /* @__PURE__ */ jsxs7(
1682
+ "th",
1683
+ {
1684
+ "data-col": cIdx,
1685
+ className: `relative border-r border-border px-2 text-left font-medium text-muted-foreground hover:bg-muted/90 transition-colors group overflow-hidden max-w-0 ${onColumnHeaderClick ? "cursor-pointer" : "cursor-default"}`,
1686
+ style: {
1687
+ width: col.width || 130,
1688
+ minWidth: col.width || 130,
1689
+ maxWidth: col.width || 130,
1690
+ overflow: "hidden",
1691
+ ...frozen ? { position: "sticky", left: ROW_HEADER_WIDTH + stickyLeft, zIndex: 25, background: "var(--muted, #f4f4f5)" } : {}
1692
+ },
1693
+ onClick: () => onColumnHeaderClick?.(col),
1694
+ onDoubleClick: (e) => handleHeaderDblClick(col.id, cIdx, e),
1695
+ children: [
1696
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between gap-1 min-w-0 overflow-hidden truncate", children: [
1697
+ /* @__PURE__ */ jsx7("span", { className: "truncate font-semibold text-foreground text-xs min-w-0", children: col.title }),
1698
+ filteredColIds.has(col.id) && /* @__PURE__ */ jsx7("span", { className: "text-[10px] text-primary shrink-0", title: "Filtered", "data-filtered": "true", children: "\u23F7" }),
1699
+ /* @__PURE__ */ jsxs7("span", { className: "text-[10px] text-muted-foreground/70 font-mono font-normal shrink-0", children: [
1700
+ "(",
1701
+ col.key,
1702
+ ")"
1703
+ ] })
1704
+ ] }),
1705
+ /* @__PURE__ */ jsx7(
1706
+ "div",
1707
+ {
1708
+ className: "col-resize-handle",
1709
+ style: { position: "absolute", top: 0, bottom: 0, width: 12, right: -6, cursor: "col-resize", zIndex: 20, display: "flex", justifyContent: "center", touchAction: "none" },
1710
+ onMouseEnter: () => setHoveredColId(col.id),
1711
+ onMouseLeave: () => setHoveredColId((cur) => cur === col.id ? null : cur),
1712
+ onMouseDown: (e) => {
1713
+ e.preventDefault();
1714
+ e.stopPropagation();
1715
+ setResizingColId(col.id);
1716
+ setResizeStartX(e.clientX);
1717
+ setResizeStartWidth(col.width || 130);
1718
+ },
1719
+ onDoubleClick: (e) => handleResizeDblClick(col.id, cIdx, e),
1720
+ role: "separator",
1721
+ "aria-orientation": "vertical",
1722
+ children: /* @__PURE__ */ jsx7(
1723
+ "div",
1724
+ {
1725
+ className: "col-resize-handle-line",
1726
+ style: {
1727
+ width: 2,
1728
+ height: "100%",
1729
+ pointerEvents: "none",
1730
+ transition: "background-color 150ms ease",
1731
+ backgroundColor: resizingColId === col.id ? "var(--primary, #6366f1)" : hoveredColId === col.id ? "color-mix(in srgb, var(--primary, #6366f1) 75%, transparent)" : "transparent"
1732
+ }
1733
+ }
1734
+ )
1735
+ }
1736
+ )
1737
+ ]
1738
+ },
1739
+ col.id
1740
+ ))(slot.viewCol)),
1741
+ onAddColumnClick && /* @__PURE__ */ jsx7(
1742
+ "th",
1743
+ {
1744
+ className: "w-10 min-w-[2.5rem] px-1 text-center bg-muted/40 hover:bg-muted cursor-pointer transition-colors",
1745
+ onClick: onAddColumnClick,
1746
+ title: "Add Column",
1747
+ children: /* @__PURE__ */ jsx7("span", { className: "text-xs font-bold text-muted-foreground", children: "+" })
1748
+ }
1749
+ )
1750
+ ] }) }),
1751
+ /* @__PURE__ */ jsx7("tbody", { children: rowSlots.map((rowSlot) => {
1752
+ if (rowSlot.kind === "space") {
1753
+ return /* @__PURE__ */ jsx7("tr", { "aria-hidden": "true", "data-spacer": "", style: { height: rowSlot.height }, children: /* @__PURE__ */ jsx7("td", { colSpan: columnSlots.length + 2, style: { padding: 0, border: 0 } }) }, rowSlot.key);
1754
+ }
1755
+ const viewRow = rowSlot.viewRow;
1756
+ if (viewRow.kind === "group") {
1757
+ return /* @__PURE__ */ jsx7(
1758
+ SpreadsheetGroupRow,
1759
+ {
1760
+ group: viewRow.group,
1761
+ columns: renderedColumns,
1762
+ labelColumns: view.columns,
1763
+ leadingColumns: viewWindow.virtual ? viewWindow.frozenColumns : void 0,
1764
+ leftSpace: viewWindow.leftSpace,
1765
+ rightSpace: viewWindow.rightSpace,
1766
+ groupColId: state.document.table?.grouping?.colId ?? "",
1767
+ onToggle: state.toggleGroupCollapsed,
1768
+ trailingCell: !!onAddColumnClick
1769
+ },
1770
+ `group:${viewRow.group.key}`
1771
+ );
1772
+ }
1773
+ const displayRowIndex = viewRow.index;
1774
+ const selection = state.selectedRange ? normalizeRange2(state.selectedRange) : null;
1775
+ return /* @__PURE__ */ jsxs7(
1776
+ "tr",
1777
+ {
1778
+ "data-row": displayRowIndex,
1779
+ className: `border-b border-border hover:bg-muted/10 transition-colors ${viewRow.section !== "body" ? "bg-muted/30 font-semibold" : ""}`,
1780
+ style: { height: viewRow.row.height || 32 },
1781
+ children: [
1782
+ /* @__PURE__ */ jsx7(
1783
+ "td",
1784
+ {
1785
+ className: "w-12 min-w-[3rem] sticky left-0 z-10 bg-muted border-r border-border text-center text-[10px] text-muted-foreground font-mono select-none",
1786
+ style: viewRow.frozen ? { top: COLUMN_HEADER_HEIGHT + viewRow.stickyTop, zIndex: 15 } : void 0,
1787
+ children: displayRowIndex + 1
1788
+ }
1789
+ ),
1790
+ columnSlots.map((slot) => {
1791
+ if (slot.kind === "space") return /* @__PURE__ */ jsx7("td", { "aria-hidden": "true", "data-spacer": "", style: { padding: 0 } }, slot.key);
1792
+ const viewCol = slot.viewCol;
1793
+ const span = view.getCellSpan(displayRowIndex, viewCol.index);
1794
+ if (!span) return null;
1795
+ const rIdx = span.source.row;
1796
+ const cIdx = span.source.col;
1797
+ const row = state.document.rows[rIdx];
1798
+ const col = state.document.columns[cIdx];
1799
+ const cell = state.getCell(row.id, col.id);
1800
+ const selected = isCellSelected(rIdx, cIdx);
1801
+ const active = isActiveCell(rIdx, cIdx);
1802
+ const isEditing = state.editingCell?.row === rIdx && state.editingCell?.col === cIdx;
1803
+ const conditional = overlay.get(rIdx, cIdx);
1804
+ const conditionalCss = getConditionalFormatCss2(conditional);
1805
+ const circled = state.controller.isInvalidDataCircled(rIdx, cIdx);
1806
+ const showListArrow = active && !isEditing && !!activeListValues;
1807
+ const inFillPreview = !!fillTarget && !selected && rangeContains(fillTarget, displayRowIndex, viewCol.index);
1808
+ const showFillHandle = !readOnly && !isEditing && !!selection && (span.merge ? rangeContains(span.merge, selection.endRow, selection.endCol) : selection.endRow === rIdx && selection.endCol === cIdx);
1809
+ return /* @__PURE__ */ jsxs7(
1810
+ "td",
1811
+ {
1812
+ "data-row": rIdx,
1813
+ "data-col": cIdx,
1814
+ rowSpan: span.rowSpan,
1815
+ colSpan: span.colSpan,
1816
+ className: `relative border-r border-border px-2 py-1 truncate max-w-0 text-foreground transition-colors ${selected ? "bg-primary/10" : ""} ${active ? "ring-2 ring-primary ring-inset z-10" : ""} ${viewCol.header ? "font-semibold" : ""}`,
1817
+ style: {
1818
+ maxWidth: 0,
1819
+ overflow: "hidden",
1820
+ ...frozenCellStyle(viewRow, viewCol),
1821
+ ...conditionalCss,
1822
+ // Keep the selection tint visible over a conditional fill.
1823
+ ...conditionalCss.background && selected ? { boxShadow: "inset 0 0 0 9999px color-mix(in srgb, var(--primary, #6366f1) 12%, transparent)" } : {},
1824
+ ...inFillPreview ? { outline: "1px dashed var(--primary, #6366f1)", outlineOffset: -1 } : {}
1825
+ },
1826
+ "data-cf-fill": conditional?.fill,
1827
+ onClick: (e) => handleCellClick(rIdx, cIdx, e),
1828
+ onDoubleClick: () => handleCellDblClick(rIdx, cIdx),
1829
+ onContextMenu: (e) => handleCellContextMenu(rIdx, cIdx, e),
1830
+ onMouseOver: () => extendFill(displayRowIndex, viewCol.index),
1831
+ children: [
1832
+ isEditing ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
1833
+ /* @__PURE__ */ jsx7(
1834
+ "input",
1835
+ {
1836
+ ref: cellInputRef,
1837
+ type: "text",
1838
+ className: "absolute inset-0 w-full h-full px-2 py-1 bg-background text-foreground outline-none font-sans text-xs border-2 border-primary z-20",
1839
+ value: state.draftValue,
1840
+ onChange: (e) => state.setDraftValue(e.target.value),
1841
+ onBlur: () => state.commitEditing(),
1842
+ onFocus: (e) => {
1843
+ const end = e.currentTarget.value.length;
1844
+ e.currentTarget.setSelectionRange(end, end);
1845
+ },
1846
+ autoFocus: true
1847
+ }
1848
+ ),
1849
+ /* @__PURE__ */ jsx7(FormulaHints, { inputRef: cellInputRef, value: state.draftValue, onAccept: state.setDraftValue, names: nameSuggestions })
1850
+ ] }) : customCellRenderer ? customCellRenderer({ row, col, cell }) : /* @__PURE__ */ jsx7(CellText, { cell, conditional }),
1851
+ circled && /* @__PURE__ */ jsx7(
1852
+ "span",
1853
+ {
1854
+ className: "spreadsheet-invalid-circle",
1855
+ "aria-label": "Invalid value",
1856
+ style: { position: "absolute", inset: "1px 2px", border: "2px solid #e11d48", borderRadius: "50%", pointerEvents: "none", zIndex: 6 }
1857
+ }
1858
+ ),
1859
+ showListArrow && /* @__PURE__ */ jsx7(
1860
+ "button",
1861
+ {
1862
+ type: "button",
1863
+ className: "spreadsheet-list-arrow",
1864
+ "aria-label": "Show allowed values",
1865
+ "aria-haspopup": "listbox",
1866
+ tabIndex: -1,
1867
+ style: { position: "absolute", right: 1, top: 1, bottom: 1, width: 16, zIndex: 24, fontSize: 10, lineHeight: 1, background: "var(--muted, #f4f4f5)", border: "1px solid var(--border, #e4e4e7)", borderRadius: 2, cursor: "pointer", padding: 0 },
1868
+ onMouseDown: (e) => e.stopPropagation(),
1869
+ onClick: (e) => {
1870
+ e.stopPropagation();
1871
+ if (listMenu) setListMenu(null);
1872
+ else openListMenu(rIdx, cIdx, e.currentTarget.parentElement);
1873
+ },
1874
+ children: "\u25BE"
1875
+ }
1876
+ ),
1877
+ activeSpill && rangeContains(activeSpill.range, rIdx, cIdx) && /* @__PURE__ */ jsx7("span", { "aria-hidden": "true", className: "spreadsheet-spill-border", style: spillBorderStyle(activeSpill.range, rIdx, cIdx) }),
1878
+ showFillHandle && /* @__PURE__ */ jsx7(
1879
+ "div",
1880
+ {
1881
+ className: "spreadsheet-fill-handle",
1882
+ "aria-label": "Fill handle",
1883
+ onMouseDown: startFill,
1884
+ onClick: (e) => e.stopPropagation(),
1885
+ style: { position: "absolute", right: 0, bottom: 0, width: 7, height: 7, background: "var(--primary, #6366f1)", cursor: "crosshair", zIndex: 25 }
1886
+ }
1887
+ )
1888
+ ]
1889
+ },
1890
+ viewCol.column.id
1891
+ );
1892
+ }),
1893
+ onAddColumnClick && /* @__PURE__ */ jsx7("td", { className: "w-10 min-w-[2.5rem] bg-muted/10 border-r border-border" })
1894
+ ]
1895
+ },
1896
+ viewRow.row.id
1897
+ );
1898
+ }) })
1899
+ ] }) }),
1900
+ menu && /* @__PURE__ */ jsx7(
1901
+ SpreadsheetContextMenu,
1902
+ {
1903
+ items: menu.items,
1904
+ x: menu.x,
1905
+ y: menu.y,
1906
+ onClose: () => {
1907
+ setMenu(null);
1908
+ gridRef.current?.focus();
1909
+ },
1910
+ onOpenPanel: setPanel
1911
+ }
1912
+ ),
1913
+ panel === "conditionalFormatting" && /* @__PURE__ */ jsx7(SpreadsheetConditionalFormatPanel, { state, onClose: () => setPanel(null) }),
1914
+ panel === "dataValidation" && /* @__PURE__ */ jsx7(SpreadsheetDataValidationPanel, { state, onClose: () => setPanel(null) }),
1915
+ listMenu && /* @__PURE__ */ jsxs7(
1916
+ "div",
1917
+ {
1918
+ role: "listbox",
1919
+ "aria-label": "Allowed values",
1920
+ className: "fixed z-50 max-h-60 min-w-[8rem] overflow-y-auto rounded-md border border-border bg-popover py-1 text-xs text-popover-foreground shadow-lg",
1921
+ style: { left: listMenu.x, top: listMenu.y, background: "var(--popover, var(--background, #fff))" },
1922
+ onKeyDown: (e) => {
1923
+ e.stopPropagation();
1924
+ if (e.key === "Escape") {
1925
+ setListMenu(null);
1926
+ gridRef.current?.focus();
1927
+ }
1928
+ },
1929
+ children: [
1930
+ listMenu.values.length === 0 && /* @__PURE__ */ jsx7("div", { className: "px-3 py-1 text-muted-foreground", children: "No values" }),
1931
+ listMenu.values.map((value, i) => /* @__PURE__ */ jsx7(
1932
+ "button",
1933
+ {
1934
+ type: "button",
1935
+ role: "option",
1936
+ "aria-selected": false,
1937
+ className: "block w-full px-3 py-1 text-left hover:bg-muted",
1938
+ onClick: () => chooseListValue(value),
1939
+ children: value
1940
+ },
1941
+ `${i}:${value}`
1942
+ ))
1943
+ ]
1944
+ }
1945
+ ),
1946
+ state.validationPrompt && /* @__PURE__ */ jsx7(SpreadsheetValidationDialog, { prompt: state.validationPrompt, onResolve: resolveValidation }),
1947
+ /* @__PURE__ */ jsx7("style", { children: `
1948
+ .col-resize-handle:hover .col-resize-handle-line {
1949
+ background-color: var(--primary, #6366f1);
1950
+ opacity: 0.75;
1951
+ }
1952
+ .col-resize-handle.active .col-resize-handle-line {
1953
+ background-color: var(--primary, #6366f1);
1954
+ opacity: 1;
1955
+ }
1956
+ table.table-fixed th {
1957
+ overflow: hidden;
1958
+ }
1959
+ table.table-fixed td {
1960
+ max-width: 0;
1961
+ overflow: hidden;
1962
+ }
1963
+ ` })
1964
+ ]
1965
+ }
1966
+ );
1967
+ if (!showSheetTabs) return grid;
1968
+ return /* @__PURE__ */ jsxs7("div", { className: "flex h-full w-full flex-col", children: [
1969
+ /* @__PURE__ */ jsx7("div", { className: "min-h-0 flex-1", children: grid }),
1970
+ /* @__PURE__ */ jsx7(SheetTabs, { state, readOnly })
1971
+ ] });
1972
+ };
1973
+
1974
+ // src/spreadsheet/NameManager.tsx
1975
+ import { useMemo as useMemo6, useState as useState9 } from "react";
1976
+ import { getNameManagerRows, parseFormula } from "@pixerate/editor/spreadsheet";
1977
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1978
+ var EMPTY_DRAFT = { original: null, name: "", refersTo: "=", scope: "", comment: "" };
1979
+ var NameManager = ({ state, readOnly = false, onClose, className = "" }) => {
1980
+ const { controller, sheets } = state;
1981
+ const rows = useMemo6(
1982
+ () => getNameManagerRows(controller),
1983
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1984
+ [controller, state.document, state.names]
1985
+ );
1986
+ const [draft, setDraft] = useState9(EMPTY_DRAFT);
1987
+ const [error, setError] = useState9(null);
1988
+ const edit = (row) => {
1989
+ setError(null);
1990
+ setDraft({
1991
+ original: { name: row.name, sheetId: row.sheetId },
1992
+ name: row.name,
1993
+ refersTo: row.refersTo,
1994
+ scope: row.sheetId ?? "",
1995
+ comment: row.comment ?? ""
1996
+ });
1997
+ };
1998
+ const save = (e) => {
1999
+ e.preventDefault();
2000
+ const scope = draft.scope ? { sheet: draft.scope } : {};
2001
+ try {
2002
+ const invalid = controller.validateDefinedName(draft.name.trim());
2003
+ if (invalid) throw new Error(invalid);
2004
+ try {
2005
+ parseFormula(draft.refersTo.trim().startsWith("=") ? draft.refersTo.trim() : `=${draft.refersTo.trim()}`);
2006
+ } catch (err) {
2007
+ throw new Error(`There's a problem with this formula: ${err?.message ?? draft.refersTo}`);
2008
+ }
2009
+ const original = draft.original;
2010
+ if (original) {
2011
+ const originalScope = original.sheetId ? { sheet: original.sheetId } : {};
2012
+ if ((original.sheetId ?? "") !== draft.scope) {
2013
+ state.deleteName(original.name, originalScope);
2014
+ } else if (original.name !== draft.name) {
2015
+ state.renameName(original.name, draft.name.trim(), originalScope);
2016
+ }
2017
+ }
2018
+ state.defineName(draft.name.trim(), draft.refersTo, { ...scope, comment: draft.comment || void 0 });
2019
+ setDraft(EMPTY_DRAFT);
2020
+ setError(null);
2021
+ } catch (err) {
2022
+ setError(err?.message ?? String(err));
2023
+ }
2024
+ };
2025
+ return /* @__PURE__ */ jsxs8("section", { "aria-label": "Name Manager", className: `flex flex-col gap-2 text-xs ${className}`, "data-name-manager": "", children: [
2026
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between", children: [
2027
+ /* @__PURE__ */ jsx8("h3", { className: "m-0 text-sm font-semibold", children: "Name Manager" }),
2028
+ onClose && /* @__PURE__ */ jsx8("button", { type: "button", "aria-label": "Close Name Manager", className: "rounded px-2 hover:bg-muted", onClick: onClose, children: "\xD7" })
2029
+ ] }),
2030
+ /* @__PURE__ */ jsxs8("table", { className: "w-full border-collapse", children: [
2031
+ /* @__PURE__ */ jsx8("thead", { children: /* @__PURE__ */ jsxs8("tr", { className: "text-left text-muted-foreground", children: [
2032
+ /* @__PURE__ */ jsx8("th", { className: "px-1 font-medium", children: "Name" }),
2033
+ /* @__PURE__ */ jsx8("th", { className: "px-1 font-medium", children: "Value" }),
2034
+ /* @__PURE__ */ jsx8("th", { className: "px-1 font-medium", children: "Refers to" }),
2035
+ /* @__PURE__ */ jsx8("th", { className: "px-1 font-medium", children: "Scope" }),
2036
+ !readOnly && /* @__PURE__ */ jsx8("th", { className: "px-1" })
2037
+ ] }) }),
2038
+ /* @__PURE__ */ jsxs8("tbody", { children: [
2039
+ rows.length === 0 && /* @__PURE__ */ jsx8("tr", { children: /* @__PURE__ */ jsx8("td", { colSpan: 5, className: "px-1 py-2 text-muted-foreground", children: "No names yet." }) }),
2040
+ rows.map((row) => /* @__PURE__ */ jsxs8("tr", { "data-name": row.name, className: "border-t border-border", title: row.comment, children: [
2041
+ /* @__PURE__ */ jsx8("td", { className: "px-1 py-0.5 font-mono font-semibold", children: row.name }),
2042
+ /* @__PURE__ */ jsx8("td", { className: "px-1 py-0.5", children: row.value }),
2043
+ /* @__PURE__ */ jsx8("td", { className: "px-1 py-0.5 font-mono", children: row.refersTo }),
2044
+ /* @__PURE__ */ jsx8("td", { className: "px-1 py-0.5", children: row.scopeLabel }),
2045
+ !readOnly && /* @__PURE__ */ jsxs8("td", { className: "whitespace-nowrap px-1 py-0.5 text-right", children: [
2046
+ /* @__PURE__ */ jsx8("button", { type: "button", "aria-label": `Edit ${row.name}`, className: "rounded px-1 hover:bg-muted", onClick: () => edit(row), children: "Edit" }),
2047
+ /* @__PURE__ */ jsx8(
2048
+ "button",
2049
+ {
2050
+ type: "button",
2051
+ "aria-label": `Delete ${row.name}`,
2052
+ className: "rounded px-1 text-destructive hover:bg-muted",
2053
+ onClick: () => {
2054
+ state.deleteName(row.name, row.sheetId ? { sheet: row.sheetId } : {});
2055
+ if (draft.original?.name === row.name && (draft.original.sheetId ?? "") === (row.sheetId ?? "")) setDraft(EMPTY_DRAFT);
2056
+ },
2057
+ children: "Delete"
2058
+ }
2059
+ )
2060
+ ] })
2061
+ ] }, `${row.sheetId ?? ""}!${row.name}`))
2062
+ ] })
2063
+ ] }),
2064
+ !readOnly && /* @__PURE__ */ jsxs8("form", { className: "grid grid-cols-[auto_1fr] items-center gap-x-2 gap-y-1", onSubmit: save, "aria-label": draft.original ? `Edit name ${draft.original.name}` : "New name", children: [
2065
+ /* @__PURE__ */ jsx8("label", { htmlFor: "pixerate-name-manager-name", children: "Name" }),
2066
+ /* @__PURE__ */ jsx8(
2067
+ "input",
2068
+ {
2069
+ id: "pixerate-name-manager-name",
2070
+ className: "rounded border border-border bg-background px-1 py-0.5",
2071
+ value: draft.name,
2072
+ onChange: (e) => setDraft({ ...draft, name: e.target.value })
2073
+ }
2074
+ ),
2075
+ /* @__PURE__ */ jsx8("label", { htmlFor: "pixerate-name-manager-refers", children: "Refers to" }),
2076
+ /* @__PURE__ */ jsx8(
2077
+ "input",
2078
+ {
2079
+ id: "pixerate-name-manager-refers",
2080
+ className: "rounded border border-border bg-background px-1 py-0.5 font-mono",
2081
+ value: draft.refersTo,
2082
+ onChange: (e) => setDraft({ ...draft, refersTo: e.target.value })
2083
+ }
2084
+ ),
2085
+ /* @__PURE__ */ jsx8("label", { htmlFor: "pixerate-name-manager-scope", children: "Scope" }),
2086
+ /* @__PURE__ */ jsxs8(
2087
+ "select",
2088
+ {
2089
+ id: "pixerate-name-manager-scope",
2090
+ className: "rounded border border-border bg-background px-1 py-0.5",
2091
+ value: draft.scope,
2092
+ onChange: (e) => setDraft({ ...draft, scope: e.target.value }),
2093
+ children: [
2094
+ /* @__PURE__ */ jsx8("option", { value: "", children: "Workbook" }),
2095
+ sheets.map((sheet) => /* @__PURE__ */ jsx8("option", { value: sheet.id, children: sheet.name }, sheet.id))
2096
+ ]
2097
+ }
2098
+ ),
2099
+ /* @__PURE__ */ jsx8("label", { htmlFor: "pixerate-name-manager-comment", children: "Comment" }),
2100
+ /* @__PURE__ */ jsx8(
2101
+ "input",
2102
+ {
2103
+ id: "pixerate-name-manager-comment",
2104
+ className: "rounded border border-border bg-background px-1 py-0.5",
2105
+ value: draft.comment,
2106
+ onChange: (e) => setDraft({ ...draft, comment: e.target.value })
2107
+ }
2108
+ ),
2109
+ /* @__PURE__ */ jsxs8("div", { className: "col-span-2 flex gap-2", children: [
2110
+ /* @__PURE__ */ jsx8("button", { type: "submit", className: "rounded bg-primary px-2 py-0.5 text-primary-foreground", children: draft.original ? "Save" : "Add name" }),
2111
+ draft.original && /* @__PURE__ */ jsx8(
2112
+ "button",
2113
+ {
2114
+ type: "button",
2115
+ className: "rounded px-2 py-0.5 hover:bg-muted",
2116
+ onClick: () => {
2117
+ setDraft(EMPTY_DRAFT);
2118
+ setError(null);
2119
+ },
2120
+ children: "Cancel"
2121
+ }
2122
+ )
2123
+ ] })
2124
+ ] }),
2125
+ error && /* @__PURE__ */ jsx8("div", { role: "alert", className: "text-destructive", children: error })
2126
+ ] });
2127
+ };
2128
+
2129
+ export {
2130
+ useSpreadsheetEditor,
2131
+ SpreadsheetContextMenu,
2132
+ SpreadsheetGroupRow,
2133
+ useSpreadsheetClipboard,
2134
+ useFillHandle,
2135
+ SpreadsheetValidationDialog,
2136
+ SpreadsheetConditionalFormatPanel,
2137
+ SpreadsheetDataValidationPanel,
2138
+ useSpreadsheetViewport,
2139
+ FormulaHints,
2140
+ SheetTabs,
2141
+ SpreadsheetEditor,
2142
+ NameManager
2143
+ };
2144
+ //# sourceMappingURL=chunk-34HEZX7J.js.map