@pixerate/editor-react 0.10.5 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ImageEditor,
3
3
  useImageEditor
4
- } from "./chunk-H7HCHLZU.js";
4
+ } from "./chunk-TL27COKM.js";
5
5
 
6
6
  // src/hooks/usePromptEditor.ts
7
7
  import { useMemo, useEffect, useRef } from "react";
@@ -16,7 +16,8 @@ import {
16
16
  plainTextToTipTapHtml,
17
17
  plainTextToSlice,
18
18
  getEditorText,
19
- tokenizePrompt
19
+ tokenizePrompt,
20
+ FileImport
20
21
  } from "@pixerate/editor";
21
22
  var DEFAULT_TEMPLATES = [];
22
23
  var DEFAULT_COLOR_MAP = /* @__PURE__ */ new Map([
@@ -41,8 +42,12 @@ function usePromptEditor({
41
42
  onMilestone,
42
43
  containerId = "editor-container",
43
44
  jsonVariables,
44
- speed = 2
45
+ speed = 2,
46
+ fileImport
45
47
  }) {
48
+ const fileImportRef = useRef(fileImport);
49
+ fileImportRef.current = fileImport;
50
+ const fileImportEnabled = !!fileImport;
46
51
  const slashCommandActiveRef = useRef(slashCommandActive);
47
52
  slashCommandActiveRef.current = slashCommandActive;
48
53
  const clearSlashCommandRef = useRef(clearSlashCommand);
@@ -97,7 +102,14 @@ function usePromptEditor({
97
102
  TemplateSuggestions.configure({
98
103
  templates
99
104
  }),
100
- LoadingNode
105
+ LoadingNode,
106
+ ...fileImportEnabled ? [
107
+ FileImport.configure({
108
+ ...typeof fileImportRef.current === "object" ? fileImportRef.current : {},
109
+ // Prompt editors only ever receive plain text.
110
+ asPlainText: true
111
+ })
112
+ ] : []
101
113
  ],
102
114
  content: plainTextToTipTapHtml(content),
103
115
  onUpdate: ({ editor: ed }) => {
@@ -194,7 +206,7 @@ function usePromptEditor({
194
206
  },
195
207
  // Only options baked into extensions recreate the editor; editable state,
196
208
  // placeholder, class names and content are applied to the live instance.
197
- [resolvedColorMap, templateKey, speed, jsonVariablesKey]
209
+ [resolvedColorMap, templateKey, speed, jsonVariablesKey, fileImportEnabled]
198
210
  );
199
211
  useEffect(() => {
200
212
  if (editor && !editor.isDestroyed) {
@@ -240,10 +252,22 @@ function usePromptEditor({
240
252
 
241
253
  // src/hooks/useEditor.ts
242
254
  import { useEditor as useTipTapEditor } from "@tiptap/react";
243
- import { createRichTextPreset } from "@pixerate/editor";
255
+ import {
256
+ createRichTextPreset
257
+ } from "@pixerate/editor";
244
258
  function useEditor2(options = {}) {
245
- const { richTextOptions, extensions = [], ...editorOptions } = options;
246
- const preset = createRichTextPreset(richTextOptions);
259
+ const {
260
+ richTextOptions,
261
+ fileImport,
262
+ richClipboard,
263
+ extensions = [],
264
+ ...editorOptions
265
+ } = options;
266
+ const preset = createRichTextPreset({
267
+ ...fileImport !== void 0 ? { fileImport } : {},
268
+ ...richClipboard !== void 0 ? { richClipboard } : {},
269
+ ...richTextOptions
270
+ });
247
271
  const combinedExtensions = [...preset, ...extensions];
248
272
  return useTipTapEditor({
249
273
  extensions: combinedExtensions,
@@ -473,6 +497,42 @@ function useHistoryShortcuts(manager, options = {}) {
473
497
  }, [manager, enabled, target, ignoreEditableElements]);
474
498
  }
475
499
 
500
+ // src/hooks/useFileImport.ts
501
+ import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef5, useSyncExternalStore as useSyncExternalStore2 } from "react";
502
+ import {
503
+ FileImportController
504
+ } from "@pixerate/editor";
505
+ function useFileImport(editor, options = {}) {
506
+ const editorRef = useRef5(editor);
507
+ editorRef.current = editor;
508
+ const controller = useMemo2(
509
+ () => new FileImportController(() => {
510
+ const current = editorRef.current;
511
+ return current && !current.isDestroyed ? current : null;
512
+ }, options),
513
+ []
514
+ );
515
+ useEffect4(() => {
516
+ controller.setOptions(options);
517
+ });
518
+ const state = useSyncExternalStore2(controller.subscribe, controller.getState, controller.getState);
519
+ return {
520
+ ...state,
521
+ accept: controller.accept,
522
+ openFilePicker: controller.openFilePicker,
523
+ importFiles: controller.importFiles
524
+ };
525
+ }
526
+
527
+ // src/hooks/useAgentToolProvider.ts
528
+ import { useEffect as useEffect5 } from "react";
529
+ function useAgentToolProvider(registry, provider) {
530
+ useEffect5(() => {
531
+ if (!registry || !provider) return;
532
+ return registry.register(provider);
533
+ }, [registry, provider]);
534
+ }
535
+
476
536
  // src/components/EditorContent.tsx
477
537
  import {
478
538
  EditorContent as TipTapEditorContent
@@ -487,7 +547,7 @@ var EditorContent = ({
487
547
  };
488
548
 
489
549
  // src/components/BubbleMenu.tsx
490
- import { useEffect as useEffect4, useRef as useRef5, useState as useState3 } from "react";
550
+ import { useEffect as useEffect6, useRef as useRef6, useState as useState3 } from "react";
491
551
  import { createPortal } from "react-dom";
492
552
  import {
493
553
  BubbleMenuPlugin
@@ -501,12 +561,12 @@ var BubbleMenu = ({
501
561
  pluginKey = "bubbleMenu",
502
562
  updateDelay
503
563
  }) => {
504
- const menuEl = useRef5(null);
564
+ const menuEl = useRef6(null);
505
565
  const [mounted, setMounted] = useState3(false);
506
566
  if (!menuEl.current && typeof document !== "undefined") {
507
567
  menuEl.current = document.createElement("div");
508
568
  }
509
- useEffect4(() => {
569
+ useEffect6(() => {
510
570
  if (!editor || !menuEl.current || editor.isDestroyed) {
511
571
  return;
512
572
  }
@@ -537,28 +597,31 @@ var BubbleMenu = ({
537
597
  };
538
598
 
539
599
  // src/components/TemplateRenderer.tsx
540
- import { useMemo as useMemo2 } from "react";
600
+ import { useMemo as useMemo3 } from "react";
541
601
  import {
542
- tokenizePrompt as tokenizePrompt2,
602
+ evaluatePromptTokens,
543
603
  resolveTemplates
544
604
  } from "@pixerate/editor";
545
- import { jsx as jsx2 } from "react/jsx-runtime";
605
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
606
+ var CONDITIONAL_PREFIX = { if: "?", unless: "!", end: "/" };
546
607
  var TemplateRenderer = ({
547
608
  content,
548
609
  templates = [],
549
610
  templateColorMap,
550
611
  resolve = false,
612
+ bindings,
613
+ values,
551
614
  className = "font-mono text-sm leading-relaxed whitespace-pre-wrap break-words",
552
615
  onTemplateClick,
553
616
  onVariableClick
554
617
  }) => {
555
- const displayText = useMemo2(() => {
618
+ const displayText = useMemo3(() => {
556
619
  if (!resolve || templates.length === 0) return content;
557
- return resolveTemplates(content, templates);
558
- }, [content, templates, resolve]);
559
- const tokens = useMemo2(() => {
560
- return tokenizePrompt2(displayText);
561
- }, [displayText]);
620
+ return resolveTemplates(content, templates, { bindings });
621
+ }, [content, templates, resolve, bindings]);
622
+ const tokens = useMemo3(() => {
623
+ return evaluatePromptTokens(displayText, values);
624
+ }, [displayText, values]);
562
625
  return /* @__PURE__ */ jsx2("div", { className, children: tokens.map((token, index) => {
563
626
  if (token.type === "template") {
564
627
  const colors = templateColorMap?.get(token.value) || {
@@ -582,13 +645,49 @@ var TemplateRenderer = ({
582
645
  );
583
646
  }
584
647
  if (token.type === "variable") {
585
- return /* @__PURE__ */ jsx2(
648
+ const details = [
649
+ `Variable: ${token.value}`,
650
+ token.description,
651
+ token.defaultValue !== void 0 ? `Default: ${token.defaultValue || "(empty)"}` : void 0
652
+ ].filter(Boolean).join("\n");
653
+ if (token.resolvedValue !== void 0) {
654
+ return /* @__PURE__ */ jsx2(
655
+ "span",
656
+ {
657
+ onClick: () => onVariableClick?.(token.value),
658
+ "data-variable": token.value,
659
+ "data-value-source": token.valueSource,
660
+ className: token.valueSource === "value" ? "inline px-1 mx-0.5 rounded-md bg-emerald-400/15 text-emerald-700 dark:text-emerald-300 border-b border-emerald-400/60 cursor-pointer" : "inline px-1 mx-0.5 rounded-md bg-violet-400/10 text-violet-600 dark:text-violet-300 italic border-b border-dashed border-violet-400/60 cursor-pointer",
661
+ title: details,
662
+ children: token.resolvedValue
663
+ },
664
+ index
665
+ );
666
+ }
667
+ return /* @__PURE__ */ jsxs(
586
668
  "span",
587
669
  {
588
670
  onClick: () => onVariableClick?.(token.value),
671
+ "data-variable": token.value,
589
672
  className: "inline-flex items-center px-1.5 py-0.5 mx-0.5 text-xs font-medium rounded-md border border-violet-400/40 bg-violet-400/15 text-violet-600 dark:text-violet-300 hover:brightness-110 cursor-pointer transition-all",
590
- title: `Variable: ${token.value}`,
591
- children: `{${token.value}}`
673
+ title: details,
674
+ children: [
675
+ `{${token.value}`,
676
+ token.defaultValue !== void 0 && /* @__PURE__ */ jsx2("span", { className: "opacity-60", children: `|${token.defaultValue}` }),
677
+ "}"
678
+ ]
679
+ },
680
+ index
681
+ );
682
+ }
683
+ if (token.type === "conditional" && token.conditional) {
684
+ return /* @__PURE__ */ jsx2(
685
+ "span",
686
+ {
687
+ "data-conditional": token.conditional,
688
+ className: "inline-flex items-center px-1 py-0.5 mx-0.5 text-xs font-medium rounded-md border border-amber-400/40 bg-amber-400/15 text-amber-700 dark:text-amber-300",
689
+ title: token.conditional === "end" ? `End of section: ${token.value}` : token.conditional === "if" ? `Shown when "${token.value}" has a value` : `Shown when "${token.value}" is empty`,
690
+ children: `{${CONDITIONAL_PREFIX[token.conditional]}${token.value}}`
592
691
  },
593
692
  index
594
693
  );
@@ -608,6 +707,143 @@ var TemplateRenderer = ({
608
707
  }) });
609
708
  };
610
709
 
710
+ // src/components/VariableForm.tsx
711
+ import { useId, useMemo as useMemo4, useState as useState4 } from "react";
712
+ import {
713
+ compactVariableValues,
714
+ getVariableFormFields
715
+ } from "@pixerate/editor";
716
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
717
+ var VariableForm = ({
718
+ content,
719
+ templates,
720
+ bindings,
721
+ values,
722
+ defaultValues,
723
+ onChange,
724
+ className = "flex flex-col gap-3 text-sm"
725
+ }) => {
726
+ const idPrefix = useId();
727
+ const [internal, setInternal] = useState4(
728
+ () => defaultValues ?? {}
729
+ );
730
+ const entered = values ?? internal;
731
+ const fields = useMemo4(
732
+ () => getVariableFormFields(content, { templates, bindings }),
733
+ [content, templates, bindings]
734
+ );
735
+ const update = (name, value) => {
736
+ const next = { ...entered, [name]: value };
737
+ if (values === void 0) setInternal(next);
738
+ onChange?.(compactVariableValues(next), next);
739
+ };
740
+ if (fields.length === 0) return null;
741
+ return /* @__PURE__ */ jsx3("form", { className, onSubmit: (event) => event.preventDefault(), children: fields.map((field) => {
742
+ const id = `${idPrefix}-${field.name}`;
743
+ if (field.conditionOnly) {
744
+ return /* @__PURE__ */ jsxs2(
745
+ "label",
746
+ {
747
+ htmlFor: id,
748
+ className: "flex items-center gap-2",
749
+ "data-variable-field": field.name,
750
+ children: [
751
+ /* @__PURE__ */ jsx3(
752
+ "input",
753
+ {
754
+ id,
755
+ name: field.name,
756
+ type: "checkbox",
757
+ checked: (entered[field.name] ?? "") !== "",
758
+ onChange: (event) => update(field.name, event.target.checked ? "true" : "")
759
+ }
760
+ ),
761
+ /* @__PURE__ */ jsx3("span", { children: field.label })
762
+ ]
763
+ },
764
+ field.name
765
+ );
766
+ }
767
+ return /* @__PURE__ */ jsxs2(
768
+ "label",
769
+ {
770
+ htmlFor: id,
771
+ className: "flex flex-col gap-1",
772
+ "data-variable-field": field.name,
773
+ children: [
774
+ /* @__PURE__ */ jsxs2("span", { className: "font-medium", children: [
775
+ field.label,
776
+ field.required && /* @__PURE__ */ jsx3("span", { className: "text-rose-500", "aria-hidden": "true", children: " *" })
777
+ ] }),
778
+ /* @__PURE__ */ jsx3(
779
+ "input",
780
+ {
781
+ id,
782
+ name: field.name,
783
+ type: "text",
784
+ value: entered[field.name] ?? "",
785
+ placeholder: field.placeholder,
786
+ "aria-required": field.required || void 0,
787
+ onChange: (event) => update(field.name, event.target.value),
788
+ className: "px-2 py-1 rounded-md border border-violet-400/40 bg-transparent"
789
+ }
790
+ )
791
+ ]
792
+ },
793
+ field.name
794
+ );
795
+ }) });
796
+ };
797
+
798
+ // src/hooks/useOutline.ts
799
+ import { useMemo as useMemo5, useSyncExternalStore as useSyncExternalStore3 } from "react";
800
+ import { createOutlineStore } from "@pixerate/editor";
801
+ function useOutline(editor) {
802
+ const store = useMemo5(() => createOutlineStore(editor), [editor]);
803
+ return useSyncExternalStore3(store.subscribe, store.getSnapshot, store.getSnapshot);
804
+ }
805
+
806
+ // src/components/OutlinePanel.tsx
807
+ import { navigateToHeading } from "@pixerate/editor";
808
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
809
+ var OutlinePanel = ({
810
+ editor,
811
+ title = "Outline",
812
+ emptyState = "Add headings to see the outline.",
813
+ maxLevel = 6,
814
+ focusOnNavigate = true,
815
+ className = "flex flex-col gap-1 text-sm",
816
+ itemClassName = "w-full truncate rounded px-2 py-1 text-left hover:bg-black/5",
817
+ activeItemClassName = "font-semibold bg-black/5",
818
+ onNavigate
819
+ }) => {
820
+ const { headings, activeId } = useOutline(editor);
821
+ const visible = headings.filter((h) => h.level <= maxLevel);
822
+ return /* @__PURE__ */ jsxs3("nav", { className, "aria-label": "Document outline", "data-outline-panel": "", children: [
823
+ title != null && /* @__PURE__ */ jsx4("div", { className: "pixerate-outline-title", children: title }),
824
+ visible.length === 0 ? /* @__PURE__ */ jsx4("div", { className: "pixerate-outline-empty", children: emptyState }) : /* @__PURE__ */ jsx4("ul", { role: "list", style: { listStyle: "none", margin: 0, padding: 0 }, children: visible.map((heading) => {
825
+ const active = heading.id === activeId;
826
+ return /* @__PURE__ */ jsx4("li", { "data-level": heading.level, children: /* @__PURE__ */ jsx4(
827
+ "button",
828
+ {
829
+ type: "button",
830
+ className: active ? `${itemClassName} ${activeItemClassName}` : itemClassName,
831
+ style: { paddingLeft: `${0.5 + heading.depth * 0.75}rem` },
832
+ "aria-current": active ? "location" : void 0,
833
+ "data-heading-id": heading.id,
834
+ onClick: () => {
835
+ if (!editor) return;
836
+ if (navigateToHeading(editor, heading, { focus: focusOnNavigate })) {
837
+ onNavigate?.(heading);
838
+ }
839
+ },
840
+ children: heading.text || "Untitled"
841
+ }
842
+ ) }, heading.id);
843
+ }) })
844
+ ] });
845
+ };
846
+
611
847
  // src/index.ts
612
848
  import {
613
849
  DirtyTracker,
@@ -618,15 +854,16 @@ import {
618
854
  } from "@pixerate/editor";
619
855
 
620
856
  // src/spreadsheet/useSpreadsheetEditor.ts
621
- import { useState as useState4, useRef as useRef6, useCallback as useCallback4 } from "react";
857
+ import { useState as useState5, useRef as useRef7, useCallback as useCallback4, useMemo as useMemo6 } from "react";
622
858
  import {
623
- SpreadsheetController
859
+ SpreadsheetController,
860
+ bindSpreadsheetTableActions
624
861
  } from "@pixerate/editor";
625
862
  function useSpreadsheetEditor(options = {}) {
626
- const optionsRef = useRef6(options);
863
+ const optionsRef = useRef7(options);
627
864
  optionsRef.current = options;
628
- const settersRef = useRef6({});
629
- const [controller] = useState4(
865
+ const settersRef = useRef7({});
866
+ const [controller] = useState5(
630
867
  () => new SpreadsheetController({
631
868
  ...options,
632
869
  onDocumentChange: (updated) => {
@@ -643,11 +880,11 @@ function useSpreadsheetEditor(options = {}) {
643
880
  }
644
881
  })
645
882
  );
646
- const [doc, setDoc] = useState4(() => ({ ...controller.document }));
647
- const [activeCell, setActiveCell] = useState4(controller.activeCell);
648
- const [selectedRange, setSelectedRange] = useState4(controller.selectedRange);
649
- const [editingCell, setEditingCell] = useState4(null);
650
- const [draftValue, setDraftValue] = useState4("");
883
+ const [doc, setDoc] = useState5(() => ({ ...controller.document }));
884
+ const [activeCell, setActiveCell] = useState5(controller.activeCell);
885
+ const [selectedRange, setSelectedRange] = useState5(controller.selectedRange);
886
+ const [editingCell, setEditingCell] = useState5(null);
887
+ const [draftValue, setDraftValue] = useState5("");
651
888
  settersRef.current = { setDoc, setActiveCell, setSelectedRange };
652
889
  const setCellValue = useCallback4((rowId, colId, raw) => {
653
890
  controller.setCellValue(rowId, colId, raw);
@@ -728,6 +965,8 @@ function useSpreadsheetEditor(options = {}) {
728
965
  controller.redo();
729
966
  setDoc({ ...controller.document });
730
967
  }, [controller]);
968
+ const tableActions = useMemo6(() => bindSpreadsheetTableActions(controller), [controller]);
969
+ const viewModel = useMemo6(() => controller.getViewModel(), [controller, doc]);
731
970
  return {
732
971
  document: doc,
733
972
  activeCell,
@@ -771,17 +1010,363 @@ function useSpreadsheetEditor(options = {}) {
771
1010
  controller.importFromTsv(data);
772
1011
  setDoc({ ...controller.document });
773
1012
  },
1013
+ ...tableActions,
1014
+ viewModel,
774
1015
  undo,
775
1016
  redo
776
1017
  };
777
1018
  }
778
1019
 
779
1020
  // src/spreadsheet/SpreadsheetEditor.tsx
780
- import { useRef as useRef7, useState as useState5, useMemo as useMemo3, useEffect as useEffect5, useCallback as useCallback5 } from "react";
1021
+ import { useRef as useRef11, useState as useState8, useMemo as useMemo8, useEffect as useEffect10, useCallback as useCallback6 } from "react";
781
1022
  import {
782
- indexToColName
1023
+ indexToColName,
1024
+ getSpreadsheetMenuItems,
1025
+ normalizeRange as normalizeRange2,
1026
+ rangeContains
783
1027
  } from "@pixerate/editor";
784
- import { jsx as jsx3, jsxs } from "react/jsx-runtime";
1028
+
1029
+ // src/spreadsheet/SpreadsheetContextMenu.tsx
1030
+ import React4, { useEffect as useEffect7, useRef as useRef8 } from "react";
1031
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1032
+ var SpreadsheetContextMenu = ({ items, x, y, onClose }) => {
1033
+ const menuRef = useRef8(null);
1034
+ const onCloseRef = useRef8(onClose);
1035
+ onCloseRef.current = onClose;
1036
+ useEffect7(() => {
1037
+ const handlePointer = (e) => {
1038
+ if (!menuRef.current?.contains(e.target)) onCloseRef.current();
1039
+ };
1040
+ const handleKey = (e) => {
1041
+ if (e.key === "Escape") onCloseRef.current();
1042
+ };
1043
+ window.addEventListener("mousedown", handlePointer);
1044
+ window.addEventListener("keydown", handleKey);
1045
+ return () => {
1046
+ window.removeEventListener("mousedown", handlePointer);
1047
+ window.removeEventListener("keydown", handleKey);
1048
+ };
1049
+ }, []);
1050
+ return /* @__PURE__ */ jsx5(
1051
+ "div",
1052
+ {
1053
+ ref: menuRef,
1054
+ role: "menu",
1055
+ "aria-label": "Table actions",
1056
+ className: "fixed z-50 min-w-[14rem] rounded-md border border-border bg-popover py-1 text-xs text-popover-foreground shadow-lg",
1057
+ style: { left: x, top: y, background: "var(--popover, var(--background, #fff))" },
1058
+ onContextMenu: (e) => e.preventDefault(),
1059
+ onKeyDown: (e) => e.stopPropagation(),
1060
+ children: items.map((item) => /* @__PURE__ */ jsxs4(React4.Fragment, { children: [
1061
+ item.separatorBefore && /* @__PURE__ */ jsx5("div", { role: "separator", className: "my-1 h-px bg-border" }),
1062
+ /* @__PURE__ */ jsx5(
1063
+ "button",
1064
+ {
1065
+ type: "button",
1066
+ role: "menuitem",
1067
+ "data-action": item.id,
1068
+ disabled: item.disabled,
1069
+ className: "block w-full px-3 py-1 text-left hover:bg-muted disabled:cursor-default disabled:opacity-40 disabled:hover:bg-transparent",
1070
+ onClick: () => {
1071
+ onClose();
1072
+ item.run();
1073
+ },
1074
+ children: item.label
1075
+ }
1076
+ )
1077
+ ] }, item.id))
1078
+ }
1079
+ );
1080
+ };
1081
+
1082
+ // src/spreadsheet/SpreadsheetGroupRow.tsx
1083
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1084
+ var formatSummary = (value) => value === null ? "\u2013" : String(value);
1085
+ var SpreadsheetGroupRow = ({
1086
+ group,
1087
+ columns,
1088
+ groupColId,
1089
+ onToggle,
1090
+ trailingCell
1091
+ }) => {
1092
+ const labelColId = columns.some((c) => c.column.id === groupColId) ? groupColId : columns[0]?.column.id;
1093
+ return /* @__PURE__ */ jsxs5("tr", { className: "border-b border-border bg-muted/40 font-semibold", "data-group-key": group.key, style: { height: 28 }, children: [
1094
+ /* @__PURE__ */ jsx6("td", { className: "w-12 min-w-[3rem] sticky left-0 z-10 bg-muted/60 border-r border-border text-center", children: /* @__PURE__ */ jsx6(
1095
+ "button",
1096
+ {
1097
+ type: "button",
1098
+ "aria-label": `${group.collapsed ? "Expand" : "Collapse"} group ${group.label}`,
1099
+ "aria-expanded": !group.collapsed,
1100
+ className: "h-5 w-5 rounded text-[10px] text-muted-foreground hover:bg-muted",
1101
+ onClick: (e) => {
1102
+ e.stopPropagation();
1103
+ onToggle(group.key);
1104
+ },
1105
+ children: group.collapsed ? "\u25B8" : "\u25BE"
1106
+ }
1107
+ ) }),
1108
+ columns.map(({ column }) => {
1109
+ const summary = group.summaries[column.id];
1110
+ return /* @__PURE__ */ jsx6("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__ */ jsxs5("span", { className: "truncate", children: [
1111
+ group.label,
1112
+ " ",
1113
+ /* @__PURE__ */ jsxs5("span", { className: "font-normal text-muted-foreground", children: [
1114
+ "(",
1115
+ group.rowIndices.length,
1116
+ ")"
1117
+ ] })
1118
+ ] }) : summary !== void 0 ? /* @__PURE__ */ jsx6("span", { className: "truncate", "data-summary": column.id, children: formatSummary(summary) }) : null }, column.id);
1119
+ }),
1120
+ trailingCell && /* @__PURE__ */ jsx6("td", { className: "w-10 min-w-[2.5rem] bg-muted/10 border-r border-border" })
1121
+ ] });
1122
+ };
1123
+
1124
+ // src/spreadsheet/useSpreadsheetTableInteractions.ts
1125
+ import { useCallback as useCallback5, useEffect as useEffect8, useRef as useRef9, useState as useState6 } from "react";
1126
+ import {
1127
+ computeFillTarget,
1128
+ normalizeRange
1129
+ } from "@pixerate/editor";
1130
+ function gridHasFocus(grid) {
1131
+ if (!grid || typeof document === "undefined") return false;
1132
+ const active = document.activeElement;
1133
+ if (!active || !grid.contains(active)) return false;
1134
+ return !(active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement);
1135
+ }
1136
+ function useSpreadsheetClipboard(gridRef, state, readOnly) {
1137
+ const stateRef = useRef9(state);
1138
+ stateRef.current = state;
1139
+ const readOnlyRef = useRef9(readOnly);
1140
+ readOnlyRef.current = readOnly;
1141
+ const copiedRef = useRef9(false);
1142
+ useEffect8(() => {
1143
+ if (typeof document === "undefined") return;
1144
+ const handleCopy = (e) => {
1145
+ const s = stateRef.current;
1146
+ if (!gridHasFocus(gridRef.current) || s.editingCell || !e.clipboardData || !s.selectedRange) return;
1147
+ for (const [type, data] of Object.entries(s.copyToClipboardData(s.selectedRange))) {
1148
+ e.clipboardData.setData(type, data);
1149
+ }
1150
+ copiedRef.current = true;
1151
+ e.preventDefault();
1152
+ };
1153
+ const handlePaste = (e) => {
1154
+ const s = stateRef.current;
1155
+ if (readOnlyRef.current || !gridHasFocus(gridRef.current) || s.editingCell || !e.clipboardData) return;
1156
+ if (s.pasteClipboardData(e.clipboardData)) e.preventDefault();
1157
+ };
1158
+ document.addEventListener("copy", handleCopy);
1159
+ document.addEventListener("paste", handlePaste);
1160
+ return () => {
1161
+ document.removeEventListener("copy", handleCopy);
1162
+ document.removeEventListener("paste", handlePaste);
1163
+ };
1164
+ }, [gridRef]);
1165
+ const copyShortcutFallback = useCallback5(() => {
1166
+ copiedRef.current = false;
1167
+ setTimeout(() => {
1168
+ if (copiedRef.current) return;
1169
+ const s = stateRef.current;
1170
+ navigator.clipboard?.writeText(s.exportToTsv(s.selectedRange));
1171
+ }, 0);
1172
+ }, []);
1173
+ return { copyShortcutFallback };
1174
+ }
1175
+ function useFillHandle(state) {
1176
+ const [drag, setDrag] = useState6(null);
1177
+ const dragRef = useRef9(drag);
1178
+ dragRef.current = drag;
1179
+ const stateRef = useRef9(state);
1180
+ stateRef.current = state;
1181
+ const dragging = drag !== null;
1182
+ useEffect8(() => {
1183
+ if (!dragging) return;
1184
+ const handleMouseUp = () => {
1185
+ const current = dragRef.current;
1186
+ setDrag(null);
1187
+ if (current?.target) stateRef.current.autofill(current.source, current.target);
1188
+ };
1189
+ window.addEventListener("mouseup", handleMouseUp);
1190
+ return () => window.removeEventListener("mouseup", handleMouseUp);
1191
+ }, [dragging]);
1192
+ const startFill = useCallback5((e) => {
1193
+ e.preventDefault();
1194
+ e.stopPropagation();
1195
+ const range = stateRef.current.selectedRange;
1196
+ if (range) setDrag({ source: normalizeRange(range), target: null });
1197
+ }, []);
1198
+ const extendFill = useCallback5((row, col) => {
1199
+ const current = dragRef.current;
1200
+ if (!current) return;
1201
+ setDrag({ source: current.source, target: computeFillTarget(current.source, { row, col }) });
1202
+ }, []);
1203
+ return { fillTarget: drag?.target ?? null, isFilling: dragging, startFill, extendFill };
1204
+ }
1205
+
1206
+ // src/spreadsheet/FormulaHints.tsx
1207
+ import { useEffect as useEffect9, useLayoutEffect, useMemo as useMemo7, useRef as useRef10, useState as useState7 } from "react";
1208
+ import { applyFormulaSuggestion, getFormulaHints } from "@pixerate/editor";
1209
+ import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1210
+ var FormulaHints = ({ inputRef, value, onAccept, limit = 8 }) => {
1211
+ const [caret, setCaret] = useState7(null);
1212
+ const [focused, setFocused] = useState7(false);
1213
+ const [selected, setSelected] = useState7(0);
1214
+ const [dismissed, setDismissed] = useState7(false);
1215
+ const [position, setPosition] = useState7(null);
1216
+ const pendingCaret = useRef10(null);
1217
+ const hints = useMemo7(
1218
+ () => getFormulaHints(value, caret ?? value.length, { limit }),
1219
+ [value, caret, limit]
1220
+ );
1221
+ const suggestions = dismissed ? [] : hints.suggestions;
1222
+ const selectedIndex = Math.max(0, Math.min(selected, suggestions.length - 1));
1223
+ const visible = focused && value.trimStart().startsWith("=") && (suggestions.length > 0 || !!hints.activeCall);
1224
+ const latest = useRef10({ suggestions, selected: selectedIndex, value, caret, onAccept });
1225
+ latest.current = { suggestions, selected: selectedIndex, value, caret, onAccept };
1226
+ const accept = (index) => {
1227
+ const { suggestions: list, value: current, caret: at, onAccept: commit } = latest.current;
1228
+ const suggestion = list[index];
1229
+ if (!suggestion) return;
1230
+ const next = applyFormulaSuggestion(current, at ?? current.length, suggestion.name);
1231
+ pendingCaret.current = next.caret;
1232
+ commit(next.value);
1233
+ };
1234
+ useEffect9(() => {
1235
+ const input = inputRef.current;
1236
+ if (!input) return;
1237
+ const syncCaret = () => setCaret(input.selectionStart ?? input.value.length);
1238
+ const onInput = () => {
1239
+ setDismissed(false);
1240
+ setSelected(0);
1241
+ syncCaret();
1242
+ };
1243
+ const onFocus = () => {
1244
+ setFocused(true);
1245
+ syncCaret();
1246
+ };
1247
+ const onBlur = () => setFocused(false);
1248
+ const onKeyDown = (e) => {
1249
+ const { suggestions: list } = latest.current;
1250
+ if (list.length === 0) return;
1251
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
1252
+ e.preventDefault();
1253
+ e.stopPropagation();
1254
+ const step = e.key === "ArrowDown" ? 1 : -1;
1255
+ setSelected((i) => (i + step + list.length) % list.length);
1256
+ } else if (e.key === "Tab" && !e.shiftKey) {
1257
+ e.preventDefault();
1258
+ e.stopPropagation();
1259
+ accept(latest.current.selected);
1260
+ } else if (e.key === "Escape") {
1261
+ e.preventDefault();
1262
+ e.stopPropagation();
1263
+ setDismissed(true);
1264
+ }
1265
+ };
1266
+ input.addEventListener("input", onInput);
1267
+ input.addEventListener("keyup", syncCaret);
1268
+ input.addEventListener("click", syncCaret);
1269
+ input.addEventListener("select", syncCaret);
1270
+ input.addEventListener("focus", onFocus);
1271
+ input.addEventListener("blur", onBlur);
1272
+ input.addEventListener("keydown", onKeyDown);
1273
+ if (typeof document !== "undefined" && document.activeElement === input) onFocus();
1274
+ return () => {
1275
+ input.removeEventListener("input", onInput);
1276
+ input.removeEventListener("keyup", syncCaret);
1277
+ input.removeEventListener("click", syncCaret);
1278
+ input.removeEventListener("select", syncCaret);
1279
+ input.removeEventListener("focus", onFocus);
1280
+ input.removeEventListener("blur", onBlur);
1281
+ input.removeEventListener("keydown", onKeyDown);
1282
+ };
1283
+ }, [inputRef]);
1284
+ useLayoutEffect(() => {
1285
+ const input = inputRef.current;
1286
+ const at = pendingCaret.current;
1287
+ if (!input || at === null || input.value !== value) return;
1288
+ pendingCaret.current = null;
1289
+ input.setSelectionRange(at, at);
1290
+ setCaret(at);
1291
+ setSelected(0);
1292
+ }, [value, inputRef]);
1293
+ useLayoutEffect(() => {
1294
+ const input = inputRef.current;
1295
+ if (!visible || !input) return;
1296
+ const rect = input.getBoundingClientRect();
1297
+ setPosition({ top: rect.bottom + 2, left: rect.left, width: rect.width });
1298
+ }, [visible, value, inputRef]);
1299
+ if (!visible) return null;
1300
+ const { activeCall } = hints;
1301
+ return /* @__PURE__ */ jsxs6(
1302
+ "div",
1303
+ {
1304
+ "data-formula-hints": "",
1305
+ className: "rounded-md border border-border bg-popover text-popover-foreground shadow-md text-xs font-sans overflow-hidden",
1306
+ style: {
1307
+ position: "fixed",
1308
+ top: position?.top ?? 0,
1309
+ left: position?.left ?? 0,
1310
+ minWidth: Math.max(260, position?.width ?? 0),
1311
+ maxWidth: 440,
1312
+ zIndex: 50,
1313
+ visibility: position ? "visible" : "hidden"
1314
+ },
1315
+ onMouseDown: (e) => e.preventDefault(),
1316
+ children: [
1317
+ activeCall && /* @__PURE__ */ jsxs6("div", { "data-formula-signature": "", className: "px-2 py-1.5 border-b border-border bg-muted/30", children: [
1318
+ /* @__PURE__ */ jsx7("div", { className: "font-mono", children: activeCall.parts.map((part, i) => /* @__PURE__ */ jsx7(
1319
+ "span",
1320
+ {
1321
+ "data-active": part.active ? "" : void 0,
1322
+ className: part.active ? "font-bold text-primary underline" : "",
1323
+ children: part.text
1324
+ },
1325
+ i
1326
+ )) }),
1327
+ /* @__PURE__ */ jsx7("div", { className: "mt-0.5 text-muted-foreground", children: activeCall.argument ? /* @__PURE__ */ jsxs6(Fragment, { children: [
1328
+ /* @__PURE__ */ jsx7("span", { className: "font-semibold", children: activeCall.argument.name }),
1329
+ ": ",
1330
+ activeCall.argument.description
1331
+ ] }) : activeCall.description })
1332
+ ] }),
1333
+ suggestions.length > 0 && /* @__PURE__ */ jsx7("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__ */ jsxs6(
1334
+ "li",
1335
+ {
1336
+ role: "option",
1337
+ "aria-selected": i === selectedIndex,
1338
+ className: `cursor-pointer px-2 py-1 ${i === selectedIndex ? "bg-primary/10" : "hover:bg-muted/50"}`,
1339
+ onMouseEnter: () => setSelected(i),
1340
+ onMouseDown: (e) => {
1341
+ e.preventDefault();
1342
+ accept(i);
1343
+ },
1344
+ children: [
1345
+ /* @__PURE__ */ jsx7("div", { className: "font-mono font-semibold text-foreground", children: s.signature }),
1346
+ /* @__PURE__ */ jsx7("div", { className: "text-muted-foreground truncate", children: s.description })
1347
+ ]
1348
+ },
1349
+ s.name
1350
+ )) })
1351
+ ]
1352
+ }
1353
+ );
1354
+ };
1355
+
1356
+ // src/spreadsheet/SpreadsheetEditor.tsx
1357
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1358
+ var COLUMN_HEADER_HEIGHT = 28;
1359
+ var ROW_HEADER_WIDTH = 48;
1360
+ function frozenCellStyle(viewRow, viewCol) {
1361
+ if (!viewRow.frozen && !viewCol.frozen) return void 0;
1362
+ return {
1363
+ position: "sticky",
1364
+ ...viewRow.frozen ? { top: COLUMN_HEADER_HEIGHT + viewRow.stickyTop } : {},
1365
+ ...viewCol.frozen ? { left: ROW_HEADER_WIDTH + viewCol.stickyLeft } : {},
1366
+ zIndex: viewRow.frozen && viewCol.frozen ? 14 : viewRow.frozen ? 12 : 11,
1367
+ background: "var(--background, #fff)"
1368
+ };
1369
+ }
785
1370
  var SpreadsheetEditor = ({
786
1371
  state: externalState,
787
1372
  readOnly = false,
@@ -792,27 +1377,47 @@ var SpreadsheetEditor = ({
792
1377
  }) => {
793
1378
  const defaultState = useSpreadsheetEditor();
794
1379
  const state = externalState || defaultState;
795
- const gridRef = useRef7(null);
796
- const [resizingColId, setResizingColId] = useState5(null);
797
- const [hoveredColId, setHoveredColId] = useState5(null);
798
- const [resizeStartX, setResizeStartX] = useState5(0);
799
- const [resizeStartWidth, setResizeStartWidth] = useState5(0);
800
- const activeRawValue = useMemo3(() => {
1380
+ const gridRef = useRef11(null);
1381
+ const formulaBarRef = useRef11(null);
1382
+ const cellInputRef = useRef11(null);
1383
+ const [resizingColId, setResizingColId] = useState8(null);
1384
+ const [hoveredColId, setHoveredColId] = useState8(null);
1385
+ const [resizeStartX, setResizeStartX] = useState8(0);
1386
+ const [resizeStartWidth, setResizeStartWidth] = useState8(0);
1387
+ const view = state.viewModel;
1388
+ const { copyShortcutFallback } = useSpreadsheetClipboard(gridRef, state, readOnly);
1389
+ const { fillTarget, startFill, extendFill } = useFillHandle(state);
1390
+ const [menu, setMenu] = useState8(null);
1391
+ const filter = state.document.table?.filter;
1392
+ const filteredColIds = useMemo8(
1393
+ () => new Set(filter?.enabled ? filter.rules.map((r) => r.colId) : []),
1394
+ [filter]
1395
+ );
1396
+ const handleCellContextMenu = useCallback6((rIdx, cIdx, e) => {
1397
+ if (readOnly) return;
1398
+ e.preventDefault();
1399
+ if (state.editingCell) state.commitEditing();
1400
+ if (!state.selectedRange || !rangeContains(state.selectedRange, rIdx, cIdx)) {
1401
+ state.selectCell(rIdx, cIdx);
1402
+ }
1403
+ setMenu({ x: e.clientX, y: e.clientY, items: getSpreadsheetMenuItems(state.controller, { row: rIdx, col: cIdx }) });
1404
+ }, [readOnly, state]);
1405
+ const activeRawValue = useMemo8(() => {
801
1406
  if (!state.activeCell) return "";
802
1407
  const row = state.document.rows[state.activeCell.row];
803
1408
  const col = state.document.columns[state.activeCell.col];
804
1409
  if (!row || !col) return "";
805
1410
  return state.getCell(row.id, col.id).raw;
806
1411
  }, [state.activeCell, state.document]);
807
- const coordLabel = useMemo3(() => {
1412
+ const coordLabel = useMemo8(() => {
808
1413
  if (!state.activeCell) return "--";
809
1414
  const colName = indexToColName(state.activeCell.col);
810
1415
  return `${colName}${state.activeCell.row + 1}`;
811
1416
  }, [state.activeCell]);
812
- const [formulaDraft, setFormulaDraft] = useState5(null);
1417
+ const [formulaDraft, setFormulaDraft] = useState8(null);
813
1418
  const activeColumn = state.activeCell ? state.document.columns[state.activeCell.col] : void 0;
814
1419
  const formulaBarReadOnly = readOnly || !state.activeCell || !!activeColumn?.readOnly;
815
- useEffect5(() => {
1420
+ useEffect10(() => {
816
1421
  setFormulaDraft(null);
817
1422
  }, [state.activeCell?.row, state.activeCell?.col]);
818
1423
  const commitFormulaBar = () => {
@@ -828,19 +1433,19 @@ var SpreadsheetEditor = ({
828
1433
  }
829
1434
  setFormulaDraft(null);
830
1435
  };
831
- const handleCellClick = useCallback5((rIdx, cIdx, e) => {
1436
+ const handleCellClick = useCallback6((rIdx, cIdx, e) => {
832
1437
  if (state.editingCell && (state.editingCell.row !== rIdx || state.editingCell.col !== cIdx)) {
833
1438
  state.commitEditing();
834
1439
  }
835
1440
  state.selectCell(rIdx, cIdx, e.shiftKey);
836
1441
  }, [state]);
837
- const handleCellDblClick = useCallback5((rIdx, cIdx) => {
1442
+ const handleCellDblClick = useCallback6((rIdx, cIdx) => {
838
1443
  if (readOnly) return;
839
1444
  const col = state.document.columns[cIdx];
840
1445
  if (col?.readOnly) return;
841
1446
  state.startEditing(rIdx, cIdx);
842
1447
  }, [readOnly, state]);
843
- const handleKeyDown = useCallback5((e) => {
1448
+ const handleKeyDown = useCallback6((e) => {
844
1449
  if (state.editingCell) {
845
1450
  if (e.key === "Enter") {
846
1451
  e.preventDefault();
@@ -897,21 +1502,19 @@ var SpreadsheetEditor = ({
897
1502
  e.preventDefault();
898
1503
  state.redo();
899
1504
  } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "c") {
900
- e.preventDefault();
901
- const tsv = state.exportToTsv(state.selectedRange);
902
- navigator.clipboard?.writeText(tsv);
1505
+ copyShortcutFallback();
903
1506
  } else if (!readOnly && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
904
1507
  const col = state.document.columns[state.activeCell.col];
905
1508
  if (!col?.readOnly) {
906
1509
  state.startEditing(state.activeCell.row, state.activeCell.col, e.key);
907
1510
  }
908
1511
  }
909
- }, [readOnly, state]);
910
- const stateRef = useRef7(state);
1512
+ }, [readOnly, state, copyShortcutFallback]);
1513
+ const stateRef = useRef11(state);
911
1514
  stateRef.current = state;
912
- const onColumnResizeRef = useRef7(onColumnResize);
1515
+ const onColumnResizeRef = useRef11(onColumnResize);
913
1516
  onColumnResizeRef.current = onColumnResize;
914
- useEffect5(() => {
1517
+ useEffect10(() => {
915
1518
  if (!resizingColId) return;
916
1519
  if (typeof document !== "undefined") {
917
1520
  document.body.style.cursor = "col-resize";
@@ -962,7 +1565,7 @@ var SpreadsheetEditor = ({
962
1565
  window.removeEventListener("mouseup", handleMouseUp);
963
1566
  };
964
1567
  }, [resizingColId, resizeStartX, resizeStartWidth]);
965
- const measureElementContentWidth = useCallback5((el, container) => {
1568
+ const measureElementContentWidth = useCallback6((el, container) => {
966
1569
  if (typeof document === "undefined") return 0;
967
1570
  let maxW = el.scrollWidth || 0;
968
1571
  const firstChild = el.firstElementChild;
@@ -1010,15 +1613,14 @@ var SpreadsheetEditor = ({
1010
1613
  }
1011
1614
  return maxW;
1012
1615
  }, []);
1013
- const autoFitColumn = useCallback5((colId, colIndex) => {
1616
+ const autoFitColumn = useCallback6((colId, colIndex) => {
1014
1617
  const col = state.document.columns.find((c) => c.id === colId);
1015
1618
  if (!col) return;
1016
1619
  let calculatedWidth = 0;
1017
1620
  if (gridRef.current && typeof document !== "undefined") {
1018
1621
  const table = gridRef.current.querySelector("table");
1019
1622
  if (table) {
1020
- const thElements = table.querySelectorAll("thead th");
1021
- const th = thElements[colIndex + 1];
1623
+ const th = table.querySelector(`thead th[data-col="${colIndex}"]`);
1022
1624
  if (th) {
1023
1625
  const headerInner = th.firstElementChild || th;
1024
1626
  const headerW = measureElementContentWidth(headerInner, gridRef.current);
@@ -1028,7 +1630,7 @@ var SpreadsheetEditor = ({
1028
1630
  const sampleLimit = Math.min(rows.length, 150);
1029
1631
  for (let r = 0; r < sampleLimit; r++) {
1030
1632
  const tr = rows[r];
1031
- const td = tr.children[colIndex + 1];
1633
+ const td = tr.querySelector(`td[data-col="${colIndex}"][colspan="1"]`);
1032
1634
  if (td) {
1033
1635
  const cellW = measureElementContentWidth(td, gridRef.current);
1034
1636
  calculatedWidth = Math.max(calculatedWidth, cellW + 20);
@@ -1047,12 +1649,12 @@ var SpreadsheetEditor = ({
1047
1649
  onColumnResize(updatedCol.id, updatedCol.width || 130);
1048
1650
  }
1049
1651
  }, [state, measureElementContentWidth, onColumnResize]);
1050
- const handleResizeDblClick = useCallback5((colId, colIndex, e) => {
1652
+ const handleResizeDblClick = useCallback6((colId, colIndex, e) => {
1051
1653
  e.preventDefault();
1052
1654
  e.stopPropagation();
1053
1655
  autoFitColumn(colId, colIndex);
1054
1656
  }, [autoFitColumn]);
1055
- const handleHeaderDblClick = useCallback5((colId, colIndex, e) => {
1657
+ const handleHeaderDblClick = useCallback6((colId, colIndex, e) => {
1056
1658
  const target = e.currentTarget;
1057
1659
  if (target) {
1058
1660
  const rect = target.getBoundingClientRect();
@@ -1074,7 +1676,7 @@ var SpreadsheetEditor = ({
1074
1676
  const isActiveCell = (rIdx, cIdx) => {
1075
1677
  return state.activeCell?.row === rIdx && state.activeCell?.col === cIdx;
1076
1678
  };
1077
- return /* @__PURE__ */ jsxs(
1679
+ return /* @__PURE__ */ jsxs7(
1078
1680
  "div",
1079
1681
  {
1080
1682
  ref: gridRef,
@@ -1084,12 +1686,13 @@ var SpreadsheetEditor = ({
1084
1686
  role: "grid",
1085
1687
  "aria-label": "Spreadsheet",
1086
1688
  children: [
1087
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-border bg-muted/20 px-3 py-1.5 text-xs font-mono", children: [
1088
- /* @__PURE__ */ jsx3("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 }),
1089
- /* @__PURE__ */ jsx3("div", { className: "flex items-center text-muted-foreground select-none", children: /* @__PURE__ */ jsx3("span", { className: "italic font-serif font-bold text-sm text-primary", children: "fx" }) }),
1090
- /* @__PURE__ */ jsx3(
1689
+ /* @__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: [
1690
+ /* @__PURE__ */ jsx8("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 }),
1691
+ /* @__PURE__ */ jsx8("div", { className: "flex items-center text-muted-foreground select-none", children: /* @__PURE__ */ jsx8("span", { className: "italic font-serif font-bold text-sm text-primary", children: "fx" }) }),
1692
+ /* @__PURE__ */ jsx8(
1091
1693
  "input",
1092
1694
  {
1695
+ ref: formulaBarRef,
1093
1696
  type: "text",
1094
1697
  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",
1095
1698
  placeholder: "Enter a value or formula (e.g. =SUM(A1:A5))",
@@ -1119,28 +1722,44 @@ var SpreadsheetEditor = ({
1119
1722
  }
1120
1723
  }
1121
1724
  }
1725
+ ),
1726
+ !formulaBarReadOnly && /* @__PURE__ */ jsx8(
1727
+ FormulaHints,
1728
+ {
1729
+ inputRef: formulaBarRef,
1730
+ value: state.editingCell ? state.draftValue : formulaDraft ?? activeRawValue,
1731
+ onAccept: (next) => state.editingCell ? state.setDraftValue(next) : setFormulaDraft(next)
1732
+ }
1122
1733
  )
1123
1734
  ] }),
1124
- /* @__PURE__ */ jsx3("div", { className: "relative flex-1 overflow-auto", children: /* @__PURE__ */ jsxs("table", { className: "border-collapse table-fixed w-max min-w-full", children: [
1125
- /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "h-7 bg-muted/60 sticky top-0 z-20 border-b border-border shadow-xs", children: [
1126
- /* @__PURE__ */ jsx3("th", { className: "w-12 min-w-[3rem] sticky left-0 z-30 bg-muted/90 border-r border-border text-center text-[10px] text-muted-foreground font-medium", children: "#" }),
1127
- state.document.columns.map((col, cIdx) => /* @__PURE__ */ jsxs(
1735
+ /* @__PURE__ */ jsx8("div", { className: "relative flex-1 overflow-auto", children: /* @__PURE__ */ jsxs7("table", { className: "border-collapse table-fixed w-max min-w-full", children: [
1736
+ /* @__PURE__ */ jsx8("thead", { children: /* @__PURE__ */ jsxs7("tr", { className: "h-7 bg-muted/60 sticky top-0 z-20 border-b border-border shadow-xs", children: [
1737
+ /* @__PURE__ */ jsx8("th", { className: "w-12 min-w-[3rem] sticky left-0 z-30 bg-muted/90 border-r border-border text-center text-[10px] text-muted-foreground font-medium", children: "#" }),
1738
+ view.columns.map(({ column: col, index: cIdx, frozen, stickyLeft }) => /* @__PURE__ */ jsxs7(
1128
1739
  "th",
1129
1740
  {
1741
+ "data-col": cIdx,
1130
1742
  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"}`,
1131
- style: { width: col.width || 130, minWidth: col.width || 130, maxWidth: col.width || 130, overflow: "hidden" },
1743
+ style: {
1744
+ width: col.width || 130,
1745
+ minWidth: col.width || 130,
1746
+ maxWidth: col.width || 130,
1747
+ overflow: "hidden",
1748
+ ...frozen ? { position: "sticky", left: ROW_HEADER_WIDTH + stickyLeft, zIndex: 25, background: "var(--muted, #f4f4f5)" } : {}
1749
+ },
1132
1750
  onClick: () => onColumnHeaderClick?.(col),
1133
1751
  onDoubleClick: (e) => handleHeaderDblClick(col.id, cIdx, e),
1134
1752
  children: [
1135
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-1 min-w-0 overflow-hidden truncate", children: [
1136
- /* @__PURE__ */ jsx3("span", { className: "truncate font-semibold text-foreground text-xs min-w-0", children: col.title }),
1137
- /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground/70 font-mono font-normal shrink-0", children: [
1753
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between gap-1 min-w-0 overflow-hidden truncate", children: [
1754
+ /* @__PURE__ */ jsx8("span", { className: "truncate font-semibold text-foreground text-xs min-w-0", children: col.title }),
1755
+ filteredColIds.has(col.id) && /* @__PURE__ */ jsx8("span", { className: "text-[10px] text-primary shrink-0", title: "Filtered", "data-filtered": "true", children: "\u23F7" }),
1756
+ /* @__PURE__ */ jsxs7("span", { className: "text-[10px] text-muted-foreground/70 font-mono font-normal shrink-0", children: [
1138
1757
  "(",
1139
1758
  col.key,
1140
1759
  ")"
1141
1760
  ] })
1142
1761
  ] }),
1143
- /* @__PURE__ */ jsx3(
1762
+ /* @__PURE__ */ jsx8(
1144
1763
  "div",
1145
1764
  {
1146
1765
  className: "col-resize-handle",
@@ -1157,7 +1776,7 @@ var SpreadsheetEditor = ({
1157
1776
  onDoubleClick: (e) => handleResizeDblClick(col.id, cIdx, e),
1158
1777
  role: "separator",
1159
1778
  "aria-orientation": "vertical",
1160
- children: /* @__PURE__ */ jsx3(
1779
+ children: /* @__PURE__ */ jsx8(
1161
1780
  "div",
1162
1781
  {
1163
1782
  className: "col-resize-handle-line",
@@ -1176,49 +1795,136 @@ var SpreadsheetEditor = ({
1176
1795
  },
1177
1796
  col.id
1178
1797
  )),
1179
- onAddColumnClick && /* @__PURE__ */ jsx3(
1798
+ onAddColumnClick && /* @__PURE__ */ jsx8(
1180
1799
  "th",
1181
1800
  {
1182
1801
  className: "w-10 min-w-[2.5rem] px-1 text-center bg-muted/40 hover:bg-muted cursor-pointer transition-colors",
1183
1802
  onClick: onAddColumnClick,
1184
1803
  title: "Add Column",
1185
- children: /* @__PURE__ */ jsx3("span", { className: "text-xs font-bold text-muted-foreground", children: "+" })
1804
+ children: /* @__PURE__ */ jsx8("span", { className: "text-xs font-bold text-muted-foreground", children: "+" })
1186
1805
  }
1187
1806
  )
1188
1807
  ] }) }),
1189
- /* @__PURE__ */ jsx3("tbody", { children: state.document.rows.map((row, rIdx) => /* @__PURE__ */ jsxs("tr", { className: "border-b border-border hover:bg-muted/10 transition-colors", style: { height: row.height || 32 }, children: [
1190
- /* @__PURE__ */ jsx3("td", { className: "w-12 min-w-[3rem] sticky left-0 z-10 bg-muted/50 border-r border-border text-center text-[10px] text-muted-foreground font-mono select-none", children: rIdx + 1 }),
1191
- state.document.columns.map((col, cIdx) => {
1192
- const cell = state.getCell(row.id, col.id);
1193
- const selected = isCellSelected(rIdx, cIdx);
1194
- const active = isActiveCell(rIdx, cIdx);
1195
- const isEditing = state.editingCell?.row === rIdx && state.editingCell?.col === cIdx;
1196
- return /* @__PURE__ */ jsx3(
1197
- "td",
1808
+ /* @__PURE__ */ jsx8("tbody", { children: view.rows.map((viewRow) => {
1809
+ if (viewRow.kind === "group") {
1810
+ return /* @__PURE__ */ jsx8(
1811
+ SpreadsheetGroupRow,
1198
1812
  {
1199
- 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" : ""}`,
1200
- style: { maxWidth: 0, overflow: "hidden" },
1201
- onClick: (e) => handleCellClick(rIdx, cIdx, e),
1202
- onDoubleClick: () => handleCellDblClick(rIdx, cIdx),
1203
- children: isEditing ? /* @__PURE__ */ jsx3(
1204
- "input",
1205
- {
1206
- type: "text",
1207
- 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",
1208
- value: state.draftValue,
1209
- onChange: (e) => state.setDraftValue(e.target.value),
1210
- onBlur: () => state.commitEditing(),
1211
- autoFocus: true
1212
- }
1213
- ) : customCellRenderer ? customCellRenderer({ row, col, cell }) : /* @__PURE__ */ jsx3("span", { className: `truncate block max-w-full ${cell.error ? "text-destructive font-semibold" : ""}`, children: cell.error || (cell.value ?? "") })
1813
+ group: viewRow.group,
1814
+ columns: view.columns,
1815
+ groupColId: state.document.table?.grouping?.colId ?? "",
1816
+ onToggle: state.toggleGroupCollapsed,
1817
+ trailingCell: !!onAddColumnClick
1214
1818
  },
1215
- col.id
1819
+ `group:${viewRow.group.key}`
1216
1820
  );
1217
- }),
1218
- onAddColumnClick && /* @__PURE__ */ jsx3("td", { className: "w-10 min-w-[2.5rem] bg-muted/10 border-r border-border" })
1219
- ] }, row.id)) })
1821
+ }
1822
+ const displayRowIndex = viewRow.index;
1823
+ const selection = state.selectedRange ? normalizeRange2(state.selectedRange) : null;
1824
+ return /* @__PURE__ */ jsxs7(
1825
+ "tr",
1826
+ {
1827
+ "data-row": displayRowIndex,
1828
+ className: `border-b border-border hover:bg-muted/10 transition-colors ${viewRow.section !== "body" ? "bg-muted/30 font-semibold" : ""}`,
1829
+ style: { height: viewRow.row.height || 32 },
1830
+ children: [
1831
+ /* @__PURE__ */ jsx8(
1832
+ "td",
1833
+ {
1834
+ className: "w-12 min-w-[3rem] sticky left-0 z-10 bg-muted/50 border-r border-border text-center text-[10px] text-muted-foreground font-mono select-none",
1835
+ style: viewRow.frozen ? { top: COLUMN_HEADER_HEIGHT + viewRow.stickyTop, zIndex: 15 } : void 0,
1836
+ children: displayRowIndex + 1
1837
+ }
1838
+ ),
1839
+ view.columns.map((viewCol) => {
1840
+ const span = view.getCellSpan(displayRowIndex, viewCol.index);
1841
+ if (!span) return null;
1842
+ const rIdx = span.source.row;
1843
+ const cIdx = span.source.col;
1844
+ const row = state.document.rows[rIdx];
1845
+ const col = state.document.columns[cIdx];
1846
+ const cell = state.getCell(row.id, col.id);
1847
+ const selected = isCellSelected(rIdx, cIdx);
1848
+ const active = isActiveCell(rIdx, cIdx);
1849
+ const isEditing = state.editingCell?.row === rIdx && state.editingCell?.col === cIdx;
1850
+ const inFillPreview = !!fillTarget && !selected && rangeContains(fillTarget, displayRowIndex, viewCol.index);
1851
+ const showFillHandle = !readOnly && !isEditing && !!selection && (span.merge ? rangeContains(span.merge, selection.endRow, selection.endCol) : selection.endRow === rIdx && selection.endCol === cIdx);
1852
+ return /* @__PURE__ */ jsxs7(
1853
+ "td",
1854
+ {
1855
+ "data-row": rIdx,
1856
+ "data-col": cIdx,
1857
+ rowSpan: span.rowSpan,
1858
+ colSpan: span.colSpan,
1859
+ 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" : ""}`,
1860
+ style: {
1861
+ maxWidth: 0,
1862
+ overflow: "hidden",
1863
+ ...frozenCellStyle(viewRow, viewCol),
1864
+ ...inFillPreview ? { outline: "1px dashed var(--primary, #6366f1)", outlineOffset: -1 } : {}
1865
+ },
1866
+ onClick: (e) => handleCellClick(rIdx, cIdx, e),
1867
+ onDoubleClick: () => handleCellDblClick(rIdx, cIdx),
1868
+ onContextMenu: (e) => handleCellContextMenu(rIdx, cIdx, e),
1869
+ onMouseOver: () => extendFill(displayRowIndex, viewCol.index),
1870
+ children: [
1871
+ isEditing ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
1872
+ /* @__PURE__ */ jsx8(
1873
+ "input",
1874
+ {
1875
+ ref: cellInputRef,
1876
+ type: "text",
1877
+ 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",
1878
+ value: state.draftValue,
1879
+ onChange: (e) => state.setDraftValue(e.target.value),
1880
+ onBlur: () => state.commitEditing(),
1881
+ autoFocus: true
1882
+ }
1883
+ ),
1884
+ /* @__PURE__ */ jsx8(FormulaHints, { inputRef: cellInputRef, value: state.draftValue, onAccept: state.setDraftValue })
1885
+ ] }) : customCellRenderer ? customCellRenderer({ row, col, cell }) : /* @__PURE__ */ jsx8(
1886
+ "span",
1887
+ {
1888
+ className: `truncate block max-w-full ${cell.error ? "text-destructive font-semibold" : ""}`,
1889
+ title: cell.error ? cell.errorMessage ?? cell.error : void 0,
1890
+ children: cell.error || (cell.value ?? "")
1891
+ }
1892
+ ),
1893
+ showFillHandle && /* @__PURE__ */ jsx8(
1894
+ "div",
1895
+ {
1896
+ className: "spreadsheet-fill-handle",
1897
+ "aria-label": "Fill handle",
1898
+ onMouseDown: startFill,
1899
+ onClick: (e) => e.stopPropagation(),
1900
+ style: { position: "absolute", right: 0, bottom: 0, width: 7, height: 7, background: "var(--primary, #6366f1)", cursor: "crosshair", zIndex: 25 }
1901
+ }
1902
+ )
1903
+ ]
1904
+ },
1905
+ viewCol.column.id
1906
+ );
1907
+ }),
1908
+ onAddColumnClick && /* @__PURE__ */ jsx8("td", { className: "w-10 min-w-[2.5rem] bg-muted/10 border-r border-border" })
1909
+ ]
1910
+ },
1911
+ viewRow.row.id
1912
+ );
1913
+ }) })
1220
1914
  ] }) }),
1221
- /* @__PURE__ */ jsx3("style", { children: `
1915
+ menu && /* @__PURE__ */ jsx8(
1916
+ SpreadsheetContextMenu,
1917
+ {
1918
+ items: menu.items,
1919
+ x: menu.x,
1920
+ y: menu.y,
1921
+ onClose: () => {
1922
+ setMenu(null);
1923
+ gridRef.current?.focus();
1924
+ }
1925
+ }
1926
+ ),
1927
+ /* @__PURE__ */ jsx8("style", { children: `
1222
1928
  .col-resize-handle:hover .col-resize-handle-line {
1223
1929
  background-color: var(--primary, #6366f1);
1224
1930
  opacity: 0.75;
@@ -1241,10 +1947,10 @@ var SpreadsheetEditor = ({
1241
1947
  };
1242
1948
 
1243
1949
  // src/slots.tsx
1244
- import { createContext, useContext, useMemo as useMemo4 } from "react";
1245
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1950
+ import { createContext, useContext, useMemo as useMemo9 } from "react";
1951
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1246
1952
  var defaultSlots = {
1247
- Button: (props) => /* @__PURE__ */ jsx4(
1953
+ Button: (props) => /* @__PURE__ */ jsx9(
1248
1954
  "button",
1249
1955
  {
1250
1956
  type: "button",
@@ -1254,43 +1960,378 @@ var defaultSlots = {
1254
1960
  children: props.children
1255
1961
  }
1256
1962
  ),
1257
- Tooltip: (props) => /* @__PURE__ */ jsx4("div", { className: `pixerate-tooltip-container ${props.className || ""}`, title: String(props.content), children: props.children }),
1258
- NodeHeader: (props) => /* @__PURE__ */ jsxs2("div", { className: `pixerate-node-header flex items-center justify-between ${props.className || ""}`, children: [
1259
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
1963
+ Tooltip: (props) => /* @__PURE__ */ jsx9("div", { className: `pixerate-tooltip-container ${props.className || ""}`, title: String(props.content), children: props.children }),
1964
+ NodeHeader: (props) => /* @__PURE__ */ jsxs8("div", { className: `pixerate-node-header flex items-center justify-between ${props.className || ""}`, children: [
1965
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
1260
1966
  props.icon,
1261
- /* @__PURE__ */ jsx4("span", { className: "font-semibold", children: props.title })
1967
+ /* @__PURE__ */ jsx9("span", { className: "font-semibold", children: props.title })
1262
1968
  ] }),
1263
- props.onClose && /* @__PURE__ */ jsx4("button", { type: "button", onClick: props.onClose, className: "opacity-70 hover:opacity-100", children: "\xD7" })
1969
+ props.onClose && /* @__PURE__ */ jsx9("button", { type: "button", onClick: props.onClose, className: "opacity-70 hover:opacity-100", children: "\xD7" })
1264
1970
  ] })
1265
1971
  };
1266
1972
  var EditorSlotsContext = createContext(defaultSlots);
1267
1973
  function EditorSlotsProvider({ slots, children }) {
1268
- const mergedSlots = useMemo4(() => ({ ...defaultSlots, ...slots }), [slots]);
1269
- return /* @__PURE__ */ jsx4(EditorSlotsContext.Provider, { value: mergedSlots, children });
1974
+ const mergedSlots = useMemo9(() => ({ ...defaultSlots, ...slots }), [slots]);
1975
+ return /* @__PURE__ */ jsx9(EditorSlotsContext.Provider, { value: mergedSlots, children });
1270
1976
  }
1271
1977
  function useEditorSlots() {
1272
1978
  return useContext(EditorSlotsContext);
1273
1979
  }
1980
+
1981
+ // src/review/useReview.ts
1982
+ import { useCallback as useCallback7, useMemo as useMemo10, useSyncExternalStore as useSyncExternalStore4 } from "react";
1983
+ import {
1984
+ getCommentStore,
1985
+ getTrackChangesStore
1986
+ } from "@pixerate/editor";
1987
+ var EMPTY_COMMENTS = Object.freeze({
1988
+ threads: [],
1989
+ activeThreadId: null
1990
+ });
1991
+ var EMPTY_TRACK_CHANGES = Object.freeze({
1992
+ enabled: false,
1993
+ author: { id: "anonymous", name: "Anonymous" },
1994
+ changes: [],
1995
+ currentChangeId: null
1996
+ });
1997
+ var noopUnsubscribe = () => {
1998
+ };
1999
+ function isCommentStore(value) {
2000
+ return !!value && typeof value.getSnapshot === "function" && typeof value.createThread === "function";
2001
+ }
2002
+ function useComments(source) {
2003
+ const store = isCommentStore(source) ? source : getCommentStore(source);
2004
+ const subscribe = useCallback7(
2005
+ (onChange) => store ? store.subscribe(() => onChange()) : noopUnsubscribe,
2006
+ [store]
2007
+ );
2008
+ const getSnapshot = () => store?.getSnapshot() ?? EMPTY_COMMENTS;
2009
+ const snapshot = useSyncExternalStore4(subscribe, getSnapshot, getSnapshot);
2010
+ return useMemo10(() => ({ ...snapshot, store }), [snapshot, store]);
2011
+ }
2012
+ function useTrackChanges(editor) {
2013
+ const store = getTrackChangesStore(editor ?? null);
2014
+ const subscribe = useCallback7(
2015
+ (onChange) => store ? store.subscribe(() => onChange()) : noopUnsubscribe,
2016
+ [store]
2017
+ );
2018
+ const getSnapshot = () => store?.getSnapshot() ?? EMPTY_TRACK_CHANGES;
2019
+ const snapshot = useSyncExternalStore4(subscribe, getSnapshot, getSnapshot);
2020
+ return useMemo10(() => {
2021
+ const run = (fn) => () => {
2022
+ if (editor && !editor.isDestroyed) fn(editor);
2023
+ };
2024
+ return {
2025
+ ...snapshot,
2026
+ currentChange: snapshot.changes.find((c) => c.id === snapshot.currentChangeId) ?? null,
2027
+ setEnabled: (enabled) => run((e) => e.commands.setTrackChanges(enabled))(),
2028
+ toggle: run((e) => e.commands.toggleTrackChanges()),
2029
+ accept: (id) => run((e) => e.commands.acceptChange(id))(),
2030
+ reject: (id) => run((e) => e.commands.rejectChange(id))(),
2031
+ acceptAll: run((e) => e.commands.acceptAllChanges()),
2032
+ rejectAll: run((e) => e.commands.rejectAllChanges()),
2033
+ next: run((e) => e.chain().focus().selectNextChange().run()),
2034
+ previous: run((e) => e.chain().focus().selectPreviousChange().run())
2035
+ };
2036
+ }, [snapshot, editor]);
2037
+ }
2038
+
2039
+ // src/review/CommentsPanel.tsx
2040
+ import { useState as useState9 } from "react";
2041
+ import {
2042
+ formatReviewTime,
2043
+ getCommentAuthor
2044
+ } from "@pixerate/editor";
2045
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2046
+ var buttonClass = "rounded px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-50";
2047
+ var primaryButtonClass = "rounded px-2 py-1 text-xs bg-primary/10 text-primary hover:bg-primary/20 disabled:opacity-50";
2048
+ var textareaClass = "w-full resize-y rounded-md border border-input bg-background p-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary";
2049
+ var CommentsPanel = ({
2050
+ editor,
2051
+ store: storeProp,
2052
+ author: authorProp,
2053
+ showResolved: showResolvedInitial = false,
2054
+ className = "flex flex-col gap-2 text-sm text-foreground",
2055
+ formatTime = (t) => formatReviewTime(t),
2056
+ emptyMessage = "No comments yet. Select text and add a comment."
2057
+ }) => {
2058
+ const { threads, activeThreadId, store } = useComments(storeProp ?? editor);
2059
+ const [showResolved, setShowResolved] = useState9(showResolvedInitial);
2060
+ const author = authorProp ?? getCommentAuthor(editor) ?? { id: "anonymous", name: "Anonymous" };
2061
+ const visible = threads.filter((t) => showResolved || !t.resolved);
2062
+ const resolvedCount = threads.filter((t) => t.resolved).length;
2063
+ return /* @__PURE__ */ jsxs9("aside", { className, "aria-label": "Comments", children: [
2064
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between gap-2", children: [
2065
+ /* @__PURE__ */ jsxs9("span", { className: "text-xs font-medium text-muted-foreground", children: [
2066
+ threads.length - resolvedCount,
2067
+ " open",
2068
+ resolvedCount > 0 ? ` \xB7 ${resolvedCount} resolved` : ""
2069
+ ] }),
2070
+ resolvedCount > 0 && /* @__PURE__ */ jsx10(
2071
+ "button",
2072
+ {
2073
+ type: "button",
2074
+ className: buttonClass,
2075
+ "aria-pressed": showResolved,
2076
+ onClick: () => setShowResolved((v) => !v),
2077
+ children: showResolved ? "Hide resolved" : "Show resolved"
2078
+ }
2079
+ )
2080
+ ] }),
2081
+ visible.length === 0 ? /* @__PURE__ */ jsx10("p", { className: "text-xs text-muted-foreground", children: emptyMessage }) : /* @__PURE__ */ jsx10("ul", { className: "flex flex-col gap-2", role: "list", children: visible.map((thread) => /* @__PURE__ */ jsx10(
2082
+ CommentThreadCard,
2083
+ {
2084
+ thread,
2085
+ active: thread.id === activeThreadId,
2086
+ editor,
2087
+ store,
2088
+ author,
2089
+ formatTime
2090
+ },
2091
+ thread.id
2092
+ )) })
2093
+ ] });
2094
+ };
2095
+ var CommentThreadCard = ({
2096
+ thread,
2097
+ active,
2098
+ editor,
2099
+ store,
2100
+ author,
2101
+ formatTime
2102
+ }) => {
2103
+ const [draft, setDraft] = useState9("");
2104
+ const isDraft = thread.body === "";
2105
+ const quote = thread.anchor?.quote ?? thread.lastQuote ?? "";
2106
+ const live = editor && !editor.isDestroyed ? editor : null;
2107
+ const focus = () => {
2108
+ if (!active) {
2109
+ if (live && !thread.orphaned) live.commands.focusComment(thread.id);
2110
+ else store?.setActiveThread(thread.id);
2111
+ }
2112
+ };
2113
+ const remove = () => {
2114
+ if (live) live.commands.deleteComment(thread.id);
2115
+ else store?.deleteThread(thread.id);
2116
+ };
2117
+ const setResolved = (resolved) => {
2118
+ if (live) live.commands.resolveComment(thread.id, resolved);
2119
+ else store?.resolveThread(thread.id, resolved, author);
2120
+ };
2121
+ const submit = () => {
2122
+ const body = draft.trim();
2123
+ if (!body || !store) return;
2124
+ if (isDraft) store.editComment(thread.id, body);
2125
+ else store.addReply(thread.id, { body, author });
2126
+ setDraft("");
2127
+ };
2128
+ const stop = (e) => e.stopPropagation();
2129
+ return /* @__PURE__ */ jsxs9(
2130
+ "li",
2131
+ {
2132
+ className: `flex flex-col gap-2 rounded-md border bg-background p-3 cursor-pointer ${active ? "border-primary ring-1 ring-primary" : "border-border"} ${thread.resolved ? "opacity-70" : ""}`,
2133
+ "data-thread-id": thread.id,
2134
+ "data-active": active || void 0,
2135
+ "aria-current": active || void 0,
2136
+ tabIndex: 0,
2137
+ onClick: focus,
2138
+ onKeyDown: (e) => {
2139
+ if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) {
2140
+ e.preventDefault();
2141
+ focus();
2142
+ }
2143
+ },
2144
+ children: [
2145
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-baseline justify-between gap-2", children: [
2146
+ /* @__PURE__ */ jsx10("span", { className: "font-medium", children: thread.author.name }),
2147
+ /* @__PURE__ */ jsx10("span", { className: "text-xs text-muted-foreground", children: formatTime(thread.createdAt) })
2148
+ ] }),
2149
+ quote && /* @__PURE__ */ jsx10("blockquote", { className: "border-l-2 border-border pl-2 text-xs text-muted-foreground line-clamp-2", children: quote }),
2150
+ thread.orphaned && /* @__PURE__ */ jsx10("span", { className: "text-xs text-destructive", children: "The commented text was deleted." }),
2151
+ thread.resolved && /* @__PURE__ */ jsx10("span", { className: "text-xs text-muted-foreground", children: "Resolved" }),
2152
+ !isDraft && /* @__PURE__ */ jsx10("p", { className: "whitespace-pre-wrap break-words", children: thread.body }),
2153
+ thread.replies.length > 0 && /* @__PURE__ */ jsx10("ul", { className: "flex flex-col gap-2 border-l border-border pl-3", role: "list", children: thread.replies.map((reply) => /* @__PURE__ */ jsxs9("li", { className: "flex flex-col gap-0.5", "data-reply-id": reply.id, children: [
2154
+ /* @__PURE__ */ jsxs9("span", { className: "text-xs", children: [
2155
+ /* @__PURE__ */ jsx10("span", { className: "font-medium", children: reply.author.name }),
2156
+ " ",
2157
+ /* @__PURE__ */ jsx10("span", { className: "text-muted-foreground", children: formatTime(reply.createdAt) })
2158
+ ] }),
2159
+ /* @__PURE__ */ jsx10("p", { className: "whitespace-pre-wrap break-words", children: reply.body })
2160
+ ] }, reply.id)) }),
2161
+ (active || isDraft) && !thread.resolved && /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-1", onClick: stop, children: [
2162
+ /* @__PURE__ */ jsx10(
2163
+ "textarea",
2164
+ {
2165
+ className: textareaClass,
2166
+ rows: 2,
2167
+ value: draft,
2168
+ "aria-label": isDraft ? "Comment" : "Reply",
2169
+ placeholder: isDraft ? "Add a comment\u2026" : "Reply\u2026",
2170
+ autoFocus: isDraft,
2171
+ onChange: (e) => setDraft(e.target.value),
2172
+ onKeyDown: (e) => {
2173
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
2174
+ e.preventDefault();
2175
+ submit();
2176
+ }
2177
+ }
2178
+ }
2179
+ ),
2180
+ /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-1", children: [
2181
+ isDraft && /* @__PURE__ */ jsx10("button", { type: "button", className: buttonClass, onClick: remove, children: "Cancel" }),
2182
+ /* @__PURE__ */ jsx10(
2183
+ "button",
2184
+ {
2185
+ type: "button",
2186
+ className: primaryButtonClass,
2187
+ disabled: !draft.trim(),
2188
+ onClick: submit,
2189
+ children: isDraft ? "Comment" : "Reply"
2190
+ }
2191
+ )
2192
+ ] })
2193
+ ] }),
2194
+ !isDraft && /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-1", onClick: stop, children: [
2195
+ /* @__PURE__ */ jsx10("button", { type: "button", className: buttonClass, onClick: () => setResolved(!thread.resolved), children: thread.resolved ? "Reopen" : "Resolve" }),
2196
+ /* @__PURE__ */ jsx10(
2197
+ "button",
2198
+ {
2199
+ type: "button",
2200
+ className: `${buttonClass} hover:text-destructive`,
2201
+ onClick: remove,
2202
+ children: "Delete"
2203
+ }
2204
+ )
2205
+ ] })
2206
+ ]
2207
+ }
2208
+ );
2209
+ };
2210
+
2211
+ // src/review/ReviewToolbar.tsx
2212
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2213
+ var buttonClass2 = "rounded px-2 py-1 text-xs text-foreground hover:bg-muted disabled:opacity-40 disabled:hover:bg-transparent";
2214
+ var ReviewToolbar = ({
2215
+ editor,
2216
+ className = "flex flex-wrap items-center gap-1 rounded-md border border-border bg-background p-1 text-sm"
2217
+ }) => {
2218
+ const review = useTrackChanges(editor);
2219
+ const count = review.changes.length;
2220
+ const current = review.currentChange;
2221
+ const index = current ? review.changes.findIndex((c) => c.id === current.id) + 1 : 0;
2222
+ return /* @__PURE__ */ jsxs10("div", { className, role: "toolbar", "aria-label": "Review", children: [
2223
+ /* @__PURE__ */ jsx11(
2224
+ "button",
2225
+ {
2226
+ type: "button",
2227
+ className: `${buttonClass2} ${review.enabled ? "bg-primary/10 text-primary" : ""}`,
2228
+ "aria-pressed": review.enabled,
2229
+ disabled: !editor,
2230
+ onClick: review.toggle,
2231
+ children: review.enabled ? "Tracking on" : "Track changes"
2232
+ }
2233
+ ),
2234
+ /* @__PURE__ */ jsx11("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": "true" }),
2235
+ /* @__PURE__ */ jsx11(
2236
+ "button",
2237
+ {
2238
+ type: "button",
2239
+ className: buttonClass2,
2240
+ "aria-label": "Previous change",
2241
+ disabled: count === 0,
2242
+ onClick: review.previous,
2243
+ children: "\u2039"
2244
+ }
2245
+ ),
2246
+ /* @__PURE__ */ jsx11("span", { className: "min-w-[4rem] text-center text-xs text-muted-foreground", "aria-live": "polite", children: count === 0 ? "No changes" : current ? `${index} of ${count}` : `${count} changes` }),
2247
+ /* @__PURE__ */ jsx11(
2248
+ "button",
2249
+ {
2250
+ type: "button",
2251
+ className: buttonClass2,
2252
+ "aria-label": "Next change",
2253
+ disabled: count === 0,
2254
+ onClick: review.next,
2255
+ children: "\u203A"
2256
+ }
2257
+ ),
2258
+ /* @__PURE__ */ jsx11("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": "true" }),
2259
+ /* @__PURE__ */ jsx11(
2260
+ "button",
2261
+ {
2262
+ type: "button",
2263
+ className: buttonClass2,
2264
+ disabled: !current,
2265
+ onClick: () => current && review.accept(current.id),
2266
+ children: "Accept"
2267
+ }
2268
+ ),
2269
+ /* @__PURE__ */ jsx11(
2270
+ "button",
2271
+ {
2272
+ type: "button",
2273
+ className: buttonClass2,
2274
+ disabled: !current,
2275
+ onClick: () => current && review.reject(current.id),
2276
+ children: "Reject"
2277
+ }
2278
+ ),
2279
+ /* @__PURE__ */ jsx11("button", { type: "button", className: buttonClass2, disabled: count === 0, onClick: review.acceptAll, children: "Accept all" }),
2280
+ /* @__PURE__ */ jsx11("button", { type: "button", className: buttonClass2, disabled: count === 0, onClick: review.rejectAll, children: "Reject all" })
2281
+ ] });
2282
+ };
2283
+
2284
+ // src/review/CommentButton.tsx
2285
+ import { jsx as jsx12 } from "react/jsx-runtime";
2286
+ var CommentButton = ({
2287
+ editor,
2288
+ className = "rounded px-2 py-1 text-xs text-popover-foreground hover:bg-muted",
2289
+ children = "Comment"
2290
+ }) => /* @__PURE__ */ jsx12(
2291
+ "button",
2292
+ {
2293
+ type: "button",
2294
+ className,
2295
+ disabled: !editor,
2296
+ onClick: () => editor?.chain().focus().addComment().run(),
2297
+ children
2298
+ }
2299
+ );
1274
2300
  export {
1275
2301
  BubbleMenu,
2302
+ CommentButton,
2303
+ CommentsPanel,
1276
2304
  DirtyTracker,
1277
2305
  EditorContent,
1278
2306
  EditorSlotsProvider,
2307
+ FormulaHints,
1279
2308
  HistoryManager2 as HistoryManager,
1280
2309
  ImageEditor,
2310
+ OutlinePanel,
2311
+ ReviewToolbar,
2312
+ SpreadsheetContextMenu,
1281
2313
  SpreadsheetEditor,
2314
+ SpreadsheetGroupRow,
1282
2315
  TemplateRenderer,
2316
+ VariableForm,
1283
2317
  createDirtyTracker,
1284
2318
  createHistoryManager2 as createHistoryManager,
1285
2319
  defaultIsEqual,
2320
+ useAgentToolProvider,
2321
+ useComments,
1286
2322
  useDismissGuard,
1287
2323
  useEditor2 as useEditor,
1288
2324
  useEditorSlots,
2325
+ useFileImport,
2326
+ useFillHandle,
1289
2327
  useHistory,
1290
2328
  useHistoryShortcuts,
1291
2329
  useImageEditor,
1292
2330
  useNavigationGuard,
2331
+ useOutline,
1293
2332
  usePromptEditor,
1294
- useSpreadsheetEditor
2333
+ useSpreadsheetClipboard,
2334
+ useSpreadsheetEditor,
2335
+ useTrackChanges
1295
2336
  };
1296
2337
  //# sourceMappingURL=index.js.map