@mjml-agent-editor/editor 0.2.0 → 0.4.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/canvas.cjs CHANGED
@@ -31,16 +31,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/canvas.ts
32
32
  var canvas_exports = {};
33
33
  __export(canvas_exports, {
34
- EmailEditor: () => EmailEditor
34
+ EmailEditor: () => EmailEditor,
35
+ blockProblems: () => blockProblems
35
36
  });
36
37
  module.exports = __toCommonJS(canvas_exports);
37
38
 
38
39
  // src/components/editor/EmailEditor.tsx
39
- var import_grapesjs = __toESM(require("grapesjs"), 1);
40
+ var import_grapesjs2 = __toESM(require("grapesjs"), 1);
40
41
  var import_react9 = __toESM(require("@grapesjs/react"), 1);
41
- var import_grapesjs_mjml = __toESM(require("grapesjs-mjml"), 1);
42
+ var import_grapesjs_mjml2 = __toESM(require("grapesjs-mjml"), 1);
42
43
  var import_react10 = require("react");
43
- var import_core2 = require("@mjml-agent-editor/core");
44
+ var import_core4 = require("@mjml-agent-editor/core");
44
45
 
45
46
  // src/stores.tsx
46
47
  var import_react = require("react");
@@ -68,8 +69,10 @@ var DEFAULT_LABELS = {
68
69
  tabLayers: "Layers",
69
70
  searchBlocks: "Search blocks\u2026",
70
71
  noBlocksMatch: "No blocks match.",
72
+ blockAdded: (block) => `Added: ${block}`,
71
73
  blockCategoryLayout: "Layout",
72
74
  blockCategoryContent: "Content",
75
+ blockCategorySections: "Sections",
73
76
  blockLabels: {
74
77
  "mj-1-column": "1 column",
75
78
  "mj-2-columns": "2 columns",
@@ -97,7 +100,8 @@ var DEFAULT_LABELS = {
97
100
  "mj-navbar-link": "Navigation link",
98
101
  "mj-social": "Social links",
99
102
  "mj-social-element": "Social link",
100
- "mj-raw": "Raw HTML"
103
+ "mj-raw": "Raw HTML",
104
+ "mj-table": "Table"
101
105
  },
102
106
  expandAll: "Expand all",
103
107
  selectAnElement: "Select an element\u2026",
@@ -937,6 +941,30 @@ function CanvasComments({
937
941
  ] });
938
942
  }
939
943
 
944
+ // src/lib/save-baseline.ts
945
+ function createSaveBaseline() {
946
+ let stored = "";
947
+ let loaded = null;
948
+ return {
949
+ /** The MJML the store holds, as far as this editor knows. */
950
+ get stored() {
951
+ return stored;
952
+ },
953
+ /** `canvas` is what the load serialises to, or null when it repaired something to write. */
954
+ load(mjml, canvas) {
955
+ stored = mjml;
956
+ loaded = canvas;
957
+ },
958
+ saved(mjml) {
959
+ stored = mjml;
960
+ loaded = null;
961
+ },
962
+ changed(mjml) {
963
+ return mjml !== stored && mjml !== loaded;
964
+ }
965
+ };
966
+ }
967
+
940
968
  // src/lib/save-queue.ts
941
969
  function createSaveQueue(save, delayMs) {
942
970
  let timer = null;
@@ -966,6 +994,39 @@ function createSaveQueue(save, delayMs) {
966
994
  };
967
995
  }
968
996
 
997
+ // src/lib/turn-follower.ts
998
+ function createTurnFollower() {
999
+ let running = false;
1000
+ let mounted = true;
1001
+ let apply = null;
1002
+ return {
1003
+ turn(on) {
1004
+ if (on === running) return;
1005
+ running = on;
1006
+ if (mounted) apply?.(on);
1007
+ },
1008
+ /**
1009
+ * Whether the canvas is still mounted. An unmounted one's late reload must not unlock
1010
+ * the shared state a canvas mounted since has locked.
1011
+ */
1012
+ get mounted() {
1013
+ return mounted;
1014
+ },
1015
+ // Both, as React mounts twice in development.
1016
+ mount() {
1017
+ mounted = true;
1018
+ },
1019
+ unmount() {
1020
+ mounted = false;
1021
+ },
1022
+ /** A turn already under way is applied at once; none means nothing to apply. */
1023
+ ready(fn) {
1024
+ apply = fn;
1025
+ if (running) fn(true);
1026
+ }
1027
+ };
1028
+ }
1029
+
969
1030
  // src/lib/shortcuts.ts
970
1031
  function opensBlockSearch(e) {
971
1032
  if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey || e.isComposing) return false;
@@ -1063,7 +1124,8 @@ function ImagePicker({
1063
1124
  const accepts = (file) => matchesAccept(file, library?.accept);
1064
1125
  const full = quota !== void 0 && quota.used >= quota.limit;
1065
1126
  const upload = async (files) => {
1066
- if (!library || !files?.length) return;
1127
+ const send = library?.upload?.bind(library);
1128
+ if (!send || !files?.length) return;
1067
1129
  const mine = session.current;
1068
1130
  const list = Array.from(files).filter((f) => {
1069
1131
  if (accepts(f)) return true;
@@ -1072,7 +1134,7 @@ function ImagePicker({
1072
1134
  });
1073
1135
  if (!list.length) return;
1074
1136
  setPending((n) => n + list.length);
1075
- const results = await Promise.allSettled(list.map(async (f) => library.upload(f)));
1137
+ const results = await Promise.allSettled(list.map(async (f) => send(f)));
1076
1138
  setPending((n) => n - list.length);
1077
1139
  for (const r of results) {
1078
1140
  if (r.status === "rejected") {
@@ -1107,155 +1169,163 @@ function ImagePicker({
1107
1169
  setDragging(false);
1108
1170
  if (!full) void upload(e.dataTransfer.files);
1109
1171
  };
1110
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(DialogContent, { className: "editor-dark gap-0 overflow-hidden border border-panel-border bg-panel p-0 text-panel-fg ring-0 sm:max-w-3xl", children: [
1111
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(DialogHeader, { className: "border-b border-panel-border px-5 py-4", children: [
1112
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DialogTitle, { children: labels.imagePickerTitle }),
1113
- quota && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DialogDescription, { className: "text-panel-muted-fg", children: labels.imagePickerQuota(quota.used, quota.limit) })
1114
- ] }),
1115
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex max-h-[70vh] flex-col gap-4 overflow-y-auto p-5", children: [
1116
- library && (full ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "rounded-lg border border-panel-border bg-panel-elevated px-4 py-3 text-sm text-panel-muted-fg", children: labels.imagePickerQuotaFull }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1117
- "div",
1118
- {
1119
- onDragOver: (e) => {
1120
- e.preventDefault();
1121
- setDragging(true);
1122
- },
1123
- onDragLeave: () => setDragging(false),
1124
- onDrop,
1125
- className: cn(
1126
- "flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed px-6 py-8 text-center transition-colors",
1127
- dragging ? "border-brand bg-brand-soft" : "border-panel-border bg-panel-elevated"
1128
- ),
1129
- children: [
1130
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.ImageUp, { className: "size-8 text-panel-muted-fg" }),
1131
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
1132
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: labels.imagePickerDrop }),
1133
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1134
- Button,
1135
- {
1136
- type: "button",
1137
- size: "sm",
1138
- className: "bg-brand text-brand-fg hover:bg-brand/90",
1139
- onClick: () => fileInput.current?.click(),
1140
- children: labels.imagePickerBrowse
1141
- }
1142
- )
1143
- ] }),
1144
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1145
- "input",
1146
- {
1147
- ref: fileInput,
1148
- type: "file",
1149
- accept: library.accept ?? "image/*",
1150
- multiple: true,
1151
- hidden: true,
1152
- onChange: (e) => {
1153
- void upload(e.target.files);
1154
- e.target.value = "";
1155
- }
1156
- }
1157
- )
1158
- ]
1159
- }
1160
- )),
1161
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1162
- "form",
1163
- {
1164
- className: "flex gap-2",
1165
- onSubmit: (e) => {
1166
- e.preventDefault();
1167
- const value = url.trim();
1168
- if (value) onSelect(value);
1169
- },
1170
- children: [
1171
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "relative flex-1", children: [
1172
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Link2, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-panel-muted-fg" }),
1173
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1174
- Input,
1175
- {
1176
- value: url,
1177
- onChange: (e) => setUrl(e.target.value),
1178
- placeholder: labels.imagePickerUrlPlaceholder,
1179
- className: "h-9 border-panel-border bg-panel-elevated pl-8 text-panel-fg placeholder:text-panel-muted-fg"
1180
- }
1181
- )
1182
- ] }),
1183
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1184
- Button,
1185
- {
1186
- type: "submit",
1187
- size: "lg",
1188
- variant: "outline",
1189
- disabled: !url.trim(),
1190
- className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
1191
- children: labels.imagePickerUseUrl
1192
- }
1193
- )
1194
- ]
1195
- }
1196
- ),
1197
- library && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { children: [
1198
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
1199
- loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
1200
- Array.from({ length: pending }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1201
- "li",
1172
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1173
+ DialogContent,
1174
+ {
1175
+ onDragOver: (e) => e.preventDefault(),
1176
+ onDrop: (e) => e.preventDefault(),
1177
+ className: "editor-dark gap-0 overflow-hidden border border-panel-border bg-panel p-0 text-panel-fg ring-0 sm:max-w-3xl",
1178
+ children: [
1179
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(DialogHeader, { className: "border-b border-panel-border px-5 py-4", children: [
1180
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DialogTitle, { children: labels.imagePickerTitle }),
1181
+ quota && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DialogDescription, { className: "text-panel-muted-fg", children: labels.imagePickerQuota(quota.used, quota.limit) })
1182
+ ] }),
1183
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex max-h-[70vh] flex-col gap-4 overflow-y-auto p-5", children: [
1184
+ library?.upload && (full ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "rounded-lg border border-panel-border bg-panel-elevated px-4 py-3 text-sm text-panel-muted-fg", children: labels.imagePickerQuotaFull }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1185
+ "div",
1202
1186
  {
1203
- className: "flex aspect-[4/3] items-center justify-center rounded-lg border border-panel-border bg-panel-elevated",
1204
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {})
1205
- },
1206
- `pending-${i}`
1187
+ onDragOver: (e) => {
1188
+ e.preventDefault();
1189
+ setDragging(true);
1190
+ },
1191
+ onDragLeave: () => setDragging(false),
1192
+ onDrop,
1193
+ className: cn(
1194
+ "flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed px-6 py-8 text-center transition-colors",
1195
+ dragging ? "border-brand bg-brand-soft" : "border-panel-border bg-panel-elevated"
1196
+ ),
1197
+ children: [
1198
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.ImageUp, { className: "size-8 text-panel-muted-fg" }),
1199
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
1200
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: labels.imagePickerDrop }),
1201
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1202
+ Button,
1203
+ {
1204
+ type: "button",
1205
+ size: "sm",
1206
+ className: "bg-brand text-brand-fg hover:bg-brand/90",
1207
+ onClick: () => fileInput.current?.click(),
1208
+ children: labels.imagePickerBrowse
1209
+ }
1210
+ )
1211
+ ] }),
1212
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1213
+ "input",
1214
+ {
1215
+ ref: fileInput,
1216
+ type: "file",
1217
+ accept: library.accept ?? "image/*",
1218
+ multiple: true,
1219
+ hidden: true,
1220
+ onChange: (e) => {
1221
+ void upload(e.target.files);
1222
+ e.target.value = "";
1223
+ }
1224
+ }
1225
+ )
1226
+ ]
1227
+ }
1207
1228
  )),
1208
- images.map((image) => {
1209
- const selected = image.url === currentUrl;
1210
- const confirming = confirmId === image.id;
1211
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("li", { className: "group relative", children: [
1212
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1213
- "button",
1214
- {
1215
- type: "button",
1216
- onClick: () => onSelect(image.url),
1217
- title: image.name,
1218
- className: cn(
1219
- "block aspect-[4/3] w-full overflow-hidden rounded-lg border bg-black/25 transition outline-none focus-visible:ring-2 focus-visible:ring-brand",
1220
- selected ? "border-brand ring-2 ring-brand" : "border-panel-border hover:border-panel-muted-fg"
1221
- ),
1222
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1223
- "img",
1229
+ library?.allowUrl !== false && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1230
+ "form",
1231
+ {
1232
+ className: "flex gap-2",
1233
+ onSubmit: (e) => {
1234
+ e.preventDefault();
1235
+ const value = url.trim();
1236
+ if (value) onSelect(value);
1237
+ },
1238
+ children: [
1239
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "relative flex-1", children: [
1240
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Link2, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-panel-muted-fg" }),
1241
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1242
+ Input,
1224
1243
  {
1225
- src: image.url,
1226
- alt: image.name ?? "",
1227
- loading: "lazy",
1228
- className: "size-full object-contain"
1244
+ value: url,
1245
+ onChange: (e) => setUrl(e.target.value),
1246
+ placeholder: labels.imagePickerUrlPlaceholder,
1247
+ className: "h-9 border-panel-border bg-panel-elevated pl-8 text-panel-fg placeholder:text-panel-muted-fg"
1229
1248
  }
1230
1249
  )
1231
- }
1232
- ),
1233
- selected && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "pointer-events-none absolute top-1.5 left-1.5 flex size-5 items-center justify-center rounded-full bg-brand text-brand-fg", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Check, { className: "size-3" }) }),
1234
- image.name && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "pointer-events-none absolute inset-x-0 bottom-0 truncate rounded-b-lg bg-black/60 px-2 py-1 text-[11px] text-white opacity-0 transition group-hover:opacity-100", children: image.name }),
1235
- library.remove && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1236
- "button",
1250
+ ] }),
1251
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1252
+ Button,
1253
+ {
1254
+ type: "submit",
1255
+ size: "lg",
1256
+ variant: "outline",
1257
+ disabled: !url.trim(),
1258
+ className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
1259
+ children: labels.imagePickerUseUrl
1260
+ }
1261
+ )
1262
+ ]
1263
+ }
1264
+ ),
1265
+ library && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { children: [
1266
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
1267
+ loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
1268
+ Array.from({ length: pending }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1269
+ "li",
1237
1270
  {
1238
- type: "button",
1239
- onClick: () => void remove(image),
1240
- onBlur: () => confirming && setConfirmId(null),
1241
- title: confirming ? labels.imagePickerConfirmDelete : labels.imagePickerDelete,
1242
- "aria-label": labels.imagePickerDelete,
1243
- className: cn(
1244
- "absolute top-1.5 right-1.5 flex h-6 items-center gap-1 rounded-md px-1.5 text-xs text-white transition focus-visible:opacity-100",
1245
- confirming ? "bg-red-600 opacity-100" : "bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80"
1271
+ className: "flex aspect-[4/3] items-center justify-center rounded-lg border border-panel-border bg-panel-elevated",
1272
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {})
1273
+ },
1274
+ `pending-${i}`
1275
+ )),
1276
+ images.map((image) => {
1277
+ const selected = image.url === currentUrl;
1278
+ const confirming = confirmId === image.id;
1279
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("li", { className: "group relative", children: [
1280
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1281
+ "button",
1282
+ {
1283
+ type: "button",
1284
+ onClick: () => onSelect(image.url),
1285
+ title: image.name,
1286
+ className: cn(
1287
+ "block aspect-[4/3] w-full overflow-hidden rounded-lg border bg-black/25 transition outline-none focus-visible:ring-2 focus-visible:ring-brand",
1288
+ selected ? "border-brand ring-2 ring-brand" : "border-panel-border hover:border-panel-muted-fg"
1289
+ ),
1290
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1291
+ "img",
1292
+ {
1293
+ src: image.url,
1294
+ alt: image.name ?? "",
1295
+ loading: "lazy",
1296
+ className: "size-full object-contain"
1297
+ }
1298
+ )
1299
+ }
1246
1300
  ),
1247
- children: [
1248
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Trash2, { className: "size-3.5" }),
1249
- confirming && labels.imagePickerConfirmDelete
1250
- ]
1251
- }
1252
- )
1253
- ] }, image.id);
1254
- })
1301
+ selected && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "pointer-events-none absolute top-1.5 left-1.5 flex size-5 items-center justify-center rounded-full bg-brand text-brand-fg", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Check, { className: "size-3" }) }),
1302
+ image.name && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "pointer-events-none absolute inset-x-0 bottom-0 truncate rounded-b-lg bg-black/60 px-2 py-1 text-[11px] text-white opacity-0 transition group-hover:opacity-100", children: image.name }),
1303
+ library.remove && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1304
+ "button",
1305
+ {
1306
+ type: "button",
1307
+ onClick: () => void remove(image),
1308
+ onBlur: () => confirming && setConfirmId(null),
1309
+ title: confirming ? labels.imagePickerConfirmDelete : labels.imagePickerDelete,
1310
+ "aria-label": labels.imagePickerDelete,
1311
+ className: cn(
1312
+ "absolute top-1.5 right-1.5 flex h-6 items-center gap-1 rounded-md px-1.5 text-xs text-white transition focus-visible:opacity-100",
1313
+ confirming ? "bg-red-600 opacity-100" : "bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80"
1314
+ ),
1315
+ children: [
1316
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Trash2, { className: "size-3.5" }),
1317
+ confirming && labels.imagePickerConfirmDelete
1318
+ ]
1319
+ }
1320
+ )
1321
+ ] }, image.id);
1322
+ })
1323
+ ] })
1324
+ ] })
1255
1325
  ] })
1256
- ] })
1257
- ] })
1258
- ] }) });
1326
+ ]
1327
+ }
1328
+ ) });
1259
1329
  }
1260
1330
  function matchesAccept(file, accept) {
1261
1331
  if (!accept) return file.type.startsWith("image/");
@@ -1282,6 +1352,7 @@ var import_react5 = require("react");
1282
1352
  var import_lucide_react5 = require("lucide-react");
1283
1353
 
1284
1354
  // src/components/editor/inspector/values.ts
1355
+ var import_core2 = require("@mjml-agent-editor/core");
1285
1356
  var SIDES = ["top", "right", "bottom", "left"];
1286
1357
  function expandBox(value) {
1287
1358
  const parts = value.trim().split(/\s+/).filter(Boolean);
@@ -1312,15 +1383,6 @@ function parseNetwork(name) {
1312
1383
  function networkName(network, share) {
1313
1384
  return share || network === "custom" ? network : `${network}${NO_SHARE}`;
1314
1385
  }
1315
- var LINK_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel", "sms"]);
1316
- function isSafeLink(value) {
1317
- const decoded = value.replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/:/g, ":").replace(/&(Tab|NewLine);/g, "").replace(/[\u0000-\u0020]/g, "");
1318
- const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(decoded)?.[1];
1319
- return !scheme || LINK_SCHEMES.has(scheme.toLowerCase());
1320
- }
1321
- function isSafeNavbarLink(baseUrl, href) {
1322
- return isSafeLink(baseUrl) && isSafeLink(href) && isSafeLink(baseUrl + href);
1323
- }
1324
1386
 
1325
1387
  // src/components/editor/inspector/controls.tsx
1326
1388
  var import_jsx_runtime12 = require("react/jsx-runtime");
@@ -2126,6 +2188,23 @@ function withoutEditorProps(mjml) {
2126
2188
  )
2127
2189
  );
2128
2190
  }
2191
+ function withClosedTags(mjml) {
2192
+ return mjml.replace(START_TAG, (tag) => {
2193
+ const selfClosed = /^<(mj-[\w-]+)([\s\S]*?)\s*\/>$/i.exec(tag);
2194
+ return selfClosed ? `<${selfClosed[1]}${selfClosed[2]}></${selfClosed[1]}>` : tag;
2195
+ });
2196
+ }
2197
+ var TABLE = /<!--[\s\S]*?(?:-->|$)|(<mj-table\b(?:[^>"']|"[^"]*"|'[^']*')*>)((?:<!--[\s\S]*?-->|(?!<!--)[\s\S])*?)(<\/mj-table\s*>)/gi;
2198
+ function withTableRowsKept(mjml) {
2199
+ return mjml.replace(
2200
+ TABLE,
2201
+ (match, open, rows, close) => open ? `${open}<template>${rows}</template>${close}` : match
2202
+ );
2203
+ }
2204
+ function sourceForEditor(mjml) {
2205
+ const cleaned = withoutEditorProps(mjml);
2206
+ return { mjml: withTableRowsKept(withClosedTags(cleaned)), repaired: cleaned !== mjml };
2207
+ }
2129
2208
  function illegalAttributes(tag, attributes) {
2130
2209
  const allowed = ALLOWED_ATTRIBUTES[tag];
2131
2210
  if (!allowed) return [];
@@ -3002,7 +3081,10 @@ var ICONS = {
3002
3081
  "mj-navbar-link": svg(
3003
3082
  '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>'
3004
3083
  ),
3005
- "mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>')
3084
+ "mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>'),
3085
+ "mj-table": svg(
3086
+ '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M12 3v18"/>'
3087
+ )
3006
3088
  };
3007
3089
  function tagOf(c) {
3008
3090
  return String(c.get("tagName") || c.get("type") || "");
@@ -3465,12 +3547,12 @@ function ElementSettings({
3465
3547
  };
3466
3548
  const safeLink = (attr) => (link) => {
3467
3549
  if (tag === "mj-navbar-link" && attr === "href") {
3468
- return isSafeNavbarLink(String(component.parent()?.getAttributes()["base-url"] ?? ""), link);
3550
+ return (0, import_core2.isSafeNavbarLink)(String(component.parent()?.getAttributes()["base-url"] ?? ""), link);
3469
3551
  }
3470
3552
  if (tag === "mj-navbar" && attr === "base-url") {
3471
- return component.components().every((c) => isSafeNavbarLink(link, String(c.getAttributes().href ?? "")));
3553
+ return component.components().every((c) => (0, import_core2.isSafeNavbarLink)(link, String(c.getAttributes().href ?? "")));
3472
3554
  }
3473
- return isSafeLink(link);
3555
+ return (0, import_core2.isSafeLink)(link);
3474
3556
  };
3475
3557
  const control = (f) => {
3476
3558
  const value = attributes[f.attr] ?? "";
@@ -3778,8 +3860,15 @@ function Header({
3778
3860
  ] });
3779
3861
  }
3780
3862
 
3863
+ // src/components/editor/blocks.ts
3864
+ var import_grapesjs = __toESM(require("grapesjs"), 1);
3865
+ var import_grapesjs_mjml = __toESM(require("grapesjs-mjml"), 1);
3866
+
3781
3867
  // src/components/editor/ids.ts
3782
- var ID_RE = { sec: /\bsec-([A-Za-z0-9_-]+)\b/, obj: /\bobj-([A-Za-z0-9_-]+)\b/ };
3868
+ var ID_RE = {
3869
+ sec: /(?:^|\s)sec-([A-Za-z0-9_-]+)(?=\s|$)/,
3870
+ obj: /(?:^|\s)obj-([A-Za-z0-9_-]+)(?=\s|$)/
3871
+ };
3783
3872
  function idOf(c, prefix) {
3784
3873
  return String(c.getAttributes()["css-class"] ?? "").match(ID_RE[prefix])?.[1] ?? null;
3785
3874
  }
@@ -3808,19 +3897,292 @@ function freshenCopiedIds(root, added) {
3808
3897
  const id = idOf(c, prefix);
3809
3898
  if (!id || !used(prefix, id, c)) continue;
3810
3899
  const cssClass = String(c.getAttributes()["css-class"]);
3811
- c.addAttributes({ "css-class": cssClass.replace(`${prefix}-${id}`, `${prefix}-${newId()}`) });
3900
+ const fresh = cssClass.replace(
3901
+ ID_RE[prefix],
3902
+ (token) => token.replace(`${prefix}-${id}`, `${prefix}-${newId()}`)
3903
+ );
3904
+ c.addAttributes({ "css-class": fresh });
3905
+ }
3906
+ });
3907
+ }
3908
+
3909
+ // src/components/editor/insert-block.ts
3910
+ var addsBlock = (e) => (e.key === "Enter" || e.key === " ") && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey;
3911
+ var tileAttributes = (id) => ({ tabindex: "0", role: "button", "data-block": id });
3912
+ var tagOf5 = (c) => String(c.get("tagName") ?? "");
3913
+ function closest(c, tag) {
3914
+ for (let cur = c; cur; cur = cur.parent() ?? void 0) if (tagOf5(cur) === tag) return cur;
3915
+ return void 0;
3916
+ }
3917
+ function childOf(parent, c) {
3918
+ let cur = c;
3919
+ while (cur.parent() && cur.parent() !== parent) cur = cur.parent();
3920
+ return cur;
3921
+ }
3922
+ function insertBlock(editor, block) {
3923
+ const content = block.getContent();
3924
+ let body;
3925
+ editor.getWrapper()?.onAll((c) => {
3926
+ if (!body && tagOf5(c) === "mj-body") body = c;
3927
+ });
3928
+ if (typeof content !== "string" || !body) return void 0;
3929
+ const selected = editor.getSelected();
3930
+ const section = closest(selected, "mj-section");
3931
+ const sections = section?.parent() ?? body;
3932
+ const afterSection = section ? { at: section.index() + 1 } : {};
3933
+ const column = closest(selected, "mj-column");
3934
+ let added;
3935
+ if (/^\s*<mj-section\b/i.test(content)) {
3936
+ added = sections.append(content, afterSection)[0];
3937
+ } else if (column && selected) {
3938
+ const at = selected === column ? {} : { at: childOf(column, selected).index() + 1 };
3939
+ added = column.append(content, at)[0];
3940
+ } else {
3941
+ const [wrapper] = sections.append(
3942
+ `<mj-section><mj-column>${content}</mj-column></mj-section>`,
3943
+ afterSection
3944
+ );
3945
+ added = wrapper?.components().at(0)?.components().at(0);
3946
+ }
3947
+ if (!added) return void 0;
3948
+ editor.select(added);
3949
+ if (block.get("activate")) added.getView()?.onActive(void 0);
3950
+ return added;
3951
+ }
3952
+
3953
+ // src/components/editor/blocks.ts
3954
+ var EDITABLE_TAGS = /* @__PURE__ */ new Set([
3955
+ "mj-section",
3956
+ "mj-group",
3957
+ "mj-column",
3958
+ "mj-text",
3959
+ "mj-image",
3960
+ "mj-button",
3961
+ "mj-divider",
3962
+ "mj-spacer",
3963
+ "mj-social",
3964
+ "mj-social-element",
3965
+ "mj-navbar",
3966
+ "mj-navbar-link"
3967
+ ]);
3968
+ var CONTENT_TAGS = /* @__PURE__ */ new Set(["mj-text", "mj-button", "mj-social-element", "mj-navbar-link"]);
3969
+ var EDITOR_INTERNALS = /data-gjs-|__p\b|custom-name/i;
3970
+ var DROPPED_MARKUP = /<(?:script|style|link|meta|iframe|object|embed|form)\b/i;
3971
+ var INLINE_IMAGE = /\b(?:src|srcset|background-url|icon)\s*=\s*(?:"[^"]*data:|'[^']*data:|data:)/i;
3972
+ var LINK_IN_MARKUP = /[\s/"'](?:xlink:)?(?:href|action|formaction)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
3973
+ var validator;
3974
+ function blockProblems(mjml, editor = validator ??= import_grapesjs.default.init({
3975
+ headless: true,
3976
+ storageManager: false,
3977
+ telemetry: false,
3978
+ plugins: [import_grapesjs_mjml.default]
3979
+ })) {
3980
+ if (EDITOR_INTERNALS.test(mjml)) return ["it contains GrapesJS's own attributes"];
3981
+ if (INLINE_IMAGE.test(mjml)) return ["it inlines an image as a data: URL"];
3982
+ let tree;
3983
+ try {
3984
+ tree = editor.runCommand("mjml-code-to-html", {
3985
+ mjml: `<mjml><mj-body>${withClosedTags(mjml)}</mj-body></mjml>`,
3986
+ validationLevel: "strict"
3987
+ }).json;
3988
+ } catch (e) {
3989
+ const errors = e.errors;
3990
+ if (!errors) throw e;
3991
+ return errors.map((error) => error.formattedMessage ?? error.message ?? String(error));
3992
+ }
3993
+ const problems = [];
3994
+ const top = tree.children?.find((c) => c.tagName === "mj-body")?.children ?? [];
3995
+ if (top.length !== 1 || top[0].tagName !== "mj-section") {
3996
+ problems.push("it is not exactly one mj-section");
3997
+ }
3998
+ const visit = (node, parent) => {
3999
+ const tag = node.tagName;
4000
+ const attributes = node.attributes ?? {};
4001
+ const content = node.content ?? "";
4002
+ if (!EDITABLE_TAGS.has(tag)) problems.push(`${tag} is not editable here`);
4003
+ const cssClass = attributes["css-class"] ?? "";
4004
+ if (ID_RE.sec.test(cssClass) || ID_RE.obj.test(cssClass)) {
4005
+ problems.push(`${tag} carries a sec-* or obj-* id`);
4006
+ }
4007
+ if ("mj-class" in attributes) problems.push(`${tag} uses mj-class`);
4008
+ const links = [attributes.href ?? ""];
4009
+ for (const match of content.matchAll(LINK_IN_MARKUP)) {
4010
+ links.push(match[1] ?? match[2] ?? match[3] ?? "");
4011
+ }
4012
+ const linked = tag === "mj-navbar-link" ? (0, import_core2.isSafeNavbarLink)(parent?.attributes?.["base-url"] ?? "", attributes.href ?? "") : true;
4013
+ if (!linked || !links.every(import_core2.isSafeLink)) {
4014
+ problems.push(`${tag} has a link scheme mail must not carry`);
4015
+ }
4016
+ if (CONTENT_TAGS.has(tag) ? /<mj-/i.test(content) || DROPPED_MARKUP.test(content) : content.trim()) {
4017
+ problems.push(`${tag} holds content the editor does not keep`);
4018
+ }
4019
+ node.children?.forEach((child) => visit(child, node));
4020
+ };
4021
+ top.forEach((node) => visit(node));
4022
+ return problems;
4023
+ }
4024
+ var escapeHtml3 = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4025
+ function registerBlocks(editor, blocks, category, media) {
4026
+ for (const block of blocks) {
4027
+ const problems = editor.Blocks.get(block.id) ? [`the id is already taken`] : blockProblems(block.mjml, editor);
4028
+ if (problems.length > 0) {
4029
+ console.error(`Block "${block.id}" is left out of the palette: ${problems.join("; ")}`);
4030
+ continue;
4031
+ }
4032
+ editor.Blocks.add(block.id, {
4033
+ // GrapesJS writes the label into the tile as markup.
4034
+ label: escapeHtml3(block.label),
4035
+ category,
4036
+ media: block.media ?? media,
4037
+ content: withClosedTags(block.mjml),
4038
+ attributes: tileAttributes(block.id)
4039
+ });
4040
+ }
4041
+ }
4042
+
4043
+ // src/components/editor/builtin-blocks.ts
4044
+ var BUILTIN_BLOCKS = [
4045
+ "mj-1-column",
4046
+ "mj-2-columns",
4047
+ "mj-3-columns",
4048
+ "mj-text",
4049
+ "mj-image",
4050
+ "mj-button",
4051
+ "mj-divider",
4052
+ "mj-spacer",
4053
+ "mj-social-group",
4054
+ "mj-navbar"
4055
+ ];
4056
+ var LAYOUT_BLOCKS = /* @__PURE__ */ new Set(["mj-1-column", "mj-2-columns", "mj-3-columns"]);
4057
+ var CONTENT2 = {
4058
+ // The plugin's version links Google+ and shares the page: a bare network name makes the
4059
+ // icon open a "share this link" dialog, while `-noshare` links to the profile a footer
4060
+ // means. Without `alt` each link is an unnamed image to a screen reader.
4061
+ "mj-social-group": `<mj-social icon-size="24px" mode="horizontal">
4062
+ <mj-social-element name="facebook-noshare" href="https://facebook.com/" alt="Facebook"></mj-social-element>
4063
+ <mj-social-element name="instagram-noshare" href="https://instagram.com/" alt="Instagram"></mj-social-element>
4064
+ <mj-social-element name="x-noshare" href="https://x.com/" alt="X"></mj-social-element>
4065
+ </mj-social>`,
4066
+ // The plugin's sample is a via.placeholder.com picture, stored in the email and loaded by
4067
+ // every reader's mail client. Dropping one opens the image picker instead.
4068
+ "mj-image": "<mj-image></mj-image>"
4069
+ };
4070
+ function builtinBlock(labels) {
4071
+ return (id) => ({
4072
+ label: labels.blockLabels[id] ?? id,
4073
+ // By id: a regex on the label broke as soon as a host translated it.
4074
+ category: LAYOUT_BLOCKS.has(id) ? labels.blockCategoryLayout : labels.blockCategoryContent,
4075
+ ...id in CONTENT2 && { content: CONTENT2[id] },
4076
+ attributes: tileAttributes(id)
4077
+ });
4078
+ }
4079
+ var CANVAS_CSS = 'img:not([src]), img[src=""] { min-height: 120px; background: #f4f4f5; outline: 1px dashed #a1a1aa; outline-offset: -1px; }';
4080
+
4081
+ // src/components/editor/table-type.ts
4082
+ var import_core3 = require("@mjml-agent-editor/core");
4083
+ var DROPPED = "script, style, link, iframe, frame, object, embed, applet, base, meta, animate, set, svg, math, template";
4084
+ var LINK_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "xlink:href", "action", "formaction"]);
4085
+ function cleanRows(rows) {
4086
+ rows.querySelectorAll(DROPPED).forEach((el) => el.remove());
4087
+ rows.querySelectorAll("*").forEach((el) => {
4088
+ for (const { name, value } of [...el.attributes]) {
4089
+ const lower = name.toLowerCase();
4090
+ const script = /^(java|vb)script:/i.test(value.replace(/[\u0000-\u0020]/g, ""));
4091
+ if (lower.startsWith("on") || script || LINK_ATTRIBUTES.has(lower) && !(0, import_core3.isSafeLink)(value)) {
4092
+ el.removeAttribute(name);
4093
+ }
4094
+ }
4095
+ });
4096
+ }
4097
+ var ROW_STYLE = "pointer-events: all; display: table; width: 100%;";
4098
+ function tablePreview(doc, attributes, rows) {
4099
+ const attr = (name, fallback) => String(attributes[name] ?? fallback);
4100
+ const template = doc.createElement("template");
4101
+ template.innerHTML = `<table>${rows}</table>`;
4102
+ const table = doc.createElement("table");
4103
+ table.setAttribute("cellpadding", String(parseInt(attr("cellpadding", "0"), 10) || 0));
4104
+ table.setAttribute("cellspacing", String(parseInt(attr("cellspacing", "0"), 10) || 0));
4105
+ Object.assign(table.style, {
4106
+ width: attr("width", "100%"),
4107
+ border: attr("border", "none"),
4108
+ color: attr("color", "#000000"),
4109
+ fontFamily: attr("font-family", "Ubuntu, Helvetica, Arial, sans-serif"),
4110
+ fontSize: attr("font-size", "13px"),
4111
+ lineHeight: attr("line-height", "22px"),
4112
+ tableLayout: attr("table-layout", "auto")
4113
+ });
4114
+ for (const row of template.content.querySelectorAll("tr")) {
4115
+ const tr = table.insertRow();
4116
+ for (const cell2 of row.querySelectorAll(":scope > th, :scope > td")) {
4117
+ const copy = doc.createElement(cell2.tagName.toLowerCase());
4118
+ copy.textContent = cell2.textContent;
4119
+ const align = cell2.getAttribute("align");
4120
+ if (align && /^(left|right|center)$/i.test(align)) copy.setAttribute("align", align);
4121
+ tr.append(copy);
4122
+ }
4123
+ }
4124
+ const cell = doc.createElement("td");
4125
+ cell.style.padding = attr("padding", "10px 25px");
4126
+ cell.append(table);
4127
+ return cell;
4128
+ }
4129
+ function addTableType(editor) {
4130
+ const base = editor.Components.getType("default").view.prototype;
4131
+ editor.Components.addType("mj-table", {
4132
+ isComponent: (el) => {
4133
+ if (el.tagName?.toLowerCase() !== "mj-table") return false;
4134
+ const rows = el.querySelector(":scope > template");
4135
+ if (rows) cleanRows(rows.content);
4136
+ return { type: "mj-table", content: rows?.innerHTML ?? "", components: [] };
4137
+ },
4138
+ model: { defaults: { tagName: "mj-table", droppable: false, editable: false } },
4139
+ view: {
4140
+ tagName: "tr",
4141
+ // GrapesJS clears the element's attributes, style among them, when the model's change.
4142
+ updateAttributes(...args) {
4143
+ base.updateAttributes.apply(this, args);
4144
+ this.el.style.cssText = ROW_STYLE;
4145
+ },
4146
+ updateContent() {
4147
+ const { el, model } = this;
4148
+ el.style.cssText = ROW_STYLE;
4149
+ el.replaceChildren(
4150
+ tablePreview(el.ownerDocument, model.getAttributes(), String(model.get("content") ?? ""))
4151
+ );
4152
+ }
3812
4153
  }
3813
4154
  });
3814
4155
  }
3815
4156
 
3816
4157
  // src/components/editor/plugin-types.ts
4158
+ var COLUMN_CHILDREN = /* @__PURE__ */ new Set([
4159
+ "mj-accordion",
4160
+ "mj-button",
4161
+ "mj-carousel",
4162
+ "mj-divider",
4163
+ "mj-image",
4164
+ "mj-navbar",
4165
+ "mj-raw",
4166
+ "mj-social",
4167
+ "mj-spacer",
4168
+ "mj-table",
4169
+ "mj-text"
4170
+ ]);
3817
4171
  function fixPluginTypes(editor) {
4172
+ addTableType(editor);
3818
4173
  for (const type of editor.Components.getTypes()) {
3819
- if (!type.id.startsWith("mj-")) continue;
4174
+ if (!type.id.startsWith("mj-") || type.id === "mj-table") continue;
3820
4175
  const base = type.model.prototype.getAttrToHTML;
3821
4176
  editor.Components.addType(type.id, {
3822
4177
  model: {
3823
- defaults: { "style-default": {} },
4178
+ defaults: {
4179
+ "style-default": {},
4180
+ ...type.id === "mj-column" && {
4181
+ droppable: (source) => COLUMN_CHILDREN.has(String(source.get("tagName")))
4182
+ },
4183
+ // GrapesJS's own is an SVG's markup, which the canvas wrote into `src` and saved.
4184
+ ...type.id === "mj-image" && { src: "" }
4185
+ },
3824
4186
  getAttrToHTML() {
3825
4187
  const attributes = base.call(this);
3826
4188
  delete attributes.__p;
@@ -3842,16 +4204,17 @@ var TYPE_LABEL = {
3842
4204
  "mj-image": "Image",
3843
4205
  "mj-column": "Column"
3844
4206
  };
3845
- function tagOf5(c) {
4207
+ function tagOf6(c) {
3846
4208
  return String(c.get("tagName") || c.get("type") || "");
3847
4209
  }
3848
4210
  var SPARKLES_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>';
3849
4211
  var IMAGE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21"/><path d="m14 19.5 3-3 3 3"/><path d="M17 22v-5.5"/><circle cx="9" cy="9" r="2"/></svg>';
4212
+ var SECTION_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="7" x="3" y="3" rx="1"/><rect width="9" height="7" x="3" y="14" rx="1"/><rect width="5" height="7" x="16" y="14" rx="1"/></svg>';
3850
4213
  function isSection(c) {
3851
- return tagOf5(c) === "mj-section";
4214
+ return tagOf6(c) === "mj-section";
3852
4215
  }
3853
4216
  function isCommentable(c) {
3854
- const t = tagOf5(c);
4217
+ const t = tagOf6(c);
3855
4218
  return t === "mj-section" || t in TYPE_LABEL;
3856
4219
  }
3857
4220
  function closestSection(c) {
@@ -3863,29 +4226,11 @@ function closestSection(c) {
3863
4226
  return null;
3864
4227
  }
3865
4228
  function crumbName(c, labels) {
3866
- const t = tagOf5(c);
4229
+ const t = tagOf6(c);
3867
4230
  return labels.elementNames[t] ?? t.replace(/^mj-/, "");
3868
4231
  }
3869
- var BLOCKS = [
3870
- "mj-1-column",
3871
- "mj-2-columns",
3872
- "mj-3-columns",
3873
- "mj-text",
3874
- "mj-image",
3875
- "mj-button",
3876
- "mj-divider",
3877
- "mj-spacer",
3878
- "mj-social-group",
3879
- "mj-navbar"
3880
- ];
3881
- var LAYOUT_BLOCKS = /* @__PURE__ */ new Set(["mj-1-column", "mj-2-columns", "mj-3-columns"]);
3882
- var SOCIAL_BLOCK = `<mj-social icon-size="24px" mode="horizontal">
3883
- <mj-social-element name="facebook-noshare" href="https://facebook.com/"></mj-social-element>
3884
- <mj-social-element name="instagram-noshare" href="https://instagram.com/"></mj-social-element>
3885
- <mj-social-element name="x-noshare" href="https://x.com/"></mj-social-element>
3886
- </mj-social>`;
3887
4232
  function objectLabel(c) {
3888
- const base = TYPE_LABEL[tagOf5(c)] ?? tagOf5(c);
4233
+ const base = TYPE_LABEL[tagOf6(c)] ?? tagOf6(c);
3889
4234
  const text = String(c.getEl?.()?.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 30);
3890
4235
  return text ? `${base}: \u201C${text}\u201D` : base;
3891
4236
  }
@@ -3953,7 +4298,13 @@ function setupRichText(editor, labels) {
3953
4298
  result: (r, action) => r.exec("hiliteColor", val(action, "input"))
3954
4299
  });
3955
4300
  }
3956
- function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4301
+ function EmailEditor({
4302
+ docId,
4303
+ onReady,
4304
+ commentsRefresh,
4305
+ onOpenCountChange,
4306
+ blocks = []
4307
+ }) {
3957
4308
  const documents = useDocumentStore();
3958
4309
  const labels = useLabels();
3959
4310
  const loadingRef = (0, import_react10.useRef)(false);
@@ -3975,8 +4326,15 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
3975
4326
  const readOnlyRef = (0, import_react10.useRef)(false);
3976
4327
  const applyReadOnly = (0, import_react10.useRef)(async () => {
3977
4328
  });
3978
- (0, import_react10.useEffect)(() => void applyReadOnly.current(turnRunning), [turnRunning]);
3979
- (0, import_react10.useEffect)(() => () => setLocked(false), [setLocked]);
4329
+ const [turn] = (0, import_react10.useState)(createTurnFollower);
4330
+ (0, import_react10.useEffect)(() => turn.turn(turnRunning), [turn, turnRunning]);
4331
+ (0, import_react10.useEffect)(() => {
4332
+ turn.mount();
4333
+ return () => {
4334
+ turn.unmount();
4335
+ setLocked(false);
4336
+ };
4337
+ }, [turn, setLocked]);
3980
4338
  const [crumbs, setCrumbs] = (0, import_react10.useState)([]);
3981
4339
  const [picker, setPicker] = (0, import_react10.useState)(
3982
4340
  null
@@ -3987,7 +4345,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
3987
4345
  const chain = [];
3988
4346
  let cur = c;
3989
4347
  while (cur) {
3990
- const t = tagOf5(cur);
4348
+ const t = tagOf6(cur);
3991
4349
  if (!t || t === "mj-body" || t === "wrapper" || t === "Wrapper") break;
3992
4350
  chain.unshift(cur);
3993
4351
  cur = cur.parent() ?? void 0;
@@ -4036,7 +4394,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4036
4394
  if (isSection(c)) addIdClass(c, "sec");
4037
4395
  if (!isCommentable(c)) return;
4038
4396
  const toolbar = [...c.get("toolbar") ?? []];
4039
- if (tagOf5(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
4397
+ if (tagOf6(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
4040
4398
  toolbar.push({
4041
4399
  command: "open-image-picker",
4042
4400
  label: IMAGE_SVG,
@@ -4056,7 +4414,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4056
4414
  editor.Commands.add("open-image-picker", {
4057
4415
  run(ed) {
4058
4416
  const target = ed.getSelected();
4059
- if (!target || tagOf5(target) !== "mj-image") return;
4417
+ if (!target || tagOf6(target) !== "mj-image") return;
4060
4418
  const am = ed.AssetManager;
4061
4419
  am.open({
4062
4420
  target,
@@ -4076,6 +4434,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4076
4434
  });
4077
4435
  setupRichText(editor, labels);
4078
4436
  fixPluginTypes(editor);
4437
+ registerBlocks(editor, blocks, labels.blockCategorySections, SECTION_SVG);
4079
4438
  setupLayerNames(editor, labels);
4080
4439
  const STRUCTURE = /* @__PURE__ */ new Set(["mj-wrapper", "mj-section", "mj-column"]);
4081
4440
  const showBodyInLayers = () => {
@@ -4083,14 +4442,14 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4083
4442
  if (!body) return;
4084
4443
  editor.Layers.setRoot(body);
4085
4444
  body.onAll((c) => {
4086
- if (STRUCTURE.has(tagOf5(c))) editor.Layers.setOpen(c, true);
4445
+ if (STRUCTURE.has(tagOf6(c))) editor.Layers.setOpen(c, true);
4087
4446
  });
4088
4447
  };
4089
4448
  const keepStructureOpen = () => {
4090
4449
  const layers = editor.Layers;
4091
4450
  const opened = layers.model?.get("opened") ?? {};
4092
4451
  for (const [cid, c] of Object.entries(opened)) {
4093
- if (STRUCTURE.has(tagOf5(c))) delete opened[cid];
4452
+ if (STRUCTURE.has(tagOf6(c))) delete opened[cid];
4094
4453
  }
4095
4454
  };
4096
4455
  editor.on("component:add", (c) => {
@@ -4118,16 +4477,16 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4118
4477
  editor.on("component:deselected", () => {
4119
4478
  buildCrumbs(editor.getSelected());
4120
4479
  });
4121
- let stored = "";
4480
+ const baseline = createSaveBaseline();
4122
4481
  const save = async () => {
4123
4482
  const view = editor.getEditing()?.getView();
4124
4483
  await view?.syncContent?.();
4125
4484
  const mjml = editor.getHtml();
4126
- if (mjml === stored) return;
4485
+ if (!baseline.changed(mjml)) return;
4127
4486
  setSave("saving");
4128
4487
  try {
4129
4488
  await documents.save(docId, { mjml, projectData: editor.getProjectData() });
4130
- stored = mjml;
4489
+ baseline.saved(mjml);
4131
4490
  setSave("saved");
4132
4491
  } catch (e) {
4133
4492
  setSave("error");
@@ -4158,6 +4517,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4158
4517
  });
4159
4518
  }
4160
4519
  const lock = (on) => {
4520
+ if (!turn.mounted) return;
4161
4521
  readOnlyRef.current = on;
4162
4522
  setLocked(on);
4163
4523
  notifyState();
@@ -4183,37 +4543,36 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4183
4543
  return;
4184
4544
  }
4185
4545
  lock(false);
4546
+ queue.schedule();
4186
4547
  };
4187
4548
  const reload = () => queue.run(async () => {
4188
4549
  const fresh = await documents.get(docId);
4189
- if (fresh.mjml !== stored) loadMjml(fresh.mjml);
4550
+ if (fresh.mjml !== baseline.stored) loadMjml(fresh.mjml);
4190
4551
  });
4191
4552
  const loadMjml = (mjml) => {
4192
4553
  loadingRef.current = true;
4193
4554
  let repaired = false;
4555
+ const adds = !mjml || (0, import_core4.listSections)(mjml).some((s) => !s.sectionId);
4194
4556
  try {
4195
4557
  queue.cancel();
4196
- stored = mjml;
4197
- const source = withoutEditorProps(mjml || import_core2.STARTER_MJML);
4198
- repaired = source !== (mjml || import_core2.STARTER_MJML);
4199
- editor.setComponents(source);
4200
- editor.getWrapper()?.find("mj-section").forEach(decorate);
4201
- for (const t of Object.keys(TYPE_LABEL)) {
4202
- editor.getWrapper()?.find(t).forEach(decorate);
4203
- }
4558
+ baseline.load(mjml, null);
4559
+ const source = sourceForEditor(mjml || import_core4.STARTER_MJML);
4560
+ repaired = source.repaired;
4561
+ editor.setComponents(source.mjml);
4204
4562
  editor.getWrapper()?.onAll((c) => {
4205
- const illegal = illegalAttributes(tagOf5(c), c.getAttributes());
4563
+ const illegal = illegalAttributes(tagOf6(c), c.getAttributes());
4206
4564
  if (illegal.length === 0) return;
4207
4565
  c.removeAttributes(illegal);
4208
4566
  repaired = true;
4209
4567
  });
4568
+ if (!repaired && !adds) baseline.load(mjml, editor.getHtml());
4210
4569
  showBodyInLayers();
4211
4570
  editor.UndoManager.clear();
4212
4571
  } finally {
4213
4572
  loadingRef.current = false;
4214
4573
  }
4215
4574
  notifyState();
4216
- if (repaired && !readOnlyRef.current) queue.schedule();
4575
+ if ((repaired || adds) && !readOnlyRef.current) queue.schedule();
4217
4576
  };
4218
4577
  const ensureHighlightStyles = () => {
4219
4578
  const cdoc = editor.Canvas.getDocument();
@@ -4235,7 +4594,10 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4235
4594
  cdoc.head.appendChild(style);
4236
4595
  };
4237
4596
  const sectionEl = (sectionId) => {
4238
- const comp = editor.getWrapper()?.find("mj-section").find((c) => sectionIdOf(c) === sectionId);
4597
+ let comp;
4598
+ editor.getWrapper()?.onAll((c) => {
4599
+ if (!comp && isSection(c) && sectionIdOf(c) === sectionId) comp = c;
4600
+ });
4239
4601
  return comp?.getEl?.() ?? void 0;
4240
4602
  };
4241
4603
  const compiledHtml = () => {
@@ -4246,9 +4608,9 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4246
4608
  return editor.getHtml();
4247
4609
  }
4248
4610
  };
4611
+ turn.ready((on) => void applyReadOnly.current(on));
4249
4612
  try {
4250
- const doc = await documents.get(docId);
4251
- loadMjml(doc.mjml);
4613
+ await queue.run(async () => loadMjml((await documents.get(docId)).mjml));
4252
4614
  } catch (e) {
4253
4615
  import_sonner2.toast.error(labels.documentLoadFailed);
4254
4616
  console.error(e);
@@ -4303,7 +4665,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4303
4665
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4304
4666
  import_react9.default,
4305
4667
  {
4306
- grapesjs: import_grapesjs.default,
4668
+ grapesjs: import_grapesjs2.default,
4307
4669
  className: "h-full",
4308
4670
  options: {
4309
4671
  height: "100%",
@@ -4312,6 +4674,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4312
4674
  // app.grapesjs.com and leave a session-storage entry, which a host would then have
4313
4675
  // to disclose and, for the entry, ask consent for.
4314
4676
  telemetry: false,
4677
+ canvasCss: CANVAS_CSS,
4315
4678
  fromElement: false,
4316
4679
  panels: { defaults: [] },
4317
4680
  blockManager: { appendTo: "#gjs-blocks" },
@@ -4331,7 +4694,12 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4331
4694
  close: () => setPicker(null)
4332
4695
  }
4333
4696
  },
4334
- canvas: { customBadgeLabel: badgeLabel(labels) },
4697
+ canvas: {
4698
+ customBadgeLabel: badgeLabel(labels),
4699
+ // A file or text dragged in from outside lands as `<img>` or `<div>`, which no MJML
4700
+ // validator accepts; drags from the palette are unaffected.
4701
+ allowExternalDrop: false
4702
+ },
4335
4703
  deviceManager: {
4336
4704
  devices: [
4337
4705
  { id: "desktop", name: "Desktop", width: "" },
@@ -4339,14 +4707,9 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4339
4707
  ]
4340
4708
  },
4341
4709
  plugins: [
4342
- (editor) => (0, import_grapesjs_mjml.default)(editor, {
4343
- blocks: BLOCKS,
4344
- block: (id) => ({
4345
- label: labels.blockLabels[id] ?? id,
4346
- // By id: a regex on the label broke as soon as a host translated it.
4347
- category: LAYOUT_BLOCKS.has(id) ? labels.blockCategoryLayout : labels.blockCategoryContent,
4348
- ...id === "mj-social-group" && { content: SOCIAL_BLOCK }
4349
- })
4710
+ (editor) => (0, import_grapesjs_mjml2.default)(editor, {
4711
+ blocks: BUILTIN_BLOCKS,
4712
+ block: builtinBlock(labels)
4350
4713
  })
4351
4714
  ]
4352
4715
  },
@@ -4428,7 +4791,7 @@ function LeftSidebar({
4428
4791
  const selectBody = () => {
4429
4792
  let body;
4430
4793
  editor?.getWrapper()?.onAll((c) => {
4431
- if (!body && tagOf5(c) === "mj-body") body = c;
4794
+ if (!body && tagOf6(c) === "mj-body") body = c;
4432
4795
  });
4433
4796
  if (body) editor?.select(body);
4434
4797
  };
@@ -4436,6 +4799,13 @@ function LeftSidebar({
4436
4799
  setLayersExpanded(expand);
4437
4800
  editor?.getWrapper()?.onAll((c) => c.set("open", expand));
4438
4801
  };
4802
+ const onTileKey = (e) => {
4803
+ const tile = e.target.closest("[data-block]");
4804
+ const block = tile && editor?.Blocks.get(tile.dataset.block ?? "");
4805
+ if (!editor || !block || !addsBlock(e)) return;
4806
+ e.preventDefault();
4807
+ if (insertBlock(editor, block)) (0, import_sonner2.toast)(labels.blockAdded(tile.textContent?.trim() ?? ""));
4808
+ };
4439
4809
  const filterBlocks = (q) => {
4440
4810
  setBlockSearch(q);
4441
4811
  const root = document.getElementById("gjs-blocks");
@@ -4492,7 +4862,7 @@ function LeftSidebar({
4492
4862
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("kbd", { className: "pointer-events-none absolute top-1/2 right-4 -translate-y-1/2 rounded border border-panel-border bg-panel px-1 py-0.5 font-mono text-[10px] text-panel-muted-fg", children: "/" })
4493
4863
  ] }),
4494
4864
  noBlockMatches && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "px-3 py-6 text-center text-xs text-panel-muted-fg", children: labels.noBlocksMatch }),
4495
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
4865
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto", onKeyDown: onTileKey })
4496
4866
  ] }),
4497
4867
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4498
4868
  Inspector,
@@ -4573,6 +4943,7 @@ function Breadcrumb({
4573
4943
  }
4574
4944
  // Annotate the CommonJS export names for ESM import in node:
4575
4945
  0 && (module.exports = {
4576
- EmailEditor
4946
+ EmailEditor,
4947
+ blockProblems
4577
4948
  });
4578
4949
  //# sourceMappingURL=canvas.cjs.map