@mjml-agent-editor/editor 0.1.11 → 0.1.14

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.
@@ -18,7 +18,7 @@ import {
18
18
  } from "./chunk-V6RR4VRD.js";
19
19
 
20
20
  // src/stores.tsx
21
- import { createContext, useContext, useMemo } from "react";
21
+ import { createContext, useContext, useMemo, useState } from "react";
22
22
 
23
23
  // src/labels.ts
24
24
  var DEFAULT_LABELS = {
@@ -49,6 +49,29 @@ var DEFAULT_LABELS = {
49
49
  rteBackgroundColor: "Background color",
50
50
  documentLoadFailed: "Failed to load the document.",
51
51
  documentSaveFailed: "Failed to save changes.",
52
+ retry: "Retry",
53
+ chooseImage: "Choose image",
54
+ imagePickerTitle: "Choose an image",
55
+ imagePickerDrop: "Drag images here, or",
56
+ imagePickerBrowse: "Browse files",
57
+ imagePickerUrlPlaceholder: "\u2026or paste an image URL",
58
+ imagePickerUseUrl: "Use URL",
59
+ imagePickerGallery: "Your images",
60
+ imagePickerEmpty: "No images yet \u2014 the ones you upload will be kept here.",
61
+ imagePickerQuota: (used, limit) => `${used} of ${limit} uploads used`,
62
+ imagePickerQuotaFull: "Upload limit reached \u2014 pick an image below or paste a URL.",
63
+ imagePickerDelete: "Delete image",
64
+ imagePickerConfirmDelete: "Click again to delete",
65
+ imagePickerLoadFailed: "Failed to load your images.",
66
+ imageUploadFailed: "Failed to upload the image.",
67
+ imageDeleteFailed: "Failed to delete the image.",
68
+ imagePickerUnsupported: (fileName) => `${fileName} is not a supported image type.`,
69
+ imageFieldRemove: "Remove image",
70
+ colorPickerOpen: "Pick a color",
71
+ colorPickerEyedropper: "Pick from the screen",
72
+ colorPickerDocument: "In this email",
73
+ colorPickerPresets: "Presets",
74
+ colorPickerTransparent: "Transparent",
52
75
  commentOnSection: "Comment on the whole section",
53
76
  closeComments: "Close",
54
77
  noCommentsYet: "No comments yet \u2014 add the first one.",
@@ -90,13 +113,27 @@ function mergeLabels(overrides) {
90
113
  import { jsx } from "react/jsx-runtime";
91
114
  var StoreContext = createContext(null);
92
115
  var LabelContext = createContext(null);
116
+ var TurnContext = createContext({
117
+ running: false,
118
+ setRunning: () => {
119
+ },
120
+ locked: false,
121
+ setLocked: () => {
122
+ }
123
+ });
93
124
  function EditorStoreProvider({
94
125
  stores,
95
126
  labels,
96
127
  children
97
128
  }) {
98
129
  const merged = useMemo(() => mergeLabels(labels), [labels]);
99
- return /* @__PURE__ */ jsx(StoreContext.Provider, { value: stores, children: /* @__PURE__ */ jsx(LabelContext.Provider, { value: merged, children }) });
130
+ const [running, setRunning] = useState(false);
131
+ const [locked, setLocked] = useState(false);
132
+ const turn = useMemo(() => ({ running, setRunning, locked, setLocked }), [running, locked]);
133
+ return /* @__PURE__ */ jsx(StoreContext.Provider, { value: stores, children: /* @__PURE__ */ jsx(LabelContext.Provider, { value: merged, children: /* @__PURE__ */ jsx(TurnContext.Provider, { value: turn, children }) }) });
134
+ }
135
+ function useAgentTurn() {
136
+ return useContext(TurnContext);
100
137
  }
101
138
  function useLabels() {
102
139
  return useContext(LabelContext) ?? mergeLabels();
@@ -116,12 +153,15 @@ function useDocumentStore() {
116
153
  function useCommentStore() {
117
154
  return useStores().comments;
118
155
  }
156
+ function useImageLibrary() {
157
+ return useStores().images;
158
+ }
119
159
 
120
160
  // src/index.ts
121
161
  import { STARTER_MJML } from "@mjml-agent-editor/core";
122
162
 
123
163
  // src/components/editor/EditorHeader.tsx
124
- import { useEffect, useState } from "react";
164
+ import { useEffect, useState as useState2 } from "react";
125
165
  import { toast } from "sonner";
126
166
  import {
127
167
  Undo2,
@@ -420,6 +460,22 @@ function DialogTitle({ className, ...props }) {
420
460
  }
421
461
  );
422
462
  }
463
+ function DialogDescription({
464
+ className,
465
+ ...props
466
+ }) {
467
+ return /* @__PURE__ */ jsx6(
468
+ DialogPrimitive.Description,
469
+ {
470
+ "data-slot": "dialog-description",
471
+ className: cn(
472
+ "text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
473
+ className
474
+ ),
475
+ ...props
476
+ }
477
+ );
478
+ }
423
479
 
424
480
  // src/components/editor/EditorHeader.tsx
425
481
  import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -465,17 +521,18 @@ function EditorHeader({
465
521
  }) {
466
522
  const documents = useDocumentStore();
467
523
  const labels = useLabels();
468
- const [name, setName] = useState("");
469
- const [state, setState] = useState({
524
+ const [name, setName] = useState2("");
525
+ const [state, setState] = useState2({
470
526
  device: "Desktop",
471
527
  canUndo: false,
472
528
  canRedo: false,
473
529
  saveStatus: "idle",
474
530
  zoom: 100,
475
- contentWidth: "600px"
531
+ contentWidth: "600px",
532
+ readOnly: false
476
533
  });
477
- const [code, setCode] = useState({ mjml: "", html: "" });
478
- const [preview, setPreview] = useState("");
534
+ const [code, setCode] = useState2({ mjml: "", html: "" });
535
+ const [preview, setPreview] = useState2("");
479
536
  useEffect(() => {
480
537
  documents.get(docId).then((d) => setName(d.name)).catch(() => {
481
538
  });
@@ -569,6 +626,7 @@ function EditorHeader({
569
626
  {
570
627
  variant: "ghost",
571
628
  size: "sm",
629
+ disabled: state.readOnly,
572
630
  className: cn(darkGhost, "gap-1.5 rounded-md border border-panel-border max-xl:hidden"),
573
631
  children: [
574
632
  /* @__PURE__ */ jsx7("span", { className: "text-panel-muted-fg", children: labels.contentWidth }),
@@ -801,7 +859,7 @@ function EditorHeader({
801
859
  // src/components/chat/ChatPanel.tsx
802
860
  import { useChat } from "@ai-sdk/react";
803
861
  import { DefaultChatTransport } from "ai";
804
- import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
862
+ import { useEffect as useEffect2, useRef, useState as useState3 } from "react";
805
863
  import { toast as toast2 } from "sonner";
806
864
  import {
807
865
  Brain,
@@ -815,6 +873,27 @@ import {
815
873
  ChevronRight
816
874
  } from "lucide-react";
817
875
 
876
+ // src/lib/refusal.ts
877
+ function refusal(error) {
878
+ if (!(error instanceof Error)) return null;
879
+ const status = error.status;
880
+ const deliberate = typeof status === "number" ? status >= 400 && status < 500 : null;
881
+ if (deliberate === false) return null;
882
+ if (error.message.startsWith("{")) {
883
+ try {
884
+ const parsed = JSON.parse(error.message);
885
+ return typeof parsed.error === "string" && parsed.error.trim() !== "" ? parsed.error : null;
886
+ } catch {
887
+ return null;
888
+ }
889
+ }
890
+ const message = error.message.trim();
891
+ return deliberate && message !== "" && !message.startsWith("<") ? error.message : null;
892
+ }
893
+ function saveToastId(docId) {
894
+ return `document-save-${docId}`;
895
+ }
896
+
818
897
  // src/components/ui/message.tsx
819
898
  import "react";
820
899
  import { jsx as jsx8 } from "react/jsx-runtime";
@@ -1199,16 +1278,6 @@ function toBlocks(parts) {
1199
1278
  }
1200
1279
  return blocks;
1201
1280
  }
1202
- function refusal(error) {
1203
- const body = error instanceof Error ? error.message : "";
1204
- if (!body.startsWith("{")) return null;
1205
- try {
1206
- const parsed = JSON.parse(body);
1207
- return typeof parsed.error === "string" && parsed.error.trim() !== "" ? parsed.error : null;
1208
- } catch {
1209
- return null;
1210
- }
1211
- }
1212
1281
  function ToolMarker({ tool }) {
1213
1282
  const labels = useLabels();
1214
1283
  const name = tool.type.slice(5);
@@ -1270,8 +1339,10 @@ function ChatPanel({
1270
1339
  onSectionEditEnd
1271
1340
  }) {
1272
1341
  const labels = useLabels();
1273
- const [input, setInput] = useState2("");
1342
+ const [input, setInput] = useState3("");
1274
1343
  const taRef = useRef(null);
1344
+ const sending = useRef(false);
1345
+ const [flushing, setFlushing] = useState3(false);
1275
1346
  const times = useRef(/* @__PURE__ */ new Map());
1276
1347
  const timeFor = (id) => {
1277
1348
  if (!times.current.has(id)) {
@@ -1335,6 +1406,9 @@ function ChatPanel({
1335
1406
  }
1336
1407
  }, [messages, onLiveUpdate, onSectionEditStart, onSectionEditEnd]);
1337
1408
  const busy = status === "submitted" || status === "streaming";
1409
+ const { setRunning, locked } = useAgentTurn();
1410
+ useEffect2(() => setRunning(busy || flushing), [busy, flushing, setRunning]);
1411
+ useEffect2(() => () => setRunning(false), [setRunning]);
1338
1412
  const lastMsg = messages[messages.length - 1];
1339
1413
  const toolRunning = lastMsg?.role === "assistant" && lastMsg.parts.some((p) => {
1340
1414
  if (!p.type.startsWith("tool-")) return false;
@@ -1349,15 +1423,26 @@ function ChatPanel({
1349
1423
  };
1350
1424
  const send = async (text) => {
1351
1425
  const trimmed = text.trim();
1352
- if (!trimmed || busy) return;
1353
- await onBeforeSend().catch(console.error);
1426
+ if (!trimmed || busy || locked || sending.current) return;
1427
+ sending.current = true;
1428
+ setFlushing(true);
1429
+ try {
1430
+ await onBeforeSend();
1431
+ } catch (error) {
1432
+ console.error(error);
1433
+ toast2.error(refusal(error) ?? labels.documentSaveFailed, { id: saveToastId(docId) });
1434
+ return;
1435
+ } finally {
1436
+ sending.current = false;
1437
+ setFlushing(false);
1438
+ }
1354
1439
  void sendMessage({ text: trimmed });
1355
1440
  setInput("");
1356
1441
  if (taRef.current) taRef.current.style.height = "auto";
1357
1442
  };
1358
1443
  return /* @__PURE__ */ jsxs4("div", { className: "flex h-full flex-col", children: [
1359
1444
  /* @__PURE__ */ jsx14(MessageScrollerProvider, { autoScroll: true, children: /* @__PURE__ */ jsxs4(MessageScroller, { className: "flex-1", children: [
1360
- /* @__PURE__ */ jsx14(MessageScrollerViewport, { children: /* @__PURE__ */ jsxs4(MessageScrollerContent, { className: "p-3", children: [
1445
+ /* @__PURE__ */ jsx14(MessageScrollerViewport, { children: /* @__PURE__ */ jsxs4(MessageScrollerContent, { className: "px-5 py-3", children: [
1361
1446
  messages.length === 0 ? /* @__PURE__ */ jsx14(Empty, { className: "h-full", children: /* @__PURE__ */ jsxs4(EmptyHeader, { children: [
1362
1447
  /* @__PURE__ */ jsx14(EmptyMedia, { variant: "icon", children: /* @__PURE__ */ jsx14(MessageSquare2, {}) }),
1363
1448
  /* @__PURE__ */ jsx14(EmptyTitle, { children: labels.chatEmptyTitle }),
@@ -1432,7 +1517,7 @@ function ChatPanel({
1432
1517
  Button,
1433
1518
  {
1434
1519
  className: "mb-2 w-full bg-brand text-brand-fg hover:bg-brand/90",
1435
- disabled: busy,
1520
+ disabled: busy || flushing || locked,
1436
1521
  onClick: () => void send(APPLY_COMMENTS_PROMPT),
1437
1522
  children: [
1438
1523
  /* @__PURE__ */ jsx14(Sparkles, {}),
@@ -1491,7 +1576,7 @@ function ChatPanel({
1491
1576
  {
1492
1577
  type: "submit",
1493
1578
  size: "sm",
1494
- disabled: busy || !input.trim(),
1579
+ disabled: busy || flushing || locked || !input.trim(),
1495
1580
  className: "bg-brand text-brand-fg hover:bg-brand/90",
1496
1581
  children: [
1497
1582
  /* @__PURE__ */ jsx14(Send, {}),
@@ -1510,7 +1595,7 @@ function ChatPanel({
1510
1595
 
1511
1596
  // src/components/comments/CanvasComments.tsx
1512
1597
  import { useEditorMaybe } from "@grapesjs/react";
1513
- import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
1598
+ import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState4 } from "react";
1514
1599
  import { Check as Check2, MessageSquarePlus, X as X2 } from "lucide-react";
1515
1600
  import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs5 } from "react/jsx-runtime";
1516
1601
  var keyOf = (c) => c.objectId ? `obj-${c.objectId}` : `sec-${c.sectionId}`;
@@ -1526,11 +1611,11 @@ function CanvasComments({
1526
1611
  const labels = useLabels();
1527
1612
  const editor = useEditorMaybe();
1528
1613
  const containerRef = useRef2(null);
1529
- const [comments, setComments] = useState3([]);
1530
- const [positions, setPositions] = useState3({});
1531
- const [activeKey, setActiveKey] = useState3(null);
1532
- const [composeInfo, setComposeInfo] = useState3(null);
1533
- const [body, setBody] = useState3("");
1614
+ const [comments, setComments] = useState4([]);
1615
+ const [positions, setPositions] = useState4({});
1616
+ const [activeKey, setActiveKey] = useState4(null);
1617
+ const [composeInfo, setComposeInfo] = useState4(null);
1618
+ const [body, setBody] = useState4("");
1534
1619
  const reload = useCallback(() => {
1535
1620
  commentStore.list(docId).then(setComments).catch(console.error);
1536
1621
  }, [docId]);
@@ -1779,13 +1864,26 @@ export {
1779
1864
  DEFAULT_LABELS,
1780
1865
  mergeLabels,
1781
1866
  EditorStoreProvider,
1867
+ useAgentTurn,
1782
1868
  useLabels,
1783
1869
  useDocumentStore,
1784
1870
  useCommentStore,
1871
+ useImageLibrary,
1872
+ Popover,
1873
+ PopoverTrigger,
1874
+ PopoverAnchor,
1875
+ PopoverContent,
1876
+ Dialog,
1877
+ DialogContent,
1878
+ DialogHeader,
1879
+ DialogTitle,
1880
+ DialogDescription,
1785
1881
  EditorHeader,
1882
+ refusal,
1883
+ saveToastId,
1786
1884
  Markdown,
1787
1885
  ChatPanel,
1788
1886
  CanvasComments,
1789
1887
  STARTER_MJML
1790
1888
  };
1791
- //# sourceMappingURL=chunk-D2Y633TP.js.map
1889
+ //# sourceMappingURL=chunk-SR3NM6VC.js.map