@emabuild/core 0.0.1 → 0.0.3

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 (72) hide show
  1. package/dist/canvas/row-renderer.d.ts +4 -0
  2. package/dist/canvas/row-renderer.d.ts.map +1 -1
  3. package/dist/dnd/drag-manager.d.ts +13 -14
  4. package/dist/dnd/drag-manager.d.ts.map +1 -1
  5. package/dist/dnd/drop-indicator.d.ts +24 -0
  6. package/dist/dnd/drop-indicator.d.ts.map +1 -0
  7. package/dist/dnd/shadow-dom-utils.d.ts +29 -0
  8. package/dist/dnd/shadow-dom-utils.d.ts.map +1 -0
  9. package/dist/index.js +1 -1
  10. package/dist/{mail-editor-DzI7SmSe.js → mail-editor-ClkIyPni.js} +830 -871
  11. package/dist/mail-editor-ClkIyPni.js.map +1 -0
  12. package/dist/mail-editor.d.ts +1 -0
  13. package/dist/mail-editor.d.ts.map +1 -1
  14. package/dist/mail-editor.js +1 -1
  15. package/dist/properties/property-panel.d.ts +4 -6
  16. package/dist/properties/property-panel.d.ts.map +1 -1
  17. package/dist/properties/widgets/alignment-widget.d.ts +4 -0
  18. package/dist/properties/widgets/alignment-widget.d.ts.map +1 -0
  19. package/dist/properties/widgets/color-picker-widget.d.ts +4 -0
  20. package/dist/properties/widgets/color-picker-widget.d.ts.map +1 -0
  21. package/dist/properties/widgets/dropdown-widget.d.ts +4 -0
  22. package/dist/properties/widgets/dropdown-widget.d.ts.map +1 -0
  23. package/dist/properties/widgets/index.d.ts +8 -0
  24. package/dist/properties/widgets/index.d.ts.map +1 -0
  25. package/dist/properties/widgets/padding-widget.d.ts +4 -0
  26. package/dist/properties/widgets/padding-widget.d.ts.map +1 -0
  27. package/dist/properties/widgets/text-input-widget.d.ts +4 -0
  28. package/dist/properties/widgets/text-input-widget.d.ts.map +1 -0
  29. package/dist/properties/widgets/textarea-widget.d.ts +4 -0
  30. package/dist/properties/widgets/textarea-widget.d.ts.map +1 -0
  31. package/dist/properties/widgets/toggle-widget.d.ts +4 -0
  32. package/dist/properties/widgets/toggle-widget.d.ts.map +1 -0
  33. package/dist/sidebar/body-settings.d.ts +15 -0
  34. package/dist/sidebar/body-settings.d.ts.map +1 -0
  35. package/dist/sidebar/editor-sidebar.d.ts.map +1 -1
  36. package/dist/state/design-factory.d.ts +9 -0
  37. package/dist/state/design-factory.d.ts.map +1 -0
  38. package/dist/state/design-lookup.d.ts +14 -0
  39. package/dist/state/design-lookup.d.ts.map +1 -0
  40. package/dist/state/editor-store.d.ts +26 -7
  41. package/dist/state/editor-store.d.ts.map +1 -1
  42. package/dist/state/history-manager.d.ts +20 -0
  43. package/dist/state/history-manager.d.ts.map +1 -0
  44. package/dist/tools/built-in/button-tool.d.ts.map +1 -1
  45. package/dist/tools/built-in/divider-tool.d.ts.map +1 -1
  46. package/dist/tools/built-in/form-tool.d.ts.map +1 -1
  47. package/dist/tools/built-in/heading-tool.d.ts.map +1 -1
  48. package/dist/tools/built-in/html-tool.d.ts.map +1 -1
  49. package/dist/tools/built-in/image-tool.d.ts.map +1 -1
  50. package/dist/tools/built-in/menu-tool.d.ts.map +1 -1
  51. package/dist/tools/built-in/paragraph-tool.d.ts.map +1 -1
  52. package/dist/tools/built-in/social-tool.d.ts.map +1 -1
  53. package/dist/tools/built-in/table-tool.d.ts.map +1 -1
  54. package/dist/tools/built-in/text-tool.d.ts.map +1 -1
  55. package/dist/tools/built-in/timer-tool.d.ts.map +1 -1
  56. package/dist/tools/built-in/video-tool.d.ts.map +1 -1
  57. package/dist/tools/helpers/email-html.d.ts +55 -0
  58. package/dist/tools/helpers/email-html.d.ts.map +1 -0
  59. package/dist/tools/helpers/index.d.ts +5 -0
  60. package/dist/tools/helpers/index.d.ts.map +1 -0
  61. package/dist/tools/helpers/types.d.ts +36 -0
  62. package/dist/tools/helpers/types.d.ts.map +1 -0
  63. package/dist/tools/helpers/value-extractor.d.ts +31 -0
  64. package/dist/tools/helpers/value-extractor.d.ts.map +1 -0
  65. package/dist/utils/event-emitter.d.ts +32 -4
  66. package/dist/utils/event-emitter.d.ts.map +1 -1
  67. package/dist/utils/id-generator.d.ts +17 -3
  68. package/dist/utils/id-generator.d.ts.map +1 -1
  69. package/package.json +3 -3
  70. package/dist/mail-editor-DzI7SmSe.js.map +0 -1
  71. package/dist/utils/deep-clone.d.ts +0 -2
  72. 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,67 +328,95 @@ 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 */
366
+ duplicateRow(rowId) {
367
+ const row = findRow(this.design, rowId);
368
+ if (!row) return;
369
+ this.history.push(this.design);
370
+ const cloned = structuredClone(row);
371
+ const rowNum = this.counterManager.next("u_row");
372
+ cloned.id = `u_row_${rowNum}`;
373
+ cloned.values._meta = { htmlID: cloned.id, htmlClassNames: "u_row" };
374
+ for (const col of cloned.columns) {
375
+ const colNum = this.counterManager.next("u_column");
376
+ col.id = `u_column_${colNum}`;
377
+ col.values._meta = { htmlID: col.id, htmlClassNames: "u_column" };
378
+ for (const content of col.contents) {
379
+ const cNum = this.counterManager.next(`u_content_${content.type}`);
380
+ content.id = `u_content_${content.type}_${cNum}`;
381
+ content.values._meta = { htmlID: content.id, htmlClassNames: `u_content_${content.type}` };
382
+ }
383
+ }
384
+ const idx = getRowIndex(this.design, rowId);
385
+ this.design.body.rows.splice(idx + 1, 0, cloned);
386
+ this.syncCounters();
387
+ this.notify();
388
+ this.emitUpdate("row_added", cloned);
389
+ }
390
+ /** Get the index of a row */
391
+ getRowIndex(rowId) {
392
+ return getRowIndex(this.design, rowId);
393
+ }
394
+ /** Update row-level values */
249
395
  updateRowValues(rowId, patch) {
250
- const row = this.findRow(rowId);
396
+ const row = findRow(this.design, rowId);
251
397
  if (!row) return;
252
- this.pushHistory();
398
+ this.history.push(this.design);
253
399
  Object.assign(row.values, patch);
254
400
  this.notify();
255
401
  this.emitUpdate("content_updated");
256
402
  }
257
- // ----------------------------------------------------------
258
- // Column operations
259
- // ----------------------------------------------------------
403
+ // ── Column Operations ──────────────────────────────────────
404
+ /** Update column-level values */
260
405
  updateColumnValues(columnId, patch) {
261
- const col = this.findColumn(columnId);
406
+ const col = findColumn(this.design, columnId);
262
407
  if (!col) return;
263
- this.pushHistory();
408
+ this.history.push(this.design);
264
409
  Object.assign(col.values, patch);
265
410
  this.notify();
266
411
  this.emitUpdate("content_updated");
267
412
  }
268
- // ----------------------------------------------------------
269
- // Content operations
270
- // ----------------------------------------------------------
413
+ // ── Content Operations ─────────────────────────────────────
414
+ /** Add content to a column at the given index */
271
415
  addContent(columnId, content, index) {
272
- const col = this.findColumn(columnId);
416
+ const col = findColumn(this.design, columnId);
273
417
  if (!col) return;
274
- this.pushHistory();
275
- const cloned = deepClone(content);
418
+ this.history.push(this.design);
419
+ const cloned = structuredClone(content);
276
420
  if (index !== void 0 && index >= 0 && index <= col.contents.length) {
277
421
  col.contents.splice(index, 0, cloned);
278
422
  } else {
@@ -282,12 +426,13 @@ class EditorStore {
282
426
  this.notify();
283
427
  this.emitUpdate("content_added", cloned);
284
428
  }
429
+ /** Remove a content block by ID */
285
430
  removeContent(contentId) {
286
431
  for (const row of this.design.body.rows) {
287
432
  for (const col of row.columns) {
288
433
  const idx = col.contents.findIndex((c) => c.id === contentId);
289
434
  if (idx !== -1) {
290
- this.pushHistory();
435
+ this.history.push(this.design);
291
436
  col.contents.splice(idx, 1);
292
437
  if (this._selectedId === contentId) this._selectedId = null;
293
438
  this.notify();
@@ -297,49 +442,47 @@ class EditorStore {
297
442
  }
298
443
  }
299
444
  }
445
+ /** Update content values by ID */
300
446
  updateContentValues(contentId, patch) {
301
- const content = this.findContent(contentId);
447
+ const content = findContent(this.design, contentId);
302
448
  if (!content) return;
303
- this.pushHistory();
449
+ this.history.push(this.design);
304
450
  Object.assign(content.values, patch);
305
451
  this.notify();
306
452
  this.emitUpdate("content_updated");
307
453
  }
454
+ /** Move a content block to a different column at a given index */
308
455
  moveContent(contentId, targetColumnId, targetIndex) {
309
- const content = this.findContent(contentId);
456
+ const content = findContent(this.design, contentId);
310
457
  if (!content) return;
458
+ this.history.push(this.design);
311
459
  for (const row of this.design.body.rows) {
312
460
  for (const col of row.columns) {
313
461
  const idx = col.contents.findIndex((c) => c.id === contentId);
314
462
  if (idx !== -1) {
315
- this.pushHistory();
316
463
  col.contents.splice(idx, 1);
317
464
  break;
318
465
  }
319
466
  }
320
467
  }
321
- const targetCol = this.findColumn(targetColumnId);
322
- if (targetCol) {
323
- targetCol.contents.splice(targetIndex, 0, content);
324
- }
468
+ const targetCol = findColumn(this.design, targetColumnId);
469
+ if (targetCol) targetCol.contents.splice(targetIndex, 0, content);
325
470
  this.notify();
326
471
  this.emitUpdate("content_reordered");
327
472
  }
473
+ /** Duplicate a content block, inserting the copy right after the original */
328
474
  duplicateContent(contentId) {
329
- const content = this.findContent(contentId);
475
+ const content = findContent(this.design, contentId);
330
476
  if (!content) return;
331
477
  for (const row of this.design.body.rows) {
332
478
  for (const col of row.columns) {
333
479
  const idx = col.contents.findIndex((c) => c.id === contentId);
334
480
  if (idx !== -1) {
335
- const cloned = deepClone(content);
481
+ this.history.push(this.design);
482
+ const cloned = structuredClone(content);
336
483
  const counter = this.counterManager.next(`u_content_${content.type}`);
337
484
  cloned.id = `u_content_${content.type}_${counter}`;
338
- cloned.values._meta = {
339
- htmlID: cloned.id,
340
- htmlClassNames: `u_content_${content.type}`
341
- };
342
- this.pushHistory();
485
+ cloned.values._meta = { htmlID: cloned.id, htmlClassNames: `u_content_${content.type}` };
343
486
  col.contents.splice(idx + 1, 0, cloned);
344
487
  this.syncCounters();
345
488
  this.notify();
@@ -349,109 +492,44 @@ class EditorStore {
349
492
  }
350
493
  }
351
494
  }
352
- // ----------------------------------------------------------
353
- // Body values
354
- // ----------------------------------------------------------
495
+ // ── Body Values ────────────────────────────────────────────
496
+ /** Update body-level values (background, fonts, etc.) */
355
497
  updateBodyValues(patch) {
356
- this.pushHistory();
498
+ this.history.push(this.design);
357
499
  Object.assign(this.design.body.values, patch);
358
500
  this.notify();
359
501
  this.emitUpdate("body_updated");
360
502
  }
361
- // ----------------------------------------------------------
362
- // Lookup helpers
363
- // ----------------------------------------------------------
503
+ // ── Lookups (delegate to design-lookup) ────────────────────
364
504
  findRow(rowId) {
365
- return this.design.body.rows.find((r) => r.id === rowId);
505
+ return findRow(this.design, rowId);
366
506
  }
367
507
  findColumn(columnId) {
368
- for (const row of this.design.body.rows) {
369
- const col = row.columns.find((c) => c.id === columnId);
370
- if (col) return col;
371
- }
372
- return void 0;
508
+ return findColumn(this.design, columnId);
373
509
  }
374
510
  findContent(contentId) {
375
- for (const row of this.design.body.rows) {
376
- for (const col of row.columns) {
377
- const content = col.contents.find((c) => c.id === contentId);
378
- if (content) return content;
379
- }
380
- }
381
- return void 0;
511
+ return findContent(this.design, contentId);
382
512
  }
383
513
  findParentColumn(contentId) {
384
- for (const row of this.design.body.rows) {
385
- for (const col of row.columns) {
386
- if (col.contents.some((c) => c.id === contentId)) return col;
387
- }
388
- }
389
- return void 0;
514
+ return findParentColumn(this.design, contentId);
390
515
  }
391
516
  findParentRow(columnId) {
392
- for (const row of this.design.body.rows) {
393
- if (row.columns.some((c) => c.id === columnId)) return row;
394
- }
395
- return void 0;
517
+ return findParentRow(this.design, columnId);
396
518
  }
397
- // ----------------------------------------------------------
398
- // Helpers
399
- // ----------------------------------------------------------
400
- /** 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 */
401
521
  createRow(cellProportions) {
402
- const rowNum = this.counterManager.next("u_row");
403
- const columns = cellProportions.map((_, i) => {
404
- const colNum = this.counterManager.next("u_column");
405
- return {
406
- id: `u_column_${colNum}`,
407
- contents: [],
408
- values: {
409
- backgroundColor: "",
410
- padding: "0px",
411
- border: {},
412
- borderRadius: "0px",
413
- _meta: { htmlID: `u_column_${colNum}`, htmlClassNames: "u_column" }
414
- }
415
- };
416
- });
522
+ const row = createRow(this.counterManager, cellProportions);
417
523
  this.syncCounters();
418
- return {
419
- id: `u_row_${rowNum}`,
420
- cells: cellProportions,
421
- columns,
422
- values: {
423
- displayCondition: null,
424
- columns: false,
425
- backgroundColor: "",
426
- columnsBackgroundColor: "",
427
- backgroundImage: { url: "", fullWidth: true, repeat: false, center: true, cover: false },
428
- padding: "0px",
429
- anchor: "",
430
- hideDesktop: false,
431
- hideMobile: false,
432
- _meta: { htmlID: `u_row_${rowNum}`, htmlClassNames: "u_row" }
433
- }
434
- };
524
+ return row;
435
525
  }
436
- /** Creates a new content block for the given tool type */
526
+ /** Create a new content block for the given tool type */
437
527
  createContent(type, values = {}) {
438
- const counter = this.counterManager.next(`u_content_${type}`);
439
- const id = `u_content_${type}_${counter}`;
528
+ const content = createContent(this.counterManager, type, values);
440
529
  this.syncCounters();
441
- return {
442
- id,
443
- type,
444
- values: {
445
- containerPadding: "10px",
446
- anchor: "",
447
- hideDesktop: false,
448
- hideMobile: false,
449
- displayCondition: null,
450
- _meta: { htmlID: id, htmlClassNames: `u_content_${type}` },
451
- ...values
452
- }
453
- };
530
+ return content;
454
531
  }
532
+ // ── Private Helpers ────────────────────────────────────────
455
533
  syncCounters() {
456
534
  this.design.counters = this.counterManager.getCounters();
457
535
  }
@@ -502,14 +580,72 @@ const dragState = {
502
580
  this.draggingContentId = null;
503
581
  }
504
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
+ }
505
643
  class DragManager {
506
644
  constructor(store, toolRegistry, root) {
507
645
  this.currentDrop = null;
508
- this.indicator = null;
646
+ this.contentIndicator = null;
509
647
  this.rowIndicator = null;
510
- this.handleDragStart = (_e) => {
511
- };
512
- this.handleDragOver = (e) => {
648
+ this.onDragOver = (e) => {
513
649
  const types = e.dataTransfer?.types || [];
514
650
  const isToolDrag = types.includes("application/maileditor-tool");
515
651
  const isLayoutDrag = types.includes("application/maileditor-layout");
@@ -518,76 +654,42 @@ class DragManager {
518
654
  e.preventDefault();
519
655
  e.dataTransfer.dropEffect = isToolDrag || isLayoutDrag ? "copy" : "move";
520
656
  if (isLayoutDrag) {
521
- const drop = this.findRowDropPosition(e.clientY);
522
- this.hideIndicator();
523
- if (drop) {
524
- this.currentDrop = drop;
525
- this.showRowIndicator(drop);
526
- } else {
527
- this.hideRowIndicator();
528
- this.currentDrop = null;
529
- }
657
+ this.currentDrop = this.findRowDropTarget(e.clientY);
658
+ hideIndicator(this.contentIndicator);
659
+ this.showRowIndicator();
530
660
  } else {
531
- const drop = this.findContentDropPosition(e.clientX, e.clientY);
532
- this.hideRowIndicator();
533
- if (drop) {
534
- this.currentDrop = drop;
535
- this.showContentIndicator(drop);
536
- } else {
537
- this.hideIndicator();
538
- this.currentDrop = null;
539
- }
661
+ this.currentDrop = this.findContentDropTarget(e.clientX, e.clientY);
662
+ hideIndicator(this.rowIndicator);
663
+ this.showContentIndicator();
540
664
  }
541
665
  };
542
- this.handleDrop = (e) => {
666
+ this.onDrop = (e) => {
543
667
  e.preventDefault();
544
- this.hideIndicator();
545
- this.hideRowIndicator();
668
+ this.hideAllIndicators();
546
669
  const layoutData = e.dataTransfer?.getData("application/maileditor-layout");
547
670
  if (layoutData) {
548
- const cells = JSON.parse(layoutData);
549
- const row = this.store.createRow(cells);
550
- const rowIndex = this.currentDrop?.type === "row" ? this.currentDrop.rowIndex : void 0;
551
- this.store.addRow(row, rowIndex);
552
- this.reset();
553
- return;
671
+ this.handleLayoutDrop(JSON.parse(layoutData));
672
+ return this.reset();
554
673
  }
555
674
  const toolName = e.dataTransfer?.getData("application/maileditor-tool");
556
675
  if (toolName) {
557
- if (this.currentDrop?.type === "content" && this.currentDrop.columnId) {
558
- const defaults = this.toolRegistry.getDefaultValues(toolName);
559
- const content = this.store.createContent(toolName, defaults);
560
- this.store.addContent(this.currentDrop.columnId, content, this.currentDrop.contentIndex);
561
- this.store.select(content.id);
562
- } else {
563
- const row = this.store.createRow([1]);
564
- this.store.addRow(row);
565
- const defaults = this.toolRegistry.getDefaultValues(toolName);
566
- const content = this.store.createContent(toolName, defaults);
567
- this.store.addContent(row.columns[0].id, content);
568
- this.store.select(content.id);
569
- }
570
- this.reset();
571
- return;
676
+ this.handleToolDrop(toolName);
677
+ return this.reset();
572
678
  }
573
679
  const contentId = e.dataTransfer?.getData("application/maileditor-content") || dragState.draggingContentId;
574
- if (contentId && this.currentDrop?.type === "content" && this.currentDrop.columnId) {
575
- this.store.moveContent(contentId, this.currentDrop.columnId, this.currentDrop.contentIndex);
576
- this.store.select(contentId);
680
+ if (contentId) {
681
+ this.handleContentDrop(contentId);
577
682
  }
578
683
  this.reset();
579
- dragState.reset();
580
684
  };
581
- this.handleDragEnd = () => {
582
- this.hideIndicator();
583
- this.hideRowIndicator();
685
+ this.onDragEnd = () => {
686
+ this.hideAllIndicators();
584
687
  this.reset();
585
688
  };
586
- this.handleDragLeave = (e) => {
689
+ this.onDragLeave = (e) => {
587
690
  const related = e.relatedTarget;
588
691
  if (!related || !this.root.contains(related)) {
589
- this.hideIndicator();
590
- this.hideRowIndicator();
692
+ this.hideAllIndicators();
591
693
  this.currentDrop = null;
592
694
  }
593
695
  };
@@ -595,56 +697,61 @@ class DragManager {
595
697
  this.toolRegistry = toolRegistry;
596
698
  this.root = root;
597
699
  }
700
+ /** Attach all drag event listeners to the shadow root */
598
701
  attach() {
599
- this.root.addEventListener("dragover", this.handleDragOver);
600
- this.root.addEventListener("drop", this.handleDrop);
601
- this.root.addEventListener("dragend", this.handleDragEnd);
602
- this.root.addEventListener("dragleave", this.handleDragLeave);
603
- this.root.addEventListener("dragstart", this.handleDragStart);
604
- this.indicator = this.createIndicator("#3b82f6");
605
- this.rowIndicator = this.createIndicator("#8b5cf6");
606
- }
607
- createIndicator(color) {
608
- const el = document.createElement("div");
609
- Object.assign(el.style, {
610
- position: "absolute",
611
- left: "0",
612
- right: "0",
613
- height: "3px",
614
- background: color,
615
- borderRadius: "2px",
616
- pointerEvents: "none",
617
- zIndex: "1000",
618
- display: "none",
619
- boxShadow: `0 0 6px ${color}80`
620
- });
621
- return el;
622
- }
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 */
623
710
  detach() {
624
- this.root.removeEventListener("dragover", this.handleDragOver);
625
- this.root.removeEventListener("drop", this.handleDrop);
626
- this.root.removeEventListener("dragend", this.handleDragEnd);
627
- this.root.removeEventListener("dragleave", this.handleDragLeave);
628
- this.root.removeEventListener("dragstart", this.handleDragStart);
629
- this.indicator?.remove();
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();
630
716
  this.rowIndicator?.remove();
631
717
  }
632
- // ----------------------------------------------------------
633
- // Find row drop position (between rows)
634
- // ----------------------------------------------------------
635
- 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) {
636
747
  const canvas = this.root.querySelector("me-editor-canvas");
637
748
  if (!canvas?.shadowRoot) return null;
638
749
  const rows = Array.from(canvas.shadowRoot.querySelectorAll("me-row-renderer"));
639
- if (rows.length === 0) {
640
- return { type: "row", rowIndex: 0, y: 0 };
641
- }
642
- const firstRect = rows[0].getBoundingClientRect();
643
- let bestDist = Math.abs(clientY - firstRect.top);
644
- let bestTarget = { type: "row", rowIndex: 0, y: firstRect.top };
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 };
645
753
  for (let i = 0; i < rows.length; i++) {
646
- const rect = rows[i].getBoundingClientRect();
647
- const y = rect.bottom;
754
+ const y = rows[i].getBoundingClientRect().bottom;
648
755
  const dist = Math.abs(clientY - y);
649
756
  if (dist < bestDist) {
650
757
  bestDist = dist;
@@ -653,21 +760,16 @@ class DragManager {
653
760
  }
654
761
  return bestTarget;
655
762
  }
656
- // ----------------------------------------------------------
657
- // Find content drop position (inside columns)
658
- // ----------------------------------------------------------
659
- findContentDropPosition(clientX, clientY) {
660
- const columns = this.queryShadowAll(this.root, "me-column-renderer");
763
+ findContentDropTarget(clientX, clientY) {
764
+ const columns = queryShadowAll(this.root, "me-column-renderer");
661
765
  let bestTarget = null;
662
766
  let bestDist = Infinity;
663
767
  for (const colEl of columns) {
664
768
  const columnId = colEl.dataset.columnId;
665
- if (!columnId) continue;
666
- const colShadow = colEl.shadowRoot;
667
- if (!colShadow) continue;
769
+ if (!columnId || !colEl.shadowRoot) continue;
668
770
  const colRect = colEl.getBoundingClientRect();
669
771
  if (clientX < colRect.left || clientX > colRect.right) continue;
670
- const contentEls = Array.from(colShadow.querySelectorAll("me-content-renderer"));
772
+ const contentEls = Array.from(colEl.shadowRoot.querySelectorAll("me-content-renderer"));
671
773
  if (contentEls.length === 0) {
672
774
  const dist2 = Math.abs(clientY - (colRect.top + colRect.height / 2));
673
775
  if (dist2 < bestDist) {
@@ -676,11 +778,11 @@ class DragManager {
676
778
  }
677
779
  continue;
678
780
  }
679
- const firstRect = contentEls[0].getBoundingClientRect();
680
- let dist = Math.abs(clientY - firstRect.top);
781
+ const firstY = contentEls[0].getBoundingClientRect().top;
782
+ let dist = Math.abs(clientY - firstY);
681
783
  if (dist < bestDist) {
682
784
  bestDist = dist;
683
- bestTarget = { type: "content", columnId, contentIndex: 0, y: firstRect.top };
785
+ bestTarget = { type: "content", columnId, contentIndex: 0, y: firstY };
684
786
  }
685
787
  for (let i = 0; i < contentEls.length; i++) {
686
788
  const rect = contentEls[i].getBoundingClientRect();
@@ -695,113 +797,72 @@ class DragManager {
695
797
  }
696
798
  return bestTarget;
697
799
  }
698
- // ----------------------------------------------------------
699
- // Indicator positioning
700
- // ----------------------------------------------------------
701
- showContentIndicator(drop) {
702
- if (!this.indicator || !drop.columnId) return;
703
- const columns = this.queryShadowAll(this.root, "me-column-renderer");
704
- const colEl = columns.find((c) => c.dataset.columnId === drop.columnId);
705
- if (!colEl?.shadowRoot) return;
706
- if (this.indicator.parentNode !== colEl.shadowRoot) {
707
- this.indicator.remove();
708
- colEl.shadowRoot.appendChild(this.indicator);
800
+ // ── Indicator Positioning ──────────────────────────────────
801
+ showContentIndicator() {
802
+ if (!this.contentIndicator || !this.currentDrop?.columnId) {
803
+ hideIndicator(this.contentIndicator);
804
+ return;
709
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;
710
809
  const contentEls = Array.from(colEl.shadowRoot.querySelectorAll("me-content-renderer"));
711
- const colRect = colEl.getBoundingClientRect();
712
- let indicatorY;
713
- if (contentEls.length === 0 || drop.contentIndex === 0) {
714
- indicatorY = 0;
715
- } else if (drop.contentIndex >= contentEls.length) {
716
- const lastRect = contentEls[contentEls.length - 1].getBoundingClientRect();
717
- indicatorY = lastRect.bottom - colRect.top;
718
- } else {
719
- const elRect = contentEls[drop.contentIndex].getBoundingClientRect();
720
- indicatorY = elRect.top - colRect.top;
721
- }
722
- Object.assign(this.indicator.style, {
723
- display: "block",
724
- top: `${indicatorY}px`,
725
- left: "4px",
726
- right: "4px",
727
- width: "auto"
728
- });
810
+ positionIndicator(this.contentIndicator, colEl.shadowRoot, contentEls, this.currentDrop.contentIndex ?? 0, "4px");
729
811
  }
730
- showRowIndicator(drop) {
731
- if (!this.rowIndicator) return;
812
+ showRowIndicator() {
813
+ if (!this.rowIndicator || !this.currentDrop) {
814
+ hideIndicator(this.rowIndicator);
815
+ return;
816
+ }
732
817
  const canvas = this.root.querySelector("me-editor-canvas");
733
- if (!canvas?.shadowRoot) return;
734
- const canvasBody = canvas.shadowRoot.querySelector(".canvas-body");
818
+ const canvasBody = canvas?.shadowRoot?.querySelector(".canvas-body");
735
819
  if (!canvasBody) return;
736
- if (this.rowIndicator.parentNode !== canvasBody) {
737
- this.rowIndicator.remove();
738
- canvasBody.appendChild(this.rowIndicator);
739
- }
740
820
  const rows = Array.from(canvas.shadowRoot.querySelectorAll("me-row-renderer"));
741
- const bodyRect = canvasBody.getBoundingClientRect();
742
- let indicatorY;
743
- if (rows.length === 0 || drop.rowIndex === 0) {
744
- indicatorY = 0;
745
- } else if (drop.rowIndex >= rows.length) {
746
- const lastRect = rows[rows.length - 1].getBoundingClientRect();
747
- indicatorY = lastRect.bottom - bodyRect.top;
748
- } else {
749
- const elRect = rows[drop.rowIndex].getBoundingClientRect();
750
- indicatorY = elRect.top - bodyRect.top;
751
- }
752
- Object.assign(this.rowIndicator.style, {
753
- display: "block",
754
- top: `${indicatorY}px`,
755
- left: "0",
756
- right: "0",
757
- width: "auto"
758
- });
759
- }
760
- hideIndicator() {
761
- if (this.indicator) this.indicator.style.display = "none";
821
+ positionIndicator(this.rowIndicator, canvasBody, rows, this.currentDrop.rowIndex ?? 0, "0");
762
822
  }
763
- hideRowIndicator() {
764
- if (this.rowIndicator) this.rowIndicator.style.display = "none";
823
+ hideAllIndicators() {
824
+ hideIndicator(this.contentIndicator);
825
+ hideIndicator(this.rowIndicator);
765
826
  }
766
827
  reset() {
767
828
  this.currentDrop = null;
768
- this.hideIndicator();
769
- this.hideRowIndicator();
829
+ dragState.reset();
770
830
  }
771
- // ----------------------------------------------------------
772
- // Shadow DOM traversal
773
- // ----------------------------------------------------------
774
- queryShadowAll(root, selector) {
775
- const results = [];
776
- this.walkShadowDom(root, (el) => {
777
- if (el.matches?.(selector)) results.push(el);
778
- });
779
- return results;
780
- }
781
- walkShadowDom(root, callback) {
782
- const children = root instanceof ShadowRoot ? root.children : root.children;
783
- for (const child of Array.from(children)) {
784
- const el = child;
785
- callback(el);
786
- if (el.shadowRoot) this.walkShadowDom(el.shadowRoot, callback);
787
- if (el.children?.length) this.walkShadowDom(el, callback);
788
- }
789
- }
790
- findContentElement(el) {
791
- while (el) {
792
- if (el.dataset?.contentId) return el;
793
- if (el.tagName?.toLowerCase() === "me-content-renderer") return el;
794
- if (el.parentElement) {
795
- el = el.parentElement;
796
- } else if (el.getRootNode().host) {
797
- el = el.getRootNode().host;
798
- } else {
799
- break;
800
- }
801
- }
802
- return null;
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;
803
843
  }
804
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
+ }
805
866
  const textTool = {
806
867
  name: "text",
807
868
  label: "Text",
@@ -813,7 +874,7 @@ const textTool = {
813
874
  title: "Text",
814
875
  options: {
815
876
  text: {
816
- label: "Text",
877
+ label: "Text Content",
817
878
  defaultValue: '<p style="font-size: 14px;">This is a new text block. Change the text.</p>',
818
879
  widget: "rich_text"
819
880
  }
@@ -824,8 +885,20 @@ const textTool = {
824
885
  options: {
825
886
  color: { label: "Text Color", defaultValue: "#000000", widget: "color_picker" },
826
887
  backgroundColor: { label: "Background Color", defaultValue: "", widget: "color_picker" },
827
- lineHeight: { label: "Line Height", defaultValue: "140%", widget: "text" },
828
- linkStyle: { label: "Link Style", defaultValue: void 0, widget: "link_style" }
888
+ textAlign: { label: "Text Align", defaultValue: "left", widget: "alignment" },
889
+ lineHeight: {
890
+ label: "Line Height",
891
+ defaultValue: "140%",
892
+ widget: "dropdown",
893
+ widgetParams: { options: [
894
+ { label: "100%", value: "100%" },
895
+ { label: "120%", value: "120%" },
896
+ { label: "140%", value: "140%" },
897
+ { label: "160%", value: "160%" },
898
+ { label: "180%", value: "180%" },
899
+ { label: "200%", value: "200%" }
900
+ ] }
901
+ }
829
902
  }
830
903
  },
831
904
  spacing: {
@@ -852,37 +925,28 @@ const textTool = {
852
925
  textAlign: "left"
853
926
  },
854
927
  renderer: {
855
- renderEditor(values, context) {
856
- const padding = values.containerPadding || "10px";
857
- const bgColor = values.backgroundColor || "transparent";
858
- const color = values.color || "inherit";
859
- const lineHeight = values.lineHeight || "140%";
860
- const textContent = values.text || "";
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");
861
934
  return html`
862
935
  <div style="padding:${padding};background-color:${bgColor};color:${color};line-height:${lineHeight};word-break:break-word;">
863
936
  ${unsafeHTML(textContent)}
864
937
  </div>
865
938
  `;
866
939
  },
867
- renderHtml(values, context) {
868
- const padding = values.containerPadding || "10px";
869
- const bgColor = values.backgroundColor || "";
870
- const color = values.color || "#000000";
871
- const lineHeight = values.lineHeight || "140%";
872
- const textContent = values.text || "";
873
- const textAlign = values.textAlign || "left";
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");
874
947
  const bgStyle = bgColor ? `background-color:${bgColor};` : "";
875
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
876
- <tbody>
877
- <tr>
878
- <td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};${bgStyle}font-family:arial,helvetica,sans-serif;" align="left">
879
- <div style="font-size:14px;color:${color};line-height:${lineHeight};text-align:${textAlign};word-wrap:break-word;">
880
- ${textContent}
881
- </div>
882
- </td>
883
- </tr>
884
- </tbody>
885
- </table>`;
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 });
886
950
  }
887
951
  }
888
952
  };
@@ -896,41 +960,74 @@ const headingTool = {
896
960
  text: {
897
961
  title: "Heading",
898
962
  options: {
899
- text: {
900
- label: "Text",
901
- defaultValue: "Heading",
902
- widget: "text"
903
- },
963
+ text: { label: "Text", defaultValue: "Heading", widget: "text" },
904
964
  headingType: {
905
965
  label: "Heading Type",
906
966
  defaultValue: "h1",
907
967
  widget: "dropdown",
908
- widgetParams: {
909
- options: [
910
- { label: "H1", value: "h1" },
911
- { label: "H2", value: "h2" },
912
- { label: "H3", value: "h3" },
913
- { label: "H4", value: "h4" }
914
- ]
915
- }
968
+ widgetParams: { options: [
969
+ { label: "H1", value: "h1" },
970
+ { label: "H2", value: "h2" },
971
+ { label: "H3", value: "h3" },
972
+ { label: "H4", value: "h4" }
973
+ ] }
916
974
  }
917
975
  }
918
976
  },
919
977
  style: {
920
978
  title: "Style",
921
979
  options: {
922
- fontSize: { label: "Font Size", defaultValue: "22px", widget: "text" },
980
+ fontSize: {
981
+ label: "Font Size",
982
+ defaultValue: "22px",
983
+ widget: "dropdown",
984
+ widgetParams: { options: [
985
+ { label: "14px", value: "14px" },
986
+ { label: "16px", value: "16px" },
987
+ { label: "18px", value: "18px" },
988
+ { label: "20px", value: "20px" },
989
+ { label: "22px", value: "22px" },
990
+ { label: "26px", value: "26px" },
991
+ { label: "30px", value: "30px" },
992
+ { label: "36px", value: "36px" },
993
+ { label: "48px", value: "48px" },
994
+ { label: "60px", value: "60px" }
995
+ ] }
996
+ },
923
997
  color: { label: "Text Color", defaultValue: "#000000", widget: "color_picker" },
924
- textAlign: { label: "Text Align", defaultValue: "left", widget: "text" },
925
- fontWeight: { label: "Font Weight", defaultValue: "700", widget: "text" },
926
- lineHeight: { label: "Line Height", defaultValue: "140%", widget: "text" },
998
+ textAlign: { label: "Text Align", defaultValue: "left", widget: "alignment" },
999
+ fontWeight: {
1000
+ label: "Font Weight",
1001
+ defaultValue: "700",
1002
+ widget: "dropdown",
1003
+ widgetParams: { options: [
1004
+ { label: "Normal", value: "400" },
1005
+ { label: "Medium", value: "500" },
1006
+ { label: "Semi Bold", value: "600" },
1007
+ { label: "Bold", value: "700" },
1008
+ { label: "Extra Bold", value: "800" }
1009
+ ] }
1010
+ },
1011
+ lineHeight: {
1012
+ label: "Line Height",
1013
+ defaultValue: "140%",
1014
+ widget: "dropdown",
1015
+ widgetParams: { options: [
1016
+ { label: "100%", value: "100%" },
1017
+ { label: "120%", value: "120%" },
1018
+ { label: "140%", value: "140%" },
1019
+ { label: "160%", value: "160%" },
1020
+ { label: "180%", value: "180%" },
1021
+ { label: "200%", value: "200%" }
1022
+ ] }
1023
+ },
927
1024
  letterSpacing: { label: "Letter Spacing", defaultValue: "normal", widget: "text" }
928
1025
  }
929
1026
  },
930
1027
  spacing: {
931
1028
  title: "Spacing",
932
1029
  options: {
933
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1030
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" }
934
1031
  }
935
1032
  },
936
1033
  general: {
@@ -954,35 +1051,32 @@ const headingTool = {
954
1051
  containerPadding: "10px"
955
1052
  },
956
1053
  renderer: {
957
- renderEditor(values, ctx) {
958
- const padding = values.containerPadding || "10px";
959
- const fontSize = values.fontSize || "22px";
960
- const color = values.color || "#000000";
961
- const textAlign = values.textAlign || "left";
962
- const fontWeight = values.fontWeight || "700";
963
- const lineHeight = values.lineHeight || "140%";
964
- const text = values.text || "Heading";
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");
965
1062
  return html`
966
1063
  <div style="padding:${padding};font-size:${fontSize};color:${color};text-align:${textAlign};font-weight:${fontWeight};line-height:${lineHeight};">
967
1064
  ${text}
968
1065
  </div>
969
1066
  `;
970
1067
  },
971
- renderHtml(values, ctx) {
972
- const padding = values.containerPadding || "10px";
973
- const fontSize = values.fontSize || "22px";
974
- const color = values.color || "#000000";
975
- const textAlign = values.textAlign || "left";
976
- const fontWeight = values.fontWeight || "700";
977
- const lineHeight = values.lineHeight || "140%";
978
- const letterSpacing = values.letterSpacing || "normal";
979
- const tag = values.headingType || "h1";
980
- const text = values.text || "Heading";
981
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
982
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
983
- <${tag} style="margin:0;font-size:${fontSize};color:${color};text-align:${textAlign};font-weight:${fontWeight};line-height:${lineHeight};letter-spacing:${letterSpacing};">${text}</${tag}>
984
- </td></tr></tbody>
985
- </table>`;
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 });
986
1080
  }
987
1081
  }
988
1082
  };
@@ -1007,20 +1101,20 @@ const paragraphTool = {
1007
1101
  title: "Style",
1008
1102
  options: {
1009
1103
  color: { label: "Text Color", defaultValue: "#374151", widget: "color_picker" },
1104
+ textAlign: { label: "Text Align", defaultValue: "left", widget: "alignment" },
1010
1105
  lineHeight: { label: "Line Height", defaultValue: "160%", widget: "text" },
1011
- letterSpacing: { label: "Letter Spacing", defaultValue: "normal", widget: "text" },
1012
- textAlign: { label: "Text Align", defaultValue: "left", widget: "text" }
1106
+ letterSpacing: { label: "Letter Spacing", defaultValue: "normal", widget: "text" }
1013
1107
  }
1014
1108
  },
1015
1109
  spacing: {
1016
1110
  title: "Spacing",
1017
1111
  options: {
1018
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1112
+ containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" }
1019
1113
  }
1020
1114
  }
1021
1115
  },
1022
1116
  defaultValues: {
1023
- text: '<p style="font-size:14px;line-height:1.6;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
1117
+ text: '<p style="font-size:14px;line-height:1.6;">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>',
1024
1118
  color: "#374151",
1025
1119
  lineHeight: "160%",
1026
1120
  letterSpacing: "normal",
@@ -1029,24 +1123,19 @@ const paragraphTool = {
1029
1123
  },
1030
1124
  renderer: {
1031
1125
  renderEditor(values) {
1032
- const padding = values.containerPadding || "10px";
1033
- const color = values.color || "#374151";
1034
- const lineHeight = values.lineHeight || "160%";
1035
- const text = values.text || "";
1036
- return html`<div style="padding:${padding};color:${color};line-height:${lineHeight};word-break:break-word;">${unsafeHTML(text)}</div>`;
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>`;
1037
1130
  },
1038
1131
  renderHtml(values) {
1039
- const padding = values.containerPadding || "10px";
1040
- const color = values.color || "#374151";
1041
- const lineHeight = values.lineHeight || "160%";
1042
- const letterSpacing = values.letterSpacing || "normal";
1043
- const textAlign = values.textAlign || "left";
1044
- const text = values.text || "";
1045
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1046
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
1047
- <div style="font-size:14px;color:${color};line-height:${lineHeight};text-align:${textAlign};letter-spacing:${letterSpacing};word-wrap:break-word;">${text}</div>
1048
- </td></tr></tbody>
1049
- </table>`;
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 });
1050
1139
  }
1051
1140
  }
1052
1141
  };
@@ -1069,17 +1158,25 @@ const imageTool = {
1069
1158
  style: {
1070
1159
  title: "Style",
1071
1160
  options: {
1072
- width: { label: "Width", defaultValue: "100%", widget: "text" },
1073
- maxWidth: { label: "Max Width", defaultValue: "100%", widget: "text" },
1074
- align: { label: "Align", defaultValue: "center", widget: "text" },
1161
+ width: {
1162
+ label: "Width",
1163
+ defaultValue: "100%",
1164
+ widget: "dropdown",
1165
+ widgetParams: { options: [
1166
+ { label: "Auto", value: "auto" },
1167
+ { label: "25%", value: "25%" },
1168
+ { label: "50%", value: "50%" },
1169
+ { label: "75%", value: "75%" },
1170
+ { label: "100%", value: "100%" }
1171
+ ] }
1172
+ },
1173
+ align: { label: "Align", defaultValue: "center", widget: "alignment" },
1075
1174
  borderRadius: { label: "Border Radius", defaultValue: "0px", widget: "text" }
1076
1175
  }
1077
1176
  },
1078
1177
  spacing: {
1079
1178
  title: "Spacing",
1080
- options: {
1081
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1082
- }
1179
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1083
1180
  },
1084
1181
  general: {
1085
1182
  title: "General",
@@ -1102,46 +1199,32 @@ const imageTool = {
1102
1199
  containerPadding: "10px"
1103
1200
  },
1104
1201
  renderer: {
1105
- renderEditor(values, ctx) {
1106
- const padding = values.containerPadding || "10px";
1107
- const src = values.src || "";
1108
- const alt = values.alt || "";
1109
- const width = values.width || "100%";
1110
- const borderRadius = values.borderRadius || "0px";
1111
- const align = values.align || "center";
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");
1112
1209
  if (!src) {
1113
- return html`
1114
- <div style="padding:${padding};text-align:${align};">
1115
- <div style="background:#f1f5f9;border:2px dashed #cbd5e1;border-radius:8px;padding:40px 20px;text-align:center;color:#94a3b8;font-family:sans-serif;font-size:13px;">
1116
- No image set. Enter a URL in the property panel.
1117
- </div>
1118
- </div>
1119
- `;
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>`;
1120
1211
  }
1121
- return html`
1122
- <div style="padding:${padding};text-align:${align};">
1123
- <img src=${src} alt=${alt} style="display:inline-block;max-width:100%;width:${width};border-radius:${borderRadius};border:0;" />
1124
- </div>
1125
- `;
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>`;
1126
1213
  },
1127
1214
  renderHtml(values, ctx) {
1128
- const padding = values.containerPadding || "10px";
1129
- const src = values.src || "";
1130
- const alt = values.alt || "";
1131
- const href = values.href || "";
1132
- const target = values.target || "_blank";
1133
- const width = values.width || "100%";
1134
- const borderRadius = values.borderRadius || "0px";
1135
- const align = values.align || "center";
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");
1136
1223
  const widthPx = width === "100%" ? ctx.columnWidth : parseInt(width);
1137
- const borderStyle = borderRadius !== "0px" ? `border-radius:${borderRadius};` : "";
1138
- const imgTag = `<img align="${align}" border="0" src="${src}" alt="${alt}" title="${alt}" style="outline:none;text-decoration:none;-ms-interpolation-mode:bicubic;clear:both;display:inline-block!important;border:none;height:auto;float:none;width:${width};max-width:${widthPx}px;${borderStyle}" width="${widthPx}" />`;
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}" />`;
1139
1226
  const content = href ? `<a href="${href}" target="${target}" style="text-decoration:none;">${imgTag}</a>` : imgTag;
1140
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1141
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${align}">
1142
- ${content}
1143
- </td></tr></tbody>
1144
- </table>`;
1227
+ return emailTableCell(content, { padding, align });
1145
1228
  }
1146
1229
  }
1147
1230
  };
@@ -1165,21 +1248,45 @@ const buttonTool = {
1165
1248
  options: {
1166
1249
  backgroundColor: { label: "Button Color", defaultValue: "#3b82f6", widget: "color_picker" },
1167
1250
  textColor: { label: "Text Color", defaultValue: "#ffffff", widget: "color_picker" },
1168
- fontSize: { label: "Font Size", defaultValue: "14px", widget: "text" },
1169
- fontWeight: { label: "Font Weight", defaultValue: "700", widget: "text" },
1251
+ fontSize: {
1252
+ label: "Font Size",
1253
+ defaultValue: "14px",
1254
+ widget: "dropdown",
1255
+ widgetParams: { options: [
1256
+ { label: "12px", value: "12px" },
1257
+ { label: "13px", value: "13px" },
1258
+ { label: "14px", value: "14px" },
1259
+ { label: "16px", value: "16px" },
1260
+ { label: "18px", value: "18px" },
1261
+ { label: "20px", value: "20px" }
1262
+ ] }
1263
+ },
1264
+ fontWeight: {
1265
+ label: "Font Weight",
1266
+ defaultValue: "700",
1267
+ widget: "dropdown",
1268
+ widgetParams: { options: [{ label: "Normal", value: "400" }, { label: "Bold", value: "700" }] }
1269
+ },
1170
1270
  borderRadius: { label: "Border Radius", defaultValue: "4px", widget: "text" },
1171
- buttonWidth: { label: "Width", defaultValue: "auto", widget: "text" },
1172
- textAlign: { label: "Align", defaultValue: "center", widget: "text" },
1173
- buttonPadding: { label: "Button Padding", defaultValue: "10px 20px", widget: "text" },
1271
+ buttonWidth: {
1272
+ label: "Width",
1273
+ defaultValue: "auto",
1274
+ widget: "dropdown",
1275
+ widgetParams: { options: [
1276
+ { label: "Auto", value: "auto" },
1277
+ { label: "100%", value: "100%" },
1278
+ { label: "50%", value: "50%" }
1279
+ ] }
1280
+ },
1281
+ textAlign: { label: "Align", defaultValue: "center", widget: "alignment" },
1282
+ buttonPadding: { label: "Button Padding", defaultValue: "10px 20px", widget: "padding" },
1174
1283
  borderColor: { label: "Border Color", defaultValue: "", widget: "color_picker" },
1175
1284
  borderWidth: { label: "Border Width", defaultValue: "0px", widget: "text" }
1176
1285
  }
1177
1286
  },
1178
1287
  spacing: {
1179
1288
  title: "Spacing",
1180
- options: {
1181
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1182
- }
1289
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1183
1290
  },
1184
1291
  general: {
1185
1292
  title: "General",
@@ -1207,64 +1314,52 @@ const buttonTool = {
1207
1314
  containerPadding: "10px"
1208
1315
  },
1209
1316
  renderer: {
1210
- renderEditor(values, ctx) {
1211
- const padding = values.containerPadding || "10px";
1212
- const bgColor = values.backgroundColor || "#3b82f6";
1213
- const textColor = values.textColor || "#ffffff";
1214
- const fontSize = values.fontSize || "14px";
1215
- const fontWeight = values.fontWeight || "700";
1216
- const borderRadius = values.borderRadius || "4px";
1217
- const buttonPadding = values.buttonPadding || "10px 20px";
1218
- const text = values.text || "Click Me";
1219
- const textAlign = values.textAlign || "center";
1220
- const buttonWidth = values.buttonWidth || "auto";
1221
- const borderColor = values.borderColor || bgColor;
1222
- const borderWidth = values.borderWidth || "0px";
1223
- const borderStyle = borderWidth !== "0px" ? `border:${borderWidth} solid ${borderColor};` : "border:none;";
1224
- const widthStyle = buttonWidth === "auto" ? "display:inline-block;" : `display:block;width:${buttonWidth};`;
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};`;
1225
1332
  return html`
1226
- <div style="padding:${padding};text-align:${textAlign};">
1227
- <a style="${widthStyle}background-color:${bgColor};color:${textColor};font-size:${fontSize};font-weight:${fontWeight};border-radius:${borderRadius};padding:${buttonPadding};text-decoration:none;text-align:center;${borderStyle}cursor:pointer;font-family:arial,helvetica,sans-serif;box-sizing:border-box;">
1228
- ${text}
1229
- </a>
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>
1230
1335
  </div>
1231
1336
  `;
1232
1337
  },
1233
- renderHtml(values, ctx) {
1234
- const padding = values.containerPadding || "10px";
1235
- const bgColor = values.backgroundColor || "#3b82f6";
1236
- const textColor = values.textColor || "#ffffff";
1237
- const fontSize = values.fontSize || "14px";
1238
- const fontWeight = values.fontWeight || "700";
1239
- const borderRadius = values.borderRadius || "4px";
1240
- const buttonPadding = values.buttonPadding || "10px 20px";
1241
- const text = values.text || "Click Me";
1242
- const textAlign = values.textAlign || "center";
1243
- const href = values.href || "#";
1244
- const target = values.target || "_blank";
1245
- const borderColor = values.borderColor || bgColor;
1246
- const borderWidth = values.borderWidth || "0px";
1247
- const borderStyle = borderWidth !== "0px" ? `border:${borderWidth} solid ${borderColor};` : "border:none;";
1248
- const vml = parseInt(borderRadius) > 0 ? `
1249
- <!--[if mso]>
1250
- <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${href}" style="height:auto;v-text-anchor:middle;width:auto;" arcsize="${Math.round(parseInt(borderRadius) / 20 * 100)}%" stroke="f" fillcolor="${bgColor}">
1251
- <w:anchorlock/>
1252
- <center style="color:${textColor};font-family:arial,helvetica,sans-serif;font-size:${fontSize};font-weight:${fontWeight};">${text}</center>
1253
- </v:roundrect>
1254
- <![endif]-->
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}
1255
1355
  <!--[if !mso]><!-->` : "<!--[if !mso]><!-->";
1256
- const vmlEnd = parseInt(borderRadius) > 0 ? "<!--<![endif]-->" : "<!--<![endif]-->";
1257
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1258
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1259
- <div align="${textAlign}">
1260
- ${vml}
1261
- <a href="${href}" target="${target}" style="box-sizing:border-box;display:inline-block;text-decoration:none;-webkit-text-size-adjust:none;text-align:center;color:${textColor};background-color:${bgColor};border-radius:${borderRadius};-webkit-border-radius:${borderRadius};-moz-border-radius:${borderRadius};font-size:${fontSize};font-weight:${fontWeight};padding:${buttonPadding};font-family:arial,helvetica,sans-serif;${borderStyle}mso-border-alt:none;word-break:keep-all;">
1262
- <span style="line-height:120%;">${text}</span>
1263
- </a>
1264
- ${vmlEnd}
1265
- </div>
1266
- </td></tr></tbody>
1267
- </table>`;
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 });
1268
1363
  }
1269
1364
  }
1270
1365
  };
@@ -1279,16 +1374,24 @@ const dividerTool = {
1279
1374
  title: "Style",
1280
1375
  options: {
1281
1376
  borderTopWidth: { label: "Width", defaultValue: "1px", widget: "text" },
1282
- borderTopStyle: { label: "Style", defaultValue: "solid", widget: "text" },
1377
+ borderTopStyle: {
1378
+ label: "Style",
1379
+ defaultValue: "solid",
1380
+ widget: "dropdown",
1381
+ widgetParams: { options: [
1382
+ { label: "Solid", value: "solid" },
1383
+ { label: "Dashed", value: "dashed" },
1384
+ { label: "Dotted", value: "dotted" },
1385
+ { label: "Double", value: "double" }
1386
+ ] }
1387
+ },
1283
1388
  borderTopColor: { label: "Color", defaultValue: "#cccccc", widget: "color_picker" },
1284
1389
  width: { label: "Line Width", defaultValue: "100%", widget: "text" }
1285
1390
  }
1286
1391
  },
1287
1392
  spacing: {
1288
1393
  title: "Spacing",
1289
- options: {
1290
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1291
- }
1394
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1292
1395
  },
1293
1396
  general: {
1294
1397
  title: "General",
@@ -1307,30 +1410,17 @@ const dividerTool = {
1307
1410
  },
1308
1411
  renderer: {
1309
1412
  renderEditor(values) {
1310
- const padding = values.containerPadding || "10px";
1311
- const width = values.width || "100%";
1312
- const bw = values.borderTopWidth || "1px";
1313
- const bs = values.borderTopStyle || "solid";
1314
- const bc = values.borderTopColor || "#cccccc";
1315
- return html`
1316
- <div style="padding:${padding};">
1317
- <div style="border-top:${bw} ${bs} ${bc};width:${width};margin:0 auto;"></div>
1318
- </div>
1319
- `;
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>`;
1320
1417
  },
1321
1418
  renderHtml(values) {
1322
- const padding = values.containerPadding || "10px";
1323
- const width = values.width || "100%";
1324
- const bw = values.borderTopWidth || "1px";
1325
- const bs = values.borderTopStyle || "solid";
1326
- const bc = values.borderTopColor || "#cccccc";
1327
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1328
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="center">
1329
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="${width}" style="border-collapse:collapse;table-layout:fixed;border-spacing:0;mso-table-lspace:0;mso-table-rspace:0;vertical-align:top;border-top:${bw} ${bs} ${bc};-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;">
1330
- <tbody><tr><td style="font-size:0;line-height:0;">&nbsp;</td></tr></tbody>
1331
- </table>
1332
- </td></tr></tbody>
1333
- </table>`;
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" });
1334
1424
  }
1335
1425
  }
1336
1426
  };
@@ -1353,9 +1443,7 @@ const htmlTool = {
1353
1443
  },
1354
1444
  spacing: {
1355
1445
  title: "Spacing",
1356
- options: {
1357
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1358
- }
1446
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" } }
1359
1447
  },
1360
1448
  general: {
1361
1449
  title: "General",
@@ -1371,22 +1459,14 @@ const htmlTool = {
1371
1459
  },
1372
1460
  renderer: {
1373
1461
  renderEditor(values) {
1374
- const padding = values.containerPadding || "10px";
1375
- const content = values.html || "";
1376
- return html`<div style="padding:${padding};">${unsafeHTML(content)}</div>`;
1462
+ return html`<div style="padding:${str(values, "containerPadding", "10px")};">${unsafeHTML(str(values, "html"))}</div>`;
1377
1463
  },
1378
1464
  renderHtml(values) {
1379
- const padding = values.containerPadding || "10px";
1380
- const content = values.html || "";
1381
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1382
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
1383
- ${content}
1384
- </td></tr></tbody>
1385
- </table>`;
1465
+ return emailTableCell(str(values, "html"), { padding: str(values, "containerPadding", "10px") });
1386
1466
  }
1387
1467
  }
1388
1468
  };
1389
- const defaultSocials = [
1469
+ const DEFAULT_ICONS = [
1390
1470
  { name: "Facebook", url: "https://facebook.com/", icon: "f", color: "#1877F2" },
1391
1471
  { name: "Twitter", url: "https://twitter.com/", icon: "𝕏", color: "#000000" },
1392
1472
  { name: "Instagram", url: "https://instagram.com/", icon: "📷", color: "#E4405F" },
@@ -1402,26 +1482,22 @@ const socialTool = {
1402
1482
  icons: {
1403
1483
  title: "Social Icons",
1404
1484
  options: {
1405
- icons: { label: "Icons (JSON)", defaultValue: JSON.stringify(defaultSocials), widget: "rich_text" },
1485
+ icons: { label: "Icons (JSON)", defaultValue: JSON.stringify(DEFAULT_ICONS), widget: "rich_text" },
1406
1486
  iconSize: { label: "Icon Size", defaultValue: "32px", widget: "text" },
1407
1487
  iconSpacing: { label: "Spacing", defaultValue: "8px", widget: "text" }
1408
1488
  }
1409
1489
  },
1410
1490
  style: {
1411
1491
  title: "Style",
1412
- options: {
1413
- textAlign: { label: "Align", defaultValue: "center", widget: "text" }
1414
- }
1492
+ options: { textAlign: { label: "Align", defaultValue: "center", widget: "alignment" } }
1415
1493
  },
1416
1494
  spacing: {
1417
1495
  title: "Spacing",
1418
- options: {
1419
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1420
- }
1496
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1421
1497
  }
1422
1498
  },
1423
1499
  defaultValues: {
1424
- icons: JSON.stringify(defaultSocials),
1500
+ icons: JSON.stringify(DEFAULT_ICONS),
1425
1501
  iconSize: "32px",
1426
1502
  iconSpacing: "8px",
1427
1503
  textAlign: "center",
@@ -1429,55 +1505,34 @@ const socialTool = {
1429
1505
  },
1430
1506
  renderer: {
1431
1507
  renderEditor(values) {
1432
- const padding = values.containerPadding || "10px";
1433
- const textAlign = values.textAlign || "center";
1434
- const iconSize = values.iconSize || "32px";
1435
- const spacing = values.iconSpacing || "8px";
1436
- let icons = defaultSocials;
1437
- try {
1438
- icons = JSON.parse(values.icons);
1439
- } catch {
1440
- }
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);
1441
1513
  return html`
1442
- <div style="padding:${padding};text-align:${textAlign};">
1443
- ${icons.map(
1444
- (s) => html`
1445
- <a href=${s.url} target="_blank" style="display:inline-block;width:${iconSize};height:${iconSize};line-height:${iconSize};text-align:center;background:${s.color};color:white;border-radius:50%;text-decoration:none;font-size:14px;font-weight:bold;margin:0 ${spacing};font-family:arial,sans-serif;vertical-align:middle;">${s.icon}</a>
1446
- `
1447
- )}
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
+ `)}
1448
1518
  </div>
1449
1519
  `;
1450
1520
  },
1451
1521
  renderHtml(values) {
1452
- const padding = values.containerPadding || "10px";
1453
- const textAlign = values.textAlign || "center";
1454
- const iconSize = parseInt(values.iconSize || "32");
1455
- const spacing = values.iconSpacing || "8px";
1456
- let icons = defaultSocials;
1457
- try {
1458
- icons = JSON.parse(values.icons);
1459
- } catch {
1460
- }
1461
- const iconsHtml = icons.map(
1462
- (s) => `<td align="center" valign="middle" style="padding:0 ${spacing};">
1463
- <a href="${s.url}" target="_blank" style="text-decoration:none;">
1464
- <table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>
1465
- <td width="${iconSize}" height="${iconSize}" align="center" valign="middle" style="width:${iconSize}px;height:${iconSize}px;background:${s.color};border-radius:50%;color:#ffffff;font-size:14px;font-weight:bold;font-family:arial,sans-serif;">
1466
- ${s.icon}
1467
- </td>
1468
- </tr></table>
1469
- </a>
1470
- </td>`
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>`
1471
1529
  ).join("\n");
1472
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1473
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1474
- <table role="presentation" cellpadding="0" cellspacing="0" border="0" align="${textAlign}"><tr>${iconsHtml}</tr></table>
1475
- </td></tr></tbody>
1476
- </table>`;
1530
+ const inner = `<table role="presentation" cellpadding="0" cellspacing="0" border="0" align="${align}"><tr>${cells}</tr></table>`;
1531
+ return emailTableCell(inner, { padding, align });
1477
1532
  }
1478
1533
  }
1479
1534
  };
1480
- const defaultItems = [
1535
+ const DEFAULT_ITEMS = [
1481
1536
  { text: "Home", href: "#" },
1482
1537
  { text: "About", href: "#" },
1483
1538
  { text: "Contact", href: "#" }
@@ -1491,14 +1546,12 @@ const menuTool = {
1491
1546
  options: {
1492
1547
  menu: {
1493
1548
  title: "Menu",
1494
- options: {
1495
- items: { label: "Items (JSON)", defaultValue: JSON.stringify(defaultItems), widget: "rich_text" }
1496
- }
1549
+ options: { items: { label: "Items (JSON)", defaultValue: JSON.stringify(DEFAULT_ITEMS), widget: "rich_text" } }
1497
1550
  },
1498
1551
  style: {
1499
1552
  title: "Style",
1500
1553
  options: {
1501
- textAlign: { label: "Align", defaultValue: "center", widget: "text" },
1554
+ textAlign: { label: "Align", defaultValue: "center", widget: "alignment" },
1502
1555
  fontSize: { label: "Font Size", defaultValue: "14px", widget: "text" },
1503
1556
  color: { label: "Text Color", defaultValue: "#333333", widget: "color_picker" },
1504
1557
  separator: { label: "Separator", defaultValue: "|", widget: "text" },
@@ -1507,13 +1560,11 @@ const menuTool = {
1507
1560
  },
1508
1561
  spacing: {
1509
1562
  title: "Spacing",
1510
- options: {
1511
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1512
- }
1563
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1513
1564
  }
1514
1565
  },
1515
1566
  defaultValues: {
1516
- items: JSON.stringify(defaultItems),
1567
+ items: JSON.stringify(DEFAULT_ITEMS),
1517
1568
  textAlign: "center",
1518
1569
  fontSize: "14px",
1519
1570
  color: "#333333",
@@ -1523,47 +1574,33 @@ const menuTool = {
1523
1574
  },
1524
1575
  renderer: {
1525
1576
  renderEditor(values) {
1526
- const padding = values.containerPadding || "10px";
1527
- const textAlign = values.textAlign || "center";
1528
- const fontSize = values.fontSize || "14px";
1529
- const color = values.color || "#333333";
1530
- const sep = values.separator || "|";
1531
- const sepColor = values.separatorColor || "#cccccc";
1532
- let items = defaultItems;
1533
- try {
1534
- items = JSON.parse(values.items);
1535
- } catch {
1536
- }
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);
1537
1584
  return html`
1538
- <div style="padding:${padding};text-align:${textAlign};font-size:${fontSize};font-family:arial,sans-serif;">
1539
- ${items.map(
1540
- (item, i) => html`${i > 0 ? html`<span style="color:${sepColor};padding:0 8px;">${sep}</span>` : ""}
1541
- <a href=${item.href} style="color:${color};text-decoration:none;">${item.text}</a>`
1542
- )}
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>`)}
1543
1588
  </div>
1544
1589
  `;
1545
1590
  },
1546
1591
  renderHtml(values) {
1547
- const padding = values.containerPadding || "10px";
1548
- const textAlign = values.textAlign || "center";
1549
- const fontSize = values.fontSize || "14px";
1550
- const color = values.color || "#333333";
1551
- const sep = values.separator || "|";
1552
- const sepColor = values.separatorColor || "#cccccc";
1553
- let items = defaultItems;
1554
- try {
1555
- items = JSON.parse(values.items);
1556
- } catch {
1557
- }
1558
- const linksHtml = items.map((item, i) => {
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) => {
1559
1600
  const prefix = i > 0 ? `<span style="color:${sepColor};padding:0 8px;">${sep}</span>` : "";
1560
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>`;
1561
1602
  }).join("");
1562
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1563
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1564
- <div style="text-align:${textAlign};">${linksHtml}</div>
1565
- </td></tr></tbody>
1566
- </table>`;
1603
+ return emailTableCell(`<div style="text-align:${align};">${links}</div>`, { padding, align });
1567
1604
  }
1568
1605
  }
1569
1606
  };
@@ -1588,15 +1625,11 @@ const videoTool = {
1588
1625
  },
1589
1626
  style: {
1590
1627
  title: "Style",
1591
- options: {
1592
- textAlign: { label: "Align", defaultValue: "center", widget: "text" }
1593
- }
1628
+ options: { textAlign: { label: "Align", defaultValue: "center", widget: "alignment" } }
1594
1629
  },
1595
1630
  spacing: {
1596
1631
  title: "Spacing",
1597
- options: {
1598
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1599
- }
1632
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1600
1633
  }
1601
1634
  },
1602
1635
  defaultValues: {
@@ -1608,46 +1641,23 @@ const videoTool = {
1608
1641
  },
1609
1642
  renderer: {
1610
1643
  renderEditor(values) {
1611
- const padding = values.containerPadding || "10px";
1612
- const url = values.url || "";
1613
- const thumbnail = values.thumbnailUrl || getYouTubeThumbnail(url) || "";
1614
- const textAlign = values.textAlign || "center";
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");
1615
1648
  if (!thumbnail) {
1616
- return html`
1617
- <div style="padding:${padding};text-align:${textAlign};">
1618
- <div style="background:#0f172a;border-radius:8px;padding:40px;text-align:center;color:white;font-family:sans-serif;position:relative;">
1619
- <div style="font-size:48px;opacity:0.8;">▶</div>
1620
- <div style="font-size:12px;margin-top:8px;opacity:0.6;">${url || "Enter video URL"}</div>
1621
- </div>
1622
- </div>
1623
- `;
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>`;
1624
1650
  }
1625
- return html`
1626
- <div style="padding:${padding};text-align:${textAlign};">
1627
- <div style="position:relative;display:inline-block;max-width:100%;cursor:pointer;">
1628
- <img src=${thumbnail} alt="Video thumbnail" style="display:block;max-width:100%;border-radius:4px;" />
1629
- <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;">
1630
- <div style="width:60px;height:60px;background:rgba(0,0,0,0.7);border-radius:50%;display:flex;align-items:center;justify-content:center;color:white;font-size:24px;">▶</div>
1631
- </div>
1632
- </div>
1633
- </div>
1634
- `;
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>`;
1635
1652
  },
1636
1653
  renderHtml(values, ctx) {
1637
- const padding = values.containerPadding || "10px";
1638
- const url = values.url || "#";
1639
- const thumbnail = values.thumbnailUrl || getYouTubeThumbnail(url) || "";
1640
- const alt = values.alt || "Video";
1641
- const textAlign = values.textAlign || "center";
1642
- const widthPx = ctx.columnWidth;
1643
- const imgTag = thumbnail ? `<img src="${thumbnail}" alt="${alt}" width="${widthPx}" style="display:block;max-width:100%;width:${widthPx}px;border:0;" />` : `<div style="background:#0f172a;padding:40px;text-align:center;color:white;font-family:arial,sans-serif;font-size:16px;">▶ Watch Video</div>`;
1644
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1645
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1646
- <a href="${url}" target="_blank" style="text-decoration:none;">
1647
- ${imgTag}
1648
- </a>
1649
- </td></tr></tbody>
1650
- </table>`;
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 });
1651
1661
  }
1652
1662
  }
1653
1663
  };
@@ -1668,7 +1678,7 @@ const timerTool = {
1668
1678
  style: {
1669
1679
  title: "Style",
1670
1680
  options: {
1671
- textAlign: { label: "Align", defaultValue: "center", widget: "text" },
1681
+ textAlign: { label: "Align", defaultValue: "center", widget: "alignment" },
1672
1682
  backgroundColor: { label: "Background", defaultValue: "#1f2937", widget: "color_picker" },
1673
1683
  textColor: { label: "Text Color", defaultValue: "#ffffff", widget: "color_picker" },
1674
1684
  fontSize: { label: "Font Size", defaultValue: "32px", widget: "text" }
@@ -1676,9 +1686,7 @@ const timerTool = {
1676
1686
  },
1677
1687
  spacing: {
1678
1688
  title: "Spacing",
1679
- options: {
1680
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1681
- }
1689
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1682
1690
  }
1683
1691
  },
1684
1692
  defaultValues: {
@@ -1692,39 +1700,25 @@ const timerTool = {
1692
1700
  },
1693
1701
  renderer: {
1694
1702
  renderEditor(values) {
1695
- const padding = values.containerPadding || "10px";
1696
- const bg = values.backgroundColor || "#1f2937";
1697
- const color = values.textColor || "#ffffff";
1698
- const fontSize = values.fontSize || "32px";
1699
- const textAlign = values.textAlign || "center";
1700
- return html`
1701
- <div style="padding:${padding};">
1702
- <div style="background:${bg};color:${color};font-size:${fontSize};text-align:${textAlign};padding:20px;border-radius:4px;font-family:monospace;font-weight:bold;letter-spacing:4px;">
1703
- 00 : 00 : 00 : 00
1704
- <div style="font-size:11px;letter-spacing:8px;opacity:0.6;margin-top:4px;">DAYS &nbsp; HRS &nbsp; MIN &nbsp; SEC</div>
1705
- </div>
1706
- </div>
1707
- `;
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>`;
1708
1709
  },
1709
1710
  renderHtml(values) {
1710
- const padding = values.containerPadding || "10px";
1711
- const bg = values.backgroundColor || "#1f2937";
1712
- const color = values.textColor || "#ffffff";
1713
- const fontSize = values.fontSize || "32px";
1714
- const textAlign = values.textAlign || "center";
1715
- values.endDate || "";
1716
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1717
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="${textAlign}">
1718
- <div style="background-color:${bg};color:${color};font-size:${fontSize};text-align:${textAlign};padding:20px;border-radius:4px;font-family:'Courier New',monospace;font-weight:bold;letter-spacing:4px;">
1719
- <div>00 : 00 : 00 : 00</div>
1720
- <div style="font-size:11px;letter-spacing:8px;opacity:0.6;margin-top:4px;">DAYS &nbsp; HRS &nbsp; MIN &nbsp; SEC</div>
1721
- </div>
1722
- </td></tr></tbody>
1723
- </table>`;
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 });
1724
1718
  }
1725
1719
  }
1726
1720
  };
1727
- const defaultTableData = [
1721
+ const DEFAULT_DATA = [
1728
1722
  ["Header 1", "Header 2", "Header 3"],
1729
1723
  ["Cell 1", "Cell 2", "Cell 3"],
1730
1724
  ["Cell 4", "Cell 5", "Cell 6"]
@@ -1738,9 +1732,7 @@ const tableTool = {
1738
1732
  options: {
1739
1733
  table: {
1740
1734
  title: "Table",
1741
- options: {
1742
- tableData: { label: "Table Data (JSON)", defaultValue: JSON.stringify(defaultTableData), widget: "rich_text" }
1743
- }
1735
+ options: { tableData: { label: "Table Data (JSON)", defaultValue: JSON.stringify(DEFAULT_DATA), widget: "rich_text" } }
1744
1736
  },
1745
1737
  style: {
1746
1738
  title: "Style",
@@ -1754,13 +1746,11 @@ const tableTool = {
1754
1746
  },
1755
1747
  spacing: {
1756
1748
  title: "Spacing",
1757
- options: {
1758
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1759
- }
1749
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1760
1750
  }
1761
1751
  },
1762
1752
  defaultValues: {
1763
- tableData: JSON.stringify(defaultTableData),
1753
+ tableData: JSON.stringify(DEFAULT_DATA),
1764
1754
  headerBg: "#f3f4f6",
1765
1755
  headerColor: "#111827",
1766
1756
  borderColor: "#e5e7eb",
@@ -1770,69 +1760,42 @@ const tableTool = {
1770
1760
  },
1771
1761
  renderer: {
1772
1762
  renderEditor(values) {
1773
- const padding = values.containerPadding || "10px";
1774
- const headerBg = values.headerBg || "#f3f4f6";
1775
- const headerColor = values.headerColor || "#111827";
1776
- const borderColor = values.borderColor || "#e5e7eb";
1777
- const cellPadding = values.cellPadding || "8px 12px";
1778
- const fontSize = values.fontSize || "14px";
1779
- let data = defaultTableData;
1780
- try {
1781
- data = JSON.parse(values.tableData);
1782
- } catch {
1783
- }
1784
- const headerRow = data[0] || [];
1785
- const bodyRows = data.slice(1);
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);
1786
1770
  return html`
1787
1771
  <div style="padding:${padding};overflow-x:auto;">
1788
- <table style="width:100%;border-collapse:collapse;font-size:${fontSize};font-family:arial,sans-serif;">
1789
- <thead>
1790
- <tr>
1791
- ${headerRow.map((cell) => html`<th style="padding:${cellPadding};background:${headerBg};color:${headerColor};border:1px solid ${borderColor};text-align:left;font-weight:600;">${cell}</th>`)}
1792
- </tr>
1793
- </thead>
1794
- <tbody>
1795
- ${bodyRows.map((row) => html`
1796
- <tr>
1797
- ${row.map((cell) => html`<td style="padding:${cellPadding};border:1px solid ${borderColor};">${cell}</td>`)}
1798
- </tr>
1799
- `)}
1800
- </tbody>
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>
1801
1775
  </table>
1802
1776
  </div>
1803
1777
  `;
1804
1778
  },
1805
1779
  renderHtml(values) {
1806
- const padding = values.containerPadding || "10px";
1807
- const headerBg = values.headerBg || "#f3f4f6";
1808
- const headerColor = values.headerColor || "#111827";
1809
- const borderColor = values.borderColor || "#e5e7eb";
1810
- const cellPadding = values.cellPadding || "8px 12px";
1811
- const fontSize = values.fontSize || "14px";
1812
- let data = defaultTableData;
1813
- try {
1814
- data = JSON.parse(values.tableData);
1815
- } catch {
1816
- }
1817
- const headerRow = data[0] || [];
1818
- const bodyRows = data.slice(1);
1819
- const headerHtml = headerRow.map(
1820
- (c) => `<th style="padding:${cellPadding};background-color:${headerBg};color:${headerColor};border:1px solid ${borderColor};text-align:left;font-weight:600;font-family:arial,helvetica,sans-serif;font-size:${fontSize};">${c}</th>`
1821
- ).join("");
1822
- const bodyHtml = bodyRows.map(
1823
- (row) => `<tr>${row.map((c) => `<td style="padding:${cellPadding};border:1px solid ${borderColor};font-family:arial,helvetica,sans-serif;font-size:${fontSize};">${c}</td>`).join("")}</tr>`
1824
- ).join("");
1825
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1826
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
1827
- <table cellpadding="0" cellspacing="0" width="100%" border="0" style="border-collapse:collapse;">
1828
- <thead><tr>${headerHtml}</tr></thead>
1829
- <tbody>${bodyHtml}</tbody>
1830
- </table>
1831
- </td></tr></tbody>
1832
- </table>`;
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 });
1833
1792
  }
1834
1793
  }
1835
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
+ ];
1836
1799
  const formTool = {
1837
1800
  name: "form",
1838
1801
  label: "Form",
@@ -1846,10 +1809,7 @@ const formTool = {
1846
1809
  actionUrl: { label: "Action URL", defaultValue: "#", widget: "text" },
1847
1810
  method: { label: "Method", defaultValue: "POST", widget: "text" },
1848
1811
  submitText: { label: "Submit Text", defaultValue: "Submit", widget: "text" },
1849
- fields: { label: "Fields (JSON)", defaultValue: JSON.stringify([
1850
- { label: "Name", name: "name", type: "text", placeholder: "Your name" },
1851
- { label: "Email", name: "email", type: "email", placeholder: "your@email.com" }
1852
- ]), widget: "rich_text" }
1812
+ fields: { label: "Fields (JSON)", defaultValue: JSON.stringify(DEFAULT_FIELDS), widget: "rich_text" }
1853
1813
  }
1854
1814
  },
1855
1815
  style: {
@@ -1861,34 +1821,25 @@ const formTool = {
1861
1821
  },
1862
1822
  spacing: {
1863
1823
  title: "Spacing",
1864
- options: {
1865
- containerPadding: { label: "Padding", defaultValue: "10px", widget: "text" }
1866
- }
1824
+ options: { containerPadding: { label: "Padding", defaultValue: "10px", widget: "padding" } }
1867
1825
  }
1868
1826
  },
1869
1827
  defaultValues: {
1870
1828
  actionUrl: "#",
1871
1829
  method: "POST",
1872
1830
  submitText: "Submit",
1873
- fields: JSON.stringify([
1874
- { label: "Name", name: "name", type: "text", placeholder: "Your name" },
1875
- { label: "Email", name: "email", type: "email", placeholder: "your@email.com" }
1876
- ]),
1831
+ fields: JSON.stringify(DEFAULT_FIELDS),
1877
1832
  buttonBg: "#3b82f6",
1878
1833
  buttonColor: "#ffffff",
1879
1834
  containerPadding: "10px"
1880
1835
  },
1881
1836
  renderer: {
1882
1837
  renderEditor(values) {
1883
- const padding = values.containerPadding || "10px";
1884
- const submitText = values.submitText || "Submit";
1885
- const buttonBg = values.buttonBg || "#3b82f6";
1886
- const buttonColor = values.buttonColor || "#ffffff";
1887
- let fields = [];
1888
- try {
1889
- fields = JSON.parse(values.fields);
1890
- } catch {
1891
- }
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);
1892
1843
  return html`
1893
1844
  <div style="padding:${padding};font-family:arial,sans-serif;">
1894
1845
  ${fields.map((f) => html`
@@ -1897,36 +1848,24 @@ const formTool = {
1897
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;" />
1898
1849
  </div>
1899
1850
  `)}
1900
- <button style="background:${buttonBg};color:${buttonColor};border:none;padding:10px 24px;border-radius:4px;font-size:14px;font-weight:600;cursor:pointer;">${submitText}</button>
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>
1901
1852
  </div>
1902
1853
  `;
1903
1854
  },
1904
1855
  renderHtml(values) {
1905
- const padding = values.containerPadding || "10px";
1906
- const actionUrl = values.actionUrl || "#";
1907
- const method = values.method || "POST";
1908
- const submitText = values.submitText || "Submit";
1909
- const buttonBg = values.buttonBg || "#3b82f6";
1910
- const buttonColor = values.buttonColor || "#ffffff";
1911
- let fields = [];
1912
- try {
1913
- fields = JSON.parse(values.fields);
1914
- } catch {
1915
- }
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;";
1916
1864
  const fieldsHtml = fields.map(
1917
- (f) => `<div style="margin-bottom:12px;">
1918
- <label style="display:block;font-size:13px;color:#374151;margin-bottom:4px;font-weight:500;font-family:arial,helvetica,sans-serif;">${f.label}</label>
1919
- <input type="${f.type || "text"}" name="${f.name}" placeholder="${f.placeholder || ""}" style="width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:4px;font-size:14px;box-sizing:border-box;font-family:arial,helvetica,sans-serif;" />
1920
- </div>`
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>`
1921
1866
  ).join("");
1922
- return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
1923
- <tbody><tr><td style="overflow-wrap:break-word;word-break:break-word;padding:${padding};font-family:arial,helvetica,sans-serif;" align="left">
1924
- <form action="${actionUrl}" method="${method}">
1925
- ${fieldsHtml}
1926
- <button type="submit" style="background-color:${buttonBg};color:${buttonColor};border:none;padding:10px 24px;border-radius:4px;font-size:14px;font-weight:600;cursor:pointer;font-family:arial,helvetica,sans-serif;">${submitText}</button>
1927
- </form>
1928
- </td></tr></tbody>
1929
- </table>`;
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 });
1930
1869
  }
1931
1870
  }
1932
1871
  };
@@ -2151,11 +2090,30 @@ let MailEditorElement = class extends LitElement {
2151
2090
  this.dragManager = null;
2152
2091
  this.callbacks = /* @__PURE__ */ new Map();
2153
2092
  this.unsubscribe = null;
2093
+ this._handleKeydown = (e) => {
2094
+ const mod = e.metaKey || e.ctrlKey;
2095
+ const tag = e.target?.tagName;
2096
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
2097
+ if (mod && e.key === "z" && !e.shiftKey) {
2098
+ e.preventDefault();
2099
+ this.store.undo();
2100
+ } else if (mod && (e.key === "y" || e.key === "z" && e.shiftKey)) {
2101
+ e.preventDefault();
2102
+ this.store.redo();
2103
+ } else if ((e.key === "Delete" || e.key === "Backspace") && this.store.selectedId) {
2104
+ e.preventDefault();
2105
+ this.store.removeContent(this.store.selectedId);
2106
+ } else if (e.key === "Escape") {
2107
+ this.store.select(null);
2108
+ }
2109
+ };
2154
2110
  }
2155
2111
  connectedCallback() {
2156
2112
  super.connectedCallback();
2157
2113
  this.registerBuiltInTools();
2158
2114
  this.applyOptions();
2115
+ this.setAttribute("tabindex", "0");
2116
+ this.addEventListener("keydown", this._handleKeydown);
2159
2117
  }
2160
2118
  firstUpdated() {
2161
2119
  this.dragManager = new DragManager(this.store, this.toolRegistry, this.shadowRoot);
@@ -2174,6 +2132,7 @@ let MailEditorElement = class extends LitElement {
2174
2132
  this.dragManager?.detach();
2175
2133
  this.unsubscribe?.();
2176
2134
  this.store.events.removeAllListeners();
2135
+ this.removeEventListener("keydown", this._handleKeydown);
2177
2136
  }
2178
2137
  // ----------------------------------------------------------
2179
2138
  // Public API — mirrors Unlayer
@@ -2283,4 +2242,4 @@ export {
2283
2242
  MailEditorElement as M,
2284
2243
  ToolRegistry as T
2285
2244
  };
2286
- //# sourceMappingURL=mail-editor-DzI7SmSe.js.map
2245
+ //# sourceMappingURL=mail-editor-ClkIyPni.js.map