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