@mjml-agent-editor/editor 0.1.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/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/canvas.cjs +1099 -0
- package/dist/canvas.cjs.map +1 -0
- package/dist/canvas.d.cts +49 -0
- package/dist/canvas.d.ts +49 -0
- package/dist/canvas.js +629 -0
- package/dist/canvas.js.map +1 -0
- package/dist/chunk-LSF33PSW.js +1649 -0
- package/dist/chunk-LSF33PSW.js.map +1 -0
- package/dist/chunk-V6RR4VRD.js +340 -0
- package/dist/chunk-V6RR4VRD.js.map +1 -0
- package/dist/index.cjs +1935 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +71 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/ui.cjs +384 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.cts +47 -0
- package/dist/ui.d.ts +47 -0
- package/dist/ui.js +42 -0
- package/dist/ui.js.map +1 -0
- package/package.json +92 -0
- package/src/components/editor/editor-theme.css +411 -0
package/dist/canvas.js
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
CanvasComments,
|
|
4
|
+
STARTER_MJML,
|
|
5
|
+
useDocumentStore
|
|
6
|
+
} from "./chunk-LSF33PSW.js";
|
|
7
|
+
import {
|
|
8
|
+
Input,
|
|
9
|
+
Spinner,
|
|
10
|
+
cn
|
|
11
|
+
} from "./chunk-V6RR4VRD.js";
|
|
12
|
+
|
|
13
|
+
// src/components/editor/EmailEditor.tsx
|
|
14
|
+
import grapesjs from "grapesjs";
|
|
15
|
+
import GjsEditor, { Canvas, useEditorMaybe } from "@grapesjs/react";
|
|
16
|
+
import grapesjsMJML from "grapesjs-mjml";
|
|
17
|
+
import { useRef, useState } from "react";
|
|
18
|
+
import { toast } from "sonner";
|
|
19
|
+
import { ChevronRight } from "lucide-react";
|
|
20
|
+
|
|
21
|
+
// src/components/ui/switch.tsx
|
|
22
|
+
import "react";
|
|
23
|
+
import { Switch as SwitchPrimitive } from "radix-ui";
|
|
24
|
+
import { jsx } from "react/jsx-runtime";
|
|
25
|
+
function Switch({
|
|
26
|
+
className,
|
|
27
|
+
size = "default",
|
|
28
|
+
...props
|
|
29
|
+
}) {
|
|
30
|
+
return /* @__PURE__ */ jsx(
|
|
31
|
+
SwitchPrimitive.Root,
|
|
32
|
+
{
|
|
33
|
+
"data-slot": "switch",
|
|
34
|
+
"data-size": size,
|
|
35
|
+
className: cn(
|
|
36
|
+
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
37
|
+
className
|
|
38
|
+
),
|
|
39
|
+
...props,
|
|
40
|
+
children: /* @__PURE__ */ jsx(
|
|
41
|
+
SwitchPrimitive.Thumb,
|
|
42
|
+
{
|
|
43
|
+
"data-slot": "switch-thumb",
|
|
44
|
+
className: "pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/components/editor/EmailEditor.tsx
|
|
52
|
+
import { Search } from "lucide-react";
|
|
53
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
54
|
+
var SEC_ID_RE = /\bsec-([A-Za-z0-9_-]+)\b/;
|
|
55
|
+
var OBJ_ID_RE = /\bobj-([A-Za-z0-9_-]+)\b/;
|
|
56
|
+
var TYPE_LABEL = {
|
|
57
|
+
"mj-text": "Text",
|
|
58
|
+
"mj-button": "Button",
|
|
59
|
+
"mj-image": "Image",
|
|
60
|
+
"mj-column": "Column"
|
|
61
|
+
};
|
|
62
|
+
function tagOf(c) {
|
|
63
|
+
return String(c.get("tagName") || c.get("type") || "");
|
|
64
|
+
}
|
|
65
|
+
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>';
|
|
66
|
+
function isSection(c) {
|
|
67
|
+
return tagOf(c) === "mj-section";
|
|
68
|
+
}
|
|
69
|
+
function isCommentable(c) {
|
|
70
|
+
const t = tagOf(c);
|
|
71
|
+
return t === "mj-section" || t in TYPE_LABEL;
|
|
72
|
+
}
|
|
73
|
+
function classMatch(c, re) {
|
|
74
|
+
const cssClass = String(c.getAttributes()["css-class"] ?? "");
|
|
75
|
+
return cssClass.match(re)?.[1] ?? null;
|
|
76
|
+
}
|
|
77
|
+
function sectionIdOf(c) {
|
|
78
|
+
return classMatch(c, SEC_ID_RE);
|
|
79
|
+
}
|
|
80
|
+
function addIdClass(c, prefix) {
|
|
81
|
+
const existing = classMatch(c, prefix === "sec" ? SEC_ID_RE : OBJ_ID_RE);
|
|
82
|
+
if (existing) return existing;
|
|
83
|
+
const cssClass = String(c.getAttributes()["css-class"] ?? "");
|
|
84
|
+
const id = Math.random().toString(36).slice(2, 10);
|
|
85
|
+
c.addAttributes({ "css-class": `${cssClass} ${prefix}-${id}`.trim() });
|
|
86
|
+
return id;
|
|
87
|
+
}
|
|
88
|
+
function closestSection(c) {
|
|
89
|
+
let cur = c;
|
|
90
|
+
while (cur) {
|
|
91
|
+
if (isSection(cur)) return cur;
|
|
92
|
+
cur = cur.parent() ?? void 0;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
var CRUMB_NAME = {
|
|
97
|
+
"mj-section": "Section",
|
|
98
|
+
"mj-column": "Column",
|
|
99
|
+
"mj-group": "Group",
|
|
100
|
+
"mj-text": "Text",
|
|
101
|
+
"mj-button": "Button",
|
|
102
|
+
"mj-image": "Image",
|
|
103
|
+
"mj-divider": "Divider",
|
|
104
|
+
"mj-spacer": "Spacer",
|
|
105
|
+
"mj-hero": "Hero",
|
|
106
|
+
"mj-navbar": "Navbar",
|
|
107
|
+
"mj-social": "Social",
|
|
108
|
+
"mj-social-element": "Social",
|
|
109
|
+
"mj-wrapper": "Wrapper"
|
|
110
|
+
};
|
|
111
|
+
function crumbName(c) {
|
|
112
|
+
const t = tagOf(c);
|
|
113
|
+
return CRUMB_NAME[t] ?? t.replace(/^mj-/, "") ?? "Element";
|
|
114
|
+
}
|
|
115
|
+
function objectLabel(c) {
|
|
116
|
+
const base = TYPE_LABEL[tagOf(c)] ?? tagOf(c);
|
|
117
|
+
const text = String(c.getEl?.()?.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 30);
|
|
118
|
+
return text ? `${base}: \u201C${text}\u201D` : base;
|
|
119
|
+
}
|
|
120
|
+
var RTE_FONTS = [
|
|
121
|
+
"Arial",
|
|
122
|
+
"Helvetica",
|
|
123
|
+
"Georgia",
|
|
124
|
+
"Times New Roman",
|
|
125
|
+
"Verdana",
|
|
126
|
+
"Tahoma",
|
|
127
|
+
"Trebuchet MS",
|
|
128
|
+
"Courier New"
|
|
129
|
+
];
|
|
130
|
+
var RTE_SIZES = [11, 12, 13, 14, 16, 18, 20, 24, 28, 32, 40];
|
|
131
|
+
function wrapSelectionStyle(el, style) {
|
|
132
|
+
const doc = el?.ownerDocument;
|
|
133
|
+
const sel = doc?.getSelection?.();
|
|
134
|
+
if (!doc || !sel || sel.rangeCount === 0 || sel.isCollapsed) return;
|
|
135
|
+
const range = sel.getRangeAt(0);
|
|
136
|
+
const span = doc.createElement("span");
|
|
137
|
+
Object.assign(span.style, style);
|
|
138
|
+
try {
|
|
139
|
+
span.appendChild(range.extractContents());
|
|
140
|
+
range.insertNode(span);
|
|
141
|
+
sel.removeAllRanges();
|
|
142
|
+
const nr = doc.createRange();
|
|
143
|
+
nr.selectNodeContents(span);
|
|
144
|
+
sel.addRange(nr);
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function setupRichText(editor) {
|
|
149
|
+
const rte = editor.RichTextEditor;
|
|
150
|
+
if (!rte || rte.__customActions) return;
|
|
151
|
+
rte.__customActions = true;
|
|
152
|
+
const val = (action, sel) => action.btn?.querySelector(sel)?.value ?? "";
|
|
153
|
+
rte.add("fontName", {
|
|
154
|
+
icon: `<select class="gjs-rte-select" title="Font"><option value="">Font</option>${RTE_FONTS.map(
|
|
155
|
+
(f) => `<option value="${f}">${f}</option>`
|
|
156
|
+
).join("")}</select>`,
|
|
157
|
+
event: "change",
|
|
158
|
+
result: (r, action) => {
|
|
159
|
+
const v = val(action, "select");
|
|
160
|
+
if (v) r.exec("fontName", v);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
rte.add("fontSize", {
|
|
164
|
+
icon: `<select class="gjs-rte-select" title="Size"><option value="">Size</option>${RTE_SIZES.map(
|
|
165
|
+
(s) => `<option value="${s}px">${s}</option>`
|
|
166
|
+
).join("")}</select>`,
|
|
167
|
+
event: "change",
|
|
168
|
+
result: (r, action) => {
|
|
169
|
+
const v = val(action, "select");
|
|
170
|
+
if (v) wrapSelectionStyle(r.el, { fontSize: v });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
rte.add("forecolor", {
|
|
174
|
+
icon: `<input type="color" class="gjs-rte-color" title="Text color" value="#000000">`,
|
|
175
|
+
event: "change",
|
|
176
|
+
result: (r, action) => r.exec("foreColor", val(action, "input"))
|
|
177
|
+
});
|
|
178
|
+
rte.add("hilitecolor", {
|
|
179
|
+
icon: `<input type="color" class="gjs-rte-color" title="Background color" value="#ffff00">`,
|
|
180
|
+
event: "change",
|
|
181
|
+
result: (r, action) => r.exec("hiliteColor", val(action, "input"))
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
185
|
+
const documents = useDocumentStore();
|
|
186
|
+
const loadingRef = useRef(false);
|
|
187
|
+
const saveTimer = useRef(null);
|
|
188
|
+
const saveStatusRef = useRef("idle");
|
|
189
|
+
const stateListeners = useRef(/* @__PURE__ */ new Set());
|
|
190
|
+
const [loading, setLoading] = useState(true);
|
|
191
|
+
const [sidebarView, setSidebarView] = useState("blocks");
|
|
192
|
+
const [composeTarget, setComposeTarget] = useState(null);
|
|
193
|
+
const selectionSource = useRef("other");
|
|
194
|
+
const editorRef = useRef(null);
|
|
195
|
+
const [crumbs, setCrumbs] = useState([]);
|
|
196
|
+
const onEditor = async (editor) => {
|
|
197
|
+
editorRef.current = editor;
|
|
198
|
+
const buildCrumbs = (c) => {
|
|
199
|
+
const chain = [];
|
|
200
|
+
let cur = c;
|
|
201
|
+
while (cur) {
|
|
202
|
+
const t = tagOf(cur);
|
|
203
|
+
if (!t || t === "mj-body" || t === "wrapper" || t === "Wrapper") break;
|
|
204
|
+
chain.unshift(cur);
|
|
205
|
+
cur = cur.parent() ?? void 0;
|
|
206
|
+
}
|
|
207
|
+
setCrumbs(chain);
|
|
208
|
+
};
|
|
209
|
+
const findByTag = (tag) => {
|
|
210
|
+
let found;
|
|
211
|
+
editor.getWrapper()?.onAll((c) => {
|
|
212
|
+
if (!found && (c.get("tagName") === tag || c.get("type") === tag)) found = c;
|
|
213
|
+
});
|
|
214
|
+
return found;
|
|
215
|
+
};
|
|
216
|
+
const bodyWidth = () => {
|
|
217
|
+
const body = findByTag("mj-body");
|
|
218
|
+
return String(body?.getAttributes?.()?.width ?? "600px");
|
|
219
|
+
};
|
|
220
|
+
const snapshot = () => ({
|
|
221
|
+
device: editor.getDevice(),
|
|
222
|
+
canUndo: editor.UndoManager.hasUndo(),
|
|
223
|
+
canRedo: editor.UndoManager.hasRedo(),
|
|
224
|
+
saveStatus: saveStatusRef.current,
|
|
225
|
+
zoom: Math.round(editor.Canvas.getZoom?.() ?? 100),
|
|
226
|
+
contentWidth: bodyWidth()
|
|
227
|
+
});
|
|
228
|
+
const notifyState = () => {
|
|
229
|
+
const s = snapshot();
|
|
230
|
+
stateListeners.current.forEach((l) => l(s));
|
|
231
|
+
};
|
|
232
|
+
const setSave = (s) => {
|
|
233
|
+
saveStatusRef.current = s;
|
|
234
|
+
notifyState();
|
|
235
|
+
};
|
|
236
|
+
const targetOf = (c) => {
|
|
237
|
+
const section = closestSection(c);
|
|
238
|
+
if (!section || !c) return null;
|
|
239
|
+
const sectionId = addIdClass(section, "sec");
|
|
240
|
+
if (isSection(c)) {
|
|
241
|
+
return { sectionId, objectId: null, objectLabel: null };
|
|
242
|
+
}
|
|
243
|
+
const objectId = addIdClass(c, "obj");
|
|
244
|
+
return { sectionId, objectId, objectLabel: objectLabel(c) };
|
|
245
|
+
};
|
|
246
|
+
const decorate = (c) => {
|
|
247
|
+
if (isSection(c)) addIdClass(c, "sec");
|
|
248
|
+
if (!isCommentable(c)) return;
|
|
249
|
+
const toolbar = [...c.get("toolbar") ?? []];
|
|
250
|
+
if (!toolbar.some((t) => t.command === "open-comments")) {
|
|
251
|
+
toolbar.push({
|
|
252
|
+
command: "open-comments",
|
|
253
|
+
label: SPARKLES_SVG,
|
|
254
|
+
attributes: { title: "Comment on element / section", "data-role": "sparkles" }
|
|
255
|
+
});
|
|
256
|
+
c.set("toolbar", toolbar);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
editor.Commands.add("open-comments", {
|
|
260
|
+
run(ed) {
|
|
261
|
+
const target = targetOf(ed.getSelected() ?? void 0);
|
|
262
|
+
if (target) setComposeTarget(target);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
setupRichText(editor);
|
|
266
|
+
const blockCategory = (label) => {
|
|
267
|
+
const l = label.toLowerCase();
|
|
268
|
+
if (/(column|section)/.test(l)) return "Layout";
|
|
269
|
+
if (/(text|image|button|divider|spacer|social)/.test(l)) return "Basic";
|
|
270
|
+
return "Advanced";
|
|
271
|
+
};
|
|
272
|
+
const bm = editor.BlockManager;
|
|
273
|
+
bm.getAll().forEach(
|
|
274
|
+
(b) => {
|
|
275
|
+
b.set("category", blockCategory(b.get("label") || ""));
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
bm.render();
|
|
279
|
+
editor.on("component:add", decorate);
|
|
280
|
+
editor.on("load", () => {
|
|
281
|
+
const body = editor.Canvas.getBody();
|
|
282
|
+
body?.addEventListener(
|
|
283
|
+
"mousedown",
|
|
284
|
+
() => {
|
|
285
|
+
selectionSource.current = "canvas";
|
|
286
|
+
},
|
|
287
|
+
true
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
editor.on("component:selected", (c) => {
|
|
291
|
+
if (selectionSource.current === "canvas") setSidebarView("settings");
|
|
292
|
+
selectionSource.current = "other";
|
|
293
|
+
buildCrumbs(c);
|
|
294
|
+
if (c?.get("type") === "mj-image" && !c.getTrait("src")) {
|
|
295
|
+
c.addTrait(
|
|
296
|
+
{
|
|
297
|
+
type: "text",
|
|
298
|
+
name: "src",
|
|
299
|
+
label: "Image (URL)",
|
|
300
|
+
placeholder: "https://\u2026",
|
|
301
|
+
changeProp: true
|
|
302
|
+
},
|
|
303
|
+
{ at: 0 }
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
editor.on("component:deselected", () => setCrumbs([]));
|
|
308
|
+
const save = async () => {
|
|
309
|
+
if (loadingRef.current) return;
|
|
310
|
+
setSave("saving");
|
|
311
|
+
try {
|
|
312
|
+
await documents.save(docId, {
|
|
313
|
+
mjml: editor.getHtml(),
|
|
314
|
+
projectData: editor.getProjectData()
|
|
315
|
+
});
|
|
316
|
+
setSave("saved");
|
|
317
|
+
} catch (e) {
|
|
318
|
+
setSave("error");
|
|
319
|
+
toast.error("Failed to save changes.");
|
|
320
|
+
throw e;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
editor.on("update", () => {
|
|
324
|
+
notifyState();
|
|
325
|
+
if (loadingRef.current) return;
|
|
326
|
+
if (saveTimer.current) clearTimeout(saveTimer.current);
|
|
327
|
+
saveTimer.current = setTimeout(() => void save().catch(console.error), 1200);
|
|
328
|
+
});
|
|
329
|
+
editor.on("change:device", notifyState);
|
|
330
|
+
const loadMjml = (mjml) => {
|
|
331
|
+
loadingRef.current = true;
|
|
332
|
+
if (saveTimer.current) clearTimeout(saveTimer.current);
|
|
333
|
+
editor.setComponents(mjml || STARTER_MJML);
|
|
334
|
+
editor.getWrapper()?.find("mj-section").forEach(decorate);
|
|
335
|
+
for (const t of Object.keys(TYPE_LABEL)) {
|
|
336
|
+
editor.getWrapper()?.find(t).forEach(decorate);
|
|
337
|
+
}
|
|
338
|
+
setTimeout(() => {
|
|
339
|
+
loadingRef.current = false;
|
|
340
|
+
}, 400);
|
|
341
|
+
};
|
|
342
|
+
const ensureHighlightStyles = () => {
|
|
343
|
+
const cdoc = editor.Canvas.getDocument();
|
|
344
|
+
if (!cdoc || cdoc.getElementById("agent-edit-style")) return;
|
|
345
|
+
const accent = getComputedStyle(document.documentElement).getPropertyValue("--mjml-editor-accent").trim() || "#7c5cfc";
|
|
346
|
+
const style = cdoc.createElement("style");
|
|
347
|
+
style.id = "agent-edit-style";
|
|
348
|
+
style.textContent = `
|
|
349
|
+
@keyframes agentEditPulse {
|
|
350
|
+
0%, 100% { outline-color: color-mix(in srgb, ${accent} 25%, transparent); }
|
|
351
|
+
50% { outline-color: color-mix(in srgb, ${accent} 95%, transparent); }
|
|
352
|
+
}
|
|
353
|
+
.agent-editing {
|
|
354
|
+
outline: 3px solid color-mix(in srgb, ${accent} 90%, transparent) !important;
|
|
355
|
+
outline-offset: -3px;
|
|
356
|
+
animation: agentEditPulse 1s ease-in-out infinite;
|
|
357
|
+
transition: outline-color 0.2s;
|
|
358
|
+
}`;
|
|
359
|
+
cdoc.head.appendChild(style);
|
|
360
|
+
};
|
|
361
|
+
const sectionEl = (sectionId) => {
|
|
362
|
+
const comp = editor.getWrapper()?.find("mj-section").find((c) => sectionIdOf(c) === sectionId);
|
|
363
|
+
return comp?.getEl?.() ?? void 0;
|
|
364
|
+
};
|
|
365
|
+
const compiledHtml = () => {
|
|
366
|
+
try {
|
|
367
|
+
const res = editor.runCommand("mjml-code-to-html");
|
|
368
|
+
return res?.html ?? editor.getHtml();
|
|
369
|
+
} catch {
|
|
370
|
+
return editor.getHtml();
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
try {
|
|
374
|
+
const doc = await documents.get(docId);
|
|
375
|
+
loadMjml(doc.mjml);
|
|
376
|
+
} catch (e) {
|
|
377
|
+
toast.error("Failed to load the document.");
|
|
378
|
+
console.error(e);
|
|
379
|
+
} finally {
|
|
380
|
+
setLoading(false);
|
|
381
|
+
}
|
|
382
|
+
onReady({
|
|
383
|
+
getMjml: () => editor.getHtml(),
|
|
384
|
+
getCompiledHtml: compiledHtml,
|
|
385
|
+
flushSave: async () => {
|
|
386
|
+
if (saveTimer.current) clearTimeout(saveTimer.current);
|
|
387
|
+
await save();
|
|
388
|
+
},
|
|
389
|
+
reloadFromDb: async () => {
|
|
390
|
+
const fresh = await documents.get(docId);
|
|
391
|
+
if (fresh.mjml !== editor.getHtml()) loadMjml(fresh.mjml);
|
|
392
|
+
},
|
|
393
|
+
highlightSection: (sectionId, on) => {
|
|
394
|
+
ensureHighlightStyles();
|
|
395
|
+
const el = sectionEl(sectionId);
|
|
396
|
+
if (!el) return;
|
|
397
|
+
el.classList.toggle("agent-editing", on);
|
|
398
|
+
},
|
|
399
|
+
getDevices: () => editor.Devices.getDevices().map((d) => ({
|
|
400
|
+
id: String(d.id),
|
|
401
|
+
name: d.getName() || String(d.id)
|
|
402
|
+
})),
|
|
403
|
+
setDevice: (name) => {
|
|
404
|
+
editor.setDevice(name);
|
|
405
|
+
notifyState();
|
|
406
|
+
},
|
|
407
|
+
undo: () => {
|
|
408
|
+
editor.UndoManager.undo();
|
|
409
|
+
notifyState();
|
|
410
|
+
},
|
|
411
|
+
redo: () => {
|
|
412
|
+
editor.UndoManager.redo();
|
|
413
|
+
notifyState();
|
|
414
|
+
},
|
|
415
|
+
setZoom: (z) => {
|
|
416
|
+
editor.Canvas.setZoom?.(Math.max(25, Math.min(200, z)));
|
|
417
|
+
notifyState();
|
|
418
|
+
},
|
|
419
|
+
setContentWidth: (w) => {
|
|
420
|
+
findByTag("mj-body")?.addAttributes({ width: w });
|
|
421
|
+
notifyState();
|
|
422
|
+
},
|
|
423
|
+
onEditorState: (cb) => {
|
|
424
|
+
stateListeners.current.add(cb);
|
|
425
|
+
cb(snapshot());
|
|
426
|
+
return () => stateListeners.current.delete(cb);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
return /* @__PURE__ */ jsx2(
|
|
431
|
+
GjsEditor,
|
|
432
|
+
{
|
|
433
|
+
grapesjs,
|
|
434
|
+
className: "h-full",
|
|
435
|
+
options: {
|
|
436
|
+
height: "100%",
|
|
437
|
+
storageManager: false,
|
|
438
|
+
fromElement: false,
|
|
439
|
+
panels: { defaults: [] },
|
|
440
|
+
blockManager: { appendTo: "#gjs-blocks" },
|
|
441
|
+
styleManager: { appendTo: "#gjs-styles" },
|
|
442
|
+
traitManager: { appendTo: "#gjs-traits" },
|
|
443
|
+
layerManager: { appendTo: "#gjs-layers" },
|
|
444
|
+
deviceManager: {
|
|
445
|
+
devices: [
|
|
446
|
+
{ id: "desktop", name: "Desktop", width: "" },
|
|
447
|
+
{ id: "mobile", name: "Mobile", width: "375px", widthMedia: "480px" }
|
|
448
|
+
]
|
|
449
|
+
},
|
|
450
|
+
plugins: [grapesjsMJML]
|
|
451
|
+
},
|
|
452
|
+
onEditor,
|
|
453
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-1", children: [
|
|
454
|
+
/* @__PURE__ */ jsx2(
|
|
455
|
+
LeftSidebar,
|
|
456
|
+
{
|
|
457
|
+
view: sidebarView,
|
|
458
|
+
onViewChange: setSidebarView,
|
|
459
|
+
markLayersSource: () => selectionSource.current = "layers"
|
|
460
|
+
}
|
|
461
|
+
),
|
|
462
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
|
|
463
|
+
/* @__PURE__ */ jsxs("div", { className: "relative min-h-0 flex-1", children: [
|
|
464
|
+
/* @__PURE__ */ jsx2(Canvas, { className: "h-full" }),
|
|
465
|
+
/* @__PURE__ */ jsx2(
|
|
466
|
+
CanvasComments,
|
|
467
|
+
{
|
|
468
|
+
docId,
|
|
469
|
+
composeTarget,
|
|
470
|
+
onComposeConsumed: () => setComposeTarget(null),
|
|
471
|
+
refreshSignal: commentsRefresh,
|
|
472
|
+
onOpenCountChange
|
|
473
|
+
}
|
|
474
|
+
),
|
|
475
|
+
loading && /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 z-10 flex items-center justify-center gap-2 bg-white/70 text-sm text-zinc-500", children: [
|
|
476
|
+
/* @__PURE__ */ jsx2(Spinner, {}),
|
|
477
|
+
" Loading editor\u2026"
|
|
478
|
+
] })
|
|
479
|
+
] }),
|
|
480
|
+
/* @__PURE__ */ jsx2(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
|
|
481
|
+
] })
|
|
482
|
+
] })
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
function CollapseSection({
|
|
487
|
+
title,
|
|
488
|
+
defaultOpen = true,
|
|
489
|
+
children
|
|
490
|
+
}) {
|
|
491
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
492
|
+
return /* @__PURE__ */ jsxs("div", { className: "border-b border-panel-border", children: [
|
|
493
|
+
/* @__PURE__ */ jsxs(
|
|
494
|
+
"button",
|
|
495
|
+
{
|
|
496
|
+
type: "button",
|
|
497
|
+
onClick: () => setOpen((o) => !o),
|
|
498
|
+
className: "flex w-full items-center gap-1.5 px-3 py-2 text-xs font-semibold tracking-wide text-panel-muted-fg uppercase hover:text-panel-fg",
|
|
499
|
+
children: [
|
|
500
|
+
/* @__PURE__ */ jsx2(ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") }),
|
|
501
|
+
title
|
|
502
|
+
]
|
|
503
|
+
}
|
|
504
|
+
),
|
|
505
|
+
/* @__PURE__ */ jsx2("div", { className: cn(!open && "hidden"), children })
|
|
506
|
+
] });
|
|
507
|
+
}
|
|
508
|
+
function LeftSidebar({
|
|
509
|
+
view,
|
|
510
|
+
onViewChange,
|
|
511
|
+
markLayersSource
|
|
512
|
+
}) {
|
|
513
|
+
const editor = useEditorMaybe();
|
|
514
|
+
const [layersExpanded, setLayersExpanded] = useState(false);
|
|
515
|
+
const [blockSearch, setBlockSearch] = useState("");
|
|
516
|
+
const changeView = (v) => {
|
|
517
|
+
if (v === "settings" && editor && !editor.getSelected()) {
|
|
518
|
+
const wrapper = editor.getWrapper();
|
|
519
|
+
const body = wrapper?.find("mj-body")[0] ?? wrapper;
|
|
520
|
+
if (body) editor.select(body);
|
|
521
|
+
}
|
|
522
|
+
onViewChange(v);
|
|
523
|
+
};
|
|
524
|
+
const toggleLayers = (expand) => {
|
|
525
|
+
setLayersExpanded(expand);
|
|
526
|
+
editor?.getWrapper()?.onAll((c) => c.set("open", expand));
|
|
527
|
+
};
|
|
528
|
+
const filterBlocks = (q) => {
|
|
529
|
+
setBlockSearch(q);
|
|
530
|
+
const root = document.getElementById("gjs-blocks");
|
|
531
|
+
if (!root) return;
|
|
532
|
+
const term = q.trim().toLowerCase();
|
|
533
|
+
root.querySelectorAll(".gjs-block").forEach((b) => {
|
|
534
|
+
const label = (b.querySelector(".gjs-block-label")?.textContent ?? b.textContent ?? "").toLowerCase().trim();
|
|
535
|
+
b.style.display = !term || label.includes(term) ? "" : "none";
|
|
536
|
+
});
|
|
537
|
+
root.querySelectorAll(".gjs-block-category").forEach((cat) => {
|
|
538
|
+
const anyVisible = [...cat.querySelectorAll(".gjs-block")].some(
|
|
539
|
+
(b) => b.style.display !== "none"
|
|
540
|
+
);
|
|
541
|
+
cat.style.display = anyVisible ? "" : "none";
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
return /* @__PURE__ */ jsxs("div", { className: "editor-dark flex w-64 shrink-0 flex-col border-r border-panel-border bg-panel text-panel-fg", children: [
|
|
545
|
+
/* @__PURE__ */ jsx2("div", { className: "flex border-b border-panel-border", children: [
|
|
546
|
+
["blocks", "Blocks"],
|
|
547
|
+
["settings", "Settings"],
|
|
548
|
+
["layers", "Layers"]
|
|
549
|
+
].map(([value, label]) => /* @__PURE__ */ jsx2(
|
|
550
|
+
"button",
|
|
551
|
+
{
|
|
552
|
+
type: "button",
|
|
553
|
+
onClick: () => changeView(value),
|
|
554
|
+
className: cn(
|
|
555
|
+
"relative flex-1 px-3 py-2.5 text-sm transition-colors",
|
|
556
|
+
"after:absolute after:inset-x-0 after:-bottom-px after:h-0.5 after:transition-colors",
|
|
557
|
+
view === value ? "font-medium text-panel-fg after:bg-brand" : "text-panel-muted-fg after:bg-transparent hover:text-panel-fg"
|
|
558
|
+
),
|
|
559
|
+
children: label
|
|
560
|
+
},
|
|
561
|
+
value
|
|
562
|
+
)) }),
|
|
563
|
+
/* @__PURE__ */ jsxs("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
|
|
564
|
+
/* @__PURE__ */ jsxs("div", { className: "relative border-b border-panel-border p-2", children: [
|
|
565
|
+
/* @__PURE__ */ jsx2(Search, { className: "pointer-events-none absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-panel-muted-fg" }),
|
|
566
|
+
/* @__PURE__ */ jsx2(
|
|
567
|
+
Input,
|
|
568
|
+
{
|
|
569
|
+
value: blockSearch,
|
|
570
|
+
onChange: (e) => filterBlocks(e.target.value),
|
|
571
|
+
placeholder: "Search blocks\u2026",
|
|
572
|
+
className: "h-8 border-panel-border bg-panel-elevated pr-10 pl-7 text-sm text-panel-fg placeholder:text-panel-muted-fg"
|
|
573
|
+
}
|
|
574
|
+
),
|
|
575
|
+
/* @__PURE__ */ jsx2("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: "\u2318K" })
|
|
576
|
+
] }),
|
|
577
|
+
/* @__PURE__ */ jsx2("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
|
|
578
|
+
] }),
|
|
579
|
+
/* @__PURE__ */ jsxs("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: [
|
|
580
|
+
/* @__PURE__ */ jsx2(CollapseSection, { title: "Attributes", children: /* @__PURE__ */ jsx2("div", { id: "gjs-traits" }) }),
|
|
581
|
+
/* @__PURE__ */ jsx2(CollapseSection, { title: "Style", children: /* @__PURE__ */ jsx2("div", { id: "gjs-styles" }) })
|
|
582
|
+
] }),
|
|
583
|
+
/* @__PURE__ */ jsxs(
|
|
584
|
+
"div",
|
|
585
|
+
{
|
|
586
|
+
className: cn("flex min-h-0 flex-1 flex-col", view !== "layers" && "hidden"),
|
|
587
|
+
onMouseDownCapture: markLayersSource,
|
|
588
|
+
children: [
|
|
589
|
+
/* @__PURE__ */ 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: [
|
|
590
|
+
/* @__PURE__ */ jsx2("span", { children: "Expand all" }),
|
|
591
|
+
/* @__PURE__ */ jsx2(
|
|
592
|
+
Switch,
|
|
593
|
+
{
|
|
594
|
+
checked: layersExpanded,
|
|
595
|
+
onCheckedChange: toggleLayers,
|
|
596
|
+
className: "data-[state=checked]:bg-brand data-[state=unchecked]:bg-panel-border"
|
|
597
|
+
}
|
|
598
|
+
)
|
|
599
|
+
] }),
|
|
600
|
+
/* @__PURE__ */ jsx2("div", { id: "gjs-layers", className: "min-h-0 flex-1 overflow-y-auto" })
|
|
601
|
+
]
|
|
602
|
+
}
|
|
603
|
+
)
|
|
604
|
+
] });
|
|
605
|
+
}
|
|
606
|
+
function Breadcrumb({
|
|
607
|
+
crumbs,
|
|
608
|
+
onSelect
|
|
609
|
+
}) {
|
|
610
|
+
return /* @__PURE__ */ jsx2("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__ */ jsx2("span", { className: "text-muted-foreground/60", children: "Select an element\u2026" }) : crumbs.map((c, i) => /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-0.5", children: [
|
|
611
|
+
i > 0 && /* @__PURE__ */ jsx2(ChevronRight, { className: "size-3 opacity-40" }),
|
|
612
|
+
/* @__PURE__ */ jsx2(
|
|
613
|
+
"button",
|
|
614
|
+
{
|
|
615
|
+
type: "button",
|
|
616
|
+
onClick: () => onSelect(c),
|
|
617
|
+
className: cn(
|
|
618
|
+
"rounded px-1.5 py-0.5 hover:bg-muted hover:text-foreground",
|
|
619
|
+
i === crumbs.length - 1 && "font-medium text-brand"
|
|
620
|
+
),
|
|
621
|
+
children: crumbName(c)
|
|
622
|
+
}
|
|
623
|
+
)
|
|
624
|
+
] }, i)) });
|
|
625
|
+
}
|
|
626
|
+
export {
|
|
627
|
+
EmailEditor
|
|
628
|
+
};
|
|
629
|
+
//# sourceMappingURL=canvas.js.map
|