@emabuild/core 0.0.1

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.
Files changed (67) hide show
  1. package/dist/canvas/column-renderer.d.ts +15 -0
  2. package/dist/canvas/column-renderer.d.ts.map +1 -0
  3. package/dist/canvas/content-renderer.d.ts +23 -0
  4. package/dist/canvas/content-renderer.d.ts.map +1 -0
  5. package/dist/canvas/editor-canvas.d.ts +14 -0
  6. package/dist/canvas/editor-canvas.d.ts.map +1 -0
  7. package/dist/canvas/row-renderer.d.ts +14 -0
  8. package/dist/canvas/row-renderer.d.ts.map +1 -0
  9. package/dist/dnd/drag-manager.d.ts +30 -0
  10. package/dist/dnd/drag-manager.d.ts.map +1 -0
  11. package/dist/dnd/drag-state.d.ts +8 -0
  12. package/dist/dnd/drag-state.d.ts.map +1 -0
  13. package/dist/index.d.ts +6 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +7 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/mail-editor-DzI7SmSe.js +2286 -0
  18. package/dist/mail-editor-DzI7SmSe.js.map +1 -0
  19. package/dist/mail-editor.d.ts +36 -0
  20. package/dist/mail-editor.d.ts.map +1 -0
  21. package/dist/mail-editor.js +7 -0
  22. package/dist/mail-editor.js.map +1 -0
  23. package/dist/properties/property-panel.d.ts +19 -0
  24. package/dist/properties/property-panel.d.ts.map +1 -0
  25. package/dist/sidebar/editor-sidebar.d.ts +19 -0
  26. package/dist/sidebar/editor-sidebar.d.ts.map +1 -0
  27. package/dist/state/editor-store.d.ts +60 -0
  28. package/dist/state/editor-store.d.ts.map +1 -0
  29. package/dist/tools/built-in/button-tool.d.ts +3 -0
  30. package/dist/tools/built-in/button-tool.d.ts.map +1 -0
  31. package/dist/tools/built-in/divider-tool.d.ts +3 -0
  32. package/dist/tools/built-in/divider-tool.d.ts.map +1 -0
  33. package/dist/tools/built-in/form-tool.d.ts +3 -0
  34. package/dist/tools/built-in/form-tool.d.ts.map +1 -0
  35. package/dist/tools/built-in/heading-tool.d.ts +3 -0
  36. package/dist/tools/built-in/heading-tool.d.ts.map +1 -0
  37. package/dist/tools/built-in/html-tool.d.ts +3 -0
  38. package/dist/tools/built-in/html-tool.d.ts.map +1 -0
  39. package/dist/tools/built-in/image-tool.d.ts +3 -0
  40. package/dist/tools/built-in/image-tool.d.ts.map +1 -0
  41. package/dist/tools/built-in/index.d.ts +14 -0
  42. package/dist/tools/built-in/index.d.ts.map +1 -0
  43. package/dist/tools/built-in/menu-tool.d.ts +3 -0
  44. package/dist/tools/built-in/menu-tool.d.ts.map +1 -0
  45. package/dist/tools/built-in/paragraph-tool.d.ts +3 -0
  46. package/dist/tools/built-in/paragraph-tool.d.ts.map +1 -0
  47. package/dist/tools/built-in/social-tool.d.ts +3 -0
  48. package/dist/tools/built-in/social-tool.d.ts.map +1 -0
  49. package/dist/tools/built-in/table-tool.d.ts +3 -0
  50. package/dist/tools/built-in/table-tool.d.ts.map +1 -0
  51. package/dist/tools/built-in/text-tool.d.ts +3 -0
  52. package/dist/tools/built-in/text-tool.d.ts.map +1 -0
  53. package/dist/tools/built-in/timer-tool.d.ts +3 -0
  54. package/dist/tools/built-in/timer-tool.d.ts.map +1 -0
  55. package/dist/tools/built-in/video-tool.d.ts +3 -0
  56. package/dist/tools/built-in/video-tool.d.ts.map +1 -0
  57. package/dist/tools/tool-registry.d.ts +18 -0
  58. package/dist/tools/tool-registry.d.ts.map +1 -0
  59. package/dist/utils/deep-clone.d.ts +2 -0
  60. package/dist/utils/deep-clone.d.ts.map +1 -0
  61. package/dist/utils/event-emitter.d.ts +10 -0
  62. package/dist/utils/event-emitter.d.ts.map +1 -0
  63. package/dist/utils/id-generator.d.ts +7 -0
  64. package/dist/utils/id-generator.d.ts.map +1 -0
  65. package/dist/utils/store-subscriber.d.ts +9 -0
  66. package/dist/utils/store-subscriber.d.ts.map +1 -0
  67. package/package.json +46 -0
@@ -0,0 +1,2286 @@
1
+ import { html, css, LitElement } from "lit";
2
+ import { property, customElement } from "lit/decorators.js";
3
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
4
+ function deepClone(obj) {
5
+ return structuredClone(obj);
6
+ }
7
+ function createCounterManager() {
8
+ const counters = {};
9
+ return {
10
+ getCounters() {
11
+ return { ...counters };
12
+ },
13
+ setCounters(c) {
14
+ Object.keys(counters).forEach((k) => delete counters[k]);
15
+ Object.assign(counters, c);
16
+ },
17
+ next(prefix) {
18
+ const current = counters[prefix] ?? 0;
19
+ counters[prefix] = current + 1;
20
+ return counters[prefix];
21
+ }
22
+ };
23
+ }
24
+ class EventEmitter {
25
+ constructor() {
26
+ this.listeners = /* @__PURE__ */ new Map();
27
+ }
28
+ on(event, listener) {
29
+ if (!this.listeners.has(event)) {
30
+ this.listeners.set(event, /* @__PURE__ */ new Set());
31
+ }
32
+ this.listeners.get(event).add(listener);
33
+ }
34
+ off(event, listener) {
35
+ this.listeners.get(event)?.delete(listener);
36
+ }
37
+ emit(event, ...args) {
38
+ this.listeners.get(event)?.forEach((fn) => {
39
+ try {
40
+ fn(...args);
41
+ } catch (e) {
42
+ console.error(`[maileditor] Error in "${event}" listener:`, e);
43
+ }
44
+ });
45
+ }
46
+ removeAllListeners(event) {
47
+ if (event) {
48
+ this.listeners.delete(event);
49
+ } else {
50
+ this.listeners.clear();
51
+ }
52
+ }
53
+ }
54
+ function createEmptyDesign() {
55
+ return {
56
+ counters: { u_row: 0, u_column: 0 },
57
+ body: {
58
+ id: "u_body",
59
+ rows: [],
60
+ headers: [],
61
+ footers: [],
62
+ values: {
63
+ backgroundColor: "#e7e7e7",
64
+ contentAlign: "center",
65
+ contentVerticalAlign: "center",
66
+ contentWidth: "600px",
67
+ fontFamily: { label: "Arial", value: "arial,helvetica,sans-serif" },
68
+ textColor: "#000000",
69
+ linkStyle: {
70
+ body: true,
71
+ linkColor: "#0000ee",
72
+ linkHoverColor: "#0000ee",
73
+ linkUnderline: true,
74
+ linkHoverUnderline: true
75
+ },
76
+ preheaderText: "",
77
+ popupPosition: "center",
78
+ popupWidth: "600px",
79
+ popupHeight: "auto",
80
+ borderRadius: "10px",
81
+ popupBackgroundColor: "#FFFFFF",
82
+ popupBackgroundImage: { url: "", fullWidth: true, repeat: "no-repeat", center: true, cover: true },
83
+ popupOverlay_backgroundColor: "rgba(0, 0, 0, 0.1)",
84
+ popupCloseButton_position: "top-right",
85
+ popupCloseButton_backgroundColor: "#DDDDDD",
86
+ popupCloseButton_iconColor: "#000000",
87
+ popupCloseButton_borderRadius: "0px",
88
+ popupCloseButton_margin: "0px",
89
+ popupCloseButton_action: {
90
+ name: "close_popup",
91
+ attrs: { onClick: "document.querySelector('.u-popup-container').style.display = 'none';" }
92
+ },
93
+ _meta: { htmlID: "u_body", htmlClassNames: "u_body" }
94
+ }
95
+ },
96
+ schemaVersion: 16
97
+ };
98
+ }
99
+ class EditorStore {
100
+ constructor() {
101
+ this.undoStack = [];
102
+ this.redoStack = [];
103
+ this.maxHistory = 50;
104
+ this.subscribers = /* @__PURE__ */ new Set();
105
+ this.counterManager = createCounterManager();
106
+ this.events = new EventEmitter();
107
+ this._selectedId = null;
108
+ this._hoveredId = null;
109
+ this._viewMode = "desktop";
110
+ this._activeTab = "content";
111
+ this.design = createEmptyDesign();
112
+ }
113
+ // ----------------------------------------------------------
114
+ // Subscriptions (for Lit reactive controllers)
115
+ // ----------------------------------------------------------
116
+ subscribe(fn) {
117
+ this.subscribers.add(fn);
118
+ return () => this.subscribers.delete(fn);
119
+ }
120
+ notify() {
121
+ this.subscribers.forEach((fn) => fn());
122
+ }
123
+ // ----------------------------------------------------------
124
+ // Getters
125
+ // ----------------------------------------------------------
126
+ getDesign() {
127
+ return this.design;
128
+ }
129
+ getBody() {
130
+ return this.design.body;
131
+ }
132
+ getRows() {
133
+ return this.design.body.rows;
134
+ }
135
+ getBodyValues() {
136
+ return this.design.body.values;
137
+ }
138
+ get selectedId() {
139
+ return this._selectedId;
140
+ }
141
+ get hoveredId() {
142
+ return this._hoveredId;
143
+ }
144
+ get viewMode() {
145
+ return this._viewMode;
146
+ }
147
+ get activeTab() {
148
+ return this._activeTab;
149
+ }
150
+ get canUndo() {
151
+ return this.undoStack.length > 0;
152
+ }
153
+ get canRedo() {
154
+ return this.redoStack.length > 0;
155
+ }
156
+ // ----------------------------------------------------------
157
+ // Design loading
158
+ // ----------------------------------------------------------
159
+ loadDesign(design) {
160
+ this.design = deepClone(design);
161
+ this.counterManager.setCounters(this.design.counters);
162
+ this.undoStack = [];
163
+ this.redoStack = [];
164
+ this._selectedId = null;
165
+ this.notify();
166
+ this.events.emit("design:loaded", { design: this.design });
167
+ }
168
+ // ----------------------------------------------------------
169
+ // History (undo/redo)
170
+ // ----------------------------------------------------------
171
+ pushHistory() {
172
+ this.undoStack.push(deepClone(this.design));
173
+ if (this.undoStack.length > this.maxHistory) {
174
+ this.undoStack.shift();
175
+ }
176
+ this.redoStack = [];
177
+ }
178
+ undo() {
179
+ const prev = this.undoStack.pop();
180
+ if (!prev) return;
181
+ this.redoStack.push(deepClone(this.design));
182
+ this.design = prev;
183
+ this.counterManager.setCounters(this.design.counters);
184
+ this.notify();
185
+ this.emitUpdate("content_updated");
186
+ }
187
+ redo() {
188
+ const next = this.redoStack.pop();
189
+ if (!next) return;
190
+ this.undoStack.push(deepClone(this.design));
191
+ this.design = next;
192
+ this.counterManager.setCounters(this.design.counters);
193
+ this.notify();
194
+ this.emitUpdate("content_updated");
195
+ }
196
+ // ----------------------------------------------------------
197
+ // Selection / UI state
198
+ // ----------------------------------------------------------
199
+ select(id) {
200
+ this._selectedId = id;
201
+ this.notify();
202
+ }
203
+ hover(id) {
204
+ this._hoveredId = id;
205
+ this.notify();
206
+ }
207
+ setViewMode(mode) {
208
+ this._viewMode = mode;
209
+ this.notify();
210
+ }
211
+ setActiveTab(tab) {
212
+ this._activeTab = tab;
213
+ this.notify();
214
+ }
215
+ // ----------------------------------------------------------
216
+ // Row operations
217
+ // ----------------------------------------------------------
218
+ addRow(row, index) {
219
+ this.pushHistory();
220
+ const rows = this.design.body.rows;
221
+ if (index !== void 0 && index >= 0 && index <= rows.length) {
222
+ rows.splice(index, 0, deepClone(row));
223
+ } else {
224
+ rows.push(deepClone(row));
225
+ }
226
+ this.syncCounters();
227
+ this.notify();
228
+ this.emitUpdate("row_added", row);
229
+ }
230
+ removeRow(rowId) {
231
+ this.pushHistory();
232
+ const rows = this.design.body.rows;
233
+ const idx = rows.findIndex((r) => r.id === rowId);
234
+ if (idx !== -1) {
235
+ rows.splice(idx, 1);
236
+ if (this._selectedId === rowId) this._selectedId = null;
237
+ this.notify();
238
+ this.emitUpdate("row_removed");
239
+ }
240
+ }
241
+ moveRow(fromIndex, toIndex) {
242
+ this.pushHistory();
243
+ const rows = this.design.body.rows;
244
+ const [row] = rows.splice(fromIndex, 1);
245
+ rows.splice(toIndex, 0, row);
246
+ this.notify();
247
+ this.emitUpdate("row_reordered");
248
+ }
249
+ updateRowValues(rowId, patch) {
250
+ const row = this.findRow(rowId);
251
+ if (!row) return;
252
+ this.pushHistory();
253
+ Object.assign(row.values, patch);
254
+ this.notify();
255
+ this.emitUpdate("content_updated");
256
+ }
257
+ // ----------------------------------------------------------
258
+ // Column operations
259
+ // ----------------------------------------------------------
260
+ updateColumnValues(columnId, patch) {
261
+ const col = this.findColumn(columnId);
262
+ if (!col) return;
263
+ this.pushHistory();
264
+ Object.assign(col.values, patch);
265
+ this.notify();
266
+ this.emitUpdate("content_updated");
267
+ }
268
+ // ----------------------------------------------------------
269
+ // Content operations
270
+ // ----------------------------------------------------------
271
+ addContent(columnId, content, index) {
272
+ const col = this.findColumn(columnId);
273
+ if (!col) return;
274
+ this.pushHistory();
275
+ const cloned = deepClone(content);
276
+ if (index !== void 0 && index >= 0 && index <= col.contents.length) {
277
+ col.contents.splice(index, 0, cloned);
278
+ } else {
279
+ col.contents.push(cloned);
280
+ }
281
+ this.syncCounters();
282
+ this.notify();
283
+ this.emitUpdate("content_added", cloned);
284
+ }
285
+ removeContent(contentId) {
286
+ for (const row of this.design.body.rows) {
287
+ for (const col of row.columns) {
288
+ const idx = col.contents.findIndex((c) => c.id === contentId);
289
+ if (idx !== -1) {
290
+ this.pushHistory();
291
+ col.contents.splice(idx, 1);
292
+ if (this._selectedId === contentId) this._selectedId = null;
293
+ this.notify();
294
+ this.emitUpdate("content_removed");
295
+ return;
296
+ }
297
+ }
298
+ }
299
+ }
300
+ updateContentValues(contentId, patch) {
301
+ const content = this.findContent(contentId);
302
+ if (!content) return;
303
+ this.pushHistory();
304
+ Object.assign(content.values, patch);
305
+ this.notify();
306
+ this.emitUpdate("content_updated");
307
+ }
308
+ moveContent(contentId, targetColumnId, targetIndex) {
309
+ const content = this.findContent(contentId);
310
+ if (!content) return;
311
+ for (const row of this.design.body.rows) {
312
+ for (const col of row.columns) {
313
+ const idx = col.contents.findIndex((c) => c.id === contentId);
314
+ if (idx !== -1) {
315
+ this.pushHistory();
316
+ col.contents.splice(idx, 1);
317
+ break;
318
+ }
319
+ }
320
+ }
321
+ const targetCol = this.findColumn(targetColumnId);
322
+ if (targetCol) {
323
+ targetCol.contents.splice(targetIndex, 0, content);
324
+ }
325
+ this.notify();
326
+ this.emitUpdate("content_reordered");
327
+ }
328
+ duplicateContent(contentId) {
329
+ const content = this.findContent(contentId);
330
+ if (!content) return;
331
+ for (const row of this.design.body.rows) {
332
+ for (const col of row.columns) {
333
+ const idx = col.contents.findIndex((c) => c.id === contentId);
334
+ if (idx !== -1) {
335
+ const cloned = deepClone(content);
336
+ const counter = this.counterManager.next(`u_content_${content.type}`);
337
+ cloned.id = `u_content_${content.type}_${counter}`;
338
+ cloned.values._meta = {
339
+ htmlID: cloned.id,
340
+ htmlClassNames: `u_content_${content.type}`
341
+ };
342
+ this.pushHistory();
343
+ col.contents.splice(idx + 1, 0, cloned);
344
+ this.syncCounters();
345
+ this.notify();
346
+ this.emitUpdate("content_added", cloned);
347
+ return;
348
+ }
349
+ }
350
+ }
351
+ }
352
+ // ----------------------------------------------------------
353
+ // Body values
354
+ // ----------------------------------------------------------
355
+ updateBodyValues(patch) {
356
+ this.pushHistory();
357
+ Object.assign(this.design.body.values, patch);
358
+ this.notify();
359
+ this.emitUpdate("body_updated");
360
+ }
361
+ // ----------------------------------------------------------
362
+ // Lookup helpers
363
+ // ----------------------------------------------------------
364
+ findRow(rowId) {
365
+ return this.design.body.rows.find((r) => r.id === rowId);
366
+ }
367
+ findColumn(columnId) {
368
+ for (const row of this.design.body.rows) {
369
+ const col = row.columns.find((c) => c.id === columnId);
370
+ if (col) return col;
371
+ }
372
+ return void 0;
373
+ }
374
+ findContent(contentId) {
375
+ for (const row of this.design.body.rows) {
376
+ for (const col of row.columns) {
377
+ const content = col.contents.find((c) => c.id === contentId);
378
+ if (content) return content;
379
+ }
380
+ }
381
+ return void 0;
382
+ }
383
+ findParentColumn(contentId) {
384
+ for (const row of this.design.body.rows) {
385
+ for (const col of row.columns) {
386
+ if (col.contents.some((c) => c.id === contentId)) return col;
387
+ }
388
+ }
389
+ return void 0;
390
+ }
391
+ findParentRow(columnId) {
392
+ for (const row of this.design.body.rows) {
393
+ if (row.columns.some((c) => c.id === columnId)) return row;
394
+ }
395
+ return void 0;
396
+ }
397
+ // ----------------------------------------------------------
398
+ // Helpers
399
+ // ----------------------------------------------------------
400
+ /** Creates a new row with the given column layout */
401
+ createRow(cellProportions) {
402
+ const rowNum = this.counterManager.next("u_row");
403
+ const columns = cellProportions.map((_, i) => {
404
+ const colNum = this.counterManager.next("u_column");
405
+ return {
406
+ id: `u_column_${colNum}`,
407
+ contents: [],
408
+ values: {
409
+ backgroundColor: "",
410
+ padding: "0px",
411
+ border: {},
412
+ borderRadius: "0px",
413
+ _meta: { htmlID: `u_column_${colNum}`, htmlClassNames: "u_column" }
414
+ }
415
+ };
416
+ });
417
+ this.syncCounters();
418
+ return {
419
+ id: `u_row_${rowNum}`,
420
+ cells: cellProportions,
421
+ columns,
422
+ values: {
423
+ displayCondition: null,
424
+ columns: false,
425
+ backgroundColor: "",
426
+ columnsBackgroundColor: "",
427
+ backgroundImage: { url: "", fullWidth: true, repeat: false, center: true, cover: false },
428
+ padding: "0px",
429
+ anchor: "",
430
+ hideDesktop: false,
431
+ hideMobile: false,
432
+ _meta: { htmlID: `u_row_${rowNum}`, htmlClassNames: "u_row" }
433
+ }
434
+ };
435
+ }
436
+ /** Creates a new content block for the given tool type */
437
+ createContent(type, values = {}) {
438
+ const counter = this.counterManager.next(`u_content_${type}`);
439
+ const id = `u_content_${type}_${counter}`;
440
+ this.syncCounters();
441
+ return {
442
+ id,
443
+ type,
444
+ values: {
445
+ containerPadding: "10px",
446
+ anchor: "",
447
+ hideDesktop: false,
448
+ hideMobile: false,
449
+ displayCondition: null,
450
+ _meta: { htmlID: id, htmlClassNames: `u_content_${type}` },
451
+ ...values
452
+ }
453
+ };
454
+ }
455
+ syncCounters() {
456
+ this.design.counters = this.counterManager.getCounters();
457
+ }
458
+ emitUpdate(type, item) {
459
+ this.events.emit("design:updated", { type, item });
460
+ }
461
+ }
462
+ class ToolRegistry {
463
+ constructor() {
464
+ this.tools = /* @__PURE__ */ new Map();
465
+ }
466
+ register(tool) {
467
+ this.tools.set(tool.name, tool);
468
+ }
469
+ get(name) {
470
+ return this.tools.get(name);
471
+ }
472
+ getAll() {
473
+ return Array.from(this.tools.values()).sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
474
+ }
475
+ has(name) {
476
+ return this.tools.has(name);
477
+ }
478
+ getDefaultValues(name) {
479
+ const tool = this.tools.get(name);
480
+ if (!tool) return {};
481
+ const defaults = { ...tool.defaultValues };
482
+ for (const group of Object.values(tool.options)) {
483
+ for (const [key, prop] of Object.entries(group.options)) {
484
+ if (!(key in defaults)) {
485
+ defaults[key] = prop.defaultValue;
486
+ }
487
+ }
488
+ }
489
+ return defaults;
490
+ }
491
+ getPropertyGroups(name) {
492
+ return this.tools.get(name)?.options ?? {};
493
+ }
494
+ }
495
+ const dragState = {
496
+ /** ID of the content currently being dragged (null if not dragging content) */
497
+ draggingContentId: null,
498
+ startContentDrag(contentId) {
499
+ this.draggingContentId = contentId;
500
+ },
501
+ reset() {
502
+ this.draggingContentId = null;
503
+ }
504
+ };
505
+ class DragManager {
506
+ constructor(store, toolRegistry, root) {
507
+ this.currentDrop = null;
508
+ this.indicator = null;
509
+ this.rowIndicator = null;
510
+ this.handleDragStart = (_e) => {
511
+ };
512
+ this.handleDragOver = (e) => {
513
+ const types = e.dataTransfer?.types || [];
514
+ const isToolDrag = types.includes("application/maileditor-tool");
515
+ const isLayoutDrag = types.includes("application/maileditor-layout");
516
+ const isContentDrag = types.includes("application/maileditor-content") || !!dragState.draggingContentId;
517
+ if (!isToolDrag && !isContentDrag && !isLayoutDrag) return;
518
+ e.preventDefault();
519
+ e.dataTransfer.dropEffect = isToolDrag || isLayoutDrag ? "copy" : "move";
520
+ if (isLayoutDrag) {
521
+ const drop = this.findRowDropPosition(e.clientY);
522
+ this.hideIndicator();
523
+ if (drop) {
524
+ this.currentDrop = drop;
525
+ this.showRowIndicator(drop);
526
+ } else {
527
+ this.hideRowIndicator();
528
+ this.currentDrop = null;
529
+ }
530
+ } else {
531
+ const drop = this.findContentDropPosition(e.clientX, e.clientY);
532
+ this.hideRowIndicator();
533
+ if (drop) {
534
+ this.currentDrop = drop;
535
+ this.showContentIndicator(drop);
536
+ } else {
537
+ this.hideIndicator();
538
+ this.currentDrop = null;
539
+ }
540
+ }
541
+ };
542
+ this.handleDrop = (e) => {
543
+ e.preventDefault();
544
+ this.hideIndicator();
545
+ this.hideRowIndicator();
546
+ const layoutData = e.dataTransfer?.getData("application/maileditor-layout");
547
+ if (layoutData) {
548
+ const cells = JSON.parse(layoutData);
549
+ const row = this.store.createRow(cells);
550
+ const rowIndex = this.currentDrop?.type === "row" ? this.currentDrop.rowIndex : void 0;
551
+ this.store.addRow(row, rowIndex);
552
+ this.reset();
553
+ return;
554
+ }
555
+ const toolName = e.dataTransfer?.getData("application/maileditor-tool");
556
+ if (toolName) {
557
+ if (this.currentDrop?.type === "content" && this.currentDrop.columnId) {
558
+ const defaults = this.toolRegistry.getDefaultValues(toolName);
559
+ const content = this.store.createContent(toolName, defaults);
560
+ this.store.addContent(this.currentDrop.columnId, content, this.currentDrop.contentIndex);
561
+ this.store.select(content.id);
562
+ } else {
563
+ const row = this.store.createRow([1]);
564
+ this.store.addRow(row);
565
+ const defaults = this.toolRegistry.getDefaultValues(toolName);
566
+ const content = this.store.createContent(toolName, defaults);
567
+ this.store.addContent(row.columns[0].id, content);
568
+ this.store.select(content.id);
569
+ }
570
+ this.reset();
571
+ return;
572
+ }
573
+ const contentId = e.dataTransfer?.getData("application/maileditor-content") || dragState.draggingContentId;
574
+ if (contentId && this.currentDrop?.type === "content" && this.currentDrop.columnId) {
575
+ this.store.moveContent(contentId, this.currentDrop.columnId, this.currentDrop.contentIndex);
576
+ this.store.select(contentId);
577
+ }
578
+ this.reset();
579
+ dragState.reset();
580
+ };
581
+ this.handleDragEnd = () => {
582
+ this.hideIndicator();
583
+ this.hideRowIndicator();
584
+ this.reset();
585
+ };
586
+ this.handleDragLeave = (e) => {
587
+ const related = e.relatedTarget;
588
+ if (!related || !this.root.contains(related)) {
589
+ this.hideIndicator();
590
+ this.hideRowIndicator();
591
+ this.currentDrop = null;
592
+ }
593
+ };
594
+ this.store = store;
595
+ this.toolRegistry = toolRegistry;
596
+ this.root = root;
597
+ }
598
+ attach() {
599
+ this.root.addEventListener("dragover", this.handleDragOver);
600
+ this.root.addEventListener("drop", this.handleDrop);
601
+ this.root.addEventListener("dragend", this.handleDragEnd);
602
+ this.root.addEventListener("dragleave", this.handleDragLeave);
603
+ this.root.addEventListener("dragstart", this.handleDragStart);
604
+ this.indicator = this.createIndicator("#3b82f6");
605
+ this.rowIndicator = this.createIndicator("#8b5cf6");
606
+ }
607
+ createIndicator(color) {
608
+ const el = document.createElement("div");
609
+ Object.assign(el.style, {
610
+ position: "absolute",
611
+ left: "0",
612
+ right: "0",
613
+ height: "3px",
614
+ background: color,
615
+ borderRadius: "2px",
616
+ pointerEvents: "none",
617
+ zIndex: "1000",
618
+ display: "none",
619
+ boxShadow: `0 0 6px ${color}80`
620
+ });
621
+ return el;
622
+ }
623
+ detach() {
624
+ this.root.removeEventListener("dragover", this.handleDragOver);
625
+ this.root.removeEventListener("drop", this.handleDrop);
626
+ this.root.removeEventListener("dragend", this.handleDragEnd);
627
+ this.root.removeEventListener("dragleave", this.handleDragLeave);
628
+ this.root.removeEventListener("dragstart", this.handleDragStart);
629
+ this.indicator?.remove();
630
+ this.rowIndicator?.remove();
631
+ }
632
+ // ----------------------------------------------------------
633
+ // Find row drop position (between rows)
634
+ // ----------------------------------------------------------
635
+ findRowDropPosition(clientY) {
636
+ const canvas = this.root.querySelector("me-editor-canvas");
637
+ if (!canvas?.shadowRoot) return null;
638
+ const rows = Array.from(canvas.shadowRoot.querySelectorAll("me-row-renderer"));
639
+ if (rows.length === 0) {
640
+ return { type: "row", rowIndex: 0, y: 0 };
641
+ }
642
+ const firstRect = rows[0].getBoundingClientRect();
643
+ let bestDist = Math.abs(clientY - firstRect.top);
644
+ let bestTarget = { type: "row", rowIndex: 0, y: firstRect.top };
645
+ for (let i = 0; i < rows.length; i++) {
646
+ const rect = rows[i].getBoundingClientRect();
647
+ const y = rect.bottom;
648
+ const dist = Math.abs(clientY - y);
649
+ if (dist < bestDist) {
650
+ bestDist = dist;
651
+ bestTarget = { type: "row", rowIndex: i + 1, y };
652
+ }
653
+ }
654
+ return bestTarget;
655
+ }
656
+ // ----------------------------------------------------------
657
+ // Find content drop position (inside columns)
658
+ // ----------------------------------------------------------
659
+ findContentDropPosition(clientX, clientY) {
660
+ const columns = this.queryShadowAll(this.root, "me-column-renderer");
661
+ let bestTarget = null;
662
+ let bestDist = Infinity;
663
+ for (const colEl of columns) {
664
+ const columnId = colEl.dataset.columnId;
665
+ if (!columnId) continue;
666
+ const colShadow = colEl.shadowRoot;
667
+ if (!colShadow) continue;
668
+ const colRect = colEl.getBoundingClientRect();
669
+ if (clientX < colRect.left || clientX > colRect.right) continue;
670
+ const contentEls = Array.from(colShadow.querySelectorAll("me-content-renderer"));
671
+ if (contentEls.length === 0) {
672
+ const dist2 = Math.abs(clientY - (colRect.top + colRect.height / 2));
673
+ if (dist2 < bestDist) {
674
+ bestDist = dist2;
675
+ bestTarget = { type: "content", columnId, contentIndex: 0, y: colRect.top + colRect.height / 2 };
676
+ }
677
+ continue;
678
+ }
679
+ const firstRect = contentEls[0].getBoundingClientRect();
680
+ let dist = Math.abs(clientY - firstRect.top);
681
+ if (dist < bestDist) {
682
+ bestDist = dist;
683
+ bestTarget = { type: "content", columnId, contentIndex: 0, y: firstRect.top };
684
+ }
685
+ for (let i = 0; i < contentEls.length; i++) {
686
+ const rect = contentEls[i].getBoundingClientRect();
687
+ const nextRect = contentEls[i + 1]?.getBoundingClientRect();
688
+ const y = nextRect ? (rect.bottom + nextRect.top) / 2 : rect.bottom;
689
+ dist = Math.abs(clientY - y);
690
+ if (dist < bestDist) {
691
+ bestDist = dist;
692
+ bestTarget = { type: "content", columnId, contentIndex: i + 1, y };
693
+ }
694
+ }
695
+ }
696
+ return bestTarget;
697
+ }
698
+ // ----------------------------------------------------------
699
+ // Indicator positioning
700
+ // ----------------------------------------------------------
701
+ showContentIndicator(drop) {
702
+ if (!this.indicator || !drop.columnId) return;
703
+ const columns = this.queryShadowAll(this.root, "me-column-renderer");
704
+ const colEl = columns.find((c) => c.dataset.columnId === drop.columnId);
705
+ if (!colEl?.shadowRoot) return;
706
+ if (this.indicator.parentNode !== colEl.shadowRoot) {
707
+ this.indicator.remove();
708
+ colEl.shadowRoot.appendChild(this.indicator);
709
+ }
710
+ const contentEls = Array.from(colEl.shadowRoot.querySelectorAll("me-content-renderer"));
711
+ const colRect = colEl.getBoundingClientRect();
712
+ let indicatorY;
713
+ if (contentEls.length === 0 || drop.contentIndex === 0) {
714
+ indicatorY = 0;
715
+ } else if (drop.contentIndex >= contentEls.length) {
716
+ const lastRect = contentEls[contentEls.length - 1].getBoundingClientRect();
717
+ indicatorY = lastRect.bottom - colRect.top;
718
+ } else {
719
+ const elRect = contentEls[drop.contentIndex].getBoundingClientRect();
720
+ indicatorY = elRect.top - colRect.top;
721
+ }
722
+ Object.assign(this.indicator.style, {
723
+ display: "block",
724
+ top: `${indicatorY}px`,
725
+ left: "4px",
726
+ right: "4px",
727
+ width: "auto"
728
+ });
729
+ }
730
+ showRowIndicator(drop) {
731
+ if (!this.rowIndicator) return;
732
+ const canvas = this.root.querySelector("me-editor-canvas");
733
+ if (!canvas?.shadowRoot) return;
734
+ const canvasBody = canvas.shadowRoot.querySelector(".canvas-body");
735
+ if (!canvasBody) return;
736
+ if (this.rowIndicator.parentNode !== canvasBody) {
737
+ this.rowIndicator.remove();
738
+ canvasBody.appendChild(this.rowIndicator);
739
+ }
740
+ const rows = Array.from(canvas.shadowRoot.querySelectorAll("me-row-renderer"));
741
+ const bodyRect = canvasBody.getBoundingClientRect();
742
+ let indicatorY;
743
+ if (rows.length === 0 || drop.rowIndex === 0) {
744
+ indicatorY = 0;
745
+ } else if (drop.rowIndex >= rows.length) {
746
+ const lastRect = rows[rows.length - 1].getBoundingClientRect();
747
+ indicatorY = lastRect.bottom - bodyRect.top;
748
+ } else {
749
+ const elRect = rows[drop.rowIndex].getBoundingClientRect();
750
+ indicatorY = elRect.top - bodyRect.top;
751
+ }
752
+ Object.assign(this.rowIndicator.style, {
753
+ display: "block",
754
+ top: `${indicatorY}px`,
755
+ left: "0",
756
+ right: "0",
757
+ width: "auto"
758
+ });
759
+ }
760
+ hideIndicator() {
761
+ if (this.indicator) this.indicator.style.display = "none";
762
+ }
763
+ hideRowIndicator() {
764
+ if (this.rowIndicator) this.rowIndicator.style.display = "none";
765
+ }
766
+ reset() {
767
+ this.currentDrop = null;
768
+ this.hideIndicator();
769
+ this.hideRowIndicator();
770
+ }
771
+ // ----------------------------------------------------------
772
+ // Shadow DOM traversal
773
+ // ----------------------------------------------------------
774
+ queryShadowAll(root, selector) {
775
+ const results = [];
776
+ this.walkShadowDom(root, (el) => {
777
+ if (el.matches?.(selector)) results.push(el);
778
+ });
779
+ return results;
780
+ }
781
+ walkShadowDom(root, callback) {
782
+ const children = root instanceof ShadowRoot ? root.children : root.children;
783
+ for (const child of Array.from(children)) {
784
+ const el = child;
785
+ callback(el);
786
+ if (el.shadowRoot) this.walkShadowDom(el.shadowRoot, callback);
787
+ if (el.children?.length) this.walkShadowDom(el, callback);
788
+ }
789
+ }
790
+ findContentElement(el) {
791
+ while (el) {
792
+ if (el.dataset?.contentId) return el;
793
+ if (el.tagName?.toLowerCase() === "me-content-renderer") return el;
794
+ if (el.parentElement) {
795
+ el = el.parentElement;
796
+ } else if (el.getRootNode().host) {
797
+ el = el.getRootNode().host;
798
+ } else {
799
+ break;
800
+ }
801
+ }
802
+ return null;
803
+ }
804
+ }
805
+ const textTool = {
806
+ name: "text",
807
+ label: "Text",
808
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/></svg>`,
809
+ supportedDisplayModes: ["email", "web"],
810
+ position: 1,
811
+ options: {
812
+ text: {
813
+ title: "Text",
814
+ options: {
815
+ text: {
816
+ label: "Text",
817
+ defaultValue: '<p style="font-size: 14px;">This is a new text block. Change the text.</p>',
818
+ widget: "rich_text"
819
+ }
820
+ }
821
+ },
822
+ style: {
823
+ title: "Style",
824
+ options: {
825
+ color: { label: "Text Color", defaultValue: "#000000", widget: "color_picker" },
826
+ backgroundColor: { label: "Background Color", defaultValue: "", widget: "color_picker" },
827
+ lineHeight: { label: "Line Height", defaultValue: "140%", widget: "text" },
828
+ linkStyle: { label: "Link Style", defaultValue: void 0, widget: "link_style" }
829
+ }
830
+ },
831
+ spacing: {
832
+ title: "Spacing",
833
+ options: {
834
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" }
835
+ }
836
+ },
837
+ general: {
838
+ title: "General",
839
+ options: {
840
+ anchor: { label: "Anchor", defaultValue: "", widget: "text" },
841
+ hideDesktop: { label: "Hide on Desktop", defaultValue: false, widget: "toggle" },
842
+ hideMobile: { label: "Hide on Mobile", defaultValue: false, widget: "toggle" }
843
+ }
844
+ }
845
+ },
846
+ defaultValues: {
847
+ text: '<p style="font-size: 14px;">This is a new text block. Change the text.</p>',
848
+ color: "#000000",
849
+ backgroundColor: "",
850
+ lineHeight: "140%",
851
+ containerPadding: "10px",
852
+ textAlign: "left"
853
+ },
854
+ renderer: {
855
+ renderEditor(values, context) {
856
+ const padding = values.containerPadding || "10px";
857
+ const bgColor = values.backgroundColor || "transparent";
858
+ const color = values.color || "inherit";
859
+ const lineHeight = values.lineHeight || "140%";
860
+ const textContent = values.text || "";
861
+ return html`
862
+ <div style="padding:${padding};background-color:${bgColor};color:${color};line-height:${lineHeight};word-break:break-word;">
863
+ ${unsafeHTML(textContent)}
864
+ </div>
865
+ `;
866
+ },
867
+ renderHtml(values, context) {
868
+ const padding = values.containerPadding || "10px";
869
+ const bgColor = values.backgroundColor || "";
870
+ const color = values.color || "#000000";
871
+ const lineHeight = values.lineHeight || "140%";
872
+ const textContent = values.text || "";
873
+ const textAlign = values.textAlign || "left";
874
+ const bgStyle = bgColor ? `background-color:${bgColor};` : "";
875
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
876
+ <tbody>
877
+ <tr>
878
+ <td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};${bgStyle}font-family:arial,helvetica,sans-serif;" align="left">
879
+ <div style="font-size:14px;color:${color};line-height:${lineHeight};text-align:${textAlign};word-wrap:break-word;">
880
+ ${textContent}
881
+ </div>
882
+ </td>
883
+ </tr>
884
+ </tbody>
885
+ </table>`;
886
+ }
887
+ }
888
+ };
889
+ const headingTool = {
890
+ name: "heading",
891
+ label: "Heading",
892
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h12"/><path d="M6 4v16"/><path d="M18 4v16"/></svg>`,
893
+ supportedDisplayModes: ["email", "web"],
894
+ position: 2,
895
+ options: {
896
+ text: {
897
+ title: "Heading",
898
+ options: {
899
+ text: {
900
+ label: "Text",
901
+ defaultValue: "Heading",
902
+ widget: "text"
903
+ },
904
+ headingType: {
905
+ label: "Heading Type",
906
+ defaultValue: "h1",
907
+ widget: "dropdown",
908
+ widgetParams: {
909
+ options: [
910
+ { label: "H1", value: "h1" },
911
+ { label: "H2", value: "h2" },
912
+ { label: "H3", value: "h3" },
913
+ { label: "H4", value: "h4" }
914
+ ]
915
+ }
916
+ }
917
+ }
918
+ },
919
+ style: {
920
+ title: "Style",
921
+ options: {
922
+ fontSize: { label: "Font Size", defaultValue: "22px", widget: "text" },
923
+ color: { label: "Text Color", defaultValue: "#000000", widget: "color_picker" },
924
+ textAlign: { label: "Text Align", defaultValue: "left", widget: "text" },
925
+ fontWeight: { label: "Font Weight", defaultValue: "700", widget: "text" },
926
+ lineHeight: { label: "Line Height", defaultValue: "140%", widget: "text" },
927
+ letterSpacing: { label: "Letter Spacing", defaultValue: "normal", widget: "text" }
928
+ }
929
+ },
930
+ spacing: {
931
+ title: "Spacing",
932
+ options: {
933
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
934
+ }
935
+ },
936
+ general: {
937
+ title: "General",
938
+ options: {
939
+ anchor: { label: "Anchor", defaultValue: "", widget: "text" },
940
+ hideDesktop: { label: "Hide on Desktop", defaultValue: false, widget: "toggle" },
941
+ hideMobile: { label: "Hide on Mobile", defaultValue: false, widget: "toggle" }
942
+ }
943
+ }
944
+ },
945
+ defaultValues: {
946
+ text: "Heading",
947
+ headingType: "h1",
948
+ fontSize: "22px",
949
+ color: "#000000",
950
+ textAlign: "left",
951
+ fontWeight: "700",
952
+ lineHeight: "140%",
953
+ letterSpacing: "normal",
954
+ containerPadding: "10px"
955
+ },
956
+ renderer: {
957
+ renderEditor(values, ctx) {
958
+ const padding = values.containerPadding || "10px";
959
+ const fontSize = values.fontSize || "22px";
960
+ const color = values.color || "#000000";
961
+ const textAlign = values.textAlign || "left";
962
+ const fontWeight = values.fontWeight || "700";
963
+ const lineHeight = values.lineHeight || "140%";
964
+ const text = values.text || "Heading";
965
+ return html`
966
+ <div style="padding:${padding};font-size:${fontSize};color:${color};text-align:${textAlign};font-weight:${fontWeight};line-height:${lineHeight};">
967
+ ${text}
968
+ </div>
969
+ `;
970
+ },
971
+ renderHtml(values, ctx) {
972
+ const padding = values.containerPadding || "10px";
973
+ const fontSize = values.fontSize || "22px";
974
+ const color = values.color || "#000000";
975
+ const textAlign = values.textAlign || "left";
976
+ const fontWeight = values.fontWeight || "700";
977
+ const lineHeight = values.lineHeight || "140%";
978
+ const letterSpacing = values.letterSpacing || "normal";
979
+ const tag = values.headingType || "h1";
980
+ const text = values.text || "Heading";
981
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
982
+ <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
983
+ <${tag} style="margin:0;font-size:${fontSize};color:${color};text-align:${textAlign};font-weight:${fontWeight};line-height:${lineHeight};letter-spacing:${letterSpacing};">${text}</${tag}>
984
+ </td></tr></tbody>
985
+ </table>`;
986
+ }
987
+ }
988
+ };
989
+ const paragraphTool = {
990
+ name: "paragraph",
991
+ label: "Paragraph",
992
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="3" y1="14" x2="17" y2="14"/></svg>`,
993
+ supportedDisplayModes: ["email", "web"],
994
+ position: 3,
995
+ options: {
996
+ text: {
997
+ title: "Paragraph",
998
+ options: {
999
+ text: {
1000
+ label: "Text",
1001
+ defaultValue: '<p style="font-size:14px;line-height:1.6;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
1002
+ widget: "rich_text"
1003
+ }
1004
+ }
1005
+ },
1006
+ style: {
1007
+ title: "Style",
1008
+ options: {
1009
+ color: { label: "Text Color", defaultValue: "#374151", widget: "color_picker" },
1010
+ lineHeight: { label: "Line Height", defaultValue: "160%", widget: "text" },
1011
+ letterSpacing: { label: "Letter Spacing", defaultValue: "normal", widget: "text" },
1012
+ textAlign: { label: "Text Align", defaultValue: "left", widget: "text" }
1013
+ }
1014
+ },
1015
+ spacing: {
1016
+ title: "Spacing",
1017
+ options: {
1018
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1019
+ }
1020
+ }
1021
+ },
1022
+ defaultValues: {
1023
+ text: '<p style="font-size:14px;line-height:1.6;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
1024
+ color: "#374151",
1025
+ lineHeight: "160%",
1026
+ letterSpacing: "normal",
1027
+ textAlign: "left",
1028
+ containerPadding: "10px"
1029
+ },
1030
+ renderer: {
1031
+ renderEditor(values) {
1032
+ const padding = values.containerPadding || "10px";
1033
+ const color = values.color || "#374151";
1034
+ const lineHeight = values.lineHeight || "160%";
1035
+ const text = values.text || "";
1036
+ return html`<div style="padding:${padding};color:${color};line-height:${lineHeight};word-break:break-word;">${unsafeHTML(text)}</div>`;
1037
+ },
1038
+ renderHtml(values) {
1039
+ const padding = values.containerPadding || "10px";
1040
+ const color = values.color || "#374151";
1041
+ const lineHeight = values.lineHeight || "160%";
1042
+ const letterSpacing = values.letterSpacing || "normal";
1043
+ const textAlign = values.textAlign || "left";
1044
+ const text = values.text || "";
1045
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1046
+ <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
1047
+ <div style="font-size:14px;color:${color};line-height:${lineHeight};text-align:${textAlign};letter-spacing:${letterSpacing};word-wrap:break-word;">${text}</div>
1048
+ </td></tr></tbody>
1049
+ </table>`;
1050
+ }
1051
+ }
1052
+ };
1053
+ const imageTool = {
1054
+ name: "image",
1055
+ label: "Image",
1056
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
1057
+ supportedDisplayModes: ["email", "web"],
1058
+ position: 3,
1059
+ options: {
1060
+ image: {
1061
+ title: "Image",
1062
+ options: {
1063
+ src: { label: "Image URL", defaultValue: "", widget: "text" },
1064
+ alt: { label: "Alt Text", defaultValue: "", widget: "text" },
1065
+ href: { label: "Link URL", defaultValue: "", widget: "text" },
1066
+ target: { label: "Link Target", defaultValue: "_blank", widget: "text" }
1067
+ }
1068
+ },
1069
+ style: {
1070
+ title: "Style",
1071
+ options: {
1072
+ width: { label: "Width", defaultValue: "100%", widget: "text" },
1073
+ maxWidth: { label: "Max Width", defaultValue: "100%", widget: "text" },
1074
+ align: { label: "Align", defaultValue: "center", widget: "text" },
1075
+ borderRadius: { label: "Border Radius", defaultValue: "0px", widget: "text" }
1076
+ }
1077
+ },
1078
+ spacing: {
1079
+ title: "Spacing",
1080
+ options: {
1081
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1082
+ }
1083
+ },
1084
+ general: {
1085
+ title: "General",
1086
+ options: {
1087
+ anchor: { label: "Anchor", defaultValue: "", widget: "text" },
1088
+ hideDesktop: { label: "Hide on Desktop", defaultValue: false, widget: "toggle" },
1089
+ hideMobile: { label: "Hide on Mobile", defaultValue: false, widget: "toggle" }
1090
+ }
1091
+ }
1092
+ },
1093
+ defaultValues: {
1094
+ src: "https://placehold.co/600x200/e2e8f0/64748b?text=Drop+Image+Here",
1095
+ alt: "Image",
1096
+ href: "",
1097
+ target: "_blank",
1098
+ width: "100%",
1099
+ maxWidth: "100%",
1100
+ align: "center",
1101
+ borderRadius: "0px",
1102
+ containerPadding: "10px"
1103
+ },
1104
+ renderer: {
1105
+ renderEditor(values, ctx) {
1106
+ const padding = values.containerPadding || "10px";
1107
+ const src = values.src || "";
1108
+ const alt = values.alt || "";
1109
+ const width = values.width || "100%";
1110
+ const borderRadius = values.borderRadius || "0px";
1111
+ const align = values.align || "center";
1112
+ if (!src) {
1113
+ return html`
1114
+ <div style="padding:${padding};text-align:${align};">
1115
+ <div style="background:#f1f5f9;border:2px dashed #cbd5e1;border-radius:8px;padding:40px 20px;text-align:center;color:#94a3b8;font-family:sans-serif;font-size:13px;">
1116
+ No image set. Enter a URL in the property panel.
1117
+ </div>
1118
+ </div>
1119
+ `;
1120
+ }
1121
+ return html`
1122
+ <div style="padding:${padding};text-align:${align};">
1123
+ <img src=${src} alt=${alt} style="display:inline-block;max-width:100%;width:${width};border-radius:${borderRadius};border:0;" />
1124
+ </div>
1125
+ `;
1126
+ },
1127
+ renderHtml(values, ctx) {
1128
+ const padding = values.containerPadding || "10px";
1129
+ const src = values.src || "";
1130
+ const alt = values.alt || "";
1131
+ const href = values.href || "";
1132
+ const target = values.target || "_blank";
1133
+ const width = values.width || "100%";
1134
+ const borderRadius = values.borderRadius || "0px";
1135
+ const align = values.align || "center";
1136
+ const widthPx = width === "100%" ? ctx.columnWidth : parseInt(width);
1137
+ const borderStyle = borderRadius !== "0px" ? `border-radius:${borderRadius};` : "";
1138
+ const imgTag = `<img align="${align}" border="0" src="${src}" alt="${alt}" title="${alt}" style="outline:none;text-decoration:none;-ms-interpolation-mode:bicubic;clear:both;display:inline-block!important;border:none;height:auto;float:none;width:${width};max-width:${widthPx}px;${borderStyle}" width="${widthPx}" />`;
1139
+ const content = href ? `<a href="${href}" target="${target}" style="text-decoration:none;">${imgTag}</a>` : imgTag;
1140
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1141
+ <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${align}">
1142
+ ${content}
1143
+ </td></tr></tbody>
1144
+ </table>`;
1145
+ }
1146
+ }
1147
+ };
1148
+ const buttonTool = {
1149
+ name: "button",
1150
+ label: "Button",
1151
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="10" rx="2"/><path d="M12 7v10"/><path d="m8 12 4-3 4 3"/></svg>`,
1152
+ supportedDisplayModes: ["email", "web"],
1153
+ position: 4,
1154
+ options: {
1155
+ button: {
1156
+ title: "Button",
1157
+ options: {
1158
+ text: { label: "Button Text", defaultValue: "Click Me", widget: "text" },
1159
+ href: { label: "Link URL", defaultValue: "#", widget: "text" },
1160
+ target: { label: "Target", defaultValue: "_blank", widget: "text" }
1161
+ }
1162
+ },
1163
+ style: {
1164
+ title: "Style",
1165
+ options: {
1166
+ backgroundColor: { label: "Button Color", defaultValue: "#3b82f6", widget: "color_picker" },
1167
+ textColor: { label: "Text Color", defaultValue: "#ffffff", widget: "color_picker" },
1168
+ fontSize: { label: "Font Size", defaultValue: "14px", widget: "text" },
1169
+ fontWeight: { label: "Font Weight", defaultValue: "700", widget: "text" },
1170
+ borderRadius: { label: "Border Radius", defaultValue: "4px", widget: "text" },
1171
+ buttonWidth: { label: "Width", defaultValue: "auto", widget: "text" },
1172
+ textAlign: { label: "Align", defaultValue: "center", widget: "text" },
1173
+ buttonPadding: { label: "Button Padding", defaultValue: "10px 20px", widget: "text" },
1174
+ borderColor: { label: "Border Color", defaultValue: "", widget: "color_picker" },
1175
+ borderWidth: { label: "Border Width", defaultValue: "0px", widget: "text" }
1176
+ }
1177
+ },
1178
+ spacing: {
1179
+ title: "Spacing",
1180
+ options: {
1181
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1182
+ }
1183
+ },
1184
+ general: {
1185
+ title: "General",
1186
+ options: {
1187
+ anchor: { label: "Anchor", defaultValue: "", widget: "text" },
1188
+ hideDesktop: { label: "Hide on Desktop", defaultValue: false, widget: "toggle" },
1189
+ hideMobile: { label: "Hide on Mobile", defaultValue: false, widget: "toggle" }
1190
+ }
1191
+ }
1192
+ },
1193
+ defaultValues: {
1194
+ text: "Click Me",
1195
+ href: "#",
1196
+ target: "_blank",
1197
+ backgroundColor: "#3b82f6",
1198
+ textColor: "#ffffff",
1199
+ fontSize: "14px",
1200
+ fontWeight: "700",
1201
+ borderRadius: "4px",
1202
+ buttonWidth: "auto",
1203
+ textAlign: "center",
1204
+ buttonPadding: "10px 20px",
1205
+ borderColor: "",
1206
+ borderWidth: "0px",
1207
+ containerPadding: "10px"
1208
+ },
1209
+ renderer: {
1210
+ renderEditor(values, ctx) {
1211
+ const padding = values.containerPadding || "10px";
1212
+ const bgColor = values.backgroundColor || "#3b82f6";
1213
+ const textColor = values.textColor || "#ffffff";
1214
+ const fontSize = values.fontSize || "14px";
1215
+ const fontWeight = values.fontWeight || "700";
1216
+ const borderRadius = values.borderRadius || "4px";
1217
+ const buttonPadding = values.buttonPadding || "10px 20px";
1218
+ const text = values.text || "Click Me";
1219
+ const textAlign = values.textAlign || "center";
1220
+ const buttonWidth = values.buttonWidth || "auto";
1221
+ const borderColor = values.borderColor || bgColor;
1222
+ const borderWidth = values.borderWidth || "0px";
1223
+ const borderStyle = borderWidth !== "0px" ? `border:${borderWidth} solid ${borderColor};` : "border:none;";
1224
+ const widthStyle = buttonWidth === "auto" ? "display:inline-block;" : `display:block;width:${buttonWidth};`;
1225
+ return html`
1226
+ <div style="padding:${padding};text-align:${textAlign};">
1227
+ <a style="${widthStyle}background-color:${bgColor};color:${textColor};font-size:${fontSize};font-weight:${fontWeight};border-radius:${borderRadius};padding:${buttonPadding};text-decoration:none;text-align:center;${borderStyle}cursor:pointer;font-family:arial,helvetica,sans-serif;box-sizing:border-box;">
1228
+ ${text}
1229
+ </a>
1230
+ </div>
1231
+ `;
1232
+ },
1233
+ renderHtml(values, ctx) {
1234
+ const padding = values.containerPadding || "10px";
1235
+ const bgColor = values.backgroundColor || "#3b82f6";
1236
+ const textColor = values.textColor || "#ffffff";
1237
+ const fontSize = values.fontSize || "14px";
1238
+ const fontWeight = values.fontWeight || "700";
1239
+ const borderRadius = values.borderRadius || "4px";
1240
+ const buttonPadding = values.buttonPadding || "10px 20px";
1241
+ const text = values.text || "Click Me";
1242
+ const textAlign = values.textAlign || "center";
1243
+ const href = values.href || "#";
1244
+ const target = values.target || "_blank";
1245
+ const borderColor = values.borderColor || bgColor;
1246
+ const borderWidth = values.borderWidth || "0px";
1247
+ const borderStyle = borderWidth !== "0px" ? `border:${borderWidth} solid ${borderColor};` : "border:none;";
1248
+ const vml = parseInt(borderRadius) > 0 ? `
1249
+ <!--[if mso]>
1250
+ <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${href}" style="height:auto;v-text-anchor:middle;width:auto;" arcsize="${Math.round(parseInt(borderRadius) / 20 * 100)}%" stroke="f" fillcolor="${bgColor}">
1251
+ <w:anchorlock/>
1252
+ <center style="color:${textColor};font-family:arial,helvetica,sans-serif;font-size:${fontSize};font-weight:${fontWeight};">${text}</center>
1253
+ </v:roundrect>
1254
+ <![endif]-->
1255
+ <!--[if !mso]><!-->` : "<!--[if !mso]><!-->";
1256
+ const vmlEnd = parseInt(borderRadius) > 0 ? "<!--<![endif]-->" : "<!--<![endif]-->";
1257
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1258
+ <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1259
+ <div align="${textAlign}">
1260
+ ${vml}
1261
+ <a href="${href}" target="${target}" style="box-sizing:border-box;display:inline-block;text-decoration:none;-webkit-text-size-adjust:none;text-align:center;color:${textColor};background-color:${bgColor};border-radius:${borderRadius};-webkit-border-radius:${borderRadius};-moz-border-radius:${borderRadius};font-size:${fontSize};font-weight:${fontWeight};padding:${buttonPadding};font-family:arial,helvetica,sans-serif;${borderStyle}mso-border-alt:none;word-break:keep-all;">
1262
+ <span style="line-height:120%;">${text}</span>
1263
+ </a>
1264
+ ${vmlEnd}
1265
+ </div>
1266
+ </td></tr></tbody>
1267
+ </table>`;
1268
+ }
1269
+ }
1270
+ };
1271
+ const dividerTool = {
1272
+ name: "divider",
1273
+ label: "Divider",
1274
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="2" y1="12" x2="22" y2="12"/></svg>`,
1275
+ supportedDisplayModes: ["email", "web"],
1276
+ position: 5,
1277
+ options: {
1278
+ style: {
1279
+ title: "Style",
1280
+ options: {
1281
+ borderTopWidth: { label: "Width", defaultValue: "1px", widget: "text" },
1282
+ borderTopStyle: { label: "Style", defaultValue: "solid", widget: "text" },
1283
+ borderTopColor: { label: "Color", defaultValue: "#cccccc", widget: "color_picker" },
1284
+ width: { label: "Line Width", defaultValue: "100%", widget: "text" }
1285
+ }
1286
+ },
1287
+ spacing: {
1288
+ title: "Spacing",
1289
+ options: {
1290
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1291
+ }
1292
+ },
1293
+ general: {
1294
+ title: "General",
1295
+ options: {
1296
+ hideDesktop: { label: "Hide on Desktop", defaultValue: false, widget: "toggle" },
1297
+ hideMobile: { label: "Hide on Mobile", defaultValue: false, widget: "toggle" }
1298
+ }
1299
+ }
1300
+ },
1301
+ defaultValues: {
1302
+ borderTopWidth: "1px",
1303
+ borderTopStyle: "solid",
1304
+ borderTopColor: "#cccccc",
1305
+ width: "100%",
1306
+ containerPadding: "10px"
1307
+ },
1308
+ renderer: {
1309
+ renderEditor(values) {
1310
+ const padding = values.containerPadding || "10px";
1311
+ const width = values.width || "100%";
1312
+ const bw = values.borderTopWidth || "1px";
1313
+ const bs = values.borderTopStyle || "solid";
1314
+ const bc = values.borderTopColor || "#cccccc";
1315
+ return html`
1316
+ <div style="padding:${padding};">
1317
+ <div style="border-top:${bw} ${bs} ${bc};width:${width};margin:0 auto;"></div>
1318
+ </div>
1319
+ `;
1320
+ },
1321
+ renderHtml(values) {
1322
+ const padding = values.containerPadding || "10px";
1323
+ const width = values.width || "100%";
1324
+ const bw = values.borderTopWidth || "1px";
1325
+ const bs = values.borderTopStyle || "solid";
1326
+ const bc = values.borderTopColor || "#cccccc";
1327
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1328
+ <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="center">
1329
+ <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="${width}" style="border-collapse:collapse;table-layout:fixed;border-spacing:0;mso-table-lspace:0;mso-table-rspace:0;vertical-align:top;border-top:${bw} ${bs} ${bc};-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;">
1330
+ <tbody><tr><td style="font-size:0;line-height:0;">&nbsp;</td></tr></tbody>
1331
+ </table>
1332
+ </td></tr></tbody>
1333
+ </table>`;
1334
+ }
1335
+ }
1336
+ };
1337
+ const htmlTool = {
1338
+ name: "html",
1339
+ label: "HTML",
1340
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>`,
1341
+ supportedDisplayModes: ["email", "web"],
1342
+ position: 6,
1343
+ options: {
1344
+ html: {
1345
+ title: "HTML",
1346
+ options: {
1347
+ html: {
1348
+ label: "Custom HTML",
1349
+ defaultValue: '<div style="padding:20px;text-align:center;color:#999;">Custom HTML Block</div>',
1350
+ widget: "rich_text"
1351
+ }
1352
+ }
1353
+ },
1354
+ spacing: {
1355
+ title: "Spacing",
1356
+ options: {
1357
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1358
+ }
1359
+ },
1360
+ general: {
1361
+ title: "General",
1362
+ options: {
1363
+ hideDesktop: { label: "Hide on Desktop", defaultValue: false, widget: "toggle" },
1364
+ hideMobile: { label: "Hide on Mobile", defaultValue: false, widget: "toggle" }
1365
+ }
1366
+ }
1367
+ },
1368
+ defaultValues: {
1369
+ html: '<div style="padding:20px;text-align:center;color:#999;">Custom HTML Block</div>',
1370
+ containerPadding: "10px"
1371
+ },
1372
+ renderer: {
1373
+ renderEditor(values) {
1374
+ const padding = values.containerPadding || "10px";
1375
+ const content = values.html || "";
1376
+ return html`<div style="padding:${padding};">${unsafeHTML(content)}</div>`;
1377
+ },
1378
+ renderHtml(values) {
1379
+ const padding = values.containerPadding || "10px";
1380
+ const content = values.html || "";
1381
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1382
+ <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
1383
+ ${content}
1384
+ </td></tr></tbody>
1385
+ </table>`;
1386
+ }
1387
+ }
1388
+ };
1389
+ const defaultSocials = [
1390
+ { name: "Facebook", url: "https://facebook.com/", icon: "f", color: "#1877F2" },
1391
+ { name: "Twitter", url: "https://twitter.com/", icon: "𝕏", color: "#000000" },
1392
+ { name: "Instagram", url: "https://instagram.com/", icon: "📷", color: "#E4405F" },
1393
+ { name: "LinkedIn", url: "https://linkedin.com/", icon: "in", color: "#0A66C2" }
1394
+ ];
1395
+ const socialTool = {
1396
+ name: "social",
1397
+ label: "Social",
1398
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>`,
1399
+ supportedDisplayModes: ["email", "web"],
1400
+ position: 8,
1401
+ options: {
1402
+ icons: {
1403
+ title: "Social Icons",
1404
+ options: {
1405
+ icons: { label: "Icons (JSON)", defaultValue: JSON.stringify(defaultSocials), widget: "rich_text" },
1406
+ iconSize: { label: "Icon Size", defaultValue: "32px", widget: "text" },
1407
+ iconSpacing: { label: "Spacing", defaultValue: "8px", widget: "text" }
1408
+ }
1409
+ },
1410
+ style: {
1411
+ title: "Style",
1412
+ options: {
1413
+ textAlign: { label: "Align", defaultValue: "center", widget: "text" }
1414
+ }
1415
+ },
1416
+ spacing: {
1417
+ title: "Spacing",
1418
+ options: {
1419
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1420
+ }
1421
+ }
1422
+ },
1423
+ defaultValues: {
1424
+ icons: JSON.stringify(defaultSocials),
1425
+ iconSize: "32px",
1426
+ iconSpacing: "8px",
1427
+ textAlign: "center",
1428
+ containerPadding: "10px"
1429
+ },
1430
+ renderer: {
1431
+ renderEditor(values) {
1432
+ const padding = values.containerPadding || "10px";
1433
+ const textAlign = values.textAlign || "center";
1434
+ const iconSize = values.iconSize || "32px";
1435
+ const spacing = values.iconSpacing || "8px";
1436
+ let icons = defaultSocials;
1437
+ try {
1438
+ icons = JSON.parse(values.icons);
1439
+ } catch {
1440
+ }
1441
+ return html`
1442
+ <div style="padding:${padding};text-align:${textAlign};">
1443
+ ${icons.map(
1444
+ (s) => html`
1445
+ <a href=${s.url} target="_blank" style="display:inline-block;width:${iconSize};height:${iconSize};line-height:${iconSize};text-align:center;background:${s.color};color:white;border-radius:50%;text-decoration:none;font-size:14px;font-weight:bold;margin:0 ${spacing};font-family:arial,sans-serif;vertical-align:middle;">${s.icon}</a>
1446
+ `
1447
+ )}
1448
+ </div>
1449
+ `;
1450
+ },
1451
+ renderHtml(values) {
1452
+ const padding = values.containerPadding || "10px";
1453
+ const textAlign = values.textAlign || "center";
1454
+ const iconSize = parseInt(values.iconSize || "32");
1455
+ const spacing = values.iconSpacing || "8px";
1456
+ let icons = defaultSocials;
1457
+ try {
1458
+ icons = JSON.parse(values.icons);
1459
+ } catch {
1460
+ }
1461
+ const iconsHtml = icons.map(
1462
+ (s) => `<td align="center" valign="middle" style="padding:0 ${spacing};">
1463
+ <a href="${s.url}" target="_blank" style="text-decoration:none;">
1464
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>
1465
+ <td width="${iconSize}" height="${iconSize}" align="center" valign="middle" style="width:${iconSize}px;height:${iconSize}px;background:${s.color};border-radius:50%;color:#ffffff;font-size:14px;font-weight:bold;font-family:arial,sans-serif;">
1466
+ ${s.icon}
1467
+ </td>
1468
+ </tr></table>
1469
+ </a>
1470
+ </td>`
1471
+ ).join("\n");
1472
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1473
+ <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1474
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" align="${textAlign}"><tr>${iconsHtml}</tr></table>
1475
+ </td></tr></tbody>
1476
+ </table>`;
1477
+ }
1478
+ }
1479
+ };
1480
+ const defaultItems = [
1481
+ { text: "Home", href: "#" },
1482
+ { text: "About", href: "#" },
1483
+ { text: "Contact", href: "#" }
1484
+ ];
1485
+ const menuTool = {
1486
+ name: "menu",
1487
+ label: "Menu",
1488
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg>`,
1489
+ supportedDisplayModes: ["email", "web"],
1490
+ position: 9,
1491
+ options: {
1492
+ menu: {
1493
+ title: "Menu",
1494
+ options: {
1495
+ items: { label: "Items (JSON)", defaultValue: JSON.stringify(defaultItems), widget: "rich_text" }
1496
+ }
1497
+ },
1498
+ style: {
1499
+ title: "Style",
1500
+ options: {
1501
+ textAlign: { label: "Align", defaultValue: "center", widget: "text" },
1502
+ fontSize: { label: "Font Size", defaultValue: "14px", widget: "text" },
1503
+ color: { label: "Text Color", defaultValue: "#333333", widget: "color_picker" },
1504
+ separator: { label: "Separator", defaultValue: "|", widget: "text" },
1505
+ separatorColor: { label: "Separator Color", defaultValue: "#cccccc", widget: "color_picker" }
1506
+ }
1507
+ },
1508
+ spacing: {
1509
+ title: "Spacing",
1510
+ options: {
1511
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1512
+ }
1513
+ }
1514
+ },
1515
+ defaultValues: {
1516
+ items: JSON.stringify(defaultItems),
1517
+ textAlign: "center",
1518
+ fontSize: "14px",
1519
+ color: "#333333",
1520
+ separator: "|",
1521
+ separatorColor: "#cccccc",
1522
+ containerPadding: "10px"
1523
+ },
1524
+ renderer: {
1525
+ renderEditor(values) {
1526
+ const padding = values.containerPadding || "10px";
1527
+ const textAlign = values.textAlign || "center";
1528
+ const fontSize = values.fontSize || "14px";
1529
+ const color = values.color || "#333333";
1530
+ const sep = values.separator || "|";
1531
+ const sepColor = values.separatorColor || "#cccccc";
1532
+ let items = defaultItems;
1533
+ try {
1534
+ items = JSON.parse(values.items);
1535
+ } catch {
1536
+ }
1537
+ return html`
1538
+ <div style="padding:${padding};text-align:${textAlign};font-size:${fontSize};font-family:arial,sans-serif;">
1539
+ ${items.map(
1540
+ (item, i) => html`${i > 0 ? html`<span style="color:${sepColor};padding:0 8px;">${sep}</span>` : ""}
1541
+ <a href=${item.href} style="color:${color};text-decoration:none;">${item.text}</a>`
1542
+ )}
1543
+ </div>
1544
+ `;
1545
+ },
1546
+ renderHtml(values) {
1547
+ const padding = values.containerPadding || "10px";
1548
+ const textAlign = values.textAlign || "center";
1549
+ const fontSize = values.fontSize || "14px";
1550
+ const color = values.color || "#333333";
1551
+ const sep = values.separator || "|";
1552
+ const sepColor = values.separatorColor || "#cccccc";
1553
+ let items = defaultItems;
1554
+ try {
1555
+ items = JSON.parse(values.items);
1556
+ } catch {
1557
+ }
1558
+ const linksHtml = items.map((item, i) => {
1559
+ const prefix = i > 0 ? `<span style="color:${sepColor};padding:0 8px;">${sep}</span>` : "";
1560
+ return `${prefix}<a href="${item.href}" target="_blank" style="color:${color};text-decoration:none;font-family:arial,helvetica,sans-serif;font-size:${fontSize};">${item.text}</a>`;
1561
+ }).join("");
1562
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1563
+ <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1564
+ <div style="text-align:${textAlign};">${linksHtml}</div>
1565
+ </td></tr></tbody>
1566
+ </table>`;
1567
+ }
1568
+ }
1569
+ };
1570
+ function getYouTubeThumbnail(url) {
1571
+ const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/);
1572
+ return match ? `https://img.youtube.com/vi/${match[1]}/maxresdefault.jpg` : null;
1573
+ }
1574
+ const videoTool = {
1575
+ name: "video",
1576
+ label: "Video",
1577
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>`,
1578
+ supportedDisplayModes: ["email", "web"],
1579
+ position: 10,
1580
+ options: {
1581
+ video: {
1582
+ title: "Video",
1583
+ options: {
1584
+ url: { label: "Video URL", defaultValue: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", widget: "text" },
1585
+ thumbnailUrl: { label: "Thumbnail URL (auto)", defaultValue: "", widget: "text" },
1586
+ alt: { label: "Alt Text", defaultValue: "Video", widget: "text" }
1587
+ }
1588
+ },
1589
+ style: {
1590
+ title: "Style",
1591
+ options: {
1592
+ textAlign: { label: "Align", defaultValue: "center", widget: "text" }
1593
+ }
1594
+ },
1595
+ spacing: {
1596
+ title: "Spacing",
1597
+ options: {
1598
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1599
+ }
1600
+ }
1601
+ },
1602
+ defaultValues: {
1603
+ url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
1604
+ thumbnailUrl: "",
1605
+ alt: "Video",
1606
+ textAlign: "center",
1607
+ containerPadding: "10px"
1608
+ },
1609
+ renderer: {
1610
+ renderEditor(values) {
1611
+ const padding = values.containerPadding || "10px";
1612
+ const url = values.url || "";
1613
+ const thumbnail = values.thumbnailUrl || getYouTubeThumbnail(url) || "";
1614
+ const textAlign = values.textAlign || "center";
1615
+ if (!thumbnail) {
1616
+ return html`
1617
+ <div style="padding:${padding};text-align:${textAlign};">
1618
+ <div style="background:#0f172a;border-radius:8px;padding:40px;text-align:center;color:white;font-family:sans-serif;position:relative;">
1619
+ <div style="font-size:48px;opacity:0.8;">▶</div>
1620
+ <div style="font-size:12px;margin-top:8px;opacity:0.6;">${url || "Enter video URL"}</div>
1621
+ </div>
1622
+ </div>
1623
+ `;
1624
+ }
1625
+ return html`
1626
+ <div style="padding:${padding};text-align:${textAlign};">
1627
+ <div style="position:relative;display:inline-block;max-width:100%;cursor:pointer;">
1628
+ <img src=${thumbnail} alt="Video thumbnail" style="display:block;max-width:100%;border-radius:4px;" />
1629
+ <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;">
1630
+ <div style="width:60px;height:60px;background:rgba(0,0,0,0.7);border-radius:50%;display:flex;align-items:center;justify-content:center;color:white;font-size:24px;">▶</div>
1631
+ </div>
1632
+ </div>
1633
+ </div>
1634
+ `;
1635
+ },
1636
+ renderHtml(values, ctx) {
1637
+ const padding = values.containerPadding || "10px";
1638
+ const url = values.url || "#";
1639
+ const thumbnail = values.thumbnailUrl || getYouTubeThumbnail(url) || "";
1640
+ const alt = values.alt || "Video";
1641
+ const textAlign = values.textAlign || "center";
1642
+ const widthPx = ctx.columnWidth;
1643
+ const imgTag = thumbnail ? `<img src="${thumbnail}" alt="${alt}" width="${widthPx}" style="display:block;max-width:100%;width:${widthPx}px;border:0;" />` : `<div style="background:#0f172a;padding:40px;text-align:center;color:white;font-family:arial,sans-serif;font-size:16px;">▶ Watch Video</div>`;
1644
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1645
+ <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1646
+ <a href="${url}" target="_blank" style="text-decoration:none;">
1647
+ ${imgTag}
1648
+ </a>
1649
+ </td></tr></tbody>
1650
+ </table>`;
1651
+ }
1652
+ }
1653
+ };
1654
+ const timerTool = {
1655
+ name: "timer",
1656
+ label: "Timer",
1657
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`,
1658
+ supportedDisplayModes: ["email", "web"],
1659
+ position: 11,
1660
+ options: {
1661
+ timer: {
1662
+ title: "Timer",
1663
+ options: {
1664
+ endDate: { label: "End Date (ISO)", defaultValue: new Date(Date.now() + 7 * 864e5).toISOString().split("T")[0], widget: "text" },
1665
+ expiredMessage: { label: "Expired Message", defaultValue: "This offer has expired", widget: "text" }
1666
+ }
1667
+ },
1668
+ style: {
1669
+ title: "Style",
1670
+ options: {
1671
+ textAlign: { label: "Align", defaultValue: "center", widget: "text" },
1672
+ backgroundColor: { label: "Background", defaultValue: "#1f2937", widget: "color_picker" },
1673
+ textColor: { label: "Text Color", defaultValue: "#ffffff", widget: "color_picker" },
1674
+ fontSize: { label: "Font Size", defaultValue: "32px", widget: "text" }
1675
+ }
1676
+ },
1677
+ spacing: {
1678
+ title: "Spacing",
1679
+ options: {
1680
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1681
+ }
1682
+ }
1683
+ },
1684
+ defaultValues: {
1685
+ endDate: new Date(Date.now() + 7 * 864e5).toISOString().split("T")[0],
1686
+ expiredMessage: "This offer has expired",
1687
+ textAlign: "center",
1688
+ backgroundColor: "#1f2937",
1689
+ textColor: "#ffffff",
1690
+ fontSize: "32px",
1691
+ containerPadding: "10px"
1692
+ },
1693
+ renderer: {
1694
+ renderEditor(values) {
1695
+ const padding = values.containerPadding || "10px";
1696
+ const bg = values.backgroundColor || "#1f2937";
1697
+ const color = values.textColor || "#ffffff";
1698
+ const fontSize = values.fontSize || "32px";
1699
+ const textAlign = values.textAlign || "center";
1700
+ return html`
1701
+ <div style="padding:${padding};">
1702
+ <div style="background:${bg};color:${color};font-size:${fontSize};text-align:${textAlign};padding:20px;border-radius:4px;font-family:monospace;font-weight:bold;letter-spacing:4px;">
1703
+ 00 : 00 : 00 : 00
1704
+ <div style="font-size:11px;letter-spacing:8px;opacity:0.6;margin-top:4px;">DAYS &nbsp; HRS &nbsp; MIN &nbsp; SEC</div>
1705
+ </div>
1706
+ </div>
1707
+ `;
1708
+ },
1709
+ renderHtml(values) {
1710
+ const padding = values.containerPadding || "10px";
1711
+ const bg = values.backgroundColor || "#1f2937";
1712
+ const color = values.textColor || "#ffffff";
1713
+ const fontSize = values.fontSize || "32px";
1714
+ const textAlign = values.textAlign || "center";
1715
+ values.endDate || "";
1716
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1717
+ <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1718
+ <div style="background-color:${bg};color:${color};font-size:${fontSize};text-align:${textAlign};padding:20px;border-radius:4px;font-family:'Courier New',monospace;font-weight:bold;letter-spacing:4px;">
1719
+ <div>00 : 00 : 00 : 00</div>
1720
+ <div style="font-size:11px;letter-spacing:8px;opacity:0.6;margin-top:4px;">DAYS &nbsp; HRS &nbsp; MIN &nbsp; SEC</div>
1721
+ </div>
1722
+ </td></tr></tbody>
1723
+ </table>`;
1724
+ }
1725
+ }
1726
+ };
1727
+ const defaultTableData = [
1728
+ ["Header 1", "Header 2", "Header 3"],
1729
+ ["Cell 1", "Cell 2", "Cell 3"],
1730
+ ["Cell 4", "Cell 5", "Cell 6"]
1731
+ ];
1732
+ const tableTool = {
1733
+ name: "table",
1734
+ label: "Table",
1735
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M9 3v18"/><path d="M15 3v18"/></svg>`,
1736
+ supportedDisplayModes: ["email", "web"],
1737
+ position: 12,
1738
+ options: {
1739
+ table: {
1740
+ title: "Table",
1741
+ options: {
1742
+ tableData: { label: "Table Data (JSON)", defaultValue: JSON.stringify(defaultTableData), widget: "rich_text" }
1743
+ }
1744
+ },
1745
+ style: {
1746
+ title: "Style",
1747
+ options: {
1748
+ headerBg: { label: "Header Background", defaultValue: "#f3f4f6", widget: "color_picker" },
1749
+ headerColor: { label: "Header Text Color", defaultValue: "#111827", widget: "color_picker" },
1750
+ borderColor: { label: "Border Color", defaultValue: "#e5e7eb", widget: "color_picker" },
1751
+ cellPadding: { label: "Cell Padding", defaultValue: "8px 12px", widget: "text" },
1752
+ fontSize: { label: "Font Size", defaultValue: "14px", widget: "text" }
1753
+ }
1754
+ },
1755
+ spacing: {
1756
+ title: "Spacing",
1757
+ options: {
1758
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1759
+ }
1760
+ }
1761
+ },
1762
+ defaultValues: {
1763
+ tableData: JSON.stringify(defaultTableData),
1764
+ headerBg: "#f3f4f6",
1765
+ headerColor: "#111827",
1766
+ borderColor: "#e5e7eb",
1767
+ cellPadding: "8px 12px",
1768
+ fontSize: "14px",
1769
+ containerPadding: "10px"
1770
+ },
1771
+ renderer: {
1772
+ renderEditor(values) {
1773
+ const padding = values.containerPadding || "10px";
1774
+ const headerBg = values.headerBg || "#f3f4f6";
1775
+ const headerColor = values.headerColor || "#111827";
1776
+ const borderColor = values.borderColor || "#e5e7eb";
1777
+ const cellPadding = values.cellPadding || "8px 12px";
1778
+ const fontSize = values.fontSize || "14px";
1779
+ let data = defaultTableData;
1780
+ try {
1781
+ data = JSON.parse(values.tableData);
1782
+ } catch {
1783
+ }
1784
+ const headerRow = data[0] || [];
1785
+ const bodyRows = data.slice(1);
1786
+ return html`
1787
+ <div style="padding:${padding};overflow-x:auto;">
1788
+ <table style="width:100%;border-collapse:collapse;font-size:${fontSize};font-family:arial,sans-serif;">
1789
+ <thead>
1790
+ <tr>
1791
+ ${headerRow.map((cell) => html`<th style="padding:${cellPadding};background:${headerBg};color:${headerColor};border:1px solid ${borderColor};text-align:left;font-weight:600;">${cell}</th>`)}
1792
+ </tr>
1793
+ </thead>
1794
+ <tbody>
1795
+ ${bodyRows.map((row) => html`
1796
+ <tr>
1797
+ ${row.map((cell) => html`<td style="padding:${cellPadding};border:1px solid ${borderColor};">${cell}</td>`)}
1798
+ </tr>
1799
+ `)}
1800
+ </tbody>
1801
+ </table>
1802
+ </div>
1803
+ `;
1804
+ },
1805
+ renderHtml(values) {
1806
+ const padding = values.containerPadding || "10px";
1807
+ const headerBg = values.headerBg || "#f3f4f6";
1808
+ const headerColor = values.headerColor || "#111827";
1809
+ const borderColor = values.borderColor || "#e5e7eb";
1810
+ const cellPadding = values.cellPadding || "8px 12px";
1811
+ const fontSize = values.fontSize || "14px";
1812
+ let data = defaultTableData;
1813
+ try {
1814
+ data = JSON.parse(values.tableData);
1815
+ } catch {
1816
+ }
1817
+ const headerRow = data[0] || [];
1818
+ const bodyRows = data.slice(1);
1819
+ const headerHtml = headerRow.map(
1820
+ (c) => `<th style="padding:${cellPadding};background-color:${headerBg};color:${headerColor};border:1px solid ${borderColor};text-align:left;font-weight:600;font-family:arial,helvetica,sans-serif;font-size:${fontSize};">${c}</th>`
1821
+ ).join("");
1822
+ const bodyHtml = bodyRows.map(
1823
+ (row) => `<tr>${row.map((c) => `<td style="padding:${cellPadding};border:1px solid ${borderColor};font-family:arial,helvetica,sans-serif;font-size:${fontSize};">${c}</td>`).join("")}</tr>`
1824
+ ).join("");
1825
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1826
+ <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
1827
+ <table cellpadding="0" cellspacing="0" width="100%" border="0" style="border-collapse:collapse;">
1828
+ <thead><tr>${headerHtml}</tr></thead>
1829
+ <tbody>${bodyHtml}</tbody>
1830
+ </table>
1831
+ </td></tr></tbody>
1832
+ </table>`;
1833
+ }
1834
+ }
1835
+ };
1836
+ const formTool = {
1837
+ name: "form",
1838
+ label: "Form",
1839
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h6"/></svg>`,
1840
+ supportedDisplayModes: ["web"],
1841
+ position: 13,
1842
+ options: {
1843
+ form: {
1844
+ title: "Form",
1845
+ options: {
1846
+ actionUrl: { label: "Action URL", defaultValue: "#", widget: "text" },
1847
+ method: { label: "Method", defaultValue: "POST", widget: "text" },
1848
+ submitText: { label: "Submit Text", defaultValue: "Submit", widget: "text" },
1849
+ fields: { label: "Fields (JSON)", defaultValue: JSON.stringify([
1850
+ { label: "Name", name: "name", type: "text", placeholder: "Your name" },
1851
+ { label: "Email", name: "email", type: "email", placeholder: "your@email.com" }
1852
+ ]), widget: "rich_text" }
1853
+ }
1854
+ },
1855
+ style: {
1856
+ title: "Style",
1857
+ options: {
1858
+ buttonBg: { label: "Button Color", defaultValue: "#3b82f6", widget: "color_picker" },
1859
+ buttonColor: { label: "Button Text", defaultValue: "#ffffff", widget: "color_picker" }
1860
+ }
1861
+ },
1862
+ spacing: {
1863
+ title: "Spacing",
1864
+ options: {
1865
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1866
+ }
1867
+ }
1868
+ },
1869
+ defaultValues: {
1870
+ actionUrl: "#",
1871
+ method: "POST",
1872
+ submitText: "Submit",
1873
+ fields: JSON.stringify([
1874
+ { label: "Name", name: "name", type: "text", placeholder: "Your name" },
1875
+ { label: "Email", name: "email", type: "email", placeholder: "your@email.com" }
1876
+ ]),
1877
+ buttonBg: "#3b82f6",
1878
+ buttonColor: "#ffffff",
1879
+ containerPadding: "10px"
1880
+ },
1881
+ renderer: {
1882
+ renderEditor(values) {
1883
+ const padding = values.containerPadding || "10px";
1884
+ const submitText = values.submitText || "Submit";
1885
+ const buttonBg = values.buttonBg || "#3b82f6";
1886
+ const buttonColor = values.buttonColor || "#ffffff";
1887
+ let fields = [];
1888
+ try {
1889
+ fields = JSON.parse(values.fields);
1890
+ } catch {
1891
+ }
1892
+ return html`
1893
+ <div style="padding:${padding};font-family:arial,sans-serif;">
1894
+ ${fields.map((f) => html`
1895
+ <div style="margin-bottom:12px;">
1896
+ <label style="display:block;font-size:13px;color:#374151;margin-bottom:4px;font-weight:500;">${f.label}</label>
1897
+ <input type=${f.type || "text"} placeholder=${f.placeholder || ""} style="width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:4px;font-size:14px;box-sizing:border-box;" />
1898
+ </div>
1899
+ `)}
1900
+ <button style="background:${buttonBg};color:${buttonColor};border:none;padding:10px 24px;border-radius:4px;font-size:14px;font-weight:600;cursor:pointer;">${submitText}</button>
1901
+ </div>
1902
+ `;
1903
+ },
1904
+ renderHtml(values) {
1905
+ const padding = values.containerPadding || "10px";
1906
+ const actionUrl = values.actionUrl || "#";
1907
+ const method = values.method || "POST";
1908
+ const submitText = values.submitText || "Submit";
1909
+ const buttonBg = values.buttonBg || "#3b82f6";
1910
+ const buttonColor = values.buttonColor || "#ffffff";
1911
+ let fields = [];
1912
+ try {
1913
+ fields = JSON.parse(values.fields);
1914
+ } catch {
1915
+ }
1916
+ const fieldsHtml = fields.map(
1917
+ (f) => `<div style="margin-bottom:12px;">
1918
+ <label style="display:block;font-size:13px;color:#374151;margin-bottom:4px;font-weight:500;font-family:arial,helvetica,sans-serif;">${f.label}</label>
1919
+ <input type="${f.type || "text"}" name="${f.name}" placeholder="${f.placeholder || ""}" style="width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:4px;font-size:14px;box-sizing:border-box;font-family:arial,helvetica,sans-serif;" />
1920
+ </div>`
1921
+ ).join("");
1922
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1923
+ <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
1924
+ <form action="${actionUrl}" method="${method}">
1925
+ ${fieldsHtml}
1926
+ <button type="submit" style="background-color:${buttonBg};color:${buttonColor};border:none;padding:10px 24px;border-radius:4px;font-size:14px;font-weight:600;cursor:pointer;font-family:arial,helvetica,sans-serif;">${submitText}</button>
1927
+ </form>
1928
+ </td></tr></tbody>
1929
+ </table>`;
1930
+ }
1931
+ }
1932
+ };
1933
+ function wrapInDocumentShell(bodyHtml, cssBlock, bodyValues) {
1934
+ const bgColor = bodyValues.backgroundColor || "#e7e7e7";
1935
+ const contentWidth = bodyValues.contentWidth || "600px";
1936
+ const fontFamily = bodyValues.fontFamily?.value || "arial,helvetica,sans-serif";
1937
+ const textColor = bodyValues.textColor || "#000000";
1938
+ const preheaderText = bodyValues.preheaderText || "";
1939
+ const preheader = preheaderText ? `<div style="display:none;font-size:1px;color:${bgColor};line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">${preheaderText}${"&zwnj;&nbsp;".repeat(80)}</div>` : "";
1940
+ return `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1941
+ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
1942
+ <head>
1943
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
1944
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1945
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
1946
+ <meta name="x-apple-disable-message-reformatting">
1947
+ <meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
1948
+ <meta name="color-scheme" content="light dark">
1949
+ <meta name="supported-color-schemes" content="light dark">
1950
+ <title></title>
1951
+ <!--[if mso]>
1952
+ <noscript><xml>
1953
+ <o:OfficeDocumentSettings>
1954
+ <o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch>
1955
+ </o:OfficeDocumentSettings>
1956
+ </xml></noscript>
1957
+ <style type="text/css">
1958
+ table, td, th { font-family: ${fontFamily} !important; }
1959
+ </style>
1960
+ <![endif]-->
1961
+ <!--[if !mso]><!-->
1962
+ <style type="text/css">
1963
+ ${cssBlock}
1964
+ </style>
1965
+ <!--<![endif]-->
1966
+ <style type="text/css">
1967
+ body { margin: 0; padding: 0; }
1968
+ table, tr, td { vertical-align: top; border-collapse: collapse; }
1969
+ p { margin: 0; }
1970
+ .ie-container table, .mso-container table { table-layout: fixed; }
1971
+ * { line-height: inherit; }
1972
+ a[x-apple-data-detectors='true'] { color: inherit !important; text-decoration: none !important; }
1973
+ </style>
1974
+ </head>
1975
+ <body class="clean-body u_body" style="margin:0;padding:0;-webkit-text-size-adjust:100%;background-color:${bgColor};color:${textColor};">
1976
+ ${preheader}
1977
+ <table id="u_body" style="border-collapse:collapse;table-layout:fixed;border-spacing:0;mso-table-lspace:0pt;mso-table-rspace:0pt;vertical-align:top;min-width:320px;margin:0 auto;background-color:${bgColor};width:100%;" cellpadding="0" cellspacing="0" border="0">
1978
+ <tbody>
1979
+ <tr style="vertical-align:top;">
1980
+ <td style="word-break:break-word;border-collapse:collapse !important;vertical-align:top;">
1981
+ <!--[if (mso)|(IE)]><table width="${parseInt(contentWidth)}" align="center" cellpadding="0" cellspacing="0" border="0"><tr><td><![endif]-->
1982
+ ${bodyHtml}
1983
+ <!--[if (mso)|(IE)]></td></tr></table><![endif]-->
1984
+ </td>
1985
+ </tr>
1986
+ </tbody>
1987
+ </table>
1988
+ </body>
1989
+ </html>`;
1990
+ }
1991
+ function renderRow(row, bodyValues, toolRenderers) {
1992
+ const contentWidth = parseInt(bodyValues.contentWidth || "600");
1993
+ const bgColor = row.values.backgroundColor || "";
1994
+ const colsBgColor = row.values.columnsBackgroundColor || "";
1995
+ const padding = row.values.padding || "0px";
1996
+ const totalCells = row.cells.reduce((a, b) => a + b, 0);
1997
+ const bgStyle = bgColor ? `background-color:${bgColor};` : "";
1998
+ const bgImage = row.values.backgroundImage?.url ? `background-image:url('${row.values.backgroundImage.url}');background-repeat:${row.values.backgroundImage.repeat ? "repeat" : "no-repeat"};background-position:center top;background-size:${row.values.backgroundImage.cover ? "cover" : "auto"};` : "";
1999
+ const columnsHtml = row.columns.map((col, i) => {
2000
+ const colWidthPx = Math.round(row.cells[i] / totalCells * contentWidth);
2001
+ return renderColumn(col, colWidthPx, colsBgColor, bodyValues, toolRenderers);
2002
+ });
2003
+ const needsGhostTable = row.columns.length > 1;
2004
+ let innerHtml;
2005
+ if (needsGhostTable) {
2006
+ const ghostCols = row.columns.map((col, i) => {
2007
+ const colWidthPx = Math.round(row.cells[i] / totalCells * contentWidth);
2008
+ const colHtml = renderColumn(col, colWidthPx, colsBgColor, bodyValues, toolRenderers);
2009
+ return `<!--[if (mso)|(IE)]><td align="center" width="${colWidthPx}" style="width:${colWidthPx}px;padding:0px;border:none;" valign="top"><![endif]-->
2010
+ ${colHtml}
2011
+ <!--[if (mso)|(IE)]></td><![endif]-->`;
2012
+ });
2013
+ innerHtml = `<!--[if (mso)|(IE)]><table role="presentation" width="${contentWidth}" cellpadding="0" cellspacing="0" border="0"><tr>${ghostCols.join("\n")}</tr></table><![endif]-->
2014
+
2015
+ <!--[if !mso]><!-->
2016
+ <div style="max-width:${contentWidth}px;margin:0 auto;">
2017
+ ${columnsHtml.join("\n")}
2018
+ </div>
2019
+ <!--<![endif]-->`;
2020
+ } else {
2021
+ innerHtml = columnsHtml.join("\n");
2022
+ }
2023
+ const hideDesktop = row.values.hideDesktop ? " u_hide_desktop" : "";
2024
+ const hideMobile = row.values.hideMobile ? " u_hide_mobile" : "";
2025
+ return `<div class="u_row${hideDesktop}${hideMobile}" style="padding:${padding};${bgStyle}${bgImage}">
2026
+ <div style="margin:0 auto;min-width:320px;max-width:${contentWidth}px;overflow-wrap:break-word;word-wrap:break-word;word-break:break-word;background-color:transparent;">
2027
+ <div style="border-collapse:collapse;display:table;width:100%;height:100%;background-color:transparent;">
2028
+ ${innerHtml}
2029
+ </div>
2030
+ </div>
2031
+ </div>`;
2032
+ }
2033
+ function renderColumn(col, widthPx, colsBgColor, bodyValues, toolRenderers) {
2034
+ const bgColor = col.values.backgroundColor || colsBgColor || "";
2035
+ const padding = col.values.padding || "0px";
2036
+ const borderRadius = col.values.borderRadius || "0px";
2037
+ const bgStyle = bgColor ? `background-color:${bgColor};` : "";
2038
+ const contentsHtml = col.contents.map((content) => {
2039
+ const renderer = toolRenderers.get(content.type);
2040
+ if (!renderer) return `<!-- unknown tool: ${content.type} -->`;
2041
+ const ctx = {
2042
+ columnWidth: widthPx,
2043
+ displayMode: "email",
2044
+ contentWidth: parseInt(bodyValues.contentWidth || "600"),
2045
+ bodyValues
2046
+ };
2047
+ return renderer(content.values, ctx);
2048
+ }).join("\n");
2049
+ return `<div class="u_column" style="max-width:${widthPx}px;min-width:${Math.min(widthPx, 320)}px;display:table-cell;vertical-align:top;">
2050
+ <div style="height:100%;width:100% !important;border-radius:${borderRadius};-webkit-border-radius:${borderRadius};${bgStyle}">
2051
+ <div style="box-sizing:border-box;height:100%;padding:${padding};border:none;border-radius:${borderRadius};-webkit-border-radius:${borderRadius};">
2052
+ ${contentsHtml || '<!--[if (!mso)&(!IE)]><!--><div style="height:0;min-height:1px;font-size:0;">&nbsp;</div><!--<![endif]-->'}
2053
+ </div>
2054
+ </div>
2055
+ </div>`;
2056
+ }
2057
+ function getResponsiveCss(contentWidth) {
2058
+ return `
2059
+ @media only screen and (min-width: ${contentWidth + 20}px) {
2060
+ .u_row .u_column { display: table-cell; }
2061
+ }
2062
+
2063
+ @media only screen and (max-width: ${contentWidth + 20}px) {
2064
+ .u_row .u_column {
2065
+ display: block !important;
2066
+ width: 100% !important;
2067
+ min-width: 320px !important;
2068
+ max-width: 100% !important;
2069
+ }
2070
+ .u_row {
2071
+ width: 100% !important;
2072
+ }
2073
+ }
2074
+
2075
+ @media only screen and (max-width: 620px) {
2076
+ .u_row-container {
2077
+ max-width: 100% !important;
2078
+ padding-left: 0 !important;
2079
+ padding-right: 0 !important;
2080
+ }
2081
+ }
2082
+
2083
+ @media (prefers-color-scheme: dark) {
2084
+ /* Dark mode overrides — add per-client rules as needed */
2085
+ }
2086
+
2087
+ /* Outlook dark mode */
2088
+ [data-ogsb] body,
2089
+ [data-ogsb] table,
2090
+ [data-ogsb] td {
2091
+ /* Preserve original colors */
2092
+ }
2093
+
2094
+ .u_hide_desktop { display: block !important; }
2095
+ .u_hide_mobile { display: block !important; }
2096
+
2097
+ @media only screen and (max-width: ${contentWidth + 20}px) {
2098
+ .u_hide_desktop { display: block !important; }
2099
+ .u_hide_mobile { display: none !important; }
2100
+ }
2101
+
2102
+ @media only screen and (min-width: ${contentWidth + 21}px) {
2103
+ .u_hide_desktop { display: none !important; }
2104
+ .u_hide_mobile { display: block !important; }
2105
+ }`;
2106
+ }
2107
+ function renderDesignToHtml(design, toolRenderers, options) {
2108
+ const bodyValues = design.body.values;
2109
+ const contentWidth = parseInt(bodyValues.contentWidth || "600");
2110
+ const rowsHtml = design.body.rows.map((row) => renderRow(row, bodyValues, toolRenderers)).join("\n");
2111
+ const cssBlock = getResponsiveCss(contentWidth);
2112
+ let fullHtml = wrapInDocumentShell(rowsHtml, cssBlock, bodyValues);
2113
+ if (options?.mergeTags) {
2114
+ for (const [tag, value] of Object.entries(options.mergeTags)) {
2115
+ fullHtml = fullHtml.replaceAll(`{{${tag}}}`, value);
2116
+ }
2117
+ }
2118
+ const bodyMatch = fullHtml.match(/<body[^>]*>([\s\S]*)<\/body>/i);
2119
+ const cssMatch = fullHtml.match(/<style[^>]*>([\s\S]*?)<\/style>/gi);
2120
+ const fontsUsed = [];
2121
+ if (bodyValues.fontFamily?.url) {
2122
+ fontsUsed.push(bodyValues.fontFamily.url);
2123
+ }
2124
+ return {
2125
+ design: structuredClone(design),
2126
+ html: fullHtml,
2127
+ chunks: {
2128
+ body: bodyMatch?.[1] ?? rowsHtml,
2129
+ css: cssMatch?.map((s) => s.replace(/<\/?style[^>]*>/gi, "")).join("\n") ?? cssBlock,
2130
+ fonts: fontsUsed,
2131
+ js: ""
2132
+ }
2133
+ };
2134
+ }
2135
+ var __defProp = Object.defineProperty;
2136
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
2137
+ var __decorateClass = (decorators, target, key, kind) => {
2138
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
2139
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
2140
+ if (decorator = decorators[i])
2141
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
2142
+ if (kind && result) __defProp(target, key, result);
2143
+ return result;
2144
+ };
2145
+ let MailEditorElement = class extends LitElement {
2146
+ constructor() {
2147
+ super(...arguments);
2148
+ this.options = {};
2149
+ this.store = new EditorStore();
2150
+ this.toolRegistry = new ToolRegistry();
2151
+ this.dragManager = null;
2152
+ this.callbacks = /* @__PURE__ */ new Map();
2153
+ this.unsubscribe = null;
2154
+ }
2155
+ connectedCallback() {
2156
+ super.connectedCallback();
2157
+ this.registerBuiltInTools();
2158
+ this.applyOptions();
2159
+ }
2160
+ firstUpdated() {
2161
+ this.dragManager = new DragManager(this.store, this.toolRegistry, this.shadowRoot);
2162
+ this.dragManager.attach();
2163
+ this.unsubscribe = this.store.subscribe(() => this.requestUpdate());
2164
+ this.store.events.on("design:loaded", (detail) => {
2165
+ this.dispatchEvent(new CustomEvent("design:loaded", { detail, bubbles: true, composed: true }));
2166
+ });
2167
+ this.store.events.on("design:updated", (detail) => {
2168
+ this.dispatchEvent(new CustomEvent("design:updated", { detail, bubbles: true, composed: true }));
2169
+ });
2170
+ this.dispatchEvent(new CustomEvent("editor:ready", { bubbles: true, composed: true }));
2171
+ }
2172
+ disconnectedCallback() {
2173
+ super.disconnectedCallback();
2174
+ this.dragManager?.detach();
2175
+ this.unsubscribe?.();
2176
+ this.store.events.removeAllListeners();
2177
+ }
2178
+ // ----------------------------------------------------------
2179
+ // Public API — mirrors Unlayer
2180
+ // ----------------------------------------------------------
2181
+ loadDesign(design) {
2182
+ this.store.loadDesign(design);
2183
+ }
2184
+ saveDesign(callback) {
2185
+ callback(structuredClone(this.store.getDesign()));
2186
+ }
2187
+ exportHtml(callback, options) {
2188
+ const design = this.store.getDesign();
2189
+ const toolRenderers = /* @__PURE__ */ new Map();
2190
+ for (const tool of this.toolRegistry.getAll()) {
2191
+ toolRenderers.set(tool.name, (values, ctx) => tool.renderer.renderHtml(values, ctx));
2192
+ }
2193
+ const result = renderDesignToHtml(design, toolRenderers, options);
2194
+ callback(result);
2195
+ }
2196
+ async exportHtmlAsync(options) {
2197
+ return new Promise((resolve) => this.exportHtml(resolve, options));
2198
+ }
2199
+ registerTool(definition) {
2200
+ this.toolRegistry.register(definition);
2201
+ this.requestUpdate();
2202
+ }
2203
+ registerPropertyEditor(_name, _editor) {
2204
+ }
2205
+ registerTab(_tab) {
2206
+ }
2207
+ registerCallback(type, callback) {
2208
+ this.callbacks.set(type, callback);
2209
+ }
2210
+ setMergeTags(_tags) {
2211
+ }
2212
+ undo() {
2213
+ this.store.undo();
2214
+ }
2215
+ redo() {
2216
+ this.store.redo();
2217
+ }
2218
+ setBodyValues(values) {
2219
+ this.store.updateBodyValues(values);
2220
+ }
2221
+ // ----------------------------------------------------------
2222
+ // Internal
2223
+ // ----------------------------------------------------------
2224
+ registerBuiltInTools() {
2225
+ this.toolRegistry.register(textTool);
2226
+ this.toolRegistry.register(headingTool);
2227
+ this.toolRegistry.register(paragraphTool);
2228
+ this.toolRegistry.register(imageTool);
2229
+ this.toolRegistry.register(buttonTool);
2230
+ this.toolRegistry.register(dividerTool);
2231
+ this.toolRegistry.register(htmlTool);
2232
+ this.toolRegistry.register(socialTool);
2233
+ this.toolRegistry.register(menuTool);
2234
+ this.toolRegistry.register(videoTool);
2235
+ this.toolRegistry.register(timerTool);
2236
+ this.toolRegistry.register(tableTool);
2237
+ this.toolRegistry.register(formTool);
2238
+ }
2239
+ applyOptions() {
2240
+ if (this.options.design) {
2241
+ this.store.loadDesign(this.options.design);
2242
+ }
2243
+ }
2244
+ render() {
2245
+ return html`
2246
+ <me-editor-sidebar
2247
+ .store=${this.store}
2248
+ .toolRegistry=${this.toolRegistry}
2249
+ ></me-editor-sidebar>
2250
+ <me-editor-canvas
2251
+ .store=${this.store}
2252
+ .toolRegistry=${this.toolRegistry}
2253
+ ></me-editor-canvas>
2254
+ <me-property-panel
2255
+ .store=${this.store}
2256
+ .toolRegistry=${this.toolRegistry}
2257
+ ></me-property-panel>
2258
+ `;
2259
+ }
2260
+ };
2261
+ MailEditorElement.styles = css`
2262
+ :host {
2263
+ display: flex;
2264
+ width: 100%;
2265
+ height: 100%;
2266
+ min-height: 500px;
2267
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2268
+ color: #111827;
2269
+ box-sizing: border-box;
2270
+ overflow: hidden;
2271
+ position: relative;
2272
+ }
2273
+ * { box-sizing: border-box; }
2274
+ `;
2275
+ __decorateClass([
2276
+ property({ type: Object })
2277
+ ], MailEditorElement.prototype, "options", 2);
2278
+ MailEditorElement = __decorateClass([
2279
+ customElement("mail-editor")
2280
+ ], MailEditorElement);
2281
+ export {
2282
+ EditorStore as E,
2283
+ MailEditorElement as M,
2284
+ ToolRegistry as T
2285
+ };
2286
+ //# sourceMappingURL=mail-editor-DzI7SmSe.js.map