best-email-builder 0.2.0 → 0.3.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/CHANGELOG.md +53 -0
- package/README.md +102 -0
- package/dist/builder.js +44 -0
- package/dist/builder.js.map +1 -0
- package/dist/{chunk-MTALS5GC.js → chunk-3M36VVXS.js} +36 -118
- package/dist/chunk-3M36VVXS.js.map +1 -0
- package/dist/chunk-57CTQNTK.js +56 -0
- package/dist/chunk-57CTQNTK.js.map +1 -0
- package/dist/chunk-BVOVAEEJ.js +73 -0
- package/dist/chunk-BVOVAEEJ.js.map +1 -0
- package/dist/chunk-ILRAQTHS.js +3677 -0
- package/dist/chunk-ILRAQTHS.js.map +1 -0
- package/dist/chunk-JOC752I3.js +529 -0
- package/dist/chunk-JOC752I3.js.map +1 -0
- package/dist/chunk-ZSUHOCCE.js +1351 -0
- package/dist/chunk-ZSUHOCCE.js.map +1 -0
- package/dist/index.js +86 -4865
- package/dist/index.js.map +1 -1
- package/dist/library.js +31 -0
- package/dist/library.js.map +1 -0
- package/dist/server.js +15 -11
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -14
- package/dist/types/builder.d.ts +8 -0
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/core/scrollbars.d.ts +13 -0
- package/dist/types/defaultEditor.d.ts +8 -0
- package/dist/types/features/templates/gallery.d.ts +7 -0
- package/dist/types/index.d.ts +4 -9
- package/dist/types/library.d.ts +5 -0
- package/dist/types/storage/client.d.ts +4 -0
- package/dist/types/ui/EmailStudio.d.ts +42 -0
- package/dist/types/ui/library/TemplateLibrary.d.ts +41 -0
- package/dist/types/ui/primitives/button.d.ts +1 -1
- package/package.json +18 -1
- package/dist/chunk-MTALS5GC.js.map +0 -1
|
@@ -0,0 +1,3677 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
DEFAULT_BUTTON_URL,
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
FONT_STACK,
|
|
10
|
+
FRAME_SCROLLBAR_CSS,
|
|
11
|
+
GROUPS,
|
|
12
|
+
Input,
|
|
13
|
+
Label,
|
|
14
|
+
NoticeArea,
|
|
15
|
+
PALETTE,
|
|
16
|
+
Popover,
|
|
17
|
+
PopoverContent,
|
|
18
|
+
PopoverTrigger,
|
|
19
|
+
PreviewButton,
|
|
20
|
+
SectionLabel,
|
|
21
|
+
Select,
|
|
22
|
+
SelectContent,
|
|
23
|
+
SelectItem,
|
|
24
|
+
SelectTrigger,
|
|
25
|
+
SelectValue,
|
|
26
|
+
Separator,
|
|
27
|
+
Skeleton,
|
|
28
|
+
TEMPLATE_GALLERY,
|
|
29
|
+
Tabs,
|
|
30
|
+
TabsList,
|
|
31
|
+
TabsTrigger,
|
|
32
|
+
Textarea,
|
|
33
|
+
TooltipWrap,
|
|
34
|
+
TruncatedText,
|
|
35
|
+
UiProvider,
|
|
36
|
+
buildGallery,
|
|
37
|
+
cn,
|
|
38
|
+
createHttpStorage,
|
|
39
|
+
createStarterMjml,
|
|
40
|
+
inWhiteSection,
|
|
41
|
+
prop,
|
|
42
|
+
socialBlock,
|
|
43
|
+
socialElementBlock,
|
|
44
|
+
useLayoutWidth,
|
|
45
|
+
useT,
|
|
46
|
+
useUi,
|
|
47
|
+
withThumbnailImages
|
|
48
|
+
} from "./chunk-ZSUHOCCE.js";
|
|
49
|
+
import {
|
|
50
|
+
isSafeUrl
|
|
51
|
+
} from "./chunk-57CTQNTK.js";
|
|
52
|
+
import {
|
|
53
|
+
CUSTOM_GROUP,
|
|
54
|
+
DEFAULT_VARIABLES,
|
|
55
|
+
EMAIL_DEVICES,
|
|
56
|
+
VARIABLE_LIMITS,
|
|
57
|
+
createDocument,
|
|
58
|
+
defineVariables,
|
|
59
|
+
imageUploadError,
|
|
60
|
+
keyFromLabel,
|
|
61
|
+
mergeVariables,
|
|
62
|
+
readDocument,
|
|
63
|
+
tokenOf,
|
|
64
|
+
validateCustomVariable
|
|
65
|
+
} from "./chunk-3M36VVXS.js";
|
|
66
|
+
|
|
67
|
+
// src/core/events.js
|
|
68
|
+
function createEmitter({ onError } = {}) {
|
|
69
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
70
|
+
const report = onError || ((error, event) => {
|
|
71
|
+
console.warn(`[best-email-builder] listener for "${event}" failed`, error);
|
|
72
|
+
});
|
|
73
|
+
function off(event, listener) {
|
|
74
|
+
listeners.get(event)?.delete(listener);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
on(event, listener) {
|
|
78
|
+
if (!listeners.has(event)) listeners.set(event, /* @__PURE__ */ new Set());
|
|
79
|
+
listeners.get(event)?.add(listener);
|
|
80
|
+
return () => off(event, listener);
|
|
81
|
+
},
|
|
82
|
+
off,
|
|
83
|
+
emit(event, payload) {
|
|
84
|
+
for (const listener of [...listeners.get(event) || []]) {
|
|
85
|
+
try {
|
|
86
|
+
listener(payload);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
report(error, event);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
clear() {
|
|
93
|
+
listeners.clear();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/core/mjmlAttributes.js
|
|
99
|
+
var LONGHAND_SPACING = /* @__PURE__ */ new Set(["padding"]);
|
|
100
|
+
var SIDES = (
|
|
101
|
+
/** @type {const} */
|
|
102
|
+
["top", "right", "bottom", "left"]
|
|
103
|
+
);
|
|
104
|
+
function parseSpacing(value) {
|
|
105
|
+
const parts = String(value || "").trim().split(/\s+/).filter(Boolean);
|
|
106
|
+
const [a = "0px", b = a, c = a, d = b] = parts;
|
|
107
|
+
return [a, b, c, d];
|
|
108
|
+
}
|
|
109
|
+
function formatSpacing(sides) {
|
|
110
|
+
const [t, r, b, l] = sides.map((s) => String(s || "0px"));
|
|
111
|
+
if (t === b && r === l) return t === r ? t : `${t} ${r}`;
|
|
112
|
+
if (r === l) return `${t} ${r} ${b}`;
|
|
113
|
+
return `${t} ${r} ${b} ${l}`;
|
|
114
|
+
}
|
|
115
|
+
function cleanAttributeValue(value) {
|
|
116
|
+
return String(value ?? "").replace(/["<>\r\n]/g, "").trim();
|
|
117
|
+
}
|
|
118
|
+
function toAttributes(values) {
|
|
119
|
+
const out = {};
|
|
120
|
+
for (const [key, raw] of Object.entries(values || {})) {
|
|
121
|
+
const value = raw === null || raw === void 0 ? "" : cleanAttributeValue(raw);
|
|
122
|
+
if (LONGHAND_SPACING.has(key)) {
|
|
123
|
+
const sides = value ? parseSpacing(value) : null;
|
|
124
|
+
SIDES.forEach((side, i) => {
|
|
125
|
+
out[`${key}-${side}`] = sides ? sides[i] : null;
|
|
126
|
+
});
|
|
127
|
+
out[key] = null;
|
|
128
|
+
} else {
|
|
129
|
+
out[key] = value || null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function toValues(attributes, properties) {
|
|
135
|
+
const values = {};
|
|
136
|
+
for (const { key } of properties) {
|
|
137
|
+
if (LONGHAND_SPACING.has(key)) {
|
|
138
|
+
const base = parseSpacing(String(attributes[key] ?? ""));
|
|
139
|
+
const sides = SIDES.map((side, i) => {
|
|
140
|
+
const v = attributes[`${key}-${side}`];
|
|
141
|
+
return v === void 0 || v === null || v === "" ? base[i] : String(v);
|
|
142
|
+
});
|
|
143
|
+
const hasAny = attributes[key] || SIDES.some((side) => attributes[`${key}-${side}`]);
|
|
144
|
+
values[key] = hasAny ? formatSpacing(sides) : "";
|
|
145
|
+
} else {
|
|
146
|
+
const v = attributes[key];
|
|
147
|
+
values[key] = v === void 0 || v === null ? "" : String(v);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return values;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/core/EmailEditor.js
|
|
154
|
+
function createEmailEditor({ adapter, blocks, features = null }) {
|
|
155
|
+
if (!adapter) throw new Error("EmailEditor: missing adapter");
|
|
156
|
+
if (!blocks) throw new Error("EmailEditor: missing block registry");
|
|
157
|
+
const emitter = createEmitter();
|
|
158
|
+
let status = "idle";
|
|
159
|
+
let generation = 0;
|
|
160
|
+
let teardowns = [];
|
|
161
|
+
function requireReady(action) {
|
|
162
|
+
if (status !== "ready") {
|
|
163
|
+
throw new Error(`EmailEditor: "${action}" called before the editor was initialized`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function targetOf(id, action) {
|
|
167
|
+
const resolved = id ?? adapter.getSelection()?.id ?? null;
|
|
168
|
+
if (!resolved) throw new Error(`EmailEditor: "${action}" with no selected element`);
|
|
169
|
+
return resolved;
|
|
170
|
+
}
|
|
171
|
+
function blockForTag(tagName) {
|
|
172
|
+
const all = blocks.list((b) => !b.insertOnly);
|
|
173
|
+
return all.find((b) => b.tagName === tagName && b.kind === "structure") || all.find((b) => b.tagName === tagName) || null;
|
|
174
|
+
}
|
|
175
|
+
function toEmailSelection(selection) {
|
|
176
|
+
if (!selection) return null;
|
|
177
|
+
const block = blockForTag(selection.tagName);
|
|
178
|
+
return {
|
|
179
|
+
...selection,
|
|
180
|
+
blockId: block?.id || null,
|
|
181
|
+
label: block?.label || selection.tagName,
|
|
182
|
+
values: toValues(selection.attributes || {}, block?.properties || [])
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function startFeatures() {
|
|
186
|
+
if (!features) return;
|
|
187
|
+
for (const feature of features.list((f) => f.enabled !== false)) {
|
|
188
|
+
try {
|
|
189
|
+
const stop = feature.setup?.(editor);
|
|
190
|
+
if (typeof stop === "function") teardowns.push(stop);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.warn(`[best-email-builder] feature "${feature.id}" failed`, error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function stopFeatures() {
|
|
197
|
+
for (const stop of teardowns.reverse()) {
|
|
198
|
+
try {
|
|
199
|
+
stop();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.warn("[best-email-builder] feature teardown failed", error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
teardowns = [];
|
|
205
|
+
}
|
|
206
|
+
const editor = {
|
|
207
|
+
get status() {
|
|
208
|
+
return status;
|
|
209
|
+
},
|
|
210
|
+
get adapterName() {
|
|
211
|
+
return adapter.name;
|
|
212
|
+
},
|
|
213
|
+
async initialize({ container, document = null }) {
|
|
214
|
+
if (!container) throw new Error("EmailEditor: missing container");
|
|
215
|
+
if (status !== "idle") {
|
|
216
|
+
throw new Error(`EmailEditor: initialization refused (status "${status}")`);
|
|
217
|
+
}
|
|
218
|
+
const ticket = ++generation;
|
|
219
|
+
status = "initializing";
|
|
220
|
+
try {
|
|
221
|
+
await adapter.initialize({
|
|
222
|
+
container,
|
|
223
|
+
blocks: blocks.list(),
|
|
224
|
+
document: document ? readDocument(document) : null,
|
|
225
|
+
onEvent: (type, payload) => {
|
|
226
|
+
if (ticket !== generation) return;
|
|
227
|
+
emitter.emit(type, type === "selection" ? toEmailSelection(payload) : payload);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (ticket === generation) status = "idle";
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
if (ticket !== generation) return;
|
|
235
|
+
status = "ready";
|
|
236
|
+
startFeatures();
|
|
237
|
+
emitter.emit("ready");
|
|
238
|
+
},
|
|
239
|
+
destroy() {
|
|
240
|
+
generation += 1;
|
|
241
|
+
stopFeatures();
|
|
242
|
+
const etait = status;
|
|
243
|
+
status = "idle";
|
|
244
|
+
adapter.destroy();
|
|
245
|
+
if (etait === "ready") emitter.emit("destroy");
|
|
246
|
+
},
|
|
247
|
+
listBlocks: () => blocks.list(),
|
|
248
|
+
getBlock: (blockId) => blocks.get(blockId),
|
|
249
|
+
addBlock(blockId, options = {}) {
|
|
250
|
+
requireReady("addBlock");
|
|
251
|
+
const block = blocks.get(blockId);
|
|
252
|
+
if (!block || !block.mjml) throw new Error(`EmailEditor: unknown block "${blockId}"`);
|
|
253
|
+
return adapter.addBlock(block, options);
|
|
254
|
+
},
|
|
255
|
+
remove(id = null) {
|
|
256
|
+
requireReady("remove");
|
|
257
|
+
return adapter.remove(targetOf(id, "remove"));
|
|
258
|
+
},
|
|
259
|
+
duplicate(id = null) {
|
|
260
|
+
requireReady("duplicate");
|
|
261
|
+
return adapter.duplicate(targetOf(id, "duplicate"));
|
|
262
|
+
},
|
|
263
|
+
move(direction, id = null) {
|
|
264
|
+
requireReady("move");
|
|
265
|
+
if (direction !== -1 && direction !== 1) throw new Error("EmailEditor: invalid move direction");
|
|
266
|
+
return adapter.move(targetOf(id, "move"), direction);
|
|
267
|
+
},
|
|
268
|
+
select(id) {
|
|
269
|
+
requireReady("select");
|
|
270
|
+
adapter.select(id);
|
|
271
|
+
},
|
|
272
|
+
getSelection() {
|
|
273
|
+
return status === "ready" ? toEmailSelection(adapter.getSelection()) : null;
|
|
274
|
+
},
|
|
275
|
+
getElement(id) {
|
|
276
|
+
return status === "ready" && id ? toEmailSelection(adapter.describe(id)) : null;
|
|
277
|
+
},
|
|
278
|
+
/** The email body: page background and width, edited without a selection. */
|
|
279
|
+
getRootId: () => status === "ready" ? adapter.getRootId() : null,
|
|
280
|
+
updateProperties(values, id = null) {
|
|
281
|
+
requireReady("updateProperties");
|
|
282
|
+
adapter.updateProperties(targetOf(id, "updateProperties"), toAttributes(values));
|
|
283
|
+
},
|
|
284
|
+
setContent(content, id = null) {
|
|
285
|
+
requireReady("setContent");
|
|
286
|
+
adapter.setContent(targetOf(id, "setContent"), content);
|
|
287
|
+
},
|
|
288
|
+
insertText(text) {
|
|
289
|
+
requireReady("insertText");
|
|
290
|
+
return adapter.insertText(String(text ?? ""));
|
|
291
|
+
},
|
|
292
|
+
/**
|
|
293
|
+
* Link at the caret of the text being edited, at the end of the selected
|
|
294
|
+
* text, or as the selected button's URL. False if nothing fits.
|
|
295
|
+
*/
|
|
296
|
+
insertLink(href, label) {
|
|
297
|
+
requireReady("insertLink");
|
|
298
|
+
return adapter.insertLink(String(href || ""), String(label || href || ""));
|
|
299
|
+
},
|
|
300
|
+
undo() {
|
|
301
|
+
requireReady("undo");
|
|
302
|
+
adapter.undo();
|
|
303
|
+
},
|
|
304
|
+
redo() {
|
|
305
|
+
requireReady("redo");
|
|
306
|
+
adapter.redo();
|
|
307
|
+
},
|
|
308
|
+
canUndo: () => status === "ready" && adapter.canUndo(),
|
|
309
|
+
canRedo: () => status === "ready" && adapter.canRedo(),
|
|
310
|
+
getDocument() {
|
|
311
|
+
requireReady("getDocument");
|
|
312
|
+
return createDocument({
|
|
313
|
+
engine: adapter.name,
|
|
314
|
+
engineVersion: adapter.version,
|
|
315
|
+
project: adapter.getProject()
|
|
316
|
+
});
|
|
317
|
+
},
|
|
318
|
+
loadDocument(raw) {
|
|
319
|
+
requireReady("loadDocument");
|
|
320
|
+
const document = readDocument(raw);
|
|
321
|
+
if (document.engine !== adapter.name) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`EmailEditor: document written for engine "${document.engine}", editor runs on "${adapter.name}"`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
adapter.loadProject(document.project);
|
|
327
|
+
adapter.clearHistory();
|
|
328
|
+
},
|
|
329
|
+
loadMjml(mjml) {
|
|
330
|
+
requireReady("loadMjml");
|
|
331
|
+
adapter.loadMjml(String(mjml || ""));
|
|
332
|
+
adapter.clearHistory();
|
|
333
|
+
},
|
|
334
|
+
exportMjml() {
|
|
335
|
+
requireReady("exportMjml");
|
|
336
|
+
return adapter.exportMjml();
|
|
337
|
+
},
|
|
338
|
+
/**
|
|
339
|
+
* The email (or the given MJML) compiled by the CANVAS compiler — for
|
|
340
|
+
* display only (thumbnails, inspection). The HTML that is stored and sent
|
|
341
|
+
* is always compiled by the server.
|
|
342
|
+
*/
|
|
343
|
+
exportHtml(mjml = null) {
|
|
344
|
+
requireReady("exportHtml");
|
|
345
|
+
return adapter.exportHtml(mjml === null || mjml === void 0 ? null : String(mjml));
|
|
346
|
+
},
|
|
347
|
+
setDevice(device) {
|
|
348
|
+
requireReady("setDevice");
|
|
349
|
+
if (!EMAIL_DEVICES.includes(device)) throw new Error(`EmailEditor: unknown device "${device}"`);
|
|
350
|
+
adapter.setDevice(device);
|
|
351
|
+
},
|
|
352
|
+
getDevice: () => status === "ready" ? adapter.getDevice() : "desktop",
|
|
353
|
+
startBlockDrag(blockId, event) {
|
|
354
|
+
requireReady("startBlockDrag");
|
|
355
|
+
const block = blocks.get(blockId);
|
|
356
|
+
if (!block || !block.mjml) throw new Error(`EmailEditor: unknown block "${blockId}"`);
|
|
357
|
+
adapter.startBlockDrag(block, event);
|
|
358
|
+
},
|
|
359
|
+
endBlockDrag(cancel = false) {
|
|
360
|
+
if (status === "ready") adapter.endBlockDrag(cancel);
|
|
361
|
+
},
|
|
362
|
+
/**
|
|
363
|
+
* Keyboard shortcut that also works INSIDE the canvas (an iframe: page
|
|
364
|
+
* listeners hear nothing there). `keys`: `mod+s` (Ctrl, or ⌘ on Mac),
|
|
365
|
+
* several combinations separated by commas.
|
|
366
|
+
*/
|
|
367
|
+
addShortcut(id, keys, handler) {
|
|
368
|
+
requireReady("addShortcut");
|
|
369
|
+
return adapter.addShortcut(id, keys, handler);
|
|
370
|
+
},
|
|
371
|
+
on: (event, listener) => emitter.on(event, listener)
|
|
372
|
+
};
|
|
373
|
+
return editor;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/core/registry.js
|
|
377
|
+
var ID_SHAPE = /^[a-z][a-z0-9-]*$/;
|
|
378
|
+
function createRegistry(kind, initial = []) {
|
|
379
|
+
const items = /* @__PURE__ */ new Map();
|
|
380
|
+
function unregister(id) {
|
|
381
|
+
return items.delete(id);
|
|
382
|
+
}
|
|
383
|
+
function register(item) {
|
|
384
|
+
if (!item || typeof item.id !== "string" || !ID_SHAPE.test(item.id)) {
|
|
385
|
+
throw new Error(`${kind}: invalid id ("${item?.id}")`);
|
|
386
|
+
}
|
|
387
|
+
if (items.has(item.id)) {
|
|
388
|
+
throw new Error(`${kind}: "${item.id}" is already registered`);
|
|
389
|
+
}
|
|
390
|
+
items.set(item.id, item);
|
|
391
|
+
return () => {
|
|
392
|
+
unregister(item.id);
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
initial.forEach(register);
|
|
396
|
+
return {
|
|
397
|
+
kind,
|
|
398
|
+
register,
|
|
399
|
+
unregister,
|
|
400
|
+
get: (id) => items.get(id),
|
|
401
|
+
has: (id) => items.has(id),
|
|
402
|
+
// Stable sort: `order` first, then registration order.
|
|
403
|
+
list: (filter) => [...items.values()].map((item, index) => ({ item, index })).sort((a, b) => (a.item.order ?? 1e3) - (b.item.order ?? 1e3) || a.index - b.index).map(({ item }) => item).filter((item) => filter ? filter(item) : true),
|
|
404
|
+
ids: () => [...items.keys()]
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/blocks/structure/index.js
|
|
409
|
+
var bodyBlock = {
|
|
410
|
+
id: "body",
|
|
411
|
+
label: "Email",
|
|
412
|
+
icon: "Mail",
|
|
413
|
+
kind: "structure",
|
|
414
|
+
tagName: "mj-body",
|
|
415
|
+
order: 900,
|
|
416
|
+
properties: [
|
|
417
|
+
prop.background("background-color", "Page background"),
|
|
418
|
+
{
|
|
419
|
+
key: "width",
|
|
420
|
+
label: "Content width",
|
|
421
|
+
type: "select",
|
|
422
|
+
group: GROUPS.layout,
|
|
423
|
+
options: [
|
|
424
|
+
{ value: "600px", label: "600 px (standard)" },
|
|
425
|
+
{ value: "640px", label: "640 px" }
|
|
426
|
+
]
|
|
427
|
+
}
|
|
428
|
+
]
|
|
429
|
+
};
|
|
430
|
+
var sectionBlock = {
|
|
431
|
+
id: "section",
|
|
432
|
+
label: "Section",
|
|
433
|
+
icon: "Rows3",
|
|
434
|
+
kind: "structure",
|
|
435
|
+
tagName: "mj-section",
|
|
436
|
+
order: 901,
|
|
437
|
+
properties: [
|
|
438
|
+
prop.background("background-color", "Background"),
|
|
439
|
+
// A background image keeps the color under it: shown when images are blocked.
|
|
440
|
+
{ key: "background-url", label: "Background image", type: "image", group: GROUPS.colors },
|
|
441
|
+
{
|
|
442
|
+
key: "background-size",
|
|
443
|
+
label: "Image size",
|
|
444
|
+
type: "select",
|
|
445
|
+
group: GROUPS.colors,
|
|
446
|
+
options: [
|
|
447
|
+
{ value: "cover", label: "Fill the section" },
|
|
448
|
+
{ value: "contain", label: "Whole image" },
|
|
449
|
+
{ value: "auto", label: "Original size" }
|
|
450
|
+
]
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
key: "background-position",
|
|
454
|
+
label: "Image position",
|
|
455
|
+
type: "select",
|
|
456
|
+
group: GROUPS.colors,
|
|
457
|
+
options: [
|
|
458
|
+
{ value: "center center", label: "Center" },
|
|
459
|
+
{ value: "center top", label: "Top" },
|
|
460
|
+
{ value: "center bottom", label: "Bottom" }
|
|
461
|
+
]
|
|
462
|
+
},
|
|
463
|
+
prop.padding("padding", "Padding"),
|
|
464
|
+
prop.radius(24)
|
|
465
|
+
]
|
|
466
|
+
};
|
|
467
|
+
var columnBlock = {
|
|
468
|
+
id: "column",
|
|
469
|
+
label: "Column",
|
|
470
|
+
icon: "Columns2",
|
|
471
|
+
kind: "structure",
|
|
472
|
+
tagName: "mj-column",
|
|
473
|
+
order: 902,
|
|
474
|
+
properties: [
|
|
475
|
+
prop.background("background-color", "Background"),
|
|
476
|
+
prop.padding("padding", "Padding"),
|
|
477
|
+
{
|
|
478
|
+
key: "vertical-align",
|
|
479
|
+
label: "Vertical alignment",
|
|
480
|
+
type: "select",
|
|
481
|
+
group: GROUPS.layout,
|
|
482
|
+
options: [
|
|
483
|
+
{ value: "top", label: "Top" },
|
|
484
|
+
{ value: "middle", label: "Middle" },
|
|
485
|
+
{ value: "bottom", label: "Bottom" }
|
|
486
|
+
]
|
|
487
|
+
}
|
|
488
|
+
]
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// src/blocks/text/index.js
|
|
492
|
+
function textMjml(content) {
|
|
493
|
+
return `<mj-text font-family="${FONT_STACK}" font-size="15px" line-height="24px" color="${PALETTE.ink}" align="left">${content}</mj-text>`;
|
|
494
|
+
}
|
|
495
|
+
var MJML = textMjml("Your text.");
|
|
496
|
+
var textBlock = {
|
|
497
|
+
id: "text",
|
|
498
|
+
label: "Text",
|
|
499
|
+
description: "Paragraph or heading",
|
|
500
|
+
icon: "Type",
|
|
501
|
+
kind: "content",
|
|
502
|
+
tagName: "mj-text",
|
|
503
|
+
order: 10,
|
|
504
|
+
mjml: MJML,
|
|
505
|
+
sectionMjml: inWhiteSection(MJML),
|
|
506
|
+
editableContent: true,
|
|
507
|
+
properties: [
|
|
508
|
+
prop.fontFamily(),
|
|
509
|
+
prop.fontSize(10, 48),
|
|
510
|
+
prop.lineHeight(),
|
|
511
|
+
{
|
|
512
|
+
key: "font-weight",
|
|
513
|
+
label: "Weight",
|
|
514
|
+
type: "select",
|
|
515
|
+
group: GROUPS.typography,
|
|
516
|
+
options: [
|
|
517
|
+
{ value: "normal", label: "Normal" },
|
|
518
|
+
{ value: "bold", label: "Bold" }
|
|
519
|
+
]
|
|
520
|
+
},
|
|
521
|
+
prop.color("Text"),
|
|
522
|
+
prop.background("container-background-color", "Background"),
|
|
523
|
+
{ ...prop.align(), options: [...prop.align().options || [], { value: "justify", label: "Justify" }] },
|
|
524
|
+
prop.padding()
|
|
525
|
+
]
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// src/blocks/image/index.js
|
|
529
|
+
var MJML2 = '<mj-image align="center"></mj-image>';
|
|
530
|
+
var imageBlock = {
|
|
531
|
+
id: "image",
|
|
532
|
+
label: "Image",
|
|
533
|
+
description: "Photo, visual or logo",
|
|
534
|
+
icon: "Image",
|
|
535
|
+
kind: "content",
|
|
536
|
+
tagName: "mj-image",
|
|
537
|
+
order: 20,
|
|
538
|
+
mjml: MJML2,
|
|
539
|
+
sectionMjml: inWhiteSection(MJML2),
|
|
540
|
+
properties: [
|
|
541
|
+
{ key: "src", label: "Image", type: "image", group: GROUPS.content },
|
|
542
|
+
{
|
|
543
|
+
key: "alt",
|
|
544
|
+
label: "Alt text",
|
|
545
|
+
type: "text",
|
|
546
|
+
group: GROUPS.content,
|
|
547
|
+
placeholder: "Describes the image if it is blocked"
|
|
548
|
+
},
|
|
549
|
+
prop.href("Link on click"),
|
|
550
|
+
{ key: "width", label: "Width", type: "size", group: GROUPS.layout, min: 20, max: 600, unit: "px" },
|
|
551
|
+
prop.align(),
|
|
552
|
+
prop.radius(40),
|
|
553
|
+
prop.padding()
|
|
554
|
+
]
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// src/blocks/button/index.js
|
|
558
|
+
var MJML3 = `<mj-button href="${DEFAULT_BUTTON_URL}" background-color="${PALETTE.navy}" color="${PALETTE.white}" font-family="${FONT_STACK}" font-size="15px" font-weight="bold" border-radius="6px" inner-padding="12px 28px" align="center">Learn more</mj-button>`;
|
|
559
|
+
var buttonBlock = {
|
|
560
|
+
id: "button",
|
|
561
|
+
label: "Button",
|
|
562
|
+
description: "Call to action",
|
|
563
|
+
icon: "RectangleHorizontal",
|
|
564
|
+
kind: "content",
|
|
565
|
+
tagName: "mj-button",
|
|
566
|
+
order: 30,
|
|
567
|
+
mjml: MJML3,
|
|
568
|
+
sectionMjml: inWhiteSection(MJML3),
|
|
569
|
+
editableContent: true,
|
|
570
|
+
properties: [
|
|
571
|
+
prop.href("Link URL"),
|
|
572
|
+
prop.background("background-color", "Button background"),
|
|
573
|
+
prop.color("Text"),
|
|
574
|
+
prop.fontFamily(),
|
|
575
|
+
prop.fontSize(11, 28),
|
|
576
|
+
{
|
|
577
|
+
key: "font-weight",
|
|
578
|
+
label: "Weight",
|
|
579
|
+
type: "select",
|
|
580
|
+
group: GROUPS.typography,
|
|
581
|
+
options: [
|
|
582
|
+
{ value: "bold", label: "Bold" },
|
|
583
|
+
{ value: "normal", label: "Normal" }
|
|
584
|
+
]
|
|
585
|
+
},
|
|
586
|
+
prop.radius(30),
|
|
587
|
+
{
|
|
588
|
+
key: "width",
|
|
589
|
+
label: "Width",
|
|
590
|
+
type: "select",
|
|
591
|
+
group: GROUPS.layout,
|
|
592
|
+
options: [
|
|
593
|
+
{ value: "", label: "Fit to text" },
|
|
594
|
+
{ value: "100%", label: "Full width" }
|
|
595
|
+
]
|
|
596
|
+
},
|
|
597
|
+
prop.align(),
|
|
598
|
+
prop.padding("inner-padding", "Button padding"),
|
|
599
|
+
prop.padding()
|
|
600
|
+
]
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// src/blocks/divider/index.js
|
|
604
|
+
var MJML4 = `<mj-divider border-color="${PALETTE.border}" border-width="1px"></mj-divider>`;
|
|
605
|
+
var dividerBlock = {
|
|
606
|
+
id: "divider",
|
|
607
|
+
label: "Divider",
|
|
608
|
+
description: "Horizontal rule",
|
|
609
|
+
icon: "SeparatorHorizontal",
|
|
610
|
+
kind: "content",
|
|
611
|
+
tagName: "mj-divider",
|
|
612
|
+
order: 40,
|
|
613
|
+
mjml: MJML4,
|
|
614
|
+
sectionMjml: inWhiteSection(MJML4),
|
|
615
|
+
properties: [
|
|
616
|
+
prop.background("border-color", "Line color"),
|
|
617
|
+
{ key: "border-width", label: "Thickness", type: "size", group: GROUPS.layout, min: 1, max: 10, unit: "px" },
|
|
618
|
+
{
|
|
619
|
+
key: "border-style",
|
|
620
|
+
label: "Line style",
|
|
621
|
+
type: "select",
|
|
622
|
+
group: GROUPS.layout,
|
|
623
|
+
options: [
|
|
624
|
+
{ value: "solid", label: "Solid" },
|
|
625
|
+
{ value: "dashed", label: "Dashed" },
|
|
626
|
+
{ value: "dotted", label: "Dotted" }
|
|
627
|
+
]
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
key: "width",
|
|
631
|
+
label: "Length",
|
|
632
|
+
type: "select",
|
|
633
|
+
group: GROUPS.layout,
|
|
634
|
+
options: [
|
|
635
|
+
{ value: "100%", label: "Full length" },
|
|
636
|
+
{ value: "75%", label: "75%" },
|
|
637
|
+
{ value: "50%", label: "50%" },
|
|
638
|
+
{ value: "25%", label: "25%" }
|
|
639
|
+
]
|
|
640
|
+
},
|
|
641
|
+
prop.align(),
|
|
642
|
+
prop.padding()
|
|
643
|
+
]
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
// src/blocks/spacer/index.js
|
|
647
|
+
var MJML5 = '<mj-spacer height="24px"></mj-spacer>';
|
|
648
|
+
var spacerBlock = {
|
|
649
|
+
id: "spacer",
|
|
650
|
+
label: "Spacer",
|
|
651
|
+
description: "Vertical space",
|
|
652
|
+
icon: "MoveVertical",
|
|
653
|
+
kind: "content",
|
|
654
|
+
tagName: "mj-spacer",
|
|
655
|
+
order: 50,
|
|
656
|
+
mjml: MJML5,
|
|
657
|
+
sectionMjml: inWhiteSection(MJML5),
|
|
658
|
+
properties: [
|
|
659
|
+
{ key: "height", label: "Height", type: "size", group: GROUPS.layout, min: 4, max: 200, unit: "px" },
|
|
660
|
+
prop.background("container-background-color", "Background")
|
|
661
|
+
]
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// src/blocks/heading/index.js
|
|
665
|
+
var MJML6 = `<mj-text font-family="${FONT_STACK}" font-size="26px" line-height="34px" font-weight="bold" color="${PALETTE.navy}" align="left">Your headline</mj-text>`;
|
|
666
|
+
var headingBlock = {
|
|
667
|
+
id: "heading",
|
|
668
|
+
label: "Heading",
|
|
669
|
+
description: "Large bold title",
|
|
670
|
+
icon: "Heading",
|
|
671
|
+
kind: "content",
|
|
672
|
+
tagName: "mj-text",
|
|
673
|
+
insertOnly: true,
|
|
674
|
+
order: 5,
|
|
675
|
+
mjml: MJML6,
|
|
676
|
+
sectionMjml: inWhiteSection(MJML6),
|
|
677
|
+
editableContent: true,
|
|
678
|
+
properties: []
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// src/blocks/navbar/index.js
|
|
682
|
+
function link(label, href) {
|
|
683
|
+
return `<mj-navbar-link href="${href}" font-family="${FONT_STACK}" font-size="14px" color="${PALETTE.navy}" padding-top="8px" padding-right="12px" padding-bottom="8px" padding-left="12px">${label}</mj-navbar-link>`;
|
|
684
|
+
}
|
|
685
|
+
var MJML7 = '<mj-navbar align="center">' + link("Shop", "https://example.com/shop") + link("Blog", "https://example.com/blog") + link("Contact", "https://example.com/contact") + "</mj-navbar>";
|
|
686
|
+
var navbarBlock = {
|
|
687
|
+
id: "navbar",
|
|
688
|
+
label: "Menu",
|
|
689
|
+
description: "Row of links to your site",
|
|
690
|
+
icon: "Menu",
|
|
691
|
+
kind: "content",
|
|
692
|
+
tagName: "mj-navbar",
|
|
693
|
+
order: 70,
|
|
694
|
+
mjml: MJML7,
|
|
695
|
+
sectionMjml: inWhiteSection(MJML7),
|
|
696
|
+
properties: [prop.align()]
|
|
697
|
+
};
|
|
698
|
+
var navbarLinkBlock = {
|
|
699
|
+
id: "navbar-link",
|
|
700
|
+
label: "Menu link",
|
|
701
|
+
icon: "Link",
|
|
702
|
+
kind: "structure",
|
|
703
|
+
tagName: "mj-navbar-link",
|
|
704
|
+
order: 904,
|
|
705
|
+
editableContent: true,
|
|
706
|
+
properties: [
|
|
707
|
+
prop.href("Link URL"),
|
|
708
|
+
prop.color("Text"),
|
|
709
|
+
prop.fontFamily(),
|
|
710
|
+
prop.fontSize(10, 24),
|
|
711
|
+
prop.padding()
|
|
712
|
+
]
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// src/blocks/hero/index.js
|
|
716
|
+
var MJML8 = `<mj-section background-color="${PALETTE.navy}" background-size="cover" background-position="center center" background-repeat="no-repeat" padding-top="56px" padding-bottom="56px"><mj-column><mj-text font-family="${FONT_STACK}" font-size="30px" line-height="38px" font-weight="bold" color="${PALETTE.white}" align="center" padding-left="32px" padding-right="32px">Your big announcement</mj-text><mj-text font-family="${FONT_STACK}" font-size="16px" line-height="24px" color="${PALETTE.border}" align="center" padding-left="32px" padding-right="32px">One sentence that makes people want to read on.</mj-text><mj-button href="${DEFAULT_BUTTON_URL}" background-color="${PALETTE.white}" color="${PALETTE.navy}" font-family="${FONT_STACK}" font-size="15px" font-weight="bold" border-radius="6px" inner-padding="12px 28px" align="center">Learn more</mj-button></mj-column></mj-section>`;
|
|
717
|
+
var heroBlock = {
|
|
718
|
+
id: "hero",
|
|
719
|
+
label: "Hero banner",
|
|
720
|
+
description: "Background image with a title and a button",
|
|
721
|
+
icon: "GalleryHorizontal",
|
|
722
|
+
kind: "layout",
|
|
723
|
+
tagName: "mj-section",
|
|
724
|
+
order: 110,
|
|
725
|
+
mjml: MJML8,
|
|
726
|
+
properties: []
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
// src/blocks/columns/index.js
|
|
730
|
+
function sectionWith(count) {
|
|
731
|
+
const columns = Array.from(
|
|
732
|
+
{ length: count },
|
|
733
|
+
(_, i) => `<mj-column>${textMjml(count === 1 ? "Your text." : `Column ${i + 1}`)}</mj-column>`
|
|
734
|
+
).join("");
|
|
735
|
+
return `<mj-section background-color="${PALETTE.surface}">${columns}</mj-section>`;
|
|
736
|
+
}
|
|
737
|
+
function layout(count, icon) {
|
|
738
|
+
return {
|
|
739
|
+
id: `columns-${count}`,
|
|
740
|
+
label: count === 1 ? "1 column" : `${count} columns`,
|
|
741
|
+
description: count === 1 ? "Full-width section" : `${count}-column section`,
|
|
742
|
+
icon,
|
|
743
|
+
kind: "layout",
|
|
744
|
+
tagName: "mj-section",
|
|
745
|
+
order: 100 + count,
|
|
746
|
+
mjml: sectionWith(count),
|
|
747
|
+
properties: []
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
var oneColumnBlock = layout(1, "Square");
|
|
751
|
+
var twoColumnsBlock = layout(2, "Columns2");
|
|
752
|
+
var threeColumnsBlock = layout(3, "Columns3");
|
|
753
|
+
var fourColumnsBlock = layout(4, "Columns4");
|
|
754
|
+
var IMAGE_TEXT_MJML = `<mj-section background-color="${PALETTE.surface}"><mj-column vertical-align="middle"><mj-image align="center"></mj-image></mj-column><mj-column vertical-align="middle"><mj-text font-family="${FONT_STACK}" font-size="20px" line-height="28px" font-weight="bold" color="${PALETTE.navy}" align="left">Title</mj-text>` + textMjml("A few words about it.") + `<mj-button href="${DEFAULT_BUTTON_URL}" background-color="${PALETTE.navy}" color="${PALETTE.white}" font-family="${FONT_STACK}" font-size="14px" font-weight="bold" border-radius="6px" inner-padding="10px 22px" align="left">Learn more</mj-button></mj-column></mj-section>`;
|
|
755
|
+
var imageTextBlock = {
|
|
756
|
+
id: "image-text",
|
|
757
|
+
label: "Image + text",
|
|
758
|
+
description: "Image beside a title, text and button",
|
|
759
|
+
icon: "LayoutPanelLeft",
|
|
760
|
+
kind: "layout",
|
|
761
|
+
tagName: "mj-section",
|
|
762
|
+
order: 105,
|
|
763
|
+
mjml: IMAGE_TEXT_MJML,
|
|
764
|
+
properties: []
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
// src/blocks/index.js
|
|
768
|
+
var BLOCKS = [
|
|
769
|
+
headingBlock,
|
|
770
|
+
textBlock,
|
|
771
|
+
imageBlock,
|
|
772
|
+
buttonBlock,
|
|
773
|
+
dividerBlock,
|
|
774
|
+
spacerBlock,
|
|
775
|
+
socialBlock,
|
|
776
|
+
navbarBlock,
|
|
777
|
+
oneColumnBlock,
|
|
778
|
+
twoColumnsBlock,
|
|
779
|
+
threeColumnsBlock,
|
|
780
|
+
fourColumnsBlock,
|
|
781
|
+
imageTextBlock,
|
|
782
|
+
heroBlock,
|
|
783
|
+
bodyBlock,
|
|
784
|
+
sectionBlock,
|
|
785
|
+
columnBlock,
|
|
786
|
+
socialElementBlock,
|
|
787
|
+
navbarLinkBlock
|
|
788
|
+
];
|
|
789
|
+
var emailBlocks = createRegistry("emailBlocks", BLOCKS);
|
|
790
|
+
|
|
791
|
+
// src/features/autosave/index.js
|
|
792
|
+
var autosaveFeature = {
|
|
793
|
+
id: "autosave",
|
|
794
|
+
label: "Autosave",
|
|
795
|
+
order: 20,
|
|
796
|
+
options: { idleMs: 2e4, maxWaitMs: 12e4, draftDelayMs: 1500 }
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// src/features/versions/VersionsMenu.jsx
|
|
800
|
+
import { useState } from "react";
|
|
801
|
+
import { History } from "lucide-react";
|
|
802
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
803
|
+
function formatWhen(value, locale) {
|
|
804
|
+
if (!value) return "";
|
|
805
|
+
const d = new Date(value);
|
|
806
|
+
return Number.isNaN(d.getTime()) ? "" : d.toLocaleString(locale, { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
807
|
+
}
|
|
808
|
+
function VersionsMenu({ context }) {
|
|
809
|
+
const { editor, storage, template, isDirty, markDirty, ready } = context;
|
|
810
|
+
const { t, confirm, notify, locale } = useUi();
|
|
811
|
+
const [open, setOpen] = useState(false);
|
|
812
|
+
const [versions, setVersions] = useState(null);
|
|
813
|
+
const [error, setError] = useState(null);
|
|
814
|
+
const [restoring, setRestoring] = useState(null);
|
|
815
|
+
const load = async () => {
|
|
816
|
+
setVersions(null);
|
|
817
|
+
setError(null);
|
|
818
|
+
try {
|
|
819
|
+
setVersions(await storage.listVersions(template.id));
|
|
820
|
+
} catch (e) {
|
|
821
|
+
setError(e.message);
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
const restore = async (version) => {
|
|
825
|
+
if (isDirty()) {
|
|
826
|
+
const ok = await confirm({
|
|
827
|
+
title: t("Restore version {n}?", { n: version.versionNumber }),
|
|
828
|
+
description: t("Unsaved changes will be replaced."),
|
|
829
|
+
confirmLabel: t("Restore"),
|
|
830
|
+
destructive: true
|
|
831
|
+
});
|
|
832
|
+
if (!ok) return;
|
|
833
|
+
}
|
|
834
|
+
setRestoring(version.id);
|
|
835
|
+
try {
|
|
836
|
+
const full = await storage.getVersion(template.id, version.id);
|
|
837
|
+
editor.loadDocument(full.document);
|
|
838
|
+
markDirty();
|
|
839
|
+
setOpen(false);
|
|
840
|
+
notify("success", t("Version {n} restored \u2014 save to keep it.", { n: version.versionNumber }));
|
|
841
|
+
} catch (e) {
|
|
842
|
+
notify("error", e.message);
|
|
843
|
+
} finally {
|
|
844
|
+
setRestoring(null);
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
return /* @__PURE__ */ jsxs(
|
|
848
|
+
Popover,
|
|
849
|
+
{
|
|
850
|
+
open,
|
|
851
|
+
onOpenChange: (next) => {
|
|
852
|
+
setOpen(next);
|
|
853
|
+
if (next) load();
|
|
854
|
+
},
|
|
855
|
+
children: [
|
|
856
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
857
|
+
Button,
|
|
858
|
+
{
|
|
859
|
+
variant: "outline",
|
|
860
|
+
size: "sm-compact",
|
|
861
|
+
disabled: !ready || !template.id,
|
|
862
|
+
title: !template.id ? t("Save the template first") : void 0,
|
|
863
|
+
children: [
|
|
864
|
+
/* @__PURE__ */ jsx(History, {}),
|
|
865
|
+
t("Versions")
|
|
866
|
+
]
|
|
867
|
+
}
|
|
868
|
+
) }),
|
|
869
|
+
/* @__PURE__ */ jsxs(PopoverContent, { align: "end", className: "eb:w-80 eb:p-0", children: [
|
|
870
|
+
/* @__PURE__ */ jsx("div", { className: "eb:border-b eb:border-border eb:px-3 eb:py-2.5 eb:text-[13px] eb:font-medium eb:text-foreground", children: t("Versions") }),
|
|
871
|
+
/* @__PURE__ */ jsx("div", { className: "eb:max-h-[320px] eb:overflow-y-auto eb:p-1", "aria-busy": versions === null && !error, children: error ? /* @__PURE__ */ jsx("p", { role: "alert", className: "eb:px-2 eb:py-1.5 eb:text-[12px] eb:text-destructive", children: error }) : versions === null ? /* @__PURE__ */ jsxs("div", { className: "eb:space-y-1 eb:p-1", "aria-hidden": "true", children: [
|
|
872
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "eb:h-8 eb:w-40" }),
|
|
873
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "eb:h-8 eb:w-32" }),
|
|
874
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "eb:h-8 eb:w-36" })
|
|
875
|
+
] }) : versions.length === 0 ? /* @__PURE__ */ jsx("p", { className: "eb:px-2 eb:py-1.5 eb:text-[12px] eb:text-foreground-muted", children: t("No versions yet.") }) : versions.map((version, index) => /* @__PURE__ */ jsxs(
|
|
876
|
+
"div",
|
|
877
|
+
{
|
|
878
|
+
className: "eb:flex eb:items-center eb:gap-2 eb:rounded-lg2 eb:px-2 eb:py-1.5 eb:hover:bg-surface-100",
|
|
879
|
+
children: [
|
|
880
|
+
/* @__PURE__ */ jsxs("div", { className: "eb:min-w-0 eb:flex-1", children: [
|
|
881
|
+
/* @__PURE__ */ jsxs("p", { className: "eb:text-[13px] eb:text-foreground", children: [
|
|
882
|
+
t("Version {n}", { n: version.versionNumber }),
|
|
883
|
+
index === 0 && /* @__PURE__ */ jsx("span", { className: "eb:ml-1.5 eb:text-[11px] eb:text-foreground-muted", children: t("current") })
|
|
884
|
+
] }),
|
|
885
|
+
/* @__PURE__ */ jsxs("p", { className: "eb:text-[11px] eb:tabular-nums eb:text-foreground-muted", children: [
|
|
886
|
+
formatWhen(version.createdAt, locale),
|
|
887
|
+
version.createdBy ? ` \xB7 ${version.createdBy}` : ""
|
|
888
|
+
] })
|
|
889
|
+
] }),
|
|
890
|
+
/* @__PURE__ */ jsx(
|
|
891
|
+
Button,
|
|
892
|
+
{
|
|
893
|
+
variant: "ghost",
|
|
894
|
+
size: "xs",
|
|
895
|
+
disabled: restoring !== null,
|
|
896
|
+
onClick: () => restore(version),
|
|
897
|
+
"aria-label": t("Restore version {n}", { n: version.versionNumber }),
|
|
898
|
+
children: t("Restore")
|
|
899
|
+
}
|
|
900
|
+
)
|
|
901
|
+
]
|
|
902
|
+
},
|
|
903
|
+
version.id
|
|
904
|
+
)) })
|
|
905
|
+
] })
|
|
906
|
+
]
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/features/versions/index.js
|
|
912
|
+
var versionsFeature = {
|
|
913
|
+
id: "versions",
|
|
914
|
+
label: "Versions",
|
|
915
|
+
order: 30,
|
|
916
|
+
toolbar: VersionsMenu
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// src/features/variables/VariablesMenu.jsx
|
|
920
|
+
import { useState as useState4 } from "react";
|
|
921
|
+
import { Braces as Braces2 } from "lucide-react";
|
|
922
|
+
|
|
923
|
+
// src/features/variables/VariableDialog.jsx
|
|
924
|
+
import { useEffect, useMemo, useState as useState2 } from "react";
|
|
925
|
+
import { Braces, Loader2, Trash2 } from "lucide-react";
|
|
926
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
927
|
+
function VariableDialog({ open, onOpenChange, variable = null, initialLabel = "", variables, storage, onSaved, onDeleted }) {
|
|
928
|
+
const { t, notify, confirm } = useUi();
|
|
929
|
+
const editing = Boolean(variable);
|
|
930
|
+
const [form, setForm] = useState2({ label: "", key: "", group: "", sample: "", fallback: "" });
|
|
931
|
+
const [keyTouched, setKeyTouched] = useState2(false);
|
|
932
|
+
const [errors, setErrors] = useState2({});
|
|
933
|
+
const [busy, setBusy] = useState2(false);
|
|
934
|
+
useEffect(() => {
|
|
935
|
+
if (!open) return;
|
|
936
|
+
setForm(
|
|
937
|
+
variable ? {
|
|
938
|
+
label: variable.label,
|
|
939
|
+
key: variable.key,
|
|
940
|
+
group: variable.group || "",
|
|
941
|
+
sample: variable.sample || "",
|
|
942
|
+
fallback: variable.fallback || ""
|
|
943
|
+
} : { label: initialLabel, key: initialLabel ? keyFromLabel(initialLabel) : "", group: "", sample: "", fallback: "" }
|
|
944
|
+
);
|
|
945
|
+
setKeyTouched(false);
|
|
946
|
+
setErrors({});
|
|
947
|
+
}, [open, variable, initialLabel]);
|
|
948
|
+
const groups = useMemo(
|
|
949
|
+
() => [.../* @__PURE__ */ new Set([CUSTOM_GROUP, ...variables.filter((v) => v.custom).map((v) => v.group)])].filter(Boolean),
|
|
950
|
+
[variables]
|
|
951
|
+
);
|
|
952
|
+
const set = (field2) => (e) => {
|
|
953
|
+
const value = e.target.value;
|
|
954
|
+
setForm((f) => {
|
|
955
|
+
const next = { ...f, [field2]: value };
|
|
956
|
+
if (field2 === "label" && !editing && !keyTouched) next.key = value.trim() ? keyFromLabel(value) : "";
|
|
957
|
+
return next;
|
|
958
|
+
});
|
|
959
|
+
if (field2 === "key") setKeyTouched(true);
|
|
960
|
+
setErrors((errs) => ({ ...errs, [field2]: void 0 }));
|
|
961
|
+
};
|
|
962
|
+
const submit = async (e) => {
|
|
963
|
+
e.preventDefault();
|
|
964
|
+
const existing = variables.filter((v) => !editing || v.key !== variable.key);
|
|
965
|
+
const check = validateCustomVariable(form, { existing, editing: editing ? variable.key : null, t });
|
|
966
|
+
if (check.errors) {
|
|
967
|
+
setErrors(check.errors);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
setBusy(true);
|
|
971
|
+
try {
|
|
972
|
+
const saved = await storage.saveVariable(check.variable, { create: !editing });
|
|
973
|
+
onSaved(saved, !editing);
|
|
974
|
+
onOpenChange(false);
|
|
975
|
+
} catch (error) {
|
|
976
|
+
notify("error", error.message);
|
|
977
|
+
} finally {
|
|
978
|
+
setBusy(false);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
const remove = async () => {
|
|
982
|
+
const ok = await confirm({
|
|
983
|
+
title: t("Delete the variable \u201C{name}\u201D?", { name: variable.label }),
|
|
984
|
+
description: t("Templates that use it will show its \u201Cvalue if empty\u201D."),
|
|
985
|
+
confirmLabel: t("Delete"),
|
|
986
|
+
destructive: true
|
|
987
|
+
});
|
|
988
|
+
if (!ok) return;
|
|
989
|
+
setBusy(true);
|
|
990
|
+
try {
|
|
991
|
+
await storage.deleteVariable(variable.key);
|
|
992
|
+
onDeleted?.(variable.key);
|
|
993
|
+
onOpenChange(false);
|
|
994
|
+
} catch (error) {
|
|
995
|
+
notify("error", error.message);
|
|
996
|
+
} finally {
|
|
997
|
+
setBusy(false);
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
const field = (name, label, props = {}, hint = null) => /* @__PURE__ */ jsxs2("div", { className: "eb:space-y-1.5", children: [
|
|
1001
|
+
/* @__PURE__ */ jsx2(Label, { htmlFor: `eb-var-${name}`, children: label }),
|
|
1002
|
+
/* @__PURE__ */ jsx2(
|
|
1003
|
+
Input,
|
|
1004
|
+
{
|
|
1005
|
+
id: `eb-var-${name}`,
|
|
1006
|
+
value: form[name],
|
|
1007
|
+
onChange: set(name),
|
|
1008
|
+
"aria-invalid": errors[name] ? true : void 0,
|
|
1009
|
+
"aria-describedby": errors[name] || hint ? `eb-var-${name}-hint` : void 0,
|
|
1010
|
+
...props
|
|
1011
|
+
}
|
|
1012
|
+
),
|
|
1013
|
+
(errors[name] || hint) && /* @__PURE__ */ jsx2("p", { id: `eb-var-${name}-hint`, className: errors[name] ? "eb:text-[12px] eb:text-destructive" : "eb:text-[11px] eb:text-foreground-muted", children: errors[name] || hint })
|
|
1014
|
+
] });
|
|
1015
|
+
return /* @__PURE__ */ jsx2(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs2(
|
|
1016
|
+
DialogContent,
|
|
1017
|
+
{
|
|
1018
|
+
className: "eb:sm:max-w-[440px]",
|
|
1019
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
1020
|
+
onCloseAutoFocus: (e) => e.preventDefault(),
|
|
1021
|
+
children: [
|
|
1022
|
+
/* @__PURE__ */ jsx2(DialogHeader, { children: /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:items-center eb:gap-3", children: [
|
|
1023
|
+
/* @__PURE__ */ jsx2("span", { className: "eb:flex eb:size-10 eb:shrink-0 eb:items-center eb:justify-center eb:rounded-lg2 eb:bg-primary/10 eb:text-primary", children: /* @__PURE__ */ jsx2(Braces, { className: "eb:size-5" }) }),
|
|
1024
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
1025
|
+
/* @__PURE__ */ jsx2(DialogTitle, { className: "eb:text-base eb:font-normal", children: editing ? t("Edit variable") : t("New variable") }),
|
|
1026
|
+
/* @__PURE__ */ jsx2(DialogDescription, { className: "eb:text-[13px]", children: t("Available in every template.") })
|
|
1027
|
+
] })
|
|
1028
|
+
] }) }),
|
|
1029
|
+
/* @__PURE__ */ jsxs2("form", { onSubmit: submit, className: "eb:space-y-3", noValidate: true, children: [
|
|
1030
|
+
field("label", t("Name"), { autoFocus: true, maxLength: VARIABLE_LIMITS.label, placeholder: t("Order number") }),
|
|
1031
|
+
field(
|
|
1032
|
+
"key",
|
|
1033
|
+
t("Tag"),
|
|
1034
|
+
{ disabled: editing, maxLength: VARIABLE_LIMITS.key, className: "eb:font-mono eb:text-[12px]" },
|
|
1035
|
+
form.key ? `{{${form.key}}}` : null
|
|
1036
|
+
),
|
|
1037
|
+
/* @__PURE__ */ jsxs2("div", { className: "eb:space-y-1.5", children: [
|
|
1038
|
+
/* @__PURE__ */ jsx2(Label, { htmlFor: "eb-var-group", children: t("Group") }),
|
|
1039
|
+
/* @__PURE__ */ jsx2(
|
|
1040
|
+
Input,
|
|
1041
|
+
{
|
|
1042
|
+
id: "eb-var-group",
|
|
1043
|
+
list: "eb-var-groups",
|
|
1044
|
+
value: form.group,
|
|
1045
|
+
onChange: set("group"),
|
|
1046
|
+
maxLength: VARIABLE_LIMITS.group,
|
|
1047
|
+
placeholder: t(CUSTOM_GROUP)
|
|
1048
|
+
}
|
|
1049
|
+
),
|
|
1050
|
+
/* @__PURE__ */ jsx2("datalist", { id: "eb-var-groups", children: groups.map((g) => /* @__PURE__ */ jsx2("option", { value: g }, g)) })
|
|
1051
|
+
] }),
|
|
1052
|
+
/* @__PURE__ */ jsxs2("div", { className: "eb:grid eb:grid-cols-2 eb:gap-3", children: [
|
|
1053
|
+
field("sample", t("Sample value"), { maxLength: VARIABLE_LIMITS.sample, placeholder: "CMD-1042" }, t("Preview and tests")),
|
|
1054
|
+
field("fallback", t("Value if empty"), { maxLength: VARIABLE_LIMITS.fallback }, t("When a recipient has none"))
|
|
1055
|
+
] }),
|
|
1056
|
+
/* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:items-center eb:gap-2 eb:pt-1", children: [
|
|
1057
|
+
editing && /* @__PURE__ */ jsxs2(Button, { variant: "ghost", className: "eb:text-destructive eb:hover:text-destructive", onClick: remove, disabled: busy, children: [
|
|
1058
|
+
/* @__PURE__ */ jsx2(Trash2, {}),
|
|
1059
|
+
t("Delete")
|
|
1060
|
+
] }),
|
|
1061
|
+
/* @__PURE__ */ jsxs2("div", { className: "eb:ml-auto eb:flex eb:gap-2", children: [
|
|
1062
|
+
/* @__PURE__ */ jsx2(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: busy, children: t("Cancel") }),
|
|
1063
|
+
/* @__PURE__ */ jsxs2(Button, { type: "submit", disabled: busy, children: [
|
|
1064
|
+
busy && /* @__PURE__ */ jsx2(Loader2, { className: "eb:animate-spin" }),
|
|
1065
|
+
editing ? t("Save") : t("Create")
|
|
1066
|
+
] })
|
|
1067
|
+
] })
|
|
1068
|
+
] })
|
|
1069
|
+
] })
|
|
1070
|
+
]
|
|
1071
|
+
}
|
|
1072
|
+
) });
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/features/variables/VariablePicker.jsx
|
|
1076
|
+
import { useEffect as useEffect2, useMemo as useMemo2, useRef, useState as useState3 } from "react";
|
|
1077
|
+
import { Pencil, Plus, Search } from "lucide-react";
|
|
1078
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1079
|
+
function VariablePicker({ variables, onPick, onCreate = null, onEdit = null, filter }) {
|
|
1080
|
+
const t = useT();
|
|
1081
|
+
const [query, setQuery] = useState3("");
|
|
1082
|
+
const [active, setActive] = useState3(0);
|
|
1083
|
+
const listRef = useRef(null);
|
|
1084
|
+
const matches = useMemo2(() => {
|
|
1085
|
+
const q = query.trim().toLowerCase();
|
|
1086
|
+
return variables.filter((v) => filter ? filter(v) : true).filter(
|
|
1087
|
+
(v) => !q || t(v.label).toLowerCase().includes(q) || v.key.toLowerCase().includes(q) || t(v.group).toLowerCase().includes(q)
|
|
1088
|
+
);
|
|
1089
|
+
}, [variables, query, filter, t]);
|
|
1090
|
+
const groups = useMemo2(() => [...new Set(matches.map((v) => v.group))], [matches]);
|
|
1091
|
+
const ordered = useMemo2(() => groups.flatMap((g) => matches.filter((v) => v.group === g)), [groups, matches]);
|
|
1092
|
+
useEffect2(() => setActive(0), [query]);
|
|
1093
|
+
useEffect2(() => {
|
|
1094
|
+
listRef.current?.querySelector('[data-active="true"]')?.scrollIntoView({ block: "nearest" });
|
|
1095
|
+
}, [active]);
|
|
1096
|
+
const onKeyDown = (e) => {
|
|
1097
|
+
if (e.key === "ArrowDown") {
|
|
1098
|
+
e.preventDefault();
|
|
1099
|
+
setActive((i) => Math.min(i + 1, ordered.length - 1));
|
|
1100
|
+
} else if (e.key === "ArrowUp") {
|
|
1101
|
+
e.preventDefault();
|
|
1102
|
+
setActive((i) => Math.max(i - 1, 0));
|
|
1103
|
+
} else if (e.key === "Enter") {
|
|
1104
|
+
e.preventDefault();
|
|
1105
|
+
if (ordered[active]) onPick(ordered[active]);
|
|
1106
|
+
else if (onCreate && query.trim()) onCreate(query.trim());
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
return /* @__PURE__ */ jsxs3("div", { className: "eb:flex eb:max-h-[min(440px,70vh)] eb:flex-col", "data-testid": "variable-picker", children: [
|
|
1110
|
+
/* @__PURE__ */ jsxs3("div", { className: "eb:relative eb:border-b eb:border-border eb:p-2", children: [
|
|
1111
|
+
/* @__PURE__ */ jsx3(Search, { className: "eb:pointer-events-none eb:absolute eb:top-1/2 eb:left-4 eb:size-3.5 eb:-translate-y-1/2 eb:text-foreground-muted" }),
|
|
1112
|
+
/* @__PURE__ */ jsx3(
|
|
1113
|
+
"input",
|
|
1114
|
+
{
|
|
1115
|
+
autoFocus: true,
|
|
1116
|
+
value: query,
|
|
1117
|
+
onChange: (e) => setQuery(e.target.value),
|
|
1118
|
+
onKeyDown,
|
|
1119
|
+
placeholder: t("Search variables\u2026"),
|
|
1120
|
+
"aria-label": t("Search variables\u2026"),
|
|
1121
|
+
className: "eb:h-8 eb:w-full eb:rounded-md eb:bg-surface-75 eb:pr-2 eb:pl-7 eb:text-[13px] eb:text-foreground eb:outline-none eb:placeholder:text-foreground-muted eb:focus-visible:ring-2 eb:focus-visible:ring-ring/40"
|
|
1122
|
+
}
|
|
1123
|
+
)
|
|
1124
|
+
] }),
|
|
1125
|
+
/* @__PURE__ */ jsxs3("div", { ref: listRef, role: "listbox", "aria-label": t("Variables"), className: "eb:min-h-0 eb:flex-1 eb:overflow-y-auto eb:p-1", children: [
|
|
1126
|
+
ordered.length === 0 && /* @__PURE__ */ jsxs3("div", { className: "eb:px-2 eb:py-3 eb:text-center eb:text-[12px] eb:text-foreground-muted", children: [
|
|
1127
|
+
/* @__PURE__ */ jsx3("p", { children: t("No variable found.") }),
|
|
1128
|
+
onCreate && query.trim() && /* @__PURE__ */ jsxs3(
|
|
1129
|
+
"button",
|
|
1130
|
+
{
|
|
1131
|
+
type: "button",
|
|
1132
|
+
onClick: () => onCreate(query.trim()),
|
|
1133
|
+
className: "eb:mt-2 eb:inline-flex eb:items-center eb:gap-1 eb:rounded-md eb:px-2 eb:py-1 eb:text-[12px] eb:font-medium eb:text-primary eb:hover:bg-accent",
|
|
1134
|
+
children: [
|
|
1135
|
+
/* @__PURE__ */ jsx3(Plus, { className: "eb:size-3.5" }),
|
|
1136
|
+
t("Create \u201C{name}\u201D", { name: query.trim() })
|
|
1137
|
+
]
|
|
1138
|
+
}
|
|
1139
|
+
)
|
|
1140
|
+
] }),
|
|
1141
|
+
groups.map((group) => /* @__PURE__ */ jsxs3("div", { role: "group", "aria-label": t(group), children: [
|
|
1142
|
+
/* @__PURE__ */ jsx3("p", { className: "eb:px-2 eb:pt-2 eb:pb-1 eb:text-[11px] eb:text-foreground-muted", children: t(group) }),
|
|
1143
|
+
ordered.filter((v) => v.group === group).map((variable) => {
|
|
1144
|
+
const index = ordered.indexOf(variable);
|
|
1145
|
+
return /* @__PURE__ */ jsxs3(
|
|
1146
|
+
"div",
|
|
1147
|
+
{
|
|
1148
|
+
"data-active": index === active,
|
|
1149
|
+
className: cn(
|
|
1150
|
+
"eb:group/var eb:flex eb:items-center eb:gap-1 eb:rounded-md",
|
|
1151
|
+
index === active ? "eb:bg-accent eb:text-accent-foreground" : "eb:hover:bg-accent/60"
|
|
1152
|
+
),
|
|
1153
|
+
onMouseEnter: () => setActive(index),
|
|
1154
|
+
children: [
|
|
1155
|
+
/* @__PURE__ */ jsxs3(
|
|
1156
|
+
"button",
|
|
1157
|
+
{
|
|
1158
|
+
type: "button",
|
|
1159
|
+
role: "option",
|
|
1160
|
+
"aria-selected": index === active,
|
|
1161
|
+
"data-variable": variable.key,
|
|
1162
|
+
onClick: () => onPick(variable),
|
|
1163
|
+
className: "eb:flex eb:min-w-0 eb:flex-1 eb:items-center eb:gap-2 eb:px-2 eb:py-1.5 eb:text-left eb:text-[13px]",
|
|
1164
|
+
title: variable.description || void 0,
|
|
1165
|
+
children: [
|
|
1166
|
+
/* @__PURE__ */ jsx3("span", { className: "eb:min-w-0 eb:flex-1 eb:truncate", children: variable.custom ? variable.label : t(variable.label) }),
|
|
1167
|
+
/* @__PURE__ */ jsx3("code", { className: "eb:shrink-0 eb:font-mono eb:text-[10.5px] eb:text-foreground-muted", children: tokenOf(variable) })
|
|
1168
|
+
]
|
|
1169
|
+
}
|
|
1170
|
+
),
|
|
1171
|
+
onEdit && variable.custom && /* @__PURE__ */ jsx3(
|
|
1172
|
+
"button",
|
|
1173
|
+
{
|
|
1174
|
+
type: "button",
|
|
1175
|
+
"aria-label": t("Edit \u201C{name}\u201D", { name: variable.label }),
|
|
1176
|
+
title: t("Edit"),
|
|
1177
|
+
onClick: () => onEdit(variable),
|
|
1178
|
+
className: "eb:mr-1 eb:flex eb:size-6 eb:shrink-0 eb:items-center eb:justify-center eb:rounded-md eb:text-foreground-muted eb:opacity-0 eb:group-hover/var:opacity-100 eb:hover:bg-background eb:hover:text-foreground eb:focus-visible:opacity-100",
|
|
1179
|
+
children: /* @__PURE__ */ jsx3(Pencil, { className: "eb:size-3" })
|
|
1180
|
+
}
|
|
1181
|
+
)
|
|
1182
|
+
]
|
|
1183
|
+
},
|
|
1184
|
+
variable.key
|
|
1185
|
+
);
|
|
1186
|
+
})
|
|
1187
|
+
] }, group))
|
|
1188
|
+
] }),
|
|
1189
|
+
onCreate && /* @__PURE__ */ jsx3("div", { className: "eb:border-t eb:border-border eb:p-1", children: /* @__PURE__ */ jsxs3(
|
|
1190
|
+
"button",
|
|
1191
|
+
{
|
|
1192
|
+
type: "button",
|
|
1193
|
+
onClick: () => onCreate(query.trim()),
|
|
1194
|
+
className: "eb:flex eb:w-full eb:items-center eb:gap-2 eb:rounded-md eb:px-2 eb:py-1.5 eb:text-left eb:text-[13px] eb:font-medium eb:text-primary eb:hover:bg-accent",
|
|
1195
|
+
children: [
|
|
1196
|
+
/* @__PURE__ */ jsx3(Plus, { className: "eb:size-4" }),
|
|
1197
|
+
t("New variable")
|
|
1198
|
+
]
|
|
1199
|
+
}
|
|
1200
|
+
) })
|
|
1201
|
+
] });
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/features/variables/VariablesMenu.jsx
|
|
1205
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1206
|
+
function VariablesMenu({ context }) {
|
|
1207
|
+
const { editor, ready, variables, storage, canManageVariables, reloadVariables } = context;
|
|
1208
|
+
const { t, notify } = useUi();
|
|
1209
|
+
const [open, setOpen] = useState4(false);
|
|
1210
|
+
const [dialog, setDialog] = useState4(null);
|
|
1211
|
+
const insert = (variable) => {
|
|
1212
|
+
setOpen(false);
|
|
1213
|
+
const done = variable.kind === "link" ? editor.insertLink(tokenOf(variable), t("Unsubscribe")) : editor.insertText(tokenOf(variable));
|
|
1214
|
+
if (!done) notify("info", t("Select a text or a button first."));
|
|
1215
|
+
return done;
|
|
1216
|
+
};
|
|
1217
|
+
const manage = canManageVariables ? {
|
|
1218
|
+
onCreate: (label) => {
|
|
1219
|
+
setOpen(false);
|
|
1220
|
+
setDialog({ variable: null, label });
|
|
1221
|
+
},
|
|
1222
|
+
onEdit: (variable) => {
|
|
1223
|
+
setOpen(false);
|
|
1224
|
+
setDialog({ variable, label: "" });
|
|
1225
|
+
}
|
|
1226
|
+
} : {};
|
|
1227
|
+
return /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
1228
|
+
/* @__PURE__ */ jsxs4(Popover, { open, onOpenChange: setOpen, children: [
|
|
1229
|
+
/* @__PURE__ */ jsx4(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs4(
|
|
1230
|
+
Button,
|
|
1231
|
+
{
|
|
1232
|
+
variant: "outline",
|
|
1233
|
+
size: "sm-compact",
|
|
1234
|
+
disabled: !ready,
|
|
1235
|
+
onPointerDown: (e) => e.pointerType === "mouse" && e.preventDefault(),
|
|
1236
|
+
children: [
|
|
1237
|
+
/* @__PURE__ */ jsx4(Braces2, {}),
|
|
1238
|
+
t("Variables")
|
|
1239
|
+
]
|
|
1240
|
+
}
|
|
1241
|
+
) }),
|
|
1242
|
+
/* @__PURE__ */ jsx4(
|
|
1243
|
+
PopoverContent,
|
|
1244
|
+
{
|
|
1245
|
+
align: "end",
|
|
1246
|
+
className: "eb:w-80 eb:p-0",
|
|
1247
|
+
onCloseAutoFocus: (e) => e.preventDefault(),
|
|
1248
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
1249
|
+
children: /* @__PURE__ */ jsx4(VariablePicker, { variables, onPick: insert, ...manage })
|
|
1250
|
+
}
|
|
1251
|
+
)
|
|
1252
|
+
] }),
|
|
1253
|
+
canManageVariables && /* @__PURE__ */ jsx4(
|
|
1254
|
+
VariableDialog,
|
|
1255
|
+
{
|
|
1256
|
+
open: Boolean(dialog),
|
|
1257
|
+
onOpenChange: (next) => !next && setDialog(null),
|
|
1258
|
+
variable: dialog?.variable || null,
|
|
1259
|
+
initialLabel: dialog?.label || "",
|
|
1260
|
+
variables,
|
|
1261
|
+
storage,
|
|
1262
|
+
onSaved: async (variable, created) => {
|
|
1263
|
+
await reloadVariables();
|
|
1264
|
+
if (created) {
|
|
1265
|
+
if (editor.insertText(tokenOf(variable))) {
|
|
1266
|
+
notify("success", t("Variable \u201C{name}\u201D created and inserted.", { name: variable.label }));
|
|
1267
|
+
} else {
|
|
1268
|
+
notify("success", t("Variable \u201C{name}\u201D created.", { name: variable.label }));
|
|
1269
|
+
}
|
|
1270
|
+
} else {
|
|
1271
|
+
notify("success", t("Variable \u201C{name}\u201D saved.", { name: variable.label }));
|
|
1272
|
+
}
|
|
1273
|
+
},
|
|
1274
|
+
onDeleted: async () => {
|
|
1275
|
+
await reloadVariables();
|
|
1276
|
+
notify("success", t("Variable deleted."));
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
)
|
|
1280
|
+
] });
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/features/variables/index.js
|
|
1284
|
+
var variablesFeature = {
|
|
1285
|
+
id: "variables",
|
|
1286
|
+
label: "Variables",
|
|
1287
|
+
order: 10,
|
|
1288
|
+
toolbar: VariablesMenu
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
// src/features/preview/index.js
|
|
1292
|
+
var previewFeature = {
|
|
1293
|
+
id: "preview",
|
|
1294
|
+
label: "Preview",
|
|
1295
|
+
order: 40,
|
|
1296
|
+
toolbar: PreviewButton,
|
|
1297
|
+
isAvailable: (context) => typeof context.storage?.render === "function"
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
// src/features/test-email/TestEmailButton.jsx
|
|
1301
|
+
import { useState as useState5 } from "react";
|
|
1302
|
+
import { CheckCircle2, Loader2 as Loader22, Send, XCircle } from "lucide-react";
|
|
1303
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1304
|
+
var KEY = "best-email-builder:test-addresses";
|
|
1305
|
+
function readAddresses(fallback) {
|
|
1306
|
+
try {
|
|
1307
|
+
return window.localStorage.getItem(KEY) ?? fallback;
|
|
1308
|
+
} catch {
|
|
1309
|
+
return fallback;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
function rememberAddresses(value) {
|
|
1313
|
+
try {
|
|
1314
|
+
window.localStorage.setItem(KEY, value);
|
|
1315
|
+
} catch {
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function TestEmailButton({ context }) {
|
|
1319
|
+
const { editor, ready, meta, storage, testRecipients = [] } = context;
|
|
1320
|
+
const { t, notify } = useUi();
|
|
1321
|
+
const [open, setOpen] = useState5(false);
|
|
1322
|
+
const [addresses, setAddresses] = useState5("");
|
|
1323
|
+
const [sending, setSending] = useState5(false);
|
|
1324
|
+
const [report, setReport] = useState5(null);
|
|
1325
|
+
const emails = addresses.split(/[,;\s]+/).map((a) => a.trim()).filter(Boolean);
|
|
1326
|
+
const send = async () => {
|
|
1327
|
+
if (!emails.length) return;
|
|
1328
|
+
rememberAddresses(addresses);
|
|
1329
|
+
setSending(true);
|
|
1330
|
+
setReport(null);
|
|
1331
|
+
try {
|
|
1332
|
+
const result = await storage.sendTest({
|
|
1333
|
+
mjml: editor.exportMjml(),
|
|
1334
|
+
subject: meta.subject,
|
|
1335
|
+
preheader: meta.preheader,
|
|
1336
|
+
emails
|
|
1337
|
+
});
|
|
1338
|
+
setReport(result);
|
|
1339
|
+
if (result.failed > 0) notify("warning", t("{sent} sent, {failed} failed.", { sent: result.sent, failed: result.failed }));
|
|
1340
|
+
else notify("success", t("{sent} test(s) sent \u2014 check your inbox.", { sent: result.sent }));
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
notify("error", error.message);
|
|
1343
|
+
} finally {
|
|
1344
|
+
setSending(false);
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
1348
|
+
/* @__PURE__ */ jsxs5(
|
|
1349
|
+
Button,
|
|
1350
|
+
{
|
|
1351
|
+
variant: "outline",
|
|
1352
|
+
size: "sm-compact",
|
|
1353
|
+
disabled: !ready,
|
|
1354
|
+
onClick: () => {
|
|
1355
|
+
setAddresses(readAddresses(testRecipients.join(", ")));
|
|
1356
|
+
setReport(null);
|
|
1357
|
+
setOpen(true);
|
|
1358
|
+
},
|
|
1359
|
+
children: [
|
|
1360
|
+
/* @__PURE__ */ jsx5(Send, {}),
|
|
1361
|
+
t("Test")
|
|
1362
|
+
]
|
|
1363
|
+
}
|
|
1364
|
+
),
|
|
1365
|
+
/* @__PURE__ */ jsx5(Dialog, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs5(DialogContent, { className: "eb:sm:max-w-[460px]", children: [
|
|
1366
|
+
/* @__PURE__ */ jsx5(DialogHeader, { children: /* @__PURE__ */ jsxs5("div", { className: "eb:flex eb:items-center eb:gap-3", children: [
|
|
1367
|
+
/* @__PURE__ */ jsx5("span", { className: "eb:flex eb:size-10 eb:shrink-0 eb:items-center eb:justify-center eb:rounded-lg2 eb:bg-primary/10 eb:text-primary", children: /* @__PURE__ */ jsx5(Send, { className: "eb:size-5" }) }),
|
|
1368
|
+
/* @__PURE__ */ jsxs5("div", { children: [
|
|
1369
|
+
/* @__PURE__ */ jsx5(DialogTitle, { className: "eb:text-base eb:font-normal", children: t("Send a test") }),
|
|
1370
|
+
/* @__PURE__ */ jsx5(DialogDescription, { className: "eb:text-[13px]", children: t("The email on screen, to reviewers\u2019 addresses.") })
|
|
1371
|
+
] })
|
|
1372
|
+
] }) }),
|
|
1373
|
+
/* @__PURE__ */ jsxs5("div", { className: "eb:space-y-2", children: [
|
|
1374
|
+
/* @__PURE__ */ jsx5(Label, { htmlFor: "eb-test-addresses", children: t("Addresses") }),
|
|
1375
|
+
/* @__PURE__ */ jsx5(
|
|
1376
|
+
Textarea,
|
|
1377
|
+
{
|
|
1378
|
+
id: "eb-test-addresses",
|
|
1379
|
+
value: addresses,
|
|
1380
|
+
onChange: (e) => setAddresses(e.target.value),
|
|
1381
|
+
placeholder: "jane@example.com, john@example.com",
|
|
1382
|
+
rows: 3
|
|
1383
|
+
}
|
|
1384
|
+
)
|
|
1385
|
+
] }),
|
|
1386
|
+
report && /* @__PURE__ */ jsxs5("ul", { className: "eb:space-y-1", "aria-label": t("Results"), children: [
|
|
1387
|
+
report.results.map((r) => /* @__PURE__ */ jsxs5("li", { className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-foreground", children: [
|
|
1388
|
+
r.ok ? /* @__PURE__ */ jsx5(CheckCircle2, { className: "eb:mt-0.5 eb:size-3.5 eb:shrink-0 eb:text-success", "aria-hidden": "true" }) : /* @__PURE__ */ jsx5(XCircle, { className: "eb:mt-0.5 eb:size-3.5 eb:shrink-0 eb:text-destructive", "aria-hidden": "true" }),
|
|
1389
|
+
/* @__PURE__ */ jsxs5("span", { className: "eb:min-w-0 eb:break-all", children: [
|
|
1390
|
+
r.email,
|
|
1391
|
+
r.error ? ` \u2014 ${r.error}` : ""
|
|
1392
|
+
] })
|
|
1393
|
+
] }, r.email)),
|
|
1394
|
+
report.invalid.map((email) => /* @__PURE__ */ jsx5("li", { className: "eb:text-[12px] eb:text-foreground-muted", children: t("Ignored (invalid): {email}", { email }) }, `invalid-${email}`))
|
|
1395
|
+
] }),
|
|
1396
|
+
/* @__PURE__ */ jsxs5("div", { className: "eb:flex eb:justify-end eb:gap-2", children: [
|
|
1397
|
+
/* @__PURE__ */ jsx5(Button, { variant: "outline", onClick: () => setOpen(false), children: t("Close") }),
|
|
1398
|
+
/* @__PURE__ */ jsxs5(Button, { onClick: send, disabled: sending || !emails.length, children: [
|
|
1399
|
+
sending ? /* @__PURE__ */ jsx5(Loader22, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx5(Send, {}),
|
|
1400
|
+
t("Send")
|
|
1401
|
+
] })
|
|
1402
|
+
] })
|
|
1403
|
+
] }) })
|
|
1404
|
+
] });
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// src/features/test-email/index.js
|
|
1408
|
+
var testEmailFeature = {
|
|
1409
|
+
id: "test-email",
|
|
1410
|
+
label: "Send a test",
|
|
1411
|
+
order: 50,
|
|
1412
|
+
toolbar: TestEmailButton,
|
|
1413
|
+
isAvailable: (context) => typeof context.storage?.sendTest === "function"
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1416
|
+
// src/features/templates/TemplateGallery.jsx
|
|
1417
|
+
import { useMemo as useMemo3, useState as useState6 } from "react";
|
|
1418
|
+
import { LayoutTemplate } from "lucide-react";
|
|
1419
|
+
import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1420
|
+
var THUMBNAIL_SCALE = 0.38;
|
|
1421
|
+
var EMAIL_WIDTH = 600;
|
|
1422
|
+
function Thumbnail({ html, title }) {
|
|
1423
|
+
return /* @__PURE__ */ jsx6(
|
|
1424
|
+
"div",
|
|
1425
|
+
{
|
|
1426
|
+
className: "eb:relative eb:h-[220px] eb:overflow-hidden eb:rounded-md eb:border eb:border-border eb:bg-surface-75",
|
|
1427
|
+
"aria-hidden": "true",
|
|
1428
|
+
children: /* @__PURE__ */ jsx6(
|
|
1429
|
+
"iframe",
|
|
1430
|
+
{
|
|
1431
|
+
title,
|
|
1432
|
+
sandbox: "",
|
|
1433
|
+
srcDoc: html,
|
|
1434
|
+
tabIndex: -1,
|
|
1435
|
+
loading: "lazy",
|
|
1436
|
+
className: "eb:pointer-events-none eb:absolute eb:top-0 eb:left-1/2 eb:origin-top eb:border-0",
|
|
1437
|
+
style: {
|
|
1438
|
+
width: EMAIL_WIDTH,
|
|
1439
|
+
height: 220 / THUMBNAIL_SCALE,
|
|
1440
|
+
transform: `translateX(-50%) scale(${THUMBNAIL_SCALE})`
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
)
|
|
1444
|
+
}
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
function TemplateGalleryButton({ context }) {
|
|
1448
|
+
const { editor, ready, templates, isDirty, markDirty, template, setMeta } = context;
|
|
1449
|
+
const { t, confirm, notify } = useUi();
|
|
1450
|
+
const [open, setOpen] = useState6(false);
|
|
1451
|
+
const cards = useMemo3(() => {
|
|
1452
|
+
if (!open || !ready) return [];
|
|
1453
|
+
return templates.map((entry) => {
|
|
1454
|
+
try {
|
|
1455
|
+
return { ...entry, html: editor.exportHtml(withThumbnailImages(entry.mjml)).html };
|
|
1456
|
+
} catch {
|
|
1457
|
+
return { ...entry, html: "" };
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
}, [open, ready, templates, editor]);
|
|
1461
|
+
const apply = async (entry) => {
|
|
1462
|
+
if (isDirty()) {
|
|
1463
|
+
setOpen(false);
|
|
1464
|
+
const ok = await confirm({
|
|
1465
|
+
title: t("Replace the current email?"),
|
|
1466
|
+
description: t("Unsaved changes will be replaced."),
|
|
1467
|
+
confirmLabel: t("Replace"),
|
|
1468
|
+
destructive: true
|
|
1469
|
+
});
|
|
1470
|
+
if (!ok) return;
|
|
1471
|
+
}
|
|
1472
|
+
editor.loadMjml(entry.mjml);
|
|
1473
|
+
setMeta((meta) => ({
|
|
1474
|
+
...meta,
|
|
1475
|
+
subject: meta.subject || entry.subject,
|
|
1476
|
+
preheader: meta.preheader || entry.preheader
|
|
1477
|
+
}));
|
|
1478
|
+
if (template.id) markDirty();
|
|
1479
|
+
setOpen(false);
|
|
1480
|
+
notify("success", t("\u201C{name}\u201D template applied.", { name: entry.name }));
|
|
1481
|
+
};
|
|
1482
|
+
return /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
1483
|
+
/* @__PURE__ */ jsxs6(Button, { variant: "outline", size: "sm-compact", disabled: !ready, onClick: () => setOpen(true), children: [
|
|
1484
|
+
/* @__PURE__ */ jsx6(LayoutTemplate, {}),
|
|
1485
|
+
t("Templates")
|
|
1486
|
+
] }),
|
|
1487
|
+
/* @__PURE__ */ jsx6(Dialog, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs6(DialogContent, { className: "eb:max-h-[calc(100dvh-2rem)] eb:overflow-y-auto eb:sm:max-w-4xl", "data-testid": "eb-template-gallery", children: [
|
|
1488
|
+
/* @__PURE__ */ jsxs6(DialogHeader, { children: [
|
|
1489
|
+
/* @__PURE__ */ jsx6(DialogTitle, { children: t("Start from a template") }),
|
|
1490
|
+
/* @__PURE__ */ jsx6(DialogDescription, { children: t("Picking a template replaces the email being edited.") })
|
|
1491
|
+
] }),
|
|
1492
|
+
/* @__PURE__ */ jsx6("ul", { className: "eb:grid eb:grid-cols-1 eb:gap-3 eb:sm:grid-cols-2 eb:md:grid-cols-3", children: cards.map((card) => /* @__PURE__ */ jsx6("li", { children: /* @__PURE__ */ jsxs6(
|
|
1493
|
+
"button",
|
|
1494
|
+
{
|
|
1495
|
+
type: "button",
|
|
1496
|
+
onClick: () => apply(card),
|
|
1497
|
+
"data-template-id": card.id,
|
|
1498
|
+
className: "eb:group eb:flex eb:w-full eb:flex-col eb:gap-2 eb:rounded-lg2 eb:border eb:border-border eb:bg-background eb:p-2 eb:text-left eb:transition-colors eb:hover:border-primary/50 eb:hover:bg-primary/5 eb:focus-visible:outline-none eb:focus-visible:ring-2 eb:focus-visible:ring-ring/50",
|
|
1499
|
+
children: [
|
|
1500
|
+
/* @__PURE__ */ jsx6(Thumbnail, { html: card.html, title: card.name }),
|
|
1501
|
+
/* @__PURE__ */ jsxs6("span", { className: "eb:px-1", children: [
|
|
1502
|
+
/* @__PURE__ */ jsx6("span", { className: "eb:block eb:text-[13px] eb:font-medium eb:text-foreground", children: card.name }),
|
|
1503
|
+
/* @__PURE__ */ jsx6("span", { className: "eb:block eb:text-[12px] eb:text-foreground-muted", children: card.description })
|
|
1504
|
+
] })
|
|
1505
|
+
]
|
|
1506
|
+
}
|
|
1507
|
+
) }, card.id)) })
|
|
1508
|
+
] }) })
|
|
1509
|
+
] });
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// src/features/templates/index.js
|
|
1513
|
+
var templatesFeature = {
|
|
1514
|
+
id: "templates",
|
|
1515
|
+
label: "Templates",
|
|
1516
|
+
order: 5,
|
|
1517
|
+
toolbar: TemplateGalleryButton,
|
|
1518
|
+
isAvailable: (context) => Array.isArray(context.templates) && context.templates.length > 0
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
// src/features/index.js
|
|
1522
|
+
var FEATURES = [
|
|
1523
|
+
templatesFeature,
|
|
1524
|
+
variablesFeature,
|
|
1525
|
+
autosaveFeature,
|
|
1526
|
+
versionsFeature,
|
|
1527
|
+
previewFeature,
|
|
1528
|
+
testEmailFeature
|
|
1529
|
+
];
|
|
1530
|
+
var emailBuilderFeatures = createRegistry("emailBuilderFeatures", FEATURES);
|
|
1531
|
+
function isFeatureEnabled(id) {
|
|
1532
|
+
const feature = emailBuilderFeatures.get(id);
|
|
1533
|
+
return Boolean(feature && feature.enabled !== false);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// src/adapters/grapesjs/engineConfig.js
|
|
1537
|
+
var DEVICES = [
|
|
1538
|
+
{ id: "desktop", name: "desktop", width: "" },
|
|
1539
|
+
// `widthMedia`: MJML's mobile rule (480px) applies inside the frame.
|
|
1540
|
+
{ id: "mobile", name: "mobile", width: "375px", widthMedia: "480px" }
|
|
1541
|
+
];
|
|
1542
|
+
var EMPTY_MJML = '<mjml><mj-body background-color="#eceff4" width="600px"></mj-body></mjml>';
|
|
1543
|
+
var IMAGE_PLACEHOLDER = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 220'%3E%3Crect width='600' height='220' fill='%23f3f5f9'/%3E%3Cg transform='translate(276 86)' fill='none' stroke='%238b93a3' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1' y='1' width='46' height='46' rx='5'/%3E%3Ccircle cx='17' cy='17' r='5'/%3E%3Cpath d='m47 31-8-8a4 4 0 0 0-6 0L5 47'/%3E%3C/g%3E%3C/svg%3E";
|
|
1544
|
+
var CANVAS_CSS = `
|
|
1545
|
+
body { background: #eceff4; }
|
|
1546
|
+
[data-gjs-highlightable] { outline-offset: -1px; }
|
|
1547
|
+
*::selection { background: rgba(53, 104, 168, .22); }
|
|
1548
|
+
${FRAME_SCROLLBAR_CSS}
|
|
1549
|
+
`;
|
|
1550
|
+
function buildEngineConfig({ container, mjmlPlugin, locales = {}, locale = "en" }) {
|
|
1551
|
+
return {
|
|
1552
|
+
container,
|
|
1553
|
+
height: "100%",
|
|
1554
|
+
width: "auto",
|
|
1555
|
+
telemetry: false,
|
|
1556
|
+
customUI: true,
|
|
1557
|
+
cssIcons: "",
|
|
1558
|
+
noticeOnUnload: false,
|
|
1559
|
+
storageManager: (
|
|
1560
|
+
/** @type {false} */
|
|
1561
|
+
false
|
|
1562
|
+
),
|
|
1563
|
+
fromElement: false,
|
|
1564
|
+
showDevices: false,
|
|
1565
|
+
showOffsets: false,
|
|
1566
|
+
multipleSelection: false,
|
|
1567
|
+
avoidInlineStyle: true,
|
|
1568
|
+
log: (
|
|
1569
|
+
/** @type {('warning'|'error')[]} */
|
|
1570
|
+
["error"]
|
|
1571
|
+
),
|
|
1572
|
+
canvasCss: CANVAS_CSS,
|
|
1573
|
+
i18n: {
|
|
1574
|
+
locale,
|
|
1575
|
+
localeFallback: locale,
|
|
1576
|
+
detectLocale: false,
|
|
1577
|
+
messages: locales.engine ? { [locale]: locales.engine } : {}
|
|
1578
|
+
},
|
|
1579
|
+
panels: { defaults: [] },
|
|
1580
|
+
blockManager: { custom: true },
|
|
1581
|
+
layerManager: { custom: true },
|
|
1582
|
+
traitManager: { custom: true },
|
|
1583
|
+
selectorManager: { custom: true, componentFirst: true },
|
|
1584
|
+
styleManager: { custom: true },
|
|
1585
|
+
assetManager: { custom: true, upload: false },
|
|
1586
|
+
deviceManager: { devices: DEVICES, default: "desktop" },
|
|
1587
|
+
undoManager: { trackSelection: false, maximumStackLength: 300 },
|
|
1588
|
+
// Canvas shortcuts: the ones users expect (undo, redo — Ctrl+Y included —,
|
|
1589
|
+
// copy, paste, delete), without GrapesJS's single-letter navigation
|
|
1590
|
+
// ("s", "w", "a", "d") that moved the selection on any keypress outside a
|
|
1591
|
+
// field.
|
|
1592
|
+
keymaps: {
|
|
1593
|
+
defaults: {
|
|
1594
|
+
"core:undo": { keys: "\u2318+z, ctrl+z", handler: "core:undo", opts: { prevent: true } },
|
|
1595
|
+
"core:redo": { keys: "\u2318+shift+z, ctrl+shift+z, ctrl+y", handler: "core:redo", opts: { prevent: true } },
|
|
1596
|
+
"core:copy": { keys: "\u2318+c, ctrl+c", handler: "core:copy" },
|
|
1597
|
+
"core:paste": { keys: "\u2318+v, ctrl+v", handler: "core:paste" },
|
|
1598
|
+
"core:component-delete": { keys: "backspace, delete", handler: "core:component-delete", opts: { prevent: true } }
|
|
1599
|
+
}
|
|
1600
|
+
},
|
|
1601
|
+
parser: {
|
|
1602
|
+
optionsHtml: {
|
|
1603
|
+
allowScripts: false,
|
|
1604
|
+
allowUnsafeAttr: false,
|
|
1605
|
+
allowUnsafeAttrValue: false
|
|
1606
|
+
}
|
|
1607
|
+
},
|
|
1608
|
+
plugins: [
|
|
1609
|
+
(editor) => mjmlPlugin(editor, {
|
|
1610
|
+
// No blocks, devices or style settings from the plugin: blocks and
|
|
1611
|
+
// properties come from `blocks/`.
|
|
1612
|
+
blocks: [],
|
|
1613
|
+
resetBlocks: true,
|
|
1614
|
+
resetDevices: false,
|
|
1615
|
+
resetStyleManager: false,
|
|
1616
|
+
hideSelector: true,
|
|
1617
|
+
useCustomTheme: false,
|
|
1618
|
+
overwriteExport: false,
|
|
1619
|
+
imagePlaceholderSrc: IMAGE_PLACEHOLDER,
|
|
1620
|
+
i18n: locales.mjml ? { [locale]: locales.mjml } : {}
|
|
1621
|
+
}),
|
|
1622
|
+
// After the plugin: the empty image gets our banner, not the raw SVG.
|
|
1623
|
+
(editor) => editor.Components.addType("mj-image", { model: { defaults: { src: IMAGE_PLACEHOLDER } } })
|
|
1624
|
+
]
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// src/adapters/grapesjs/unwrapModule.js
|
|
1629
|
+
function unwrapModule(mod) {
|
|
1630
|
+
let value = mod;
|
|
1631
|
+
for (let depth = 0; depth < 3 && isInteropLayer(value); depth += 1) value = value.default;
|
|
1632
|
+
return value;
|
|
1633
|
+
}
|
|
1634
|
+
function isInteropLayer(value) {
|
|
1635
|
+
if (value === null || typeof value !== "object" || !("default" in value)) return false;
|
|
1636
|
+
return value.__esModule === true || Object.prototype.toString.call(value) === "[object Module]";
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// src/adapters/grapesjs/GrapesJsAdapter.js
|
|
1640
|
+
var ENGINE_NAME = "grapesjs-mjml";
|
|
1641
|
+
var ENGINE_VERSION = "0.23.6";
|
|
1642
|
+
var BLOCK_PREFIX = "eb-";
|
|
1643
|
+
var LOCKED_TYPES = /* @__PURE__ */ new Set(["mjml", "mj-head", "mj-body", "wrapper"]);
|
|
1644
|
+
var LABEL_TYPES = /* @__PURE__ */ new Set(["mj-button", "mj-navbar-link"]);
|
|
1645
|
+
var RICH_TYPES = /* @__PURE__ */ new Set(["mj-text", "mj-button"]);
|
|
1646
|
+
var ENGINE_LOCALES = {
|
|
1647
|
+
fr: () => Promise.all([import("grapesjs/locale/fr.mjs"), import("grapesjs-mjml/locale/fr")])
|
|
1648
|
+
};
|
|
1649
|
+
async function defaultLoadEngine(locale) {
|
|
1650
|
+
const loadLocales = Object.prototype.hasOwnProperty.call(ENGINE_LOCALES, locale) ? ENGINE_LOCALES[locale] : null;
|
|
1651
|
+
const [grapesjs, mjml, localeModules] = await Promise.all([
|
|
1652
|
+
import("grapesjs"),
|
|
1653
|
+
import("grapesjs-mjml"),
|
|
1654
|
+
loadLocales ? loadLocales() : Promise.resolve(null)
|
|
1655
|
+
]);
|
|
1656
|
+
return {
|
|
1657
|
+
grapesjs: unwrapModule(grapesjs),
|
|
1658
|
+
mjmlPlugin: unwrapModule(mjml),
|
|
1659
|
+
locales: localeModules ? { engine: unwrapModule(localeModules[0]), mjml: unwrapModule(localeModules[1]) } : {}
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
var ENGINE_SVG = /^\s*<svg[\s>]/i;
|
|
1663
|
+
var PLACEHOLDER_SRC_ATTR = new RegExp(
|
|
1664
|
+
`\\ssrc="(?:${IMAGE_PLACEHOLDER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|\\s*<svg[\\s\\S]*?<\\/svg>\\s*)"`,
|
|
1665
|
+
"g"
|
|
1666
|
+
);
|
|
1667
|
+
var PLACEHOLDER_SRC_STYLE = /src:\s*<svg[\s\S]*?<\/svg>;?/gi;
|
|
1668
|
+
function isEnginePlaceholder(value) {
|
|
1669
|
+
return typeof value === "string" && (value === IMAGE_PLACEHOLDER || ENGINE_SVG.test(value));
|
|
1670
|
+
}
|
|
1671
|
+
function withoutPlaceholders(node) {
|
|
1672
|
+
if (Array.isArray(node)) return node.map(withoutPlaceholders);
|
|
1673
|
+
if (!node || typeof node !== "object") return node;
|
|
1674
|
+
const out = {};
|
|
1675
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1676
|
+
if (key === "src" && isEnginePlaceholder(value)) out[key] = "";
|
|
1677
|
+
else if (key === "style" && typeof value === "string") {
|
|
1678
|
+
out[key] = value.replace(PLACEHOLDER_SRC_STYLE, "").split(`src:${IMAGE_PLACEHOLDER};`).join("");
|
|
1679
|
+
} else out[key] = withoutPlaceholders(value);
|
|
1680
|
+
}
|
|
1681
|
+
return out;
|
|
1682
|
+
}
|
|
1683
|
+
function escapeHtml(text) {
|
|
1684
|
+
return String(text ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1685
|
+
}
|
|
1686
|
+
function insertAtCaret(ed, html) {
|
|
1687
|
+
const view = ed.getEditing()?.view;
|
|
1688
|
+
const rte = view?.activeRte;
|
|
1689
|
+
if (!view || typeof rte?.insertHTML !== "function") return false;
|
|
1690
|
+
rte.insertHTML(html);
|
|
1691
|
+
view.onInput?.();
|
|
1692
|
+
void view.syncContent?.();
|
|
1693
|
+
return true;
|
|
1694
|
+
}
|
|
1695
|
+
function createGrapesJsAdapter({ locale = "en", loadEngine = defaultLoadEngine } = {}) {
|
|
1696
|
+
let editor = null;
|
|
1697
|
+
let host = null;
|
|
1698
|
+
let emit = () => {
|
|
1699
|
+
};
|
|
1700
|
+
let generation = 0;
|
|
1701
|
+
let selectionQueued = false;
|
|
1702
|
+
let loading = false;
|
|
1703
|
+
function muteUntilFlushed() {
|
|
1704
|
+
loading = true;
|
|
1705
|
+
setTimeout(() => {
|
|
1706
|
+
loading = false;
|
|
1707
|
+
}, 0);
|
|
1708
|
+
}
|
|
1709
|
+
function requireEditor() {
|
|
1710
|
+
if (!editor) throw new Error("GrapesJsAdapter: engine not initialized");
|
|
1711
|
+
return editor;
|
|
1712
|
+
}
|
|
1713
|
+
function byId(id) {
|
|
1714
|
+
const all = requireEditor().Components.allById();
|
|
1715
|
+
return all[id] || null;
|
|
1716
|
+
}
|
|
1717
|
+
function typeOf(component) {
|
|
1718
|
+
return component ? String(component.get("type") || "") : "";
|
|
1719
|
+
}
|
|
1720
|
+
function body() {
|
|
1721
|
+
const ed = requireEditor();
|
|
1722
|
+
return ed.getWrapper().findType("mj-body")[0] || null;
|
|
1723
|
+
}
|
|
1724
|
+
function closest(component, types) {
|
|
1725
|
+
let current = component;
|
|
1726
|
+
while (current) {
|
|
1727
|
+
if (types.includes(typeOf(current))) return current;
|
|
1728
|
+
current = current.parent();
|
|
1729
|
+
}
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
function attributesOf(component) {
|
|
1733
|
+
const out = {};
|
|
1734
|
+
for (const [key, value] of Object.entries(component.getAttributes() || {})) {
|
|
1735
|
+
if (key === "style" || key === "id" || value === void 0 || value === null) continue;
|
|
1736
|
+
out[key] = String(value);
|
|
1737
|
+
}
|
|
1738
|
+
if (component.get("src") !== void 0) out.src = String(component.get("src") || "");
|
|
1739
|
+
if (isEnginePlaceholder(out.src)) out.src = "";
|
|
1740
|
+
return out;
|
|
1741
|
+
}
|
|
1742
|
+
function describe(component) {
|
|
1743
|
+
if (!component) return null;
|
|
1744
|
+
const type = typeOf(component);
|
|
1745
|
+
const parent = component.parent();
|
|
1746
|
+
const index = parent ? parent.components().indexOf(component) : -1;
|
|
1747
|
+
const siblings = parent ? parent.components().length : 0;
|
|
1748
|
+
const locked = LOCKED_TYPES.has(type);
|
|
1749
|
+
return {
|
|
1750
|
+
id: component.getId(),
|
|
1751
|
+
tagName: type,
|
|
1752
|
+
attributes: attributesOf(component),
|
|
1753
|
+
content: LABEL_TYPES.has(type) ? textOf(component) : null,
|
|
1754
|
+
canRemove: !locked && component.get("removable") !== false,
|
|
1755
|
+
canDuplicate: !locked && component.get("copyable") !== false,
|
|
1756
|
+
canMoveUp: !locked && index > 0,
|
|
1757
|
+
canMoveDown: !locked && index >= 0 && index < siblings - 1
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
function textOf(component) {
|
|
1761
|
+
const html = component.getInnerHTML ? component.getInnerHTML() : "";
|
|
1762
|
+
return String(html).replace(/<br\s*\/?>/gi, " ").replace(/<[^>]*>/g, "").replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").trim();
|
|
1763
|
+
}
|
|
1764
|
+
function queueSelection() {
|
|
1765
|
+
if (selectionQueued) return;
|
|
1766
|
+
selectionQueued = true;
|
|
1767
|
+
queueMicrotask(() => {
|
|
1768
|
+
selectionQueued = false;
|
|
1769
|
+
if (editor) emit("selection", describe(editor.getSelected()));
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
function emitHistory() {
|
|
1773
|
+
if (!editor) return;
|
|
1774
|
+
const um = editor.UndoManager;
|
|
1775
|
+
emit("history", { canUndo: um.hasUndo(), canRedo: um.hasRedo() });
|
|
1776
|
+
}
|
|
1777
|
+
function firstOf(added) {
|
|
1778
|
+
return Array.isArray(added) ? added[0] : added;
|
|
1779
|
+
}
|
|
1780
|
+
function placementFor(block, target) {
|
|
1781
|
+
const root = body();
|
|
1782
|
+
if (!root) throw new Error("GrapesJsAdapter: email body not found");
|
|
1783
|
+
const section = target ? closest(target, ["mj-section", "mj-wrapper", "mj-hero"]) : null;
|
|
1784
|
+
const afterSection = () => {
|
|
1785
|
+
if (section && section.parent()) {
|
|
1786
|
+
const coll = section.parent().components();
|
|
1787
|
+
return { collection: coll, at: coll.indexOf(section) + 1, wrap: false };
|
|
1788
|
+
}
|
|
1789
|
+
return { collection: root.components(), at: root.components().length, wrap: false };
|
|
1790
|
+
};
|
|
1791
|
+
if (block.kind === "layout") return afterSection();
|
|
1792
|
+
const type = typeOf(target);
|
|
1793
|
+
if (type === "mj-column") {
|
|
1794
|
+
return { collection: target.components(), at: target.components().length, wrap: false };
|
|
1795
|
+
}
|
|
1796
|
+
const column = target ? closest(target, ["mj-column"]) : null;
|
|
1797
|
+
if (column && target !== column) {
|
|
1798
|
+
let child = target;
|
|
1799
|
+
while (child.parent() && child.parent() !== column) child = child.parent();
|
|
1800
|
+
const coll = column.components();
|
|
1801
|
+
return { collection: coll, at: coll.indexOf(child) + 1, wrap: false };
|
|
1802
|
+
}
|
|
1803
|
+
return { ...afterSection(), wrap: true };
|
|
1804
|
+
}
|
|
1805
|
+
const adapter = {
|
|
1806
|
+
name: ENGINE_NAME,
|
|
1807
|
+
version: ENGINE_VERSION,
|
|
1808
|
+
get engine() {
|
|
1809
|
+
return editor;
|
|
1810
|
+
},
|
|
1811
|
+
async initialize({ container, blocks: blockList, document, onEvent }) {
|
|
1812
|
+
if (editor) throw new Error("GrapesJsAdapter: already initialized");
|
|
1813
|
+
const ticket = ++generation;
|
|
1814
|
+
const { grapesjs, mjmlPlugin, locales } = await loadEngine(locale);
|
|
1815
|
+
if (ticket !== generation) return;
|
|
1816
|
+
emit = onEvent;
|
|
1817
|
+
host = createEngineHost(container);
|
|
1818
|
+
const instance = grapesjs.init(buildEngineConfig({ container: host, mjmlPlugin, locales, locale }));
|
|
1819
|
+
editor = instance;
|
|
1820
|
+
await new Promise((resolve) => instance.onReady(() => resolve(void 0)));
|
|
1821
|
+
if (ticket !== generation) return;
|
|
1822
|
+
for (const block of blockList) {
|
|
1823
|
+
if (!block.mjml) continue;
|
|
1824
|
+
instance.Blocks.add(`${BLOCK_PREFIX}${block.id}`, {
|
|
1825
|
+
label: block.label,
|
|
1826
|
+
content: block.mjml,
|
|
1827
|
+
activate: true,
|
|
1828
|
+
select: true
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
muteUntilFlushed();
|
|
1832
|
+
if (document) instance.loadProjectData(document.project);
|
|
1833
|
+
else instance.setComponents(EMPTY_MJML);
|
|
1834
|
+
instance.UndoManager.clear();
|
|
1835
|
+
instance.on("update", () => {
|
|
1836
|
+
if (!loading) emit("change");
|
|
1837
|
+
emitHistory();
|
|
1838
|
+
});
|
|
1839
|
+
instance.on("undo", emitHistory);
|
|
1840
|
+
instance.on("redo", emitHistory);
|
|
1841
|
+
instance.on("component:toggled", queueSelection);
|
|
1842
|
+
instance.on("component:update", (component) => {
|
|
1843
|
+
if (component && component === instance.getSelected()) queueSelection();
|
|
1844
|
+
});
|
|
1845
|
+
instance.on("change:device", () => emit("device", adapter.getDevice()));
|
|
1846
|
+
emitHistory();
|
|
1847
|
+
},
|
|
1848
|
+
destroy() {
|
|
1849
|
+
generation += 1;
|
|
1850
|
+
if (editor) {
|
|
1851
|
+
try {
|
|
1852
|
+
editor.destroy();
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
console.warn("[best-email-builder] engine destroy failed", error);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
editor = null;
|
|
1858
|
+
if (host?.parentNode) host.parentNode.removeChild(host);
|
|
1859
|
+
host = null;
|
|
1860
|
+
emit = () => {
|
|
1861
|
+
};
|
|
1862
|
+
},
|
|
1863
|
+
addBlock(block, { targetId = null } = {}) {
|
|
1864
|
+
const ed = requireEditor();
|
|
1865
|
+
if (!block.mjml) return null;
|
|
1866
|
+
const target = targetId ? byId(targetId) : ed.getSelected();
|
|
1867
|
+
const { collection, at, wrap } = placementFor(block, target);
|
|
1868
|
+
const source = wrap ? block.sectionMjml || `<mj-section><mj-column>${block.mjml}</mj-column></mj-section>` : block.mjml;
|
|
1869
|
+
const added = firstOf(collection.add(source, { at }));
|
|
1870
|
+
if (!added) return null;
|
|
1871
|
+
const focus = wrap ? added.findType(block.tagName)[0] || added : added;
|
|
1872
|
+
ed.select(focus);
|
|
1873
|
+
return focus.getId();
|
|
1874
|
+
},
|
|
1875
|
+
remove(id) {
|
|
1876
|
+
const component = byId(id);
|
|
1877
|
+
if (!component || LOCKED_TYPES.has(typeOf(component))) return false;
|
|
1878
|
+
if (component.get("removable") === false) return false;
|
|
1879
|
+
const ed = requireEditor();
|
|
1880
|
+
if (ed.getSelected() === component) ed.select(null);
|
|
1881
|
+
component.remove();
|
|
1882
|
+
return true;
|
|
1883
|
+
},
|
|
1884
|
+
duplicate(id) {
|
|
1885
|
+
const component = byId(id);
|
|
1886
|
+
if (!component || LOCKED_TYPES.has(typeOf(component)) || !component.parent()) return null;
|
|
1887
|
+
const collection = component.parent().components();
|
|
1888
|
+
const copy = firstOf(collection.add(component.clone(), { at: collection.indexOf(component) + 1 }));
|
|
1889
|
+
if (!copy) return null;
|
|
1890
|
+
requireEditor().select(copy);
|
|
1891
|
+
return copy.getId();
|
|
1892
|
+
},
|
|
1893
|
+
move(id, direction) {
|
|
1894
|
+
const component = byId(id);
|
|
1895
|
+
if (!component || LOCKED_TYPES.has(typeOf(component))) return false;
|
|
1896
|
+
const parent = component.parent();
|
|
1897
|
+
if (!parent) return false;
|
|
1898
|
+
const index = parent.components().indexOf(component);
|
|
1899
|
+
const to = index + direction;
|
|
1900
|
+
if (to < 0 || to >= parent.components().length) return false;
|
|
1901
|
+
component.move(parent, { at: direction > 0 ? to + 1 : to });
|
|
1902
|
+
requireEditor().select(component);
|
|
1903
|
+
queueSelection();
|
|
1904
|
+
return true;
|
|
1905
|
+
},
|
|
1906
|
+
select(id) {
|
|
1907
|
+
const ed = requireEditor();
|
|
1908
|
+
ed.select(id ? byId(id) : null);
|
|
1909
|
+
},
|
|
1910
|
+
getSelection() {
|
|
1911
|
+
return editor ? describe(editor.getSelected()) : null;
|
|
1912
|
+
},
|
|
1913
|
+
describe(id) {
|
|
1914
|
+
return editor ? describe(byId(id)) : null;
|
|
1915
|
+
},
|
|
1916
|
+
getRootId() {
|
|
1917
|
+
return editor ? body()?.getId() || null : null;
|
|
1918
|
+
},
|
|
1919
|
+
updateProperties(id, attributes) {
|
|
1920
|
+
const component = byId(id);
|
|
1921
|
+
if (!component) return;
|
|
1922
|
+
const toSet = {};
|
|
1923
|
+
const toRemove = [];
|
|
1924
|
+
for (const [key, value] of Object.entries(attributes || {})) {
|
|
1925
|
+
if (key === "src" && component.get("src") !== void 0) {
|
|
1926
|
+
component.set("src", value || "");
|
|
1927
|
+
} else if (value === null || value === void 0 || value === "") {
|
|
1928
|
+
toRemove.push(key);
|
|
1929
|
+
} else {
|
|
1930
|
+
toSet[key] = String(value);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
if (Object.keys(toSet).length) component.addAttributes(toSet);
|
|
1934
|
+
if (toRemove.length) {
|
|
1935
|
+
component.removeStyle?.(toRemove);
|
|
1936
|
+
component.removeAttributes(toRemove);
|
|
1937
|
+
}
|
|
1938
|
+
queueSelection();
|
|
1939
|
+
},
|
|
1940
|
+
setContent(id, content) {
|
|
1941
|
+
const component = byId(id);
|
|
1942
|
+
if (!component) return;
|
|
1943
|
+
if (LABEL_TYPES.has(typeOf(component))) {
|
|
1944
|
+
component.components(escapeHtml(content));
|
|
1945
|
+
} else if (RICH_TYPES.has(typeOf(component))) {
|
|
1946
|
+
component.components(String(content || ""));
|
|
1947
|
+
}
|
|
1948
|
+
queueSelection();
|
|
1949
|
+
},
|
|
1950
|
+
insertText(text) {
|
|
1951
|
+
const ed = requireEditor();
|
|
1952
|
+
if (insertAtCaret(ed, escapeHtml(text))) return true;
|
|
1953
|
+
const target = ed.getSelected();
|
|
1954
|
+
if (target && RICH_TYPES.has(typeOf(target))) {
|
|
1955
|
+
target.append({ type: "textnode", content: ` ${text}` });
|
|
1956
|
+
queueSelection();
|
|
1957
|
+
return true;
|
|
1958
|
+
}
|
|
1959
|
+
return false;
|
|
1960
|
+
},
|
|
1961
|
+
insertLink(href, label) {
|
|
1962
|
+
const ed = requireEditor();
|
|
1963
|
+
const safeHref = escapeHtml(href).replace(/"/g, """);
|
|
1964
|
+
const html = `<a href="${safeHref}">${escapeHtml(label)}</a>`;
|
|
1965
|
+
if (insertAtCaret(ed, html)) return true;
|
|
1966
|
+
const target = ed.getSelected();
|
|
1967
|
+
if (target && typeOf(target) === "mj-button") {
|
|
1968
|
+
target.addAttributes({ href });
|
|
1969
|
+
queueSelection();
|
|
1970
|
+
return true;
|
|
1971
|
+
}
|
|
1972
|
+
if (target && typeOf(target) === "mj-text") {
|
|
1973
|
+
target.append(` ${html}`);
|
|
1974
|
+
queueSelection();
|
|
1975
|
+
return true;
|
|
1976
|
+
}
|
|
1977
|
+
return false;
|
|
1978
|
+
},
|
|
1979
|
+
undo() {
|
|
1980
|
+
requireEditor().UndoManager.undo();
|
|
1981
|
+
emitHistory();
|
|
1982
|
+
},
|
|
1983
|
+
redo() {
|
|
1984
|
+
requireEditor().UndoManager.redo();
|
|
1985
|
+
emitHistory();
|
|
1986
|
+
},
|
|
1987
|
+
canUndo: () => Boolean(editor?.UndoManager.hasUndo()),
|
|
1988
|
+
canRedo: () => Boolean(editor?.UndoManager.hasRedo()),
|
|
1989
|
+
clearHistory() {
|
|
1990
|
+
requireEditor().UndoManager.clear();
|
|
1991
|
+
emitHistory();
|
|
1992
|
+
},
|
|
1993
|
+
getProject() {
|
|
1994
|
+
return withoutPlaceholders(requireEditor().getProjectData());
|
|
1995
|
+
},
|
|
1996
|
+
loadProject(project) {
|
|
1997
|
+
const ed = requireEditor();
|
|
1998
|
+
ed.select(null);
|
|
1999
|
+
muteUntilFlushed();
|
|
2000
|
+
ed.loadProjectData(project);
|
|
2001
|
+
emit("load");
|
|
2002
|
+
queueSelection();
|
|
2003
|
+
},
|
|
2004
|
+
loadMjml(mjml) {
|
|
2005
|
+
const ed = requireEditor();
|
|
2006
|
+
ed.select(null);
|
|
2007
|
+
muteUntilFlushed();
|
|
2008
|
+
ed.setComponents(mjml || EMPTY_MJML);
|
|
2009
|
+
emit("load");
|
|
2010
|
+
queueSelection();
|
|
2011
|
+
},
|
|
2012
|
+
exportMjml() {
|
|
2013
|
+
return String(requireEditor().runCommand("mjml-code") || "").replace(PLACEHOLDER_SRC_ATTR, "").trim();
|
|
2014
|
+
},
|
|
2015
|
+
exportHtml(mjml = null) {
|
|
2016
|
+
const source = mjml ?? adapter.exportMjml();
|
|
2017
|
+
const result = requireEditor().runCommand("mjml-code-to-html", { mjml: source }) || {};
|
|
2018
|
+
return {
|
|
2019
|
+
html: String(result.html || ""),
|
|
2020
|
+
errors: (result.errors || []).map((e) => ({
|
|
2021
|
+
line: e.line,
|
|
2022
|
+
message: String(e.formattedMessage || e.message || e),
|
|
2023
|
+
tagName: e.tagName
|
|
2024
|
+
}))
|
|
2025
|
+
};
|
|
2026
|
+
},
|
|
2027
|
+
setDevice(device) {
|
|
2028
|
+
requireEditor().setDevice(device);
|
|
2029
|
+
},
|
|
2030
|
+
getDevice() {
|
|
2031
|
+
const name = editor?.getDevice?.();
|
|
2032
|
+
return name === "mobile" ? "mobile" : "desktop";
|
|
2033
|
+
},
|
|
2034
|
+
startBlockDrag(block, event) {
|
|
2035
|
+
const ed = requireEditor();
|
|
2036
|
+
const engineBlock = ed.Blocks.get(`${BLOCK_PREFIX}${block.id}`);
|
|
2037
|
+
if (engineBlock) ed.Blocks.startDrag(engineBlock, event);
|
|
2038
|
+
},
|
|
2039
|
+
endBlockDrag(cancel = false) {
|
|
2040
|
+
editor?.Blocks.endDrag(cancel);
|
|
2041
|
+
},
|
|
2042
|
+
addShortcut(id, keys, handler) {
|
|
2043
|
+
const ed = requireEditor();
|
|
2044
|
+
const keymapId = `eb:${id}`;
|
|
2045
|
+
const engineKeys = keys.split(",").map((k) => k.trim()).flatMap((k) => k.startsWith("mod+") ? [`\u2318+${k.slice(4)}`, `ctrl+${k.slice(4)}`] : [k]).join(", ");
|
|
2046
|
+
ed.Keymaps.add(keymapId, engineKeys, () => handler(), { prevent: true });
|
|
2047
|
+
return () => {
|
|
2048
|
+
if (editor === ed) ed.Keymaps.remove(keymapId);
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
return adapter;
|
|
2053
|
+
}
|
|
2054
|
+
function createEngineHost(container) {
|
|
2055
|
+
const el = container.ownerDocument.createElement("div");
|
|
2056
|
+
el.className = "eb-engine-host";
|
|
2057
|
+
el.style.height = "100%";
|
|
2058
|
+
container.appendChild(el);
|
|
2059
|
+
return el;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// src/defaultEditor.js
|
|
2063
|
+
function createDefaultEmailEditor(options = {}) {
|
|
2064
|
+
const { locale } = options;
|
|
2065
|
+
return createEmailEditor({
|
|
2066
|
+
adapter: createGrapesJsAdapter({ locale }),
|
|
2067
|
+
blocks: emailBlocks,
|
|
2068
|
+
features: emailBuilderFeatures
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// src/ui/EmailCanvas.jsx
|
|
2073
|
+
import { useEffect as useEffect3, useRef as useRef2 } from "react";
|
|
2074
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
2075
|
+
function EmailCanvas({ editor, document = null, onReady, onError, className }) {
|
|
2076
|
+
const ref = useRef2(null);
|
|
2077
|
+
const documentRef = useRef2(document);
|
|
2078
|
+
const callbacks = useRef2({ onReady, onError });
|
|
2079
|
+
callbacks.current = { onReady, onError };
|
|
2080
|
+
useEffect3(() => {
|
|
2081
|
+
if (!editor || !ref.current) return void 0;
|
|
2082
|
+
let alive = true;
|
|
2083
|
+
editor.initialize({ container: ref.current, document: documentRef.current }).then(() => {
|
|
2084
|
+
if (alive && editor.status === "ready") callbacks.current.onReady?.(editor);
|
|
2085
|
+
}).catch((error) => {
|
|
2086
|
+
if (alive) callbacks.current.onError?.(error);
|
|
2087
|
+
});
|
|
2088
|
+
return () => {
|
|
2089
|
+
alive = false;
|
|
2090
|
+
editor.destroy();
|
|
2091
|
+
};
|
|
2092
|
+
}, [editor]);
|
|
2093
|
+
return /* @__PURE__ */ jsx7("div", { ref, className: cn("eb-canvas eb:h-full eb:w-full", className), "data-testid": "eb-canvas" });
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/ui/EmailBuilder.jsx
|
|
2097
|
+
import { useCallback as useCallback2, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef6, useState as useState11 } from "react";
|
|
2098
|
+
import { AlertTriangle, Loader2 as Loader24, Plus as Plus2, Save, SlidersHorizontal } from "lucide-react";
|
|
2099
|
+
|
|
2100
|
+
// src/features/autosave/scheduler.js
|
|
2101
|
+
function createAutosaveScheduler({
|
|
2102
|
+
run,
|
|
2103
|
+
idleMs = 2e4,
|
|
2104
|
+
maxWaitMs = 12e4,
|
|
2105
|
+
now = () => Date.now(),
|
|
2106
|
+
setTimer = (fn, ms) => setTimeout(fn, ms),
|
|
2107
|
+
clearTimer = (handle) => clearTimeout(handle)
|
|
2108
|
+
}) {
|
|
2109
|
+
let firstChangeAt = null;
|
|
2110
|
+
let handle = null;
|
|
2111
|
+
let disposed = false;
|
|
2112
|
+
function clear() {
|
|
2113
|
+
if (handle !== null) clearTimer(handle);
|
|
2114
|
+
handle = null;
|
|
2115
|
+
}
|
|
2116
|
+
function fire() {
|
|
2117
|
+
handle = null;
|
|
2118
|
+
firstChangeAt = null;
|
|
2119
|
+
if (!disposed) run();
|
|
2120
|
+
}
|
|
2121
|
+
return {
|
|
2122
|
+
notify() {
|
|
2123
|
+
if (disposed) return;
|
|
2124
|
+
const at = now();
|
|
2125
|
+
if (firstChangeAt === null) firstChangeAt = at;
|
|
2126
|
+
const remainingMax = Math.max(0, firstChangeAt + maxWaitMs - at);
|
|
2127
|
+
clear();
|
|
2128
|
+
handle = setTimer(fire, Math.min(idleMs, remainingMax));
|
|
2129
|
+
},
|
|
2130
|
+
reset() {
|
|
2131
|
+
clear();
|
|
2132
|
+
firstChangeAt = null;
|
|
2133
|
+
},
|
|
2134
|
+
get pending() {
|
|
2135
|
+
return handle !== null;
|
|
2136
|
+
},
|
|
2137
|
+
dispose() {
|
|
2138
|
+
disposed = true;
|
|
2139
|
+
clear();
|
|
2140
|
+
}
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// src/features/autosave/localDraft.js
|
|
2145
|
+
var KEY2 = "best-email-builder:new-template-draft";
|
|
2146
|
+
var MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2147
|
+
function usable(store) {
|
|
2148
|
+
return store && typeof store.getItem === "function" ? store : null;
|
|
2149
|
+
}
|
|
2150
|
+
function defaultStore() {
|
|
2151
|
+
try {
|
|
2152
|
+
return typeof window !== "undefined" ? window.localStorage : null;
|
|
2153
|
+
} catch {
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
function saveLocalDraft({ document, meta }, store = defaultStore()) {
|
|
2158
|
+
const target = usable(store);
|
|
2159
|
+
if (!target) return false;
|
|
2160
|
+
try {
|
|
2161
|
+
target.setItem(KEY2, JSON.stringify({ savedAt: Date.now(), document, meta }));
|
|
2162
|
+
return true;
|
|
2163
|
+
} catch {
|
|
2164
|
+
return false;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
function readLocalDraft(store = defaultStore(), { now = Date.now() } = {}) {
|
|
2168
|
+
const target = usable(store);
|
|
2169
|
+
if (!target) return null;
|
|
2170
|
+
try {
|
|
2171
|
+
const raw = target.getItem(KEY2);
|
|
2172
|
+
if (!raw) return null;
|
|
2173
|
+
const parsed = JSON.parse(raw);
|
|
2174
|
+
if (!parsed || now - Number(parsed.savedAt) > MAX_AGE_MS) {
|
|
2175
|
+
target.removeItem(KEY2);
|
|
2176
|
+
return null;
|
|
2177
|
+
}
|
|
2178
|
+
return { savedAt: Number(parsed.savedAt), document: readDocument(parsed.document), meta: parsed.meta || {} };
|
|
2179
|
+
} catch {
|
|
2180
|
+
target.removeItem(KEY2);
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function clearLocalDraft(store = defaultStore()) {
|
|
2185
|
+
try {
|
|
2186
|
+
usable(store)?.removeItem(KEY2);
|
|
2187
|
+
} catch {
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// src/ui/BlockSidebar.jsx
|
|
2192
|
+
import {
|
|
2193
|
+
Columns2,
|
|
2194
|
+
Columns3,
|
|
2195
|
+
Columns4,
|
|
2196
|
+
GalleryHorizontal,
|
|
2197
|
+
Heading,
|
|
2198
|
+
Image as ImageIcon,
|
|
2199
|
+
LayoutPanelLeft,
|
|
2200
|
+
Menu,
|
|
2201
|
+
MoveVertical,
|
|
2202
|
+
RectangleHorizontal,
|
|
2203
|
+
SeparatorHorizontal,
|
|
2204
|
+
Share2,
|
|
2205
|
+
Square,
|
|
2206
|
+
Type
|
|
2207
|
+
} from "lucide-react";
|
|
2208
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2209
|
+
var ICONS = {
|
|
2210
|
+
Heading,
|
|
2211
|
+
Type,
|
|
2212
|
+
Share2,
|
|
2213
|
+
Menu,
|
|
2214
|
+
Columns4,
|
|
2215
|
+
LayoutPanelLeft,
|
|
2216
|
+
GalleryHorizontal,
|
|
2217
|
+
Image: ImageIcon,
|
|
2218
|
+
RectangleHorizontal,
|
|
2219
|
+
SeparatorHorizontal,
|
|
2220
|
+
MoveVertical,
|
|
2221
|
+
Square,
|
|
2222
|
+
Columns2,
|
|
2223
|
+
Columns3
|
|
2224
|
+
};
|
|
2225
|
+
var GROUPS2 = [
|
|
2226
|
+
{ kind: "content", title: "Content" },
|
|
2227
|
+
{ kind: "layout", title: "Layout" }
|
|
2228
|
+
];
|
|
2229
|
+
function BlockTile({ block, disabled, onAdd, onDragStart, onDragEnd }) {
|
|
2230
|
+
const t = useT();
|
|
2231
|
+
const Icon = ICONS[block.icon] || Square;
|
|
2232
|
+
return /* @__PURE__ */ jsx8(TooltipWrap, { label: block.description ? t(block.description) : block.description, children: /* @__PURE__ */ jsxs7(
|
|
2233
|
+
"button",
|
|
2234
|
+
{
|
|
2235
|
+
type: "button",
|
|
2236
|
+
draggable: !disabled,
|
|
2237
|
+
disabled,
|
|
2238
|
+
onClick: () => onAdd(block.id),
|
|
2239
|
+
onDragStart: (e) => {
|
|
2240
|
+
e.dataTransfer.effectAllowed = "copy";
|
|
2241
|
+
e.dataTransfer.setData("text/plain", block.id);
|
|
2242
|
+
onDragStart(block.id, e.nativeEvent);
|
|
2243
|
+
},
|
|
2244
|
+
onDragEnd: (e) => onDragEnd(e.dataTransfer?.dropEffect === "none"),
|
|
2245
|
+
"data-block-id": block.id,
|
|
2246
|
+
className: cn(
|
|
2247
|
+
"eb:flex eb:h-20 eb:cursor-grab eb:select-none eb:flex-col eb:items-center eb:justify-center eb:gap-1.5 eb:rounded-lg2 eb:border eb:border-border eb:bg-background eb:px-2 eb:text-center eb:transition-colors eb:active:cursor-grabbing",
|
|
2248
|
+
"eb:hover:border-primary/50 eb:hover:bg-primary/5 eb:focus-visible:outline-none eb:focus-visible:ring-2 eb:focus-visible:ring-ring/50",
|
|
2249
|
+
disabled && "eb:cursor-not-allowed eb:opacity-50"
|
|
2250
|
+
),
|
|
2251
|
+
children: [
|
|
2252
|
+
/* @__PURE__ */ jsx8(Icon, { className: "eb:h-5 eb:w-5 eb:text-foreground-lighter", "aria-hidden": "true" }),
|
|
2253
|
+
/* @__PURE__ */ jsx8("span", { className: "eb:text-[12px] eb:text-foreground", children: t(block.label) })
|
|
2254
|
+
]
|
|
2255
|
+
}
|
|
2256
|
+
) });
|
|
2257
|
+
}
|
|
2258
|
+
function BlockSidebar({ editor, disabled = false, onAdded }) {
|
|
2259
|
+
const t = useT();
|
|
2260
|
+
const blocks = editor.listBlocks();
|
|
2261
|
+
return /* @__PURE__ */ jsx8("aside", { "aria-label": t("Blocks"), className: "eb:min-h-0 eb:space-y-5 eb:overflow-y-auto eb:border-r eb:border-border eb:p-3", children: GROUPS2.map((group) => {
|
|
2262
|
+
const items = blocks.filter((b) => b.kind === group.kind && b.mjml);
|
|
2263
|
+
if (!items.length) return null;
|
|
2264
|
+
return /* @__PURE__ */ jsxs7("section", { className: "eb:space-y-2", children: [
|
|
2265
|
+
/* @__PURE__ */ jsx8(SectionLabel, { children: t(group.title) }),
|
|
2266
|
+
/* @__PURE__ */ jsx8("div", { className: "eb:grid eb:grid-cols-2 eb:gap-2", children: items.map((block) => /* @__PURE__ */ jsx8(
|
|
2267
|
+
BlockTile,
|
|
2268
|
+
{
|
|
2269
|
+
block,
|
|
2270
|
+
disabled,
|
|
2271
|
+
onAdd: (id) => {
|
|
2272
|
+
if (editor.addBlock(id)) onAdded?.(id);
|
|
2273
|
+
},
|
|
2274
|
+
onDragStart: (id, event) => editor.startBlockDrag(id, event),
|
|
2275
|
+
onDragEnd: (cancel) => editor.endBlockDrag(cancel)
|
|
2276
|
+
},
|
|
2277
|
+
block.id
|
|
2278
|
+
)) })
|
|
2279
|
+
] }, group.kind);
|
|
2280
|
+
}) });
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// src/ui/ElementActions.jsx
|
|
2284
|
+
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Copy, Trash2 as Trash22 } from "lucide-react";
|
|
2285
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2286
|
+
function ElementActions({ editor, selection }) {
|
|
2287
|
+
const t = useT();
|
|
2288
|
+
if (!selection || !selection.canRemove && !selection.canDuplicate) return null;
|
|
2289
|
+
const horizontal = selection.tagName === "mj-column";
|
|
2290
|
+
const [Prev, Next] = horizontal ? [ArrowLeft, ArrowRight] : [ArrowUp, ArrowDown];
|
|
2291
|
+
const [prevLabel, nextLabel] = horizontal ? [t("Move left"), t("Move right")] : [t("Move up"), t("Move down")];
|
|
2292
|
+
return /* @__PURE__ */ jsxs8("div", { className: "eb:flex eb:items-center eb:gap-0.5 eb:border-b eb:border-border eb:px-3 eb:py-1.5", role: "toolbar", "aria-label": t("Element actions"), children: [
|
|
2293
|
+
/* @__PURE__ */ jsx9(TooltipWrap, { label: prevLabel, children: /* @__PURE__ */ jsx9(
|
|
2294
|
+
Button,
|
|
2295
|
+
{
|
|
2296
|
+
variant: "ghost",
|
|
2297
|
+
size: "icon-sm-compact",
|
|
2298
|
+
"aria-label": prevLabel,
|
|
2299
|
+
disabled: !selection.canMoveUp,
|
|
2300
|
+
onClick: () => editor.move(-1, selection.id),
|
|
2301
|
+
children: /* @__PURE__ */ jsx9(Prev, {})
|
|
2302
|
+
}
|
|
2303
|
+
) }),
|
|
2304
|
+
/* @__PURE__ */ jsx9(TooltipWrap, { label: nextLabel, children: /* @__PURE__ */ jsx9(
|
|
2305
|
+
Button,
|
|
2306
|
+
{
|
|
2307
|
+
variant: "ghost",
|
|
2308
|
+
size: "icon-sm-compact",
|
|
2309
|
+
"aria-label": nextLabel,
|
|
2310
|
+
disabled: !selection.canMoveDown,
|
|
2311
|
+
onClick: () => editor.move(1, selection.id),
|
|
2312
|
+
children: /* @__PURE__ */ jsx9(Next, {})
|
|
2313
|
+
}
|
|
2314
|
+
) }),
|
|
2315
|
+
/* @__PURE__ */ jsx9(TooltipWrap, { label: t("Duplicate"), children: /* @__PURE__ */ jsx9(
|
|
2316
|
+
Button,
|
|
2317
|
+
{
|
|
2318
|
+
variant: "ghost",
|
|
2319
|
+
size: "icon-sm-compact",
|
|
2320
|
+
"aria-label": t("Duplicate"),
|
|
2321
|
+
disabled: !selection.canDuplicate,
|
|
2322
|
+
onClick: () => editor.duplicate(selection.id),
|
|
2323
|
+
children: /* @__PURE__ */ jsx9(Copy, {})
|
|
2324
|
+
}
|
|
2325
|
+
) }),
|
|
2326
|
+
/* @__PURE__ */ jsx9(TooltipWrap, { label: t("Delete (Del)"), children: /* @__PURE__ */ jsx9(
|
|
2327
|
+
Button,
|
|
2328
|
+
{
|
|
2329
|
+
variant: "ghost",
|
|
2330
|
+
size: "icon-sm-compact",
|
|
2331
|
+
"aria-label": t("Delete"),
|
|
2332
|
+
disabled: !selection.canRemove,
|
|
2333
|
+
onClick: () => editor.remove(selection.id),
|
|
2334
|
+
className: "eb:ml-auto eb:text-foreground-muted eb:hover:bg-destructive/10 eb:hover:text-destructive",
|
|
2335
|
+
children: /* @__PURE__ */ jsx9(Trash22, {})
|
|
2336
|
+
}
|
|
2337
|
+
) })
|
|
2338
|
+
] });
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
// src/ui/PropertiesPanel.jsx
|
|
2342
|
+
import { useMemo as useMemo4, useRef as useRef4 } from "react";
|
|
2343
|
+
import { X } from "lucide-react";
|
|
2344
|
+
|
|
2345
|
+
// src/ui/fields.jsx
|
|
2346
|
+
import { useEffect as useEffect4, useRef as useRef3, useState as useState8 } from "react";
|
|
2347
|
+
|
|
2348
|
+
// src/ui/primitives/color-picker.jsx
|
|
2349
|
+
import { useState as useState7 } from "react";
|
|
2350
|
+
import { HexAlphaColorPicker } from "react-colorful";
|
|
2351
|
+
import { Check } from "lucide-react";
|
|
2352
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2353
|
+
var PRESET_COLORS = [
|
|
2354
|
+
"#8B5CF6",
|
|
2355
|
+
"#A78BFA",
|
|
2356
|
+
"#6366F1",
|
|
2357
|
+
"#3B82F6",
|
|
2358
|
+
"#06B6D4",
|
|
2359
|
+
"#17A2B8",
|
|
2360
|
+
"#10B981",
|
|
2361
|
+
"#22C55E",
|
|
2362
|
+
"#28A745",
|
|
2363
|
+
"#84CC16",
|
|
2364
|
+
"#EAB308",
|
|
2365
|
+
"#FFC107",
|
|
2366
|
+
"#F59E0B",
|
|
2367
|
+
"#F97316",
|
|
2368
|
+
"#EF4444",
|
|
2369
|
+
"#DC3545",
|
|
2370
|
+
"#EC4899",
|
|
2371
|
+
"#6B7280"
|
|
2372
|
+
];
|
|
2373
|
+
var FALLBACK_COLOR = "#8B5CF6";
|
|
2374
|
+
var isValidHex = (v) => /^#([0-9A-Fa-f]{8}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{3})$/.test(v || "");
|
|
2375
|
+
function normalizeHex(v) {
|
|
2376
|
+
if (!v) return "";
|
|
2377
|
+
let s = v.trim();
|
|
2378
|
+
if (s && !s.startsWith("#")) s = `#${s}`;
|
|
2379
|
+
return s.toUpperCase();
|
|
2380
|
+
}
|
|
2381
|
+
function ColorPicker({ value, onChange, id, className }) {
|
|
2382
|
+
const t = useT();
|
|
2383
|
+
const [open, setOpen] = useState7(false);
|
|
2384
|
+
const valid = isValidHex(value);
|
|
2385
|
+
const normalized = valid ? normalizeHex(value) : "";
|
|
2386
|
+
const emit = (hex) => onChange(normalizeHex(hex));
|
|
2387
|
+
return /* @__PURE__ */ jsxs9("div", { className: cn("eb:flex eb:gap-2", className), children: [
|
|
2388
|
+
/* @__PURE__ */ jsxs9(Popover, { open, onOpenChange: setOpen, children: [
|
|
2389
|
+
/* @__PURE__ */ jsx10(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx10(
|
|
2390
|
+
"button",
|
|
2391
|
+
{
|
|
2392
|
+
type: "button",
|
|
2393
|
+
"aria-label": t("Pick a colour"),
|
|
2394
|
+
title: t("Pick a colour"),
|
|
2395
|
+
className: "eb-checker eb:relative eb:h-8 eb:w-[44px] eb:shrink-0 eb:overflow-hidden eb:rounded-lg2 eb:shadow-sm eb:ring-1 eb:ring-black/10 eb:ring-inset eb:transition-transform eb:hover:scale-[1.04]",
|
|
2396
|
+
children: /* @__PURE__ */ jsx10("span", { className: "eb:absolute eb:inset-0", style: { backgroundColor: valid ? value : "transparent" }, "aria-hidden": true })
|
|
2397
|
+
}
|
|
2398
|
+
) }),
|
|
2399
|
+
/* @__PURE__ */ jsxs9(PopoverContent, { align: "start", className: "eb:w-64 eb:space-y-3 eb:p-3", children: [
|
|
2400
|
+
/* @__PURE__ */ jsx10("div", { className: "eb-picker", children: /* @__PURE__ */ jsx10(HexAlphaColorPicker, { color: valid ? value : FALLBACK_COLOR, onChange: emit }) }),
|
|
2401
|
+
/* @__PURE__ */ jsxs9("div", { className: "eb:flex eb:items-center eb:gap-2", children: [
|
|
2402
|
+
/* @__PURE__ */ jsx10(
|
|
2403
|
+
"span",
|
|
2404
|
+
{
|
|
2405
|
+
className: "eb-checker eb:relative eb:size-8 eb:shrink-0 eb:overflow-hidden eb:rounded-lg2 eb:ring-1 eb:ring-black/10 eb:ring-inset",
|
|
2406
|
+
"aria-hidden": true,
|
|
2407
|
+
children: /* @__PURE__ */ jsx10("span", { className: "eb:absolute eb:inset-0", style: { backgroundColor: valid ? value : "transparent" } })
|
|
2408
|
+
}
|
|
2409
|
+
),
|
|
2410
|
+
/* @__PURE__ */ jsx10(
|
|
2411
|
+
Input,
|
|
2412
|
+
{
|
|
2413
|
+
value: value || "",
|
|
2414
|
+
onChange: (e) => emit(e.target.value),
|
|
2415
|
+
placeholder: "#RRGGBB",
|
|
2416
|
+
maxLength: 9,
|
|
2417
|
+
"aria-label": t("Hex code"),
|
|
2418
|
+
className: "eb:h-8 eb:flex-1 eb:font-mono eb:text-[12px] eb:uppercase"
|
|
2419
|
+
}
|
|
2420
|
+
)
|
|
2421
|
+
] }),
|
|
2422
|
+
/* @__PURE__ */ jsx10("div", { className: "eb:grid eb:grid-cols-6 eb:gap-1.5 eb:border-t eb:border-border eb:pt-2.5", children: PRESET_COLORS.map((c) => /* @__PURE__ */ jsx10(
|
|
2423
|
+
"button",
|
|
2424
|
+
{
|
|
2425
|
+
type: "button",
|
|
2426
|
+
onClick: () => onChange(c),
|
|
2427
|
+
title: c,
|
|
2428
|
+
"aria-label": c,
|
|
2429
|
+
className: "eb:relative eb:size-6 eb:rounded-lg2 eb:ring-1 eb:ring-black/10 eb:ring-inset eb:transition-transform eb:hover:scale-110",
|
|
2430
|
+
style: { backgroundColor: c },
|
|
2431
|
+
children: normalized === c && /* @__PURE__ */ jsx10(Check, { className: "eb:absolute eb:inset-0 eb:m-auto eb:size-3.5 eb:text-white eb:drop-shadow-[0_1px_1px_rgba(0,0,0,0.6)]" })
|
|
2432
|
+
},
|
|
2433
|
+
c
|
|
2434
|
+
)) })
|
|
2435
|
+
] })
|
|
2436
|
+
] }),
|
|
2437
|
+
/* @__PURE__ */ jsx10(
|
|
2438
|
+
Input,
|
|
2439
|
+
{
|
|
2440
|
+
id,
|
|
2441
|
+
value: value || "",
|
|
2442
|
+
onChange: (e) => onChange(e.target.value),
|
|
2443
|
+
maxLength: 9,
|
|
2444
|
+
placeholder: "#RRGGBB",
|
|
2445
|
+
className: "eb:h-8 eb:flex-1 eb:font-mono eb:text-[13px]"
|
|
2446
|
+
}
|
|
2447
|
+
)
|
|
2448
|
+
] });
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// src/ui/fields.jsx
|
|
2452
|
+
import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Loader2 as Loader23, Upload } from "lucide-react";
|
|
2453
|
+
import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2454
|
+
var COLOR_DEBOUNCE_MS = 250;
|
|
2455
|
+
function emailColor(value) {
|
|
2456
|
+
const v = String(value || "").trim();
|
|
2457
|
+
if (/^#[0-9a-f]{3}$/i.test(v) || /^#[0-9a-f]{6}$/i.test(v)) return v.toLowerCase();
|
|
2458
|
+
if (/^#[0-9a-f]{8}$/i.test(v)) return v.slice(0, 7).toLowerCase();
|
|
2459
|
+
return null;
|
|
2460
|
+
}
|
|
2461
|
+
function useDraft(value, toDraft) {
|
|
2462
|
+
const [draft, setDraft] = useState8(() => toDraft(value));
|
|
2463
|
+
const ref = useRef3(null);
|
|
2464
|
+
const convert = useRef3(toDraft);
|
|
2465
|
+
convert.current = toDraft;
|
|
2466
|
+
useEffect4(() => {
|
|
2467
|
+
const el = ref.current;
|
|
2468
|
+
const focused = el && el.ownerDocument?.activeElement && el.contains(el.ownerDocument.activeElement);
|
|
2469
|
+
if (!focused) setDraft(convert.current(value));
|
|
2470
|
+
}, [value]);
|
|
2471
|
+
return [draft, setDraft, ref];
|
|
2472
|
+
}
|
|
2473
|
+
var asText = (v) => v || "";
|
|
2474
|
+
function FieldLabel({ id, children }) {
|
|
2475
|
+
return /* @__PURE__ */ jsx11(Label, { htmlFor: id, className: "eb:text-[12px] eb:font-normal eb:text-foreground-lighter", children });
|
|
2476
|
+
}
|
|
2477
|
+
function FieldError({ children }) {
|
|
2478
|
+
return children ? /* @__PURE__ */ jsx11("p", { className: "eb:text-[12px] eb:text-destructive", children }) : null;
|
|
2479
|
+
}
|
|
2480
|
+
function TextField({ id, property, value, onCommit, addon = null, error = null }) {
|
|
2481
|
+
const t = useT();
|
|
2482
|
+
const [draft, setDraft, ref] = useDraft(value, asText);
|
|
2483
|
+
const isUrl = property.type === "url" || property.type === "image";
|
|
2484
|
+
const kind = property.type === "image" ? "image" : "link";
|
|
2485
|
+
const invalid = isUrl && draft.trim() !== "" && !isSafeUrl(draft, kind);
|
|
2486
|
+
const commit = () => {
|
|
2487
|
+
const next = draft.trim();
|
|
2488
|
+
if (invalid || next === (value || "")) return;
|
|
2489
|
+
onCommit(next || null);
|
|
2490
|
+
};
|
|
2491
|
+
const keyDown = (e) => {
|
|
2492
|
+
if (e.key === "Escape") {
|
|
2493
|
+
setDraft(value || "");
|
|
2494
|
+
e.currentTarget.blur();
|
|
2495
|
+
} else if (e.key === "Enter" && property.type !== "textarea") {
|
|
2496
|
+
e.preventDefault();
|
|
2497
|
+
commit();
|
|
2498
|
+
}
|
|
2499
|
+
};
|
|
2500
|
+
const shared = {
|
|
2501
|
+
id,
|
|
2502
|
+
value: draft,
|
|
2503
|
+
placeholder: property.placeholder ? t(property.placeholder) : property.placeholder,
|
|
2504
|
+
onChange: (e) => setDraft(e.target.value),
|
|
2505
|
+
onBlur: commit,
|
|
2506
|
+
onKeyDown: keyDown,
|
|
2507
|
+
"aria-invalid": invalid || void 0
|
|
2508
|
+
};
|
|
2509
|
+
return /* @__PURE__ */ jsxs10("div", { ref, className: "eb:space-y-1.5", children: [
|
|
2510
|
+
/* @__PURE__ */ jsx11(FieldLabel, { id, children: t(property.label) }),
|
|
2511
|
+
property.type === "textarea" ? /* @__PURE__ */ jsx11(Textarea, { rows: 3, ...shared }) : addon ? /* @__PURE__ */ jsxs10("div", { className: "eb:flex eb:gap-2", children: [
|
|
2512
|
+
/* @__PURE__ */ jsx11(Input, { ...shared, className: "eb:min-w-0 eb:flex-1" }),
|
|
2513
|
+
addon
|
|
2514
|
+
] }) : /* @__PURE__ */ jsx11(Input, { ...shared }),
|
|
2515
|
+
/* @__PURE__ */ jsx11(FieldError, { children: invalid ? kind === "image" ? t("An https:// address is required") : t("Address not allowed") : error })
|
|
2516
|
+
] });
|
|
2517
|
+
}
|
|
2518
|
+
function ImageField({ id, property, value, onCommit, services }) {
|
|
2519
|
+
const t = useT();
|
|
2520
|
+
const inputRef = useRef3(
|
|
2521
|
+
/** @type {HTMLInputElement|null} */
|
|
2522
|
+
null
|
|
2523
|
+
);
|
|
2524
|
+
const [busy, setBusy] = useState8(false);
|
|
2525
|
+
const [error, setError] = useState8(
|
|
2526
|
+
/** @type {string|null} */
|
|
2527
|
+
null
|
|
2528
|
+
);
|
|
2529
|
+
const upload = services?.uploadImage;
|
|
2530
|
+
const pick = async (event) => {
|
|
2531
|
+
const file = event.target.files?.[0];
|
|
2532
|
+
event.target.value = "";
|
|
2533
|
+
if (!file || !upload) return;
|
|
2534
|
+
const refused = imageUploadError(file, { t });
|
|
2535
|
+
setError(refused);
|
|
2536
|
+
if (refused) return;
|
|
2537
|
+
setBusy(true);
|
|
2538
|
+
try {
|
|
2539
|
+
const { path } = await upload(file);
|
|
2540
|
+
onCommit(path);
|
|
2541
|
+
} catch (err) {
|
|
2542
|
+
setError(err?.message || t("Upload failed"));
|
|
2543
|
+
} finally {
|
|
2544
|
+
setBusy(false);
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
const addon = upload ? /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
2548
|
+
/* @__PURE__ */ jsx11(
|
|
2549
|
+
"input",
|
|
2550
|
+
{
|
|
2551
|
+
ref: inputRef,
|
|
2552
|
+
type: "file",
|
|
2553
|
+
accept: "image/png,image/jpeg,image/gif",
|
|
2554
|
+
className: "eb:sr-only",
|
|
2555
|
+
tabIndex: -1,
|
|
2556
|
+
"aria-hidden": "true",
|
|
2557
|
+
"data-testid": `${id}-file`,
|
|
2558
|
+
onChange: pick
|
|
2559
|
+
}
|
|
2560
|
+
),
|
|
2561
|
+
/* @__PURE__ */ jsxs10(
|
|
2562
|
+
Button,
|
|
2563
|
+
{
|
|
2564
|
+
type: "button",
|
|
2565
|
+
variant: "outline",
|
|
2566
|
+
size: "sm",
|
|
2567
|
+
disabled: busy,
|
|
2568
|
+
onClick: () => inputRef.current?.click(),
|
|
2569
|
+
children: [
|
|
2570
|
+
busy ? /* @__PURE__ */ jsx11(Loader23, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx11(Upload, {}),
|
|
2571
|
+
t("Upload")
|
|
2572
|
+
]
|
|
2573
|
+
}
|
|
2574
|
+
)
|
|
2575
|
+
] }) : null;
|
|
2576
|
+
return /* @__PURE__ */ jsx11(TextField, { id, property, value, onCommit, addon, error });
|
|
2577
|
+
}
|
|
2578
|
+
var asNumber = (v) => {
|
|
2579
|
+
const n = Number.parseFloat(String(v || ""));
|
|
2580
|
+
return Number.isFinite(n) ? String(n) : "";
|
|
2581
|
+
};
|
|
2582
|
+
function SizeField({ id, property, value, onCommit }) {
|
|
2583
|
+
const t = useT();
|
|
2584
|
+
const [draft, setDraft, ref] = useDraft(value, asNumber);
|
|
2585
|
+
const commit = (raw = draft) => {
|
|
2586
|
+
if (String(raw).trim() === "") {
|
|
2587
|
+
if (value) onCommit(null);
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
let n = Number.parseFloat(String(raw));
|
|
2591
|
+
if (!Number.isFinite(n)) return;
|
|
2592
|
+
if (property.min !== void 0) n = Math.max(property.min, n);
|
|
2593
|
+
if (property.max !== void 0) n = Math.min(property.max, n);
|
|
2594
|
+
setDraft(String(n));
|
|
2595
|
+
const next = `${n}${property.unit || "px"}`;
|
|
2596
|
+
if (next !== value) onCommit(next);
|
|
2597
|
+
};
|
|
2598
|
+
return /* @__PURE__ */ jsxs10("div", { ref, className: "eb:space-y-1.5", children: [
|
|
2599
|
+
/* @__PURE__ */ jsx11(FieldLabel, { id, children: t(property.label) }),
|
|
2600
|
+
/* @__PURE__ */ jsxs10("div", { className: "eb:relative", children: [
|
|
2601
|
+
/* @__PURE__ */ jsx11(
|
|
2602
|
+
Input,
|
|
2603
|
+
{
|
|
2604
|
+
id,
|
|
2605
|
+
type: "number",
|
|
2606
|
+
inputMode: "numeric",
|
|
2607
|
+
min: property.min,
|
|
2608
|
+
max: property.max,
|
|
2609
|
+
value: draft,
|
|
2610
|
+
onChange: (e) => setDraft(e.target.value),
|
|
2611
|
+
onBlur: () => commit(),
|
|
2612
|
+
onKeyDown: (e) => {
|
|
2613
|
+
if (e.key === "Enter") commit();
|
|
2614
|
+
if (e.key === "Escape") setDraft(asNumber(value));
|
|
2615
|
+
},
|
|
2616
|
+
className: "eb:pr-9 eb:tabular-nums"
|
|
2617
|
+
}
|
|
2618
|
+
),
|
|
2619
|
+
/* @__PURE__ */ jsx11("span", { className: "eb:pointer-events-none eb:absolute eb:right-3 eb:top-1/2 eb:-translate-y-1/2 eb:text-[12px] eb:text-foreground-muted", children: property.unit || "px" })
|
|
2620
|
+
] })
|
|
2621
|
+
] });
|
|
2622
|
+
}
|
|
2623
|
+
function ColorField({ id, property, value, onCommit }) {
|
|
2624
|
+
const t = useT();
|
|
2625
|
+
const [draft, setDraft, ref] = useDraft(value, asText);
|
|
2626
|
+
const timer = useRef3(null);
|
|
2627
|
+
const latest = useRef3({ value, onCommit });
|
|
2628
|
+
latest.current = { value, onCommit };
|
|
2629
|
+
useEffect4(() => () => clearTimeout(timer.current), []);
|
|
2630
|
+
const change = (next) => {
|
|
2631
|
+
setDraft(next);
|
|
2632
|
+
clearTimeout(timer.current);
|
|
2633
|
+
timer.current = setTimeout(() => {
|
|
2634
|
+
const color = emailColor(next);
|
|
2635
|
+
if (!String(next || "").trim()) {
|
|
2636
|
+
if (latest.current.value) latest.current.onCommit(null);
|
|
2637
|
+
} else if (color && color !== latest.current.value) {
|
|
2638
|
+
latest.current.onCommit(color);
|
|
2639
|
+
}
|
|
2640
|
+
}, COLOR_DEBOUNCE_MS);
|
|
2641
|
+
};
|
|
2642
|
+
return /* @__PURE__ */ jsxs10("div", { ref, className: "eb:space-y-1.5", children: [
|
|
2643
|
+
/* @__PURE__ */ jsx11(FieldLabel, { id, children: t(property.label) }),
|
|
2644
|
+
/* @__PURE__ */ jsx11(ColorPicker, { id, value: draft, onChange: change })
|
|
2645
|
+
] });
|
|
2646
|
+
}
|
|
2647
|
+
var DEFAULT_OPTION = "__default__";
|
|
2648
|
+
function SelectField({ id, property, value, onCommit }) {
|
|
2649
|
+
const t = useT();
|
|
2650
|
+
const current = value || DEFAULT_OPTION;
|
|
2651
|
+
return /* @__PURE__ */ jsxs10("div", { className: "eb:space-y-1.5", children: [
|
|
2652
|
+
/* @__PURE__ */ jsx11(FieldLabel, { id, children: t(property.label) }),
|
|
2653
|
+
/* @__PURE__ */ jsxs10(
|
|
2654
|
+
Select,
|
|
2655
|
+
{
|
|
2656
|
+
value: current,
|
|
2657
|
+
onValueChange: (next) => {
|
|
2658
|
+
const v = next === DEFAULT_OPTION ? "" : next;
|
|
2659
|
+
if (v !== (value || "")) onCommit(v || null);
|
|
2660
|
+
},
|
|
2661
|
+
children: [
|
|
2662
|
+
/* @__PURE__ */ jsx11(SelectTrigger, { id, className: "eb:w-full", children: /* @__PURE__ */ jsx11(SelectValue, { placeholder: t("Default") }) }),
|
|
2663
|
+
/* @__PURE__ */ jsxs10(SelectContent, { children: [
|
|
2664
|
+
!(property.options || []).some((o) => !o.value) && /* @__PURE__ */ jsx11(SelectItem, { value: DEFAULT_OPTION, children: t("Default") }),
|
|
2665
|
+
(property.options || []).map((o) => /* @__PURE__ */ jsx11(SelectItem, { value: o.value || DEFAULT_OPTION, children: t(o.label) }, o.value || DEFAULT_OPTION))
|
|
2666
|
+
] })
|
|
2667
|
+
]
|
|
2668
|
+
}
|
|
2669
|
+
)
|
|
2670
|
+
] });
|
|
2671
|
+
}
|
|
2672
|
+
var ALIGN_ICONS = { left: AlignLeft, center: AlignCenter, right: AlignRight, justify: AlignJustify };
|
|
2673
|
+
function AlignField({ id, property, value, onCommit }) {
|
|
2674
|
+
const t = useT();
|
|
2675
|
+
return /* @__PURE__ */ jsxs10("div", { className: "eb:space-y-1.5", children: [
|
|
2676
|
+
/* @__PURE__ */ jsx11(FieldLabel, { id, children: t(property.label) }),
|
|
2677
|
+
/* @__PURE__ */ jsx11(
|
|
2678
|
+
"div",
|
|
2679
|
+
{
|
|
2680
|
+
id,
|
|
2681
|
+
role: "radiogroup",
|
|
2682
|
+
"aria-label": t(property.label),
|
|
2683
|
+
className: "eb:flex eb:w-fit eb:items-center eb:gap-0.5 eb:rounded-lg2 eb:border eb:border-border-strong eb:bg-surface-75 eb:p-0.5",
|
|
2684
|
+
children: (property.options || []).map((option) => {
|
|
2685
|
+
const Icon = ALIGN_ICONS[option.value] || AlignLeft;
|
|
2686
|
+
const active = value === option.value;
|
|
2687
|
+
return /* @__PURE__ */ jsx11(TooltipWrap, { label: t(option.label), children: /* @__PURE__ */ jsx11(
|
|
2688
|
+
"button",
|
|
2689
|
+
{
|
|
2690
|
+
type: "button",
|
|
2691
|
+
role: "radio",
|
|
2692
|
+
"aria-checked": active,
|
|
2693
|
+
"aria-label": t(option.label),
|
|
2694
|
+
onClick: () => !active && onCommit(option.value),
|
|
2695
|
+
className: cn(
|
|
2696
|
+
"eb:inline-flex eb:h-7 eb:w-7 eb:items-center eb:justify-center eb:rounded-lg2 eb:transition-colors",
|
|
2697
|
+
active ? "eb:bg-primary eb:text-primary-foreground" : "eb:text-foreground-lighter eb:hover:bg-surface-200 eb:hover:text-foreground"
|
|
2698
|
+
),
|
|
2699
|
+
children: /* @__PURE__ */ jsx11(Icon, { className: "eb:h-3.5 eb:w-3.5" })
|
|
2700
|
+
}
|
|
2701
|
+
) }, option.value);
|
|
2702
|
+
})
|
|
2703
|
+
}
|
|
2704
|
+
)
|
|
2705
|
+
] });
|
|
2706
|
+
}
|
|
2707
|
+
var SIDE_LABELS = [
|
|
2708
|
+
["top", "Top"],
|
|
2709
|
+
["right", "Right"],
|
|
2710
|
+
["bottom", "Bottom"],
|
|
2711
|
+
["left", "Left"]
|
|
2712
|
+
];
|
|
2713
|
+
var asSides = (v) => parseSpacing(v || "").map((s) => String(Number.parseFloat(s) || 0));
|
|
2714
|
+
function SpacingField({ id, property, value, onCommit }) {
|
|
2715
|
+
const t = useT();
|
|
2716
|
+
const [draft, setDraft, ref] = useDraft(value, asSides);
|
|
2717
|
+
const commit = (sides = draft) => {
|
|
2718
|
+
const next = formatSpacing(sides.map((s) => `${Math.max(0, Math.min(200, Number.parseFloat(s) || 0))}px`));
|
|
2719
|
+
if (next !== (value || "")) onCommit(next);
|
|
2720
|
+
};
|
|
2721
|
+
return /* @__PURE__ */ jsxs10("fieldset", { ref, className: "eb:space-y-1.5", children: [
|
|
2722
|
+
/* @__PURE__ */ jsx11("legend", { className: "eb:mb-1.5 eb:text-[12px] eb:text-foreground-lighter", children: t(property.label) }),
|
|
2723
|
+
/* @__PURE__ */ jsx11("div", { className: "eb:grid eb:grid-cols-4 eb:gap-1.5", children: SIDE_LABELS.map(([side, label], i) => /* @__PURE__ */ jsxs10("div", { className: "eb:space-y-1", children: [
|
|
2724
|
+
/* @__PURE__ */ jsx11(
|
|
2725
|
+
Input,
|
|
2726
|
+
{
|
|
2727
|
+
id: `${id}-${side}`,
|
|
2728
|
+
type: "number",
|
|
2729
|
+
min: 0,
|
|
2730
|
+
max: 200,
|
|
2731
|
+
"aria-label": t("{label} \u2014 {side}", { label: t(property.label), side: t(label) }),
|
|
2732
|
+
title: t(label),
|
|
2733
|
+
value: draft[i],
|
|
2734
|
+
onChange: (e) => setDraft((d) => d.map((v, j) => j === i ? e.target.value : v)),
|
|
2735
|
+
onBlur: () => commit(),
|
|
2736
|
+
onKeyDown: (e) => {
|
|
2737
|
+
if (e.key === "Enter") commit();
|
|
2738
|
+
},
|
|
2739
|
+
className: "eb:px-2 eb:text-center eb:tabular-nums"
|
|
2740
|
+
}
|
|
2741
|
+
),
|
|
2742
|
+
/* @__PURE__ */ jsx11("span", { className: "eb:block eb:text-center eb:text-[10.5px] eb:text-foreground-muted", children: t(label) })
|
|
2743
|
+
] }, side)) })
|
|
2744
|
+
] });
|
|
2745
|
+
}
|
|
2746
|
+
var FIELDS = {
|
|
2747
|
+
text: TextField,
|
|
2748
|
+
textarea: TextField,
|
|
2749
|
+
url: TextField,
|
|
2750
|
+
image: ImageField,
|
|
2751
|
+
size: SizeField,
|
|
2752
|
+
color: ColorField,
|
|
2753
|
+
select: SelectField,
|
|
2754
|
+
align: AlignField,
|
|
2755
|
+
spacing: SpacingField
|
|
2756
|
+
};
|
|
2757
|
+
function PropertyField({ property, value, onCommit, idPrefix = "eb", services = null }) {
|
|
2758
|
+
const Field = FIELDS[property.type] || TextField;
|
|
2759
|
+
const id = `${idPrefix}-${property.key}`;
|
|
2760
|
+
return /* @__PURE__ */ jsx11(Field, { id, property, value, onCommit, services });
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
// src/features/variables/VariableInsertButton.jsx
|
|
2764
|
+
import { useState as useState9 } from "react";
|
|
2765
|
+
import { Braces as Braces3 } from "lucide-react";
|
|
2766
|
+
import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2767
|
+
function VariableInsertButton({ variables, inputRef, value, onChange, label }) {
|
|
2768
|
+
const [open, setOpen] = useState9(false);
|
|
2769
|
+
const insert = (variable) => {
|
|
2770
|
+
const input = inputRef.current;
|
|
2771
|
+
const text = String(value || "");
|
|
2772
|
+
const token = tokenOf(variable);
|
|
2773
|
+
const start = input?.selectionStart ?? text.length;
|
|
2774
|
+
const end = input?.selectionEnd ?? text.length;
|
|
2775
|
+
const next = `${text.slice(0, start)}${token}${text.slice(end)}`;
|
|
2776
|
+
onChange(next);
|
|
2777
|
+
setOpen(false);
|
|
2778
|
+
requestAnimationFrame(() => {
|
|
2779
|
+
if (!input) return;
|
|
2780
|
+
input.focus();
|
|
2781
|
+
input.setSelectionRange(start + token.length, start + token.length);
|
|
2782
|
+
});
|
|
2783
|
+
};
|
|
2784
|
+
return /* @__PURE__ */ jsxs11(Popover, { open, onOpenChange: setOpen, children: [
|
|
2785
|
+
/* @__PURE__ */ jsx12(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx12(Button, { variant: "ghost", size: "icon-xs", "aria-label": label, title: label, children: /* @__PURE__ */ jsx12(Braces3, {}) }) }),
|
|
2786
|
+
/* @__PURE__ */ jsx12(PopoverContent, { align: "end", className: "eb:w-72 eb:p-0", onCloseAutoFocus: (e) => e.preventDefault(), children: /* @__PURE__ */ jsx12(VariablePicker, { variables, onPick: insert, filter: (v) => v.kind !== "link" }) })
|
|
2787
|
+
] });
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// src/ui/PropertiesPanel.jsx
|
|
2791
|
+
import { Fragment as Fragment5, jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2792
|
+
var GROUP_ORDER = Object.values(GROUPS);
|
|
2793
|
+
function grouped(properties) {
|
|
2794
|
+
const map = /* @__PURE__ */ new Map();
|
|
2795
|
+
for (const property of properties) {
|
|
2796
|
+
if (!map.has(property.group)) map.set(property.group, []);
|
|
2797
|
+
map.get(property.group).push(property);
|
|
2798
|
+
}
|
|
2799
|
+
return [...map.entries()].sort(
|
|
2800
|
+
([a], [b]) => (GROUP_ORDER.indexOf(a) + 1 || 99) - (GROUP_ORDER.indexOf(b) + 1 || 99)
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2803
|
+
function PropertyGroups({ editor, element, block, idPrefix, services = null }) {
|
|
2804
|
+
const t = useT();
|
|
2805
|
+
return grouped(block?.properties || []).map(([group, properties]) => /* @__PURE__ */ jsxs12("section", { className: "eb:space-y-3", children: [
|
|
2806
|
+
/* @__PURE__ */ jsx13(SectionLabel, { children: t(group) }),
|
|
2807
|
+
properties.map((property) => /* @__PURE__ */ jsx13(
|
|
2808
|
+
PropertyField,
|
|
2809
|
+
{
|
|
2810
|
+
idPrefix,
|
|
2811
|
+
property,
|
|
2812
|
+
value: element.values[property.key],
|
|
2813
|
+
services,
|
|
2814
|
+
onCommit: (value) => editor.updateProperties({ [property.key]: value }, element.id)
|
|
2815
|
+
},
|
|
2816
|
+
`${element.id}:${property.key}`
|
|
2817
|
+
))
|
|
2818
|
+
] }, group));
|
|
2819
|
+
}
|
|
2820
|
+
var LABEL_PROPERTY = { key: "content", label: "Label", type: "text", group: GROUPS.content };
|
|
2821
|
+
function PanelHeader({ title, onClose, closeLabel = "Deselect" }) {
|
|
2822
|
+
const t = useT();
|
|
2823
|
+
return /* @__PURE__ */ jsxs12("header", { className: "eb:sticky eb:top-0 eb:z-10 eb:flex eb:h-11 eb:shrink-0 eb:items-center eb:gap-2 eb:border-b eb:border-border eb:bg-background eb:px-4", children: [
|
|
2824
|
+
/* @__PURE__ */ jsx13("h3", { className: "eb:min-w-0 eb:flex-1 eb:text-[13px] eb:font-medium eb:text-foreground", children: /* @__PURE__ */ jsx13(TruncatedText, { children: title }) }),
|
|
2825
|
+
onClose && /* @__PURE__ */ jsx13(Button, { variant: "ghost", size: "icon-sm-compact", onClick: onClose, "aria-label": t(closeLabel), children: /* @__PURE__ */ jsx13(X, {}) })
|
|
2826
|
+
] });
|
|
2827
|
+
}
|
|
2828
|
+
function TemplateSettings({ editor, meta, onMetaChange, changes, showErrors, variables = null, onDismiss = null }) {
|
|
2829
|
+
const t = useT();
|
|
2830
|
+
const subjectRef = useRef4(null);
|
|
2831
|
+
const preheaderRef = useRef4(null);
|
|
2832
|
+
const rootId = editor.getRootId();
|
|
2833
|
+
const root = useMemo4(() => rootId ? editor.getElement(rootId) : null, [editor, rootId, changes]);
|
|
2834
|
+
const bodyBlock2 = root?.blockId ? editor.getBlock(root.blockId) : null;
|
|
2835
|
+
const field = (key) => (e) => onMetaChange({ ...meta, [key]: e.target.value });
|
|
2836
|
+
return /* @__PURE__ */ jsxs12(Fragment5, { children: [
|
|
2837
|
+
/* @__PURE__ */ jsx13(PanelHeader, { title: t("Email"), onClose: onDismiss, closeLabel: "Close" }),
|
|
2838
|
+
/* @__PURE__ */ jsxs12("div", { className: "eb:space-y-5 eb:p-4", children: [
|
|
2839
|
+
/* @__PURE__ */ jsxs12("section", { className: "eb:space-y-3", children: [
|
|
2840
|
+
/* @__PURE__ */ jsx13(SectionLabel, { children: t("Template") }),
|
|
2841
|
+
/* @__PURE__ */ jsxs12("div", { className: "eb:space-y-1.5", children: [
|
|
2842
|
+
/* @__PURE__ */ jsxs12(Label, { htmlFor: "eb-meta-name", className: "eb:text-[12px] eb:font-normal eb:text-foreground-lighter", children: [
|
|
2843
|
+
t("Name"),
|
|
2844
|
+
" ",
|
|
2845
|
+
/* @__PURE__ */ jsx13("span", { className: "eb:text-destructive", children: "*" })
|
|
2846
|
+
] }),
|
|
2847
|
+
/* @__PURE__ */ jsx13(
|
|
2848
|
+
Input,
|
|
2849
|
+
{
|
|
2850
|
+
id: "eb-meta-name",
|
|
2851
|
+
maxLength: 200,
|
|
2852
|
+
value: meta.name,
|
|
2853
|
+
onChange: field("name"),
|
|
2854
|
+
"aria-invalid": showErrors && !meta.name.trim() || void 0
|
|
2855
|
+
}
|
|
2856
|
+
),
|
|
2857
|
+
showErrors && !meta.name.trim() && /* @__PURE__ */ jsx13("p", { className: "eb:text-[12px] eb:text-destructive", children: t("Name is required") })
|
|
2858
|
+
] }),
|
|
2859
|
+
/* @__PURE__ */ jsxs12("div", { className: "eb:space-y-1.5", children: [
|
|
2860
|
+
/* @__PURE__ */ jsxs12("div", { className: "eb:flex eb:h-6 eb:items-center eb:justify-between", children: [
|
|
2861
|
+
/* @__PURE__ */ jsx13(Label, { htmlFor: "eb-meta-subject", className: "eb:text-[12px] eb:font-normal eb:text-foreground-lighter", children: t("Subject") }),
|
|
2862
|
+
variables && /* @__PURE__ */ jsx13(
|
|
2863
|
+
VariableInsertButton,
|
|
2864
|
+
{
|
|
2865
|
+
variables,
|
|
2866
|
+
inputRef: subjectRef,
|
|
2867
|
+
value: meta.subject,
|
|
2868
|
+
onChange: (subject) => onMetaChange({ ...meta, subject }),
|
|
2869
|
+
label: t("Insert a variable in the subject")
|
|
2870
|
+
}
|
|
2871
|
+
)
|
|
2872
|
+
] }),
|
|
2873
|
+
/* @__PURE__ */ jsx13(Input, { ref: subjectRef, id: "eb-meta-subject", maxLength: 250, value: meta.subject, onChange: field("subject") })
|
|
2874
|
+
] }),
|
|
2875
|
+
/* @__PURE__ */ jsxs12("div", { className: "eb:space-y-1.5", children: [
|
|
2876
|
+
/* @__PURE__ */ jsxs12("div", { className: "eb:flex eb:h-6 eb:items-center eb:justify-between", children: [
|
|
2877
|
+
/* @__PURE__ */ jsx13(Label, { htmlFor: "eb-meta-preheader", className: "eb:text-[12px] eb:font-normal eb:text-foreground-lighter", children: t("Preview text") }),
|
|
2878
|
+
variables && /* @__PURE__ */ jsx13(
|
|
2879
|
+
VariableInsertButton,
|
|
2880
|
+
{
|
|
2881
|
+
variables,
|
|
2882
|
+
inputRef: preheaderRef,
|
|
2883
|
+
value: meta.preheader,
|
|
2884
|
+
onChange: (preheader) => onMetaChange({ ...meta, preheader }),
|
|
2885
|
+
label: t("Insert a variable in the preview text")
|
|
2886
|
+
}
|
|
2887
|
+
)
|
|
2888
|
+
] }),
|
|
2889
|
+
/* @__PURE__ */ jsx13(
|
|
2890
|
+
Textarea,
|
|
2891
|
+
{
|
|
2892
|
+
ref: preheaderRef,
|
|
2893
|
+
id: "eb-meta-preheader",
|
|
2894
|
+
rows: 2,
|
|
2895
|
+
maxLength: 250,
|
|
2896
|
+
value: meta.preheader,
|
|
2897
|
+
onChange: field("preheader"),
|
|
2898
|
+
placeholder: t("Shown after the subject in the inbox")
|
|
2899
|
+
}
|
|
2900
|
+
)
|
|
2901
|
+
] })
|
|
2902
|
+
] }),
|
|
2903
|
+
root && bodyBlock2 && /* @__PURE__ */ jsx13(PropertyGroups, { editor, element: root, block: bodyBlock2, idPrefix: "eb-page" })
|
|
2904
|
+
] })
|
|
2905
|
+
] });
|
|
2906
|
+
}
|
|
2907
|
+
function PropertiesPanel({
|
|
2908
|
+
editor,
|
|
2909
|
+
selection,
|
|
2910
|
+
meta,
|
|
2911
|
+
onMetaChange,
|
|
2912
|
+
changes,
|
|
2913
|
+
actions = null,
|
|
2914
|
+
showErrors = false,
|
|
2915
|
+
services = null,
|
|
2916
|
+
variables = null,
|
|
2917
|
+
onDismiss = null
|
|
2918
|
+
}) {
|
|
2919
|
+
const t = useT();
|
|
2920
|
+
const block = selection?.blockId ? editor.getBlock(selection.blockId) : null;
|
|
2921
|
+
return /* @__PURE__ */ jsx13("aside", { "aria-label": t("Properties"), className: "eb:min-h-0 eb:overflow-y-auto eb:border-l eb:border-border", "data-testid": "eb-properties", children: !selection ? /* @__PURE__ */ jsx13(
|
|
2922
|
+
TemplateSettings,
|
|
2923
|
+
{
|
|
2924
|
+
editor,
|
|
2925
|
+
meta,
|
|
2926
|
+
onMetaChange,
|
|
2927
|
+
changes,
|
|
2928
|
+
showErrors,
|
|
2929
|
+
variables,
|
|
2930
|
+
onDismiss
|
|
2931
|
+
}
|
|
2932
|
+
) : /* @__PURE__ */ jsxs12(Fragment5, { children: [
|
|
2933
|
+
/* @__PURE__ */ jsx13(
|
|
2934
|
+
PanelHeader,
|
|
2935
|
+
{
|
|
2936
|
+
title: t(selection.label),
|
|
2937
|
+
onClose: onDismiss || (() => editor.select(null)),
|
|
2938
|
+
closeLabel: onDismiss ? "Close" : "Deselect"
|
|
2939
|
+
}
|
|
2940
|
+
),
|
|
2941
|
+
actions,
|
|
2942
|
+
/* @__PURE__ */ jsxs12("div", { className: "eb:space-y-5 eb:p-4", children: [
|
|
2943
|
+
selection.content !== null && selection.content !== void 0 && /* @__PURE__ */ jsxs12("section", { className: "eb:space-y-3", children: [
|
|
2944
|
+
/* @__PURE__ */ jsx13(SectionLabel, { children: t(GROUPS.content) }),
|
|
2945
|
+
/* @__PURE__ */ jsx13(
|
|
2946
|
+
PropertyField,
|
|
2947
|
+
{
|
|
2948
|
+
idPrefix: "eb-prop",
|
|
2949
|
+
property: LABEL_PROPERTY,
|
|
2950
|
+
value: selection.content || "",
|
|
2951
|
+
onCommit: (value) => editor.setContent(value || "", selection.id)
|
|
2952
|
+
},
|
|
2953
|
+
`${selection.id}:content`
|
|
2954
|
+
)
|
|
2955
|
+
] }),
|
|
2956
|
+
selection.tagName === "mj-text" && /* @__PURE__ */ jsx13("p", { className: "eb:text-[12px] eb:text-foreground-muted", children: t("Double-click the text to edit it.") }),
|
|
2957
|
+
/* @__PURE__ */ jsx13(PropertyGroups, { editor, element: selection, block, idPrefix: "eb-prop", services })
|
|
2958
|
+
] })
|
|
2959
|
+
] }) });
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// src/ui/Toolbar.jsx
|
|
2963
|
+
import { ArrowLeft as ArrowLeft2, Monitor, Redo2, Smartphone, Undo2 } from "lucide-react";
|
|
2964
|
+
import { Fragment as Fragment6, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2965
|
+
function Toolbar({
|
|
2966
|
+
title,
|
|
2967
|
+
status,
|
|
2968
|
+
ready,
|
|
2969
|
+
canUndo,
|
|
2970
|
+
canRedo,
|
|
2971
|
+
onUndo,
|
|
2972
|
+
onRedo,
|
|
2973
|
+
device,
|
|
2974
|
+
onDeviceChange,
|
|
2975
|
+
onBack,
|
|
2976
|
+
actions = null,
|
|
2977
|
+
primary = null,
|
|
2978
|
+
compact = false
|
|
2979
|
+
}) {
|
|
2980
|
+
const t = useT();
|
|
2981
|
+
const identity = /* @__PURE__ */ jsxs13(Fragment6, { children: [
|
|
2982
|
+
/* @__PURE__ */ jsxs13(
|
|
2983
|
+
Button,
|
|
2984
|
+
{
|
|
2985
|
+
variant: "ghost",
|
|
2986
|
+
size: compact ? "icon-sm-compact" : "sm-compact",
|
|
2987
|
+
onClick: onBack,
|
|
2988
|
+
"aria-label": compact ? t("Back") : void 0,
|
|
2989
|
+
children: [
|
|
2990
|
+
/* @__PURE__ */ jsx14(ArrowLeft2, {}),
|
|
2991
|
+
!compact && t("Back")
|
|
2992
|
+
]
|
|
2993
|
+
}
|
|
2994
|
+
),
|
|
2995
|
+
/* @__PURE__ */ jsx14(Separator, { orientation: "vertical", className: "eb:h-5" }),
|
|
2996
|
+
/* @__PURE__ */ jsxs13("div", { className: cn("eb:min-w-0 eb:flex-1", !compact && "eb:max-w-[320px]"), children: [
|
|
2997
|
+
/* @__PURE__ */ jsx14("p", { className: "eb:text-[13px] eb:font-medium eb:text-foreground", children: /* @__PURE__ */ jsx14(TruncatedText, { children: title || t("New template") }) }),
|
|
2998
|
+
/* @__PURE__ */ jsx14("p", { className: "eb:text-[11px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: status })
|
|
2999
|
+
] })
|
|
3000
|
+
] });
|
|
3001
|
+
const editing = /* @__PURE__ */ jsxs13("div", { className: "eb:flex eb:shrink-0 eb:items-center eb:gap-2", children: [
|
|
3002
|
+
/* @__PURE__ */ jsxs13("div", { className: "eb:flex eb:items-center eb:gap-0.5", children: [
|
|
3003
|
+
/* @__PURE__ */ jsx14(TooltipWrap, { label: t("Undo (Ctrl+Z)"), children: /* @__PURE__ */ jsx14(Button, { variant: "ghost", size: "icon-sm-compact", onClick: onUndo, disabled: !ready || !canUndo, "aria-label": t("Undo"), children: /* @__PURE__ */ jsx14(Undo2, {}) }) }),
|
|
3004
|
+
/* @__PURE__ */ jsx14(TooltipWrap, { label: t("Redo (Ctrl+Y)"), children: /* @__PURE__ */ jsx14(Button, { variant: "ghost", size: "icon-sm-compact", onClick: onRedo, disabled: !ready || !canRedo, "aria-label": t("Redo"), children: /* @__PURE__ */ jsx14(Redo2, {}) }) })
|
|
3005
|
+
] }),
|
|
3006
|
+
/* @__PURE__ */ jsx14(Tabs, { value: device, onValueChange: onDeviceChange, children: /* @__PURE__ */ jsxs13(TabsList, { "aria-label": t("Device"), children: [
|
|
3007
|
+
/* @__PURE__ */ jsxs13(TabsTrigger, { value: "desktop", disabled: !ready, "aria-label": compact ? t("Desktop") : void 0, children: [
|
|
3008
|
+
/* @__PURE__ */ jsx14(Monitor, {}),
|
|
3009
|
+
!compact && t("Desktop")
|
|
3010
|
+
] }),
|
|
3011
|
+
/* @__PURE__ */ jsxs13(TabsTrigger, { value: "mobile", disabled: !ready, "aria-label": compact ? t("Mobile") : void 0, children: [
|
|
3012
|
+
/* @__PURE__ */ jsx14(Smartphone, {}),
|
|
3013
|
+
!compact && t("Mobile")
|
|
3014
|
+
] })
|
|
3015
|
+
] }) })
|
|
3016
|
+
] });
|
|
3017
|
+
if (compact) {
|
|
3018
|
+
return /* @__PURE__ */ jsxs13("div", { className: "eb:shrink-0 eb:border-b eb:border-border", "data-layout": "compact", children: [
|
|
3019
|
+
/* @__PURE__ */ jsxs13("div", { className: "eb:flex eb:h-12 eb:items-center eb:gap-2 eb:px-2", children: [
|
|
3020
|
+
identity,
|
|
3021
|
+
primary
|
|
3022
|
+
] }),
|
|
3023
|
+
/* @__PURE__ */ jsxs13("div", { className: "eb:flex eb:items-center eb:gap-2 eb:overflow-x-auto eb:px-2 eb:pb-2", "data-testid": "eb-toolbar-actions", children: [
|
|
3024
|
+
editing,
|
|
3025
|
+
/* @__PURE__ */ jsx14(Separator, { orientation: "vertical", className: "eb:h-5 eb:shrink-0" }),
|
|
3026
|
+
/* @__PURE__ */ jsx14("div", { className: "eb:flex eb:shrink-0 eb:items-center eb:gap-2", children: actions })
|
|
3027
|
+
] })
|
|
3028
|
+
] });
|
|
3029
|
+
}
|
|
3030
|
+
return /* @__PURE__ */ jsxs13("div", { className: "eb:flex eb:h-12 eb:shrink-0 eb:items-center eb:gap-3 eb:border-b eb:border-border eb:px-3", children: [
|
|
3031
|
+
identity,
|
|
3032
|
+
editing,
|
|
3033
|
+
/* @__PURE__ */ jsxs13("div", { className: "eb:ml-auto eb:flex eb:items-center eb:gap-2", "data-testid": "eb-toolbar-actions", children: [
|
|
3034
|
+
actions,
|
|
3035
|
+
primary
|
|
3036
|
+
] })
|
|
3037
|
+
] });
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
// src/ui/Drawer.jsx
|
|
3041
|
+
import { useEffect as useEffect5 } from "react";
|
|
3042
|
+
import { X as X2 } from "lucide-react";
|
|
3043
|
+
import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3044
|
+
function Drawer({ open, side = "left", title, onClose, children, testId, header = true }) {
|
|
3045
|
+
const t = useT();
|
|
3046
|
+
useEffect5(() => {
|
|
3047
|
+
if (!open) return void 0;
|
|
3048
|
+
const onKey = (e) => {
|
|
3049
|
+
if (e.key === "Escape") onClose();
|
|
3050
|
+
};
|
|
3051
|
+
window.addEventListener("keydown", onKey);
|
|
3052
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
3053
|
+
}, [open, onClose]);
|
|
3054
|
+
return /* @__PURE__ */ jsxs14("div", { className: cn("eb:absolute eb:inset-0 eb:z-20", !open && "eb:hidden"), "data-testid": testId, "data-open": open ? "true" : "false", children: [
|
|
3055
|
+
/* @__PURE__ */ jsx15("div", { className: "eb:absolute eb:inset-0 eb:bg-black/30", onClick: onClose, "aria-hidden": "true" }),
|
|
3056
|
+
/* @__PURE__ */ jsxs14(
|
|
3057
|
+
"section",
|
|
3058
|
+
{
|
|
3059
|
+
role: "dialog",
|
|
3060
|
+
"aria-label": title,
|
|
3061
|
+
className: cn(
|
|
3062
|
+
"eb:absolute eb:inset-y-0 eb:flex eb:w-[min(340px,88%)] eb:flex-col eb:bg-background eb:shadow-lg",
|
|
3063
|
+
side === "left" ? "eb:left-0 eb:border-r eb:border-border" : "eb:right-0 eb:border-l eb:border-border"
|
|
3064
|
+
),
|
|
3065
|
+
children: [
|
|
3066
|
+
header && /* @__PURE__ */ jsxs14("header", { className: "eb:flex eb:h-10 eb:shrink-0 eb:items-center eb:justify-between eb:border-b eb:border-border eb:px-3", children: [
|
|
3067
|
+
/* @__PURE__ */ jsx15("h3", { className: "eb:text-[13px] eb:font-medium eb:text-foreground", children: title }),
|
|
3068
|
+
/* @__PURE__ */ jsx15(Button, { variant: "ghost", size: "icon-sm-compact", onClick: onClose, "aria-label": t("Close"), children: /* @__PURE__ */ jsx15(X2, {}) })
|
|
3069
|
+
] }),
|
|
3070
|
+
/* @__PURE__ */ jsx15("div", { className: "eb:grid eb:min-h-0 eb:flex-1", children })
|
|
3071
|
+
]
|
|
3072
|
+
}
|
|
3073
|
+
)
|
|
3074
|
+
] });
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
// src/ui/useEmailEditorState.js
|
|
3078
|
+
import { useCallback, useEffect as useEffect6, useRef as useRef5, useState as useState10 } from "react";
|
|
3079
|
+
var DIRTY_CHECK_MS = 150;
|
|
3080
|
+
function useEmailEditorState(editor) {
|
|
3081
|
+
const [state, setState] = useState10(() => ({
|
|
3082
|
+
status: editor?.status || "idle",
|
|
3083
|
+
selection: null,
|
|
3084
|
+
canUndo: false,
|
|
3085
|
+
canRedo: false,
|
|
3086
|
+
device: "desktop",
|
|
3087
|
+
changes: 0,
|
|
3088
|
+
dirty: false
|
|
3089
|
+
}));
|
|
3090
|
+
const clean = useRef5(null);
|
|
3091
|
+
const forced = useRef5(false);
|
|
3092
|
+
const timer = useRef5(null);
|
|
3093
|
+
const snapshot = useCallback(() => {
|
|
3094
|
+
try {
|
|
3095
|
+
return editor.status === "ready" ? editor.exportMjml() : null;
|
|
3096
|
+
} catch {
|
|
3097
|
+
return null;
|
|
3098
|
+
}
|
|
3099
|
+
}, [editor]);
|
|
3100
|
+
const markClean = useCallback((saved = null) => {
|
|
3101
|
+
clean.current = saved ?? snapshot();
|
|
3102
|
+
forced.current = false;
|
|
3103
|
+
const current = snapshot();
|
|
3104
|
+
setState((s) => ({ ...s, dirty: current !== null && current !== clean.current }));
|
|
3105
|
+
}, [snapshot]);
|
|
3106
|
+
const checkDirty = useCallback(() => {
|
|
3107
|
+
if (forced.current) return true;
|
|
3108
|
+
const current = snapshot();
|
|
3109
|
+
return current !== null && current !== clean.current;
|
|
3110
|
+
}, [snapshot]);
|
|
3111
|
+
const markDirty = useCallback(() => {
|
|
3112
|
+
forced.current = true;
|
|
3113
|
+
setState((s) => ({ ...s, dirty: true }));
|
|
3114
|
+
}, []);
|
|
3115
|
+
useEffect6(() => {
|
|
3116
|
+
if (!editor) return void 0;
|
|
3117
|
+
const patch = (values) => setState((s) => ({ ...s, ...values }));
|
|
3118
|
+
const measure = () => {
|
|
3119
|
+
clearTimeout(timer.current);
|
|
3120
|
+
timer.current = setTimeout(() => {
|
|
3121
|
+
const current = snapshot();
|
|
3122
|
+
patch({ dirty: forced.current || current !== null && current !== clean.current });
|
|
3123
|
+
}, DIRTY_CHECK_MS);
|
|
3124
|
+
};
|
|
3125
|
+
const offs = [
|
|
3126
|
+
editor.on("ready", () => {
|
|
3127
|
+
clean.current = snapshot();
|
|
3128
|
+
forced.current = false;
|
|
3129
|
+
patch({
|
|
3130
|
+
status: "ready",
|
|
3131
|
+
device: editor.getDevice(),
|
|
3132
|
+
canUndo: editor.canUndo(),
|
|
3133
|
+
canRedo: editor.canRedo(),
|
|
3134
|
+
selection: editor.getSelection(),
|
|
3135
|
+
dirty: false
|
|
3136
|
+
});
|
|
3137
|
+
}),
|
|
3138
|
+
editor.on("destroy", () => patch({ status: "idle", selection: null, canUndo: false, canRedo: false })),
|
|
3139
|
+
editor.on("selection", (selection) => patch({ selection })),
|
|
3140
|
+
editor.on("history", ({ canUndo, canRedo }) => patch({ canUndo, canRedo })),
|
|
3141
|
+
editor.on("device", (device) => patch({ device })),
|
|
3142
|
+
editor.on("change", () => {
|
|
3143
|
+
setState((s) => ({ ...s, changes: s.changes + 1 }));
|
|
3144
|
+
measure();
|
|
3145
|
+
}),
|
|
3146
|
+
editor.on("load", () => {
|
|
3147
|
+
clearTimeout(timer.current);
|
|
3148
|
+
clean.current = snapshot();
|
|
3149
|
+
forced.current = false;
|
|
3150
|
+
setState((s) => ({ ...s, changes: s.changes + 1, dirty: false }));
|
|
3151
|
+
})
|
|
3152
|
+
];
|
|
3153
|
+
return () => {
|
|
3154
|
+
clearTimeout(timer.current);
|
|
3155
|
+
offs.forEach((off) => off());
|
|
3156
|
+
};
|
|
3157
|
+
}, [editor, snapshot]);
|
|
3158
|
+
return { ...state, markClean, markDirty, checkDirty };
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
// src/ui/EmailBuilder.jsx
|
|
3162
|
+
import { Fragment as Fragment7, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3163
|
+
var EMPTY_META = { name: "", subject: "", preheader: "", description: "" };
|
|
3164
|
+
function metaOf(template) {
|
|
3165
|
+
return {
|
|
3166
|
+
name: template?.name || "",
|
|
3167
|
+
subject: template?.subject || "",
|
|
3168
|
+
preheader: template?.preheader || "",
|
|
3169
|
+
description: template?.description || ""
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
var sameMeta = (a, b) => Object.keys(EMPTY_META).every((k) => (a[k] || "") === (b[k] || ""));
|
|
3173
|
+
function Builder({
|
|
3174
|
+
editor: injected,
|
|
3175
|
+
storage: injectedStorage,
|
|
3176
|
+
templateId: initialTemplateId,
|
|
3177
|
+
variables: variablesProp,
|
|
3178
|
+
sender,
|
|
3179
|
+
starterMjml,
|
|
3180
|
+
templates: galleryProp,
|
|
3181
|
+
testRecipients,
|
|
3182
|
+
initialMjml,
|
|
3183
|
+
initialMeta,
|
|
3184
|
+
onClose,
|
|
3185
|
+
onSaved,
|
|
3186
|
+
onDirtyChange,
|
|
3187
|
+
dirtyCheckRef,
|
|
3188
|
+
className
|
|
3189
|
+
}) {
|
|
3190
|
+
const { t, locale, notify, confirm } = useUi();
|
|
3191
|
+
const editor = useMemo5(() => injected || createDefaultEmailEditor({ locale }), [injected, locale]);
|
|
3192
|
+
const storage = useMemo5(() => injectedStorage || createHttpStorage(), [injectedStorage]);
|
|
3193
|
+
const systemVariables = useMemo5(() => defineVariables(variablesProp || DEFAULT_VARIABLES), [variablesProp]);
|
|
3194
|
+
const [customVariables, setCustomVariables] = useState11([]);
|
|
3195
|
+
const variables = useMemo5(() => mergeVariables(systemVariables, customVariables), [systemVariables, customVariables]);
|
|
3196
|
+
const canManageVariables = typeof storage.saveVariable === "function" && typeof storage.listVariables === "function";
|
|
3197
|
+
const reloadVariables = useCallback2(async () => {
|
|
3198
|
+
if (typeof storage.listVariables !== "function") return;
|
|
3199
|
+
try {
|
|
3200
|
+
setCustomVariables(await storage.listVariables());
|
|
3201
|
+
} catch (error) {
|
|
3202
|
+
notify("error", error.message);
|
|
3203
|
+
}
|
|
3204
|
+
}, [storage, notify]);
|
|
3205
|
+
useEffect7(() => {
|
|
3206
|
+
reloadVariables();
|
|
3207
|
+
}, [reloadVariables]);
|
|
3208
|
+
const starter = useMemo5(() => starterMjml || createStarterMjml({ sender, t }), [starterMjml, sender, t]);
|
|
3209
|
+
const templates = useMemo5(
|
|
3210
|
+
() => buildGallery({ templates: galleryProp ?? TEMPLATE_GALLERY, sender: sender || {}, t }),
|
|
3211
|
+
[galleryProp, sender, t]
|
|
3212
|
+
);
|
|
3213
|
+
const services = useMemo5(() => ({ uploadImage: (file) => storage.uploadImage(file) }), [storage]);
|
|
3214
|
+
const state = useEmailEditorState(editor);
|
|
3215
|
+
const autosaveOn = isFeatureEnabled("autosave");
|
|
3216
|
+
const autosaveOptions = emailBuilderFeatures.get("autosave")?.options || {};
|
|
3217
|
+
const [template, setTemplate] = useState11({ id: initialTemplateId, revision: null, savedAt: null, auto: false });
|
|
3218
|
+
const [boot, setBoot] = useState11({ status: "loading", document: null, draft: false, message: null });
|
|
3219
|
+
const [meta, setMeta] = useState11(EMPTY_META);
|
|
3220
|
+
const [savedMeta, setSavedMeta] = useState11(EMPTY_META);
|
|
3221
|
+
const [saving, setSaving] = useState11(null);
|
|
3222
|
+
const [showErrors, setShowErrors] = useState11(false);
|
|
3223
|
+
const [autosaveBlocked, setAutosaveBlocked] = useState11(null);
|
|
3224
|
+
const [engineError, setEngineError] = useState11(null);
|
|
3225
|
+
const [attempt, setAttempt] = useState11(0);
|
|
3226
|
+
const rootRef = useRef6(null);
|
|
3227
|
+
const layout2 = useLayoutWidth(rootRef);
|
|
3228
|
+
const compact = layout2 !== "wide";
|
|
3229
|
+
const blocksInDrawer = layout2 === "narrow";
|
|
3230
|
+
const [drawer, setDrawer] = useState11(null);
|
|
3231
|
+
const closeDrawer = useCallback2(() => setDrawer(null), []);
|
|
3232
|
+
useEffect7(() => {
|
|
3233
|
+
if (!compact) setDrawer(null);
|
|
3234
|
+
else if (!blocksInDrawer) setDrawer((d) => d === "blocks" ? null : d);
|
|
3235
|
+
}, [compact, blocksInDrawer]);
|
|
3236
|
+
const ready = state.status === "ready";
|
|
3237
|
+
const metaDirty = !sameMeta(meta, savedMeta);
|
|
3238
|
+
const dirty = state.dirty || metaDirty;
|
|
3239
|
+
const timeOf = useCallback2(
|
|
3240
|
+
(date) => date ? new Date(date).toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" }) : "",
|
|
3241
|
+
[locale]
|
|
3242
|
+
);
|
|
3243
|
+
const booted = useRef6(false);
|
|
3244
|
+
useEffect7(() => {
|
|
3245
|
+
if (booted.current) return;
|
|
3246
|
+
booted.current = true;
|
|
3247
|
+
(async () => {
|
|
3248
|
+
if (initialTemplateId) {
|
|
3249
|
+
try {
|
|
3250
|
+
const loaded = await storage.load(initialTemplateId);
|
|
3251
|
+
setMeta(metaOf(loaded));
|
|
3252
|
+
setSavedMeta(metaOf(loaded));
|
|
3253
|
+
setTemplate({ id: loaded.id, revision: loaded.revision, savedAt: null, auto: false });
|
|
3254
|
+
setBoot({ status: "ready", document: loaded.document, draft: false, message: null });
|
|
3255
|
+
} catch (error) {
|
|
3256
|
+
setBoot({ status: "error", document: null, draft: false, message: error.message });
|
|
3257
|
+
}
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
if (initialMjml) {
|
|
3261
|
+
setMeta({ ...EMPTY_META, ...initialMeta });
|
|
3262
|
+
setBoot({ status: "ready", document: null, draft: true, message: null });
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
const draft = autosaveOn ? readLocalDraft() : null;
|
|
3266
|
+
if (draft) {
|
|
3267
|
+
const when = new Date(draft.savedAt).toLocaleString(locale, {
|
|
3268
|
+
day: "2-digit",
|
|
3269
|
+
month: "2-digit",
|
|
3270
|
+
hour: "2-digit",
|
|
3271
|
+
minute: "2-digit"
|
|
3272
|
+
});
|
|
3273
|
+
const restore = await confirm({
|
|
3274
|
+
title: t("Resume the draft?"),
|
|
3275
|
+
description: t("Unsaved template from {when}.", { when }),
|
|
3276
|
+
confirmLabel: t("Resume"),
|
|
3277
|
+
cancelLabel: t("Start over")
|
|
3278
|
+
});
|
|
3279
|
+
if (restore) {
|
|
3280
|
+
setMeta({ ...EMPTY_META, ...draft.meta });
|
|
3281
|
+
setBoot({ status: "ready", document: draft.document, draft: true, message: null });
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
clearLocalDraft();
|
|
3285
|
+
}
|
|
3286
|
+
setBoot({ status: "ready", document: null, draft: false, message: null });
|
|
3287
|
+
})();
|
|
3288
|
+
}, [initialTemplateId, storage, autosaveOn, confirm, t, locale]);
|
|
3289
|
+
const onReady = useCallback2(() => {
|
|
3290
|
+
if (!boot.document) editor.loadMjml(initialMjml || starter);
|
|
3291
|
+
}, [editor, boot.document, starter, initialMjml]);
|
|
3292
|
+
const { markDirty, markClean, checkDirty } = state;
|
|
3293
|
+
useEffect7(() => {
|
|
3294
|
+
if (ready && boot.draft) markDirty();
|
|
3295
|
+
}, [ready, boot.draft, markDirty]);
|
|
3296
|
+
const isDirty = useCallback2(
|
|
3297
|
+
() => editor.status === "ready" && checkDirty() || metaDirty,
|
|
3298
|
+
[editor, checkDirty, metaDirty]
|
|
3299
|
+
);
|
|
3300
|
+
const reportDirty = useRef6(onDirtyChange);
|
|
3301
|
+
reportDirty.current = onDirtyChange;
|
|
3302
|
+
useEffect7(() => {
|
|
3303
|
+
reportDirty.current?.(dirty);
|
|
3304
|
+
}, [dirty]);
|
|
3305
|
+
useEffect7(() => {
|
|
3306
|
+
if (!dirtyCheckRef) return void 0;
|
|
3307
|
+
dirtyCheckRef.current = isDirty;
|
|
3308
|
+
return () => {
|
|
3309
|
+
dirtyCheckRef.current = null;
|
|
3310
|
+
};
|
|
3311
|
+
}, [dirtyCheckRef, isDirty]);
|
|
3312
|
+
useEffect7(() => {
|
|
3313
|
+
if (!dirty) return void 0;
|
|
3314
|
+
const warn = (e) => {
|
|
3315
|
+
e.preventDefault();
|
|
3316
|
+
e.returnValue = "";
|
|
3317
|
+
};
|
|
3318
|
+
window.addEventListener("beforeunload", warn);
|
|
3319
|
+
return () => window.removeEventListener("beforeunload", warn);
|
|
3320
|
+
}, [dirty]);
|
|
3321
|
+
const savingRef = useRef6(false);
|
|
3322
|
+
const latest = useRef6({});
|
|
3323
|
+
latest.current = { template, meta, onSaved, compact };
|
|
3324
|
+
const save = useCallback2(
|
|
3325
|
+
async ({ auto = false } = {}) => {
|
|
3326
|
+
if (savingRef.current || editor.status !== "ready") return false;
|
|
3327
|
+
const { template: current, meta: currentMeta } = latest.current;
|
|
3328
|
+
if (!currentMeta.name.trim()) {
|
|
3329
|
+
if (!auto) {
|
|
3330
|
+
setShowErrors(true);
|
|
3331
|
+
editor.select(null);
|
|
3332
|
+
setDrawer((d) => d === null && latest.current.compact ? "properties" : d);
|
|
3333
|
+
notify("error", t("Give the template a name before saving it."));
|
|
3334
|
+
}
|
|
3335
|
+
return false;
|
|
3336
|
+
}
|
|
3337
|
+
const document = editor.getDocument();
|
|
3338
|
+
const mjml = editor.exportMjml();
|
|
3339
|
+
const payload = { ...currentMeta, document, mjml };
|
|
3340
|
+
savingRef.current = true;
|
|
3341
|
+
setSaving(auto ? "auto" : "manual");
|
|
3342
|
+
try {
|
|
3343
|
+
const result = current.id ? await storage.save(current.id, { ...payload, revision: current.revision, createVersion: !auto }) : await storage.create(payload);
|
|
3344
|
+
setTemplate({ id: result.template.id, revision: result.template.revision, savedAt: /* @__PURE__ */ new Date(), auto });
|
|
3345
|
+
setSavedMeta(currentMeta);
|
|
3346
|
+
setShowErrors(false);
|
|
3347
|
+
setAutosaveBlocked(null);
|
|
3348
|
+
markClean(mjml);
|
|
3349
|
+
if (!current.id) clearLocalDraft();
|
|
3350
|
+
latest.current.onSaved?.(result.template);
|
|
3351
|
+
const warnings = result.warnings || [];
|
|
3352
|
+
if (warnings.length) {
|
|
3353
|
+
notify(
|
|
3354
|
+
"warning",
|
|
3355
|
+
t("Saved, with {count} MJML warning(s): {message}", { count: warnings.length, message: warnings[0].message })
|
|
3356
|
+
);
|
|
3357
|
+
} else if (!auto) {
|
|
3358
|
+
notify(
|
|
3359
|
+
"success",
|
|
3360
|
+
result.version ? t("Saved \u2014 version {n}.", { n: result.version.versionNumber }) : t("Template saved.")
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
return true;
|
|
3364
|
+
} catch (error) {
|
|
3365
|
+
if (error.status === 409 && current.id) {
|
|
3366
|
+
if (auto) {
|
|
3367
|
+
setAutosaveBlocked(error.message);
|
|
3368
|
+
return false;
|
|
3369
|
+
}
|
|
3370
|
+
const copy = await confirm({
|
|
3371
|
+
title: t("Template changed in the meantime"),
|
|
3372
|
+
description: t("{message} Your version can be saved as a copy.", { message: error.message }),
|
|
3373
|
+
confirmLabel: t("Save a copy")
|
|
3374
|
+
});
|
|
3375
|
+
if (!copy) return false;
|
|
3376
|
+
try {
|
|
3377
|
+
const result = await storage.create({
|
|
3378
|
+
...payload,
|
|
3379
|
+
name: t("{name} (copy)", { name: currentMeta.name }).slice(0, 200)
|
|
3380
|
+
});
|
|
3381
|
+
const copyMeta = { ...currentMeta, name: result.template.name };
|
|
3382
|
+
setMeta(copyMeta);
|
|
3383
|
+
setSavedMeta(copyMeta);
|
|
3384
|
+
setTemplate({ id: result.template.id, revision: result.template.revision, savedAt: /* @__PURE__ */ new Date(), auto: false });
|
|
3385
|
+
setAutosaveBlocked(null);
|
|
3386
|
+
markClean(mjml);
|
|
3387
|
+
latest.current.onSaved?.(result.template);
|
|
3388
|
+
notify("success", t("Copy saved: \u201C{name}\u201D.", { name: result.template.name }));
|
|
3389
|
+
return true;
|
|
3390
|
+
} catch (copyError) {
|
|
3391
|
+
notify("error", copyError.message);
|
|
3392
|
+
return false;
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
if (auto) setAutosaveBlocked(error.message);
|
|
3396
|
+
else notify("error", error.message);
|
|
3397
|
+
return false;
|
|
3398
|
+
} finally {
|
|
3399
|
+
savingRef.current = false;
|
|
3400
|
+
setSaving(null);
|
|
3401
|
+
}
|
|
3402
|
+
},
|
|
3403
|
+
[editor, storage, markClean, notify, confirm, t]
|
|
3404
|
+
);
|
|
3405
|
+
useEffect7(() => {
|
|
3406
|
+
const onKey = (e) => {
|
|
3407
|
+
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === "s") {
|
|
3408
|
+
e.preventDefault();
|
|
3409
|
+
save();
|
|
3410
|
+
}
|
|
3411
|
+
};
|
|
3412
|
+
window.addEventListener("keydown", onKey, true);
|
|
3413
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
3414
|
+
}, [save]);
|
|
3415
|
+
useEffect7(() => {
|
|
3416
|
+
if (!ready) return void 0;
|
|
3417
|
+
return editor.addShortcut("save", "mod+s", () => save());
|
|
3418
|
+
}, [ready, editor, save]);
|
|
3419
|
+
const scheduler = useMemo5(
|
|
3420
|
+
() => createAutosaveScheduler({
|
|
3421
|
+
run: () => save({ auto: true }),
|
|
3422
|
+
idleMs: autosaveOptions.idleMs,
|
|
3423
|
+
maxWaitMs: autosaveOptions.maxWaitMs
|
|
3424
|
+
}),
|
|
3425
|
+
[save, autosaveOptions.idleMs, autosaveOptions.maxWaitMs]
|
|
3426
|
+
);
|
|
3427
|
+
useEffect7(() => () => scheduler.dispose(), [scheduler]);
|
|
3428
|
+
useEffect7(() => {
|
|
3429
|
+
if (!autosaveOn || !ready || !template.id || autosaveBlocked) return;
|
|
3430
|
+
if (dirty) scheduler.notify();
|
|
3431
|
+
else scheduler.reset();
|
|
3432
|
+
}, [autosaveOn, ready, template.id, autosaveBlocked, dirty, state.changes, meta, scheduler]);
|
|
3433
|
+
useEffect7(() => {
|
|
3434
|
+
if (!autosaveOn || !ready || template.id || !dirty) return void 0;
|
|
3435
|
+
const timer = setTimeout(() => {
|
|
3436
|
+
if (editor.status === "ready") saveLocalDraft({ document: editor.getDocument(), meta });
|
|
3437
|
+
}, autosaveOptions.draftDelayMs ?? 1500);
|
|
3438
|
+
return () => clearTimeout(timer);
|
|
3439
|
+
}, [autosaveOn, ready, template.id, dirty, state.changes, meta, editor, autosaveOptions.draftDelayMs]);
|
|
3440
|
+
const back = async () => {
|
|
3441
|
+
if (isDirty()) {
|
|
3442
|
+
const ok = await confirm({
|
|
3443
|
+
title: t("Leave the builder?"),
|
|
3444
|
+
description: template.id ? t("Unsaved changes will be lost.") : t("The draft will be offered again next time."),
|
|
3445
|
+
confirmLabel: t("Leave"),
|
|
3446
|
+
destructive: true
|
|
3447
|
+
});
|
|
3448
|
+
if (!ok) return;
|
|
3449
|
+
}
|
|
3450
|
+
onClose?.();
|
|
3451
|
+
};
|
|
3452
|
+
const status = !ready ? t("Loading\u2026") : saving === "manual" ? t("Saving\u2026") : saving === "auto" ? t("Autosaving\u2026") : autosaveBlocked ? t("Autosave paused") : dirty ? t("Unsaved changes") : template.savedAt ? template.auto ? t("Autosaved at {time}", { time: timeOf(template.savedAt) }) : t("Saved at {time}", { time: timeOf(template.savedAt) }) : template.id ? t("Saved") : t("No changes");
|
|
3453
|
+
const featureContext = {
|
|
3454
|
+
editor,
|
|
3455
|
+
storage,
|
|
3456
|
+
template,
|
|
3457
|
+
meta,
|
|
3458
|
+
setMeta,
|
|
3459
|
+
templates,
|
|
3460
|
+
ready,
|
|
3461
|
+
isDirty,
|
|
3462
|
+
markDirty,
|
|
3463
|
+
save,
|
|
3464
|
+
variables,
|
|
3465
|
+
systemVariables,
|
|
3466
|
+
canManageVariables,
|
|
3467
|
+
reloadVariables,
|
|
3468
|
+
testRecipients
|
|
3469
|
+
};
|
|
3470
|
+
const toolbarFeatures = emailBuilderFeatures.list(
|
|
3471
|
+
(f) => f.enabled !== false && Boolean(f.toolbar) && (f.isAvailable ? f.isAvailable(featureContext) : true)
|
|
3472
|
+
);
|
|
3473
|
+
const properties = /* @__PURE__ */ jsx16(
|
|
3474
|
+
PropertiesPanel,
|
|
3475
|
+
{
|
|
3476
|
+
onDismiss: compact ? closeDrawer : null,
|
|
3477
|
+
editor,
|
|
3478
|
+
selection: ready ? state.selection : null,
|
|
3479
|
+
meta,
|
|
3480
|
+
onMetaChange: setMeta,
|
|
3481
|
+
changes: state.changes,
|
|
3482
|
+
showErrors,
|
|
3483
|
+
actions: /* @__PURE__ */ jsx16(ElementActions, { editor, selection: state.selection }),
|
|
3484
|
+
services,
|
|
3485
|
+
variables
|
|
3486
|
+
}
|
|
3487
|
+
);
|
|
3488
|
+
return /* @__PURE__ */ jsxs15(
|
|
3489
|
+
"div",
|
|
3490
|
+
{
|
|
3491
|
+
ref: rootRef,
|
|
3492
|
+
className: cn("eb-root eb:relative eb:flex eb:h-full eb:min-h-0 eb:flex-col eb:bg-background eb:text-foreground", className),
|
|
3493
|
+
"data-testid": "email-builder",
|
|
3494
|
+
"data-status": state.status,
|
|
3495
|
+
"data-layout": layout2,
|
|
3496
|
+
"data-template-id": template.id || "",
|
|
3497
|
+
children: [
|
|
3498
|
+
/* @__PURE__ */ jsx16(
|
|
3499
|
+
Toolbar,
|
|
3500
|
+
{
|
|
3501
|
+
title: meta.name,
|
|
3502
|
+
status,
|
|
3503
|
+
ready,
|
|
3504
|
+
canUndo: state.canUndo,
|
|
3505
|
+
canRedo: state.canRedo,
|
|
3506
|
+
onUndo: () => editor.undo(),
|
|
3507
|
+
onRedo: () => editor.redo(),
|
|
3508
|
+
device: state.device,
|
|
3509
|
+
onDeviceChange: (device) => editor.setDevice(device),
|
|
3510
|
+
onBack: onClose ? back : void 0,
|
|
3511
|
+
compact,
|
|
3512
|
+
actions: toolbarFeatures.map((feature) => {
|
|
3513
|
+
const Action = feature.toolbar;
|
|
3514
|
+
return /* @__PURE__ */ jsx16(Action, { context: featureContext }, feature.id);
|
|
3515
|
+
}),
|
|
3516
|
+
primary: /* @__PURE__ */ jsxs15(Button, { size: "sm-compact", onClick: () => save(), disabled: !ready || saving !== null, children: [
|
|
3517
|
+
saving === "manual" ? /* @__PURE__ */ jsx16(Loader24, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx16(Save, {}),
|
|
3518
|
+
t("Save")
|
|
3519
|
+
] })
|
|
3520
|
+
}
|
|
3521
|
+
),
|
|
3522
|
+
/* @__PURE__ */ jsxs15(
|
|
3523
|
+
"div",
|
|
3524
|
+
{
|
|
3525
|
+
className: cn(
|
|
3526
|
+
"eb:relative eb:grid eb:min-h-0 eb:flex-1 eb:grid-rows-1",
|
|
3527
|
+
layout2 === "wide" && "eb:grid-cols-[224px_minmax(0,1fr)_296px]",
|
|
3528
|
+
layout2 === "medium" && "eb:grid-cols-[200px_minmax(0,1fr)]",
|
|
3529
|
+
layout2 === "narrow" && "eb:grid-cols-1"
|
|
3530
|
+
),
|
|
3531
|
+
children: [
|
|
3532
|
+
!blocksInDrawer && /* @__PURE__ */ jsx16(BlockSidebar, { editor, disabled: !ready, onAdded: compact ? () => setDrawer("properties") : void 0 }),
|
|
3533
|
+
/* @__PURE__ */ jsx16("div", { className: "eb:relative eb:min-w-0 eb:bg-surface-75", "aria-busy": !ready, children: boot.status === "error" || engineError ? /* @__PURE__ */ jsxs15("div", { role: "alert", className: "eb:flex eb:h-full eb:flex-col eb:items-center eb:justify-center eb:gap-2 eb:p-6 eb:text-center", children: [
|
|
3534
|
+
/* @__PURE__ */ jsx16(AlertTriangle, { className: "eb:size-6 eb:text-destructive", "aria-hidden": "true" }),
|
|
3535
|
+
/* @__PURE__ */ jsx16("p", { className: "eb:text-[13px] eb:text-foreground", children: boot.status === "error" ? t("The template could not be loaded.") : t("The builder could not start.") }),
|
|
3536
|
+
/* @__PURE__ */ jsx16("p", { className: "eb:max-w-sm eb:text-[12px] eb:text-foreground-muted", children: boot.message || engineError }),
|
|
3537
|
+
engineError && /* @__PURE__ */ jsx16(
|
|
3538
|
+
Button,
|
|
3539
|
+
{
|
|
3540
|
+
variant: "outline",
|
|
3541
|
+
size: "sm-compact",
|
|
3542
|
+
onClick: () => {
|
|
3543
|
+
setEngineError(null);
|
|
3544
|
+
setAttempt((n) => n + 1);
|
|
3545
|
+
},
|
|
3546
|
+
children: t("Retry")
|
|
3547
|
+
}
|
|
3548
|
+
)
|
|
3549
|
+
] }) : /* @__PURE__ */ jsxs15(Fragment7, { children: [
|
|
3550
|
+
boot.status === "ready" && /* @__PURE__ */ jsx16(
|
|
3551
|
+
EmailCanvas,
|
|
3552
|
+
{
|
|
3553
|
+
editor,
|
|
3554
|
+
document: boot.document,
|
|
3555
|
+
onReady,
|
|
3556
|
+
onError: (error) => setEngineError(error.message)
|
|
3557
|
+
},
|
|
3558
|
+
attempt
|
|
3559
|
+
),
|
|
3560
|
+
!ready && /* @__PURE__ */ jsxs15("div", { className: "eb:absolute eb:inset-0 eb:flex eb:items-center eb:justify-center eb:bg-surface-75", role: "status", "aria-live": "polite", children: [
|
|
3561
|
+
/* @__PURE__ */ jsx16(Loader24, { className: "eb:size-6 eb:animate-spin eb:text-foreground-muted", "aria-hidden": "true" }),
|
|
3562
|
+
/* @__PURE__ */ jsx16("span", { className: "eb:sr-only", children: t("Loading the builder") })
|
|
3563
|
+
] })
|
|
3564
|
+
] }) }),
|
|
3565
|
+
!compact && properties,
|
|
3566
|
+
blocksInDrawer && /* @__PURE__ */ jsx16(Drawer, { open: drawer === "blocks", side: "left", title: t("Add a block"), onClose: closeDrawer, testId: "eb-blocks-drawer", children: /* @__PURE__ */ jsx16(BlockSidebar, { editor, disabled: !ready, onAdded: () => setDrawer("properties") }) }),
|
|
3567
|
+
compact && /* @__PURE__ */ jsx16(
|
|
3568
|
+
Drawer,
|
|
3569
|
+
{
|
|
3570
|
+
open: drawer === "properties",
|
|
3571
|
+
side: "right",
|
|
3572
|
+
title: t("Properties"),
|
|
3573
|
+
onClose: closeDrawer,
|
|
3574
|
+
testId: "eb-properties-drawer",
|
|
3575
|
+
header: false,
|
|
3576
|
+
children: properties
|
|
3577
|
+
}
|
|
3578
|
+
)
|
|
3579
|
+
]
|
|
3580
|
+
}
|
|
3581
|
+
),
|
|
3582
|
+
compact && /* @__PURE__ */ jsxs15("div", { className: "eb:flex eb:shrink-0 eb:items-center eb:gap-2 eb:border-t eb:border-border eb:px-2 eb:py-2", "data-testid": "eb-compact-bar", children: [
|
|
3583
|
+
blocksInDrawer && /* @__PURE__ */ jsxs15(Button, { variant: "outline", size: "sm-compact", disabled: !ready, onClick: () => setDrawer("blocks"), children: [
|
|
3584
|
+
/* @__PURE__ */ jsx16(Plus2, {}),
|
|
3585
|
+
t("Add a block")
|
|
3586
|
+
] }),
|
|
3587
|
+
/* @__PURE__ */ jsxs15(
|
|
3588
|
+
Button,
|
|
3589
|
+
{
|
|
3590
|
+
variant: state.selection ? "default" : "outline",
|
|
3591
|
+
size: "sm-compact",
|
|
3592
|
+
className: "eb:ml-auto eb:min-w-0",
|
|
3593
|
+
disabled: !ready,
|
|
3594
|
+
onClick: () => setDrawer("properties"),
|
|
3595
|
+
children: [
|
|
3596
|
+
/* @__PURE__ */ jsx16(SlidersHorizontal, {}),
|
|
3597
|
+
/* @__PURE__ */ jsx16("span", { className: "eb:truncate", children: ready && state.selection ? t("Edit: {element}", { element: t(state.selection.label) }) : t("Email settings") })
|
|
3598
|
+
]
|
|
3599
|
+
}
|
|
3600
|
+
)
|
|
3601
|
+
] }),
|
|
3602
|
+
/* @__PURE__ */ jsx16(NoticeArea, {})
|
|
3603
|
+
]
|
|
3604
|
+
}
|
|
3605
|
+
);
|
|
3606
|
+
}
|
|
3607
|
+
function EmailBuilder({ locale = "en", messages, onNotify, ...props }) {
|
|
3608
|
+
return /* @__PURE__ */ jsx16(UiProvider, { locale, messages, onNotify, children: /* @__PURE__ */ jsx16(Builder, { ...props, templateId: props.templateId ?? null }) });
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
// src/ui/EmailBuilderBoundary.jsx
|
|
3612
|
+
import { Component } from "react";
|
|
3613
|
+
import { AlertTriangle as AlertTriangle2, ArrowLeft as ArrowLeft3, RefreshCw } from "lucide-react";
|
|
3614
|
+
import { jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
3615
|
+
var isChunkError = (error) => error?.name === "ChunkLoadError" || /Loading (CSS )?chunk .* failed/i.test(String(error?.message || ""));
|
|
3616
|
+
var english = (text) => text;
|
|
3617
|
+
var EmailBuilderBoundary = class extends Component {
|
|
3618
|
+
constructor(props) {
|
|
3619
|
+
super(props);
|
|
3620
|
+
this.state = { error: null, attempt: 0 };
|
|
3621
|
+
}
|
|
3622
|
+
static getDerivedStateFromError(error) {
|
|
3623
|
+
return { error };
|
|
3624
|
+
}
|
|
3625
|
+
componentDidCatch(error) {
|
|
3626
|
+
console.warn("[best-email-builder] the builder could not load", error);
|
|
3627
|
+
}
|
|
3628
|
+
retry = () => {
|
|
3629
|
+
if (isChunkError(this.state.error)) {
|
|
3630
|
+
window.location.reload();
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
this.setState((s) => ({ error: null, attempt: s.attempt + 1 }));
|
|
3634
|
+
};
|
|
3635
|
+
render() {
|
|
3636
|
+
const { error, attempt } = this.state;
|
|
3637
|
+
const t = this.props.t || english;
|
|
3638
|
+
if (!error) {
|
|
3639
|
+
return /* @__PURE__ */ jsx17("div", { style: { height: "100%" }, children: this.props.children }, attempt);
|
|
3640
|
+
}
|
|
3641
|
+
return /* @__PURE__ */ jsxs16(
|
|
3642
|
+
"div",
|
|
3643
|
+
{
|
|
3644
|
+
role: "alert",
|
|
3645
|
+
className: "eb-root eb:flex eb:h-full eb:flex-col eb:items-center eb:justify-center eb:gap-3 eb:p-6 eb:text-center",
|
|
3646
|
+
children: [
|
|
3647
|
+
/* @__PURE__ */ jsx17(AlertTriangle2, { className: "eb:size-6 eb:text-destructive", "aria-hidden": "true" }),
|
|
3648
|
+
/* @__PURE__ */ jsx17("p", { className: "eb:text-[13px] eb:text-foreground", children: t("The builder could not load.") }),
|
|
3649
|
+
/* @__PURE__ */ jsxs16("div", { className: "eb:flex eb:gap-2", children: [
|
|
3650
|
+
this.props.onClose && /* @__PURE__ */ jsxs16(Button, { variant: "outline", size: "sm", onClick: this.props.onClose, children: [
|
|
3651
|
+
/* @__PURE__ */ jsx17(ArrowLeft3, {}),
|
|
3652
|
+
t("Back")
|
|
3653
|
+
] }),
|
|
3654
|
+
/* @__PURE__ */ jsxs16(Button, { size: "sm", onClick: this.retry, children: [
|
|
3655
|
+
/* @__PURE__ */ jsx17(RefreshCw, {}),
|
|
3656
|
+
t("Retry")
|
|
3657
|
+
] })
|
|
3658
|
+
] })
|
|
3659
|
+
]
|
|
3660
|
+
}
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3663
|
+
};
|
|
3664
|
+
|
|
3665
|
+
export {
|
|
3666
|
+
createEmailEditor,
|
|
3667
|
+
createRegistry,
|
|
3668
|
+
emailBlocks,
|
|
3669
|
+
emailBuilderFeatures,
|
|
3670
|
+
isFeatureEnabled,
|
|
3671
|
+
createGrapesJsAdapter,
|
|
3672
|
+
createDefaultEmailEditor,
|
|
3673
|
+
EmailCanvas,
|
|
3674
|
+
EmailBuilder,
|
|
3675
|
+
EmailBuilderBoundary
|
|
3676
|
+
};
|
|
3677
|
+
//# sourceMappingURL=chunk-ILRAQTHS.js.map
|