@pixerate/editor-react 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -64,6 +64,8 @@ var DEFAULT_TEMPLATES = [];
64
64
  var DEFAULT_COLOR_MAP = /* @__PURE__ */ new Map([
65
65
  ["default", { from: "#f43f5e", to: "#8b5cf6" }]
66
66
  ]);
67
+ var DEFAULT_CLASS_NAME = "w-full border border-input rounded-md p-3 font-mono text-sm leading-relaxed focus:outline-none focus:ring-2 focus:ring-primary whitespace-pre-wrap break-words";
68
+ var normalizeNewlines = (text) => text.replace(/\r\n?/g, "\n");
67
69
  function usePromptEditor({
68
70
  content,
69
71
  onContentChange,
@@ -104,6 +106,7 @@ function usePromptEditor({
104
106
  const templateKey = (0, import_react.useMemo)(() => {
105
107
  return (templates || []).map((t) => `${t.id || t.name}:${t.latestVersion || ""}`).sort().join(",");
106
108
  }, [templates]);
109
+ const colorMapKey = userColorMap ? JSON.stringify(Array.from(userColorMap.entries())) : "";
107
110
  const resolvedColorMap = (0, import_react.useMemo)(() => {
108
111
  if (userColorMap) return userColorMap;
109
112
  const map = /* @__PURE__ */ new Map();
@@ -111,7 +114,9 @@ function usePromptEditor({
111
114
  map.set(t.name, { from: "#f43f5e", to: "#8b5cf6" });
112
115
  });
113
116
  return map.size > 0 ? map : DEFAULT_COLOR_MAP;
114
- }, [userColorMap, templateKey]);
117
+ }, [colorMapKey, templateKey]);
118
+ const jsonVariablesKey = (jsonVariables || []).join("\0");
119
+ const reachedMilestonesRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
115
120
  const StarterKitExt = import_starter_kit.default?.configure ? import_starter_kit.default : import_starter_kit.default?.default?.configure ? import_starter_kit.default?.default : import_starter_kit.default;
116
121
  const PlaceholderExt = import_extension_placeholder.default?.configure ? import_extension_placeholder.default : import_extension_placeholder.default?.default?.configure ? import_extension_placeholder.default?.default : import_extension_placeholder.default;
117
122
  const editor = (0, import_react2.useEditor)(
@@ -141,11 +146,13 @@ function usePromptEditor({
141
146
  const plainText = (0, import_editor.getEditorText)(ed);
142
147
  onContentChangeRef.current?.(plainText);
143
148
  if (onMilestoneRef.current) {
144
- if (plainText.match(/(?<!\{)\{([^{}]+)\}(?!\})/g)) {
145
- onMilestoneRef.current("used_variable");
146
- }
147
- if (plainText.match(/__([^_]+)__/g)) {
148
- onMilestoneRef.current("used_magic");
149
+ const reached = reachedMilestonesRef.current;
150
+ for (const token of (0, import_editor.tokenizePrompt)(plainText)) {
151
+ const milestone = token.type === "variable" ? "used_variable" : token.type === "instruction" ? "used_magic" : null;
152
+ if (milestone && !reached.has(milestone)) {
153
+ reached.add(milestone);
154
+ onMilestoneRef.current(milestone);
155
+ }
149
156
  }
150
157
  }
151
158
  const { state, view } = ed;
@@ -183,7 +190,7 @@ function usePromptEditor({
183
190
  },
184
191
  editorProps: {
185
192
  attributes: {
186
- class: className || "w-full border border-input rounded-md p-3 font-mono text-sm leading-relaxed focus:outline-none focus:ring-2 focus:ring-primary whitespace-pre-wrap break-words",
193
+ class: className || DEFAULT_CLASS_NAME,
187
194
  role: "textbox",
188
195
  "aria-label": "Prompt editor"
189
196
  },
@@ -215,51 +222,61 @@ function usePromptEditor({
215
222
  event.preventDefault();
216
223
  const schema = view.state.schema;
217
224
  if (!schema) return false;
218
- const blocks = text.replace(/\r\n?/g, "\n").split("\n");
219
- const nodes = [];
220
- blocks.forEach((line) => {
221
- const nodeJson = { type: "paragraph" };
222
- if (line.length > 0) {
223
- nodeJson.content = [{ type: "text", text: line }];
224
- }
225
- try {
226
- const node = import_model.Node.fromJSON(schema, nodeJson);
227
- nodes.push(node);
228
- } catch {
229
- }
230
- });
231
- const parsedSlice = import_model.Slice.maxOpen(import_model.Fragment.fromArray(nodes));
232
- const tr = view.state.tr.replaceSelection(parsedSlice);
233
- view.dispatch(tr);
225
+ view.dispatch(
226
+ view.state.tr.replaceSelection((0, import_editor.plainTextToSlice)(text, schema))
227
+ );
234
228
  return true;
235
229
  },
236
230
  clipboardTextParser: (text, context, _plain, view) => {
237
231
  const schema = context?.doc?.type?.schema || context?.schema || view?.state?.schema;
238
232
  if (!schema) return import_model.Slice.empty;
239
- const blocks = text.replace(/\r\n?/g, "\n").split("\n");
240
- const nodes = [];
241
- blocks.forEach((line) => {
242
- const nodeJson = { type: "paragraph" };
243
- if (line.length > 0) {
244
- nodeJson.content = [{ type: "text", text: line }];
245
- }
246
- try {
247
- const node = import_model.Node.fromJSON(schema, nodeJson);
248
- nodes.push(node);
249
- } catch {
250
- }
251
- });
252
- return import_model.Slice.maxOpen(import_model.Fragment.fromArray(nodes));
233
+ return (0, import_editor.plainTextToSlice)(text, schema);
253
234
  }
254
235
  }
255
236
  },
256
- [resolvedColorMap, templateKey, isEditing, placeholder, speed]
237
+ // Only options baked into extensions recreate the editor; editable state,
238
+ // placeholder, class names and content are applied to the live instance.
239
+ [resolvedColorMap, templateKey, speed, jsonVariablesKey]
257
240
  );
258
241
  (0, import_react.useEffect)(() => {
259
242
  if (editor && !editor.isDestroyed) {
260
243
  editor.setEditable(isEditing);
261
244
  }
262
245
  }, [editor, isEditing]);
246
+ (0, import_react.useEffect)(() => {
247
+ if (!editor || editor.isDestroyed) return;
248
+ if (normalizeNewlines(content) === (0, import_editor.getEditorText)(editor)) return;
249
+ const { from, to } = editor.state.selection;
250
+ editor.commands.setContent((0, import_editor.plainTextToTipTapHtml)(content), false);
251
+ const max = editor.state.doc.content.size;
252
+ editor.commands.setTextSelection({
253
+ from: Math.min(from, max),
254
+ to: Math.min(to, max)
255
+ });
256
+ }, [editor, content]);
257
+ (0, import_react.useEffect)(() => {
258
+ if (!editor || editor.isDestroyed) return;
259
+ editor.setOptions({
260
+ editorProps: {
261
+ ...editor.options.editorProps,
262
+ attributes: {
263
+ ...editor.options.editorProps.attributes,
264
+ class: className || DEFAULT_CLASS_NAME
265
+ }
266
+ }
267
+ });
268
+ }, [editor, className]);
269
+ (0, import_react.useEffect)(() => {
270
+ if (!editor || editor.isDestroyed) return;
271
+ const placeholderExt = editor.extensionManager.extensions.find(
272
+ (ext) => ext.name === "placeholder"
273
+ );
274
+ if (!placeholderExt || placeholderExt.options.placeholder === placeholder) {
275
+ return;
276
+ }
277
+ placeholderExt.options.placeholder = placeholder;
278
+ editor.view.dispatch(editor.state.tr);
279
+ }, [editor, placeholder]);
263
280
  return editor;
264
281
  }
265
282
 
@@ -628,36 +645,32 @@ var import_editor7 = require("@pixerate/editor");
628
645
  var import_react11 = require("react");
629
646
  var import_editor5 = require("@pixerate/editor");
630
647
  function useSpreadsheetEditor(options = {}) {
631
- const [doc, setDoc] = (0, import_react11.useState)(() => ({
632
- id: options.document?.id || `sheet_${Date.now()}`,
633
- name: options.document?.name || "Sheet 1",
634
- columns: options.document?.columns || [],
635
- rows: options.document?.rows || [],
636
- cells: options.document?.cells || {},
637
- createdAt: options.document?.createdAt || Date.now(),
638
- updatedAt: options.document?.updatedAt || Date.now()
639
- }));
640
- const [activeCell, setActiveCell] = (0, import_react11.useState)(null);
641
- const [selectedRange, setSelectedRange] = (0, import_react11.useState)(null);
642
- const [editingCell, setEditingCell] = (0, import_react11.useState)(null);
643
- const [draftValue, setDraftValue] = (0, import_react11.useState)("");
644
- const controller = (0, import_react11.useMemo)(() => {
645
- return new import_editor5.SpreadsheetController({
648
+ const optionsRef = (0, import_react11.useRef)(options);
649
+ optionsRef.current = options;
650
+ const settersRef = (0, import_react11.useRef)({});
651
+ const [controller] = (0, import_react11.useState)(
652
+ () => new import_editor5.SpreadsheetController({
646
653
  ...options,
647
654
  onDocumentChange: (updated) => {
648
- setDoc({ ...updated });
649
- options.onDocumentChange?.(updated);
655
+ settersRef.current.setDoc?.({ ...updated });
656
+ optionsRef.current.onDocumentChange?.(updated);
650
657
  },
651
658
  onSelectionChange: (active, range) => {
652
- setActiveCell(active);
653
- setSelectedRange(range);
654
- options.onSelectionChange?.(active, range);
659
+ settersRef.current.setActiveCell?.(active);
660
+ settersRef.current.setSelectedRange?.(range);
661
+ optionsRef.current.onSelectionChange?.(active, range);
655
662
  },
656
663
  onCellCommit: (rowId, colId, raw, value) => {
657
- options.onCellCommit?.(rowId, colId, raw, value);
664
+ optionsRef.current.onCellCommit?.(rowId, colId, raw, value);
658
665
  }
659
- });
660
- }, []);
666
+ })
667
+ );
668
+ const [doc, setDoc] = (0, import_react11.useState)(() => ({ ...controller.document }));
669
+ const [activeCell, setActiveCell] = (0, import_react11.useState)(controller.activeCell);
670
+ const [selectedRange, setSelectedRange] = (0, import_react11.useState)(controller.selectedRange);
671
+ const [editingCell, setEditingCell] = (0, import_react11.useState)(null);
672
+ const [draftValue, setDraftValue] = (0, import_react11.useState)("");
673
+ settersRef.current = { setDoc, setActiveCell, setSelectedRange };
661
674
  const setCellValue = (0, import_react11.useCallback)((rowId, colId, raw) => {
662
675
  controller.setCellValue(rowId, colId, raw);
663
676
  setDoc({ ...controller.document });
@@ -775,7 +788,7 @@ function useSpreadsheetEditor(options = {}) {
775
788
  registerDataSource,
776
789
  setPrimaryDataSource,
777
790
  recalculateAll,
778
- exportToTsv: () => controller.exportToTsv(),
791
+ exportToTsv: (range) => controller.exportToTsv(range),
779
792
  importFromTsv: (data) => {
780
793
  controller.importFromTsv(data);
781
794
  setDoc({ ...controller.document });
@@ -816,6 +829,25 @@ var SpreadsheetEditor = ({
816
829
  const colName = (0, import_editor6.indexToColName)(state.activeCell.col);
817
830
  return `${colName}${state.activeCell.row + 1}`;
818
831
  }, [state.activeCell]);
832
+ const [formulaDraft, setFormulaDraft] = (0, import_react12.useState)(null);
833
+ const activeColumn = state.activeCell ? state.document.columns[state.activeCell.col] : void 0;
834
+ const formulaBarReadOnly = readOnly || !state.activeCell || !!activeColumn?.readOnly;
835
+ (0, import_react12.useEffect)(() => {
836
+ setFormulaDraft(null);
837
+ }, [state.activeCell?.row, state.activeCell?.col]);
838
+ const commitFormulaBar = () => {
839
+ if (state.editingCell) {
840
+ state.commitEditing();
841
+ return;
842
+ }
843
+ if (formulaDraft === null || !state.activeCell) return;
844
+ const row = state.document.rows[state.activeCell.row];
845
+ const col = state.document.columns[state.activeCell.col];
846
+ if (row && col && !formulaBarReadOnly && formulaDraft !== activeRawValue) {
847
+ state.setCellValue(row.id, col.id, formulaDraft);
848
+ }
849
+ setFormulaDraft(null);
850
+ };
819
851
  const handleCellClick = (0, import_react12.useCallback)((rIdx, cIdx, e) => {
820
852
  if (state.editingCell && (state.editingCell.row !== rIdx || state.editingCell.col !== cIdx)) {
821
853
  state.commitEditing();
@@ -874,16 +906,19 @@ var SpreadsheetEditor = ({
874
906
  e.preventDefault();
875
907
  state.setCellValue(row.id, col.id, "");
876
908
  }
877
- } else if ((e.metaKey || e.ctrlKey) && e.key === "z") {
909
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
878
910
  e.preventDefault();
879
911
  if (e.shiftKey) {
880
912
  state.redo();
881
913
  } else {
882
914
  state.undo();
883
915
  }
884
- } else if ((e.metaKey || e.ctrlKey) && e.key === "c") {
916
+ } else if (e.ctrlKey && !e.metaKey && e.key.toLowerCase() === "y") {
917
+ e.preventDefault();
918
+ state.redo();
919
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "c") {
885
920
  e.preventDefault();
886
- const tsv = state.exportToTsv();
921
+ const tsv = state.exportToTsv(state.selectedRange);
887
922
  navigator.clipboard?.writeText(tsv);
888
923
  } else if (!readOnly && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
889
924
  const col = state.document.columns[state.activeCell.col];
@@ -892,6 +927,10 @@ var SpreadsheetEditor = ({
892
927
  }
893
928
  }
894
929
  }, [readOnly, state]);
930
+ const stateRef = (0, import_react12.useRef)(state);
931
+ stateRef.current = state;
932
+ const onColumnResizeRef = (0, import_react12.useRef)(onColumnResize);
933
+ onColumnResizeRef.current = onColumnResize;
895
934
  (0, import_react12.useEffect)(() => {
896
935
  if (!resizingColId) return;
897
936
  if (typeof document !== "undefined") {
@@ -910,7 +949,7 @@ var SpreadsheetEditor = ({
910
949
  if (rafId === null) {
911
950
  rafId = requestAnimationFrame(() => {
912
951
  rafId = null;
913
- state.setColumnWidth(resizingColId, pendingWidth);
952
+ stateRef.current.setColumnWidth(resizingColId, pendingWidth);
914
953
  });
915
954
  }
916
955
  };
@@ -920,11 +959,8 @@ var SpreadsheetEditor = ({
920
959
  rafId = null;
921
960
  }
922
961
  if (hasDragged) {
923
- state.setColumnWidth(resizingColId, pendingWidth);
924
- const col = state.document.columns.find((c) => c.id === resizingColId);
925
- if (col && onColumnResize) {
926
- onColumnResize(col.id, col.width || pendingWidth);
927
- }
962
+ stateRef.current.setColumnWidth(resizingColId, pendingWidth);
963
+ onColumnResizeRef.current?.(resizingColId, pendingWidth);
928
964
  }
929
965
  setResizingColId(null);
930
966
  if (typeof document !== "undefined") {
@@ -945,7 +981,7 @@ var SpreadsheetEditor = ({
945
981
  window.removeEventListener("mousemove", handleMouseMove);
946
982
  window.removeEventListener("mouseup", handleMouseUp);
947
983
  };
948
- }, [resizingColId, resizeStartX, resizeStartWidth, state, onColumnResize]);
984
+ }, [resizingColId, resizeStartX, resizeStartWidth]);
949
985
  const measureElementContentWidth = (0, import_react12.useCallback)((el, container) => {
950
986
  if (typeof document === "undefined") return 0;
951
987
  let maxW = el.scrollWidth || 0;
@@ -1077,25 +1113,29 @@ var SpreadsheetEditor = ({
1077
1113
  type: "text",
1078
1114
  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",
1079
1115
  placeholder: "Enter a value or formula (e.g. =SUM(A1:A5))",
1080
- value: state.editingCell ? state.draftValue : activeRawValue,
1116
+ "aria-label": "Formula bar",
1117
+ readOnly: formulaBarReadOnly,
1118
+ value: state.editingCell ? state.draftValue : formulaDraft ?? activeRawValue,
1119
+ onFocus: () => {
1120
+ if (!state.editingCell) setFormulaDraft(activeRawValue);
1121
+ },
1081
1122
  onChange: (e) => {
1082
- if (state.activeCell) {
1123
+ if (state.editingCell) {
1083
1124
  state.setDraftValue(e.target.value);
1125
+ } else if (state.activeCell) {
1126
+ setFormulaDraft(e.target.value);
1084
1127
  }
1085
1128
  },
1129
+ onBlur: () => commitFormulaBar(),
1086
1130
  onKeyDown: (e) => {
1131
+ e.stopPropagation();
1087
1132
  if (e.key === "Enter") {
1088
1133
  e.preventDefault();
1089
- if (state.activeCell) {
1090
- const row = state.document.rows[state.activeCell.row];
1091
- const col = state.document.columns[state.activeCell.col];
1092
- if (row && col) {
1093
- state.setCellValue(row.id, col.id, e.target.value);
1094
- }
1095
- }
1134
+ commitFormulaBar();
1096
1135
  } else if (e.key === "Escape") {
1097
1136
  e.preventDefault();
1098
- state.cancelEditing();
1137
+ setFormulaDraft(null);
1138
+ if (state.editingCell) state.cancelEditing();
1099
1139
  }
1100
1140
  }
1101
1141
  }