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