@mjml-agent-editor/editor 0.1.5 → 0.1.7

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/README.md CHANGED
@@ -60,11 +60,23 @@ warning about it:
60
60
  @source "../../node_modules/@mjml-agent-editor/editor/dist";
61
61
  ```
62
62
 
63
- ## Routing
63
+ ## Routing and branding
64
64
 
65
65
  The package has no router dependency. `EditorHeader` takes a `homeLink` node, so a Next.js
66
66
  host passes `<Link href="/">` and anything else passes an anchor.
67
67
 
68
+ `logo` works the same way. A brand mark belongs to the host, so the header renders whatever
69
+ node it is given and falls back to a generic envelope:
70
+
71
+ ```tsx
72
+ <EditorHeader
73
+ docId={id}
74
+ api={api}
75
+ openCount={n}
76
+ logo={<img src="/logo.png" alt="" className="size-8" />}
77
+ />
78
+ ```
79
+
68
80
  ## Data access
69
81
 
70
82
  Nothing here talks to a database. The host passes a `DocumentStore` and a `CommentStore`
@@ -86,6 +98,31 @@ satisfying the two interfaces works, including an in-memory object.
86
98
 
87
99
  Forgetting the provider throws with an explanation rather than failing on a null read.
88
100
 
101
+ ## Copy
102
+
103
+ Every word the editor renders comes from a dictionary with English defaults, so a host
104
+ building in another language overrides rather than forks:
105
+
106
+ ```tsx
107
+ <EditorStoreProvider
108
+ stores={stores}
109
+ labels={{
110
+ appName: "Edytor MJML",
111
+ send: "Wyślij",
112
+ agentWorking: "Agent pracuje…",
113
+ toolLabels: { set_section: "Podmieniam sekcję" },
114
+ }}
115
+ >
116
+ ```
117
+
118
+ Overrides are shallow-merged, so anything omitted stays English. `toolLabels` is the one
119
+ exception and merges key by key — renaming one step would otherwise silently drop the other
120
+ eight. `DEFAULT_LABELS` and the `EditorLabels` type are exported if you want to see the full
121
+ list or build a complete translation.
122
+
123
+ Values that interpolate are functions rather than format strings — `copied(kind)`,
124
+ `completedSteps(count, failed)` — so word order belongs to the translator.
125
+
89
126
  ## Reaching the agent
90
127
 
91
128
  `ChatPanel` posts to `/api/chat` on the same origin by default, which is what
package/dist/canvas.cjs CHANGED
@@ -44,8 +44,80 @@ var import_core2 = require("@mjml-agent-editor/core");
44
44
 
45
45
  // src/stores.tsx
46
46
  var import_react = require("react");
47
+
48
+ // src/labels.ts
49
+ var DEFAULT_LABELS = {
50
+ appName: "MJML Editor",
51
+ emailNameAria: "Email name",
52
+ renameFailed: "Failed to rename.",
53
+ saving: "Saving\u2026",
54
+ saved: "Saved",
55
+ saveFailed: "Save failed",
56
+ undo: "Undo",
57
+ redo: "Redo",
58
+ contentWidth: "Content width",
59
+ preview: "Preview",
60
+ closePreview: "Close preview",
61
+ previewFrameTitle: "Email preview",
62
+ sourceCode: "Source code",
63
+ exportHtml: "Export HTML",
64
+ exportMjml: "Export MJML",
65
+ copied: (kind) => `${kind} copied.`,
66
+ searchBlocks: "Search blocks\u2026",
67
+ expandAll: "Expand all",
68
+ selectAnElement: "Select an element\u2026",
69
+ attributes: "Attributes",
70
+ style: "Style",
71
+ rteFont: "Font",
72
+ rteSize: "Size",
73
+ rteTextColor: "Text color",
74
+ rteBackgroundColor: "Background color",
75
+ documentLoadFailed: "Failed to load the document.",
76
+ documentSaveFailed: "Failed to save changes.",
77
+ commentOnSection: "Comment on the whole section",
78
+ closeComments: "Close",
79
+ noCommentsYet: "No comments yet \u2014 add the first one.",
80
+ resolveComment: "Resolve",
81
+ addComment: "Add",
82
+ addCommentPlaceholder: "Add a comment\u2026 (\u2318/Ctrl+Enter to send)",
83
+ chatEmptyTitle: "Start a conversation with the agent",
84
+ chatEmptyDescription: "Describe the email (goal, tone, content) and paste your data \u2014 the agent will design the sections and images.",
85
+ agentWorking: "Agent is working\u2026",
86
+ reasoning: "Reasoning",
87
+ completedSteps: (count, failed) => failed > 0 ? `Completed steps (${count}, ${failed} failed)` : `Completed steps (${count})`,
88
+ applyComments: "Apply changes from comments",
89
+ chatPlaceholder: "Describe what you want to change\u2026 (Enter = send, Shift+Enter = new line)",
90
+ stop: "Stop",
91
+ send: "Send",
92
+ chatError: "Agent chat error.",
93
+ toolLabels: {
94
+ get_document: "Reading document",
95
+ get_section: "Reading section",
96
+ set_document: "Saving whole email",
97
+ set_section: "Replacing section",
98
+ insert_section: "Adding section",
99
+ remove_section: "Removing section",
100
+ generate_image: "Generating image",
101
+ list_open_comments: "Reading comments",
102
+ resolve_comment: "Resolving comment"
103
+ }
104
+ };
105
+ function mergeLabels(overrides) {
106
+ if (!overrides) return DEFAULT_LABELS;
107
+ return {
108
+ ...DEFAULT_LABELS,
109
+ ...overrides,
110
+ toolLabels: { ...DEFAULT_LABELS.toolLabels, ...overrides.toolLabels }
111
+ };
112
+ }
113
+
114
+ // src/stores.tsx
47
115
  var import_jsx_runtime = require("react/jsx-runtime");
48
116
  var StoreContext = (0, import_react.createContext)(null);
117
+ var LabelContext = (0, import_react.createContext)(null);
118
+ function useLabels() {
119
+ return (0, import_react.useContext)(LabelContext) ?? mergeLabels();
120
+ }
49
121
  function useStores() {
50
122
  const stores = (0, import_react.useContext)(StoreContext);
51
123
  if (!stores) {
@@ -223,6 +295,7 @@ function CanvasComments({
223
295
  onOpenCountChange
224
296
  }) {
225
297
  const commentStore = useCommentStore();
298
+ const labels = useLabels();
226
299
  const editor = (0, import_react2.useEditorMaybe)();
227
300
  const containerRef = (0, import_react3.useRef)(null);
228
301
  const [comments, setComments] = (0, import_react3.useState)([]);
@@ -411,7 +484,7 @@ function CanvasComments({
411
484
  " ",
412
485
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "font-medium text-foreground", children: activeTarget.objectLabel })
413
486
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
414
- "Comment on the whole section",
487
+ labels.commentOnSection,
415
488
  " ",
416
489
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("code", { className: "font-mono", children: activeTarget?.sectionId })
417
490
  ] }) }),
@@ -420,14 +493,14 @@ function CanvasComments({
420
493
  {
421
494
  type: "button",
422
495
  onClick: closePopover,
423
- "aria-label": "Close",
496
+ "aria-label": labels.closeComments,
424
497
  className: "-mr-1 -mt-0.5 shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
425
498
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.X, { className: "size-4" })
426
499
  }
427
500
  )
428
501
  ] }),
429
502
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "max-h-64 space-y-2 overflow-y-auto p-3", children: [
430
- activeComments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-xs text-muted-foreground", children: "No comments yet \u2014 add the first one." }),
503
+ activeComments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-xs text-muted-foreground", children: labels.noCommentsYet }),
431
504
  activeComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "rounded-md border bg-muted/40 p-2 text-sm", children: [
432
505
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "whitespace-pre-wrap", children: c.body }),
433
506
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
@@ -437,7 +510,8 @@ function CanvasComments({
437
510
  className: "mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground",
438
511
  children: [
439
512
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.Check, { className: "size-3" }),
440
- " Resolve"
513
+ " ",
514
+ labels.resolveComment
441
515
  ]
442
516
  }
443
517
  )
@@ -450,7 +524,7 @@ function CanvasComments({
450
524
  autoFocus: true,
451
525
  value: body,
452
526
  onChange: (e) => setBody(e.target.value),
453
- placeholder: "Add a comment\u2026 (\u2318/Ctrl+Enter to send)",
527
+ placeholder: labels.addCommentPlaceholder,
454
528
  className: "min-h-14 resize-none text-sm",
455
529
  onKeyDown: (e) => {
456
530
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
@@ -462,7 +536,8 @@ function CanvasComments({
462
536
  ),
463
537
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "mt-2 flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Button, { size: "sm", disabled: !body.trim(), onClick: () => void submit(), children: [
464
538
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.MessageSquarePlus, {}),
465
- " Add"
539
+ " ",
540
+ labels.addComment
466
541
  ] }) })
467
542
  ] })
468
543
  ]
@@ -603,13 +678,13 @@ function wrapSelectionStyle(el, style) {
603
678
  } catch {
604
679
  }
605
680
  }
606
- function setupRichText(editor) {
681
+ function setupRichText(editor, labels) {
607
682
  const rte = editor.RichTextEditor;
608
683
  if (!rte || rte.__customActions) return;
609
684
  rte.__customActions = true;
610
685
  const val = (action, sel) => action.btn?.querySelector(sel)?.value ?? "";
611
686
  rte.add("fontName", {
612
- icon: `<select class="gjs-rte-select" title="Font"><option value="">Font</option>${RTE_FONTS.map(
687
+ icon: `<select class="gjs-rte-select" title="${labels.rteFont}"><option value="">${labels.rteFont}</option>${RTE_FONTS.map(
613
688
  (f) => `<option value="${f}">${f}</option>`
614
689
  ).join("")}</select>`,
615
690
  event: "change",
@@ -619,7 +694,7 @@ function setupRichText(editor) {
619
694
  }
620
695
  });
621
696
  rte.add("fontSize", {
622
- icon: `<select class="gjs-rte-select" title="Size"><option value="">Size</option>${RTE_SIZES.map(
697
+ icon: `<select class="gjs-rte-select" title="${labels.rteSize}"><option value="">${labels.rteSize}</option>${RTE_SIZES.map(
623
698
  (s) => `<option value="${s}px">${s}</option>`
624
699
  ).join("")}</select>`,
625
700
  event: "change",
@@ -629,18 +704,19 @@ function setupRichText(editor) {
629
704
  }
630
705
  });
631
706
  rte.add("forecolor", {
632
- icon: `<input type="color" class="gjs-rte-color" title="Text color" value="#000000">`,
707
+ icon: `<input type="color" class="gjs-rte-color" title="${labels.rteTextColor}" value="#000000">`,
633
708
  event: "change",
634
709
  result: (r, action) => r.exec("foreColor", val(action, "input"))
635
710
  });
636
711
  rte.add("hilitecolor", {
637
- icon: `<input type="color" class="gjs-rte-color" title="Background color" value="#ffff00">`,
712
+ icon: `<input type="color" class="gjs-rte-color" title="${labels.rteBackgroundColor}" value="#ffff00">`,
638
713
  event: "change",
639
714
  result: (r, action) => r.exec("hiliteColor", val(action, "input"))
640
715
  });
641
716
  }
642
717
  function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
643
718
  const documents = useDocumentStore();
719
+ const labels = useLabels();
644
720
  const loadingRef = (0, import_react5.useRef)(false);
645
721
  const saveTimer = (0, import_react5.useRef)(null);
646
722
  const saveStatusRef = (0, import_react5.useRef)("idle");
@@ -720,7 +796,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
720
796
  if (target) setComposeTarget(target);
721
797
  }
722
798
  });
723
- setupRichText(editor);
799
+ setupRichText(editor, labels);
724
800
  const blockCategory = (label) => {
725
801
  const l = label.toLowerCase();
726
802
  if (/(column|section)/.test(l)) return "Layout";
@@ -774,7 +850,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
774
850
  setSave("saved");
775
851
  } catch (e) {
776
852
  setSave("error");
777
- import_sonner.toast.error("Failed to save changes.");
853
+ import_sonner.toast.error(labels.documentSaveFailed);
778
854
  throw e;
779
855
  }
780
856
  };
@@ -832,7 +908,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
832
908
  const doc = await documents.get(docId);
833
909
  loadMjml(doc.mjml);
834
910
  } catch (e) {
835
- import_sonner.toast.error("Failed to load the document.");
911
+ import_sonner.toast.error(labels.documentLoadFailed);
836
912
  console.error(e);
837
913
  } finally {
838
914
  setLoading(false);
@@ -969,6 +1045,7 @@ function LeftSidebar({
969
1045
  markLayersSource
970
1046
  }) {
971
1047
  const editor = (0, import_react4.useEditorMaybe)();
1048
+ const labels = useLabels();
972
1049
  const [layersExpanded, setLayersExpanded] = (0, import_react5.useState)(false);
973
1050
  const [blockSearch, setBlockSearch] = (0, import_react5.useState)("");
974
1051
  const changeView = (v) => {
@@ -1026,7 +1103,7 @@ function LeftSidebar({
1026
1103
  {
1027
1104
  value: blockSearch,
1028
1105
  onChange: (e) => filterBlocks(e.target.value),
1029
- placeholder: "Search blocks\u2026",
1106
+ placeholder: labels.searchBlocks,
1030
1107
  className: "h-8 border-panel-border bg-panel-elevated pr-10 pl-7 text-sm text-panel-fg placeholder:text-panel-muted-fg"
1031
1108
  }
1032
1109
  ),
@@ -1035,8 +1112,8 @@ function LeftSidebar({
1035
1112
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
1036
1113
  ] }),
1037
1114
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: [
1038
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CollapseSection, { title: "Attributes", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-traits" }) }),
1039
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CollapseSection, { title: "Style", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-styles" }) })
1115
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CollapseSection, { title: labels.attributes, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-traits" }) }),
1116
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CollapseSection, { title: labels.style, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-styles" }) })
1040
1117
  ] }),
1041
1118
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1042
1119
  "div",
@@ -1045,7 +1122,7 @@ function LeftSidebar({
1045
1122
  onMouseDownCapture: markLayersSource,
1046
1123
  children: [
1047
1124
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "flex items-center justify-between gap-2 border-b border-panel-border px-3 py-2 text-xs text-panel-muted-fg", children: [
1048
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: "Expand all" }),
1125
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: labels.expandAll }),
1049
1126
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1050
1127
  Switch,
1051
1128
  {
@@ -1065,7 +1142,8 @@ function Breadcrumb({
1065
1142
  crumbs,
1066
1143
  onSelect
1067
1144
  }) {
1068
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex h-9 shrink-0 items-center gap-0.5 border-t border-border bg-surface px-3 text-xs text-muted-foreground", children: crumbs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-muted-foreground/60", children: "Select an element\u2026" }) : crumbs.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "flex items-center gap-0.5", children: [
1145
+ const labels = useLabels();
1146
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex h-9 shrink-0 items-center gap-0.5 border-t border-border bg-surface px-3 text-xs text-muted-foreground", children: crumbs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-muted-foreground/60", children: labels.selectAnElement }) : crumbs.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "flex items-center gap-0.5", children: [
1069
1147
  i > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ChevronRight, { className: "size-3 opacity-40" }),
1070
1148
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1071
1149
  "button",