@pixerate/editor-react 0.11.0 → 0.13.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,3 +1,12 @@
1
+ import {
2
+ FormulaHints,
3
+ SpreadsheetContextMenu,
4
+ SpreadsheetEditor,
5
+ SpreadsheetGroupRow,
6
+ useFillHandle,
7
+ useSpreadsheetClipboard,
8
+ useSpreadsheetEditor
9
+ } from "./chunk-MHTAR3RS.js";
1
10
  import {
2
11
  ImageEditor,
3
12
  useImageEditor
@@ -16,7 +25,8 @@ import {
16
25
  plainTextToTipTapHtml,
17
26
  plainTextToSlice,
18
27
  getEditorText,
19
- tokenizePrompt
28
+ tokenizePrompt,
29
+ FileImport
20
30
  } from "@pixerate/editor";
21
31
  var DEFAULT_TEMPLATES = [];
22
32
  var DEFAULT_COLOR_MAP = /* @__PURE__ */ new Map([
@@ -41,8 +51,12 @@ function usePromptEditor({
41
51
  onMilestone,
42
52
  containerId = "editor-container",
43
53
  jsonVariables,
44
- speed = 2
54
+ speed = 2,
55
+ fileImport
45
56
  }) {
57
+ const fileImportRef = useRef(fileImport);
58
+ fileImportRef.current = fileImport;
59
+ const fileImportEnabled = !!fileImport;
46
60
  const slashCommandActiveRef = useRef(slashCommandActive);
47
61
  slashCommandActiveRef.current = slashCommandActive;
48
62
  const clearSlashCommandRef = useRef(clearSlashCommand);
@@ -97,7 +111,14 @@ function usePromptEditor({
97
111
  TemplateSuggestions.configure({
98
112
  templates
99
113
  }),
100
- LoadingNode
114
+ LoadingNode,
115
+ ...fileImportEnabled ? [
116
+ FileImport.configure({
117
+ ...typeof fileImportRef.current === "object" ? fileImportRef.current : {},
118
+ // Prompt editors only ever receive plain text.
119
+ asPlainText: true
120
+ })
121
+ ] : []
101
122
  ],
102
123
  content: plainTextToTipTapHtml(content),
103
124
  onUpdate: ({ editor: ed }) => {
@@ -194,7 +215,7 @@ function usePromptEditor({
194
215
  },
195
216
  // Only options baked into extensions recreate the editor; editable state,
196
217
  // placeholder, class names and content are applied to the live instance.
197
- [resolvedColorMap, templateKey, speed, jsonVariablesKey]
218
+ [resolvedColorMap, templateKey, speed, jsonVariablesKey, fileImportEnabled]
198
219
  );
199
220
  useEffect(() => {
200
221
  if (editor && !editor.isDestroyed) {
@@ -240,10 +261,22 @@ function usePromptEditor({
240
261
 
241
262
  // src/hooks/useEditor.ts
242
263
  import { useEditor as useTipTapEditor } from "@tiptap/react";
243
- import { createRichTextPreset } from "@pixerate/editor";
264
+ import {
265
+ createRichTextPreset
266
+ } from "@pixerate/editor";
244
267
  function useEditor2(options = {}) {
245
- const { richTextOptions, extensions = [], ...editorOptions } = options;
246
- const preset = createRichTextPreset(richTextOptions);
268
+ const {
269
+ richTextOptions,
270
+ fileImport,
271
+ richClipboard,
272
+ extensions = [],
273
+ ...editorOptions
274
+ } = options;
275
+ const preset = createRichTextPreset({
276
+ ...fileImport !== void 0 ? { fileImport } : {},
277
+ ...richClipboard !== void 0 ? { richClipboard } : {},
278
+ ...richTextOptions
279
+ });
247
280
  const combinedExtensions = [...preset, ...extensions];
248
281
  return useTipTapEditor({
249
282
  extensions: combinedExtensions,
@@ -473,6 +506,42 @@ function useHistoryShortcuts(manager, options = {}) {
473
506
  }, [manager, enabled, target, ignoreEditableElements]);
474
507
  }
475
508
 
509
+ // src/hooks/useFileImport.ts
510
+ import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef5, useSyncExternalStore as useSyncExternalStore2 } from "react";
511
+ import {
512
+ FileImportController
513
+ } from "@pixerate/editor";
514
+ function useFileImport(editor, options = {}) {
515
+ const editorRef = useRef5(editor);
516
+ editorRef.current = editor;
517
+ const controller = useMemo2(
518
+ () => new FileImportController(() => {
519
+ const current = editorRef.current;
520
+ return current && !current.isDestroyed ? current : null;
521
+ }, options),
522
+ []
523
+ );
524
+ useEffect4(() => {
525
+ controller.setOptions(options);
526
+ });
527
+ const state = useSyncExternalStore2(controller.subscribe, controller.getState, controller.getState);
528
+ return {
529
+ ...state,
530
+ accept: controller.accept,
531
+ openFilePicker: controller.openFilePicker,
532
+ importFiles: controller.importFiles
533
+ };
534
+ }
535
+
536
+ // src/hooks/useAgentToolProvider.ts
537
+ import { useEffect as useEffect5 } from "react";
538
+ function useAgentToolProvider(registry, provider) {
539
+ useEffect5(() => {
540
+ if (!registry || !provider) return;
541
+ return registry.register(provider);
542
+ }, [registry, provider]);
543
+ }
544
+
476
545
  // src/components/EditorContent.tsx
477
546
  import {
478
547
  EditorContent as TipTapEditorContent
@@ -487,7 +556,7 @@ var EditorContent = ({
487
556
  };
488
557
 
489
558
  // src/components/BubbleMenu.tsx
490
- import { useEffect as useEffect4, useRef as useRef5, useState as useState3 } from "react";
559
+ import { useEffect as useEffect6, useRef as useRef6, useState as useState3 } from "react";
491
560
  import { createPortal } from "react-dom";
492
561
  import {
493
562
  BubbleMenuPlugin
@@ -501,12 +570,12 @@ var BubbleMenu = ({
501
570
  pluginKey = "bubbleMenu",
502
571
  updateDelay
503
572
  }) => {
504
- const menuEl = useRef5(null);
573
+ const menuEl = useRef6(null);
505
574
  const [mounted, setMounted] = useState3(false);
506
575
  if (!menuEl.current && typeof document !== "undefined") {
507
576
  menuEl.current = document.createElement("div");
508
577
  }
509
- useEffect4(() => {
578
+ useEffect6(() => {
510
579
  if (!editor || !menuEl.current || editor.isDestroyed) {
511
580
  return;
512
581
  }
@@ -537,28 +606,31 @@ var BubbleMenu = ({
537
606
  };
538
607
 
539
608
  // src/components/TemplateRenderer.tsx
540
- import { useMemo as useMemo2 } from "react";
609
+ import { useMemo as useMemo3 } from "react";
541
610
  import {
542
- tokenizePrompt as tokenizePrompt2,
611
+ evaluatePromptTokens,
543
612
  resolveTemplates
544
613
  } from "@pixerate/editor";
545
- import { jsx as jsx2 } from "react/jsx-runtime";
614
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
615
+ var CONDITIONAL_PREFIX = { if: "?", unless: "!", end: "/" };
546
616
  var TemplateRenderer = ({
547
617
  content,
548
618
  templates = [],
549
619
  templateColorMap,
550
620
  resolve = false,
621
+ bindings,
622
+ values,
551
623
  className = "font-mono text-sm leading-relaxed whitespace-pre-wrap break-words",
552
624
  onTemplateClick,
553
625
  onVariableClick
554
626
  }) => {
555
- const displayText = useMemo2(() => {
627
+ const displayText = useMemo3(() => {
556
628
  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]);
629
+ return resolveTemplates(content, templates, { bindings });
630
+ }, [content, templates, resolve, bindings]);
631
+ const tokens = useMemo3(() => {
632
+ return evaluatePromptTokens(displayText, values);
633
+ }, [displayText, values]);
562
634
  return /* @__PURE__ */ jsx2("div", { className, children: tokens.map((token, index) => {
563
635
  if (token.type === "template") {
564
636
  const colors = templateColorMap?.get(token.value) || {
@@ -582,13 +654,49 @@ var TemplateRenderer = ({
582
654
  );
583
655
  }
584
656
  if (token.type === "variable") {
585
- return /* @__PURE__ */ jsx2(
657
+ const details = [
658
+ `Variable: ${token.value}`,
659
+ token.description,
660
+ token.defaultValue !== void 0 ? `Default: ${token.defaultValue || "(empty)"}` : void 0
661
+ ].filter(Boolean).join("\n");
662
+ if (token.resolvedValue !== void 0) {
663
+ return /* @__PURE__ */ jsx2(
664
+ "span",
665
+ {
666
+ onClick: () => onVariableClick?.(token.value),
667
+ "data-variable": token.value,
668
+ "data-value-source": token.valueSource,
669
+ 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",
670
+ title: details,
671
+ children: token.resolvedValue
672
+ },
673
+ index
674
+ );
675
+ }
676
+ return /* @__PURE__ */ jsxs(
586
677
  "span",
587
678
  {
588
679
  onClick: () => onVariableClick?.(token.value),
680
+ "data-variable": token.value,
589
681
  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}}`
682
+ title: details,
683
+ children: [
684
+ `{${token.value}`,
685
+ token.defaultValue !== void 0 && /* @__PURE__ */ jsx2("span", { className: "opacity-60", children: `|${token.defaultValue}` }),
686
+ "}"
687
+ ]
688
+ },
689
+ index
690
+ );
691
+ }
692
+ if (token.type === "conditional" && token.conditional) {
693
+ return /* @__PURE__ */ jsx2(
694
+ "span",
695
+ {
696
+ "data-conditional": token.conditional,
697
+ 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",
698
+ 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`,
699
+ children: `{${CONDITIONAL_PREFIX[token.conditional]}${token.value}}`
592
700
  },
593
701
  index
594
702
  );
@@ -608,6 +716,143 @@ var TemplateRenderer = ({
608
716
  }) });
609
717
  };
610
718
 
719
+ // src/components/VariableForm.tsx
720
+ import { useId, useMemo as useMemo4, useState as useState4 } from "react";
721
+ import {
722
+ compactVariableValues,
723
+ getVariableFormFields
724
+ } from "@pixerate/editor";
725
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
726
+ var VariableForm = ({
727
+ content,
728
+ templates,
729
+ bindings,
730
+ values,
731
+ defaultValues,
732
+ onChange,
733
+ className = "flex flex-col gap-3 text-sm"
734
+ }) => {
735
+ const idPrefix = useId();
736
+ const [internal, setInternal] = useState4(
737
+ () => defaultValues ?? {}
738
+ );
739
+ const entered = values ?? internal;
740
+ const fields = useMemo4(
741
+ () => getVariableFormFields(content, { templates, bindings }),
742
+ [content, templates, bindings]
743
+ );
744
+ const update = (name, value) => {
745
+ const next = { ...entered, [name]: value };
746
+ if (values === void 0) setInternal(next);
747
+ onChange?.(compactVariableValues(next), next);
748
+ };
749
+ if (fields.length === 0) return null;
750
+ return /* @__PURE__ */ jsx3("form", { className, onSubmit: (event) => event.preventDefault(), children: fields.map((field) => {
751
+ const id = `${idPrefix}-${field.name}`;
752
+ if (field.conditionOnly) {
753
+ return /* @__PURE__ */ jsxs2(
754
+ "label",
755
+ {
756
+ htmlFor: id,
757
+ className: "flex items-center gap-2",
758
+ "data-variable-field": field.name,
759
+ children: [
760
+ /* @__PURE__ */ jsx3(
761
+ "input",
762
+ {
763
+ id,
764
+ name: field.name,
765
+ type: "checkbox",
766
+ checked: (entered[field.name] ?? "") !== "",
767
+ onChange: (event) => update(field.name, event.target.checked ? "true" : "")
768
+ }
769
+ ),
770
+ /* @__PURE__ */ jsx3("span", { children: field.label })
771
+ ]
772
+ },
773
+ field.name
774
+ );
775
+ }
776
+ return /* @__PURE__ */ jsxs2(
777
+ "label",
778
+ {
779
+ htmlFor: id,
780
+ className: "flex flex-col gap-1",
781
+ "data-variable-field": field.name,
782
+ children: [
783
+ /* @__PURE__ */ jsxs2("span", { className: "font-medium", children: [
784
+ field.label,
785
+ field.required && /* @__PURE__ */ jsx3("span", { className: "text-rose-500", "aria-hidden": "true", children: " *" })
786
+ ] }),
787
+ /* @__PURE__ */ jsx3(
788
+ "input",
789
+ {
790
+ id,
791
+ name: field.name,
792
+ type: "text",
793
+ value: entered[field.name] ?? "",
794
+ placeholder: field.placeholder,
795
+ "aria-required": field.required || void 0,
796
+ onChange: (event) => update(field.name, event.target.value),
797
+ className: "px-2 py-1 rounded-md border border-violet-400/40 bg-transparent"
798
+ }
799
+ )
800
+ ]
801
+ },
802
+ field.name
803
+ );
804
+ }) });
805
+ };
806
+
807
+ // src/hooks/useOutline.ts
808
+ import { useMemo as useMemo5, useSyncExternalStore as useSyncExternalStore3 } from "react";
809
+ import { createOutlineStore } from "@pixerate/editor";
810
+ function useOutline(editor) {
811
+ const store = useMemo5(() => createOutlineStore(editor), [editor]);
812
+ return useSyncExternalStore3(store.subscribe, store.getSnapshot, store.getSnapshot);
813
+ }
814
+
815
+ // src/components/OutlinePanel.tsx
816
+ import { navigateToHeading } from "@pixerate/editor";
817
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
818
+ var OutlinePanel = ({
819
+ editor,
820
+ title = "Outline",
821
+ emptyState = "Add headings to see the outline.",
822
+ maxLevel = 6,
823
+ focusOnNavigate = true,
824
+ className = "flex flex-col gap-1 text-sm",
825
+ itemClassName = "w-full truncate rounded px-2 py-1 text-left hover:bg-black/5",
826
+ activeItemClassName = "font-semibold bg-black/5",
827
+ onNavigate
828
+ }) => {
829
+ const { headings, activeId } = useOutline(editor);
830
+ const visible = headings.filter((h) => h.level <= maxLevel);
831
+ return /* @__PURE__ */ jsxs3("nav", { className, "aria-label": "Document outline", "data-outline-panel": "", children: [
832
+ title != null && /* @__PURE__ */ jsx4("div", { className: "pixerate-outline-title", children: title }),
833
+ 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) => {
834
+ const active = heading.id === activeId;
835
+ return /* @__PURE__ */ jsx4("li", { "data-level": heading.level, children: /* @__PURE__ */ jsx4(
836
+ "button",
837
+ {
838
+ type: "button",
839
+ className: active ? `${itemClassName} ${activeItemClassName}` : itemClassName,
840
+ style: { paddingLeft: `${0.5 + heading.depth * 0.75}rem` },
841
+ "aria-current": active ? "location" : void 0,
842
+ "data-heading-id": heading.id,
843
+ onClick: () => {
844
+ if (!editor) return;
845
+ if (navigateToHeading(editor, heading, { focus: focusOnNavigate })) {
846
+ onNavigate?.(heading);
847
+ }
848
+ },
849
+ children: heading.text || "Untitled"
850
+ }
851
+ ) }, heading.id);
852
+ }) })
853
+ ] });
854
+ };
855
+
611
856
  // src/index.ts
612
857
  import {
613
858
  DirtyTracker,
@@ -617,680 +862,392 @@ import {
617
862
  createHistoryManager as createHistoryManager2
618
863
  } from "@pixerate/editor";
619
864
 
620
- // src/spreadsheet/useSpreadsheetEditor.ts
621
- import { useState as useState4, useRef as useRef6, useCallback as useCallback4 } from "react";
865
+ // src/slots.tsx
866
+ import { createContext, useContext, useMemo as useMemo6 } from "react";
867
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
868
+ var defaultSlots = {
869
+ Button: (props) => /* @__PURE__ */ jsx5(
870
+ "button",
871
+ {
872
+ type: "button",
873
+ className: `pixerate-btn ${props.className || ""}`,
874
+ onClick: props.onClick,
875
+ disabled: props.disabled,
876
+ children: props.children
877
+ }
878
+ ),
879
+ Tooltip: (props) => /* @__PURE__ */ jsx5("div", { className: `pixerate-tooltip-container ${props.className || ""}`, title: String(props.content), children: props.children }),
880
+ NodeHeader: (props) => /* @__PURE__ */ jsxs4("div", { className: `pixerate-node-header flex items-center justify-between ${props.className || ""}`, children: [
881
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
882
+ props.icon,
883
+ /* @__PURE__ */ jsx5("span", { className: "font-semibold", children: props.title })
884
+ ] }),
885
+ props.onClose && /* @__PURE__ */ jsx5("button", { type: "button", onClick: props.onClose, className: "opacity-70 hover:opacity-100", children: "\xD7" })
886
+ ] })
887
+ };
888
+ var EditorSlotsContext = createContext(defaultSlots);
889
+ function EditorSlotsProvider({ slots, children }) {
890
+ const mergedSlots = useMemo6(() => ({ ...defaultSlots, ...slots }), [slots]);
891
+ return /* @__PURE__ */ jsx5(EditorSlotsContext.Provider, { value: mergedSlots, children });
892
+ }
893
+ function useEditorSlots() {
894
+ return useContext(EditorSlotsContext);
895
+ }
896
+
897
+ // src/review/useReview.ts
898
+ import { useCallback as useCallback4, useMemo as useMemo7, useSyncExternalStore as useSyncExternalStore4 } from "react";
622
899
  import {
623
- SpreadsheetController
900
+ getCommentStore,
901
+ getTrackChangesStore
624
902
  } from "@pixerate/editor";
625
- function useSpreadsheetEditor(options = {}) {
626
- const optionsRef = useRef6(options);
627
- optionsRef.current = options;
628
- const settersRef = useRef6({});
629
- const [controller] = useState4(
630
- () => new SpreadsheetController({
631
- ...options,
632
- onDocumentChange: (updated) => {
633
- settersRef.current.setDoc?.({ ...updated });
634
- optionsRef.current.onDocumentChange?.(updated);
635
- },
636
- onSelectionChange: (active, range) => {
637
- settersRef.current.setActiveCell?.(active);
638
- settersRef.current.setSelectedRange?.(range);
639
- optionsRef.current.onSelectionChange?.(active, range);
640
- },
641
- onCellCommit: (rowId, colId, raw, value) => {
642
- optionsRef.current.onCellCommit?.(rowId, colId, raw, value);
643
- }
644
- })
903
+ var EMPTY_COMMENTS = Object.freeze({
904
+ threads: [],
905
+ activeThreadId: null
906
+ });
907
+ var EMPTY_TRACK_CHANGES = Object.freeze({
908
+ enabled: false,
909
+ author: { id: "anonymous", name: "Anonymous" },
910
+ changes: [],
911
+ currentChangeId: null
912
+ });
913
+ var noopUnsubscribe = () => {
914
+ };
915
+ function isCommentStore(value) {
916
+ return !!value && typeof value.getSnapshot === "function" && typeof value.createThread === "function";
917
+ }
918
+ function useComments(source) {
919
+ const store = isCommentStore(source) ? source : getCommentStore(source);
920
+ const subscribe = useCallback4(
921
+ (onChange) => store ? store.subscribe(() => onChange()) : noopUnsubscribe,
922
+ [store]
645
923
  );
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("");
651
- settersRef.current = { setDoc, setActiveCell, setSelectedRange };
652
- const setCellValue = useCallback4((rowId, colId, raw) => {
653
- controller.setCellValue(rowId, colId, raw);
654
- setDoc({ ...controller.document });
655
- }, [controller]);
656
- const selectCell = useCallback4((row, col, extendSelection = false) => {
657
- controller.selectCell(row, col, extendSelection);
658
- setActiveCell(controller.activeCell);
659
- setSelectedRange(controller.selectedRange);
660
- }, [controller]);
661
- const selectRange = useCallback4((range) => {
662
- controller.selectRange(range);
663
- setSelectedRange(controller.selectedRange);
664
- }, [controller]);
665
- const startEditing = useCallback4((row, col, initialVal) => {
666
- controller.startEditing(row, col, initialVal);
667
- setActiveCell(controller.activeCell);
668
- setEditingCell(controller.editingCell);
669
- setDraftValue(controller.draftValue);
670
- }, [controller]);
671
- const commitEditing = useCallback4((val) => {
672
- controller.commitEditing(val);
673
- setEditingCell(null);
674
- setDraftValue("");
675
- setDoc({ ...controller.document });
676
- }, [controller]);
677
- const cancelEditing = useCallback4(() => {
678
- controller.cancelEditing();
679
- setEditingCell(null);
680
- setDraftValue("");
681
- }, [controller]);
682
- const moveCursor = useCallback4((dir, extend = false) => {
683
- controller.moveCursor(dir, extend);
684
- setActiveCell(controller.activeCell);
685
- setSelectedRange(controller.selectedRange);
686
- }, [controller]);
687
- const insertColumn = useCallback4((index, config) => {
688
- controller.insertColumn(index, config);
689
- setDoc({ ...controller.document });
690
- }, [controller]);
691
- const deleteColumn = useCallback4((colId) => {
692
- controller.deleteColumn(colId);
693
- setDoc({ ...controller.document });
694
- }, [controller]);
695
- const insertRow = useCallback4((index, config) => {
696
- controller.insertRow(index, config);
697
- setDoc({ ...controller.document });
698
- }, [controller]);
699
- const deleteRow = useCallback4((rowId) => {
700
- controller.deleteRow(rowId);
701
- setDoc({ ...controller.document });
702
- }, [controller]);
703
- const setColumnWidth = useCallback4((colId, width) => {
704
- controller.setColumnWidth(colId, width);
705
- setDoc({ ...controller.document });
706
- }, [controller]);
707
- const autoFitColumnWidth = useCallback4((colId, customWidth) => {
708
- controller.autoFitColumnWidth(colId, customWidth);
709
- setDoc({ ...controller.document });
710
- }, [controller]);
711
- const registerDataSource = useCallback4((ds) => {
712
- controller.registerDataSource(ds);
713
- setDoc({ ...controller.document });
714
- }, [controller]);
715
- const setPrimaryDataSource = useCallback4((id) => {
716
- controller.setPrimaryDataSource(id);
717
- setDoc({ ...controller.document });
718
- }, [controller]);
719
- const recalculateAll = useCallback4(() => {
720
- controller.recalculateAll();
721
- setDoc({ ...controller.document });
722
- }, [controller]);
723
- const undo = useCallback4(() => {
724
- controller.undo();
725
- setDoc({ ...controller.document });
726
- }, [controller]);
727
- const redo = useCallback4(() => {
728
- controller.redo();
729
- setDoc({ ...controller.document });
730
- }, [controller]);
731
- return {
732
- document: doc,
733
- activeCell,
734
- selectedRange,
735
- editingCell,
736
- draftValue,
737
- setDraftValue,
738
- controller,
739
- getCell: (rowId, colId) => controller.getCell(rowId, colId),
740
- setCellValue,
741
- selectCell,
742
- selectRange,
743
- startEditing,
744
- commitEditing,
745
- cancelEditing,
746
- moveCursor,
747
- insertColumn,
748
- deleteColumn,
749
- insertRow,
750
- deleteRow,
751
- setColumnWidth,
752
- autoFitColumnWidth,
753
- loadDocument: (document2) => {
754
- controller.loadDocument(document2);
755
- setDoc({ ...controller.document });
756
- setActiveCell(null);
757
- setSelectedRange(null);
758
- setEditingCell(null);
759
- setDraftValue("");
760
- },
761
- syncWithDataSource: () => {
762
- controller.syncWithDataSource();
763
- controller.recalculateAll();
764
- setDoc({ ...controller.document });
765
- },
766
- registerDataSource,
767
- setPrimaryDataSource,
768
- recalculateAll,
769
- exportToTsv: (range) => controller.exportToTsv(range),
770
- importFromTsv: (data) => {
771
- controller.importFromTsv(data);
772
- setDoc({ ...controller.document });
773
- },
774
- undo,
775
- redo
776
- };
924
+ const getSnapshot = () => store?.getSnapshot() ?? EMPTY_COMMENTS;
925
+ const snapshot = useSyncExternalStore4(subscribe, getSnapshot, getSnapshot);
926
+ return useMemo7(() => ({ ...snapshot, store }), [snapshot, store]);
927
+ }
928
+ function useTrackChanges(editor) {
929
+ const store = getTrackChangesStore(editor ?? null);
930
+ const subscribe = useCallback4(
931
+ (onChange) => store ? store.subscribe(() => onChange()) : noopUnsubscribe,
932
+ [store]
933
+ );
934
+ const getSnapshot = () => store?.getSnapshot() ?? EMPTY_TRACK_CHANGES;
935
+ const snapshot = useSyncExternalStore4(subscribe, getSnapshot, getSnapshot);
936
+ return useMemo7(() => {
937
+ const run = (fn) => () => {
938
+ if (editor && !editor.isDestroyed) fn(editor);
939
+ };
940
+ return {
941
+ ...snapshot,
942
+ currentChange: snapshot.changes.find((c) => c.id === snapshot.currentChangeId) ?? null,
943
+ setEnabled: (enabled) => run((e) => e.commands.setTrackChanges(enabled))(),
944
+ toggle: run((e) => e.commands.toggleTrackChanges()),
945
+ accept: (id) => run((e) => e.commands.acceptChange(id))(),
946
+ reject: (id) => run((e) => e.commands.rejectChange(id))(),
947
+ acceptAll: run((e) => e.commands.acceptAllChanges()),
948
+ rejectAll: run((e) => e.commands.rejectAllChanges()),
949
+ next: run((e) => e.chain().focus().selectNextChange().run()),
950
+ previous: run((e) => e.chain().focus().selectPreviousChange().run())
951
+ };
952
+ }, [snapshot, editor]);
777
953
  }
778
954
 
779
- // src/spreadsheet/SpreadsheetEditor.tsx
780
- import { useRef as useRef7, useState as useState5, useMemo as useMemo3, useEffect as useEffect5, useCallback as useCallback5 } from "react";
955
+ // src/review/CommentsPanel.tsx
956
+ import { useState as useState5 } from "react";
781
957
  import {
782
- indexToColName
958
+ formatReviewTime,
959
+ getCommentAuthor
783
960
  } from "@pixerate/editor";
784
- import { jsx as jsx3, jsxs } from "react/jsx-runtime";
785
- var SpreadsheetEditor = ({
786
- state: externalState,
787
- readOnly = false,
788
- customCellRenderer,
789
- onAddColumnClick,
790
- onColumnHeaderClick,
791
- onColumnResize
961
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
962
+ var buttonClass = "rounded px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-50";
963
+ var primaryButtonClass = "rounded px-2 py-1 text-xs bg-primary/10 text-primary hover:bg-primary/20 disabled:opacity-50";
964
+ 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";
965
+ var CommentsPanel = ({
966
+ editor,
967
+ store: storeProp,
968
+ author: authorProp,
969
+ showResolved: showResolvedInitial = false,
970
+ className = "flex flex-col gap-2 text-sm text-foreground",
971
+ formatTime = (t) => formatReviewTime(t),
972
+ emptyMessage = "No comments yet. Select text and add a comment."
792
973
  }) => {
793
- const defaultState = useSpreadsheetEditor();
794
- 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(() => {
801
- if (!state.activeCell) return "";
802
- const row = state.document.rows[state.activeCell.row];
803
- const col = state.document.columns[state.activeCell.col];
804
- if (!row || !col) return "";
805
- return state.getCell(row.id, col.id).raw;
806
- }, [state.activeCell, state.document]);
807
- const coordLabel = useMemo3(() => {
808
- if (!state.activeCell) return "--";
809
- const colName = indexToColName(state.activeCell.col);
810
- return `${colName}${state.activeCell.row + 1}`;
811
- }, [state.activeCell]);
812
- const [formulaDraft, setFormulaDraft] = useState5(null);
813
- const activeColumn = state.activeCell ? state.document.columns[state.activeCell.col] : void 0;
814
- const formulaBarReadOnly = readOnly || !state.activeCell || !!activeColumn?.readOnly;
815
- useEffect5(() => {
816
- setFormulaDraft(null);
817
- }, [state.activeCell?.row, state.activeCell?.col]);
818
- const commitFormulaBar = () => {
819
- if (state.editingCell) {
820
- state.commitEditing();
821
- return;
822
- }
823
- if (formulaDraft === null || !state.activeCell) return;
824
- const row = state.document.rows[state.activeCell.row];
825
- const col = state.document.columns[state.activeCell.col];
826
- if (row && col && !formulaBarReadOnly && formulaDraft !== activeRawValue) {
827
- state.setCellValue(row.id, col.id, formulaDraft);
828
- }
829
- setFormulaDraft(null);
830
- };
831
- const handleCellClick = useCallback5((rIdx, cIdx, e) => {
832
- if (state.editingCell && (state.editingCell.row !== rIdx || state.editingCell.col !== cIdx)) {
833
- state.commitEditing();
834
- }
835
- state.selectCell(rIdx, cIdx, e.shiftKey);
836
- }, [state]);
837
- const handleCellDblClick = useCallback5((rIdx, cIdx) => {
838
- if (readOnly) return;
839
- const col = state.document.columns[cIdx];
840
- if (col?.readOnly) return;
841
- state.startEditing(rIdx, cIdx);
842
- }, [readOnly, state]);
843
- const handleKeyDown = useCallback5((e) => {
844
- if (state.editingCell) {
845
- if (e.key === "Enter") {
846
- e.preventDefault();
847
- state.commitEditing();
848
- state.moveCursor("down");
849
- } else if (e.key === "Escape") {
850
- e.preventDefault();
851
- state.cancelEditing();
852
- } else if (e.key === "Tab") {
853
- e.preventDefault();
854
- state.commitEditing();
855
- state.moveCursor(e.shiftKey ? "left" : "right");
856
- }
857
- return;
858
- }
859
- if (!state.activeCell) return;
860
- if (e.key === "ArrowUp") {
861
- e.preventDefault();
862
- state.moveCursor("up", e.shiftKey);
863
- } else if (e.key === "ArrowDown") {
864
- e.preventDefault();
865
- state.moveCursor("down", e.shiftKey);
866
- } else if (e.key === "ArrowLeft") {
867
- e.preventDefault();
868
- state.moveCursor("left", e.shiftKey);
869
- } else if (e.key === "ArrowRight") {
870
- e.preventDefault();
871
- state.moveCursor("right", e.shiftKey);
872
- } else if (e.key === "Tab") {
873
- e.preventDefault();
874
- state.moveCursor(e.shiftKey ? "left" : "right");
875
- } else if (e.key === "Enter") {
876
- e.preventDefault();
877
- const col = state.document.columns[state.activeCell.col];
878
- if (!readOnly && !col?.readOnly) {
879
- state.startEditing();
880
- }
881
- } else if (e.key === "Backspace" || e.key === "Delete") {
882
- if (readOnly) return;
883
- const row = state.document.rows[state.activeCell.row];
884
- const col = state.document.columns[state.activeCell.col];
885
- if (row && col && !col.readOnly) {
886
- e.preventDefault();
887
- state.setCellValue(row.id, col.id, "");
888
- }
889
- } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
890
- e.preventDefault();
891
- if (e.shiftKey) {
892
- state.redo();
893
- } else {
894
- state.undo();
895
- }
896
- } else if (e.ctrlKey && !e.metaKey && e.key.toLowerCase() === "y") {
897
- e.preventDefault();
898
- state.redo();
899
- } 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);
903
- } else if (!readOnly && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
904
- const col = state.document.columns[state.activeCell.col];
905
- if (!col?.readOnly) {
906
- state.startEditing(state.activeCell.row, state.activeCell.col, e.key);
907
- }
908
- }
909
- }, [readOnly, state]);
910
- const stateRef = useRef7(state);
911
- stateRef.current = state;
912
- const onColumnResizeRef = useRef7(onColumnResize);
913
- onColumnResizeRef.current = onColumnResize;
914
- useEffect5(() => {
915
- if (!resizingColId) return;
916
- if (typeof document !== "undefined") {
917
- document.body.style.cursor = "col-resize";
918
- document.body.style.userSelect = "none";
919
- }
920
- let rafId = null;
921
- let pendingWidth = resizeStartWidth;
922
- let hasDragged = false;
923
- const handleMouseMove = (e) => {
924
- const delta = e.clientX - resizeStartX;
925
- if (Math.abs(delta) > 2) {
926
- hasDragged = true;
927
- }
928
- pendingWidth = Math.max(30, resizeStartWidth + delta);
929
- if (rafId === null) {
930
- rafId = requestAnimationFrame(() => {
931
- rafId = null;
932
- stateRef.current.setColumnWidth(resizingColId, pendingWidth);
933
- });
934
- }
935
- };
936
- const handleMouseUp = () => {
937
- if (rafId !== null) {
938
- cancelAnimationFrame(rafId);
939
- rafId = null;
940
- }
941
- if (hasDragged) {
942
- stateRef.current.setColumnWidth(resizingColId, pendingWidth);
943
- onColumnResizeRef.current?.(resizingColId, pendingWidth);
944
- }
945
- setResizingColId(null);
946
- if (typeof document !== "undefined") {
947
- document.body.style.cursor = "";
948
- document.body.style.userSelect = "";
949
- }
950
- };
951
- window.addEventListener("mousemove", handleMouseMove);
952
- window.addEventListener("mouseup", handleMouseUp);
953
- return () => {
954
- if (rafId !== null) {
955
- cancelAnimationFrame(rafId);
956
- }
957
- if (typeof document !== "undefined") {
958
- document.body.style.cursor = "";
959
- document.body.style.userSelect = "";
960
- }
961
- window.removeEventListener("mousemove", handleMouseMove);
962
- window.removeEventListener("mouseup", handleMouseUp);
963
- };
964
- }, [resizingColId, resizeStartX, resizeStartWidth]);
965
- const measureElementContentWidth = useCallback5((el, container) => {
966
- if (typeof document === "undefined") return 0;
967
- let maxW = el.scrollWidth || 0;
968
- const firstChild = el.firstElementChild;
969
- if (firstChild && firstChild.scrollWidth > maxW) {
970
- maxW = firstChild.scrollWidth;
971
- }
972
- const truncated = el.querySelectorAll(".truncate, [title], span, div");
973
- truncated.forEach((node) => {
974
- if (node.scrollWidth > maxW) {
975
- maxW = node.scrollWidth;
976
- }
977
- });
978
- try {
979
- const clone = firstChild ? firstChild.cloneNode(true) : el.cloneNode(true);
980
- clone.style.cssText = `
981
- position: absolute !important;
982
- visibility: hidden !important;
983
- width: max-content !important;
984
- min-width: max-content !important;
985
- max-width: none !important;
986
- overflow: visible !important;
987
- white-space: nowrap !important;
988
- pointer-events: none !important;
989
- top: -9999px !important;
990
- left: -9999px !important;
991
- `;
992
- const allNodes = clone.querySelectorAll("*");
993
- allNodes.forEach((node) => {
994
- node.style.maxWidth = "none";
995
- node.style.overflow = "visible";
996
- node.style.textOverflow = "clip";
997
- node.style.whiteSpace = "nowrap";
998
- 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")) {
999
- node.style.width = "max-content";
1000
- node.style.minWidth = "max-content";
1001
- }
1002
- });
1003
- container.appendChild(clone);
1004
- const rect = clone.getBoundingClientRect();
1005
- container.removeChild(clone);
1006
- if (rect.width > maxW) {
1007
- maxW = rect.width;
1008
- }
1009
- } catch {
1010
- }
1011
- return maxW;
1012
- }, []);
1013
- const autoFitColumn = useCallback5((colId, colIndex) => {
1014
- const col = state.document.columns.find((c) => c.id === colId);
1015
- if (!col) return;
1016
- let calculatedWidth = 0;
1017
- if (gridRef.current && typeof document !== "undefined") {
1018
- const table = gridRef.current.querySelector("table");
1019
- if (table) {
1020
- const thElements = table.querySelectorAll("thead th");
1021
- const th = thElements[colIndex + 1];
1022
- if (th) {
1023
- const headerInner = th.firstElementChild || th;
1024
- const headerW = measureElementContentWidth(headerInner, gridRef.current);
1025
- calculatedWidth = Math.max(calculatedWidth, headerW + 28);
1026
- }
1027
- const rows = table.querySelectorAll("tbody tr");
1028
- const sampleLimit = Math.min(rows.length, 150);
1029
- for (let r = 0; r < sampleLimit; r++) {
1030
- const tr = rows[r];
1031
- const td = tr.children[colIndex + 1];
1032
- if (td) {
1033
- const cellW = measureElementContentWidth(td, gridRef.current);
1034
- calculatedWidth = Math.max(calculatedWidth, cellW + 20);
1035
- }
974
+ const { threads, activeThreadId, store } = useComments(storeProp ?? editor);
975
+ const [showResolved, setShowResolved] = useState5(showResolvedInitial);
976
+ const author = authorProp ?? getCommentAuthor(editor) ?? { id: "anonymous", name: "Anonymous" };
977
+ const visible = threads.filter((t) => showResolved || !t.resolved);
978
+ const resolvedCount = threads.filter((t) => t.resolved).length;
979
+ return /* @__PURE__ */ jsxs5("aside", { className, "aria-label": "Comments", children: [
980
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between gap-2", children: [
981
+ /* @__PURE__ */ jsxs5("span", { className: "text-xs font-medium text-muted-foreground", children: [
982
+ threads.length - resolvedCount,
983
+ " open",
984
+ resolvedCount > 0 ? ` \xB7 ${resolvedCount} resolved` : ""
985
+ ] }),
986
+ resolvedCount > 0 && /* @__PURE__ */ jsx6(
987
+ "button",
988
+ {
989
+ type: "button",
990
+ className: buttonClass,
991
+ "aria-pressed": showResolved,
992
+ onClick: () => setShowResolved((v) => !v),
993
+ children: showResolved ? "Hide resolved" : "Show resolved"
1036
994
  }
1037
- }
1038
- }
1039
- if (calculatedWidth > 0) {
1040
- const finalWidth = Math.min(600, Math.max(50, Math.ceil(calculatedWidth)));
1041
- state.setColumnWidth(colId, finalWidth);
1042
- } else {
1043
- state.autoFitColumnWidth?.(colId);
1044
- }
1045
- const updatedCol = state.document.columns.find((c) => c.id === colId);
1046
- if (updatedCol && onColumnResize) {
1047
- onColumnResize(updatedCol.id, updatedCol.width || 130);
1048
- }
1049
- }, [state, measureElementContentWidth, onColumnResize]);
1050
- const handleResizeDblClick = useCallback5((colId, colIndex, e) => {
1051
- e.preventDefault();
1052
- e.stopPropagation();
1053
- autoFitColumn(colId, colIndex);
1054
- }, [autoFitColumn]);
1055
- const handleHeaderDblClick = useCallback5((colId, colIndex, e) => {
1056
- const target = e.currentTarget;
1057
- if (target) {
1058
- const rect = target.getBoundingClientRect();
1059
- if (rect.right - e.clientX <= 12) {
1060
- e.preventDefault();
1061
- e.stopPropagation();
1062
- autoFitColumn(colId, colIndex);
1063
- }
995
+ )
996
+ ] }),
997
+ visible.length === 0 ? /* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground", children: emptyMessage }) : /* @__PURE__ */ jsx6("ul", { className: "flex flex-col gap-2", role: "list", children: visible.map((thread) => /* @__PURE__ */ jsx6(
998
+ CommentThreadCard,
999
+ {
1000
+ thread,
1001
+ active: thread.id === activeThreadId,
1002
+ editor,
1003
+ store,
1004
+ author,
1005
+ formatTime
1006
+ },
1007
+ thread.id
1008
+ )) })
1009
+ ] });
1010
+ };
1011
+ var CommentThreadCard = ({
1012
+ thread,
1013
+ active,
1014
+ editor,
1015
+ store,
1016
+ author,
1017
+ formatTime
1018
+ }) => {
1019
+ const [draft, setDraft] = useState5("");
1020
+ const isDraft = thread.body === "";
1021
+ const quote = thread.anchor?.quote ?? thread.lastQuote ?? "";
1022
+ const live = editor && !editor.isDestroyed ? editor : null;
1023
+ const focus = () => {
1024
+ if (!active) {
1025
+ if (live && !thread.orphaned) live.commands.focusComment(thread.id);
1026
+ else store?.setActiveThread(thread.id);
1064
1027
  }
1065
- }, [autoFitColumn]);
1066
- const isCellSelected = (rIdx, cIdx) => {
1067
- if (!state.selectedRange) return false;
1068
- const minR = Math.min(state.selectedRange.startRow, state.selectedRange.endRow);
1069
- const maxR = Math.max(state.selectedRange.startRow, state.selectedRange.endRow);
1070
- const minC = Math.min(state.selectedRange.startCol, state.selectedRange.endCol);
1071
- const maxC = Math.max(state.selectedRange.startCol, state.selectedRange.endCol);
1072
- return rIdx >= minR && rIdx <= maxR && cIdx >= minC && cIdx <= maxC;
1073
1028
  };
1074
- const isActiveCell = (rIdx, cIdx) => {
1075
- return state.activeCell?.row === rIdx && state.activeCell?.col === cIdx;
1029
+ const remove = () => {
1030
+ if (live) live.commands.deleteComment(thread.id);
1031
+ else store?.deleteThread(thread.id);
1032
+ };
1033
+ const setResolved = (resolved) => {
1034
+ if (live) live.commands.resolveComment(thread.id, resolved);
1035
+ else store?.resolveThread(thread.id, resolved, author);
1036
+ };
1037
+ const submit = () => {
1038
+ const body = draft.trim();
1039
+ if (!body || !store) return;
1040
+ if (isDraft) store.editComment(thread.id, body);
1041
+ else store.addReply(thread.id, { body, author });
1042
+ setDraft("");
1076
1043
  };
1077
- return /* @__PURE__ */ jsxs(
1078
- "div",
1044
+ const stop = (e) => e.stopPropagation();
1045
+ return /* @__PURE__ */ jsxs5(
1046
+ "li",
1079
1047
  {
1080
- ref: gridRef,
1081
- 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",
1048
+ 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" : ""}`,
1049
+ "data-thread-id": thread.id,
1050
+ "data-active": active || void 0,
1051
+ "aria-current": active || void 0,
1082
1052
  tabIndex: 0,
1083
- onKeyDown: handleKeyDown,
1084
- role: "grid",
1085
- "aria-label": "Spreadsheet",
1053
+ onClick: focus,
1054
+ onKeyDown: (e) => {
1055
+ if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) {
1056
+ e.preventDefault();
1057
+ focus();
1058
+ }
1059
+ },
1086
1060
  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(
1091
- "input",
1061
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-baseline justify-between gap-2", children: [
1062
+ /* @__PURE__ */ jsx6("span", { className: "font-medium", children: thread.author.name }),
1063
+ /* @__PURE__ */ jsx6("span", { className: "text-xs text-muted-foreground", children: formatTime(thread.createdAt) })
1064
+ ] }),
1065
+ quote && /* @__PURE__ */ jsx6("blockquote", { className: "border-l-2 border-border pl-2 text-xs text-muted-foreground line-clamp-2", children: quote }),
1066
+ thread.orphaned && /* @__PURE__ */ jsx6("span", { className: "text-xs text-destructive", children: "The commented text was deleted." }),
1067
+ thread.resolved && /* @__PURE__ */ jsx6("span", { className: "text-xs text-muted-foreground", children: "Resolved" }),
1068
+ !isDraft && /* @__PURE__ */ jsx6("p", { className: "whitespace-pre-wrap break-words", children: thread.body }),
1069
+ thread.replies.length > 0 && /* @__PURE__ */ jsx6("ul", { className: "flex flex-col gap-2 border-l border-border pl-3", role: "list", children: thread.replies.map((reply) => /* @__PURE__ */ jsxs5("li", { className: "flex flex-col gap-0.5", "data-reply-id": reply.id, children: [
1070
+ /* @__PURE__ */ jsxs5("span", { className: "text-xs", children: [
1071
+ /* @__PURE__ */ jsx6("span", { className: "font-medium", children: reply.author.name }),
1072
+ " ",
1073
+ /* @__PURE__ */ jsx6("span", { className: "text-muted-foreground", children: formatTime(reply.createdAt) })
1074
+ ] }),
1075
+ /* @__PURE__ */ jsx6("p", { className: "whitespace-pre-wrap break-words", children: reply.body })
1076
+ ] }, reply.id)) }),
1077
+ (active || isDraft) && !thread.resolved && /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-1", onClick: stop, children: [
1078
+ /* @__PURE__ */ jsx6(
1079
+ "textarea",
1092
1080
  {
1093
- type: "text",
1094
- 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
- placeholder: "Enter a value or formula (e.g. =SUM(A1:A5))",
1096
- "aria-label": "Formula bar",
1097
- readOnly: formulaBarReadOnly,
1098
- value: state.editingCell ? state.draftValue : formulaDraft ?? activeRawValue,
1099
- onFocus: () => {
1100
- if (!state.editingCell) setFormulaDraft(activeRawValue);
1101
- },
1102
- onChange: (e) => {
1103
- if (state.editingCell) {
1104
- state.setDraftValue(e.target.value);
1105
- } else if (state.activeCell) {
1106
- setFormulaDraft(e.target.value);
1107
- }
1108
- },
1109
- onBlur: () => commitFormulaBar(),
1081
+ className: textareaClass,
1082
+ rows: 2,
1083
+ value: draft,
1084
+ "aria-label": isDraft ? "Comment" : "Reply",
1085
+ placeholder: isDraft ? "Add a comment\u2026" : "Reply\u2026",
1086
+ autoFocus: isDraft,
1087
+ onChange: (e) => setDraft(e.target.value),
1110
1088
  onKeyDown: (e) => {
1111
- e.stopPropagation();
1112
- if (e.key === "Enter") {
1113
- e.preventDefault();
1114
- commitFormulaBar();
1115
- } else if (e.key === "Escape") {
1089
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
1116
1090
  e.preventDefault();
1117
- setFormulaDraft(null);
1118
- if (state.editingCell) state.cancelEditing();
1091
+ submit();
1119
1092
  }
1120
1093
  }
1121
1094
  }
1122
- )
1123
- ] }),
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(
1128
- "th",
1129
- {
1130
- 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" },
1132
- onClick: () => onColumnHeaderClick?.(col),
1133
- onDoubleClick: (e) => handleHeaderDblClick(col.id, cIdx, e),
1134
- 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: [
1138
- "(",
1139
- col.key,
1140
- ")"
1141
- ] })
1142
- ] }),
1143
- /* @__PURE__ */ jsx3(
1144
- "div",
1145
- {
1146
- className: "col-resize-handle",
1147
- style: { position: "absolute", top: 0, bottom: 0, width: 12, right: -6, cursor: "col-resize", zIndex: 20, display: "flex", justifyContent: "center", touchAction: "none" },
1148
- onMouseEnter: () => setHoveredColId(col.id),
1149
- onMouseLeave: () => setHoveredColId((cur) => cur === col.id ? null : cur),
1150
- onMouseDown: (e) => {
1151
- e.preventDefault();
1152
- e.stopPropagation();
1153
- setResizingColId(col.id);
1154
- setResizeStartX(e.clientX);
1155
- setResizeStartWidth(col.width || 130);
1156
- },
1157
- onDoubleClick: (e) => handleResizeDblClick(col.id, cIdx, e),
1158
- role: "separator",
1159
- "aria-orientation": "vertical",
1160
- children: /* @__PURE__ */ jsx3(
1161
- "div",
1162
- {
1163
- className: "col-resize-handle-line",
1164
- style: {
1165
- width: 2,
1166
- height: "100%",
1167
- pointerEvents: "none",
1168
- transition: "background-color 150ms ease",
1169
- backgroundColor: resizingColId === col.id ? "var(--primary, #6366f1)" : hoveredColId === col.id ? "color-mix(in srgb, var(--primary, #6366f1) 75%, transparent)" : "transparent"
1170
- }
1171
- }
1172
- )
1173
- }
1174
- )
1175
- ]
1176
- },
1177
- col.id
1178
- )),
1179
- onAddColumnClick && /* @__PURE__ */ jsx3(
1180
- "th",
1095
+ ),
1096
+ /* @__PURE__ */ jsxs5("div", { className: "flex justify-end gap-1", children: [
1097
+ isDraft && /* @__PURE__ */ jsx6("button", { type: "button", className: buttonClass, onClick: remove, children: "Cancel" }),
1098
+ /* @__PURE__ */ jsx6(
1099
+ "button",
1181
1100
  {
1182
- className: "w-10 min-w-[2.5rem] px-1 text-center bg-muted/40 hover:bg-muted cursor-pointer transition-colors",
1183
- onClick: onAddColumnClick,
1184
- title: "Add Column",
1185
- children: /* @__PURE__ */ jsx3("span", { className: "text-xs font-bold text-muted-foreground", children: "+" })
1101
+ type: "button",
1102
+ className: primaryButtonClass,
1103
+ disabled: !draft.trim(),
1104
+ onClick: submit,
1105
+ children: isDraft ? "Comment" : "Reply"
1186
1106
  }
1187
1107
  )
1188
- ] }) }),
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",
1198
- {
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 ?? "") })
1214
- },
1215
- col.id
1216
- );
1217
- }),
1218
- onAddColumnClick && /* @__PURE__ */ jsx3("td", { className: "w-10 min-w-[2.5rem] bg-muted/10 border-r border-border" })
1219
- ] }, row.id)) })
1220
- ] }) }),
1221
- /* @__PURE__ */ jsx3("style", { children: `
1222
- .col-resize-handle:hover .col-resize-handle-line {
1223
- background-color: var(--primary, #6366f1);
1224
- opacity: 0.75;
1225
- }
1226
- .col-resize-handle.active .col-resize-handle-line {
1227
- background-color: var(--primary, #6366f1);
1228
- opacity: 1;
1229
- }
1230
- table.table-fixed th {
1231
- overflow: hidden;
1232
- }
1233
- table.table-fixed td {
1234
- max-width: 0;
1235
- overflow: hidden;
1236
- }
1237
- ` })
1108
+ ] })
1109
+ ] }),
1110
+ !isDraft && /* @__PURE__ */ jsxs5("div", { className: "flex justify-end gap-1", onClick: stop, children: [
1111
+ /* @__PURE__ */ jsx6("button", { type: "button", className: buttonClass, onClick: () => setResolved(!thread.resolved), children: thread.resolved ? "Reopen" : "Resolve" }),
1112
+ /* @__PURE__ */ jsx6(
1113
+ "button",
1114
+ {
1115
+ type: "button",
1116
+ className: `${buttonClass} hover:text-destructive`,
1117
+ onClick: remove,
1118
+ children: "Delete"
1119
+ }
1120
+ )
1121
+ ] })
1238
1122
  ]
1239
1123
  }
1240
1124
  );
1241
1125
  };
1242
1126
 
1243
- // src/slots.tsx
1244
- import { createContext, useContext, useMemo as useMemo4 } from "react";
1245
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1246
- var defaultSlots = {
1247
- Button: (props) => /* @__PURE__ */ jsx4(
1248
- "button",
1249
- {
1250
- type: "button",
1251
- className: `pixerate-btn ${props.className || ""}`,
1252
- onClick: props.onClick,
1253
- disabled: props.disabled,
1254
- children: props.children
1255
- }
1256
- ),
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: [
1260
- props.icon,
1261
- /* @__PURE__ */ jsx4("span", { className: "font-semibold", children: props.title })
1262
- ] }),
1263
- props.onClose && /* @__PURE__ */ jsx4("button", { type: "button", onClick: props.onClose, className: "opacity-70 hover:opacity-100", children: "\xD7" })
1264
- ] })
1127
+ // src/review/ReviewToolbar.tsx
1128
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1129
+ var buttonClass2 = "rounded px-2 py-1 text-xs text-foreground hover:bg-muted disabled:opacity-40 disabled:hover:bg-transparent";
1130
+ var ReviewToolbar = ({
1131
+ editor,
1132
+ className = "flex flex-wrap items-center gap-1 rounded-md border border-border bg-background p-1 text-sm"
1133
+ }) => {
1134
+ const review = useTrackChanges(editor);
1135
+ const count = review.changes.length;
1136
+ const current = review.currentChange;
1137
+ const index = current ? review.changes.findIndex((c) => c.id === current.id) + 1 : 0;
1138
+ return /* @__PURE__ */ jsxs6("div", { className, role: "toolbar", "aria-label": "Review", children: [
1139
+ /* @__PURE__ */ jsx7(
1140
+ "button",
1141
+ {
1142
+ type: "button",
1143
+ className: `${buttonClass2} ${review.enabled ? "bg-primary/10 text-primary" : ""}`,
1144
+ "aria-pressed": review.enabled,
1145
+ disabled: !editor,
1146
+ onClick: review.toggle,
1147
+ children: review.enabled ? "Tracking on" : "Track changes"
1148
+ }
1149
+ ),
1150
+ /* @__PURE__ */ jsx7("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": "true" }),
1151
+ /* @__PURE__ */ jsx7(
1152
+ "button",
1153
+ {
1154
+ type: "button",
1155
+ className: buttonClass2,
1156
+ "aria-label": "Previous change",
1157
+ disabled: count === 0,
1158
+ onClick: review.previous,
1159
+ children: "\u2039"
1160
+ }
1161
+ ),
1162
+ /* @__PURE__ */ jsx7("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` }),
1163
+ /* @__PURE__ */ jsx7(
1164
+ "button",
1165
+ {
1166
+ type: "button",
1167
+ className: buttonClass2,
1168
+ "aria-label": "Next change",
1169
+ disabled: count === 0,
1170
+ onClick: review.next,
1171
+ children: "\u203A"
1172
+ }
1173
+ ),
1174
+ /* @__PURE__ */ jsx7("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": "true" }),
1175
+ /* @__PURE__ */ jsx7(
1176
+ "button",
1177
+ {
1178
+ type: "button",
1179
+ className: buttonClass2,
1180
+ disabled: !current,
1181
+ onClick: () => current && review.accept(current.id),
1182
+ children: "Accept"
1183
+ }
1184
+ ),
1185
+ /* @__PURE__ */ jsx7(
1186
+ "button",
1187
+ {
1188
+ type: "button",
1189
+ className: buttonClass2,
1190
+ disabled: !current,
1191
+ onClick: () => current && review.reject(current.id),
1192
+ children: "Reject"
1193
+ }
1194
+ ),
1195
+ /* @__PURE__ */ jsx7("button", { type: "button", className: buttonClass2, disabled: count === 0, onClick: review.acceptAll, children: "Accept all" }),
1196
+ /* @__PURE__ */ jsx7("button", { type: "button", className: buttonClass2, disabled: count === 0, onClick: review.rejectAll, children: "Reject all" })
1197
+ ] });
1265
1198
  };
1266
- var EditorSlotsContext = createContext(defaultSlots);
1267
- function EditorSlotsProvider({ slots, children }) {
1268
- const mergedSlots = useMemo4(() => ({ ...defaultSlots, ...slots }), [slots]);
1269
- return /* @__PURE__ */ jsx4(EditorSlotsContext.Provider, { value: mergedSlots, children });
1270
- }
1271
- function useEditorSlots() {
1272
- return useContext(EditorSlotsContext);
1273
- }
1199
+
1200
+ // src/review/CommentButton.tsx
1201
+ import { jsx as jsx8 } from "react/jsx-runtime";
1202
+ var CommentButton = ({
1203
+ editor,
1204
+ className = "rounded px-2 py-1 text-xs text-popover-foreground hover:bg-muted",
1205
+ children = "Comment"
1206
+ }) => /* @__PURE__ */ jsx8(
1207
+ "button",
1208
+ {
1209
+ type: "button",
1210
+ className,
1211
+ disabled: !editor,
1212
+ onClick: () => editor?.chain().focus().addComment().run(),
1213
+ children
1214
+ }
1215
+ );
1274
1216
  export {
1275
1217
  BubbleMenu,
1218
+ CommentButton,
1219
+ CommentsPanel,
1276
1220
  DirtyTracker,
1277
1221
  EditorContent,
1278
1222
  EditorSlotsProvider,
1223
+ FormulaHints,
1279
1224
  HistoryManager2 as HistoryManager,
1280
1225
  ImageEditor,
1226
+ OutlinePanel,
1227
+ ReviewToolbar,
1228
+ SpreadsheetContextMenu,
1281
1229
  SpreadsheetEditor,
1230
+ SpreadsheetGroupRow,
1282
1231
  TemplateRenderer,
1232
+ VariableForm,
1283
1233
  createDirtyTracker,
1284
1234
  createHistoryManager2 as createHistoryManager,
1285
1235
  defaultIsEqual,
1236
+ useAgentToolProvider,
1237
+ useComments,
1286
1238
  useDismissGuard,
1287
1239
  useEditor2 as useEditor,
1288
1240
  useEditorSlots,
1241
+ useFileImport,
1242
+ useFillHandle,
1289
1243
  useHistory,
1290
1244
  useHistoryShortcuts,
1291
1245
  useImageEditor,
1292
1246
  useNavigationGuard,
1247
+ useOutline,
1293
1248
  usePromptEditor,
1294
- useSpreadsheetEditor
1249
+ useSpreadsheetClipboard,
1250
+ useSpreadsheetEditor,
1251
+ useTrackChanges
1295
1252
  };
1296
1253
  //# sourceMappingURL=index.js.map