@mieweb/ui 0.10.0-dev.39 → 0.10.0-dev.40

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 (66) hide show
  1. package/dist/brands/index.cjs +12 -12
  2. package/dist/brands/index.js +3 -3
  3. package/dist/{chunk-RPFATYUI.js → chunk-753ENNB6.js} +2 -2
  4. package/dist/{chunk-RPFATYUI.js.map → chunk-753ENNB6.js.map} +1 -1
  5. package/dist/{chunk-ZG6FXMDX.cjs → chunk-BWSZ2BHJ.cjs} +3 -3
  6. package/dist/{chunk-ZG6FXMDX.cjs.map → chunk-BWSZ2BHJ.cjs.map} +1 -1
  7. package/dist/{chunk-MHR2OS5I.js → chunk-CVXESGXJ.js} +4 -4
  8. package/dist/{chunk-MHR2OS5I.js.map → chunk-CVXESGXJ.js.map} +1 -1
  9. package/dist/{chunk-IPL2O2OL.cjs → chunk-DHU3FFC3.cjs} +32 -2
  10. package/dist/chunk-DHU3FFC3.cjs.map +1 -0
  11. package/dist/{chunk-TVEJKKIM.js → chunk-F6W62K4J.js} +3 -3
  12. package/dist/{chunk-TVEJKKIM.js.map → chunk-F6W62K4J.js.map} +1 -1
  13. package/dist/chunk-LUPRGPKP.js +2540 -0
  14. package/dist/chunk-LUPRGPKP.js.map +1 -0
  15. package/dist/{chunk-FYQZWLAM.cjs → chunk-OODVC2UD.cjs} +2 -2
  16. package/dist/{chunk-FYQZWLAM.cjs.map → chunk-OODVC2UD.cjs.map} +1 -1
  17. package/dist/{chunk-HNNWWLKY.cjs → chunk-S4XCPDEO.cjs} +4 -4
  18. package/dist/{chunk-HNNWWLKY.cjs.map → chunk-S4XCPDEO.cjs.map} +1 -1
  19. package/dist/{chunk-5A4SL436.js → chunk-SACHTD7B.js} +32 -2
  20. package/dist/chunk-SACHTD7B.js.map +1 -0
  21. package/dist/{chunk-6WX2OR3U.cjs → chunk-SAGJHL2Z.cjs} +35 -2
  22. package/dist/chunk-SAGJHL2Z.cjs.map +1 -0
  23. package/dist/{chunk-FDA3R5E7.js → chunk-T5NOFXOL.js} +35 -2
  24. package/dist/chunk-T5NOFXOL.js.map +1 -0
  25. package/dist/chunk-W3HXHIDO.cjs +2573 -0
  26. package/dist/chunk-W3HXHIDO.cjs.map +1 -0
  27. package/dist/components/CustomizableDashboard/index.cjs +9 -9
  28. package/dist/components/CustomizableDashboard/index.js +2 -2
  29. package/dist/components/EmailEditor/index.cjs +67 -0
  30. package/dist/components/EmailEditor/index.cjs.map +1 -0
  31. package/dist/components/EmailEditor/index.d.cts +347 -0
  32. package/dist/components/EmailEditor/index.d.ts +347 -0
  33. package/dist/components/EmailEditor/index.js +26 -0
  34. package/dist/components/EmailEditor/index.js.map +1 -0
  35. package/dist/components/PropertyList/index.d.cts +17 -1
  36. package/dist/components/PropertyList/index.d.ts +17 -1
  37. package/dist/components/RichTextEditor/index.cjs +4 -4
  38. package/dist/components/RichTextEditor/index.d.cts +5 -0
  39. package/dist/components/RichTextEditor/index.d.ts +5 -0
  40. package/dist/components/RichTextEditor/index.js +1 -1
  41. package/dist/components/Select/index.d.cts +2 -1
  42. package/dist/components/Select/index.d.ts +2 -1
  43. package/dist/components/Textarea/index.d.cts +2 -1
  44. package/dist/components/Textarea/index.d.ts +2 -1
  45. package/dist/esheet.cjs +6 -6
  46. package/dist/esheet.js +3 -3
  47. package/dist/hooks/index.cjs +5 -5
  48. package/dist/hooks/index.js +1 -1
  49. package/dist/index.cjs +276 -239
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +359 -10
  52. package/dist/index.d.ts +359 -10
  53. package/dist/index.js +29 -28
  54. package/dist/index.js.map +1 -1
  55. package/dist/styles.css +1 -1
  56. package/dist/tailwind-preset.cjs +4 -4
  57. package/dist/tailwind-preset.js +1 -1
  58. package/dist/utils/index.cjs +9 -9
  59. package/dist/utils/index.js +1 -1
  60. package/package.json +1 -1
  61. package/dist/Input-yN516s5_.d.cts +0 -3
  62. package/dist/Input-yN516s5_.d.ts +0 -3
  63. package/dist/chunk-5A4SL436.js.map +0 -1
  64. package/dist/chunk-6WX2OR3U.cjs.map +0 -1
  65. package/dist/chunk-FDA3R5E7.js.map +0 -1
  66. package/dist/chunk-IPL2O2OL.cjs.map +0 -1
@@ -0,0 +1,2573 @@
1
+ 'use strict';
2
+
3
+ var chunkLC7PVBXH_cjs = require('./chunk-LC7PVBXH.cjs');
4
+ var chunkDHU3FFC3_cjs = require('./chunk-DHU3FFC3.cjs');
5
+ var chunkPLMXN23Q_cjs = require('./chunk-PLMXN23Q.cjs');
6
+ var chunkBSDBI6H7_cjs = require('./chunk-BSDBI6H7.cjs');
7
+ var chunkUPWIJC6I_cjs = require('./chunk-UPWIJC6I.cjs');
8
+ var chunkZ5S5JEJD_cjs = require('./chunk-Z5S5JEJD.cjs');
9
+ var chunkCPDNX3LS_cjs = require('./chunk-CPDNX3LS.cjs');
10
+ var chunkANMX7YDL_cjs = require('./chunk-ANMX7YDL.cjs');
11
+ var chunkS5RVSXDV_cjs = require('./chunk-S5RVSXDV.cjs');
12
+ var React3 = require('react');
13
+ var core = require('@dnd-kit/core');
14
+ var sortable = require('@dnd-kit/sortable');
15
+ var utilities = require('@dnd-kit/utilities');
16
+ var lucideReact = require('lucide-react');
17
+ var DOMPurify = require('dompurify');
18
+ var jsxRuntime = require('react/jsx-runtime');
19
+
20
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
21
+
22
+ function _interopNamespace(e) {
23
+ if (e && e.__esModule) return e;
24
+ var n = Object.create(null);
25
+ if (e) {
26
+ Object.keys(e).forEach(function (k) {
27
+ if (k !== 'default') {
28
+ var d = Object.getOwnPropertyDescriptor(e, k);
29
+ Object.defineProperty(n, k, d.get ? d : {
30
+ enumerable: true,
31
+ get: function () { return e[k]; }
32
+ });
33
+ }
34
+ });
35
+ }
36
+ n.default = e;
37
+ return Object.freeze(n);
38
+ }
39
+
40
+ var React3__namespace = /*#__PURE__*/_interopNamespace(React3);
41
+ var DOMPurify__default = /*#__PURE__*/_interopDefault(DOMPurify);
42
+
43
+ // src/components/EmailEditor/types.ts
44
+ function generateEmailBlockId() {
45
+ return globalThis.crypto?.randomUUID?.() ?? `b${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
46
+ }
47
+ function createDefaultDesignSettings() {
48
+ return {
49
+ bodyBackgroundColor: "#f4f4f5",
50
+ contentBackgroundColor: "#ffffff",
51
+ contentWidth: 600,
52
+ textColor: "#374151",
53
+ fontFamily: "Arial, Helvetica, sans-serif",
54
+ headingColor: "#111827",
55
+ linkColor: "#2563eb",
56
+ buttonBackgroundColor: "#2563eb",
57
+ buttonTextColor: "#ffffff",
58
+ buttonBorderRadius: 6
59
+ };
60
+ }
61
+ function resolveDesignSettings(design) {
62
+ const defined = Object.entries(design ?? {}).filter(
63
+ ([, v]) => v !== void 0
64
+ );
65
+ return { ...createDefaultDesignSettings(), ...Object.fromEntries(defined) };
66
+ }
67
+ function createEmailBlock(type) {
68
+ return newBlock(type);
69
+ }
70
+ function newBlock(type) {
71
+ const id = generateEmailBlockId();
72
+ switch (type) {
73
+ case "heading":
74
+ return { id, type, level: 2, text: "Heading", alignment: "left" };
75
+ case "text":
76
+ return { id, type, content: "<p>Write your message here.</p>" };
77
+ case "button":
78
+ return {
79
+ id,
80
+ type,
81
+ text: "Learn more",
82
+ url: "https://",
83
+ alignment: "center"
84
+ };
85
+ case "image":
86
+ return { id, type, src: "", alt: "", alignment: "center" };
87
+ case "divider":
88
+ return { id, type, style: "solid", color: "#e5e7eb" };
89
+ case "spacer":
90
+ return { id, type, height: 24 };
91
+ case "social":
92
+ return {
93
+ id,
94
+ type,
95
+ alignment: "center",
96
+ iconSize: "md",
97
+ links: [
98
+ { platform: "linkedin", url: "https://linkedin.com" },
99
+ { platform: "website", url: "https://" }
100
+ ]
101
+ };
102
+ case "html":
103
+ return { id, type, html: "<p>Custom HTML</p>" };
104
+ case "footer":
105
+ return {
106
+ id,
107
+ type,
108
+ showUnsubscribe: true,
109
+ unsubscribeText: "Unsubscribe from these emails",
110
+ alignment: "center",
111
+ color: "#6b7280"
112
+ };
113
+ case "quote":
114
+ return {
115
+ id,
116
+ type,
117
+ text: "A short testimonial.",
118
+ author: "Name",
119
+ role: "Title"
120
+ };
121
+ case "table":
122
+ return {
123
+ id,
124
+ type,
125
+ headerRow: true,
126
+ rows: [
127
+ ["Item", "Detail"],
128
+ ["", ""]
129
+ ]
130
+ };
131
+ case "hero":
132
+ return {
133
+ id,
134
+ type,
135
+ headline: "Big announcement",
136
+ subtitle: "One line that says why it matters.",
137
+ ctaText: "Read more",
138
+ ctaUrl: "https://",
139
+ backgroundColor: "#1e3a5f",
140
+ textColor: "#ffffff",
141
+ alignment: "center",
142
+ padding: 48
143
+ };
144
+ case "columns":
145
+ return {
146
+ id,
147
+ type,
148
+ columnCount: 2,
149
+ columns: [
150
+ { id: generateEmailBlockId(), width: 50, blocks: [] },
151
+ { id: generateEmailBlockId(), width: 50, blocks: [] }
152
+ ]
153
+ };
154
+ }
155
+ }
156
+ function createEmptyEmailContentTree() {
157
+ return {
158
+ version: "1.0",
159
+ blocks: [
160
+ {
161
+ ...createEmailBlock("heading"),
162
+ level: 1,
163
+ text: "Your email title",
164
+ alignment: "center"
165
+ },
166
+ createEmailBlock("text"),
167
+ createEmailBlock("footer")
168
+ ]
169
+ };
170
+ }
171
+
172
+ // src/components/EmailEditor/renderEmailMjml.ts
173
+ function escapeHtml(value) {
174
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
175
+ }
176
+ function isSafeUrl(value) {
177
+ return /^(https?:|mailto:|tel:|#|\/(?![/\\\s])|\{\{[^{}]*\}\}$)/i.test(
178
+ (value ?? "").trim()
179
+ );
180
+ }
181
+ function safeUrl(value) {
182
+ const url = (value ?? "").trim();
183
+ return isSafeUrl(url) ? escapeHtml(url) : "#";
184
+ }
185
+ var COLOR = /^(#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})|(rgb|rgba|hsl|hsla)\([\d\s.,%]+\)|[a-z]+)$/i;
186
+ function safeColor(value, fallback) {
187
+ return value && COLOR.test(value.trim()) ? value.trim() : fallback;
188
+ }
189
+ function safeGradient(value) {
190
+ const gradient = value?.trim() ?? "";
191
+ return /^(linear|radial)-gradient\([^;{}<>"()]*(\([^;{}<>"()]*\)[^;{}<>"()]*)*\)$/i.test(
192
+ gradient
193
+ ) && !/url\s*\(/i.test(gradient) ? gradient : null;
194
+ }
195
+ function heroGradient(block) {
196
+ return /^[\w-]+$/.test(block.id) ? safeGradient(block.backgroundGradient) : null;
197
+ }
198
+ function hasHeroCta(block) {
199
+ return Boolean(block.ctaText && block.ctaUrl);
200
+ }
201
+ function align(value, fallback) {
202
+ return value === "left" || value === "center" || value === "right" ? value : fallback;
203
+ }
204
+ function px(value, fallback, max = 2e3) {
205
+ const n = Number(value);
206
+ return Number.isFinite(n) ? Math.min(Math.max(Math.round(n), 0), max) : fallback;
207
+ }
208
+ var SANITIZE_CONFIG = {
209
+ FORBID_TAGS: ["style", "form", "input", "textarea", "select", "button"],
210
+ // Host CSS classes (e.g. `fixed inset-0`) and ids would escape the style
211
+ // allowlist; srcset/xlink:href would escape the URL policy below.
212
+ FORBID_ATTR: ["class", "id", "srcset", "xlink:href"]
213
+ };
214
+ var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "background", "poster"]);
215
+ var SAFE_STYLE_PROPERTY = /^(color|background-color|font(-[a-z]+)?|text-(align|decoration|transform|indent)|line-height|letter-spacing|word-spacing|white-space|vertical-align|(margin|padding)(-(top|right|bottom|left))?|border(-(top|right|bottom|left))?(-(width|style|color))?|border-(collapse|spacing|radius)|(max-|min-)?(width|height)|display|list-style(-type)?)$/;
216
+ function sanitizeInlineStyle(style) {
217
+ return style.split(";").map((declaration) => {
218
+ const colon = declaration.indexOf(":");
219
+ if (colon === -1) return "";
220
+ const property = declaration.slice(0, colon).trim().toLowerCase();
221
+ const value = declaration.slice(colon + 1).trim();
222
+ return SAFE_STYLE_PROPERTY.test(property) && value && !/url\s*\(|expression\s*\(|@import|[<>\\]/i.test(value) && // Negative margins could pull content over the surrounding page.
223
+ !(property.startsWith("margin") && value.includes("-")) ? `${property}: ${value}` : "";
224
+ }).filter(Boolean).join("; ");
225
+ }
226
+ var purifier;
227
+ function installEmailPolicy(instance) {
228
+ instance.addHook("uponSanitizeAttribute", (_node, data) => {
229
+ if (URL_ATTRIBUTES.has(data.attrName)) {
230
+ if (!isSafeUrl(data.attrValue)) data.keepAttr = false;
231
+ return;
232
+ }
233
+ if (data.attrName !== "style") return;
234
+ data.attrValue = sanitizeInlineStyle(data.attrValue);
235
+ if (!data.attrValue) data.keepAttr = false;
236
+ });
237
+ return instance;
238
+ }
239
+ function createEmailHtmlSanitizer(win) {
240
+ const instance = DOMPurify__default.default(win);
241
+ if (!instance.isSupported) {
242
+ throw new Error(
243
+ "createEmailHtmlSanitizer: the supplied window cannot run DOMPurify."
244
+ );
245
+ }
246
+ installEmailPolicy(instance);
247
+ return (html) => instance.sanitize(html, SANITIZE_CONFIG);
248
+ }
249
+ function getPurifier() {
250
+ if (purifier !== void 0) return purifier;
251
+ if (typeof window === "undefined") return null;
252
+ const instance = DOMPurify__default.default(window);
253
+ if (!instance.isSupported) return purifier = null;
254
+ return purifier = installEmailPolicy(instance);
255
+ }
256
+ function sanitizeEmailHtml(html) {
257
+ return getPurifier()?.sanitize(html, SANITIZE_CONFIG) ?? null;
258
+ }
259
+ function defaultSanitize(html) {
260
+ const clean = sanitizeEmailHtml(html);
261
+ if (clean === null) {
262
+ throw new Error(
263
+ "renderEmailMjml: no DOM is available to sanitise HTML. Pass options.sanitizeHtml."
264
+ );
265
+ }
266
+ return clean;
267
+ }
268
+ var HEADING_SIZES = { 1: 32, 2: 26, 3: 22, 4: 18 };
269
+ var SOCIAL_NAMES = {
270
+ facebook: "facebook-noshare",
271
+ twitter: "twitter-noshare",
272
+ linkedin: "linkedin-noshare",
273
+ instagram: "instagram",
274
+ youtube: "youtube",
275
+ github: "github",
276
+ website: "web"
277
+ };
278
+ var ICON_SIZES = { sm: 20, md: 25, lg: 35 };
279
+ function renderContent(block, ctx, pad, available) {
280
+ const { design } = ctx;
281
+ switch (block.type) {
282
+ case "heading": {
283
+ const level = HEADING_SIZES[block.level] ? block.level : 2;
284
+ const color = safeColor(block.color, design.headingColor);
285
+ const size = HEADING_SIZES[level];
286
+ return `<mj-text align="${align(block.alignment, "left")}" font-size="${size}px" font-weight="bold" color="${color}" padding="${pad}"><h${level} style="font-size: ${size}px; line-height: 1.25;">${escapeHtml(block.text)}</h${level}></mj-text>`;
287
+ }
288
+ case "text": {
289
+ const color = block.color ? ` color="${safeColor(block.color, design.textColor)}"` : "";
290
+ return `<mj-text align="${align(block.alignment, "left")}"${color} padding="${pad}">${ctx.sanitize(block.content)}</mj-text>`;
291
+ }
292
+ case "button": {
293
+ const bg = safeColor(block.backgroundColor, design.buttonBackgroundColor);
294
+ const fg = safeColor(block.textColor, design.buttonTextColor);
295
+ const radius = px(
296
+ block.borderRadius ?? design.buttonBorderRadius,
297
+ 6,
298
+ 100
299
+ );
300
+ const width = block.fullWidth ? ' width="100%"' : "";
301
+ return `<mj-button align="${align(block.alignment, "center")}" background-color="${bg}" color="${fg}" border-radius="${radius}px" href="${safeUrl(block.url)}" font-size="16px" padding="${pad}"${width}>${escapeHtml(block.text)}</mj-button>`;
302
+ }
303
+ case "image": {
304
+ if (!block.src) return "";
305
+ const percent = /^(\d+(?:\.\d+)?|\.\d+)%$/.exec(
306
+ String(block.width ?? "").trim()
307
+ );
308
+ const share = percent ? Math.min(Number(percent[1]), 100) : 0;
309
+ const widthPx = percent ? Math.round(available * share / 100) : parseInt(String(block.width ?? ""), 10);
310
+ const width = Number.isFinite(widthPx) && widthPx > 0 && share !== 100 ? ` width="${px(widthPx, 600)}px"` : "";
311
+ const href = block.href ? ` href="${safeUrl(block.href)}"` : "";
312
+ const radius = block.borderRadius ? ` border-radius="${px(block.borderRadius, 0, 100)}px"` : "";
313
+ return `<mj-image src="${safeUrl(block.src)}" alt="${escapeHtml(block.alt ?? "")}" align="${align(block.alignment, "center")}"${width} padding="${pad}"${href}${radius} />`;
314
+ }
315
+ case "divider": {
316
+ const style = ["solid", "dashed", "dotted"].includes(block.style ?? "") ? block.style : "solid";
317
+ const width = /^\d+(px|%)$/.test(block.width ?? "") ? block.width : "100%";
318
+ return `<mj-divider border-color="${safeColor(block.color, "#e5e7eb")}" border-style="${style}" border-width="1px" width="${width}" padding="${pad}" />`;
319
+ }
320
+ case "spacer":
321
+ return `<mj-spacer height="${px(block.height, 24, 400)}px" />`;
322
+ case "social": {
323
+ const size = ICON_SIZES[block.iconSize ?? "md"] ?? ICON_SIZES.md;
324
+ const items = block.links.filter((link) => SOCIAL_NAMES[link.platform]).map(
325
+ (link) => `<mj-social-element name="${SOCIAL_NAMES[link.platform]}" href="${safeUrl(link.url)}" icon-size="${size}px" />`
326
+ ).join("");
327
+ return `<mj-social align="${align(block.alignment, "center")}" icon-size="${size}px" mode="horizontal" padding="${pad}">${items}</mj-social>`;
328
+ }
329
+ case "html":
330
+ return `<mj-raw>${ctx.sanitize(block.html)}</mj-raw>`;
331
+ case "footer": {
332
+ const color = safeColor(block.color, "#6b7280");
333
+ const lines = [block.companyName, block.address, block.phone].filter((line) => Boolean(line)).map(escapeHtml);
334
+ const links = [];
335
+ if (block.showUnsubscribe) {
336
+ links.push(
337
+ `<a href="${safeUrl(ctx.unsubscribeUrl)}" style="color: ${color};">${escapeHtml(block.unsubscribeText || "Unsubscribe")}</a>`
338
+ );
339
+ }
340
+ if (block.managePreferencesUrl) {
341
+ links.push(
342
+ `<a href="${safeUrl(block.managePreferencesUrl)}" style="color: ${color};">${escapeHtml(block.managePreferencesText || "Manage preferences")}</a>`
343
+ );
344
+ }
345
+ if (links.length) lines.push(links.join(" &nbsp; "));
346
+ return `<mj-text align="${align(block.alignment, "center")}" color="${color}" font-size="12px" padding="${pad}">${lines.join("<br />")}</mj-text>`;
347
+ }
348
+ case "quote": {
349
+ const fg = safeColor(block.textColor, "#1e293b");
350
+ const accent = safeColor(block.accentColor, design.linkColor);
351
+ const bg = safeColor(block.backgroundColor, "#f8fafc");
352
+ let attribution = "";
353
+ if (block.author) {
354
+ attribution = `<p style="margin: 12px 0 0; font-weight: 600;">${escapeHtml(block.author)}</p>`;
355
+ if (block.role) {
356
+ attribution += `<p style="margin: 2px 0 0; font-size: 13px; opacity: 0.75;">${escapeHtml(block.role)}</p>`;
357
+ }
358
+ }
359
+ return `<mj-text align="${align(block.alignment, "left")}" color="${fg}" container-background-color="${bg}" padding="${pad}"><div style="border-left: 4px solid ${accent}; padding: 8px 0 8px 16px;"><p style="font-size: 18px; font-style: italic; line-height: 1.6; margin: 0;">&ldquo;${escapeHtml(block.text)}&rdquo;</p>${attribution}</div></mj-text>`;
360
+ }
361
+ case "table": {
362
+ if (!block.rows.length) return "";
363
+ const border = safeColor(block.borderColor, "#e5e7eb");
364
+ const headBg = safeColor(block.headerBackgroundColor, "#f3f4f6");
365
+ const headFg = safeColor(block.headerTextColor, design.headingColor);
366
+ const textAlign = align(block.alignment, "left");
367
+ const rows = block.rows.map((row, r) => {
368
+ const head = block.headerRow && r === 0;
369
+ const tag = head ? "th" : "td";
370
+ const style = head ? `background-color: ${headBg}; color: ${headFg}; font-weight: 600; ` : "";
371
+ return `<tr>${row.map(
372
+ (cell) => `<${tag} style="${style}padding: 10px 12px; border: 1px solid ${border}; text-align: ${textAlign};">${escapeHtml(cell)}</${tag}>`
373
+ ).join("")}</tr>`;
374
+ }).join("");
375
+ return `<mj-table padding="${pad}" width="100%" cellpadding="0" cellspacing="0" css-class="email-table">${rows}</mj-table>`;
376
+ }
377
+ // Unsupported block types (host-owned blocks in a loaded document) are
378
+ // omitted from the email; the canvas shows a placeholder for them.
379
+ default:
380
+ return "";
381
+ }
382
+ }
383
+ function renderHero(block) {
384
+ const bg = safeColor(block.backgroundColor, "#1e3a5f");
385
+ const fg = safeColor(block.textColor, "#ffffff");
386
+ const a = align(block.alignment, "center");
387
+ const parts = [];
388
+ if (block.categoryText) {
389
+ parts.push(
390
+ `<mj-text align="${a}" color="${fg}" font-size="12px" font-weight="700" letter-spacing="2px" text-transform="uppercase" padding="0 0 8px 0">${escapeHtml(block.categoryText)}</mj-text>`
391
+ );
392
+ }
393
+ parts.push(
394
+ `<mj-text align="${a}" color="${fg}" font-size="32px" font-weight="bold" line-height="1.2" padding="0 0 8px 0"><h1 style="color: ${fg}; margin: 0; font-size: 32px; line-height: 1.2;">${escapeHtml(block.headline)}</h1></mj-text>`
395
+ );
396
+ if (block.subtitle) {
397
+ parts.push(
398
+ `<mj-text align="${a}" color="${fg}" font-size="16px" line-height="1.5" padding="0 0 16px 0">${escapeHtml(block.subtitle)}</mj-text>`
399
+ );
400
+ }
401
+ if (hasHeroCta(block)) {
402
+ parts.push(
403
+ `<mj-button align="${a}" background-color="${safeColor(block.ctaColor, "#ffffff")}" color="${bg}" font-size="16px" font-weight="600" border-radius="6px" padding="8px 0" href="${safeUrl(block.ctaUrl)}">${escapeHtml(block.ctaText ?? "")}</mj-button>`
404
+ );
405
+ }
406
+ const gradient = heroGradient(block) ? ` css-class="hero-gradient-${block.id}"` : "";
407
+ return `<mj-section full-width="full-width" background-color="${bg}" padding="${px(block.padding, 48, 200)}px 25px"${gradient}><mj-column>${parts.join("")}</mj-column></mj-section>`;
408
+ }
409
+ function renderBlock(block, ctx) {
410
+ if (block.type === "hero") return renderHero(block);
411
+ if (block.type === "columns") {
412
+ const bg = block.backgroundColor ? ` background-color="${safeColor(block.backgroundColor, "transparent")}"` : "";
413
+ const columns = block.columns.map((column) => {
414
+ const width = column.width ? ` width="${px(column.width, 50, 100)}%"` : "";
415
+ const colBg = column.backgroundColor ? ` background-color="${safeColor(column.backgroundColor, "transparent")}"` : "";
416
+ const share = px(column.width ?? 100 / block.columns.length, 50, 100);
417
+ const available = Math.round(ctx.design.contentWidth * share / 100) - 24;
418
+ const inner = column.blocks.map((b) => renderContent(b, ctx, "8px 12px", available)).join("");
419
+ return `<mj-column${width}${colBg}>${inner || "<mj-text> </mj-text>"}</mj-column>`;
420
+ }).join("");
421
+ return `<mj-section${bg}>${columns}</mj-section>`;
422
+ }
423
+ const content = renderContent(
424
+ block,
425
+ ctx,
426
+ "10px 25px",
427
+ ctx.design.contentWidth - 50
428
+ );
429
+ return content ? `<mj-section><mj-column>${content}</mj-column></mj-section>` : "";
430
+ }
431
+ function normalizeDesignSettings(design) {
432
+ const defaults = createDefaultDesignSettings();
433
+ const d = resolveDesignSettings(design);
434
+ const color = (key) => safeColor(d[key], defaults[key]);
435
+ return {
436
+ bodyBackgroundColor: color("bodyBackgroundColor"),
437
+ contentBackgroundColor: color("contentBackgroundColor"),
438
+ textColor: color("textColor"),
439
+ headingColor: color("headingColor"),
440
+ linkColor: color("linkColor"),
441
+ buttonBackgroundColor: color("buttonBackgroundColor"),
442
+ buttonTextColor: color("buttonTextColor"),
443
+ buttonBorderRadius: px(d.buttonBorderRadius, 6, 100),
444
+ // The design UI's 320px floor also applies to documents set programmatically.
445
+ contentWidth: Math.max(320, px(d.contentWidth, 600, 1200)),
446
+ fontFamily: typeof d.fontFamily === "string" && /^[\w\s,'-]+$/.test(d.fontFamily) ? d.fontFamily : defaults.fontFamily
447
+ };
448
+ }
449
+ function renderEmailMjml(tree, options = {}) {
450
+ const design = normalizeDesignSettings(options.design);
451
+ const ctx = {
452
+ design,
453
+ sanitize: options.sanitizeHtml ?? defaultSanitize,
454
+ unsubscribeUrl: options.unsubscribeUrl ?? "{{unsubscribe_url}}"
455
+ };
456
+ const gradients = tree.blocks.filter((b) => b.type === "hero").map((b) => {
457
+ const gradient = heroGradient(b);
458
+ return gradient ? `.hero-gradient-${b.id} { background: ${gradient} !important; }` : "";
459
+ }).filter(Boolean).join("\n");
460
+ return `<mjml>
461
+ <mj-head>
462
+ <mj-attributes>
463
+ <mj-all font-family="${escapeHtml(design.fontFamily)}" color="${design.textColor}" />
464
+ <mj-text font-size="16px" line-height="1.6" />
465
+ <mj-section background-color="${design.contentBackgroundColor}" />
466
+ </mj-attributes>
467
+ <mj-style>
468
+ a { color: ${design.linkColor}; }
469
+ h1, h2, h3, h4 { color: inherit; margin: 0; }
470
+ ${gradients}
471
+ </mj-style>
472
+ <mj-style inline="inline">
473
+ .email-table table { border-collapse: collapse; }
474
+ </mj-style>
475
+ </mj-head>
476
+ <mj-body background-color="${design.bodyBackgroundColor}" width="${design.contentWidth}px">
477
+ ${tree.blocks.map((block) => renderBlock(block, ctx)).join("\n ")}
478
+ </mj-body>
479
+ </mjml>`;
480
+ }
481
+ var SOCIAL_ICONS = {
482
+ facebook: lucideReact.Facebook,
483
+ twitter: lucideReact.Twitter,
484
+ linkedin: lucideReact.Linkedin,
485
+ instagram: lucideReact.Instagram,
486
+ youtube: lucideReact.Youtube,
487
+ github: lucideReact.Github,
488
+ website: lucideReact.Globe
489
+ };
490
+ var HEADING_SIZES2 = { 1: 32, 2: 26, 3: 22, 4: 18 };
491
+ function SafeHtml({
492
+ html,
493
+ style
494
+ }) {
495
+ const [clean, setClean] = React3__namespace.useState("");
496
+ React3__namespace.useEffect(() => setClean(sanitizeEmailHtml(html) ?? ""), [html]);
497
+ return /* @__PURE__ */ jsxRuntime.jsx(
498
+ "div",
499
+ {
500
+ style,
501
+ className: "[&_a]:underline [&_ol]:list-decimal [&_ol]:ps-6 [&_p]:my-3 [&_ul]:list-disc [&_ul]:ps-6",
502
+ dangerouslySetInnerHTML: { __html: clean }
503
+ }
504
+ );
505
+ }
506
+ function EmailBlockPreview({
507
+ block,
508
+ design,
509
+ renderChild,
510
+ emptyColumnLabel,
511
+ unsupportedBlockLabel,
512
+ platformLabels
513
+ }) {
514
+ const pad = "10px 25px";
515
+ switch (block.type) {
516
+ case "heading": {
517
+ const Tag = `h${block.level}`;
518
+ return /* @__PURE__ */ jsxRuntime.jsx(
519
+ Tag,
520
+ {
521
+ style: {
522
+ margin: 0,
523
+ padding: pad,
524
+ fontSize: HEADING_SIZES2[block.level],
525
+ fontWeight: 700,
526
+ lineHeight: 1.25,
527
+ textAlign: block.alignment ?? "left",
528
+ color: safeColor(block.color, design.headingColor)
529
+ },
530
+ children: block.text
531
+ }
532
+ );
533
+ }
534
+ case "text":
535
+ return /* @__PURE__ */ jsxRuntime.jsx(
536
+ SafeHtml,
537
+ {
538
+ html: block.content,
539
+ style: {
540
+ padding: pad,
541
+ textAlign: block.alignment ?? "left",
542
+ color: safeColor(block.color, design.textColor)
543
+ }
544
+ }
545
+ );
546
+ case "button":
547
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: pad, textAlign: block.alignment ?? "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(
548
+ "span",
549
+ {
550
+ style: {
551
+ display: block.fullWidth ? "block" : "inline-block",
552
+ padding: "10px 25px",
553
+ background: safeColor(
554
+ block.backgroundColor,
555
+ design.buttonBackgroundColor
556
+ ),
557
+ color: safeColor(block.textColor, design.buttonTextColor),
558
+ borderRadius: block.borderRadius ?? design.buttonBorderRadius,
559
+ fontWeight: 600
560
+ },
561
+ children: block.text
562
+ }
563
+ ) });
564
+ case "image":
565
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: pad, textAlign: block.alignment ?? "center" }, children: block.src ? /* @__PURE__ */ jsxRuntime.jsx(
566
+ "img",
567
+ {
568
+ src: block.src,
569
+ alt: block.alt,
570
+ style: {
571
+ display: "inline-block",
572
+ maxWidth: "100%",
573
+ // Zero/invalid widths fall back to full width, matching the
574
+ // omitted width in renderEmailMjml.
575
+ width: parseFloat(String(block.width ?? "")) > 0 ? block.width : "100%",
576
+ borderRadius: block.borderRadius
577
+ }
578
+ }
579
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-border text-muted-foreground flex h-32 items-center justify-center rounded-md border-2 border-dashed", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Image, { className: "h-8 w-8", "aria-hidden": "true" }) }) });
580
+ case "divider":
581
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: pad }, children: /* @__PURE__ */ jsxRuntime.jsx(
582
+ "hr",
583
+ {
584
+ style: {
585
+ margin: "0 auto",
586
+ border: 0,
587
+ borderTop: `1px ${block.style ?? "solid"} ${safeColor(block.color, "#e5e7eb")}`,
588
+ width: block.width ?? "100%"
589
+ }
590
+ }
591
+ ) });
592
+ case "spacer":
593
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: block.height } });
594
+ case "social": {
595
+ const size = { sm: 20, md: 25, lg: 35 }[block.iconSize ?? "md"];
596
+ return /* @__PURE__ */ jsxRuntime.jsx(
597
+ "div",
598
+ {
599
+ style: {
600
+ padding: pad,
601
+ display: "flex",
602
+ gap: 8,
603
+ justifyContent: block.alignment === "left" ? "flex-start" : block.alignment === "right" ? "flex-end" : "center"
604
+ },
605
+ children: block.links.map((link, i) => {
606
+ const Icon = SOCIAL_ICONS[link.platform] ?? lucideReact.Globe;
607
+ return /* @__PURE__ */ jsxRuntime.jsx(
608
+ Icon,
609
+ {
610
+ width: size,
611
+ height: size,
612
+ role: "img",
613
+ "aria-label": platformLabels[link.platform] ?? link.platform
614
+ },
615
+ i
616
+ );
617
+ })
618
+ }
619
+ );
620
+ }
621
+ case "html":
622
+ return /* @__PURE__ */ jsxRuntime.jsx(SafeHtml, { html: block.html, style: { padding: pad } });
623
+ case "footer": {
624
+ const color = safeColor(block.color, "#6b7280");
625
+ return /* @__PURE__ */ jsxRuntime.jsxs(
626
+ "div",
627
+ {
628
+ style: {
629
+ padding: pad,
630
+ fontSize: 12,
631
+ color,
632
+ textAlign: block.alignment ?? "center"
633
+ },
634
+ children: [
635
+ [block.companyName, block.address, block.phone].filter(Boolean).map((line, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { children: line }, i)),
636
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
637
+ block.showUnsubscribe && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { textDecoration: "underline" }, children: block.unsubscribeText || "Unsubscribe" }),
638
+ block.managePreferencesUrl && /* @__PURE__ */ jsxRuntime.jsx(
639
+ "span",
640
+ {
641
+ style: { textDecoration: "underline", marginInlineStart: 12 },
642
+ children: block.managePreferencesText || "Manage preferences"
643
+ }
644
+ )
645
+ ] })
646
+ ]
647
+ }
648
+ );
649
+ }
650
+ case "quote":
651
+ return /* @__PURE__ */ jsxRuntime.jsxs(
652
+ "div",
653
+ {
654
+ style: {
655
+ margin: pad,
656
+ padding: "8px 16px",
657
+ background: safeColor(block.backgroundColor, "#f8fafc"),
658
+ color: safeColor(block.textColor, "#1e293b"),
659
+ borderInlineStart: `4px solid ${safeColor(block.accentColor, design.linkColor)}`,
660
+ textAlign: block.alignment ?? "left"
661
+ },
662
+ children: [
663
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { margin: 0, fontSize: 18, fontStyle: "italic" }, children: [
664
+ "\u201C",
665
+ block.text,
666
+ "\u201D"
667
+ ] }),
668
+ block.author && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "12px 0 0", fontWeight: 600 }, children: block.author }),
669
+ block.author && block.role && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "2px 0 0", fontSize: 13, opacity: 0.75 }, children: block.role })
670
+ ]
671
+ }
672
+ );
673
+ case "table": {
674
+ const border = `1px solid ${safeColor(block.borderColor, "#e5e7eb")}`;
675
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: pad }, children: /* @__PURE__ */ jsxRuntime.jsx("table", { style: { width: "100%", borderCollapse: "collapse" }, children: /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: block.rows.map((row, r) => {
676
+ const head = block.headerRow && r === 0;
677
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { children: row.map((cell, c) => {
678
+ const Cell = head ? "th" : "td";
679
+ return /* @__PURE__ */ jsxRuntime.jsx(
680
+ Cell,
681
+ {
682
+ style: {
683
+ border,
684
+ padding: "10px 12px",
685
+ textAlign: block.alignment ?? "left",
686
+ ...head && {
687
+ background: safeColor(
688
+ block.headerBackgroundColor,
689
+ "#f3f4f6"
690
+ ),
691
+ color: safeColor(
692
+ block.headerTextColor,
693
+ design.headingColor
694
+ )
695
+ }
696
+ },
697
+ children: cell
698
+ },
699
+ c
700
+ );
701
+ }) }, r);
702
+ }) }) }) });
703
+ }
704
+ case "hero": {
705
+ const color = safeColor(block.textColor, "#ffffff");
706
+ const bg = safeColor(block.backgroundColor, "#1e3a5f");
707
+ return /* @__PURE__ */ jsxRuntime.jsxs(
708
+ "div",
709
+ {
710
+ style: {
711
+ padding: `${block.padding ?? 48}px 25px`,
712
+ background: safeGradient(block.backgroundGradient) ?? bg,
713
+ color,
714
+ textAlign: block.alignment ?? "center"
715
+ },
716
+ children: [
717
+ block.categoryText && /* @__PURE__ */ jsxRuntime.jsx(
718
+ "div",
719
+ {
720
+ style: {
721
+ fontSize: 12,
722
+ fontWeight: 700,
723
+ letterSpacing: 2,
724
+ textTransform: "uppercase",
725
+ marginBottom: 8
726
+ },
727
+ children: block.categoryText
728
+ }
729
+ ),
730
+ /* @__PURE__ */ jsxRuntime.jsx(
731
+ "h1",
732
+ {
733
+ style: { margin: "0 0 8px", fontSize: 32, lineHeight: 1.2, color },
734
+ children: block.headline
735
+ }
736
+ ),
737
+ block.subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 16px" }, children: block.subtitle }),
738
+ hasHeroCta(block) && /* @__PURE__ */ jsxRuntime.jsx(
739
+ "span",
740
+ {
741
+ style: {
742
+ display: "inline-block",
743
+ padding: "10px 25px",
744
+ borderRadius: 6,
745
+ fontWeight: 600,
746
+ background: safeColor(block.ctaColor, "#ffffff"),
747
+ color: bg
748
+ },
749
+ children: block.ctaText
750
+ }
751
+ )
752
+ ]
753
+ }
754
+ );
755
+ }
756
+ case "columns":
757
+ return /* @__PURE__ */ jsxRuntime.jsx(
758
+ "div",
759
+ {
760
+ style: {
761
+ display: "flex",
762
+ flexWrap: "wrap",
763
+ background: block.backgroundColor ? safeColor(block.backgroundColor, "transparent") : void 0
764
+ },
765
+ children: block.columns.map((column) => /* @__PURE__ */ jsxRuntime.jsx(
766
+ "div",
767
+ {
768
+ style: {
769
+ flex: `1 1 ${column.width ?? 100 / block.columns.length}%`,
770
+ minWidth: 160,
771
+ padding: 4,
772
+ background: column.backgroundColor ? safeColor(column.backgroundColor, "transparent") : void 0
773
+ },
774
+ children: column.blocks.length ? column.blocks.map((child) => /* @__PURE__ */ jsxRuntime.jsx(React3__namespace.Fragment, { children: renderChild(child) }, child.id)) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-border text-muted-foreground rounded-md border border-dashed p-4 text-center text-xs", children: emptyColumnLabel })
775
+ },
776
+ column.id
777
+ ))
778
+ }
779
+ );
780
+ // Host-owned block types (e.g. Waggleline-only blocks) are kept in the
781
+ // document but rendered as a visible placeholder; `renderEmailMjml` omits them.
782
+ default:
783
+ return /* @__PURE__ */ jsxRuntime.jsx(
784
+ "div",
785
+ {
786
+ role: "note",
787
+ className: "border-border text-muted-foreground m-2 rounded-md border border-dashed p-4 text-center text-xs",
788
+ children: unsupportedBlockLabel.replace(
789
+ "{type}",
790
+ block.type
791
+ )
792
+ }
793
+ );
794
+ }
795
+ }
796
+ function Button2(props) {
797
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkANMX7YDL_cjs.Button, { type: "button", ...props });
798
+ }
799
+ function TextField({
800
+ multiline,
801
+ ...props
802
+ }) {
803
+ const Control = multiline ? chunkBSDBI6H7_cjs.Textarea : chunkZ5S5JEJD_cjs.Input;
804
+ return /* @__PURE__ */ jsxRuntime.jsx(
805
+ Control,
806
+ {
807
+ label: props.label,
808
+ value: props.value ?? "",
809
+ placeholder: props.placeholder,
810
+ onChange: (e) => props.onChange(e.target.value)
811
+ }
812
+ );
813
+ }
814
+ function NumberField({
815
+ label,
816
+ value,
817
+ onChange,
818
+ min = 0,
819
+ max
820
+ }) {
821
+ return /* @__PURE__ */ jsxRuntime.jsx(
822
+ chunkZ5S5JEJD_cjs.Input,
823
+ {
824
+ type: "number",
825
+ label,
826
+ min,
827
+ max,
828
+ value: value ?? "",
829
+ onChange: (e) => {
830
+ const n = Number(e.target.value);
831
+ if (e.target.value !== "" && Number.isFinite(n)) onChange(n);
832
+ }
833
+ }
834
+ );
835
+ }
836
+ var HEX6 = /^#[0-9a-f]{6}$/i;
837
+ function ColorField({
838
+ label,
839
+ value,
840
+ onChange,
841
+ fallback = "#000000"
842
+ }) {
843
+ const swatch = value && HEX6.test(value) ? value : fallback;
844
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-end gap-2", children: [
845
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
846
+ chunkZ5S5JEJD_cjs.Input,
847
+ {
848
+ label,
849
+ value: value ?? "",
850
+ placeholder: fallback,
851
+ onChange: (e) => onChange(e.target.value)
852
+ }
853
+ ) }),
854
+ /* @__PURE__ */ jsxRuntime.jsx(
855
+ "input",
856
+ {
857
+ type: "color",
858
+ "aria-label": label,
859
+ value: HEX6.test(swatch) ? swatch : "#000000",
860
+ onChange: (e) => onChange(e.target.value),
861
+ className: "border-border bg-background h-10 w-10 shrink-0 cursor-pointer rounded-md border p-1"
862
+ }
863
+ )
864
+ ] });
865
+ }
866
+ function SelectField({
867
+ label,
868
+ value,
869
+ onChange,
870
+ options
871
+ }) {
872
+ return /* @__PURE__ */ jsxRuntime.jsx(
873
+ chunkUPWIJC6I_cjs.Select,
874
+ {
875
+ label,
876
+ value,
877
+ options,
878
+ onValueChange: (v) => onChange(v)
879
+ }
880
+ );
881
+ }
882
+ function SwitchField({ label, value, onChange }) {
883
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkPLMXN23Q_cjs.Switch, { label, checked: Boolean(value), onCheckedChange: onChange });
884
+ }
885
+ var SOCIAL_PLATFORMS = [
886
+ "facebook",
887
+ "twitter",
888
+ "linkedin",
889
+ "instagram",
890
+ "youtube",
891
+ "github",
892
+ "website"
893
+ ];
894
+ var CONTENT_BLOCK_TYPES = [
895
+ "heading",
896
+ "text",
897
+ "button",
898
+ "image",
899
+ "divider",
900
+ "spacer",
901
+ "social",
902
+ "quote",
903
+ "table",
904
+ "html",
905
+ "footer"
906
+ ];
907
+ function resizeColumns(columns, count) {
908
+ const width = Math.floor(100 / count);
909
+ const next = Array.from({ length: count }, (_, i) => ({
910
+ ...columns[i] ?? { id: generateEmailBlockId(), blocks: [] },
911
+ // The last column absorbs the rounding remainder so widths sum to 100.
912
+ width: i === count - 1 ? 100 - width * (count - 1) : width
913
+ }));
914
+ const dropped = columns.slice(count).flatMap((c) => c.blocks);
915
+ next[count - 1] = {
916
+ ...next[count - 1],
917
+ blocks: [...next[count - 1].blocks, ...dropped]
918
+ };
919
+ return next;
920
+ }
921
+ function EmailBlockSettings({
922
+ block,
923
+ design,
924
+ onChange,
925
+ onAddToColumn,
926
+ labels,
927
+ variableGroups,
928
+ onUploadImage
929
+ }) {
930
+ const f = labels.fields;
931
+ const o = labels.options;
932
+ const set = onChange;
933
+ const alignOptions = [
934
+ { value: "left", label: o.left },
935
+ { value: "center", label: o.center },
936
+ { value: "right", label: o.right }
937
+ ];
938
+ const alignment = (value, fallback) => /* @__PURE__ */ jsxRuntime.jsx(
939
+ SelectField,
940
+ {
941
+ label: f.alignment,
942
+ value: value ?? fallback,
943
+ options: alignOptions,
944
+ onChange: (v) => onChange({ alignment: v })
945
+ }
946
+ );
947
+ const uploadRef = React3__namespace.useRef(null);
948
+ const [uploading, setUploading] = React3__namespace.useState(false);
949
+ const [uploadFailed, setUploadFailed] = React3__namespace.useState(false);
950
+ const emitted = React3__namespace.useRef(null);
951
+ switch (block.type) {
952
+ case "heading":
953
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
954
+ /* @__PURE__ */ jsxRuntime.jsx(
955
+ TextField,
956
+ {
957
+ label: f.text,
958
+ value: block.text,
959
+ onChange: (text) => set({ text })
960
+ }
961
+ ),
962
+ /* @__PURE__ */ jsxRuntime.jsx(
963
+ SelectField,
964
+ {
965
+ label: f.level,
966
+ value: String(block.level),
967
+ options: ["1", "2", "3", "4"].map((v) => ({
968
+ value: v,
969
+ label: `H${v}`
970
+ })),
971
+ onChange: (v) => set({ level: Number(v) })
972
+ }
973
+ ),
974
+ alignment(block.alignment, "left"),
975
+ /* @__PURE__ */ jsxRuntime.jsx(
976
+ ColorField,
977
+ {
978
+ label: f.color,
979
+ value: block.color,
980
+ fallback: design.headingColor,
981
+ onChange: (color) => set({ color })
982
+ }
983
+ )
984
+ ] });
985
+ case "text":
986
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
987
+ /* @__PURE__ */ jsxRuntime.jsx(
988
+ chunkDHU3FFC3_cjs.RichTextEditor,
989
+ {
990
+ "aria-label": f.content,
991
+ value: block.content === emitted.current ? block.content : sanitizeEmailHtml(block.content) ?? "",
992
+ onChange: (content) => {
993
+ emitted.current = content;
994
+ set({ content });
995
+ },
996
+ sanitizeHtml: (html) => sanitizeEmailHtml(html) ?? "",
997
+ variableGroups,
998
+ enableDictation: false
999
+ }
1000
+ ),
1001
+ alignment(block.alignment, "left"),
1002
+ /* @__PURE__ */ jsxRuntime.jsx(
1003
+ ColorField,
1004
+ {
1005
+ label: f.color,
1006
+ value: block.color,
1007
+ fallback: design.textColor,
1008
+ onChange: (color) => set({ color })
1009
+ }
1010
+ )
1011
+ ] });
1012
+ case "button":
1013
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1014
+ /* @__PURE__ */ jsxRuntime.jsx(
1015
+ TextField,
1016
+ {
1017
+ label: f.text,
1018
+ value: block.text,
1019
+ onChange: (text) => set({ text })
1020
+ }
1021
+ ),
1022
+ /* @__PURE__ */ jsxRuntime.jsx(
1023
+ TextField,
1024
+ {
1025
+ label: f.url,
1026
+ value: block.url,
1027
+ onChange: (url) => set({ url })
1028
+ }
1029
+ ),
1030
+ alignment(block.alignment, "center"),
1031
+ /* @__PURE__ */ jsxRuntime.jsx(
1032
+ ColorField,
1033
+ {
1034
+ label: f.backgroundColor,
1035
+ value: block.backgroundColor,
1036
+ fallback: design.buttonBackgroundColor,
1037
+ onChange: (backgroundColor) => set({ backgroundColor })
1038
+ }
1039
+ ),
1040
+ /* @__PURE__ */ jsxRuntime.jsx(
1041
+ ColorField,
1042
+ {
1043
+ label: f.textColor,
1044
+ value: block.textColor,
1045
+ fallback: design.buttonTextColor,
1046
+ onChange: (textColor) => set({ textColor })
1047
+ }
1048
+ ),
1049
+ /* @__PURE__ */ jsxRuntime.jsx(
1050
+ NumberField,
1051
+ {
1052
+ label: f.borderRadius,
1053
+ value: block.borderRadius ?? design.buttonBorderRadius,
1054
+ max: 100,
1055
+ onChange: (borderRadius) => set({ borderRadius })
1056
+ }
1057
+ ),
1058
+ /* @__PURE__ */ jsxRuntime.jsx(
1059
+ SwitchField,
1060
+ {
1061
+ label: f.fullWidth,
1062
+ value: block.fullWidth,
1063
+ onChange: (fullWidth) => set({ fullWidth })
1064
+ }
1065
+ )
1066
+ ] });
1067
+ case "image":
1068
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1069
+ /* @__PURE__ */ jsxRuntime.jsx(
1070
+ TextField,
1071
+ {
1072
+ label: f.imageUrl,
1073
+ value: block.src,
1074
+ onChange: (src) => set({ src })
1075
+ }
1076
+ ),
1077
+ onUploadImage && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1078
+ /* @__PURE__ */ jsxRuntime.jsx(
1079
+ "input",
1080
+ {
1081
+ ref: uploadRef,
1082
+ type: "file",
1083
+ accept: "image/*",
1084
+ className: "sr-only",
1085
+ tabIndex: -1,
1086
+ "aria-hidden": "true",
1087
+ onChange: async (e) => {
1088
+ const file = e.target.files?.[0];
1089
+ e.target.value = "";
1090
+ if (!file) return;
1091
+ setUploading(true);
1092
+ setUploadFailed(false);
1093
+ try {
1094
+ set({ src: await onUploadImage(file) });
1095
+ } catch {
1096
+ setUploadFailed(true);
1097
+ } finally {
1098
+ setUploading(false);
1099
+ }
1100
+ }
1101
+ }
1102
+ ),
1103
+ /* @__PURE__ */ jsxRuntime.jsx(
1104
+ Button2,
1105
+ {
1106
+ variant: "outline",
1107
+ size: "sm",
1108
+ isLoading: uploading,
1109
+ leftIcon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Upload, { className: "h-4 w-4", "aria-hidden": "true" }),
1110
+ onClick: () => uploadRef.current?.click(),
1111
+ children: f.uploadImage
1112
+ }
1113
+ ),
1114
+ uploadFailed && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "text-destructive text-sm", children: f.uploadFailed })
1115
+ ] }),
1116
+ /* @__PURE__ */ jsxRuntime.jsx(
1117
+ TextField,
1118
+ {
1119
+ label: f.altText,
1120
+ value: block.alt,
1121
+ onChange: (alt) => set({ alt })
1122
+ }
1123
+ ),
1124
+ /* @__PURE__ */ jsxRuntime.jsx(
1125
+ TextField,
1126
+ {
1127
+ label: f.url,
1128
+ value: block.href,
1129
+ onChange: (href) => set({ href })
1130
+ }
1131
+ ),
1132
+ /* @__PURE__ */ jsxRuntime.jsx(
1133
+ TextField,
1134
+ {
1135
+ label: f.width,
1136
+ value: block.width === void 0 ? "" : String(block.width),
1137
+ placeholder: "100%",
1138
+ onChange: (width) => set({ width: /^\d+$/.test(width) ? Number(width) : width })
1139
+ }
1140
+ ),
1141
+ /* @__PURE__ */ jsxRuntime.jsx(
1142
+ NumberField,
1143
+ {
1144
+ label: f.borderRadius,
1145
+ value: block.borderRadius,
1146
+ max: 100,
1147
+ onChange: (borderRadius) => set({ borderRadius })
1148
+ }
1149
+ ),
1150
+ alignment(block.alignment, "center")
1151
+ ] });
1152
+ case "divider":
1153
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1154
+ /* @__PURE__ */ jsxRuntime.jsx(
1155
+ SelectField,
1156
+ {
1157
+ label: f.style,
1158
+ value: block.style ?? "solid",
1159
+ options: ["solid", "dashed", "dotted"].map((v) => ({
1160
+ value: v,
1161
+ label: o[v]
1162
+ })),
1163
+ onChange: (style) => set({ style })
1164
+ }
1165
+ ),
1166
+ /* @__PURE__ */ jsxRuntime.jsx(
1167
+ ColorField,
1168
+ {
1169
+ label: f.color,
1170
+ value: block.color,
1171
+ onChange: (color) => set({ color })
1172
+ }
1173
+ ),
1174
+ /* @__PURE__ */ jsxRuntime.jsx(
1175
+ TextField,
1176
+ {
1177
+ label: f.width,
1178
+ value: block.width,
1179
+ placeholder: "100%",
1180
+ onChange: (width) => set({ width })
1181
+ }
1182
+ )
1183
+ ] });
1184
+ case "spacer":
1185
+ return /* @__PURE__ */ jsxRuntime.jsx(
1186
+ NumberField,
1187
+ {
1188
+ label: f.height,
1189
+ value: block.height,
1190
+ max: 400,
1191
+ onChange: (height) => set({ height })
1192
+ }
1193
+ );
1194
+ case "social":
1195
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1196
+ alignment(block.alignment, "center"),
1197
+ /* @__PURE__ */ jsxRuntime.jsx(
1198
+ SelectField,
1199
+ {
1200
+ label: f.iconSize,
1201
+ value: block.iconSize ?? "md",
1202
+ options: ["sm", "md", "lg"].map((v) => ({
1203
+ value: v,
1204
+ label: o[v]
1205
+ })),
1206
+ onChange: (iconSize) => set({ iconSize })
1207
+ }
1208
+ ),
1209
+ block.links.map((link, i) => {
1210
+ const update = (patch) => set({
1211
+ links: block.links.map(
1212
+ (l, j) => j === i ? { ...l, ...patch } : l
1213
+ )
1214
+ });
1215
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1216
+ "fieldset",
1217
+ {
1218
+ className: "border-border space-y-2 rounded-md border p-3",
1219
+ children: [
1220
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { className: "sr-only", children: f.socialLink.replace("{number}", String(i + 1)) }),
1221
+ /* @__PURE__ */ jsxRuntime.jsx(
1222
+ SelectField,
1223
+ {
1224
+ label: f.platform,
1225
+ value: link.platform,
1226
+ options: SOCIAL_PLATFORMS.map((p) => ({
1227
+ value: p,
1228
+ label: labels.platforms[p]
1229
+ })),
1230
+ onChange: (platform) => update({ platform })
1231
+ }
1232
+ ),
1233
+ /* @__PURE__ */ jsxRuntime.jsx(
1234
+ TextField,
1235
+ {
1236
+ label: f.url,
1237
+ value: link.url,
1238
+ onChange: (url) => update({ url })
1239
+ }
1240
+ ),
1241
+ /* @__PURE__ */ jsxRuntime.jsx(
1242
+ Button2,
1243
+ {
1244
+ variant: "ghost",
1245
+ size: "sm",
1246
+ leftIcon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-4 w-4", "aria-hidden": "true" }),
1247
+ onClick: () => set({ links: block.links.filter((_, j) => j !== i) }),
1248
+ children: f.removeLink
1249
+ }
1250
+ )
1251
+ ]
1252
+ },
1253
+ i
1254
+ );
1255
+ }),
1256
+ /* @__PURE__ */ jsxRuntime.jsx(
1257
+ Button2,
1258
+ {
1259
+ variant: "outline",
1260
+ size: "sm",
1261
+ leftIcon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4", "aria-hidden": "true" }),
1262
+ onClick: () => set({
1263
+ links: [
1264
+ ...block.links,
1265
+ { platform: "website", url: "https://" }
1266
+ ]
1267
+ }),
1268
+ children: f.addLink
1269
+ }
1270
+ )
1271
+ ] });
1272
+ case "html":
1273
+ return /* @__PURE__ */ jsxRuntime.jsx(
1274
+ chunkBSDBI6H7_cjs.Textarea,
1275
+ {
1276
+ label: f.html,
1277
+ value: block.html,
1278
+ rows: 10,
1279
+ className: "font-mono text-xs",
1280
+ onChange: (e) => set({ html: e.target.value })
1281
+ }
1282
+ );
1283
+ case "footer":
1284
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1285
+ /* @__PURE__ */ jsxRuntime.jsx(
1286
+ TextField,
1287
+ {
1288
+ label: f.companyName,
1289
+ value: block.companyName,
1290
+ onChange: (companyName) => set({ companyName })
1291
+ }
1292
+ ),
1293
+ /* @__PURE__ */ jsxRuntime.jsx(
1294
+ TextField,
1295
+ {
1296
+ label: f.address,
1297
+ value: block.address,
1298
+ onChange: (address) => set({ address })
1299
+ }
1300
+ ),
1301
+ /* @__PURE__ */ jsxRuntime.jsx(
1302
+ TextField,
1303
+ {
1304
+ label: f.phone,
1305
+ value: block.phone,
1306
+ onChange: (phone) => set({ phone })
1307
+ }
1308
+ ),
1309
+ /* @__PURE__ */ jsxRuntime.jsx(
1310
+ SwitchField,
1311
+ {
1312
+ label: f.showUnsubscribe,
1313
+ value: block.showUnsubscribe,
1314
+ onChange: (showUnsubscribe) => set({ showUnsubscribe })
1315
+ }
1316
+ ),
1317
+ block.showUnsubscribe && /* @__PURE__ */ jsxRuntime.jsx(
1318
+ TextField,
1319
+ {
1320
+ label: f.unsubscribeText,
1321
+ value: block.unsubscribeText,
1322
+ onChange: (unsubscribeText) => set({ unsubscribeText })
1323
+ }
1324
+ ),
1325
+ /* @__PURE__ */ jsxRuntime.jsx(
1326
+ TextField,
1327
+ {
1328
+ label: f.managePreferencesUrl,
1329
+ value: block.managePreferencesUrl,
1330
+ onChange: (managePreferencesUrl) => set({ managePreferencesUrl })
1331
+ }
1332
+ ),
1333
+ /* @__PURE__ */ jsxRuntime.jsx(
1334
+ TextField,
1335
+ {
1336
+ label: f.managePreferencesText,
1337
+ value: block.managePreferencesText,
1338
+ onChange: (managePreferencesText) => set({ managePreferencesText })
1339
+ }
1340
+ ),
1341
+ alignment(block.alignment, "center"),
1342
+ /* @__PURE__ */ jsxRuntime.jsx(
1343
+ ColorField,
1344
+ {
1345
+ label: f.color,
1346
+ value: block.color,
1347
+ onChange: (color) => set({ color })
1348
+ }
1349
+ )
1350
+ ] });
1351
+ case "quote":
1352
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1353
+ /* @__PURE__ */ jsxRuntime.jsx(
1354
+ TextField,
1355
+ {
1356
+ multiline: true,
1357
+ label: f.text,
1358
+ value: block.text,
1359
+ onChange: (text) => set({ text })
1360
+ }
1361
+ ),
1362
+ /* @__PURE__ */ jsxRuntime.jsx(
1363
+ TextField,
1364
+ {
1365
+ label: f.author,
1366
+ value: block.author,
1367
+ onChange: (author) => set({ author })
1368
+ }
1369
+ ),
1370
+ /* @__PURE__ */ jsxRuntime.jsx(
1371
+ TextField,
1372
+ {
1373
+ label: f.role,
1374
+ value: block.role,
1375
+ onChange: (role) => set({ role })
1376
+ }
1377
+ ),
1378
+ alignment(block.alignment, "left"),
1379
+ /* @__PURE__ */ jsxRuntime.jsx(
1380
+ ColorField,
1381
+ {
1382
+ label: f.backgroundColor,
1383
+ value: block.backgroundColor,
1384
+ onChange: (backgroundColor) => set({ backgroundColor })
1385
+ }
1386
+ ),
1387
+ /* @__PURE__ */ jsxRuntime.jsx(
1388
+ ColorField,
1389
+ {
1390
+ label: f.textColor,
1391
+ value: block.textColor,
1392
+ onChange: (textColor) => set({ textColor })
1393
+ }
1394
+ ),
1395
+ /* @__PURE__ */ jsxRuntime.jsx(
1396
+ ColorField,
1397
+ {
1398
+ label: f.accentColor,
1399
+ value: block.accentColor,
1400
+ fallback: design.linkColor,
1401
+ onChange: (accentColor) => set({ accentColor })
1402
+ }
1403
+ )
1404
+ ] });
1405
+ case "table": {
1406
+ const cols = Math.max(1, ...block.rows.map((r) => r.length));
1407
+ const setCell = (r, c, value) => set({
1408
+ rows: block.rows.map(
1409
+ (row, i) => i === r ? row.map((cell, j) => j === c ? value : cell) : row
1410
+ )
1411
+ });
1412
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1413
+ /* @__PURE__ */ jsxRuntime.jsx(
1414
+ SwitchField,
1415
+ {
1416
+ label: f.headerRow,
1417
+ value: block.headerRow,
1418
+ onChange: (headerRow) => set({ headerRow })
1419
+ }
1420
+ ),
1421
+ /* @__PURE__ */ jsxRuntime.jsx(
1422
+ "div",
1423
+ {
1424
+ className: "grid gap-1",
1425
+ style: { gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` },
1426
+ children: block.rows.map(
1427
+ (row, r) => row.map((cell, c) => /* @__PURE__ */ jsxRuntime.jsx(
1428
+ "input",
1429
+ {
1430
+ "aria-label": f.cell.replace("{row}", String(r + 1)).replace("{column}", String(c + 1)),
1431
+ value: cell,
1432
+ onChange: (e) => setCell(r, c, e.target.value),
1433
+ className: "border-input bg-background text-foreground focus-visible:ring-ring h-8 min-w-0 rounded-md border px-2 text-sm focus-visible:ring-2 focus-visible:outline-none"
1434
+ },
1435
+ `${r}-${c}`
1436
+ ))
1437
+ )
1438
+ }
1439
+ ),
1440
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
1441
+ /* @__PURE__ */ jsxRuntime.jsx(
1442
+ Button2,
1443
+ {
1444
+ variant: "outline",
1445
+ size: "sm",
1446
+ onClick: () => set({ rows: [...block.rows, Array(cols).fill("")] }),
1447
+ children: f.addRow
1448
+ }
1449
+ ),
1450
+ /* @__PURE__ */ jsxRuntime.jsx(
1451
+ Button2,
1452
+ {
1453
+ variant: "outline",
1454
+ size: "sm",
1455
+ onClick: () => set({ rows: block.rows.map((r) => [...r, ""]) }),
1456
+ children: f.addColumn
1457
+ }
1458
+ ),
1459
+ /* @__PURE__ */ jsxRuntime.jsx(
1460
+ Button2,
1461
+ {
1462
+ variant: "ghost",
1463
+ size: "sm",
1464
+ disabled: block.rows.length <= 1,
1465
+ onClick: () => set({ rows: block.rows.slice(0, -1) }),
1466
+ children: f.removeRow
1467
+ }
1468
+ ),
1469
+ /* @__PURE__ */ jsxRuntime.jsx(
1470
+ Button2,
1471
+ {
1472
+ variant: "ghost",
1473
+ size: "sm",
1474
+ disabled: cols <= 1,
1475
+ onClick: () => set({ rows: block.rows.map((r) => r.slice(0, -1)) }),
1476
+ children: f.removeColumn
1477
+ }
1478
+ )
1479
+ ] }),
1480
+ alignment(block.alignment, "left"),
1481
+ /* @__PURE__ */ jsxRuntime.jsx(
1482
+ ColorField,
1483
+ {
1484
+ label: f.borderColor,
1485
+ value: block.borderColor,
1486
+ onChange: (borderColor) => set({ borderColor })
1487
+ }
1488
+ ),
1489
+ /* @__PURE__ */ jsxRuntime.jsx(
1490
+ ColorField,
1491
+ {
1492
+ label: f.headerBackgroundColor,
1493
+ value: block.headerBackgroundColor,
1494
+ onChange: (headerBackgroundColor) => set({ headerBackgroundColor })
1495
+ }
1496
+ ),
1497
+ /* @__PURE__ */ jsxRuntime.jsx(
1498
+ ColorField,
1499
+ {
1500
+ label: f.headerTextColor,
1501
+ value: block.headerTextColor,
1502
+ onChange: (headerTextColor) => set({ headerTextColor })
1503
+ }
1504
+ )
1505
+ ] });
1506
+ }
1507
+ case "hero":
1508
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1509
+ /* @__PURE__ */ jsxRuntime.jsx(
1510
+ TextField,
1511
+ {
1512
+ label: f.categoryText,
1513
+ value: block.categoryText,
1514
+ onChange: (categoryText) => set({ categoryText })
1515
+ }
1516
+ ),
1517
+ /* @__PURE__ */ jsxRuntime.jsx(
1518
+ TextField,
1519
+ {
1520
+ label: f.headline,
1521
+ value: block.headline,
1522
+ onChange: (headline) => set({ headline })
1523
+ }
1524
+ ),
1525
+ /* @__PURE__ */ jsxRuntime.jsx(
1526
+ TextField,
1527
+ {
1528
+ multiline: true,
1529
+ label: f.subtitle,
1530
+ value: block.subtitle,
1531
+ onChange: (subtitle) => set({ subtitle })
1532
+ }
1533
+ ),
1534
+ /* @__PURE__ */ jsxRuntime.jsx(
1535
+ TextField,
1536
+ {
1537
+ label: f.ctaText,
1538
+ value: block.ctaText,
1539
+ onChange: (ctaText) => set({ ctaText })
1540
+ }
1541
+ ),
1542
+ /* @__PURE__ */ jsxRuntime.jsx(
1543
+ TextField,
1544
+ {
1545
+ label: f.ctaUrl,
1546
+ value: block.ctaUrl,
1547
+ onChange: (ctaUrl) => set({ ctaUrl })
1548
+ }
1549
+ ),
1550
+ alignment(block.alignment, "center"),
1551
+ /* @__PURE__ */ jsxRuntime.jsx(
1552
+ ColorField,
1553
+ {
1554
+ label: f.backgroundColor,
1555
+ value: block.backgroundColor,
1556
+ onChange: (backgroundColor) => set({ backgroundColor })
1557
+ }
1558
+ ),
1559
+ /* @__PURE__ */ jsxRuntime.jsx(
1560
+ TextField,
1561
+ {
1562
+ label: f.backgroundGradient,
1563
+ value: block.backgroundGradient,
1564
+ placeholder: "linear-gradient(135deg, #1e3a5f, #2563eb)",
1565
+ onChange: (backgroundGradient) => set({ backgroundGradient })
1566
+ }
1567
+ ),
1568
+ /* @__PURE__ */ jsxRuntime.jsx(
1569
+ ColorField,
1570
+ {
1571
+ label: f.textColor,
1572
+ value: block.textColor,
1573
+ onChange: (textColor) => set({ textColor })
1574
+ }
1575
+ ),
1576
+ /* @__PURE__ */ jsxRuntime.jsx(
1577
+ ColorField,
1578
+ {
1579
+ label: f.ctaColor,
1580
+ value: block.ctaColor,
1581
+ onChange: (ctaColor) => set({ ctaColor })
1582
+ }
1583
+ ),
1584
+ /* @__PURE__ */ jsxRuntime.jsx(
1585
+ NumberField,
1586
+ {
1587
+ label: f.padding,
1588
+ value: block.padding,
1589
+ max: 200,
1590
+ onChange: (padding) => set({ padding })
1591
+ }
1592
+ )
1593
+ ] });
1594
+ case "columns":
1595
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1596
+ /* @__PURE__ */ jsxRuntime.jsx(
1597
+ SelectField,
1598
+ {
1599
+ label: f.columnCount,
1600
+ value: String(block.columnCount),
1601
+ options: ["1", "2", "3"].map((v) => ({
1602
+ value: v,
1603
+ label: v
1604
+ })),
1605
+ onChange: (v) => {
1606
+ const columnCount = Number(v);
1607
+ set({
1608
+ columnCount,
1609
+ columns: resizeColumns(block.columns, columnCount)
1610
+ });
1611
+ }
1612
+ }
1613
+ ),
1614
+ /* @__PURE__ */ jsxRuntime.jsx(
1615
+ ColorField,
1616
+ {
1617
+ label: f.backgroundColor,
1618
+ value: block.backgroundColor,
1619
+ onChange: (backgroundColor) => set({ backgroundColor })
1620
+ }
1621
+ ),
1622
+ block.columns.map((column, i) => /* @__PURE__ */ jsxRuntime.jsx(
1623
+ chunkUPWIJC6I_cjs.Select,
1624
+ {
1625
+ label: `${labels.addBlock} \u2014 ${labels.column} ${i + 1}`,
1626
+ placeholder: labels.addBlock,
1627
+ value: "",
1628
+ options: CONTENT_BLOCK_TYPES.map((type) => ({
1629
+ value: type,
1630
+ label: labels.blockTypes[type]
1631
+ })),
1632
+ onValueChange: (type) => onAddToColumn(
1633
+ i,
1634
+ createEmailBlock(
1635
+ type
1636
+ )
1637
+ )
1638
+ },
1639
+ column.id
1640
+ ))
1641
+ ] });
1642
+ }
1643
+ }
1644
+ var FONT_OPTIONS = [
1645
+ "Arial, Helvetica, sans-serif",
1646
+ "Helvetica, Arial, sans-serif",
1647
+ "Verdana, Geneva, sans-serif",
1648
+ "Tahoma, Geneva, sans-serif",
1649
+ "'Trebuchet MS', Helvetica, sans-serif",
1650
+ "Georgia, serif",
1651
+ "'Times New Roman', Times, serif",
1652
+ "'Courier New', Courier, monospace"
1653
+ ];
1654
+ function EmailDesignPanel({
1655
+ design,
1656
+ onChange,
1657
+ labels
1658
+ }) {
1659
+ const f = labels.fields;
1660
+ const fonts = FONT_OPTIONS.includes(design.fontFamily) ? FONT_OPTIONS : [design.fontFamily, ...FONT_OPTIONS];
1661
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1662
+ /* @__PURE__ */ jsxRuntime.jsx(
1663
+ SelectField,
1664
+ {
1665
+ label: f.fontFamily,
1666
+ value: design.fontFamily,
1667
+ options: fonts.map((font) => ({
1668
+ value: font,
1669
+ label: font.split(",")[0].replace(/'/g, "")
1670
+ })),
1671
+ onChange: (fontFamily) => onChange({ fontFamily })
1672
+ }
1673
+ ),
1674
+ /* @__PURE__ */ jsxRuntime.jsx(
1675
+ NumberField,
1676
+ {
1677
+ label: f.contentWidth,
1678
+ value: design.contentWidth,
1679
+ min: 320,
1680
+ max: 1200,
1681
+ onChange: (contentWidth) => onChange({ contentWidth })
1682
+ }
1683
+ ),
1684
+ [
1685
+ "bodyBackgroundColor",
1686
+ "contentBackgroundColor",
1687
+ "textColor",
1688
+ "headingColor",
1689
+ "linkColor",
1690
+ "buttonBackgroundColor",
1691
+ "buttonTextColor"
1692
+ ].map((key) => /* @__PURE__ */ jsxRuntime.jsx(
1693
+ ColorField,
1694
+ {
1695
+ label: f[key],
1696
+ value: design[key],
1697
+ onChange: (v) => onChange({ [key]: v })
1698
+ },
1699
+ key
1700
+ )),
1701
+ /* @__PURE__ */ jsxRuntime.jsx(
1702
+ NumberField,
1703
+ {
1704
+ label: f.buttonBorderRadius,
1705
+ value: design.buttonBorderRadius,
1706
+ max: 100,
1707
+ onChange: (buttonBorderRadius) => onChange({ buttonBorderRadius })
1708
+ }
1709
+ )
1710
+ ] });
1711
+ }
1712
+
1713
+ // src/components/EmailEditor/labels.ts
1714
+ var defaultEmailEditorLabels = {
1715
+ blocks: "Blocks",
1716
+ design: "Design",
1717
+ blockSettings: "Block settings",
1718
+ undo: "Undo",
1719
+ redo: "Redo",
1720
+ viewport: "Preview width",
1721
+ desktop: "Desktop",
1722
+ mobile: "Mobile",
1723
+ canvas: "Email canvas",
1724
+ moveUp: "Move up",
1725
+ moveDown: "Move down",
1726
+ duplicate: "Duplicate",
1727
+ remove: "Delete",
1728
+ dragHandle: "Drag to reorder",
1729
+ addBlock: "Add block",
1730
+ emptyCanvas: "Drag a block here, or click one in the palette.",
1731
+ emptyColumn: "Empty column",
1732
+ unsupportedBlock: "Unsupported block ({type}). It will not be included in the sent email.",
1733
+ noSelection: "Select a block in the canvas to edit it.",
1734
+ column: "Column",
1735
+ selectBlock: "Select {item}",
1736
+ announceAdded: "{item} added.",
1737
+ announceMoved: "{item} moved to position {position}.",
1738
+ announceDuplicated: "{item} duplicated.",
1739
+ announceRemoved: "{item} deleted.",
1740
+ blockTypes: {
1741
+ heading: "Heading",
1742
+ text: "Text",
1743
+ button: "Button",
1744
+ image: "Image",
1745
+ divider: "Divider",
1746
+ spacer: "Spacer",
1747
+ columns: "Columns",
1748
+ social: "Social links",
1749
+ hero: "Hero banner",
1750
+ html: "Custom HTML",
1751
+ footer: "Footer",
1752
+ quote: "Quote",
1753
+ table: "Table"
1754
+ },
1755
+ platforms: {
1756
+ facebook: "Facebook",
1757
+ twitter: "Twitter",
1758
+ linkedin: "LinkedIn",
1759
+ instagram: "Instagram",
1760
+ youtube: "YouTube",
1761
+ github: "GitHub",
1762
+ website: "Website"
1763
+ },
1764
+ fields: {
1765
+ text: "Text",
1766
+ content: "Content",
1767
+ level: "Level",
1768
+ alignment: "Alignment",
1769
+ color: "Color",
1770
+ textColor: "Text color",
1771
+ backgroundColor: "Background color",
1772
+ backgroundGradient: "Background gradient (CSS)",
1773
+ accentColor: "Accent color",
1774
+ url: "Link URL",
1775
+ borderRadius: "Corner radius (px)",
1776
+ fullWidth: "Full width",
1777
+ imageUrl: "Image URL",
1778
+ uploadImage: "Upload image",
1779
+ uploadFailed: "The image could not be uploaded. Try again.",
1780
+ altText: "Alt text",
1781
+ width: "Width (px or %)",
1782
+ style: "Style",
1783
+ height: "Height (px)",
1784
+ iconSize: "Icon size",
1785
+ platform: "Platform",
1786
+ socialLink: "Link {number}",
1787
+ addLink: "Add link",
1788
+ removeLink: "Remove link",
1789
+ html: "HTML",
1790
+ companyName: "Company name",
1791
+ address: "Address",
1792
+ phone: "Phone",
1793
+ showUnsubscribe: "Show unsubscribe link",
1794
+ unsubscribeText: "Unsubscribe text",
1795
+ managePreferencesUrl: "Preferences URL",
1796
+ managePreferencesText: "Preferences text",
1797
+ author: "Author",
1798
+ role: "Role",
1799
+ headerRow: "First row is a header",
1800
+ headerBackgroundColor: "Header background",
1801
+ headerTextColor: "Header text color",
1802
+ borderColor: "Border color",
1803
+ addRow: "Add row",
1804
+ addColumn: "Add column",
1805
+ removeRow: "Remove last row",
1806
+ removeColumn: "Remove last column",
1807
+ cell: "Row {row}, column {column}",
1808
+ categoryText: "Eyebrow",
1809
+ headline: "Headline",
1810
+ subtitle: "Subtitle",
1811
+ ctaText: "Button text",
1812
+ ctaUrl: "Button URL",
1813
+ ctaColor: "Button color",
1814
+ padding: "Vertical padding (px)",
1815
+ columnCount: "Columns",
1816
+ bodyBackgroundColor: "Page background",
1817
+ contentBackgroundColor: "Content background",
1818
+ contentWidth: "Content width (px)",
1819
+ fontFamily: "Font",
1820
+ headingColor: "Heading color",
1821
+ linkColor: "Link color",
1822
+ buttonBackgroundColor: "Button color",
1823
+ buttonTextColor: "Button text color",
1824
+ buttonBorderRadius: "Button corner radius (px)"
1825
+ },
1826
+ options: {
1827
+ left: "Left",
1828
+ center: "Center",
1829
+ right: "Right",
1830
+ solid: "Solid",
1831
+ dashed: "Dashed",
1832
+ dotted: "Dotted",
1833
+ sm: "Small",
1834
+ md: "Medium",
1835
+ lg: "Large"
1836
+ }
1837
+ };
1838
+ function mergeEmailEditorLabels(overrides) {
1839
+ if (!overrides) return defaultEmailEditorLabels;
1840
+ const d = defaultEmailEditorLabels;
1841
+ return {
1842
+ ...d,
1843
+ ...overrides,
1844
+ blockTypes: { ...d.blockTypes, ...overrides.blockTypes },
1845
+ platforms: { ...d.platforms, ...overrides.platforms },
1846
+ fields: { ...d.fields, ...overrides.fields },
1847
+ options: { ...d.options, ...overrides.options }
1848
+ };
1849
+ }
1850
+
1851
+ // src/components/EmailEditor/tree.ts
1852
+ function editContaining(blocks, id, edit) {
1853
+ const index = blocks.findIndex((b) => b.id === id);
1854
+ if (index !== -1) return edit(blocks, index);
1855
+ let changed = false;
1856
+ const next = blocks.map((block) => {
1857
+ if (block.type !== "columns") return block;
1858
+ let hit = false;
1859
+ const columns = block.columns.map((column) => {
1860
+ const inner = editContaining(column.blocks, id, edit);
1861
+ if (inner === column.blocks) return column;
1862
+ hit = changed = true;
1863
+ return { ...column, blocks: inner };
1864
+ });
1865
+ return hit ? { ...block, columns } : block;
1866
+ });
1867
+ return changed ? next : blocks;
1868
+ }
1869
+ function findEmailBlock(blocks, id) {
1870
+ if (!id) return void 0;
1871
+ for (const block of blocks) {
1872
+ if (block.id === id) return block;
1873
+ if (block.type === "columns") {
1874
+ for (const column of block.columns) {
1875
+ const found = findEmailBlock(column.blocks, id);
1876
+ if (found) return found;
1877
+ }
1878
+ }
1879
+ }
1880
+ return void 0;
1881
+ }
1882
+ function updateEmailBlock(blocks, id, patch) {
1883
+ return editContaining(
1884
+ blocks,
1885
+ id,
1886
+ (list, i) => list.map((b, j) => j === i ? { ...b, ...patch } : b)
1887
+ );
1888
+ }
1889
+ function removeEmailBlock(blocks, id) {
1890
+ return editContaining(
1891
+ blocks,
1892
+ id,
1893
+ (list, i) => list.filter((_, j) => j !== i)
1894
+ );
1895
+ }
1896
+ function moveEmailBlock(blocks, id, delta) {
1897
+ return editContaining(blocks, id, (list, i) => {
1898
+ const target = i + delta;
1899
+ if (target < 0 || target >= list.length) return list;
1900
+ const next = [...list];
1901
+ const [moved] = next.splice(i, 1);
1902
+ next.splice(target, 0, moved);
1903
+ return next;
1904
+ });
1905
+ }
1906
+ function cloneWithNewIds(block) {
1907
+ const copy = { ...block, id: generateEmailBlockId() };
1908
+ if (copy.type === "columns") {
1909
+ copy.columns = copy.columns.map((column) => ({
1910
+ ...column,
1911
+ id: generateEmailBlockId(),
1912
+ blocks: column.blocks.map(cloneWithNewIds)
1913
+ }));
1914
+ }
1915
+ return copy;
1916
+ }
1917
+ function duplicateEmailBlock(blocks, id) {
1918
+ let newId = null;
1919
+ const next = editContaining(blocks, id, (list, i) => {
1920
+ const copy = cloneWithNewIds(list[i]);
1921
+ newId = copy.id;
1922
+ return [...list.slice(0, i + 1), copy, ...list.slice(i + 1)];
1923
+ });
1924
+ return { blocks: next, newId };
1925
+ }
1926
+ function addBlockToColumn(blocks, columnsId, columnIndex, block) {
1927
+ return blocks.map(
1928
+ (b) => b.id === columnsId && b.type === "columns" ? {
1929
+ ...b,
1930
+ columns: b.columns.map(
1931
+ (column, i) => i === columnIndex ? { ...column, blocks: [...column.blocks, block] } : column
1932
+ )
1933
+ } : b
1934
+ );
1935
+ }
1936
+ var BLOCK_ICONS = {
1937
+ hero: lucideReact.Megaphone,
1938
+ columns: lucideReact.Columns2,
1939
+ heading: lucideReact.Heading,
1940
+ text: lucideReact.Type,
1941
+ button: lucideReact.MousePointerClick,
1942
+ image: lucideReact.Image,
1943
+ divider: lucideReact.Minus,
1944
+ spacer: lucideReact.MoveVertical,
1945
+ social: lucideReact.Share2,
1946
+ quote: lucideReact.Quote,
1947
+ table: lucideReact.Table,
1948
+ html: lucideReact.Code,
1949
+ footer: lucideReact.PanelBottom
1950
+ };
1951
+ var EMAIL_BLOCK_TYPES = Object.keys(BLOCK_ICONS);
1952
+ var PALETTE = "palette:";
1953
+ var CANVAS_END = "canvas:end";
1954
+ var MOBILE_WIDTH = 375;
1955
+ function fill(template, values) {
1956
+ return template.replace(
1957
+ /\{(\w+)\}/g,
1958
+ (_, key) => String(values[key] ?? "")
1959
+ );
1960
+ }
1961
+ function BlockFrame({
1962
+ block,
1963
+ selected,
1964
+ labels,
1965
+ onSelect,
1966
+ onMove,
1967
+ onDuplicate,
1968
+ onRemove,
1969
+ isFirst,
1970
+ isLast,
1971
+ dragHandle,
1972
+ children
1973
+ }) {
1974
+ const name = labels.blockTypes[block.type] ?? block.type;
1975
+ const tool = "text-muted-foreground hover:text-foreground h-7 w-7";
1976
+ const own = (action) => (e) => {
1977
+ e.stopPropagation();
1978
+ action();
1979
+ };
1980
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1981
+ "div",
1982
+ {
1983
+ "data-slot": "email-editor-block",
1984
+ "data-selected": selected || void 0,
1985
+ className: chunkS5RVSXDV_cjs.cn(
1986
+ "relative outline-offset-[-2px]",
1987
+ selected ? "outline-primary-500 outline outline-2" : "hover:outline-primary-300 focus-within:outline-primary-300 focus-within:outline-1 focus-within:outline-dashed hover:outline-1 hover:outline-dashed"
1988
+ ),
1989
+ children: [
1990
+ /* @__PURE__ */ jsxRuntime.jsxs(
1991
+ "div",
1992
+ {
1993
+ "data-slot": "email-editor-block-toolbar",
1994
+ className: chunkS5RVSXDV_cjs.cn(
1995
+ "bg-background border-border absolute end-2 -top-3.5 z-10 flex items-center rounded-md border shadow-sm",
1996
+ // Reveal on hover of this frame only, not of a frame nested inside it.
1997
+ !selected && "opacity-0 focus-within:opacity-100 [[data-slot=email-editor-block]:hover:not(:has([data-slot=email-editor-block]:hover))>&]:opacity-100"
1998
+ ),
1999
+ children: [
2000
+ dragHandle,
2001
+ /* @__PURE__ */ jsxRuntime.jsx(
2002
+ "button",
2003
+ {
2004
+ type: "button",
2005
+ "aria-pressed": selected,
2006
+ "aria-label": fill(labels.selectBlock, { item: name }),
2007
+ onClick: own(onSelect),
2008
+ className: "text-foreground focus-visible:ring-ring rounded px-2 text-xs font-medium focus-visible:ring-2 focus-visible:outline-none",
2009
+ children: name
2010
+ }
2011
+ ),
2012
+ /* @__PURE__ */ jsxRuntime.jsx(
2013
+ Button2,
2014
+ {
2015
+ variant: "ghost",
2016
+ size: "icon",
2017
+ className: tool,
2018
+ "aria-label": labels.moveUp,
2019
+ disabled: isFirst,
2020
+ onClick: own(() => onMove(-1)),
2021
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronUp, { className: "h-4 w-4", "aria-hidden": "true" })
2022
+ }
2023
+ ),
2024
+ /* @__PURE__ */ jsxRuntime.jsx(
2025
+ Button2,
2026
+ {
2027
+ variant: "ghost",
2028
+ size: "icon",
2029
+ className: tool,
2030
+ "aria-label": labels.moveDown,
2031
+ disabled: isLast,
2032
+ onClick: own(() => onMove(1)),
2033
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: "h-4 w-4", "aria-hidden": "true" })
2034
+ }
2035
+ ),
2036
+ /* @__PURE__ */ jsxRuntime.jsx(
2037
+ Button2,
2038
+ {
2039
+ variant: "ghost",
2040
+ size: "icon",
2041
+ className: tool,
2042
+ "aria-label": labels.duplicate,
2043
+ onClick: own(onDuplicate),
2044
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { className: "h-4 w-4", "aria-hidden": "true" })
2045
+ }
2046
+ ),
2047
+ /* @__PURE__ */ jsxRuntime.jsx(
2048
+ Button2,
2049
+ {
2050
+ variant: "ghost",
2051
+ size: "icon",
2052
+ className: chunkS5RVSXDV_cjs.cn(tool, "hover:text-destructive"),
2053
+ "aria-label": labels.remove,
2054
+ onClick: own(onRemove),
2055
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-4 w-4", "aria-hidden": "true" })
2056
+ }
2057
+ )
2058
+ ]
2059
+ }
2060
+ ),
2061
+ /* @__PURE__ */ jsxRuntime.jsx(
2062
+ "div",
2063
+ {
2064
+ onClick: (e) => {
2065
+ e.stopPropagation();
2066
+ if (e.target.closest("a")) e.preventDefault();
2067
+ onSelect();
2068
+ },
2069
+ className: "cursor-pointer [&_a]:pointer-events-none",
2070
+ children
2071
+ }
2072
+ )
2073
+ ]
2074
+ }
2075
+ );
2076
+ }
2077
+ function SortableBlock({
2078
+ id,
2079
+ children
2080
+ }) {
2081
+ const {
2082
+ setNodeRef,
2083
+ setActivatorNodeRef,
2084
+ listeners,
2085
+ transform,
2086
+ transition,
2087
+ isDragging
2088
+ } = sortable.useSortable({ id });
2089
+ const handle = /* @__PURE__ */ jsxRuntime.jsx(
2090
+ "span",
2091
+ {
2092
+ ref: setActivatorNodeRef,
2093
+ ...listeners,
2094
+ "aria-hidden": "true",
2095
+ className: "text-muted-foreground flex h-7 w-6 cursor-grab touch-none items-center justify-center",
2096
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GripVertical, { className: "h-4 w-4" })
2097
+ }
2098
+ );
2099
+ return /* @__PURE__ */ jsxRuntime.jsx(
2100
+ "div",
2101
+ {
2102
+ ref: setNodeRef,
2103
+ style: { transform: utilities.CSS.Transform.toString(transform), transition },
2104
+ className: chunkS5RVSXDV_cjs.cn(isDragging && "relative z-20 opacity-60"),
2105
+ children: children(handle)
2106
+ }
2107
+ );
2108
+ }
2109
+ function PaletteItem({
2110
+ type,
2111
+ label,
2112
+ onAdd
2113
+ }) {
2114
+ const { setNodeRef, listeners } = core.useDraggable({ id: PALETTE + type });
2115
+ const Icon = BLOCK_ICONS[type];
2116
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2117
+ "button",
2118
+ {
2119
+ ref: setNodeRef,
2120
+ type: "button",
2121
+ onPointerDown: listeners?.onPointerDown,
2122
+ onClick: onAdd,
2123
+ className: "border-border bg-background hover:bg-muted hover:border-primary-300 text-foreground focus-visible:ring-ring flex w-24 shrink-0 flex-col items-center gap-1 rounded-md border p-2 text-xs focus-visible:ring-2 focus-visible:outline-none lg:w-auto",
2124
+ children: [
2125
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "text-muted-foreground h-5 w-5", "aria-hidden": "true" }),
2126
+ label
2127
+ ]
2128
+ }
2129
+ );
2130
+ }
2131
+ function CanvasEnd({ empty, label }) {
2132
+ const { setNodeRef, isOver } = core.useDroppable({ id: CANVAS_END });
2133
+ return /* @__PURE__ */ jsxRuntime.jsx(
2134
+ "div",
2135
+ {
2136
+ ref: setNodeRef,
2137
+ className: chunkS5RVSXDV_cjs.cn(
2138
+ "text-muted-foreground flex items-center justify-center text-center text-sm",
2139
+ empty ? "border-border m-4 min-h-32 rounded-md border-2 border-dashed p-6" : "min-h-8",
2140
+ isOver && "bg-primary-500/10"
2141
+ ),
2142
+ children: empty && label
2143
+ }
2144
+ );
2145
+ }
2146
+ var EmailEditor = React3__namespace.forwardRef(
2147
+ ({
2148
+ value,
2149
+ onChange,
2150
+ design: designProp,
2151
+ onDesignChange,
2152
+ mergeTags,
2153
+ blockTypes = EMAIL_BLOCK_TYPES,
2154
+ onUploadImage,
2155
+ labels: labelOverrides,
2156
+ className
2157
+ }, ref) => {
2158
+ const labels = React3__namespace.useMemo(
2159
+ () => mergeEmailEditorLabels(labelOverrides),
2160
+ [labelOverrides]
2161
+ );
2162
+ const design = React3__namespace.useMemo(
2163
+ () => normalizeDesignSettings(designProp),
2164
+ [designProp]
2165
+ );
2166
+ const [selectedId, setSelectedId] = React3__namespace.useState(null);
2167
+ const [viewport, setViewport] = React3__namespace.useState(
2168
+ "desktop"
2169
+ );
2170
+ const [dragging, setDragging] = React3__namespace.useState(null);
2171
+ const [past, setPast] = React3__namespace.useState([]);
2172
+ const [future, setFuture] = React3__namespace.useState([]);
2173
+ const [announcement, announce] = chunkCPDNX3LS_cjs.useLiveAnnouncement();
2174
+ const variableGroups = React3__namespace.useMemo(() => {
2175
+ if (!mergeTags?.length) return void 0;
2176
+ const groups = /* @__PURE__ */ new Map();
2177
+ for (const tag of mergeTags) {
2178
+ const group = groups.get(tag.group) ?? {
2179
+ label: tag.group,
2180
+ variables: []
2181
+ };
2182
+ group.variables.push({ label: tag.label, value: tag.token });
2183
+ groups.set(tag.group, group);
2184
+ }
2185
+ return [...groups.values()];
2186
+ }, [mergeTags]);
2187
+ const latest = React3__namespace.useRef({ value, designProp, onChange });
2188
+ React3__namespace.useEffect(() => {
2189
+ latest.current = { value, designProp, onChange };
2190
+ });
2191
+ const present = (changed) => ({
2192
+ tree: value,
2193
+ design: designProp,
2194
+ changed
2195
+ });
2196
+ const commit = (tree, nextDesign) => {
2197
+ const current = latest.current;
2198
+ const changed = {
2199
+ tree: tree !== current.value,
2200
+ design: Boolean(nextDesign)
2201
+ };
2202
+ if (!changed.tree && !changed.design) return;
2203
+ setPast((p) => [
2204
+ ...p.slice(-49),
2205
+ { tree: current.value, design: current.designProp, changed }
2206
+ ]);
2207
+ setFuture([]);
2208
+ if (changed.tree) current.onChange(tree);
2209
+ if (nextDesign) onDesignChange?.(nextDesign);
2210
+ };
2211
+ const setBlocks = (blocks) => commit({ ...value, blocks });
2212
+ const patchBlock = (id, patch) => {
2213
+ const doc = latest.current.value;
2214
+ if (!findEmailBlock(doc.blocks, id)) return;
2215
+ commit({ ...doc, blocks: updateEmailBlock(doc.blocks, id, patch) });
2216
+ };
2217
+ const restore = (snapshot) => {
2218
+ if (snapshot.changed.tree) onChange(snapshot.tree);
2219
+ if (snapshot.changed.design) onDesignChange?.(snapshot.design ?? {});
2220
+ };
2221
+ const undo = () => {
2222
+ const previous = past[past.length - 1];
2223
+ if (!previous) return;
2224
+ setPast((p) => p.slice(0, -1));
2225
+ setFuture((f) => [present(previous.changed), ...f]);
2226
+ restore(previous);
2227
+ };
2228
+ const redo = () => {
2229
+ const next = future[0];
2230
+ if (!next) return;
2231
+ setFuture((f) => f.slice(1));
2232
+ setPast((p) => [...p, present(next.changed)]);
2233
+ restore(next);
2234
+ };
2235
+ const nameOf = (type) => labels.blockTypes[type] ?? type;
2236
+ const insertBlock = (type, index) => {
2237
+ const block = createEmailBlock(type);
2238
+ const blocks = [...value.blocks];
2239
+ const selectedIndex = blocks.findIndex(
2240
+ (b) => Boolean(findEmailBlock([b], selectedId))
2241
+ );
2242
+ const at = index ?? (selectedIndex === -1 ? blocks.length : selectedIndex + 1);
2243
+ blocks.splice(at, 0, block);
2244
+ setBlocks(blocks);
2245
+ setSelectedId(block.id);
2246
+ announce(fill(labels.announceAdded, { item: nameOf(type) }));
2247
+ };
2248
+ const selected = findEmailBlock(value.blocks, selectedId);
2249
+ const frameProps = (block, siblings, index) => ({
2250
+ block,
2251
+ labels,
2252
+ selected: block.id === selectedId,
2253
+ isFirst: index === 0,
2254
+ isLast: index === siblings.length - 1,
2255
+ onSelect: () => setSelectedId(block.id),
2256
+ onMove: (delta) => {
2257
+ setBlocks(moveEmailBlock(value.blocks, block.id, delta));
2258
+ announce(
2259
+ fill(labels.announceMoved, {
2260
+ item: nameOf(block.type),
2261
+ position: index + 1 + delta
2262
+ })
2263
+ );
2264
+ },
2265
+ onDuplicate: () => {
2266
+ const { blocks, newId } = duplicateEmailBlock(value.blocks, block.id);
2267
+ setBlocks(blocks);
2268
+ setSelectedId(newId);
2269
+ announce(fill(labels.announceDuplicated, { item: nameOf(block.type) }));
2270
+ },
2271
+ onRemove: () => {
2272
+ setBlocks(removeEmailBlock(value.blocks, block.id));
2273
+ if (findEmailBlock([block], selectedId)) setSelectedId(null);
2274
+ announce(fill(labels.announceRemoved, { item: nameOf(block.type) }));
2275
+ }
2276
+ });
2277
+ const renderPreview = (block) => /* @__PURE__ */ jsxRuntime.jsx(
2278
+ EmailBlockPreview,
2279
+ {
2280
+ block,
2281
+ design,
2282
+ emptyColumnLabel: labels.emptyColumn,
2283
+ unsupportedBlockLabel: labels.unsupportedBlock,
2284
+ platformLabels: labels.platforms,
2285
+ renderChild: (child) => {
2286
+ const column = block.type === "columns" ? block.columns.find(
2287
+ (c) => c.blocks.some((b) => b.id === child.id)
2288
+ ) : void 0;
2289
+ const siblings = column?.blocks ?? [];
2290
+ return /* @__PURE__ */ jsxRuntime.jsx(
2291
+ BlockFrame,
2292
+ {
2293
+ ...frameProps(child, siblings, siblings.indexOf(child)),
2294
+ children: renderPreview(child)
2295
+ }
2296
+ );
2297
+ }
2298
+ }
2299
+ );
2300
+ const sensors = core.useSensors(
2301
+ core.useSensor(core.PointerSensor, { activationConstraint: { distance: 5 } })
2302
+ );
2303
+ const onDragStart = (event) => setDragging(String(event.active.id));
2304
+ const onDragEnd = ({ active, over }) => {
2305
+ setDragging(null);
2306
+ if (!over) return;
2307
+ const activeId = String(active.id);
2308
+ const overId = String(over.id);
2309
+ const overIndex = value.blocks.findIndex((b) => b.id === overId);
2310
+ if (activeId.startsWith(PALETTE)) {
2311
+ insertBlock(
2312
+ activeId.slice(PALETTE.length),
2313
+ overIndex === -1 ? value.blocks.length : overIndex
2314
+ );
2315
+ return;
2316
+ }
2317
+ const from = value.blocks.findIndex((b) => b.id === activeId);
2318
+ const to = overId === CANVAS_END ? value.blocks.length - 1 : overIndex;
2319
+ if (from === -1 || to === -1 || from === to) return;
2320
+ setBlocks(sortable.arrayMove(value.blocks, from, to));
2321
+ announce(
2322
+ fill(labels.announceMoved, {
2323
+ item: nameOf(value.blocks[from].type),
2324
+ position: to + 1
2325
+ })
2326
+ );
2327
+ };
2328
+ const onKeyDown = (event) => {
2329
+ const target = event.target;
2330
+ if (target.closest('input, textarea, select, [contenteditable="true"]'))
2331
+ return;
2332
+ if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "z")
2333
+ return;
2334
+ event.preventDefault();
2335
+ if (event.shiftKey) redo();
2336
+ else undo();
2337
+ };
2338
+ const draggingType = dragging?.startsWith(PALETTE) ? dragging.slice(PALETTE.length) : value.blocks.find((b) => b.id === dragging)?.type ?? null;
2339
+ const panel = "border-border bg-background flex min-h-0 flex-col";
2340
+ const panelBody = "flex flex-col gap-3 overflow-y-auto p-4";
2341
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2342
+ core.DndContext,
2343
+ {
2344
+ sensors,
2345
+ collisionDetection: core.closestCenter,
2346
+ onDragStart,
2347
+ onDragEnd,
2348
+ onDragCancel: () => setDragging(null),
2349
+ children: [
2350
+ /* @__PURE__ */ jsxRuntime.jsxs(
2351
+ "div",
2352
+ {
2353
+ ref,
2354
+ "data-slot": "email-editor",
2355
+ onKeyDown,
2356
+ className: chunkS5RVSXDV_cjs.cn(
2357
+ "border-border bg-muted/30 text-foreground flex min-h-[640px] flex-col overflow-hidden rounded-lg border lg:flex-row",
2358
+ className
2359
+ ),
2360
+ children: [
2361
+ /* @__PURE__ */ jsxRuntime.jsx(
2362
+ "aside",
2363
+ {
2364
+ "data-slot": "email-editor-palette",
2365
+ className: chunkS5RVSXDV_cjs.cn(panel, "border-b lg:w-64 lg:border-e lg:border-b-0"),
2366
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
2367
+ chunkLC7PVBXH_cjs.Tabs,
2368
+ {
2369
+ defaultValue: "blocks",
2370
+ className: "flex min-h-0 flex-1 flex-col",
2371
+ children: [
2372
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkLC7PVBXH_cjs.TabsList, { className: "px-2", children: [
2373
+ /* @__PURE__ */ jsxRuntime.jsx(chunkLC7PVBXH_cjs.TabsTrigger, { value: "blocks", children: labels.blocks }),
2374
+ onDesignChange && /* @__PURE__ */ jsxRuntime.jsx(chunkLC7PVBXH_cjs.TabsTrigger, { value: "design", children: labels.design })
2375
+ ] }),
2376
+ /* @__PURE__ */ jsxRuntime.jsx(chunkLC7PVBXH_cjs.TabsContent, { value: "blocks", className: "overflow-y-auto p-3", children: /* @__PURE__ */ jsxRuntime.jsx(
2377
+ "div",
2378
+ {
2379
+ className: "flex gap-2 overflow-x-auto pb-1 lg:grid lg:grid-cols-2 lg:overflow-visible lg:pb-0",
2380
+ "aria-label": labels.addBlock,
2381
+ role: "group",
2382
+ children: blockTypes.map((type) => /* @__PURE__ */ jsxRuntime.jsx(
2383
+ PaletteItem,
2384
+ {
2385
+ type,
2386
+ label: nameOf(type),
2387
+ onAdd: () => insertBlock(type)
2388
+ },
2389
+ type
2390
+ ))
2391
+ }
2392
+ ) }),
2393
+ onDesignChange && /* @__PURE__ */ jsxRuntime.jsx(chunkLC7PVBXH_cjs.TabsContent, { value: "design", className: panelBody, children: /* @__PURE__ */ jsxRuntime.jsx(
2394
+ EmailDesignPanel,
2395
+ {
2396
+ design: resolveDesignSettings(designProp),
2397
+ labels,
2398
+ onChange: (patch) => commit(value, { ...designProp, ...patch })
2399
+ }
2400
+ ) })
2401
+ ]
2402
+ }
2403
+ )
2404
+ }
2405
+ ),
2406
+ /* @__PURE__ */ jsxRuntime.jsxs(
2407
+ "section",
2408
+ {
2409
+ "data-slot": "email-editor-canvas",
2410
+ "aria-label": labels.canvas,
2411
+ className: "flex min-w-0 flex-1 flex-col",
2412
+ children: [
2413
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-border bg-background flex items-center gap-1 border-b px-3 py-2", children: [
2414
+ /* @__PURE__ */ jsxRuntime.jsx(
2415
+ Button2,
2416
+ {
2417
+ variant: "ghost",
2418
+ size: "icon",
2419
+ "aria-label": labels.undo,
2420
+ title: labels.undo,
2421
+ disabled: !past.length,
2422
+ onClick: undo,
2423
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Undo2, { className: "h-4 w-4", "aria-hidden": "true" })
2424
+ }
2425
+ ),
2426
+ /* @__PURE__ */ jsxRuntime.jsx(
2427
+ Button2,
2428
+ {
2429
+ variant: "ghost",
2430
+ size: "icon",
2431
+ "aria-label": labels.redo,
2432
+ title: labels.redo,
2433
+ disabled: !future.length,
2434
+ onClick: redo,
2435
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Redo2, { className: "h-4 w-4", "aria-hidden": "true" })
2436
+ }
2437
+ ),
2438
+ /* @__PURE__ */ jsxRuntime.jsx(
2439
+ "div",
2440
+ {
2441
+ role: "group",
2442
+ "aria-label": labels.viewport,
2443
+ className: "ms-auto flex gap-1",
2444
+ children: [
2445
+ ["desktop", lucideReact.Monitor, labels.desktop],
2446
+ ["mobile", lucideReact.Smartphone, labels.mobile]
2447
+ ].map(([mode, Icon, label]) => /* @__PURE__ */ jsxRuntime.jsx(
2448
+ Button2,
2449
+ {
2450
+ variant: viewport === mode ? "secondary" : "ghost",
2451
+ size: "icon",
2452
+ "aria-label": label,
2453
+ title: label,
2454
+ "aria-pressed": viewport === mode,
2455
+ onClick: () => setViewport(mode),
2456
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-4 w-4", "aria-hidden": "true" })
2457
+ },
2458
+ mode
2459
+ ))
2460
+ }
2461
+ )
2462
+ ] }),
2463
+ /* @__PURE__ */ jsxRuntime.jsx(
2464
+ "div",
2465
+ {
2466
+ className: "flex-1 overflow-auto px-2 py-8 sm:px-6",
2467
+ style: { background: design.bodyBackgroundColor },
2468
+ onClick: (e) => e.target === e.currentTarget && setSelectedId(null),
2469
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
2470
+ "div",
2471
+ {
2472
+ dir: "ltr",
2473
+ className: "mx-auto shadow-sm transition-[width]",
2474
+ style: {
2475
+ width: viewport === "mobile" ? MOBILE_WIDTH : design.contentWidth,
2476
+ maxWidth: "100%",
2477
+ background: design.contentBackgroundColor,
2478
+ color: design.textColor,
2479
+ fontFamily: design.fontFamily,
2480
+ fontSize: 16,
2481
+ lineHeight: 1.6
2482
+ },
2483
+ children: [
2484
+ /* @__PURE__ */ jsxRuntime.jsx(
2485
+ sortable.SortableContext,
2486
+ {
2487
+ items: value.blocks.map((b) => b.id),
2488
+ strategy: sortable.verticalListSortingStrategy,
2489
+ children: value.blocks.map((block, index) => /* @__PURE__ */ jsxRuntime.jsx(SortableBlock, { id: block.id, children: (handle) => /* @__PURE__ */ jsxRuntime.jsx(
2490
+ BlockFrame,
2491
+ {
2492
+ ...frameProps(block, value.blocks, index),
2493
+ dragHandle: handle,
2494
+ children: renderPreview(block)
2495
+ }
2496
+ ) }, block.id))
2497
+ }
2498
+ ),
2499
+ /* @__PURE__ */ jsxRuntime.jsx(
2500
+ CanvasEnd,
2501
+ {
2502
+ empty: !value.blocks.length,
2503
+ label: labels.emptyCanvas
2504
+ }
2505
+ )
2506
+ ]
2507
+ }
2508
+ )
2509
+ }
2510
+ )
2511
+ ]
2512
+ }
2513
+ ),
2514
+ /* @__PURE__ */ jsxRuntime.jsxs(
2515
+ "aside",
2516
+ {
2517
+ "data-slot": "email-editor-settings",
2518
+ "aria-label": labels.blockSettings,
2519
+ className: chunkS5RVSXDV_cjs.cn(panel, "border-t lg:w-80 lg:border-s lg:border-t-0"),
2520
+ children: [
2521
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "border-border border-b px-4 py-3 text-sm font-semibold", children: selected ? nameOf(selected.type) : labels.blockSettings }),
2522
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: panelBody, children: selected ? /* @__PURE__ */ jsxRuntime.jsx(
2523
+ EmailBlockSettings,
2524
+ {
2525
+ block: selected,
2526
+ design,
2527
+ labels,
2528
+ variableGroups,
2529
+ onUploadImage,
2530
+ onChange: (patch) => patchBlock(selected.id, patch),
2531
+ onAddToColumn: (columnIndex, block) => {
2532
+ setBlocks(
2533
+ addBlockToColumn(
2534
+ value.blocks,
2535
+ selected.id,
2536
+ columnIndex,
2537
+ block
2538
+ )
2539
+ );
2540
+ setSelectedId(block.id);
2541
+ announce(
2542
+ fill(labels.announceAdded, { item: nameOf(block.type) })
2543
+ );
2544
+ }
2545
+ },
2546
+ selected.id
2547
+ ) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-sm", children: labels.noSelection }) })
2548
+ ]
2549
+ }
2550
+ ),
2551
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-live": "polite", className: "sr-only", children: announcement })
2552
+ ]
2553
+ }
2554
+ ),
2555
+ /* @__PURE__ */ jsxRuntime.jsx(core.DragOverlay, { dropAnimation: null, children: draggingType && dragging?.startsWith(PALETTE) ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-background border-primary-500 text-foreground rounded-md border px-3 py-2 text-xs font-medium shadow-lg", children: nameOf(draggingType) }) : null })
2556
+ ]
2557
+ }
2558
+ );
2559
+ }
2560
+ );
2561
+ EmailEditor.displayName = "EmailEditor";
2562
+
2563
+ exports.EMAIL_BLOCK_TYPES = EMAIL_BLOCK_TYPES;
2564
+ exports.EmailEditor = EmailEditor;
2565
+ exports.createDefaultDesignSettings = createDefaultDesignSettings;
2566
+ exports.createEmailBlock = createEmailBlock;
2567
+ exports.createEmailHtmlSanitizer = createEmailHtmlSanitizer;
2568
+ exports.createEmptyEmailContentTree = createEmptyEmailContentTree;
2569
+ exports.defaultEmailEditorLabels = defaultEmailEditorLabels;
2570
+ exports.generateEmailBlockId = generateEmailBlockId;
2571
+ exports.renderEmailMjml = renderEmailMjml;
2572
+ //# sourceMappingURL=chunk-W3HXHIDO.cjs.map
2573
+ //# sourceMappingURL=chunk-W3HXHIDO.cjs.map