@emabuild/core 0.0.2 → 0.0.4

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 (74) hide show
  1. package/README.md +410 -0
  2. package/dist/dnd/drag-manager.d.ts +13 -14
  3. package/dist/dnd/drag-manager.d.ts.map +1 -1
  4. package/dist/dnd/drop-indicator.d.ts +24 -0
  5. package/dist/dnd/drop-indicator.d.ts.map +1 -0
  6. package/dist/dnd/shadow-dom-utils.d.ts +29 -0
  7. package/dist/dnd/shadow-dom-utils.d.ts.map +1 -0
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/{mail-editor-CoB2C5CT.js → mail-editor-D0FbEUZu.js} +677 -870
  12. package/dist/mail-editor-D0FbEUZu.js.map +1 -0
  13. package/dist/mail-editor.d.ts +3 -3
  14. package/dist/mail-editor.d.ts.map +1 -1
  15. package/dist/mail-editor.js +1 -1
  16. package/dist/properties/property-panel.d.ts +4 -9
  17. package/dist/properties/property-panel.d.ts.map +1 -1
  18. package/dist/properties/widgets/alignment-widget.d.ts +4 -0
  19. package/dist/properties/widgets/alignment-widget.d.ts.map +1 -0
  20. package/dist/properties/widgets/color-picker-widget.d.ts +4 -0
  21. package/dist/properties/widgets/color-picker-widget.d.ts.map +1 -0
  22. package/dist/properties/widgets/dropdown-widget.d.ts +4 -0
  23. package/dist/properties/widgets/dropdown-widget.d.ts.map +1 -0
  24. package/dist/properties/widgets/index.d.ts +8 -0
  25. package/dist/properties/widgets/index.d.ts.map +1 -0
  26. package/dist/properties/widgets/padding-widget.d.ts +4 -0
  27. package/dist/properties/widgets/padding-widget.d.ts.map +1 -0
  28. package/dist/properties/widgets/text-input-widget.d.ts +4 -0
  29. package/dist/properties/widgets/text-input-widget.d.ts.map +1 -0
  30. package/dist/properties/widgets/textarea-widget.d.ts +4 -0
  31. package/dist/properties/widgets/textarea-widget.d.ts.map +1 -0
  32. package/dist/properties/widgets/toggle-widget.d.ts +4 -0
  33. package/dist/properties/widgets/toggle-widget.d.ts.map +1 -0
  34. package/dist/sidebar/body-settings.d.ts +15 -0
  35. package/dist/sidebar/body-settings.d.ts.map +1 -0
  36. package/dist/sidebar/editor-sidebar.d.ts +0 -3
  37. package/dist/sidebar/editor-sidebar.d.ts.map +1 -1
  38. package/dist/state/design-factory.d.ts +9 -0
  39. package/dist/state/design-factory.d.ts.map +1 -0
  40. package/dist/state/design-lookup.d.ts +14 -0
  41. package/dist/state/design-lookup.d.ts.map +1 -0
  42. package/dist/state/editor-store.d.ts +27 -10
  43. package/dist/state/editor-store.d.ts.map +1 -1
  44. package/dist/state/history-manager.d.ts +20 -0
  45. package/dist/state/history-manager.d.ts.map +1 -0
  46. package/dist/tools/built-in/button-tool.d.ts.map +1 -1
  47. package/dist/tools/built-in/divider-tool.d.ts.map +1 -1
  48. package/dist/tools/built-in/form-tool.d.ts.map +1 -1
  49. package/dist/tools/built-in/heading-tool.d.ts.map +1 -1
  50. package/dist/tools/built-in/html-tool.d.ts.map +1 -1
  51. package/dist/tools/built-in/image-tool.d.ts.map +1 -1
  52. package/dist/tools/built-in/menu-tool.d.ts.map +1 -1
  53. package/dist/tools/built-in/paragraph-tool.d.ts.map +1 -1
  54. package/dist/tools/built-in/social-tool.d.ts.map +1 -1
  55. package/dist/tools/built-in/table-tool.d.ts.map +1 -1
  56. package/dist/tools/built-in/text-tool.d.ts.map +1 -1
  57. package/dist/tools/built-in/timer-tool.d.ts.map +1 -1
  58. package/dist/tools/built-in/video-tool.d.ts.map +1 -1
  59. package/dist/tools/helpers/email-html.d.ts +55 -0
  60. package/dist/tools/helpers/email-html.d.ts.map +1 -0
  61. package/dist/tools/helpers/index.d.ts +5 -0
  62. package/dist/tools/helpers/index.d.ts.map +1 -0
  63. package/dist/tools/helpers/types.d.ts +36 -0
  64. package/dist/tools/helpers/types.d.ts.map +1 -0
  65. package/dist/tools/helpers/value-extractor.d.ts +31 -0
  66. package/dist/tools/helpers/value-extractor.d.ts.map +1 -0
  67. package/dist/utils/event-emitter.d.ts +32 -4
  68. package/dist/utils/event-emitter.d.ts.map +1 -1
  69. package/dist/utils/id-generator.d.ts +17 -3
  70. package/dist/utils/id-generator.d.ts.map +1 -1
  71. package/package.json +16 -4
  72. package/dist/mail-editor-CoB2C5CT.js.map +0 -1
  73. package/dist/utils/deep-clone.d.ts +0 -2
  74. package/dist/utils/deep-clone.d.ts.map +0 -1
@@ -1,9 +1,6 @@
1
1
  import { html, css, LitElement } from "lit";
2
2
  import { property, customElement } from "lit/decorators.js";
3
3
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
4
- function deepClone(obj) {
5
- return structuredClone(obj);
6
- }
7
4
  function createCounterManager() {
8
5
  const counters = {};
9
6
  return {
@@ -11,7 +8,9 @@ function createCounterManager() {
11
8
  return { ...counters };
12
9
  },
13
10
  setCounters(c) {
14
- Object.keys(counters).forEach((k) => delete counters[k]);
11
+ for (const key of Object.keys(counters)) {
12
+ delete counters[key];
13
+ }
15
14
  Object.assign(counters, c);
16
15
  },
17
16
  next(prefix) {
@@ -25,24 +24,28 @@ class EventEmitter {
25
24
  constructor() {
26
25
  this.listeners = /* @__PURE__ */ new Map();
27
26
  }
27
+ /** Register a listener for an event */
28
28
  on(event, listener) {
29
29
  if (!this.listeners.has(event)) {
30
30
  this.listeners.set(event, /* @__PURE__ */ new Set());
31
31
  }
32
32
  this.listeners.get(event).add(listener);
33
33
  }
34
+ /** Remove a specific listener */
34
35
  off(event, listener) {
35
36
  this.listeners.get(event)?.delete(listener);
36
37
  }
37
- emit(event, ...args) {
38
+ /** Emit an event with a payload. Errors in listeners are caught and logged. */
39
+ emit(event, payload) {
38
40
  this.listeners.get(event)?.forEach((fn) => {
39
41
  try {
40
- fn(...args);
42
+ fn(payload);
41
43
  } catch (e) {
42
- console.error(`[maileditor] Error in "${event}" listener:`, e);
44
+ console.error(`[emabuild] Error in "${event}" listener:`, e);
43
45
  }
44
46
  });
45
47
  }
48
+ /** Remove all listeners, optionally scoped to a single event */
46
49
  removeAllListeners(event) {
47
50
  if (event) {
48
51
  this.listeners.delete(event);
@@ -51,6 +54,48 @@ class EventEmitter {
51
54
  }
52
55
  }
53
56
  }
57
+ class HistoryManager {
58
+ constructor(maxHistory = 50) {
59
+ this.undoStack = [];
60
+ this.redoStack = [];
61
+ this.maxHistory = maxHistory;
62
+ }
63
+ /** Whether there are states to undo to */
64
+ get canUndo() {
65
+ return this.undoStack.length > 0;
66
+ }
67
+ /** Whether there are states to redo to */
68
+ get canRedo() {
69
+ return this.redoStack.length > 0;
70
+ }
71
+ /** Save current design to the undo stack before a mutation */
72
+ push(design) {
73
+ this.undoStack.push(structuredClone(design));
74
+ if (this.undoStack.length > this.maxHistory) {
75
+ this.undoStack.shift();
76
+ }
77
+ this.redoStack = [];
78
+ }
79
+ /** Restore the previous state, pushing current state to redo. Returns the restored design or undefined. */
80
+ undo(currentDesign) {
81
+ const prev = this.undoStack.pop();
82
+ if (!prev) return void 0;
83
+ this.redoStack.push(structuredClone(currentDesign));
84
+ return prev;
85
+ }
86
+ /** Restore the next state, pushing current state to undo. Returns the restored design or undefined. */
87
+ redo(currentDesign) {
88
+ const next = this.redoStack.pop();
89
+ if (!next) return void 0;
90
+ this.undoStack.push(structuredClone(currentDesign));
91
+ return next;
92
+ }
93
+ /** Clear all history (e.g. when loading a new design) */
94
+ clear() {
95
+ this.undoStack = [];
96
+ this.redoStack = [];
97
+ }
98
+ }
54
99
  function createEmptyDesign() {
55
100
  return {
56
101
  counters: { u_row: 0, u_column: 0 },
@@ -96,13 +141,98 @@ function createEmptyDesign() {
96
141
  schemaVersion: 16
97
142
  };
98
143
  }
144
+ function createRow(cm, cellProportions) {
145
+ const rowNum = cm.next("u_row");
146
+ const columns = cellProportions.map(() => {
147
+ const colNum = cm.next("u_column");
148
+ return {
149
+ id: `u_column_${colNum}`,
150
+ contents: [],
151
+ values: {
152
+ backgroundColor: "",
153
+ padding: "0px",
154
+ border: {},
155
+ borderRadius: "0px",
156
+ _meta: { htmlID: `u_column_${colNum}`, htmlClassNames: "u_column" }
157
+ }
158
+ };
159
+ });
160
+ return {
161
+ id: `u_row_${rowNum}`,
162
+ cells: cellProportions,
163
+ columns,
164
+ values: {
165
+ displayCondition: null,
166
+ columns: false,
167
+ backgroundColor: "",
168
+ columnsBackgroundColor: "",
169
+ backgroundImage: { url: "", fullWidth: true, repeat: false, center: true, cover: false },
170
+ padding: "0px",
171
+ anchor: "",
172
+ hideDesktop: false,
173
+ hideMobile: false,
174
+ _meta: { htmlID: `u_row_${rowNum}`, htmlClassNames: "u_row" }
175
+ }
176
+ };
177
+ }
178
+ function createContent(cm, type, values = {}) {
179
+ const counter = cm.next(`u_content_${type}`);
180
+ const id = `u_content_${type}_${counter}`;
181
+ return {
182
+ id,
183
+ type,
184
+ values: {
185
+ containerPadding: "10px",
186
+ anchor: "",
187
+ hideDesktop: false,
188
+ hideMobile: false,
189
+ displayCondition: null,
190
+ _meta: { htmlID: id, htmlClassNames: `u_content_${type}` },
191
+ ...values
192
+ }
193
+ };
194
+ }
195
+ function findRow(design, rowId) {
196
+ return design.body.rows.find((r) => r.id === rowId);
197
+ }
198
+ function findColumn(design, columnId) {
199
+ for (const row of design.body.rows) {
200
+ const col = row.columns.find((c) => c.id === columnId);
201
+ if (col) return col;
202
+ }
203
+ return void 0;
204
+ }
205
+ function findContent(design, contentId) {
206
+ for (const row of design.body.rows) {
207
+ for (const col of row.columns) {
208
+ const content = col.contents.find((c) => c.id === contentId);
209
+ if (content) return content;
210
+ }
211
+ }
212
+ return void 0;
213
+ }
214
+ function findParentColumn(design, contentId) {
215
+ for (const row of design.body.rows) {
216
+ for (const col of row.columns) {
217
+ if (col.contents.some((c) => c.id === contentId)) return col;
218
+ }
219
+ }
220
+ return void 0;
221
+ }
222
+ function findParentRow(design, columnId) {
223
+ for (const row of design.body.rows) {
224
+ if (row.columns.some((c) => c.id === columnId)) return row;
225
+ }
226
+ return void 0;
227
+ }
228
+ function getRowIndex(design, rowId) {
229
+ return design.body.rows.findIndex((r) => r.id === rowId);
230
+ }
99
231
  class EditorStore {
100
232
  constructor() {
101
- this.undoStack = [];
102
- this.redoStack = [];
103
- this.maxHistory = 50;
104
- this.subscribers = /* @__PURE__ */ new Set();
233
+ this.history = new HistoryManager();
105
234
  this.counterManager = createCounterManager();
235
+ this.subscribers = /* @__PURE__ */ new Set();
106
236
  this.events = new EventEmitter();
107
237
  this._selectedId = null;
108
238
  this._hoveredId = null;
@@ -110,9 +240,8 @@ class EditorStore {
110
240
  this._activeTab = "content";
111
241
  this.design = createEmptyDesign();
112
242
  }
113
- // ----------------------------------------------------------
114
- // Subscriptions (for Lit reactive controllers)
115
- // ----------------------------------------------------------
243
+ // ── Subscriptions ──────────────────────────────────────────
244
+ /** Subscribe to all state changes. Returns an unsubscribe function. */
116
245
  subscribe(fn) {
117
246
  this.subscribers.add(fn);
118
247
  return () => this.subscribers.delete(fn);
@@ -120,18 +249,20 @@ class EditorStore {
120
249
  notify() {
121
250
  this.subscribers.forEach((fn) => fn());
122
251
  }
123
- // ----------------------------------------------------------
124
- // Getters
125
- // ----------------------------------------------------------
252
+ // ── Getters ────────────────────────────────────────────────
253
+ /** Get the full design document */
126
254
  getDesign() {
127
255
  return this.design;
128
256
  }
257
+ /** Get the design body */
129
258
  getBody() {
130
259
  return this.design.body;
131
260
  }
261
+ /** Get all rows */
132
262
  getRows() {
133
263
  return this.design.body.rows;
134
264
  }
265
+ /** Get body-level values (background, fonts, etc.) */
135
266
  getBodyValues() {
136
267
  return this.design.body.values;
137
268
  }
@@ -148,54 +279,39 @@ class EditorStore {
148
279
  return this._activeTab;
149
280
  }
150
281
  get canUndo() {
151
- return this.undoStack.length > 0;
282
+ return this.history.canUndo;
152
283
  }
153
284
  get canRedo() {
154
- return this.redoStack.length > 0;
285
+ return this.history.canRedo;
155
286
  }
156
- // ----------------------------------------------------------
157
- // Design loading
158
- // ----------------------------------------------------------
287
+ // ── Design Loading ─────────────────────────────────────────
288
+ /** Load a design document, resetting history and selection */
159
289
  loadDesign(design) {
160
- this.design = deepClone(design);
290
+ this.design = structuredClone(design);
161
291
  this.counterManager.setCounters(this.design.counters);
162
- this.undoStack = [];
163
- this.redoStack = [];
292
+ this.history.clear();
164
293
  this._selectedId = null;
165
294
  this.notify();
166
295
  this.events.emit("design:loaded", { design: this.design });
167
296
  }
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
- }
297
+ // ── Undo / Redo ────────────────────────────────────────────
178
298
  undo() {
179
- const prev = this.undoStack.pop();
299
+ const prev = this.history.undo(this.design);
180
300
  if (!prev) return;
181
- this.redoStack.push(deepClone(this.design));
182
301
  this.design = prev;
183
302
  this.counterManager.setCounters(this.design.counters);
184
303
  this.notify();
185
304
  this.emitUpdate("content_updated");
186
305
  }
187
306
  redo() {
188
- const next = this.redoStack.pop();
307
+ const next = this.history.redo(this.design);
189
308
  if (!next) return;
190
- this.undoStack.push(deepClone(this.design));
191
309
  this.design = next;
192
310
  this.counterManager.setCounters(this.design.counters);
193
311
  this.notify();
194
312
  this.emitUpdate("content_updated");
195
313
  }
196
- // ----------------------------------------------------------
197
- // Selection / UI state
198
- // ----------------------------------------------------------
314
+ // ── Selection / UI State ───────────────────────────────────
199
315
  select(id) {
200
316
  this._selectedId = id;
201
317
  this.notify();
@@ -212,45 +328,46 @@ class EditorStore {
212
328
  this._activeTab = tab;
213
329
  this.notify();
214
330
  }
215
- // ----------------------------------------------------------
216
- // Row operations
217
- // ----------------------------------------------------------
331
+ // ── Row Operations ─────────────────────────────────────────
332
+ /** Add a row at the given index (or at the end) */
218
333
  addRow(row, index) {
219
- this.pushHistory();
334
+ this.history.push(this.design);
220
335
  const rows = this.design.body.rows;
336
+ const cloned = structuredClone(row);
221
337
  if (index !== void 0 && index >= 0 && index <= rows.length) {
222
- rows.splice(index, 0, deepClone(row));
338
+ rows.splice(index, 0, cloned);
223
339
  } else {
224
- rows.push(deepClone(row));
340
+ rows.push(cloned);
225
341
  }
226
342
  this.syncCounters();
227
343
  this.notify();
228
- this.emitUpdate("row_added", row);
344
+ this.emitUpdate("row_added", cloned);
229
345
  }
346
+ /** Remove a row by ID */
230
347
  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
- }
348
+ const idx = getRowIndex(this.design, rowId);
349
+ if (idx === -1) return;
350
+ this.history.push(this.design);
351
+ this.design.body.rows.splice(idx, 1);
352
+ if (this._selectedId === rowId) this._selectedId = null;
353
+ this.notify();
354
+ this.emitUpdate("row_removed");
240
355
  }
356
+ /** Move a row from one index to another */
241
357
  moveRow(fromIndex, toIndex) {
242
- this.pushHistory();
358
+ this.history.push(this.design);
243
359
  const rows = this.design.body.rows;
244
360
  const [row] = rows.splice(fromIndex, 1);
245
361
  rows.splice(toIndex, 0, row);
246
362
  this.notify();
247
363
  this.emitUpdate("row_reordered");
248
364
  }
365
+ /** Duplicate a row, assigning fresh IDs to all nested elements */
249
366
  duplicateRow(rowId) {
250
- const row = this.findRow(rowId);
367
+ const row = findRow(this.design, rowId);
251
368
  if (!row) return;
252
- this.pushHistory();
253
- const cloned = deepClone(row);
369
+ this.history.push(this.design);
370
+ const cloned = structuredClone(row);
254
371
  const rowNum = this.counterManager.next("u_row");
255
372
  cloned.id = `u_row_${rowNum}`;
256
373
  cloned.values._meta = { htmlID: cloned.id, htmlClassNames: "u_row" };
@@ -264,43 +381,42 @@ class EditorStore {
264
381
  content.values._meta = { htmlID: content.id, htmlClassNames: `u_content_${content.type}` };
265
382
  }
266
383
  }
267
- const rows = this.design.body.rows;
268
- const idx = rows.findIndex((r) => r.id === rowId);
269
- rows.splice(idx + 1, 0, cloned);
384
+ const idx = getRowIndex(this.design, rowId);
385
+ this.design.body.rows.splice(idx + 1, 0, cloned);
270
386
  this.syncCounters();
271
387
  this.notify();
272
388
  this.emitUpdate("row_added", cloned);
273
389
  }
390
+ /** Get the index of a row */
274
391
  getRowIndex(rowId) {
275
- return this.design.body.rows.findIndex((r) => r.id === rowId);
392
+ return getRowIndex(this.design, rowId);
276
393
  }
394
+ /** Update row-level values */
277
395
  updateRowValues(rowId, patch) {
278
- const row = this.findRow(rowId);
396
+ const row = findRow(this.design, rowId);
279
397
  if (!row) return;
280
- this.pushHistory();
398
+ this.history.push(this.design);
281
399
  Object.assign(row.values, patch);
282
400
  this.notify();
283
401
  this.emitUpdate("content_updated");
284
402
  }
285
- // ----------------------------------------------------------
286
- // Column operations
287
- // ----------------------------------------------------------
403
+ // ── Column Operations ──────────────────────────────────────
404
+ /** Update column-level values */
288
405
  updateColumnValues(columnId, patch) {
289
- const col = this.findColumn(columnId);
406
+ const col = findColumn(this.design, columnId);
290
407
  if (!col) return;
291
- this.pushHistory();
408
+ this.history.push(this.design);
292
409
  Object.assign(col.values, patch);
293
410
  this.notify();
294
411
  this.emitUpdate("content_updated");
295
412
  }
296
- // ----------------------------------------------------------
297
- // Content operations
298
- // ----------------------------------------------------------
413
+ // ── Content Operations ─────────────────────────────────────
414
+ /** Add content to a column at the given index */
299
415
  addContent(columnId, content, index) {
300
- const col = this.findColumn(columnId);
416
+ const col = findColumn(this.design, columnId);
301
417
  if (!col) return;
302
- this.pushHistory();
303
- const cloned = deepClone(content);
418
+ this.history.push(this.design);
419
+ const cloned = structuredClone(content);
304
420
  if (index !== void 0 && index >= 0 && index <= col.contents.length) {
305
421
  col.contents.splice(index, 0, cloned);
306
422
  } else {
@@ -310,12 +426,13 @@ class EditorStore {
310
426
  this.notify();
311
427
  this.emitUpdate("content_added", cloned);
312
428
  }
429
+ /** Remove a content block by ID */
313
430
  removeContent(contentId) {
314
431
  for (const row of this.design.body.rows) {
315
432
  for (const col of row.columns) {
316
433
  const idx = col.contents.findIndex((c) => c.id === contentId);
317
434
  if (idx !== -1) {
318
- this.pushHistory();
435
+ this.history.push(this.design);
319
436
  col.contents.splice(idx, 1);
320
437
  if (this._selectedId === contentId) this._selectedId = null;
321
438
  this.notify();
@@ -325,49 +442,47 @@ class EditorStore {
325
442
  }
326
443
  }
327
444
  }
445
+ /** Update content values by ID */
328
446
  updateContentValues(contentId, patch) {
329
- const content = this.findContent(contentId);
447
+ const content = findContent(this.design, contentId);
330
448
  if (!content) return;
331
- this.pushHistory();
449
+ this.history.push(this.design);
332
450
  Object.assign(content.values, patch);
333
451
  this.notify();
334
452
  this.emitUpdate("content_updated");
335
453
  }
454
+ /** Move a content block to a different column at a given index */
336
455
  moveContent(contentId, targetColumnId, targetIndex) {
337
- const content = this.findContent(contentId);
456
+ const content = findContent(this.design, contentId);
338
457
  if (!content) return;
458
+ this.history.push(this.design);
339
459
  for (const row of this.design.body.rows) {
340
460
  for (const col of row.columns) {
341
461
  const idx = col.contents.findIndex((c) => c.id === contentId);
342
462
  if (idx !== -1) {
343
- this.pushHistory();
344
463
  col.contents.splice(idx, 1);
345
464
  break;
346
465
  }
347
466
  }
348
467
  }
349
- const targetCol = this.findColumn(targetColumnId);
350
- if (targetCol) {
351
- targetCol.contents.splice(targetIndex, 0, content);
352
- }
468
+ const targetCol = findColumn(this.design, targetColumnId);
469
+ if (targetCol) targetCol.contents.splice(targetIndex, 0, content);
353
470
  this.notify();
354
471
  this.emitUpdate("content_reordered");
355
472
  }
473
+ /** Duplicate a content block, inserting the copy right after the original */
356
474
  duplicateContent(contentId) {
357
- const content = this.findContent(contentId);
475
+ const content = findContent(this.design, contentId);
358
476
  if (!content) return;
359
477
  for (const row of this.design.body.rows) {
360
478
  for (const col of row.columns) {
361
479
  const idx = col.contents.findIndex((c) => c.id === contentId);
362
480
  if (idx !== -1) {
363
- const cloned = deepClone(content);
481
+ this.history.push(this.design);
482
+ const cloned = structuredClone(content);
364
483
  const counter = this.counterManager.next(`u_content_${content.type}`);
365
484
  cloned.id = `u_content_${content.type}_${counter}`;
366
- cloned.values._meta = {
367
- htmlID: cloned.id,
368
- htmlClassNames: `u_content_${content.type}`
369
- };
370
- this.pushHistory();
485
+ cloned.values._meta = { htmlID: cloned.id, htmlClassNames: `u_content_${content.type}` };
371
486
  col.contents.splice(idx + 1, 0, cloned);
372
487
  this.syncCounters();
373
488
  this.notify();
@@ -377,109 +492,44 @@ class EditorStore {
377
492
  }
378
493
  }
379
494
  }
380
- // ----------------------------------------------------------
381
- // Body values
382
- // ----------------------------------------------------------
495
+ // ── Body Values ────────────────────────────────────────────
496
+ /** Update body-level values (background, fonts, etc.) */
383
497
  updateBodyValues(patch) {
384
- this.pushHistory();
498
+ this.history.push(this.design);
385
499
  Object.assign(this.design.body.values, patch);
386
500
  this.notify();
387
501
  this.emitUpdate("body_updated");
388
502
  }
389
- // ----------------------------------------------------------
390
- // Lookup helpers
391
- // ----------------------------------------------------------
503
+ // ── Lookups (delegate to design-lookup) ────────────────────
392
504
  findRow(rowId) {
393
- return this.design.body.rows.find((r) => r.id === rowId);
505
+ return findRow(this.design, rowId);
394
506
  }
395
507
  findColumn(columnId) {
396
- for (const row of this.design.body.rows) {
397
- const col = row.columns.find((c) => c.id === columnId);
398
- if (col) return col;
399
- }
400
- return void 0;
508
+ return findColumn(this.design, columnId);
401
509
  }
402
510
  findContent(contentId) {
403
- for (const row of this.design.body.rows) {
404
- for (const col of row.columns) {
405
- const content = col.contents.find((c) => c.id === contentId);
406
- if (content) return content;
407
- }
408
- }
409
- return void 0;
511
+ return findContent(this.design, contentId);
410
512
  }
411
513
  findParentColumn(contentId) {
412
- for (const row of this.design.body.rows) {
413
- for (const col of row.columns) {
414
- if (col.contents.some((c) => c.id === contentId)) return col;
415
- }
416
- }
417
- return void 0;
514
+ return findParentColumn(this.design, contentId);
418
515
  }
419
516
  findParentRow(columnId) {
420
- for (const row of this.design.body.rows) {
421
- if (row.columns.some((c) => c.id === columnId)) return row;
422
- }
423
- return void 0;
517
+ return findParentRow(this.design, columnId);
424
518
  }
425
- // ----------------------------------------------------------
426
- // Helpers
427
- // ----------------------------------------------------------
428
- /** Creates a new row with the given column layout */
519
+ // ── Factory Methods (delegate to design-factory) ───────────
520
+ /** Create a new row with the given column layout */
429
521
  createRow(cellProportions) {
430
- const rowNum = this.counterManager.next("u_row");
431
- const columns = cellProportions.map((_, i) => {
432
- const colNum = this.counterManager.next("u_column");
433
- return {
434
- id: `u_column_${colNum}`,
435
- contents: [],
436
- values: {
437
- backgroundColor: "",
438
- padding: "0px",
439
- border: {},
440
- borderRadius: "0px",
441
- _meta: { htmlID: `u_column_${colNum}`, htmlClassNames: "u_column" }
442
- }
443
- };
444
- });
522
+ const row = createRow(this.counterManager, cellProportions);
445
523
  this.syncCounters();
446
- return {
447
- id: `u_row_${rowNum}`,
448
- cells: cellProportions,
449
- columns,
450
- values: {
451
- displayCondition: null,
452
- columns: false,
453
- backgroundColor: "",
454
- columnsBackgroundColor: "",
455
- backgroundImage: { url: "", fullWidth: true, repeat: false, center: true, cover: false },
456
- padding: "0px",
457
- anchor: "",
458
- hideDesktop: false,
459
- hideMobile: false,
460
- _meta: { htmlID: `u_row_${rowNum}`, htmlClassNames: "u_row" }
461
- }
462
- };
524
+ return row;
463
525
  }
464
- /** Creates a new content block for the given tool type */
526
+ /** Create a new content block for the given tool type */
465
527
  createContent(type, values = {}) {
466
- const counter = this.counterManager.next(`u_content_${type}`);
467
- const id = `u_content_${type}_${counter}`;
528
+ const content = createContent(this.counterManager, type, values);
468
529
  this.syncCounters();
469
- return {
470
- id,
471
- type,
472
- values: {
473
- containerPadding: "10px",
474
- anchor: "",
475
- hideDesktop: false,
476
- hideMobile: false,
477
- displayCondition: null,
478
- _meta: { htmlID: id, htmlClassNames: `u_content_${type}` },
479
- ...values
480
- }
481
- };
530
+ return content;
482
531
  }
532
+ // ── Private Helpers ────────────────────────────────────────
483
533
  syncCounters() {
484
534
  this.design.counters = this.counterManager.getCounters();
485
535
  }
@@ -530,14 +580,72 @@ const dragState = {
530
580
  this.draggingContentId = null;
531
581
  }
532
582
  };
583
+ function createDropIndicator(color) {
584
+ const el = document.createElement("div");
585
+ Object.assign(el.style, {
586
+ position: "absolute",
587
+ left: "0",
588
+ right: "0",
589
+ height: "3px",
590
+ background: color,
591
+ borderRadius: "2px",
592
+ pointerEvents: "none",
593
+ zIndex: "1000",
594
+ display: "none",
595
+ boxShadow: `0 0 6px ${color}80`
596
+ });
597
+ return el;
598
+ }
599
+ function positionIndicator(indicator, container, items, index, insetX = "4px") {
600
+ if (indicator.parentNode !== container) {
601
+ indicator.remove();
602
+ container.appendChild(indicator);
603
+ }
604
+ const containerEl = container instanceof ShadowRoot ? container.host : container;
605
+ const containerRect = containerEl.getBoundingClientRect();
606
+ let indicatorY;
607
+ if (items.length === 0 || index === 0) {
608
+ indicatorY = items.length === 0 ? 0 : items[0].getBoundingClientRect().top - containerRect.top;
609
+ } else if (index >= items.length) {
610
+ const lastRect = items[items.length - 1].getBoundingClientRect();
611
+ indicatorY = lastRect.bottom - containerRect.top;
612
+ } else {
613
+ const elRect = items[index].getBoundingClientRect();
614
+ indicatorY = elRect.top - containerRect.top;
615
+ }
616
+ Object.assign(indicator.style, {
617
+ display: "block",
618
+ top: `${indicatorY}px`,
619
+ left: insetX,
620
+ right: insetX,
621
+ width: "auto"
622
+ });
623
+ }
624
+ function hideIndicator(indicator) {
625
+ if (indicator) indicator.style.display = "none";
626
+ }
627
+ function walkShadowDom(root, callback) {
628
+ const children = root instanceof ShadowRoot ? root.children : root.children;
629
+ for (const child of Array.from(children)) {
630
+ const el = child;
631
+ callback(el);
632
+ if (el.shadowRoot) walkShadowDom(el.shadowRoot, callback);
633
+ if (el.children?.length) walkShadowDom(el, callback);
634
+ }
635
+ }
636
+ function queryShadowAll(root, selector) {
637
+ const results = [];
638
+ walkShadowDom(root, (el) => {
639
+ if (el.matches?.(selector)) results.push(el);
640
+ });
641
+ return results;
642
+ }
533
643
  class DragManager {
534
644
  constructor(store, toolRegistry, root) {
535
645
  this.currentDrop = null;
536
- this.indicator = null;
646
+ this.contentIndicator = null;
537
647
  this.rowIndicator = null;
538
- this.handleDragStart = (_e) => {
539
- };
540
- this.handleDragOver = (e) => {
648
+ this.onDragOver = (e) => {
541
649
  const types = e.dataTransfer?.types || [];
542
650
  const isToolDrag = types.includes("application/maileditor-tool");
543
651
  const isLayoutDrag = types.includes("application/maileditor-layout");
@@ -546,76 +654,42 @@ class DragManager {
546
654
  e.preventDefault();
547
655
  e.dataTransfer.dropEffect = isToolDrag || isLayoutDrag ? "copy" : "move";
548
656
  if (isLayoutDrag) {
549
- const drop = this.findRowDropPosition(e.clientY);
550
- this.hideIndicator();
551
- if (drop) {
552
- this.currentDrop = drop;
553
- this.showRowIndicator(drop);
554
- } else {
555
- this.hideRowIndicator();
556
- this.currentDrop = null;
557
- }
657
+ this.currentDrop = this.findRowDropTarget(e.clientY);
658
+ hideIndicator(this.contentIndicator);
659
+ this.showRowIndicator();
558
660
  } else {
559
- const drop = this.findContentDropPosition(e.clientX, e.clientY);
560
- this.hideRowIndicator();
561
- if (drop) {
562
- this.currentDrop = drop;
563
- this.showContentIndicator(drop);
564
- } else {
565
- this.hideIndicator();
566
- this.currentDrop = null;
567
- }
661
+ this.currentDrop = this.findContentDropTarget(e.clientX, e.clientY);
662
+ hideIndicator(this.rowIndicator);
663
+ this.showContentIndicator();
568
664
  }
569
665
  };
570
- this.handleDrop = (e) => {
666
+ this.onDrop = (e) => {
571
667
  e.preventDefault();
572
- this.hideIndicator();
573
- this.hideRowIndicator();
668
+ this.hideAllIndicators();
574
669
  const layoutData = e.dataTransfer?.getData("application/maileditor-layout");
575
670
  if (layoutData) {
576
- const cells = JSON.parse(layoutData);
577
- const row = this.store.createRow(cells);
578
- const rowIndex = this.currentDrop?.type === "row" ? this.currentDrop.rowIndex : void 0;
579
- this.store.addRow(row, rowIndex);
580
- this.reset();
581
- return;
671
+ this.handleLayoutDrop(JSON.parse(layoutData));
672
+ return this.reset();
582
673
  }
583
674
  const toolName = e.dataTransfer?.getData("application/maileditor-tool");
584
675
  if (toolName) {
585
- if (this.currentDrop?.type === "content" && this.currentDrop.columnId) {
586
- const defaults = this.toolRegistry.getDefaultValues(toolName);
587
- const content = this.store.createContent(toolName, defaults);
588
- this.store.addContent(this.currentDrop.columnId, content, this.currentDrop.contentIndex);
589
- this.store.select(content.id);
590
- } else {
591
- const row = this.store.createRow([1]);
592
- this.store.addRow(row);
593
- const defaults = this.toolRegistry.getDefaultValues(toolName);
594
- const content = this.store.createContent(toolName, defaults);
595
- this.store.addContent(row.columns[0].id, content);
596
- this.store.select(content.id);
597
- }
598
- this.reset();
599
- return;
676
+ this.handleToolDrop(toolName);
677
+ return this.reset();
600
678
  }
601
679
  const contentId = e.dataTransfer?.getData("application/maileditor-content") || dragState.draggingContentId;
602
- if (contentId && this.currentDrop?.type === "content" && this.currentDrop.columnId) {
603
- this.store.moveContent(contentId, this.currentDrop.columnId, this.currentDrop.contentIndex);
604
- this.store.select(contentId);
680
+ if (contentId) {
681
+ this.handleContentDrop(contentId);
605
682
  }
606
683
  this.reset();
607
- dragState.reset();
608
684
  };
609
- this.handleDragEnd = () => {
610
- this.hideIndicator();
611
- this.hideRowIndicator();
685
+ this.onDragEnd = () => {
686
+ this.hideAllIndicators();
612
687
  this.reset();
613
688
  };
614
- this.handleDragLeave = (e) => {
689
+ this.onDragLeave = (e) => {
615
690
  const related = e.relatedTarget;
616
691
  if (!related || !this.root.contains(related)) {
617
- this.hideIndicator();
618
- this.hideRowIndicator();
692
+ this.hideAllIndicators();
619
693
  this.currentDrop = null;
620
694
  }
621
695
  };
@@ -623,56 +697,61 @@ class DragManager {
623
697
  this.toolRegistry = toolRegistry;
624
698
  this.root = root;
625
699
  }
700
+ /** Attach all drag event listeners to the shadow root */
626
701
  attach() {
627
- this.root.addEventListener("dragover", this.handleDragOver);
628
- this.root.addEventListener("drop", this.handleDrop);
629
- this.root.addEventListener("dragend", this.handleDragEnd);
630
- this.root.addEventListener("dragleave", this.handleDragLeave);
631
- this.root.addEventListener("dragstart", this.handleDragStart);
632
- this.indicator = this.createIndicator("#3b82f6");
633
- this.rowIndicator = this.createIndicator("#8b5cf6");
634
- }
635
- createIndicator(color) {
636
- const el = document.createElement("div");
637
- Object.assign(el.style, {
638
- position: "absolute",
639
- left: "0",
640
- right: "0",
641
- height: "3px",
642
- background: color,
643
- borderRadius: "2px",
644
- pointerEvents: "none",
645
- zIndex: "1000",
646
- display: "none",
647
- boxShadow: `0 0 6px ${color}80`
648
- });
649
- return el;
650
- }
702
+ this.root.addEventListener("dragover", this.onDragOver);
703
+ this.root.addEventListener("drop", this.onDrop);
704
+ this.root.addEventListener("dragend", this.onDragEnd);
705
+ this.root.addEventListener("dragleave", this.onDragLeave);
706
+ this.contentIndicator = createDropIndicator("#3b82f6");
707
+ this.rowIndicator = createDropIndicator("#8b5cf6");
708
+ }
709
+ /** Remove all event listeners and clean up indicators */
651
710
  detach() {
652
- this.root.removeEventListener("dragover", this.handleDragOver);
653
- this.root.removeEventListener("drop", this.handleDrop);
654
- this.root.removeEventListener("dragend", this.handleDragEnd);
655
- this.root.removeEventListener("dragleave", this.handleDragLeave);
656
- this.root.removeEventListener("dragstart", this.handleDragStart);
657
- this.indicator?.remove();
711
+ this.root.removeEventListener("dragover", this.onDragOver);
712
+ this.root.removeEventListener("drop", this.onDrop);
713
+ this.root.removeEventListener("dragend", this.onDragEnd);
714
+ this.root.removeEventListener("dragleave", this.onDragLeave);
715
+ this.contentIndicator?.remove();
658
716
  this.rowIndicator?.remove();
659
717
  }
660
- // ----------------------------------------------------------
661
- // Find row drop position (between rows)
662
- // ----------------------------------------------------------
663
- findRowDropPosition(clientY) {
718
+ // ── Drop Handlers ──────────────────────────────────────────
719
+ handleLayoutDrop(cells) {
720
+ const row = this.store.createRow(cells);
721
+ const rowIndex = this.currentDrop?.type === "row" ? this.currentDrop.rowIndex : void 0;
722
+ this.store.addRow(row, rowIndex);
723
+ }
724
+ handleToolDrop(toolName) {
725
+ if (this.currentDrop?.type === "content" && this.currentDrop.columnId) {
726
+ const defaults = this.toolRegistry.getDefaultValues(toolName);
727
+ const content = this.store.createContent(toolName, defaults);
728
+ this.store.addContent(this.currentDrop.columnId, content, this.currentDrop.contentIndex);
729
+ this.store.select(content.id);
730
+ } else {
731
+ const row = this.store.createRow([1]);
732
+ this.store.addRow(row);
733
+ const defaults = this.toolRegistry.getDefaultValues(toolName);
734
+ const content = this.store.createContent(toolName, defaults);
735
+ this.store.addContent(row.columns[0].id, content);
736
+ this.store.select(content.id);
737
+ }
738
+ }
739
+ handleContentDrop(contentId) {
740
+ if (this.currentDrop?.type === "content" && this.currentDrop.columnId) {
741
+ this.store.moveContent(contentId, this.currentDrop.columnId, this.currentDrop.contentIndex);
742
+ this.store.select(contentId);
743
+ }
744
+ }
745
+ // ── Drop Target Detection ─────────────────────────────────
746
+ findRowDropTarget(clientY) {
664
747
  const canvas = this.root.querySelector("me-editor-canvas");
665
748
  if (!canvas?.shadowRoot) return null;
666
749
  const rows = Array.from(canvas.shadowRoot.querySelectorAll("me-row-renderer"));
667
- if (rows.length === 0) {
668
- return { type: "row", rowIndex: 0, y: 0 };
669
- }
670
- const firstRect = rows[0].getBoundingClientRect();
671
- let bestDist = Math.abs(clientY - firstRect.top);
672
- let bestTarget = { type: "row", rowIndex: 0, y: firstRect.top };
750
+ if (rows.length === 0) return { type: "row", rowIndex: 0, y: 0 };
751
+ let bestDist = Math.abs(clientY - rows[0].getBoundingClientRect().top);
752
+ let bestTarget = { type: "row", rowIndex: 0, y: rows[0].getBoundingClientRect().top };
673
753
  for (let i = 0; i < rows.length; i++) {
674
- const rect = rows[i].getBoundingClientRect();
675
- const y = rect.bottom;
754
+ const y = rows[i].getBoundingClientRect().bottom;
676
755
  const dist = Math.abs(clientY - y);
677
756
  if (dist < bestDist) {
678
757
  bestDist = dist;
@@ -681,21 +760,16 @@ class DragManager {
681
760
  }
682
761
  return bestTarget;
683
762
  }
684
- // ----------------------------------------------------------
685
- // Find content drop position (inside columns)
686
- // ----------------------------------------------------------
687
- findContentDropPosition(clientX, clientY) {
688
- const columns = this.queryShadowAll(this.root, "me-column-renderer");
763
+ findContentDropTarget(clientX, clientY) {
764
+ const columns = queryShadowAll(this.root, "me-column-renderer");
689
765
  let bestTarget = null;
690
766
  let bestDist = Infinity;
691
767
  for (const colEl of columns) {
692
768
  const columnId = colEl.dataset.columnId;
693
- if (!columnId) continue;
694
- const colShadow = colEl.shadowRoot;
695
- if (!colShadow) continue;
769
+ if (!columnId || !colEl.shadowRoot) continue;
696
770
  const colRect = colEl.getBoundingClientRect();
697
771
  if (clientX < colRect.left || clientX > colRect.right) continue;
698
- const contentEls = Array.from(colShadow.querySelectorAll("me-content-renderer"));
772
+ const contentEls = Array.from(colEl.shadowRoot.querySelectorAll("me-content-renderer"));
699
773
  if (contentEls.length === 0) {
700
774
  const dist2 = Math.abs(clientY - (colRect.top + colRect.height / 2));
701
775
  if (dist2 < bestDist) {
@@ -704,11 +778,11 @@ class DragManager {
704
778
  }
705
779
  continue;
706
780
  }
707
- const firstRect = contentEls[0].getBoundingClientRect();
708
- let dist = Math.abs(clientY - firstRect.top);
781
+ const firstY = contentEls[0].getBoundingClientRect().top;
782
+ let dist = Math.abs(clientY - firstY);
709
783
  if (dist < bestDist) {
710
784
  bestDist = dist;
711
- bestTarget = { type: "content", columnId, contentIndex: 0, y: firstRect.top };
785
+ bestTarget = { type: "content", columnId, contentIndex: 0, y: firstY };
712
786
  }
713
787
  for (let i = 0; i < contentEls.length; i++) {
714
788
  const rect = contentEls[i].getBoundingClientRect();
@@ -723,113 +797,72 @@ class DragManager {
723
797
  }
724
798
  return bestTarget;
725
799
  }
726
- // ----------------------------------------------------------
727
- // Indicator positioning
728
- // ----------------------------------------------------------
729
- showContentIndicator(drop) {
730
- if (!this.indicator || !drop.columnId) return;
731
- const columns = this.queryShadowAll(this.root, "me-column-renderer");
732
- const colEl = columns.find((c) => c.dataset.columnId === drop.columnId);
733
- if (!colEl?.shadowRoot) return;
734
- if (this.indicator.parentNode !== colEl.shadowRoot) {
735
- this.indicator.remove();
736
- colEl.shadowRoot.appendChild(this.indicator);
800
+ // ── Indicator Positioning ──────────────────────────────────
801
+ showContentIndicator() {
802
+ if (!this.contentIndicator || !this.currentDrop?.columnId) {
803
+ hideIndicator(this.contentIndicator);
804
+ return;
737
805
  }
806
+ const columns = queryShadowAll(this.root, "me-column-renderer");
807
+ const colEl = columns.find((c) => c.dataset.columnId === this.currentDrop.columnId);
808
+ if (!colEl?.shadowRoot) return;
738
809
  const contentEls = Array.from(colEl.shadowRoot.querySelectorAll("me-content-renderer"));
739
- const colRect = colEl.getBoundingClientRect();
740
- let indicatorY;
741
- if (contentEls.length === 0 || drop.contentIndex === 0) {
742
- indicatorY = 0;
743
- } else if (drop.contentIndex >= contentEls.length) {
744
- const lastRect = contentEls[contentEls.length - 1].getBoundingClientRect();
745
- indicatorY = lastRect.bottom - colRect.top;
746
- } else {
747
- const elRect = contentEls[drop.contentIndex].getBoundingClientRect();
748
- indicatorY = elRect.top - colRect.top;
749
- }
750
- Object.assign(this.indicator.style, {
751
- display: "block",
752
- top: `${indicatorY}px`,
753
- left: "4px",
754
- right: "4px",
755
- width: "auto"
756
- });
810
+ positionIndicator(this.contentIndicator, colEl.shadowRoot, contentEls, this.currentDrop.contentIndex ?? 0, "4px");
757
811
  }
758
- showRowIndicator(drop) {
759
- if (!this.rowIndicator) return;
812
+ showRowIndicator() {
813
+ if (!this.rowIndicator || !this.currentDrop) {
814
+ hideIndicator(this.rowIndicator);
815
+ return;
816
+ }
760
817
  const canvas = this.root.querySelector("me-editor-canvas");
761
- if (!canvas?.shadowRoot) return;
762
- const canvasBody = canvas.shadowRoot.querySelector(".canvas-body");
818
+ const canvasBody = canvas?.shadowRoot?.querySelector(".canvas-body");
763
819
  if (!canvasBody) return;
764
- if (this.rowIndicator.parentNode !== canvasBody) {
765
- this.rowIndicator.remove();
766
- canvasBody.appendChild(this.rowIndicator);
767
- }
768
820
  const rows = Array.from(canvas.shadowRoot.querySelectorAll("me-row-renderer"));
769
- const bodyRect = canvasBody.getBoundingClientRect();
770
- let indicatorY;
771
- if (rows.length === 0 || drop.rowIndex === 0) {
772
- indicatorY = 0;
773
- } else if (drop.rowIndex >= rows.length) {
774
- const lastRect = rows[rows.length - 1].getBoundingClientRect();
775
- indicatorY = lastRect.bottom - bodyRect.top;
776
- } else {
777
- const elRect = rows[drop.rowIndex].getBoundingClientRect();
778
- indicatorY = elRect.top - bodyRect.top;
779
- }
780
- Object.assign(this.rowIndicator.style, {
781
- display: "block",
782
- top: `${indicatorY}px`,
783
- left: "0",
784
- right: "0",
785
- width: "auto"
786
- });
787
- }
788
- hideIndicator() {
789
- if (this.indicator) this.indicator.style.display = "none";
821
+ positionIndicator(this.rowIndicator, canvasBody, rows, this.currentDrop.rowIndex ?? 0, "0");
790
822
  }
791
- hideRowIndicator() {
792
- if (this.rowIndicator) this.rowIndicator.style.display = "none";
823
+ hideAllIndicators() {
824
+ hideIndicator(this.contentIndicator);
825
+ hideIndicator(this.rowIndicator);
793
826
  }
794
827
  reset() {
795
828
  this.currentDrop = null;
796
- this.hideIndicator();
797
- this.hideRowIndicator();
829
+ dragState.reset();
798
830
  }
799
- // ----------------------------------------------------------
800
- // Shadow DOM traversal
801
- // ----------------------------------------------------------
802
- queryShadowAll(root, selector) {
803
- const results = [];
804
- this.walkShadowDom(root, (el) => {
805
- if (el.matches?.(selector)) results.push(el);
806
- });
807
- return results;
808
- }
809
- walkShadowDom(root, callback) {
810
- const children = root instanceof ShadowRoot ? root.children : root.children;
811
- for (const child of Array.from(children)) {
812
- const el = child;
813
- callback(el);
814
- if (el.shadowRoot) this.walkShadowDom(el.shadowRoot, callback);
815
- if (el.children?.length) this.walkShadowDom(el, callback);
816
- }
817
- }
818
- findContentElement(el) {
819
- while (el) {
820
- if (el.dataset?.contentId) return el;
821
- if (el.tagName?.toLowerCase() === "me-content-renderer") return el;
822
- if (el.parentElement) {
823
- el = el.parentElement;
824
- } else if (el.getRootNode().host) {
825
- el = el.getRootNode().host;
826
- } else {
827
- break;
828
- }
829
- }
830
- return null;
831
+ }
832
+ function str(values, key, fallback = "") {
833
+ const v = values[key];
834
+ if (typeof v === "string" && v !== "") return v;
835
+ return fallback;
836
+ }
837
+ function jsonParse(value, fallback) {
838
+ if (typeof value !== "string") return fallback;
839
+ try {
840
+ return JSON.parse(value);
841
+ } catch {
842
+ return fallback;
831
843
  }
832
844
  }
845
+ function emailTableCell(content, options) {
846
+ const { padding, align = "left", extraTdStyle = "" } = options;
847
+ const tdStyle = `overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;${extraTdStyle}`;
848
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
849
+ <tbody><tr><td style="${tdStyle}" align="${align}">
850
+ ${content}
851
+ </td></tr></tbody>
852
+ </table>`;
853
+ }
854
+ function vmlRoundrectButton(text, href, options) {
855
+ const { bgColor, textColor, fontSize, fontWeight, borderRadius } = options;
856
+ const radiusPx = parseInt(borderRadius) || 0;
857
+ if (radiusPx <= 0) return "";
858
+ const arcsize = Math.round(radiusPx / 20 * 100);
859
+ return `<!--[if mso]>
860
+ <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="${arcsize}%" stroke="f" fillcolor="${bgColor}">
861
+ <w:anchorlock/>
862
+ <center style="color:${textColor};font-family:arial,helvetica,sans-serif;font-size:${fontSize};font-weight:${fontWeight};">${text}</center>
863
+ </v:roundrect>
864
+ <![endif]-->`;
865
+ }
833
866
  const textTool = {
834
867
  name: "text",
835
868
  label: "Text",
@@ -852,11 +885,7 @@ const textTool = {
852
885
  options: {
853
886
  color: { label: "Text Color", defaultValue: "#000000", widget: "color_picker" },
854
887
  backgroundColor: { label: "Background Color", defaultValue: "", widget: "color_picker" },
855
- textAlign: {
856
- label: "Text Align",
857
- defaultValue: "left",
858
- widget: "alignment"
859
- },
888
+ textAlign: { label: "Text Align", defaultValue: "left", widget: "alignment" },
860
889
  lineHeight: {
861
890
  label: "Line Height",
862
891
  defaultValue: "140%",
@@ -896,37 +925,28 @@ const textTool = {
896
925
  textAlign: "left"
897
926
  },
898
927
  renderer: {
899
- renderEditor(values, context) {
900
- const padding = values.containerPadding || "10px";
901
- const bgColor = values.backgroundColor || "transparent";
902
- const color = values.color || "inherit";
903
- const lineHeight = values.lineHeight || "140%";
904
- const textContent = values.text || "";
928
+ renderEditor(values) {
929
+ const padding = str(values, "containerPadding", "10px");
930
+ const bgColor = str(values, "backgroundColor", "transparent");
931
+ const color = str(values, "color", "inherit");
932
+ const lineHeight = str(values, "lineHeight", "140%");
933
+ const textContent = str(values, "text");
905
934
  return html`
906
935
  <div style="padding:${padding};background-color:${bgColor};color:${color};line-height:${lineHeight};word-break:break-word;">
907
936
  ${unsafeHTML(textContent)}
908
937
  </div>
909
938
  `;
910
939
  },
911
- renderHtml(values, context) {
912
- const padding = values.containerPadding || "10px";
913
- const bgColor = values.backgroundColor || "";
914
- const color = values.color || "#000000";
915
- const lineHeight = values.lineHeight || "140%";
916
- const textContent = values.text || "";
917
- const textAlign = values.textAlign || "left";
940
+ renderHtml(values) {
941
+ const padding = str(values, "containerPadding", "10px");
942
+ const bgColor = str(values, "backgroundColor");
943
+ const color = str(values, "color", "#000000");
944
+ const lineHeight = str(values, "lineHeight", "140%");
945
+ const textAlign = str(values, "textAlign", "left");
946
+ const textContent = str(values, "text");
918
947
  const bgStyle = bgColor ? `background-color:${bgColor};` : "";
919
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
920
- <tbody>
921
- <tr>
922
- <td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};${bgStyle}font-family:arial,helvetica,sans-serif;" align="left">
923
- <div style="font-size:14px;color:${color};line-height:${lineHeight};text-align:${textAlign};word-wrap:break-word;">
924
- ${textContent}
925
- </div>
926
- </td>
927
- </tr>
928
- </tbody>
929
- </table>`;
948
+ const inner = `<div style="font-size:14px;color:${color};line-height:${lineHeight};text-align:${textAlign};word-wrap:break-word;">${textContent}</div>`;
949
+ return emailTableCell(inner, { padding, extraTdStyle: bgStyle });
930
950
  }
931
951
  }
932
952
  };
@@ -940,23 +960,17 @@ const headingTool = {
940
960
  text: {
941
961
  title: "Heading",
942
962
  options: {
943
- text: {
944
- label: "Text",
945
- defaultValue: "Heading",
946
- widget: "text"
947
- },
963
+ text: { label: "Text", defaultValue: "Heading", widget: "text" },
948
964
  headingType: {
949
965
  label: "Heading Type",
950
966
  defaultValue: "h1",
951
967
  widget: "dropdown",
952
- widgetParams: {
953
- options: [
954
- { label: "H1", value: "h1" },
955
- { label: "H2", value: "h2" },
956
- { label: "H3", value: "h3" },
957
- { label: "H4", value: "h4" }
958
- ]
959
- }
968
+ widgetParams: { options: [
969
+ { label: "H1", value: "h1" },
970
+ { label: "H2", value: "h2" },
971
+ { label: "H3", value: "h3" },
972
+ { label: "H4", value: "h4" }
973
+ ] }
960
974
  }
961
975
  }
962
976
  },
@@ -1037,35 +1051,32 @@ const headingTool = {
1037
1051
  containerPadding: "10px"
1038
1052
  },
1039
1053
  renderer: {
1040
- renderEditor(values, ctx) {
1041
- const padding = values.containerPadding || "10px";
1042
- const fontSize = values.fontSize || "22px";
1043
- const color = values.color || "#000000";
1044
- const textAlign = values.textAlign || "left";
1045
- const fontWeight = values.fontWeight || "700";
1046
- const lineHeight = values.lineHeight || "140%";
1047
- const text = values.text || "Heading";
1054
+ renderEditor(values) {
1055
+ const padding = str(values, "containerPadding", "10px");
1056
+ const fontSize = str(values, "fontSize", "22px");
1057
+ const color = str(values, "color", "#000000");
1058
+ const textAlign = str(values, "textAlign", "left");
1059
+ const fontWeight = str(values, "fontWeight", "700");
1060
+ const lineHeight = str(values, "lineHeight", "140%");
1061
+ const text = str(values, "text", "Heading");
1048
1062
  return html`
1049
1063
  <div style="padding:${padding};font-size:${fontSize};color:${color};text-align:${textAlign};font-weight:${fontWeight};line-height:${lineHeight};">
1050
1064
  ${text}
1051
1065
  </div>
1052
1066
  `;
1053
1067
  },
1054
- renderHtml(values, ctx) {
1055
- const padding = values.containerPadding || "10px";
1056
- const fontSize = values.fontSize || "22px";
1057
- const color = values.color || "#000000";
1058
- const textAlign = values.textAlign || "left";
1059
- const fontWeight = values.fontWeight || "700";
1060
- const lineHeight = values.lineHeight || "140%";
1061
- const letterSpacing = values.letterSpacing || "normal";
1062
- const tag = values.headingType || "h1";
1063
- const text = values.text || "Heading";
1064
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1065
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
1066
- <${tag} style="margin:0;font-size:${fontSize};color:${color};text-align:${textAlign};font-weight:${fontWeight};line-height:${lineHeight};letter-spacing:${letterSpacing};">${text}</${tag}>
1067
- </td></tr></tbody>
1068
- </table>`;
1068
+ renderHtml(values) {
1069
+ const padding = str(values, "containerPadding", "10px");
1070
+ const fontSize = str(values, "fontSize", "22px");
1071
+ const color = str(values, "color", "#000000");
1072
+ const textAlign = str(values, "textAlign", "left");
1073
+ const fontWeight = str(values, "fontWeight", "700");
1074
+ const lineHeight = str(values, "lineHeight", "140%");
1075
+ const letterSpacing = str(values, "letterSpacing", "normal");
1076
+ const tag = str(values, "headingType", "h1");
1077
+ const text = str(values, "text", "Heading");
1078
+ const inner = `<${tag} style="margin:0;font-size:${fontSize};color:${color};text-align:${textAlign};font-weight:${fontWeight};line-height:${lineHeight};letter-spacing:${letterSpacing};">${text}</${tag}>`;
1079
+ return emailTableCell(inner, { padding });
1069
1080
  }
1070
1081
  }
1071
1082
  };
@@ -1090,9 +1101,9 @@ const paragraphTool = {
1090
1101
  title: "Style",
1091
1102
  options: {
1092
1103
  color: { label: "Text Color", defaultValue: "#374151", widget: "color_picker" },
1104
+ textAlign: { label: "Text Align", defaultValue: "left", widget: "alignment" },
1093
1105
  lineHeight: { label: "Line Height", defaultValue: "160%", widget: "text" },
1094
- letterSpacing: { label: "Letter Spacing", defaultValue: "normal", widget: "text" },
1095
- textAlign: { label: "Text Align", defaultValue: "left", widget: "alignment" }
1106
+ letterSpacing: { label: "Letter Spacing", defaultValue: "normal", widget: "text" }
1096
1107
  }
1097
1108
  },
1098
1109
  spacing: {
@@ -1103,7 +1114,7 @@ const paragraphTool = {
1103
1114
  }
1104
1115
  },
1105
1116
  defaultValues: {
1106
- 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>',
1117
+ text: '<p style="font-size:14px;line-height:1.6;">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>',
1107
1118
  color: "#374151",
1108
1119
  lineHeight: "160%",
1109
1120
  letterSpacing: "normal",
@@ -1112,24 +1123,19 @@ const paragraphTool = {
1112
1123
  },
1113
1124
  renderer: {
1114
1125
  renderEditor(values) {
1115
- const padding = values.containerPadding || "10px";
1116
- const color = values.color || "#374151";
1117
- const lineHeight = values.lineHeight || "160%";
1118
- const text = values.text || "";
1119
- return html`<div style="padding:${padding};color:${color};line-height:${lineHeight};word-break:break-word;">${unsafeHTML(text)}</div>`;
1126
+ const padding = str(values, "containerPadding", "10px");
1127
+ const color = str(values, "color", "#374151");
1128
+ const lineHeight = str(values, "lineHeight", "160%");
1129
+ return html`<div style="padding:${padding};color:${color};line-height:${lineHeight};word-break:break-word;">${unsafeHTML(str(values, "text"))}</div>`;
1120
1130
  },
1121
1131
  renderHtml(values) {
1122
- const padding = values.containerPadding || "10px";
1123
- const color = values.color || "#374151";
1124
- const lineHeight = values.lineHeight || "160%";
1125
- const letterSpacing = values.letterSpacing || "normal";
1126
- const textAlign = values.textAlign || "left";
1127
- const text = values.text || "";
1128
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1129
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
1130
- <div style="font-size:14px;color:${color};line-height:${lineHeight};text-align:${textAlign};letter-spacing:${letterSpacing};word-wrap:break-word;">${text}</div>
1131
- </td></tr></tbody>
1132
- </table>`;
1132
+ const padding = str(values, "containerPadding", "10px");
1133
+ const color = str(values, "color", "#374151");
1134
+ const lineHeight = str(values, "lineHeight", "160%");
1135
+ const textAlign = str(values, "textAlign", "left");
1136
+ const letterSpacing = str(values, "letterSpacing", "normal");
1137
+ const inner = `<div style="font-size:14px;color:${color};line-height:${lineHeight};text-align:${textAlign};letter-spacing:${letterSpacing};word-wrap:break-word;">${str(values, "text")}</div>`;
1138
+ return emailTableCell(inner, { padding });
1133
1139
  }
1134
1140
  }
1135
1141
  };
@@ -1170,9 +1176,7 @@ const imageTool = {
1170
1176
  },
1171
1177
  spacing: {
1172
1178
  title: "Spacing",
1173
- options: {
1174
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" }
1175
- }
1179
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1176
1180
  },
1177
1181
  general: {
1178
1182
  title: "General",
@@ -1195,46 +1199,32 @@ const imageTool = {
1195
1199
  containerPadding: "10px"
1196
1200
  },
1197
1201
  renderer: {
1198
- renderEditor(values, ctx) {
1199
- const padding = values.containerPadding || "10px";
1200
- const src = values.src || "";
1201
- const alt = values.alt || "";
1202
- const width = values.width || "100%";
1203
- const borderRadius = values.borderRadius || "0px";
1204
- const align = values.align || "center";
1202
+ renderEditor(values) {
1203
+ const padding = str(values, "containerPadding", "10px");
1204
+ const src = str(values, "src");
1205
+ const alt = str(values, "alt");
1206
+ const width = str(values, "width", "100%");
1207
+ const borderRadius = str(values, "borderRadius", "0px");
1208
+ const align = str(values, "align", "center");
1205
1209
  if (!src) {
1206
- return html`
1207
- <div style="padding:${padding};text-align:${align};">
1208
- <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;">
1209
- No image set. Enter a URL in the property panel.
1210
- </div>
1211
- </div>
1212
- `;
1210
+ return html`<div style="padding:${padding};text-align:${align};"><div style="background:#f1f5f9;border:2px dashed #cbd5e1;border-radius:8px;padding:40px 20px;text-align:center;color:#94a3b8;font-size:13px;">No image set. Enter a URL in the property panel.</div></div>`;
1213
1211
  }
1214
- return html`
1215
- <div style="padding:${padding};text-align:${align};">
1216
- <img src=${src} alt=${alt} style="display:inline-block;max-width:100%;width:${width};border-radius:${borderRadius};border:0;" />
1217
- </div>
1218
- `;
1212
+ return html`<div style="padding:${padding};text-align:${align};"><img src=${src} alt=${alt} style="display:inline-block;max-width:100%;width:${width};border-radius:${borderRadius};border:0;" /></div>`;
1219
1213
  },
1220
1214
  renderHtml(values, ctx) {
1221
- const padding = values.containerPadding || "10px";
1222
- const src = values.src || "";
1223
- const alt = values.alt || "";
1224
- const href = values.href || "";
1225
- const target = values.target || "_blank";
1226
- const width = values.width || "100%";
1227
- const borderRadius = values.borderRadius || "0px";
1228
- const align = values.align || "center";
1215
+ const padding = str(values, "containerPadding", "10px");
1216
+ const src = str(values, "src");
1217
+ const alt = str(values, "alt");
1218
+ const href = str(values, "href");
1219
+ const target = str(values, "target", "_blank");
1220
+ const width = str(values, "width", "100%");
1221
+ const borderRadius = str(values, "borderRadius", "0px");
1222
+ const align = str(values, "align", "center");
1229
1223
  const widthPx = width === "100%" ? ctx.columnWidth : parseInt(width);
1230
- const borderStyle = borderRadius !== "0px" ? `border-radius:${borderRadius};` : "";
1231
- 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}" />`;
1224
+ const brStyle = borderRadius !== "0px" ? `border-radius:${borderRadius};` : "";
1225
+ const imgTag = `<img align="${align}" border="0" src="${src}" alt="${alt}" title="${alt}" style="outline:none;text-decoration:none;clear:both;display:inline-block!important;border:none;height:auto;float:none;width:${width};max-width:${widthPx}px;${brStyle}" width="${widthPx}" />`;
1232
1226
  const content = href ? `<a href="${href}" target="${target}" style="text-decoration:none;">${imgTag}</a>` : imgTag;
1233
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1234
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${align}">
1235
- ${content}
1236
- </td></tr></tbody>
1237
- </table>`;
1227
+ return emailTableCell(content, { padding, align });
1238
1228
  }
1239
1229
  }
1240
1230
  };
@@ -1275,10 +1265,7 @@ const buttonTool = {
1275
1265
  label: "Font Weight",
1276
1266
  defaultValue: "700",
1277
1267
  widget: "dropdown",
1278
- widgetParams: { options: [
1279
- { label: "Normal", value: "400" },
1280
- { label: "Bold", value: "700" }
1281
- ] }
1268
+ widgetParams: { options: [{ label: "Normal", value: "400" }, { label: "Bold", value: "700" }] }
1282
1269
  },
1283
1270
  borderRadius: { label: "Border Radius", defaultValue: "4px", widget: "text" },
1284
1271
  buttonWidth: {
@@ -1299,9 +1286,7 @@ const buttonTool = {
1299
1286
  },
1300
1287
  spacing: {
1301
1288
  title: "Spacing",
1302
- options: {
1303
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" }
1304
- }
1289
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1305
1290
  },
1306
1291
  general: {
1307
1292
  title: "General",
@@ -1329,64 +1314,52 @@ const buttonTool = {
1329
1314
  containerPadding: "10px"
1330
1315
  },
1331
1316
  renderer: {
1332
- renderEditor(values, ctx) {
1333
- const padding = values.containerPadding || "10px";
1334
- const bgColor = values.backgroundColor || "#3b82f6";
1335
- const textColor = values.textColor || "#ffffff";
1336
- const fontSize = values.fontSize || "14px";
1337
- const fontWeight = values.fontWeight || "700";
1338
- const borderRadius = values.borderRadius || "4px";
1339
- const buttonPadding = values.buttonPadding || "10px 20px";
1340
- const text = values.text || "Click Me";
1341
- const textAlign = values.textAlign || "center";
1342
- const buttonWidth = values.buttonWidth || "auto";
1343
- const borderColor = values.borderColor || bgColor;
1344
- const borderWidth = values.borderWidth || "0px";
1345
- const borderStyle = borderWidth !== "0px" ? `border:${borderWidth} solid ${borderColor};` : "border:none;";
1346
- const widthStyle = buttonWidth === "auto" ? "display:inline-block;" : `display:block;width:${buttonWidth};`;
1317
+ renderEditor(values) {
1318
+ const padding = str(values, "containerPadding", "10px");
1319
+ const bg = str(values, "backgroundColor", "#3b82f6");
1320
+ const color = str(values, "textColor", "#ffffff");
1321
+ const fontSize = str(values, "fontSize", "14px");
1322
+ const fontWeight = str(values, "fontWeight", "700");
1323
+ const radius = str(values, "borderRadius", "4px");
1324
+ const btnPad = str(values, "buttonPadding", "10px 20px");
1325
+ const text = str(values, "text", "Click Me");
1326
+ const align = str(values, "textAlign", "center");
1327
+ const btnWidth = str(values, "buttonWidth", "auto");
1328
+ const bw = str(values, "borderWidth", "0px");
1329
+ const bc = str(values, "borderColor", bg);
1330
+ const borderStyle = bw !== "0px" ? `border:${bw} solid ${bc};` : "border:none;";
1331
+ const widthStyle = btnWidth === "auto" ? "display:inline-block;" : `display:block;width:${btnWidth};`;
1347
1332
  return html`
1348
- <div style="padding:${padding};text-align:${textAlign};">
1349
- <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;">
1350
- ${text}
1351
- </a>
1333
+ <div style="padding:${padding};text-align:${align};">
1334
+ <a style="${widthStyle}background-color:${bg};color:${color};font-size:${fontSize};font-weight:${fontWeight};border-radius:${radius};padding:${btnPad};text-decoration:none;text-align:center;${borderStyle}cursor:pointer;font-family:arial,helvetica,sans-serif;box-sizing:border-box;">${text}</a>
1352
1335
  </div>
1353
1336
  `;
1354
1337
  },
1355
- renderHtml(values, ctx) {
1356
- const padding = values.containerPadding || "10px";
1357
- const bgColor = values.backgroundColor || "#3b82f6";
1358
- const textColor = values.textColor || "#ffffff";
1359
- const fontSize = values.fontSize || "14px";
1360
- const fontWeight = values.fontWeight || "700";
1361
- const borderRadius = values.borderRadius || "4px";
1362
- const buttonPadding = values.buttonPadding || "10px 20px";
1363
- const text = values.text || "Click Me";
1364
- const textAlign = values.textAlign || "center";
1365
- const href = values.href || "#";
1366
- const target = values.target || "_blank";
1367
- const borderColor = values.borderColor || bgColor;
1368
- const borderWidth = values.borderWidth || "0px";
1369
- const borderStyle = borderWidth !== "0px" ? `border:${borderWidth} solid ${borderColor};` : "border:none;";
1370
- const vml = parseInt(borderRadius) > 0 ? `
1371
- <!--[if mso]>
1372
- <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}">
1373
- <w:anchorlock/>
1374
- <center style="color:${textColor};font-family:arial,helvetica,sans-serif;font-size:${fontSize};font-weight:${fontWeight};">${text}</center>
1375
- </v:roundrect>
1376
- <![endif]-->
1338
+ renderHtml(values) {
1339
+ const padding = str(values, "containerPadding", "10px");
1340
+ const bg = str(values, "backgroundColor", "#3b82f6");
1341
+ const color = str(values, "textColor", "#ffffff");
1342
+ const fontSize = str(values, "fontSize", "14px");
1343
+ const fontWeight = str(values, "fontWeight", "700");
1344
+ const radius = str(values, "borderRadius", "4px");
1345
+ const btnPad = str(values, "buttonPadding", "10px 20px");
1346
+ const text = str(values, "text", "Click Me");
1347
+ const align = str(values, "textAlign", "center");
1348
+ const href = str(values, "href", "#");
1349
+ const target = str(values, "target", "_blank");
1350
+ const bw = str(values, "borderWidth", "0px");
1351
+ const bc = str(values, "borderColor", bg);
1352
+ const borderStyle = bw !== "0px" ? `border:${bw} solid ${bc};` : "border:none;";
1353
+ const vml = vmlRoundrectButton(text, href, { bgColor: bg, textColor: color, fontSize, fontWeight, borderRadius: radius });
1354
+ const vmlOpen = vml ? `${vml}
1377
1355
  <!--[if !mso]><!-->` : "<!--[if !mso]><!-->";
1378
- const vmlEnd = parseInt(borderRadius) > 0 ? "<!--<![endif]-->" : "<!--<![endif]-->";
1379
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1380
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1381
- <div align="${textAlign}">
1382
- ${vml}
1383
- <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;">
1384
- <span style="line-height:120%;">${text}</span>
1385
- </a>
1386
- ${vmlEnd}
1387
- </div>
1388
- </td></tr></tbody>
1389
- </table>`;
1356
+ const vmlClose = "<!--<![endif]-->";
1357
+ const inner = `<div align="${align}">
1358
+ ${vmlOpen}
1359
+ <a href="${href}" target="${target}" style="box-sizing:border-box;display:inline-block;text-decoration:none;text-align:center;color:${color};background-color:${bg};border-radius:${radius};font-size:${fontSize};font-weight:${fontWeight};padding:${btnPad};font-family:arial,helvetica,sans-serif;${borderStyle}mso-border-alt:none;word-break:keep-all;"><span style="line-height:120%;">${text}</span></a>
1360
+ ${vmlClose}
1361
+ </div>`;
1362
+ return emailTableCell(inner, { padding, align });
1390
1363
  }
1391
1364
  }
1392
1365
  };
@@ -1418,9 +1391,7 @@ const dividerTool = {
1418
1391
  },
1419
1392
  spacing: {
1420
1393
  title: "Spacing",
1421
- options: {
1422
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" }
1423
- }
1394
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1424
1395
  },
1425
1396
  general: {
1426
1397
  title: "General",
@@ -1439,30 +1410,17 @@ const dividerTool = {
1439
1410
  },
1440
1411
  renderer: {
1441
1412
  renderEditor(values) {
1442
- const padding = values.containerPadding || "10px";
1443
- const width = values.width || "100%";
1444
- const bw = values.borderTopWidth || "1px";
1445
- const bs = values.borderTopStyle || "solid";
1446
- const bc = values.borderTopColor || "#cccccc";
1447
- return html`
1448
- <div style="padding:${padding};">
1449
- <div style="border-top:${bw} ${bs} ${bc};width:${width};margin:0 auto;"></div>
1450
- </div>
1451
- `;
1413
+ const padding = str(values, "containerPadding", "10px");
1414
+ const width = str(values, "width", "100%");
1415
+ const border = `${str(values, "borderTopWidth", "1px")} ${str(values, "borderTopStyle", "solid")} ${str(values, "borderTopColor", "#cccccc")}`;
1416
+ return html`<div style="padding:${padding};"><div style="border-top:${border};width:${width};margin:0 auto;"></div></div>`;
1452
1417
  },
1453
1418
  renderHtml(values) {
1454
- const padding = values.containerPadding || "10px";
1455
- const width = values.width || "100%";
1456
- const bw = values.borderTopWidth || "1px";
1457
- const bs = values.borderTopStyle || "solid";
1458
- const bc = values.borderTopColor || "#cccccc";
1459
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1460
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="center">
1461
- <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%;">
1462
- <tbody><tr><td style="font-size:0;line-height:0;">&nbsp;</td></tr></tbody>
1463
- </table>
1464
- </td></tr></tbody>
1465
- </table>`;
1419
+ const padding = str(values, "containerPadding", "10px");
1420
+ const width = str(values, "width", "100%");
1421
+ const border = `${str(values, "borderTopWidth", "1px")} ${str(values, "borderTopStyle", "solid")} ${str(values, "borderTopColor", "#cccccc")}`;
1422
+ const inner = `<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="${width}" style="border-collapse:collapse;border-top:${border};"><tbody><tr><td style="font-size:0;line-height:0;">&nbsp;</td></tr></tbody></table>`;
1423
+ return emailTableCell(inner, { padding, align: "center" });
1466
1424
  }
1467
1425
  }
1468
1426
  };
@@ -1485,9 +1443,7 @@ const htmlTool = {
1485
1443
  },
1486
1444
  spacing: {
1487
1445
  title: "Spacing",
1488
- options: {
1489
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1490
- }
1446
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" } }
1491
1447
  },
1492
1448
  general: {
1493
1449
  title: "General",
@@ -1503,22 +1459,14 @@ const htmlTool = {
1503
1459
  },
1504
1460
  renderer: {
1505
1461
  renderEditor(values) {
1506
- const padding = values.containerPadding || "10px";
1507
- const content = values.html || "";
1508
- return html`<div style="padding:${padding};">${unsafeHTML(content)}</div>`;
1462
+ return html`<div style="padding:${str(values, "containerPadding", "10px")};">${unsafeHTML(str(values, "html"))}</div>`;
1509
1463
  },
1510
1464
  renderHtml(values) {
1511
- const padding = values.containerPadding || "10px";
1512
- const content = values.html || "";
1513
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1514
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
1515
- ${content}
1516
- </td></tr></tbody>
1517
- </table>`;
1465
+ return emailTableCell(str(values, "html"), { padding: str(values, "containerPadding", "10px") });
1518
1466
  }
1519
1467
  }
1520
1468
  };
1521
- const defaultSocials = [
1469
+ const DEFAULT_ICONS = [
1522
1470
  { name: "Facebook", url: "https://facebook.com/", icon: "f", color: "#1877F2" },
1523
1471
  { name: "Twitter", url: "https://twitter.com/", icon: "𝕏", color: "#000000" },
1524
1472
  { name: "Instagram", url: "https://instagram.com/", icon: "📷", color: "#E4405F" },
@@ -1534,26 +1482,22 @@ const socialTool = {
1534
1482
  icons: {
1535
1483
  title: "Social Icons",
1536
1484
  options: {
1537
- icons: { label: "Icons (JSON)", defaultValue: JSON.stringify(defaultSocials), widget: "rich_text" },
1485
+ icons: { label: "Icons (JSON)", defaultValue: JSON.stringify(DEFAULT_ICONS), widget: "rich_text" },
1538
1486
  iconSize: { label: "Icon Size", defaultValue: "32px", widget: "text" },
1539
1487
  iconSpacing: { label: "Spacing", defaultValue: "8px", widget: "text" }
1540
1488
  }
1541
1489
  },
1542
1490
  style: {
1543
1491
  title: "Style",
1544
- options: {
1545
- textAlign: { label: "Align", defaultValue: "center", widget: "text" }
1546
- }
1492
+ options: { textAlign: { label: "Align", defaultValue: "center", widget: "alignment" } }
1547
1493
  },
1548
1494
  spacing: {
1549
1495
  title: "Spacing",
1550
- options: {
1551
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1552
- }
1496
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1553
1497
  }
1554
1498
  },
1555
1499
  defaultValues: {
1556
- icons: JSON.stringify(defaultSocials),
1500
+ icons: JSON.stringify(DEFAULT_ICONS),
1557
1501
  iconSize: "32px",
1558
1502
  iconSpacing: "8px",
1559
1503
  textAlign: "center",
@@ -1561,55 +1505,34 @@ const socialTool = {
1561
1505
  },
1562
1506
  renderer: {
1563
1507
  renderEditor(values) {
1564
- const padding = values.containerPadding || "10px";
1565
- const textAlign = values.textAlign || "center";
1566
- const iconSize = values.iconSize || "32px";
1567
- const spacing = values.iconSpacing || "8px";
1568
- let icons = defaultSocials;
1569
- try {
1570
- icons = JSON.parse(values.icons);
1571
- } catch {
1572
- }
1508
+ const padding = str(values, "containerPadding", "10px");
1509
+ const align = str(values, "textAlign", "center");
1510
+ const iconSize = str(values, "iconSize", "32px");
1511
+ const spacing = str(values, "iconSpacing", "8px");
1512
+ const icons = jsonParse(values.icons, DEFAULT_ICONS);
1573
1513
  return html`
1574
- <div style="padding:${padding};text-align:${textAlign};">
1575
- ${icons.map(
1576
- (s) => html`
1577
- <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>
1578
- `
1579
- )}
1514
+ <div style="padding:${padding};text-align:${align};">
1515
+ ${icons.map((s) => html`
1516
+ <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>
1517
+ `)}
1580
1518
  </div>
1581
1519
  `;
1582
1520
  },
1583
1521
  renderHtml(values) {
1584
- const padding = values.containerPadding || "10px";
1585
- const textAlign = values.textAlign || "center";
1586
- const iconSize = parseInt(values.iconSize || "32");
1587
- const spacing = values.iconSpacing || "8px";
1588
- let icons = defaultSocials;
1589
- try {
1590
- icons = JSON.parse(values.icons);
1591
- } catch {
1592
- }
1593
- const iconsHtml = icons.map(
1594
- (s) => `<td align="center" valign="middle" style="padding:0 ${spacing};">
1595
- <a href="${s.url}" target="_blank" style="text-decoration:none;">
1596
- <table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>
1597
- <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;">
1598
- ${s.icon}
1599
- </td>
1600
- </tr></table>
1601
- </a>
1602
- </td>`
1522
+ const padding = str(values, "containerPadding", "10px");
1523
+ const align = str(values, "textAlign", "center");
1524
+ const iconSize = str(values, "iconSize", "32");
1525
+ const spacing = str(values, "iconSpacing", "8px");
1526
+ const icons = jsonParse(values.icons, DEFAULT_ICONS);
1527
+ const cells = icons.map(
1528
+ (s) => `<td align="center" valign="middle" style="padding:0 ${spacing};"><a href="${s.url}" target="_blank" style="text-decoration:none;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td width="${iconSize}" height="${iconSize}" align="center" valign="middle" style="width:${iconSize}px;height:${iconSize}px;background:${s.color};border-radius:50%;color:#fff;font-size:14px;font-weight:bold;font-family:arial,sans-serif;">${s.icon}</td></tr></table></a></td>`
1603
1529
  ).join("\n");
1604
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1605
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1606
- <table role="presentation" cellpadding="0" cellspacing="0" border="0" align="${textAlign}"><tr>${iconsHtml}</tr></table>
1607
- </td></tr></tbody>
1608
- </table>`;
1530
+ const inner = `<table role="presentation" cellpadding="0" cellspacing="0" border="0" align="${align}"><tr>${cells}</tr></table>`;
1531
+ return emailTableCell(inner, { padding, align });
1609
1532
  }
1610
1533
  }
1611
1534
  };
1612
- const defaultItems = [
1535
+ const DEFAULT_ITEMS = [
1613
1536
  { text: "Home", href: "#" },
1614
1537
  { text: "About", href: "#" },
1615
1538
  { text: "Contact", href: "#" }
@@ -1623,14 +1546,12 @@ const menuTool = {
1623
1546
  options: {
1624
1547
  menu: {
1625
1548
  title: "Menu",
1626
- options: {
1627
- items: { label: "Items (JSON)", defaultValue: JSON.stringify(defaultItems), widget: "rich_text" }
1628
- }
1549
+ options: { items: { label: "Items (JSON)", defaultValue: JSON.stringify(DEFAULT_ITEMS), widget: "rich_text" } }
1629
1550
  },
1630
1551
  style: {
1631
1552
  title: "Style",
1632
1553
  options: {
1633
- textAlign: { label: "Align", defaultValue: "center", widget: "text" },
1554
+ textAlign: { label: "Align", defaultValue: "center", widget: "alignment" },
1634
1555
  fontSize: { label: "Font Size", defaultValue: "14px", widget: "text" },
1635
1556
  color: { label: "Text Color", defaultValue: "#333333", widget: "color_picker" },
1636
1557
  separator: { label: "Separator", defaultValue: "|", widget: "text" },
@@ -1639,13 +1560,11 @@ const menuTool = {
1639
1560
  },
1640
1561
  spacing: {
1641
1562
  title: "Spacing",
1642
- options: {
1643
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1644
- }
1563
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1645
1564
  }
1646
1565
  },
1647
1566
  defaultValues: {
1648
- items: JSON.stringify(defaultItems),
1567
+ items: JSON.stringify(DEFAULT_ITEMS),
1649
1568
  textAlign: "center",
1650
1569
  fontSize: "14px",
1651
1570
  color: "#333333",
@@ -1655,47 +1574,33 @@ const menuTool = {
1655
1574
  },
1656
1575
  renderer: {
1657
1576
  renderEditor(values) {
1658
- const padding = values.containerPadding || "10px";
1659
- const textAlign = values.textAlign || "center";
1660
- const fontSize = values.fontSize || "14px";
1661
- const color = values.color || "#333333";
1662
- const sep = values.separator || "|";
1663
- const sepColor = values.separatorColor || "#cccccc";
1664
- let items = defaultItems;
1665
- try {
1666
- items = JSON.parse(values.items);
1667
- } catch {
1668
- }
1577
+ const padding = str(values, "containerPadding", "10px");
1578
+ const align = str(values, "textAlign", "center");
1579
+ const fontSize = str(values, "fontSize", "14px");
1580
+ const color = str(values, "color", "#333333");
1581
+ const sep = str(values, "separator", "|");
1582
+ const sepColor = str(values, "separatorColor", "#cccccc");
1583
+ const items = jsonParse(values.items, DEFAULT_ITEMS);
1669
1584
  return html`
1670
- <div style="padding:${padding};text-align:${textAlign};font-size:${fontSize};font-family:arial,sans-serif;">
1671
- ${items.map(
1672
- (item, i) => html`${i > 0 ? html`<span style="color:${sepColor};padding:0 8px;">${sep}</span>` : ""}
1673
- <a href=${item.href} style="color:${color};text-decoration:none;">${item.text}</a>`
1674
- )}
1585
+ <div style="padding:${padding};text-align:${align};font-size:${fontSize};font-family:arial,sans-serif;">
1586
+ ${items.map((item, i) => html`${i > 0 ? html`<span style="color:${sepColor};padding:0 8px;">${sep}</span>` : ""}
1587
+ <a href=${item.href} style="color:${color};text-decoration:none;">${item.text}</a>`)}
1675
1588
  </div>
1676
1589
  `;
1677
1590
  },
1678
1591
  renderHtml(values) {
1679
- const padding = values.containerPadding || "10px";
1680
- const textAlign = values.textAlign || "center";
1681
- const fontSize = values.fontSize || "14px";
1682
- const color = values.color || "#333333";
1683
- const sep = values.separator || "|";
1684
- const sepColor = values.separatorColor || "#cccccc";
1685
- let items = defaultItems;
1686
- try {
1687
- items = JSON.parse(values.items);
1688
- } catch {
1689
- }
1690
- const linksHtml = items.map((item, i) => {
1592
+ const padding = str(values, "containerPadding", "10px");
1593
+ const align = str(values, "textAlign", "center");
1594
+ const fontSize = str(values, "fontSize", "14px");
1595
+ const color = str(values, "color", "#333333");
1596
+ const sep = str(values, "separator", "|");
1597
+ const sepColor = str(values, "separatorColor", "#cccccc");
1598
+ const items = jsonParse(values.items, DEFAULT_ITEMS);
1599
+ const links = items.map((item, i) => {
1691
1600
  const prefix = i > 0 ? `<span style="color:${sepColor};padding:0 8px;">${sep}</span>` : "";
1692
1601
  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>`;
1693
1602
  }).join("");
1694
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1695
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1696
- <div style="text-align:${textAlign};">${linksHtml}</div>
1697
- </td></tr></tbody>
1698
- </table>`;
1603
+ return emailTableCell(`<div style="text-align:${align};">${links}</div>`, { padding, align });
1699
1604
  }
1700
1605
  }
1701
1606
  };
@@ -1720,15 +1625,11 @@ const videoTool = {
1720
1625
  },
1721
1626
  style: {
1722
1627
  title: "Style",
1723
- options: {
1724
- textAlign: { label: "Align", defaultValue: "center", widget: "text" }
1725
- }
1628
+ options: { textAlign: { label: "Align", defaultValue: "center", widget: "alignment" } }
1726
1629
  },
1727
1630
  spacing: {
1728
1631
  title: "Spacing",
1729
- options: {
1730
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1731
- }
1632
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1732
1633
  }
1733
1634
  },
1734
1635
  defaultValues: {
@@ -1740,46 +1641,23 @@ const videoTool = {
1740
1641
  },
1741
1642
  renderer: {
1742
1643
  renderEditor(values) {
1743
- const padding = values.containerPadding || "10px";
1744
- const url = values.url || "";
1745
- const thumbnail = values.thumbnailUrl || getYouTubeThumbnail(url) || "";
1746
- const textAlign = values.textAlign || "center";
1644
+ const padding = str(values, "containerPadding", "10px");
1645
+ const url = str(values, "url");
1646
+ const thumbnail = str(values, "thumbnailUrl") || getYouTubeThumbnail(url) || "";
1647
+ const align = str(values, "textAlign", "center");
1747
1648
  if (!thumbnail) {
1748
- return html`
1749
- <div style="padding:${padding};text-align:${textAlign};">
1750
- <div style="background:#0f172a;border-radius:8px;padding:40px;text-align:center;color:white;font-family:sans-serif;position:relative;">
1751
- <div style="font-size:48px;opacity:0.8;">▶</div>
1752
- <div style="font-size:12px;margin-top:8px;opacity:0.6;">${url || "Enter video URL"}</div>
1753
- </div>
1754
- </div>
1755
- `;
1649
+ return html`<div style="padding:${padding};text-align:${align};"><div style="background:#0f172a;border-radius:8px;padding:40px;text-align:center;color:white;font-family:sans-serif;"><div style="font-size:48px;opacity:0.8;">▶</div><div style="font-size:12px;margin-top:8px;opacity:0.6;">${url || "Enter video URL"}</div></div></div>`;
1756
1650
  }
1757
- return html`
1758
- <div style="padding:${padding};text-align:${textAlign};">
1759
- <div style="position:relative;display:inline-block;max-width:100%;cursor:pointer;">
1760
- <img src=${thumbnail} alt="Video thumbnail" style="display:block;max-width:100%;border-radius:4px;" />
1761
- <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;">
1762
- <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>
1763
- </div>
1764
- </div>
1765
- </div>
1766
- `;
1651
+ return html`<div style="padding:${padding};text-align:${align};"><div style="position:relative;display:inline-block;max-width:100%;cursor:pointer;"><img src=${thumbnail} alt="Video thumbnail" style="display:block;max-width:100%;border-radius:4px;" /><div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;"><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></div></div></div>`;
1767
1652
  },
1768
1653
  renderHtml(values, ctx) {
1769
- const padding = values.containerPadding || "10px";
1770
- const url = values.url || "#";
1771
- const thumbnail = values.thumbnailUrl || getYouTubeThumbnail(url) || "";
1772
- const alt = values.alt || "Video";
1773
- const textAlign = values.textAlign || "center";
1774
- const widthPx = ctx.columnWidth;
1775
- 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>`;
1776
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1777
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1778
- <a href="${url}" target="_blank" style="text-decoration:none;">
1779
- ${imgTag}
1780
- </a>
1781
- </td></tr></tbody>
1782
- </table>`;
1654
+ const padding = str(values, "containerPadding", "10px");
1655
+ const url = str(values, "url", "#");
1656
+ const thumbnail = str(values, "thumbnailUrl") || getYouTubeThumbnail(url) || "";
1657
+ const alt = str(values, "alt", "Video");
1658
+ const align = str(values, "textAlign", "center");
1659
+ const imgTag = thumbnail ? `<img src="${thumbnail}" alt="${alt}" width="${ctx.columnWidth}" style="display:block;max-width:100%;width:${ctx.columnWidth}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>`;
1660
+ return emailTableCell(`<a href="${url}" target="_blank" style="text-decoration:none;">${imgTag}</a>`, { padding, align });
1783
1661
  }
1784
1662
  }
1785
1663
  };
@@ -1800,7 +1678,7 @@ const timerTool = {
1800
1678
  style: {
1801
1679
  title: "Style",
1802
1680
  options: {
1803
- textAlign: { label: "Align", defaultValue: "center", widget: "text" },
1681
+ textAlign: { label: "Align", defaultValue: "center", widget: "alignment" },
1804
1682
  backgroundColor: { label: "Background", defaultValue: "#1f2937", widget: "color_picker" },
1805
1683
  textColor: { label: "Text Color", defaultValue: "#ffffff", widget: "color_picker" },
1806
1684
  fontSize: { label: "Font Size", defaultValue: "32px", widget: "text" }
@@ -1808,9 +1686,7 @@ const timerTool = {
1808
1686
  },
1809
1687
  spacing: {
1810
1688
  title: "Spacing",
1811
- options: {
1812
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1813
- }
1689
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1814
1690
  }
1815
1691
  },
1816
1692
  defaultValues: {
@@ -1824,39 +1700,25 @@ const timerTool = {
1824
1700
  },
1825
1701
  renderer: {
1826
1702
  renderEditor(values) {
1827
- const padding = values.containerPadding || "10px";
1828
- const bg = values.backgroundColor || "#1f2937";
1829
- const color = values.textColor || "#ffffff";
1830
- const fontSize = values.fontSize || "32px";
1831
- const textAlign = values.textAlign || "center";
1832
- return html`
1833
- <div style="padding:${padding};">
1834
- <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;">
1835
- 00 : 00 : 00 : 00
1836
- <div style="font-size:11px;letter-spacing:8px;opacity:0.6;margin-top:4px;">DAYS &nbsp; HRS &nbsp; MIN &nbsp; SEC</div>
1837
- </div>
1838
- </div>
1839
- `;
1703
+ const padding = str(values, "containerPadding", "10px");
1704
+ const bg = str(values, "backgroundColor", "#1f2937");
1705
+ const color = str(values, "textColor", "#ffffff");
1706
+ const fontSize = str(values, "fontSize", "32px");
1707
+ const align = str(values, "textAlign", "center");
1708
+ return html`<div style="padding:${padding};"><div style="background:${bg};color:${color};font-size:${fontSize};text-align:${align};padding:20px;border-radius:4px;font-family:monospace;font-weight:bold;letter-spacing:4px;">00 : 00 : 00 : 00<div style="font-size:11px;letter-spacing:8px;opacity:0.6;margin-top:4px;">DAYS &nbsp; HRS &nbsp; MIN &nbsp; SEC</div></div></div>`;
1840
1709
  },
1841
1710
  renderHtml(values) {
1842
- const padding = values.containerPadding || "10px";
1843
- const bg = values.backgroundColor || "#1f2937";
1844
- const color = values.textColor || "#ffffff";
1845
- const fontSize = values.fontSize || "32px";
1846
- const textAlign = values.textAlign || "center";
1847
- values.endDate || "";
1848
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1849
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1850
- <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;">
1851
- <div>00 : 00 : 00 : 00</div>
1852
- <div style="font-size:11px;letter-spacing:8px;opacity:0.6;margin-top:4px;">DAYS &nbsp; HRS &nbsp; MIN &nbsp; SEC</div>
1853
- </div>
1854
- </td></tr></tbody>
1855
- </table>`;
1711
+ const padding = str(values, "containerPadding", "10px");
1712
+ const bg = str(values, "backgroundColor", "#1f2937");
1713
+ const color = str(values, "textColor", "#ffffff");
1714
+ const fontSize = str(values, "fontSize", "32px");
1715
+ const align = str(values, "textAlign", "center");
1716
+ const inner = `<div style="background-color:${bg};color:${color};font-size:${fontSize};text-align:${align};padding:20px;border-radius:4px;font-family:'Courier New',monospace;font-weight:bold;letter-spacing:4px;"><div>00 : 00 : 00 : 00</div><div style="font-size:11px;letter-spacing:8px;opacity:0.6;margin-top:4px;">DAYS &nbsp; HRS &nbsp; MIN &nbsp; SEC</div></div>`;
1717
+ return emailTableCell(inner, { padding, align });
1856
1718
  }
1857
1719
  }
1858
1720
  };
1859
- const defaultTableData = [
1721
+ const DEFAULT_DATA = [
1860
1722
  ["Header 1", "Header 2", "Header 3"],
1861
1723
  ["Cell 1", "Cell 2", "Cell 3"],
1862
1724
  ["Cell 4", "Cell 5", "Cell 6"]
@@ -1870,9 +1732,7 @@ const tableTool = {
1870
1732
  options: {
1871
1733
  table: {
1872
1734
  title: "Table",
1873
- options: {
1874
- tableData: { label: "Table Data (JSON)", defaultValue: JSON.stringify(defaultTableData), widget: "rich_text" }
1875
- }
1735
+ options: { tableData: { label: "Table Data (JSON)", defaultValue: JSON.stringify(DEFAULT_DATA), widget: "rich_text" } }
1876
1736
  },
1877
1737
  style: {
1878
1738
  title: "Style",
@@ -1886,13 +1746,11 @@ const tableTool = {
1886
1746
  },
1887
1747
  spacing: {
1888
1748
  title: "Spacing",
1889
- options: {
1890
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1891
- }
1749
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1892
1750
  }
1893
1751
  },
1894
1752
  defaultValues: {
1895
- tableData: JSON.stringify(defaultTableData),
1753
+ tableData: JSON.stringify(DEFAULT_DATA),
1896
1754
  headerBg: "#f3f4f6",
1897
1755
  headerColor: "#111827",
1898
1756
  borderColor: "#e5e7eb",
@@ -1902,69 +1760,42 @@ const tableTool = {
1902
1760
  },
1903
1761
  renderer: {
1904
1762
  renderEditor(values) {
1905
- const padding = values.containerPadding || "10px";
1906
- const headerBg = values.headerBg || "#f3f4f6";
1907
- const headerColor = values.headerColor || "#111827";
1908
- const borderColor = values.borderColor || "#e5e7eb";
1909
- const cellPadding = values.cellPadding || "8px 12px";
1910
- const fontSize = values.fontSize || "14px";
1911
- let data = defaultTableData;
1912
- try {
1913
- data = JSON.parse(values.tableData);
1914
- } catch {
1915
- }
1916
- const headerRow = data[0] || [];
1917
- const bodyRows = data.slice(1);
1763
+ const padding = str(values, "containerPadding", "10px");
1764
+ const hBg = str(values, "headerBg", "#f3f4f6");
1765
+ const hColor = str(values, "headerColor", "#111827");
1766
+ const bColor = str(values, "borderColor", "#e5e7eb");
1767
+ const cPad = str(values, "cellPadding", "8px 12px");
1768
+ const fSize = str(values, "fontSize", "14px");
1769
+ const data = jsonParse(values.tableData, DEFAULT_DATA);
1918
1770
  return html`
1919
1771
  <div style="padding:${padding};overflow-x:auto;">
1920
- <table style="width:100%;border-collapse:collapse;font-size:${fontSize};font-family:arial,sans-serif;">
1921
- <thead>
1922
- <tr>
1923
- ${headerRow.map((cell) => html`<th style="padding:${cellPadding};background:${headerBg};color:${headerColor};border:1px solid ${borderColor};text-align:left;font-weight:600;">${cell}</th>`)}
1924
- </tr>
1925
- </thead>
1926
- <tbody>
1927
- ${bodyRows.map((row) => html`
1928
- <tr>
1929
- ${row.map((cell) => html`<td style="padding:${cellPadding};border:1px solid ${borderColor};">${cell}</td>`)}
1930
- </tr>
1931
- `)}
1932
- </tbody>
1772
+ <table style="width:100%;border-collapse:collapse;font-size:${fSize};font-family:arial,sans-serif;">
1773
+ <thead><tr>${data[0]?.map((c) => html`<th style="padding:${cPad};background:${hBg};color:${hColor};border:1px solid ${bColor};text-align:left;font-weight:600;">${c}</th>`)}</tr></thead>
1774
+ <tbody>${data.slice(1).map((row) => html`<tr>${row.map((c) => html`<td style="padding:${cPad};border:1px solid ${bColor};">${c}</td>`)}</tr>`)}</tbody>
1933
1775
  </table>
1934
1776
  </div>
1935
1777
  `;
1936
1778
  },
1937
1779
  renderHtml(values) {
1938
- const padding = values.containerPadding || "10px";
1939
- const headerBg = values.headerBg || "#f3f4f6";
1940
- const headerColor = values.headerColor || "#111827";
1941
- const borderColor = values.borderColor || "#e5e7eb";
1942
- const cellPadding = values.cellPadding || "8px 12px";
1943
- const fontSize = values.fontSize || "14px";
1944
- let data = defaultTableData;
1945
- try {
1946
- data = JSON.parse(values.tableData);
1947
- } catch {
1948
- }
1949
- const headerRow = data[0] || [];
1950
- const bodyRows = data.slice(1);
1951
- const headerHtml = headerRow.map(
1952
- (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>`
1953
- ).join("");
1954
- const bodyHtml = bodyRows.map(
1955
- (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>`
1956
- ).join("");
1957
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1958
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
1959
- <table cellpadding="0" cellspacing="0" width="100%" border="0" style="border-collapse:collapse;">
1960
- <thead><tr>${headerHtml}</tr></thead>
1961
- <tbody>${bodyHtml}</tbody>
1962
- </table>
1963
- </td></tr></tbody>
1964
- </table>`;
1780
+ const padding = str(values, "containerPadding", "10px");
1781
+ const hBg = str(values, "headerBg", "#f3f4f6");
1782
+ const hColor = str(values, "headerColor", "#111827");
1783
+ const bColor = str(values, "borderColor", "#e5e7eb");
1784
+ const cPad = str(values, "cellPadding", "8px 12px");
1785
+ const fSize = str(values, "fontSize", "14px");
1786
+ const data = jsonParse(values.tableData, DEFAULT_DATA);
1787
+ const font = "font-family:arial,helvetica,sans-serif;";
1788
+ const hCells = (data[0] || []).map((c) => `<th style="padding:${cPad};background-color:${hBg};color:${hColor};border:1px solid ${bColor};text-align:left;font-weight:600;${font}font-size:${fSize};">${c}</th>`).join("");
1789
+ const bRows = data.slice(1).map((row) => `<tr>${row.map((c) => `<td style="padding:${cPad};border:1px solid ${bColor};${font}font-size:${fSize};">${c}</td>`).join("")}</tr>`).join("");
1790
+ const inner = `<table cellpadding="0" cellspacing="0" width="100%" border="0" style="border-collapse:collapse;"><thead><tr>${hCells}</tr></thead><tbody>${bRows}</tbody></table>`;
1791
+ return emailTableCell(inner, { padding });
1965
1792
  }
1966
1793
  }
1967
1794
  };
1795
+ const DEFAULT_FIELDS = [
1796
+ { label: "Name", name: "name", type: "text", placeholder: "Your name" },
1797
+ { label: "Email", name: "email", type: "email", placeholder: "your@email.com" }
1798
+ ];
1968
1799
  const formTool = {
1969
1800
  name: "form",
1970
1801
  label: "Form",
@@ -1978,10 +1809,7 @@ const formTool = {
1978
1809
  actionUrl: { label: "Action URL", defaultValue: "#", widget: "text" },
1979
1810
  method: { label: "Method", defaultValue: "POST", widget: "text" },
1980
1811
  submitText: { label: "Submit Text", defaultValue: "Submit", widget: "text" },
1981
- fields: { label: "Fields (JSON)", defaultValue: JSON.stringify([
1982
- { label: "Name", name: "name", type: "text", placeholder: "Your name" },
1983
- { label: "Email", name: "email", type: "email", placeholder: "your@email.com" }
1984
- ]), widget: "rich_text" }
1812
+ fields: { label: "Fields (JSON)", defaultValue: JSON.stringify(DEFAULT_FIELDS), widget: "rich_text" }
1985
1813
  }
1986
1814
  },
1987
1815
  style: {
@@ -1993,34 +1821,25 @@ const formTool = {
1993
1821
  },
1994
1822
  spacing: {
1995
1823
  title: "Spacing",
1996
- options: {
1997
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1998
- }
1824
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1999
1825
  }
2000
1826
  },
2001
1827
  defaultValues: {
2002
1828
  actionUrl: "#",
2003
1829
  method: "POST",
2004
1830
  submitText: "Submit",
2005
- fields: JSON.stringify([
2006
- { label: "Name", name: "name", type: "text", placeholder: "Your name" },
2007
- { label: "Email", name: "email", type: "email", placeholder: "your@email.com" }
2008
- ]),
1831
+ fields: JSON.stringify(DEFAULT_FIELDS),
2009
1832
  buttonBg: "#3b82f6",
2010
1833
  buttonColor: "#ffffff",
2011
1834
  containerPadding: "10px"
2012
1835
  },
2013
1836
  renderer: {
2014
1837
  renderEditor(values) {
2015
- const padding = values.containerPadding || "10px";
2016
- const submitText = values.submitText || "Submit";
2017
- const buttonBg = values.buttonBg || "#3b82f6";
2018
- const buttonColor = values.buttonColor || "#ffffff";
2019
- let fields = [];
2020
- try {
2021
- fields = JSON.parse(values.fields);
2022
- } catch {
2023
- }
1838
+ const padding = str(values, "containerPadding", "10px");
1839
+ const submitText = str(values, "submitText", "Submit");
1840
+ const btnBg = str(values, "buttonBg", "#3b82f6");
1841
+ const btnColor = str(values, "buttonColor", "#ffffff");
1842
+ const fields = jsonParse(values.fields, DEFAULT_FIELDS);
2024
1843
  return html`
2025
1844
  <div style="padding:${padding};font-family:arial,sans-serif;">
2026
1845
  ${fields.map((f) => html`
@@ -2029,36 +1848,24 @@ const formTool = {
2029
1848
  <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;" />
2030
1849
  </div>
2031
1850
  `)}
2032
- <button style="background:${buttonBg};color:${buttonColor};border:none;padding:10px 24px;border-radius:4px;font-size:14px;font-weight:600;cursor:pointer;">${submitText}</button>
1851
+ <button style="background:${btnBg};color:${btnColor};border:none;padding:10px 24px;border-radius:4px;font-size:14px;font-weight:600;cursor:pointer;">${submitText}</button>
2033
1852
  </div>
2034
1853
  `;
2035
1854
  },
2036
1855
  renderHtml(values) {
2037
- const padding = values.containerPadding || "10px";
2038
- const actionUrl = values.actionUrl || "#";
2039
- const method = values.method || "POST";
2040
- const submitText = values.submitText || "Submit";
2041
- const buttonBg = values.buttonBg || "#3b82f6";
2042
- const buttonColor = values.buttonColor || "#ffffff";
2043
- let fields = [];
2044
- try {
2045
- fields = JSON.parse(values.fields);
2046
- } catch {
2047
- }
1856
+ const padding = str(values, "containerPadding", "10px");
1857
+ const actionUrl = str(values, "actionUrl", "#");
1858
+ const method = str(values, "method", "POST");
1859
+ const submitText = str(values, "submitText", "Submit");
1860
+ const btnBg = str(values, "buttonBg", "#3b82f6");
1861
+ const btnColor = str(values, "buttonColor", "#ffffff");
1862
+ const fields = jsonParse(values.fields, DEFAULT_FIELDS);
1863
+ const font = "font-family:arial,helvetica,sans-serif;";
2048
1864
  const fieldsHtml = fields.map(
2049
- (f) => `<div style="margin-bottom:12px;">
2050
- <label style="display:block;font-size:13px;color:#374151;margin-bottom:4px;font-weight:500;font-family:arial,helvetica,sans-serif;">${f.label}</label>
2051
- <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;" />
2052
- </div>`
1865
+ (f) => `<div style="margin-bottom:12px;"><label style="display:block;font-size:13px;color:#374151;margin-bottom:4px;font-weight:500;${font}">${f.label}</label><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}" /></div>`
2053
1866
  ).join("");
2054
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
2055
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
2056
- <form action="${actionUrl}" method="${method}">
2057
- ${fieldsHtml}
2058
- <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>
2059
- </form>
2060
- </td></tr></tbody>
2061
- </table>`;
1867
+ const inner = `<form action="${actionUrl}" method="${method}">${fieldsHtml}<button type="submit" style="background-color:${btnBg};color:${btnColor};border:none;padding:10px 24px;border-radius:4px;font-size:14px;font-weight:600;cursor:pointer;${font}">${submitText}</button></form>`;
1868
+ return emailTableCell(inner, { padding });
2062
1869
  }
2063
1870
  }
2064
1871
  };
@@ -2328,7 +2135,7 @@ let MailEditorElement = class extends LitElement {
2328
2135
  this.removeEventListener("keydown", this._handleKeydown);
2329
2136
  }
2330
2137
  // ----------------------------------------------------------
2331
- // Public API — mirrors Unlayer
2138
+ // Public API — public API
2332
2139
  // ----------------------------------------------------------
2333
2140
  loadDesign(design) {
2334
2141
  this.store.loadDesign(design);
@@ -2435,4 +2242,4 @@ export {
2435
2242
  MailEditorElement as M,
2436
2243
  ToolRegistry as T
2437
2244
  };
2438
- //# sourceMappingURL=mail-editor-CoB2C5CT.js.map
2245
+ //# sourceMappingURL=mail-editor-D0FbEUZu.js.map