@vitrina/email-builder 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1475 @@
1
+ 'use client';
2
+ import { useState, useRef, useMemo, useEffect } from 'react';
3
+ import { Undo2, Redo2, Eye, Monitor, Smartphone, X, ChevronLeft, GripVertical, Copy, Trash2, Heading, Type, MousePointerClick, Image, Minus, MoveVertical, Menu, Share2, Code2, Plus, ChevronUp, ChevronDown, Bold, Italic, Underline, Strikethrough, List, ListOrdered, Link2, Eraser, Upload } from 'lucide-react';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import hljs from 'highlight.js/lib/core';
6
+ import xml from 'highlight.js/lib/languages/xml';
7
+
8
+ // src/EmailBuilder.tsx
9
+
10
+ // src/types.ts
11
+ function uid() {
12
+ return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2);
13
+ }
14
+ var EMAIL_FONT_STACKS = [
15
+ { label: "Sistema", value: '-apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif' },
16
+ { label: "Arial", value: "Arial, Helvetica, sans-serif" },
17
+ { label: "Georgia", value: 'Georgia, "Times New Roman", serif' },
18
+ { label: "Verdana", value: "Verdana, Geneva, sans-serif" },
19
+ { label: "Trebuchet", value: '"Trebuchet MS", Tahoma, sans-serif' },
20
+ { label: "Courier", value: '"Courier New", Courier, monospace' }
21
+ ];
22
+ var ROW_LAYOUTS = [
23
+ { key: "1", weights: [1] },
24
+ { key: "1-1", weights: [1, 1] },
25
+ { key: "1-2", weights: [1, 2] },
26
+ { key: "2-1", weights: [2, 1] },
27
+ { key: "1-1-1", weights: [1, 1, 1] },
28
+ { key: "1-2-1", weights: [1, 2, 1] },
29
+ { key: "2-1-1", weights: [2, 1, 1] },
30
+ { key: "1-1-2", weights: [1, 1, 2] },
31
+ { key: "1-1-1-1", weights: [1, 1, 1, 1] }
32
+ ];
33
+ function defaultSettings() {
34
+ return {
35
+ backgroundColor: "#f4f4f5",
36
+ contentWidth: 600,
37
+ fontFamily: EMAIL_FONT_STACKS[0].value,
38
+ textColor: "#18181b",
39
+ linkColor: "#2563eb",
40
+ preheader: ""
41
+ };
42
+ }
43
+ function makeBlock(type) {
44
+ const base = { id: uid(), padding: 0 };
45
+ switch (type) {
46
+ case "text":
47
+ return { ...base, type, html: "Escribe tu mensaje aqu\xED\u2026", align: "left", fontSize: 15, color: "#3f3f46", lineHeight: 1.6 };
48
+ case "heading":
49
+ return { ...base, type, html: "Un t\xEDtulo que engancha", level: 2, align: "left", color: "#18181b" };
50
+ case "button":
51
+ return { ...base, type, label: "Ver m\xE1s", href: "https://", align: "left", background: "#2563eb", color: "#ffffff", radius: 8, fullWidth: false };
52
+ case "image":
53
+ return { ...base, type, src: "", alt: "", href: "", linkAction: "url", align: "center", width: 0 };
54
+ case "divider":
55
+ return { ...base, type, color: "#e4e4e7", thickness: 1 };
56
+ case "spacer":
57
+ return { ...base, type, height: 24 };
58
+ case "social":
59
+ return { ...base, type, align: "center", size: 32, spacing: 8, iconStyle: "circle", items: [{ network: "instagram", url: "https://" }, { network: "facebook", url: "https://" }, { network: "whatsapp", url: "https://" }] };
60
+ case "html":
61
+ return { ...base, type, html: "" };
62
+ case "menu":
63
+ return { ...base, type, layout: "horizontal", align: "center", fontSize: 14, color: "#3f3f46", gap: 20, items: [{ label: "Inicio", url: "https://" }, { label: "Productos", url: "https://" }, { label: "Contacto", url: "https://" }] };
64
+ default: {
65
+ const never = type;
66
+ throw new Error(`unknown block type ${String(never)}`);
67
+ }
68
+ }
69
+ }
70
+ function makeColumn(weight = 1, blocks = []) {
71
+ return { id: uid(), weight, background: "", paddingX: 0, paddingY: 0, blocks };
72
+ }
73
+ function makeRow(weights = [1]) {
74
+ const ws = weights.length ? weights : [1];
75
+ return { id: uid(), background: "#ffffff", contentBackground: "", backgroundImage: "", paddingY: 24, gap: 16, columns: ws.map((w) => makeColumn(w)) };
76
+ }
77
+ function defaultDesign() {
78
+ const row = makeRow([1]);
79
+ row.paddingY = 32;
80
+ row.columns[0].blocks = [makeBlock("heading"), makeBlock("text"), makeBlock("button")];
81
+ return { version: 1, settings: defaultSettings(), rows: [row] };
82
+ }
83
+ function blockCount(design) {
84
+ return design.rows.reduce((n, r) => n + r.columns.reduce((m, c) => m + c.blocks.length, 0), 0);
85
+ }
86
+ function relayoutRow(row, weights) {
87
+ const ws = weights.length ? weights : [1];
88
+ const cols = ws.map((w, i) => row.columns[i] ? { ...row.columns[i], weight: w } : makeColumn(w));
89
+ if (row.columns.length > ws.length) {
90
+ const overflow = row.columns.slice(ws.length).flatMap((c) => c.blocks);
91
+ const last = cols[cols.length - 1];
92
+ cols[cols.length - 1] = { ...last, blocks: [...last.blocks, ...overflow] };
93
+ }
94
+ return { ...row, columns: cols };
95
+ }
96
+
97
+ // src/compile.ts
98
+ function escapeHtml(s) {
99
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
100
+ }
101
+ function escapeAttr(s) {
102
+ return escapeHtml(s).replace(/"/g, "&quot;");
103
+ }
104
+ function styleToString(style) {
105
+ return Object.entries(style).map(([k, v]) => `${k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${v}`).join(";");
106
+ }
107
+ var HEADING_SIZE = { 1: "28px", 2: "22px", 3: "18px" };
108
+ function textStyle(b, s) {
109
+ return {
110
+ margin: "0",
111
+ fontFamily: s.fontFamily,
112
+ fontSize: `${b.fontSize}px`,
113
+ lineHeight: String(b.lineHeight),
114
+ color: b.color,
115
+ textAlign: b.align
116
+ };
117
+ }
118
+ function headingStyle(b, s) {
119
+ return {
120
+ margin: "0",
121
+ fontFamily: s.fontFamily,
122
+ fontSize: HEADING_SIZE[b.level],
123
+ lineHeight: "1.3",
124
+ fontWeight: "700",
125
+ color: b.color,
126
+ textAlign: b.align
127
+ };
128
+ }
129
+ var SOCIAL_NETWORKS = {
130
+ instagram: { label: "Instagram", bg: "#E4405F", mark: "IG" },
131
+ facebook: { label: "Facebook", bg: "#1877F2", mark: "f" },
132
+ whatsapp: { label: "WhatsApp", bg: "#25D366", mark: "WA" },
133
+ x: { label: "X", bg: "#000000", mark: "X" },
134
+ twitter: { label: "Twitter", bg: "#1DA1F2", mark: "t" },
135
+ linkedin: { label: "LinkedIn", bg: "#0A66C2", mark: "in" },
136
+ youtube: { label: "YouTube", bg: "#FF0000", mark: "YT" },
137
+ tiktok: { label: "TikTok", bg: "#111111", mark: "TT" },
138
+ pinterest: { label: "Pinterest", bg: "#E60023", mark: "P" },
139
+ snapchat: { label: "Snapchat", bg: "#FFFC00", mark: "SC" },
140
+ telegram: { label: "Telegram", bg: "#26A5E4", mark: "TG" },
141
+ messenger: { label: "Messenger", bg: "#0084FF", mark: "M" },
142
+ reddit: { label: "Reddit", bg: "#FF4500", mark: "R" },
143
+ spotify: { label: "Spotify", bg: "#1DB954", mark: "S" },
144
+ threads: { label: "Threads", bg: "#000000", mark: "@" },
145
+ github: { label: "GitHub", bg: "#181717", mark: "GH" },
146
+ vimeo: { label: "Vimeo", bg: "#1AB7EA", mark: "V" },
147
+ tumblr: { label: "Tumblr", bg: "#36465D", mark: "t" },
148
+ discord: { label: "Discord", bg: "#5865F2", mark: "D" },
149
+ email: { label: "Email", bg: "#52525b", mark: "@" },
150
+ website: { label: "Sitio web", bg: "#52525b", mark: "WWW" }
151
+ };
152
+ var SOCIAL_SLUGS = Object.keys(SOCIAL_NETWORKS);
153
+ function socialMeta(network) {
154
+ return SOCIAL_NETWORKS[network] ?? { label: network, bg: "#52525b", mark: (network[0] ?? "?").toUpperCase() };
155
+ }
156
+ var MOBILE_STACK_CSS = "<style>@media only screen and (max-width:600px){.eb-col{max-width:100%!important}}</style>";
157
+ var ALLOWED_INLINE = /* @__PURE__ */ new Set(["B", "STRONG", "I", "EM", "U", "S", "STRIKE", "A", "BR", "SPAN", "FONT", "UL", "OL", "LI", "DIV", "P"]);
158
+ var DROP_ENTIRELY = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "IFRAME", "OBJECT", "EMBED", "LINK", "META", "NOSCRIPT"]);
159
+ var ALLOWED_STYLE = /^(color|font-size|font-weight|font-style|text-decoration|text-align|background-color|line-height)$/;
160
+ function scrubStyle(el) {
161
+ const raw = el.getAttribute("style");
162
+ if (!raw) return;
163
+ const kept = raw.split(";").map((d) => d.trim()).filter(Boolean).filter((d) => ALLOWED_STYLE.test(d.split(":")[0]?.trim() ?? ""));
164
+ if (kept.length) el.setAttribute("style", kept.join(";"));
165
+ else el.removeAttribute("style");
166
+ }
167
+ function scrub(node) {
168
+ for (const child of Array.from(node.childNodes)) {
169
+ if (child.nodeType === 3) continue;
170
+ if (child.nodeType !== 1) {
171
+ child.parentNode?.removeChild(child);
172
+ continue;
173
+ }
174
+ const el = child;
175
+ if (DROP_ENTIRELY.has(el.tagName)) {
176
+ el.remove();
177
+ continue;
178
+ }
179
+ scrub(el);
180
+ if (!ALLOWED_INLINE.has(el.tagName)) {
181
+ el.replaceWith(...Array.from(el.childNodes));
182
+ continue;
183
+ }
184
+ for (const attr of Array.from(el.attributes)) {
185
+ if (el.tagName === "A" && attr.name === "href") continue;
186
+ if (attr.name === "style") continue;
187
+ if (el.tagName === "FONT" && (attr.name === "color" || attr.name === "size")) continue;
188
+ el.removeAttribute(attr.name);
189
+ }
190
+ scrubStyle(el);
191
+ if (el.tagName === "A") {
192
+ const href = el.getAttribute("href") ?? "";
193
+ if (!/^(https?:|mailto:)/i.test(href)) el.replaceWith(...Array.from(el.childNodes));
194
+ }
195
+ }
196
+ }
197
+ function sanitizeInline(html) {
198
+ if (typeof document === "undefined") return html.replace(/<[^>]+>/g, "");
199
+ const root = document.createElement("div");
200
+ root.innerHTML = html;
201
+ scrub(root);
202
+ return root.innerHTML.trim();
203
+ }
204
+ function sanitizeRawHtml(html) {
205
+ return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/\son\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, "");
206
+ }
207
+ var SAMPLE = { nombre: "Mar\xEDa", name: "Mar\xEDa", email: "maria@ejemplo.cl" };
208
+ function previewFill(html) {
209
+ return html.replace(/\{\{\s*([a-z_]+)(?:\|([^}]*))?\s*\}\}/gi, (_m, key, fb) => SAMPLE[key.toLowerCase()] ?? fb ?? "");
210
+ }
211
+ function iconRadius(style, size) {
212
+ if (style === "square") return "0";
213
+ if (style === "rounded") return `${Math.round(size * 0.25)}px`;
214
+ return "50%";
215
+ }
216
+ function renderSocial(block, opts) {
217
+ const s = block.size;
218
+ const gap = Math.max(0, Math.round((block.spacing ?? 8) / 2));
219
+ const radius = iconRadius(block.iconStyle, s);
220
+ const base = opts?.socialIconBase?.replace(/\/+$/, "");
221
+ const chips = block.items.map((it) => {
222
+ const m = socialMeta(it.network);
223
+ const inner = base ? `<img src="${escapeAttr(`${base}/${it.network}.png`)}" alt="${escapeAttr(m.label)}" width="${s}" height="${s}" style="display:inline-block;width:${s}px;height:${s}px;border:0;outline:none;border-radius:${radius};" />` : `<span style="display:inline-block;width:${s}px;height:${s}px;line-height:${s}px;background:${m.bg};color:#ffffff;border-radius:${radius};text-align:center;font-family:Arial,sans-serif;font-size:${Math.round(s * 0.4)}px;font-weight:700;">${escapeHtml(m.mark)}</span>`;
224
+ return `<a href="${escapeAttr(it.url || "#")}" target="_blank" style="display:inline-block;margin:0 ${gap}px;text-decoration:none;">${inner}</a>`;
225
+ }).join("");
226
+ return `<div style="text-align:${block.align};font-size:0;">${chips}</div>`;
227
+ }
228
+ function renderMenu(block, s) {
229
+ const sep = block.layout === "horizontal" ? `margin:0 ${Math.round(block.gap / 2)}px` : `display:block;margin:${Math.round(block.gap / 2)}px 0`;
230
+ const links = block.items.map((it) => `<a href="${escapeAttr(it.url || "#")}" target="_blank" style="${sep};font-family:${s.fontFamily};font-size:${block.fontSize}px;color:${block.color};text-decoration:none;">${escapeHtml(it.label)}</a>`).join("");
231
+ return `<div style="text-align:${block.align};">${links}</div>`;
232
+ }
233
+ function imageHref(block) {
234
+ const raw = block.href.trim();
235
+ if (!raw) return "";
236
+ if (block.linkAction === "email") return raw.startsWith("mailto:") ? raw : `mailto:${raw}`;
237
+ if (block.linkAction === "phone") return raw.startsWith("tel:") ? raw : `tel:${raw.replace(/[^\d+]/g, "")}`;
238
+ return raw;
239
+ }
240
+ function compileBlockInner(block, s, opts) {
241
+ switch (block.type) {
242
+ case "text":
243
+ return `<div style="${styleToString(textStyle(block, s))}">${block.html || "&nbsp;"}</div>`;
244
+ case "heading":
245
+ return `<div style="${styleToString(headingStyle(block, s))}">${block.html || "&nbsp;"}</div>`;
246
+ case "button": {
247
+ const a = `<a href="${escapeAttr(block.href || "#")}" target="_blank" style="display:${block.fullWidth ? "block" : "inline-block"};padding:12px 24px;font-family:${s.fontFamily};font-size:15px;font-weight:600;color:${block.color};text-decoration:none;border-radius:${block.radius}px;text-align:center;">${escapeHtml(block.label)}</a>`;
248
+ const tableStyle = block.fullWidth ? "width:100%;" : "display:inline-block;";
249
+ const table = `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="${tableStyle}border-collapse:separate;line-height:1;"><tr><td align="center" bgcolor="${block.background}" style="background:${block.background};border-radius:${block.radius}px;">${a}</td></tr></table>`;
250
+ return `<div style="text-align:${block.align};">${table}</div>`;
251
+ }
252
+ case "image": {
253
+ if (!block.src) return "";
254
+ const w = block.width > 0 ? `${block.width}px` : "100%";
255
+ const wAttr = block.width > 0 ? String(block.width) : "100%";
256
+ const img = `<img src="${escapeAttr(block.src)}" alt="${escapeAttr(block.alt)}" width="${wAttr}" style="display:inline-block;border:0;outline:none;text-decoration:none;width:${w};max-width:100%;height:auto;" />`;
257
+ const href = imageHref(block);
258
+ const linked = href ? `<a href="${escapeAttr(href)}" target="_blank">${img}</a>` : img;
259
+ return `<div style="text-align:${block.align};">${linked}</div>`;
260
+ }
261
+ case "divider":
262
+ return `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="border-top:${block.thickness}px solid ${block.color};font-size:0;line-height:0;">&nbsp;</td></tr></table>`;
263
+ case "spacer":
264
+ return `<div style="height:${block.height}px;line-height:${block.height}px;font-size:0;">&nbsp;</div>`;
265
+ case "social":
266
+ return renderSocial(block, opts);
267
+ case "menu":
268
+ return renderMenu(block, s);
269
+ case "html":
270
+ return sanitizeRawHtml(block.html);
271
+ default: {
272
+ const never = block;
273
+ throw new Error(`unknown block ${JSON.stringify(never)}`);
274
+ }
275
+ }
276
+ }
277
+ function bandWrap(row, content) {
278
+ const bandImg = row.backgroundImage ? `background-image:url('${escapeAttr(row.backgroundImage)}');background-size:cover;background-position:center;` : "";
279
+ return `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:${row.background};${bandImg}"><tr><td align="center" style="padding:${row.paddingY}px 12px;">${content}</td></tr></table>`;
280
+ }
281
+ function compileRow(row, s, opts) {
282
+ const cw = s.contentWidth;
283
+ const contentBg = row.contentBackground ? `background:${row.contentBackground};` : "";
284
+ const total = row.columns.reduce((n, c) => n + (c.weight || 1), 0) || 1;
285
+ const colStyle = (c) => `${c.background ? `background:${c.background};` : ""}padding:${c.paddingY}px ${c.paddingX}px;`;
286
+ if (row.columns.length === 1) {
287
+ const c = row.columns[0];
288
+ const content2 = `<table role="presentation" width="${cw}" cellpadding="0" cellspacing="0" border="0" align="center" style="width:100%;max-width:${cw}px;${contentBg}"><tr><td valign="top" style="${colStyle(c)}">${columnBody(c, row.gap, s, opts)}</td></tr></table>`;
289
+ return bandWrap(row, content2);
290
+ }
291
+ const cells = row.columns.map((c) => {
292
+ const px = Math.max(1, Math.round(cw * (c.weight || 1) / total));
293
+ const body = columnBody(c, row.gap, s, opts);
294
+ return `<!--[if mso]><td width="${px}" valign="top"><![endif]--><div class="eb-col" style="display:inline-block;width:100%;max-width:${px}px;vertical-align:top;box-sizing:border-box;font-size:14px;text-align:left;${colStyle(c)}">${body}</div><!--[if mso]></td><![endif]-->`;
295
+ }).join("");
296
+ const content = `<table role="presentation" width="${cw}" cellpadding="0" cellspacing="0" border="0" align="center" style="width:100%;max-width:${cw}px;${contentBg}"><tr><td align="center" style="font-size:0;"><!--[if mso]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><![endif]-->` + cells + "<!--[if mso]></tr></table><![endif]--></td></tr></table>";
297
+ return bandWrap(row, content);
298
+ }
299
+ function columnBody(col, gap, s, opts) {
300
+ const last = col.blocks.length - 1;
301
+ const cells = col.blocks.map((b, i) => {
302
+ const inner = compileBlockInner(b, s, opts);
303
+ if (!inner) return "";
304
+ const padded = b.padding ? `<div style="padding:${b.padding}px;">${inner}</div>` : inner;
305
+ return `<div style="${i < last ? `padding-bottom:${gap}px;` : ""}">${padded}</div>`;
306
+ }).join("");
307
+ return cells || "&nbsp;";
308
+ }
309
+ function preheaderSpan(text) {
310
+ if (!text.trim()) return "";
311
+ return `<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:#ffffff;opacity:0;">${escapeHtml(text)}</div>`;
312
+ }
313
+ function compileDesign(design, opts) {
314
+ const rows = design.rows.map((r) => compileRow(r, design.settings, opts)).join("");
315
+ return `${preheaderSpan(design.settings.preheader)}<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:${design.settings.backgroundColor};margin:0;"><tr><td align="center" style="padding:0;">${rows}</td></tr></table>`;
316
+ }
317
+
318
+ // src/dnd.ts
319
+ var MIME = "application/x-eb-block";
320
+ function setDrag(e, data) {
321
+ e.dataTransfer.setData(MIME, JSON.stringify(data));
322
+ e.dataTransfer.setData("text/plain", "");
323
+ e.dataTransfer.effectAllowed = "move";
324
+ }
325
+ function getDrag(e) {
326
+ try {
327
+ return JSON.parse(e.dataTransfer.getData(MIME));
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+ function EditableText({ id, html, style, onCommit, onFocus }) {
333
+ const ref = useRef(null);
334
+ useEffect(() => {
335
+ const el = ref.current;
336
+ if (el && document.activeElement !== el && el.innerHTML !== html) el.innerHTML = html;
337
+ }, [html, id]);
338
+ return /* @__PURE__ */ jsx(
339
+ "div",
340
+ {
341
+ ref,
342
+ contentEditable: true,
343
+ suppressContentEditableWarning: true,
344
+ spellCheck: false,
345
+ className: "outline-none focus:ring-2 focus:ring-primary/40 rounded-sm [&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5",
346
+ style,
347
+ onFocus,
348
+ onBlur: (e) => onCommit(sanitizeInline(e.currentTarget.innerHTML))
349
+ }
350
+ );
351
+ }
352
+ var SWATCHES = ["#18181b", "#71717a", "#ffffff", "#2563eb", "#dc2626", "#16a34a", "#d97706", "#7c3aed"];
353
+ function TextToolbar({ labels }) {
354
+ const [linkOpen, setLinkOpen] = useState(false);
355
+ const [linkUrl, setLinkUrl] = useState("");
356
+ const [sizeOpen, setSizeOpen] = useState(false);
357
+ const savedRange = useRef(null);
358
+ const exec = (cmd, value) => document.execCommand(cmd, false, value);
359
+ const hold = (fn) => (e) => {
360
+ e.preventDefault();
361
+ fn();
362
+ };
363
+ const btn = "rounded p-1.5 hover:bg-muted text-foreground";
364
+ const applyFontSize = (px) => {
365
+ const sel = window.getSelection();
366
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
367
+ const range = sel.getRangeAt(0);
368
+ const span = document.createElement("span");
369
+ span.style.fontSize = `${px}px`;
370
+ try {
371
+ range.surroundContents(span);
372
+ } catch {
373
+ span.appendChild(range.extractContents());
374
+ range.insertNode(span);
375
+ }
376
+ sel.removeAllRanges();
377
+ };
378
+ const saveRange = () => {
379
+ const sel = window.getSelection();
380
+ savedRange.current = sel && sel.rangeCount ? sel.getRangeAt(0).cloneRange() : null;
381
+ };
382
+ const applyLink = () => {
383
+ const sel = window.getSelection();
384
+ if (savedRange.current && sel) {
385
+ sel.removeAllRanges();
386
+ sel.addRange(savedRange.current);
387
+ }
388
+ if (linkUrl.trim()) exec("createLink", linkUrl.trim());
389
+ setLinkOpen(false);
390
+ setLinkUrl("");
391
+ };
392
+ return /* @__PURE__ */ jsxs("div", { className: "absolute -top-10 left-0 z-20 flex items-center gap-0.5 rounded-md border border-border bg-popover p-0.5 shadow-md", children: [
393
+ /* @__PURE__ */ jsx("button", { type: "button", title: labels.bold, className: btn, onMouseDown: hold(() => exec("bold")), children: /* @__PURE__ */ jsx(Bold, { size: 14 }) }),
394
+ /* @__PURE__ */ jsx("button", { type: "button", title: labels.italic, className: btn, onMouseDown: hold(() => exec("italic")), children: /* @__PURE__ */ jsx(Italic, { size: 14 }) }),
395
+ /* @__PURE__ */ jsx("button", { type: "button", title: labels.underline, className: btn, onMouseDown: hold(() => exec("underline")), children: /* @__PURE__ */ jsx(Underline, { size: 14 }) }),
396
+ /* @__PURE__ */ jsx("button", { type: "button", title: labels.strike, className: btn, onMouseDown: hold(() => exec("strikeThrough")), children: /* @__PURE__ */ jsx(Strikethrough, { size: 14 }) }),
397
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
398
+ /* @__PURE__ */ jsx("button", { type: "button", title: labels.fontSize, className: `${btn} text-xs font-semibold`, onMouseDown: hold(() => setSizeOpen((v) => !v)), children: "A\xB1" }),
399
+ sizeOpen && /* @__PURE__ */ jsx("div", { className: "absolute left-0 top-8 z-30 flex flex-col rounded-md border border-border bg-popover p-0.5 shadow-md", children: [12, 14, 16, 18, 20, 24, 28, 32].map((n) => /* @__PURE__ */ jsxs("button", { type: "button", className: "rounded px-3 py-1 text-left text-xs hover:bg-muted", onMouseDown: hold(() => {
400
+ applyFontSize(n);
401
+ setSizeOpen(false);
402
+ }), children: [
403
+ n,
404
+ "px"
405
+ ] }, n)) })
406
+ ] }),
407
+ /* @__PURE__ */ jsx("span", { className: "mx-0.5 h-4 w-px bg-border" }),
408
+ /* @__PURE__ */ jsx("button", { type: "button", title: labels.bulletList, className: btn, onMouseDown: hold(() => exec("insertUnorderedList")), children: /* @__PURE__ */ jsx(List, { size: 14 }) }),
409
+ /* @__PURE__ */ jsx("button", { type: "button", title: labels.numberList, className: btn, onMouseDown: hold(() => exec("insertOrderedList")), children: /* @__PURE__ */ jsx(ListOrdered, { size: 14 }) }),
410
+ /* @__PURE__ */ jsx("span", { className: "mx-0.5 h-4 w-px bg-border" }),
411
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
412
+ /* @__PURE__ */ jsx("button", { type: "button", title: labels.link, className: btn, onMouseDown: hold(() => {
413
+ saveRange();
414
+ setLinkOpen((v) => !v);
415
+ }), children: /* @__PURE__ */ jsx(Link2, { size: 14 }) }),
416
+ linkOpen && /* @__PURE__ */ jsxs("div", { className: "absolute left-0 top-8 z-30 flex gap-1 rounded-md border border-border bg-popover p-1 shadow-md", children: [
417
+ /* @__PURE__ */ jsx(
418
+ "input",
419
+ {
420
+ autoFocus: true,
421
+ value: linkUrl,
422
+ placeholder: labels.linkUrl,
423
+ className: "w-44 rounded border border-input bg-background px-2 py-1 text-xs outline-none",
424
+ onChange: (e) => setLinkUrl(e.target.value),
425
+ onKeyDown: (e) => {
426
+ if (e.key === "Enter") {
427
+ e.preventDefault();
428
+ applyLink();
429
+ }
430
+ }
431
+ }
432
+ ),
433
+ /* @__PURE__ */ jsx("button", { type: "button", className: "rounded bg-primary px-2 text-xs text-primary-foreground", onMouseDown: (e) => {
434
+ e.preventDefault();
435
+ applyLink();
436
+ }, children: "OK" })
437
+ ] })
438
+ ] }),
439
+ /* @__PURE__ */ jsx("span", { className: "mx-0.5 flex items-center gap-0.5", children: SWATCHES.map((c) => /* @__PURE__ */ jsx(
440
+ "button",
441
+ {
442
+ type: "button",
443
+ title: labels.textColor,
444
+ className: "h-4 w-4 rounded-full border border-border",
445
+ style: { background: c },
446
+ onMouseDown: hold(() => document.execCommand("foreColor", false, c))
447
+ },
448
+ c
449
+ )) }),
450
+ /* @__PURE__ */ jsx("span", { className: "mx-0.5 h-4 w-px bg-border" }),
451
+ /* @__PURE__ */ jsxs(
452
+ "select",
453
+ {
454
+ className: "rounded bg-transparent px-1 py-0.5 text-xs outline-none hover:bg-muted",
455
+ value: "",
456
+ onMouseDown: (e) => e.stopPropagation(),
457
+ onChange: (e) => {
458
+ const v = e.target.value;
459
+ if (v) exec("insertText", v);
460
+ e.currentTarget.value = "";
461
+ },
462
+ children: [
463
+ /* @__PURE__ */ jsx("option", { value: "", children: labels.insertField }),
464
+ /* @__PURE__ */ jsx("option", { value: "{{nombre|cliente}}", children: "nombre" }),
465
+ /* @__PURE__ */ jsx("option", { value: "{{email}}", children: "email" })
466
+ ]
467
+ }
468
+ ),
469
+ /* @__PURE__ */ jsx("button", { type: "button", title: labels.clearFormat, className: btn, onMouseDown: hold(() => exec("removeFormat")), children: /* @__PURE__ */ jsx(Eraser, { size: 14 }) })
470
+ ] });
471
+ }
472
+ function ImageDrop({ labels, uploadImage, onDone }) {
473
+ const ref = useRef(null);
474
+ const [busy, setBusy] = useState(false);
475
+ const [over, setOver] = useState(false);
476
+ const take = async (f) => {
477
+ if (!f || !uploadImage) return;
478
+ setBusy(true);
479
+ try {
480
+ onDone(await uploadImage(f));
481
+ } finally {
482
+ setBusy(false);
483
+ }
484
+ };
485
+ return /* @__PURE__ */ jsxs(
486
+ "div",
487
+ {
488
+ className: `flex h-28 cursor-pointer flex-col items-center justify-center gap-1 rounded border border-dashed text-muted-foreground ${over ? "border-primary bg-primary/5" : "border-border"}`,
489
+ onClick: (e) => {
490
+ e.stopPropagation();
491
+ if (uploadImage) ref.current?.click();
492
+ },
493
+ onDragOver: (e) => {
494
+ if (uploadImage) {
495
+ e.preventDefault();
496
+ e.stopPropagation();
497
+ setOver(true);
498
+ }
499
+ },
500
+ onDragLeave: () => setOver(false),
501
+ onDrop: (e) => {
502
+ e.preventDefault();
503
+ e.stopPropagation();
504
+ setOver(false);
505
+ void take(e.dataTransfer.files?.[0]);
506
+ },
507
+ children: [
508
+ uploadImage ? /* @__PURE__ */ jsx(Upload, { size: 20 }) : /* @__PURE__ */ jsx(Image, { size: 20 }),
509
+ /* @__PURE__ */ jsx("span", { className: "px-2 text-center text-xs", children: busy ? labels.uploading : uploadImage ? labels.dropImage : labels.blockNames.image }),
510
+ /* @__PURE__ */ jsx("input", { ref, type: "file", accept: "image/*", className: "hidden", onChange: (e) => {
511
+ void take(e.target.files?.[0]);
512
+ if (ref.current) ref.current.value = "";
513
+ } })
514
+ ]
515
+ }
516
+ );
517
+ }
518
+ function SelectionTag({ text }) {
519
+ return /* @__PURE__ */ jsx("span", { className: "absolute -top-[7px] left-2 z-10 rounded bg-primary px-1.5 py-px text-[10px] font-medium leading-tight text-primary-foreground", children: text });
520
+ }
521
+ function SocialChip({ network, size, base }) {
522
+ const [failed, setFailed] = useState(false);
523
+ const m = SOCIAL_NETWORKS[network] ?? { label: network, bg: "#52525b", mark: (network[0] ?? "?").toUpperCase() };
524
+ if (base && !failed) {
525
+ return /* @__PURE__ */ jsx("img", { src: `${base.replace(/\/+$/, "")}/${network}.png`, alt: m.label, width: size, height: size, style: { display: "inline-block" }, onError: () => setFailed(true) });
526
+ }
527
+ return /* @__PURE__ */ jsx("span", { style: { display: "inline-block", width: size, height: size, lineHeight: `${size}px`, background: m.bg, color: "#fff", borderRadius: "50%", textAlign: "center", fontFamily: "Arial, sans-serif", fontSize: Math.round(size * 0.4), fontWeight: 700 }, children: m.mark });
528
+ }
529
+ function SocialPreview({ block, base }) {
530
+ const gap = Math.max(0, Math.round((block.spacing ?? 8) / 2));
531
+ return /* @__PURE__ */ jsx("div", { style: { textAlign: block.align, fontSize: 0 }, children: block.items.map((it, i) => /* @__PURE__ */ jsx("span", { style: { display: "inline-block", margin: `0 ${gap}px` }, children: /* @__PURE__ */ jsx(SocialChip, { network: it.network, size: block.size, base }) }, i)) });
532
+ }
533
+ function BlockPlaceholder({ label }) {
534
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-24 flex-col items-center justify-center gap-1 rounded border border-dashed border-border text-muted-foreground", children: [
535
+ /* @__PURE__ */ jsx(Code2, { size: 20 }),
536
+ /* @__PURE__ */ jsx("span", { className: "text-xs", children: label })
537
+ ] });
538
+ }
539
+ function BlockView({ block, rowId, props }) {
540
+ const selected = props.selectedBlockId === block.id;
541
+ const editable = block.type === "text" || block.type === "heading";
542
+ const style = block.type === "text" ? textStyle(block, props.design.settings) : block.type === "heading" ? headingStyle(block, props.design.settings) : {};
543
+ return /* @__PURE__ */ jsxs(
544
+ "div",
545
+ {
546
+ className: `group relative ${selected ? "ring-2 ring-primary" : "ring-1 ring-transparent hover:ring-primary/30"} rounded-sm`,
547
+ onClick: (e) => {
548
+ e.stopPropagation();
549
+ props.onSelectBlock(block.id, rowId);
550
+ },
551
+ children: [
552
+ selected && /* @__PURE__ */ jsx(SelectionTag, { text: props.labels.blockNames[block.type] }),
553
+ selected && editable && /* @__PURE__ */ jsx(TextToolbar, { labels: props.labels }),
554
+ /* @__PURE__ */ jsx(
555
+ "div",
556
+ {
557
+ draggable: true,
558
+ onDragStart: (e) => {
559
+ setDrag(e, { kind: "move-block", blockId: block.id });
560
+ props.onDragStateChange(true);
561
+ },
562
+ onDragEnd: () => props.onDragStateChange(false),
563
+ className: `absolute -left-6 top-0 hidden cursor-grab items-center text-muted-foreground group-hover:flex ${selected ? "flex" : ""}`,
564
+ title: "Drag",
565
+ children: /* @__PURE__ */ jsx(GripVertical, { size: 15 })
566
+ }
567
+ ),
568
+ /* @__PURE__ */ jsxs("div", { className: `absolute -top-3 right-1 z-10 hidden items-center gap-0.5 rounded-md border border-border bg-popover p-0.5 shadow-sm group-hover:flex ${selected ? "flex" : ""}`, children: [
569
+ /* @__PURE__ */ jsx("button", { type: "button", title: props.labels.moveUp, className: "rounded p-1 hover:bg-muted", onClick: (e) => {
570
+ e.stopPropagation();
571
+ props.onBlockAction(block.id, "up");
572
+ }, children: /* @__PURE__ */ jsx(ChevronUp, { size: 13 }) }),
573
+ /* @__PURE__ */ jsx("button", { type: "button", title: props.labels.moveDown, className: "rounded p-1 hover:bg-muted", onClick: (e) => {
574
+ e.stopPropagation();
575
+ props.onBlockAction(block.id, "down");
576
+ }, children: /* @__PURE__ */ jsx(ChevronDown, { size: 13 }) }),
577
+ /* @__PURE__ */ jsx("button", { type: "button", title: props.labels.duplicate, className: "rounded p-1 hover:bg-muted", onClick: (e) => {
578
+ e.stopPropagation();
579
+ props.onBlockAction(block.id, "duplicate");
580
+ }, children: /* @__PURE__ */ jsx(Copy, { size: 13 }) }),
581
+ /* @__PURE__ */ jsx("button", { type: "button", title: props.labels.remove, className: "rounded p-1 text-destructive hover:bg-muted", onClick: (e) => {
582
+ e.stopPropagation();
583
+ props.onBlockAction(block.id, "remove");
584
+ }, children: /* @__PURE__ */ jsx(Trash2, { size: 13 }) })
585
+ ] }),
586
+ /* @__PURE__ */ jsx("div", { style: { padding: block.padding || void 0 }, children: editable ? /* @__PURE__ */ jsx(
587
+ EditableText,
588
+ {
589
+ id: block.id,
590
+ html: block.html,
591
+ style,
592
+ onFocus: () => props.onSelectBlock(block.id, rowId),
593
+ onCommit: (html) => props.onBlockChange({ ...block, html })
594
+ }
595
+ ) : block.type === "image" && !block.src ? /* @__PURE__ */ jsx(ImageDrop, { labels: props.labels, uploadImage: props.uploadImage, onDone: (src) => props.onBlockChange({ ...block, src }) }) : block.type === "social" ? /* @__PURE__ */ jsx(SocialPreview, { block, base: props.socialIconBase }) : block.type === "html" && !block.html.trim() ? /* @__PURE__ */ jsx(BlockPlaceholder, { label: props.labels.blockNames.html }) : /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: previewFill(compileBlockInner(block, props.design.settings, { socialIconBase: props.socialIconBase })) } }) })
596
+ ]
597
+ }
598
+ );
599
+ }
600
+ function DropZone({ colId, index, props }) {
601
+ const ref = useRef(null);
602
+ const setActive = (on) => {
603
+ if (ref.current) ref.current.dataset.active = on ? "1" : "";
604
+ };
605
+ return /* @__PURE__ */ jsx(
606
+ "div",
607
+ {
608
+ ref,
609
+ onDragOver: (e) => {
610
+ e.preventDefault();
611
+ e.dataTransfer.dropEffect = "move";
612
+ setActive(true);
613
+ },
614
+ onDragLeave: () => setActive(false),
615
+ onDrop: (e) => {
616
+ e.preventDefault();
617
+ e.stopPropagation();
618
+ setActive(false);
619
+ props.onDragStateChange(false);
620
+ const data = getDrag(e);
621
+ if (data && (data.kind === "new-block" || data.kind === "move-block")) props.onDropInColumn(colId, index, data);
622
+ },
623
+ className: `${props.dragging ? "h-6" : "h-2"} -my-1 rounded transition-[height] data-[active=1]:bg-primary/15 data-[active=1]:outline data-[active=1]:outline-2 data-[active=1]:outline-primary/50`
624
+ }
625
+ );
626
+ }
627
+ function ColumnView({ col, rowId, gap, props }) {
628
+ const selected = props.selectedColumnId === col.id && !props.selectedBlockId;
629
+ return /* @__PURE__ */ jsxs(
630
+ "div",
631
+ {
632
+ className: `relative min-w-0 ${selected ? "outline outline-2 -outline-offset-2 outline-primary" : "hover:outline hover:outline-1 hover:-outline-offset-1 hover:outline-primary/30"}`,
633
+ style: { background: col.background || void 0, padding: `${col.paddingY}px ${col.paddingX}px` },
634
+ onClick: (e) => {
635
+ e.stopPropagation();
636
+ props.onSelectColumn(col.id, rowId);
637
+ },
638
+ children: [
639
+ selected && /* @__PURE__ */ jsx(SelectionTag, { text: props.labels.columnSettings }),
640
+ /* @__PURE__ */ jsx(DropZone, { colId: col.id, index: 0, props }),
641
+ col.blocks.length === 0 && /* @__PURE__ */ jsx("div", { className: "py-5 text-center text-xs text-muted-foreground", children: props.labels.empty }),
642
+ col.blocks.map((b, i) => /* @__PURE__ */ jsxs("div", { style: { marginBottom: i < col.blocks.length - 1 ? gap : 0 }, children: [
643
+ /* @__PURE__ */ jsx(BlockView, { block: b, rowId, props }),
644
+ /* @__PURE__ */ jsx(DropZone, { colId: col.id, index: i + 1, props })
645
+ ] }, b.id))
646
+ ]
647
+ }
648
+ );
649
+ }
650
+ function RowView({ row, props }) {
651
+ const selected = props.selectedRowId === row.id && !props.selectedBlockId && !props.selectedColumnId;
652
+ const cols = row.columns.map((c) => `${c.weight || 1}fr`).join(" ");
653
+ return /* @__PURE__ */ jsxs(
654
+ "div",
655
+ {
656
+ className: `group/row relative ${selected ? "outline outline-2 -outline-offset-2 outline-primary" : ""}`,
657
+ style: {
658
+ background: row.background,
659
+ padding: `${row.paddingY}px 12px`,
660
+ ...row.backgroundImage ? { backgroundImage: `url('${row.backgroundImage}')`, backgroundSize: "cover", backgroundPosition: "center" } : {}
661
+ },
662
+ onClick: (e) => {
663
+ e.stopPropagation();
664
+ props.onSelectRow(row.id);
665
+ },
666
+ children: [
667
+ selected && /* @__PURE__ */ jsx(SelectionTag, { text: props.labels.rowSettings }),
668
+ /* @__PURE__ */ jsxs("div", { className: `absolute right-1 top-1 z-10 hidden items-center gap-0.5 rounded-md border border-border bg-popover p-0.5 shadow-sm group-hover/row:flex ${selected ? "flex" : ""}`, children: [
669
+ /* @__PURE__ */ jsx(
670
+ "div",
671
+ {
672
+ draggable: true,
673
+ onDragStart: (e) => {
674
+ setDrag(e, { kind: "move-row", rowId: row.id });
675
+ props.onDragStateChange(true);
676
+ },
677
+ onDragEnd: () => props.onDragStateChange(false),
678
+ className: "cursor-grab rounded p-1 hover:bg-muted",
679
+ title: props.labels.moveUp,
680
+ children: /* @__PURE__ */ jsx(GripVertical, { size: 13 })
681
+ }
682
+ ),
683
+ /* @__PURE__ */ jsx("button", { type: "button", title: props.labels.duplicate, className: "rounded p-1 hover:bg-muted", onClick: (e) => {
684
+ e.stopPropagation();
685
+ props.onRowAction(row.id, "duplicate");
686
+ }, children: /* @__PURE__ */ jsx(Copy, { size: 13 }) }),
687
+ /* @__PURE__ */ jsx("button", { type: "button", title: props.labels.remove, className: "rounded p-1 text-destructive hover:bg-muted", onClick: (e) => {
688
+ e.stopPropagation();
689
+ props.onRowAction(row.id, "remove");
690
+ }, children: /* @__PURE__ */ jsx(Trash2, { size: 13 }) })
691
+ ] }),
692
+ /* @__PURE__ */ jsx("div", { style: { maxWidth: props.design.settings.contentWidth, margin: "0 auto", background: row.contentBackground || void 0 }, children: /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: cols, gap: 0 }, children: row.columns.map((c) => /* @__PURE__ */ jsx(ColumnView, { col: c, rowId: row.id, gap: row.gap, props }, c.id)) }) })
693
+ ]
694
+ }
695
+ );
696
+ }
697
+ function RowDrop({ index, props }) {
698
+ const ref = useRef(null);
699
+ const setActive = (on) => {
700
+ if (ref.current) ref.current.dataset.active = on ? "1" : "";
701
+ };
702
+ return /* @__PURE__ */ jsx(
703
+ "div",
704
+ {
705
+ ref,
706
+ onDragOver: (e) => {
707
+ e.preventDefault();
708
+ e.dataTransfer.dropEffect = "move";
709
+ setActive(true);
710
+ },
711
+ onDragLeave: () => setActive(false),
712
+ onDrop: (e) => {
713
+ e.preventDefault();
714
+ setActive(false);
715
+ props.onDragStateChange(false);
716
+ const data = getDrag(e);
717
+ if (data && (data.kind === "new-row" || data.kind === "move-row")) props.onDropRow(index, data);
718
+ },
719
+ className: `${props.dragging ? "h-7" : "h-2"} mx-auto rounded transition-[height] data-[active=1]:bg-primary/20 data-[active=1]:outline data-[active=1]:outline-2 data-[active=1]:outline-primary/60`,
720
+ style: { maxWidth: props.design.settings.contentWidth }
721
+ }
722
+ );
723
+ }
724
+ function Canvas(props) {
725
+ return /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-8", style: { background: "#e9e9ec" }, onClick: () => props.onSelectNone(), children: /* @__PURE__ */ jsxs("div", { className: "mx-auto shadow-sm", style: { maxWidth: props.design.settings.contentWidth + 48, background: props.design.settings.backgroundColor }, children: [
726
+ /* @__PURE__ */ jsx(RowDrop, { index: 0, props }),
727
+ props.design.rows.map((r, i) => /* @__PURE__ */ jsxs("div", { children: [
728
+ /* @__PURE__ */ jsx(RowView, { row: r, props }),
729
+ /* @__PURE__ */ jsx(RowDrop, { index: i + 1, props })
730
+ ] }, r.id)),
731
+ props.design.rows.length === 0 && /* @__PURE__ */ jsx("div", { className: "py-16 text-center text-sm text-muted-foreground", children: props.labels.dropRowHere })
732
+ ] }) });
733
+ }
734
+ if (!hljs.getLanguage("xml")) hljs.registerLanguage("xml", xml);
735
+ function escapeHtml2(s) {
736
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
737
+ }
738
+ function CodeArea({ value, onChange, placeholder, height = 200 }) {
739
+ const taRef = useRef(null);
740
+ const gutRef = useRef(null);
741
+ const preRef = useRef(null);
742
+ const lineCount = Math.max(1, value.split("\n").length);
743
+ const highlighted = useMemo(() => {
744
+ try {
745
+ return hljs.highlight(value || "", { language: "xml" }).value;
746
+ } catch {
747
+ return escapeHtml2(value);
748
+ }
749
+ }, [value]);
750
+ const onScroll = (e) => {
751
+ const { scrollTop, scrollLeft } = e.currentTarget;
752
+ if (gutRef.current) gutRef.current.scrollTop = scrollTop;
753
+ if (preRef.current) {
754
+ preRef.current.scrollTop = scrollTop;
755
+ preRef.current.scrollLeft = scrollLeft;
756
+ }
757
+ };
758
+ const onKeyDown = (e) => {
759
+ if (e.key !== "Tab") return;
760
+ e.preventDefault();
761
+ const ta = e.currentTarget;
762
+ const start = ta.selectionStart;
763
+ onChange(`${value.slice(0, start)} ${value.slice(ta.selectionEnd)}`);
764
+ requestAnimationFrame(() => {
765
+ ta.selectionStart = ta.selectionEnd = start + 2;
766
+ });
767
+ };
768
+ const boxStyle = { fontSize: 12, lineHeight: "18px", fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" };
769
+ return /* @__PURE__ */ jsxs("div", { className: "flex overflow-hidden rounded-md border border-input bg-background", style: { height, ...boxStyle }, children: [
770
+ /* @__PURE__ */ jsx("div", { ref: gutRef, className: "select-none overflow-hidden border-r border-border bg-muted/40 py-2 pl-2 pr-1.5 text-right text-muted-foreground", style: { minWidth: 34 }, children: Array.from({ length: lineCount }, (_, i) => /* @__PURE__ */ jsx("div", { children: i + 1 }, i)) }),
771
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1 overflow-hidden", children: [
772
+ /* @__PURE__ */ jsx("pre", { ref: preRef, "aria-hidden": true, className: "pointer-events-none absolute inset-0 m-0 overflow-hidden whitespace-pre px-2 py-2", style: boxStyle, children: /* @__PURE__ */ jsx("code", { className: "hljs language-xml", style: { background: "transparent", padding: 0 }, dangerouslySetInnerHTML: { __html: `${highlighted}
773
+ ` } }) }),
774
+ /* @__PURE__ */ jsx(
775
+ "textarea",
776
+ {
777
+ ref: taRef,
778
+ value,
779
+ placeholder,
780
+ spellCheck: false,
781
+ wrap: "off",
782
+ onChange: (e) => onChange(e.target.value),
783
+ onScroll,
784
+ onKeyDown,
785
+ className: "absolute inset-0 resize-none whitespace-pre bg-transparent px-2 py-2 text-transparent caret-foreground outline-none",
786
+ style: boxStyle
787
+ }
788
+ )
789
+ ] })
790
+ ] });
791
+ }
792
+ var field = "mb-3";
793
+ var lbl = "mb-1 block text-xs font-medium text-muted-foreground";
794
+ var input = "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm outline-none focus:ring-2 focus:ring-ring";
795
+ function Text({ label, value, onChange, placeholder }) {
796
+ return /* @__PURE__ */ jsxs("div", { className: field, children: [
797
+ /* @__PURE__ */ jsx("label", { className: lbl, children: label }),
798
+ /* @__PURE__ */ jsx("input", { className: input, value, placeholder, onChange: (e) => onChange(e.target.value) })
799
+ ] });
800
+ }
801
+ function Num({ label, value, onChange, min = 0, max = 999 }) {
802
+ return /* @__PURE__ */ jsxs("div", { className: field, children: [
803
+ /* @__PURE__ */ jsx("label", { className: lbl, children: label }),
804
+ /* @__PURE__ */ jsx("input", { type: "number", className: input, value, min, max, onChange: (e) => onChange(Math.max(min, Math.min(max, Number(e.target.value) || 0))) })
805
+ ] });
806
+ }
807
+ function Color({ label, value, onChange }) {
808
+ return /* @__PURE__ */ jsxs("div", { className: field, children: [
809
+ /* @__PURE__ */ jsx("label", { className: lbl, children: label }),
810
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
811
+ /* @__PURE__ */ jsx("input", { type: "color", className: "h-8 w-9 shrink-0 cursor-pointer rounded border border-input bg-background p-0.5", value: value || "#ffffff", onChange: (e) => onChange(e.target.value) }),
812
+ /* @__PURE__ */ jsx("input", { className: input, value, placeholder: "\u2014", onChange: (e) => onChange(e.target.value) })
813
+ ] })
814
+ ] });
815
+ }
816
+ function Select({ label, value, options, onChange }) {
817
+ return /* @__PURE__ */ jsxs("div", { className: field, children: [
818
+ /* @__PURE__ */ jsx("label", { className: lbl, children: label }),
819
+ /* @__PURE__ */ jsx("select", { className: input, value, onChange: (e) => onChange(e.target.value), children: options.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value)) })
820
+ ] });
821
+ }
822
+ function Toggle({ label, value, onChange }) {
823
+ return /* @__PURE__ */ jsxs("label", { className: `${field} flex cursor-pointer items-center justify-between`, children: [
824
+ /* @__PURE__ */ jsx("span", { className: lbl.replace("mb-1 block", ""), children: label }),
825
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked), className: "h-4 w-4 accent-[var(--primary)]" })
826
+ ] });
827
+ }
828
+ function AlignPicker({ label, value, onChange }) {
829
+ const opts = ["left", "center", "right"];
830
+ return /* @__PURE__ */ jsxs("div", { className: field, children: [
831
+ /* @__PURE__ */ jsx("label", { className: lbl, children: label }),
832
+ /* @__PURE__ */ jsx("div", { className: "inline-flex rounded-md border border-input p-0.5", children: opts.map((o) => /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onChange(o), className: `rounded px-3 py-1 text-xs ${value === o ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted"}`, children: o === "left" ? "\u2B05" : o === "center" ? "\u2B0C" : "\u27A1" }, o)) })
833
+ ] });
834
+ }
835
+ function ImageUpload({ labels, uploadImage, onDone }) {
836
+ const ref = useRef(null);
837
+ const [busy, setBusy] = useState(false);
838
+ return /* @__PURE__ */ jsxs("div", { className: field, children: [
839
+ /* @__PURE__ */ jsx("input", { ref, type: "file", accept: "image/*", className: "hidden", onChange: async (e) => {
840
+ const f = e.target.files?.[0];
841
+ if (!f) return;
842
+ setBusy(true);
843
+ try {
844
+ onDone(await uploadImage(f));
845
+ } finally {
846
+ setBusy(false);
847
+ if (ref.current) ref.current.value = "";
848
+ }
849
+ } }),
850
+ /* @__PURE__ */ jsx("button", { type: "button", disabled: busy, onClick: () => ref.current?.click(), className: "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm hover:bg-muted disabled:opacity-60", children: busy ? labels.uploading : labels.uploadImage })
851
+ ] });
852
+ }
853
+ function ItemCard({ children, onRemove }) {
854
+ return /* @__PURE__ */ jsx("div", { className: "mb-2 rounded-md border border-border p-2", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1", children: [
855
+ /* @__PURE__ */ jsx("div", { className: "flex-1 space-y-1.5", children }),
856
+ /* @__PURE__ */ jsx("button", { type: "button", className: "shrink-0 rounded p-1 text-destructive hover:bg-muted", onClick: onRemove, children: /* @__PURE__ */ jsx(Trash2, { size: 14 }) })
857
+ ] }) });
858
+ }
859
+ function SocialEditor({ block, labels, onChange }) {
860
+ const networks = Object.keys(SOCIAL_NETWORKS);
861
+ const set = (items) => onChange({ ...block, items });
862
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
863
+ /* @__PURE__ */ jsx("label", { className: lbl, children: labels.socialAdd }),
864
+ /* @__PURE__ */ jsx("div", { className: "mb-4 grid grid-cols-6 gap-1.5", children: SOCIAL_SLUGS.map((slug) => {
865
+ const m = SOCIAL_NETWORKS[slug];
866
+ return /* @__PURE__ */ jsx(
867
+ "button",
868
+ {
869
+ type: "button",
870
+ title: m.label,
871
+ onClick: () => set([...block.items, { network: slug, url: "https://" }]),
872
+ className: "flex h-8 w-8 items-center justify-center rounded-full text-[9px] font-bold text-white transition-opacity hover:opacity-80",
873
+ style: { background: m.bg },
874
+ children: m.mark
875
+ },
876
+ slug
877
+ );
878
+ }) }),
879
+ /* @__PURE__ */ jsx(Select, { label: labels.iconStyle, value: block.iconStyle, options: [{ label: labels.styleCircle, value: "circle" }, { label: labels.styleRounded, value: "rounded" }, { label: labels.styleSquare, value: "square" }], onChange: (v) => onChange({ ...block, iconStyle: v }) }),
880
+ /* @__PURE__ */ jsx(AlignPicker, { label: labels.align, value: block.align, onChange: (align) => onChange({ ...block, align }) }),
881
+ /* @__PURE__ */ jsx(Num, { label: labels.size, value: block.size, min: 16, max: 64, onChange: (size) => onChange({ ...block, size }) }),
882
+ /* @__PURE__ */ jsx(Num, { label: labels.iconSpacing, value: block.spacing, min: 0, max: 40, onChange: (spacing) => onChange({ ...block, spacing }) }),
883
+ /* @__PURE__ */ jsx("label", { className: lbl, children: labels.socialLinks }),
884
+ block.items.map((it, i) => /* @__PURE__ */ jsxs(ItemCard, { onRemove: () => set(block.items.filter((_, j) => j !== i)), children: [
885
+ /* @__PURE__ */ jsx("select", { className: input, value: it.network, onChange: (e) => set(block.items.map((x, j) => j === i ? { ...x, network: e.target.value } : x)), children: networks.map((n) => /* @__PURE__ */ jsx("option", { value: n, children: SOCIAL_NETWORKS[n].label }, n)) }),
886
+ /* @__PURE__ */ jsx("input", { className: input, value: it.url, placeholder: "https://", onChange: (e) => set(block.items.map((x, j) => j === i ? { ...x, url: e.target.value } : x)) })
887
+ ] }, i))
888
+ ] });
889
+ }
890
+ function MenuEditor({ block, labels, onChange }) {
891
+ const set = (items) => onChange({ ...block, items });
892
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
893
+ /* @__PURE__ */ jsx("label", { className: lbl, children: labels.menuItems }),
894
+ block.items.map((it, i) => /* @__PURE__ */ jsxs(ItemCard, { onRemove: () => set(block.items.filter((_, j) => j !== i)), children: [
895
+ /* @__PURE__ */ jsx("input", { className: input, value: it.label, placeholder: labels.buttonLabel, onChange: (e) => set(block.items.map((x, j) => j === i ? { ...x, label: e.target.value } : x)) }),
896
+ /* @__PURE__ */ jsx("input", { className: input, value: it.url, placeholder: "https://", onChange: (e) => set(block.items.map((x, j) => j === i ? { ...x, url: e.target.value } : x)) })
897
+ ] }, i)),
898
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "mb-3 flex items-center gap-1 text-xs text-primary hover:underline", onClick: () => set([...block.items, { label: "Enlace", url: "https://" }]), children: [
899
+ /* @__PURE__ */ jsx(Plus, { size: 13 }),
900
+ labels.menuAdd
901
+ ] }),
902
+ /* @__PURE__ */ jsx(Select, { label: labels.layout, value: block.layout, options: [{ label: labels.horizontal, value: "horizontal" }, { label: labels.vertical, value: "vertical" }], onChange: (v) => onChange({ ...block, layout: v }) }),
903
+ /* @__PURE__ */ jsx(AlignPicker, { label: labels.align, value: block.align, onChange: (align) => onChange({ ...block, align }) }),
904
+ /* @__PURE__ */ jsx(Num, { label: labels.fontSize, value: block.fontSize, min: 10, max: 28, onChange: (fontSize) => onChange({ ...block, fontSize }) }),
905
+ /* @__PURE__ */ jsx(Color, { label: labels.textColor, value: block.color, onChange: (color) => onChange({ ...block, color }) }),
906
+ /* @__PURE__ */ jsx(Num, { label: labels.itemGap, value: block.gap, min: 0, max: 60, onChange: (gap) => onChange({ ...block, gap }) })
907
+ ] });
908
+ }
909
+ function BlockEditor({ block, labels, uploadImage, onChange }) {
910
+ const patch = (p) => onChange({ ...block, ...p });
911
+ switch (block.type) {
912
+ case "text": {
913
+ const b = block;
914
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
915
+ /* @__PURE__ */ jsx("p", { className: "mb-3 text-xs text-muted-foreground", children: labels.textEditHint }),
916
+ /* @__PURE__ */ jsx(AlignPicker, { label: labels.align, value: b.align, onChange: (align) => patch({ align }) }),
917
+ /* @__PURE__ */ jsx(Num, { label: labels.fontSize, value: b.fontSize, min: 10, max: 48, onChange: (fontSize) => patch({ fontSize }) }),
918
+ /* @__PURE__ */ jsx(Color, { label: labels.textColor, value: b.color, onChange: (color) => patch({ color }) }),
919
+ /* @__PURE__ */ jsx(Num, { label: labels.lineHeight, value: b.lineHeight, min: 1, max: 3, onChange: (lineHeight) => patch({ lineHeight }) })
920
+ ] });
921
+ }
922
+ case "heading": {
923
+ const b = block;
924
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
925
+ /* @__PURE__ */ jsx("p", { className: "mb-3 text-xs text-muted-foreground", children: labels.textEditHint }),
926
+ /* @__PURE__ */ jsx(Select, { label: labels.level, value: String(b.level), options: [{ label: "H1", value: "1" }, { label: "H2", value: "2" }, { label: "H3", value: "3" }], onChange: (v) => patch({ level: Number(v) }) }),
927
+ /* @__PURE__ */ jsx(AlignPicker, { label: labels.align, value: b.align, onChange: (align) => patch({ align }) }),
928
+ /* @__PURE__ */ jsx(Color, { label: labels.textColor, value: b.color, onChange: (color) => patch({ color }) })
929
+ ] });
930
+ }
931
+ case "button": {
932
+ const b = block;
933
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
934
+ /* @__PURE__ */ jsx(Text, { label: labels.buttonLabel, value: b.label, onChange: (label) => patch({ label }) }),
935
+ /* @__PURE__ */ jsx(Text, { label: labels.buttonHref, value: b.href, placeholder: "https://", onChange: (href) => patch({ href }) }),
936
+ /* @__PURE__ */ jsx(AlignPicker, { label: labels.align, value: b.align, onChange: (align) => patch({ align }) }),
937
+ /* @__PURE__ */ jsx(Toggle, { label: labels.fullWidth, value: b.fullWidth, onChange: (fullWidth) => patch({ fullWidth }) }),
938
+ /* @__PURE__ */ jsx(Color, { label: labels.buttonBg, value: b.background, onChange: (background) => patch({ background }) }),
939
+ /* @__PURE__ */ jsx(Color, { label: labels.textColor, value: b.color, onChange: (color) => patch({ color }) }),
940
+ /* @__PURE__ */ jsx(Num, { label: labels.radius, value: b.radius, min: 0, max: 40, onChange: (radius) => patch({ radius }) })
941
+ ] });
942
+ }
943
+ case "image": {
944
+ const b = block;
945
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
946
+ uploadImage && /* @__PURE__ */ jsx(ImageUpload, { labels, uploadImage, onDone: (src) => patch({ src }) }),
947
+ /* @__PURE__ */ jsx(Text, { label: labels.imageUrl, value: b.src, placeholder: "https://\u2026", onChange: (src) => patch({ src }) }),
948
+ /* @__PURE__ */ jsx(Text, { label: labels.imageAlt, value: b.alt, onChange: (alt) => patch({ alt }) }),
949
+ /* @__PURE__ */ jsx(AlignPicker, { label: labels.align, value: b.align, onChange: (align) => patch({ align }) }),
950
+ /* @__PURE__ */ jsx(Num, { label: labels.imageWidth, value: b.width, min: 0, max: 600, onChange: (width) => patch({ width }) }),
951
+ /* @__PURE__ */ jsx("p", { className: "mb-3 text-xs text-muted-foreground", children: labels.imageWidthHint }),
952
+ /* @__PURE__ */ jsx("div", { className: "mb-2 border-t border-border pt-3 text-xs font-semibold text-muted-foreground", children: labels.action }),
953
+ /* @__PURE__ */ jsx(
954
+ Select,
955
+ {
956
+ label: labels.linkType,
957
+ value: b.linkAction,
958
+ options: [{ label: labels.openWebsite, value: "url" }, { label: labels.sendEmail, value: "email" }, { label: labels.callPhone, value: "phone" }],
959
+ onChange: (v) => patch({ linkAction: v })
960
+ }
961
+ ),
962
+ /* @__PURE__ */ jsx(Text, { label: labels.linkValue, value: b.href, placeholder: b.linkAction === "email" ? "correo@dominio.cl" : b.linkAction === "phone" ? "+56 9 \u2026" : "https://", onChange: (href) => patch({ href }) })
963
+ ] });
964
+ }
965
+ case "divider": {
966
+ const b = block;
967
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
968
+ /* @__PURE__ */ jsx(Color, { label: labels.color, value: b.color, onChange: (color) => patch({ color }) }),
969
+ /* @__PURE__ */ jsx(Num, { label: labels.thickness, value: b.thickness, min: 1, max: 12, onChange: (thickness) => patch({ thickness }) })
970
+ ] });
971
+ }
972
+ case "spacer": {
973
+ const b = block;
974
+ return /* @__PURE__ */ jsx(Num, { label: labels.height, value: b.height, min: 4, max: 160, onChange: (height) => patch({ height }) });
975
+ }
976
+ case "social":
977
+ return /* @__PURE__ */ jsx(SocialEditor, { block, labels, onChange });
978
+ case "menu":
979
+ return /* @__PURE__ */ jsx(MenuEditor, { block, labels, onChange });
980
+ case "html": {
981
+ const b = block;
982
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
983
+ /* @__PURE__ */ jsx("label", { className: lbl, children: labels.htmlContent }),
984
+ /* @__PURE__ */ jsx(CodeArea, { value: b.html, onChange: (html) => patch({ html }), placeholder: "<div>\u2026</div>", height: 220 }),
985
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: labels.htmlHint })
986
+ ] });
987
+ }
988
+ default:
989
+ return null;
990
+ }
991
+ }
992
+ var TOOLS = [
993
+ { type: "heading", Icon: Heading },
994
+ { type: "text", Icon: Type },
995
+ { type: "button", Icon: MousePointerClick },
996
+ { type: "image", Icon: Image },
997
+ { type: "divider", Icon: Minus },
998
+ { type: "spacer", Icon: MoveVertical },
999
+ { type: "menu", Icon: Menu },
1000
+ { type: "social", Icon: Share2 },
1001
+ { type: "html", Icon: Code2 }
1002
+ ];
1003
+ function ContentPalette({ labels, onAppend, onDragState }) {
1004
+ return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: TOOLS.map(({ type, Icon }) => /* @__PURE__ */ jsxs(
1005
+ "button",
1006
+ {
1007
+ type: "button",
1008
+ draggable: true,
1009
+ onDragStart: (e) => {
1010
+ setDrag(e, { kind: "new-block", blockType: type });
1011
+ onDragState(true);
1012
+ },
1013
+ onDragEnd: () => onDragState(false),
1014
+ onClick: () => onAppend(type),
1015
+ className: "flex cursor-grab flex-col items-center gap-1.5 rounded-md border border-border bg-background px-2 py-3 text-[11px] text-muted-foreground transition-colors hover:border-primary/40 hover:bg-muted hover:text-foreground",
1016
+ children: [
1017
+ /* @__PURE__ */ jsx(Icon, { size: 17 }),
1018
+ labels.blockNames[type]
1019
+ ]
1020
+ },
1021
+ type
1022
+ )) });
1023
+ }
1024
+ function LayoutGrid({ onPick, onDragState }) {
1025
+ return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-1.5", children: ROW_LAYOUTS.map((layout) => /* @__PURE__ */ jsx(
1026
+ "button",
1027
+ {
1028
+ type: "button",
1029
+ draggable: Boolean(onDragState),
1030
+ onDragStart: onDragState ? (e) => {
1031
+ setDrag(e, { kind: "new-row", weights: layout.weights });
1032
+ onDragState(true);
1033
+ } : void 0,
1034
+ onDragEnd: onDragState ? () => onDragState(false) : void 0,
1035
+ onClick: () => onPick(layout.weights),
1036
+ className: `flex items-stretch gap-0.5 rounded-md border border-border bg-background p-1.5 transition-colors hover:border-primary/40 hover:bg-muted ${onDragState ? "cursor-grab" : "cursor-pointer"}`,
1037
+ style: { height: 34 },
1038
+ children: layout.weights.map((w, i) => /* @__PURE__ */ jsx("span", { className: "rounded-sm bg-muted-foreground/30", style: { flex: w } }, i))
1039
+ },
1040
+ layout.key
1041
+ )) });
1042
+ }
1043
+ function RowsPalette({ onAppend, onDragState }) {
1044
+ return /* @__PURE__ */ jsx(LayoutGrid, { onPick: onAppend, onDragState });
1045
+ }
1046
+ function RowEditor({ row, labels, uploadImage, onRowChange, onColumnChange, onRowLayout }) {
1047
+ const [colIdx, setColIdx] = useState(0);
1048
+ const active = row.columns[Math.min(colIdx, row.columns.length - 1)];
1049
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1050
+ /* @__PURE__ */ jsx("label", { className: lbl, children: labels.columns }),
1051
+ /* @__PURE__ */ jsx("div", { className: "mb-4", children: /* @__PURE__ */ jsx(LayoutGrid, { onPick: (w) => onRowLayout(row.id, w) }) }),
1052
+ /* @__PURE__ */ jsx(Color, { label: labels.rowBg, value: row.background, onChange: (background) => onRowChange({ ...row, background }) }),
1053
+ /* @__PURE__ */ jsx(Color, { label: labels.contentBg, value: row.contentBackground, onChange: (contentBackground) => onRowChange({ ...row, contentBackground }) }),
1054
+ /* @__PURE__ */ jsx(Num, { label: labels.paddingY, value: row.paddingY, min: 0, max: 80, onChange: (paddingY) => onRowChange({ ...row, paddingY }) }),
1055
+ /* @__PURE__ */ jsx(Num, { label: labels.blockGap, value: row.gap, min: 0, max: 48, onChange: (gap) => onRowChange({ ...row, gap }) }),
1056
+ /* @__PURE__ */ jsx("div", { className: "mb-2 mt-1 border-t border-border pt-3 text-xs font-semibold text-muted-foreground", children: labels.backgroundImage }),
1057
+ uploadImage && /* @__PURE__ */ jsx(ImageUpload, { labels, uploadImage, onDone: (url) => onRowChange({ ...row, backgroundImage: url }) }),
1058
+ /* @__PURE__ */ jsx(Text, { label: labels.imageUrl, value: row.backgroundImage, placeholder: "https://\u2026", onChange: (backgroundImage) => onRowChange({ ...row, backgroundImage }) }),
1059
+ row.columns.length > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
1060
+ /* @__PURE__ */ jsx("div", { className: "mb-2 mt-1 border-t border-border pt-3 text-xs font-semibold text-muted-foreground", children: labels.columnSettings }),
1061
+ /* @__PURE__ */ jsx("div", { className: "mb-3 flex gap-1", children: row.columns.map((c, i) => /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setColIdx(i), className: `rounded px-2.5 py-1 text-xs ${i === colIdx ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:text-foreground"}`, children: i + 1 }, c.id)) }),
1062
+ /* @__PURE__ */ jsx(Color, { label: labels.columnBg, value: active.background, onChange: (background) => onColumnChange({ ...active, background }) }),
1063
+ /* @__PURE__ */ jsx(Num, { label: labels.paddingX, value: active.paddingX, min: 0, max: 48, onChange: (paddingX) => onColumnChange({ ...active, paddingX }) }),
1064
+ /* @__PURE__ */ jsx(Num, { label: labels.paddingY, value: active.paddingY, min: 0, max: 48, onChange: (paddingY) => onColumnChange({ ...active, paddingY }) })
1065
+ ] })
1066
+ ] });
1067
+ }
1068
+ function SettingsPanel({ settings, labels, onChange }) {
1069
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1070
+ /* @__PURE__ */ jsx(Color, { label: labels.pageBg, value: settings.backgroundColor, onChange: (backgroundColor) => onChange({ ...settings, backgroundColor }) }),
1071
+ /* @__PURE__ */ jsx(Num, { label: labels.contentWidth, value: settings.contentWidth, min: 480, max: 800, onChange: (contentWidth) => onChange({ ...settings, contentWidth }) }),
1072
+ /* @__PURE__ */ jsx(Select, { label: labels.font, value: settings.fontFamily, options: EMAIL_FONT_STACKS, onChange: (fontFamily) => onChange({ ...settings, fontFamily }) }),
1073
+ /* @__PURE__ */ jsx(Color, { label: labels.textColor, value: settings.textColor, onChange: (textColor) => onChange({ ...settings, textColor }) }),
1074
+ /* @__PURE__ */ jsx(Color, { label: labels.linkColor, value: settings.linkColor, onChange: (linkColor) => onChange({ ...settings, linkColor }) }),
1075
+ /* @__PURE__ */ jsxs("div", { className: field, children: [
1076
+ /* @__PURE__ */ jsx("label", { className: lbl, children: labels.preheader }),
1077
+ /* @__PURE__ */ jsx("input", { className: input, value: settings.preheader, onChange: (e) => onChange({ ...settings, preheader: e.target.value }) }),
1078
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: labels.preheaderHint })
1079
+ ] })
1080
+ ] });
1081
+ }
1082
+ function RightPanel(props) {
1083
+ const { design, labels } = props;
1084
+ const [tab, setTab] = useState("content");
1085
+ const block = props.selectedBlockId ? design.rows.flatMap((r) => r.columns.flatMap((c) => c.blocks)).find((b) => b.id === props.selectedBlockId) ?? null : null;
1086
+ const column = !block && props.selectedColumnId ? design.rows.flatMap((r) => r.columns).find((c) => c.id === props.selectedColumnId) ?? null : null;
1087
+ const row = !block && !column && props.selectedRowId ? design.rows.find((r) => r.id === props.selectedRowId) ?? null : null;
1088
+ const selection = block ?? column ?? row;
1089
+ let title = "";
1090
+ let body = null;
1091
+ if (block) {
1092
+ title = labels.blockNames[block.type];
1093
+ body = /* @__PURE__ */ jsxs(Fragment, { children: [
1094
+ /* @__PURE__ */ jsx(BlockEditor, { block, labels, uploadImage: props.uploadImage, onChange: props.onBlockChange }),
1095
+ /* @__PURE__ */ jsx("div", { className: "mt-3 border-t border-border pt-3", children: /* @__PURE__ */ jsx(Num, { label: labels.containerPadding, value: block.padding ?? 0, min: 0, max: 60, onChange: (padding) => props.onBlockChange({ ...block, padding }) }) })
1096
+ ] });
1097
+ } else if (column) {
1098
+ title = labels.columnSettings;
1099
+ body = /* @__PURE__ */ jsxs(Fragment, { children: [
1100
+ /* @__PURE__ */ jsx(Color, { label: labels.columnBg, value: column.background, onChange: (background) => props.onColumnChange({ ...column, background }) }),
1101
+ /* @__PURE__ */ jsx(Num, { label: labels.paddingX, value: column.paddingX, min: 0, max: 48, onChange: (paddingX) => props.onColumnChange({ ...column, paddingX }) }),
1102
+ /* @__PURE__ */ jsx(Num, { label: labels.paddingY, value: column.paddingY, min: 0, max: 48, onChange: (paddingY) => props.onColumnChange({ ...column, paddingY }) })
1103
+ ] });
1104
+ } else if (row) {
1105
+ title = labels.rowSettings;
1106
+ body = /* @__PURE__ */ jsx(RowEditor, { row, labels, uploadImage: props.uploadImage, onRowChange: props.onRowChange, onColumnChange: props.onColumnChange, onRowLayout: props.onRowLayout });
1107
+ }
1108
+ return /* @__PURE__ */ jsx("div", { className: "flex w-72 shrink-0 flex-col overflow-hidden border-l border-border bg-card", children: selection ? /* @__PURE__ */ jsxs(Fragment, { children: [
1109
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: props.onClearSelection, className: "flex items-center gap-1 border-b border-border px-3 py-2.5 text-sm font-medium hover:bg-muted", children: [
1110
+ /* @__PURE__ */ jsx(ChevronLeft, { size: 15 }),
1111
+ " ",
1112
+ title
1113
+ ] }),
1114
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-4", children: body })
1115
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1116
+ /* @__PURE__ */ jsx("div", { className: "flex border-b border-border", children: ["content", "rows", "settings"].map((t) => /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setTab(t), className: `flex-1 border-b-2 py-2.5 text-xs font-medium ${tab === t ? "border-primary text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"}`, children: t === "content" ? labels.tabContent : t === "rows" ? labels.tabRows : labels.tabSettings }, t)) }),
1117
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4", children: [
1118
+ tab === "content" && /* @__PURE__ */ jsx(ContentPalette, { labels, onAppend: props.onAppendBlock, onDragState: props.onDragStateChange }),
1119
+ tab === "rows" && /* @__PURE__ */ jsx(RowsPalette, { onAppend: props.onAppendRow, onDragState: props.onDragStateChange }),
1120
+ tab === "settings" && /* @__PURE__ */ jsx(SettingsPanel, { settings: design.settings, labels, onChange: props.onSettingsChange })
1121
+ ] })
1122
+ ] }) });
1123
+ }
1124
+
1125
+ // src/labels.ts
1126
+ var DEFAULT_LABELS = {
1127
+ tabContent: "Content",
1128
+ tabRows: "Rows",
1129
+ tabSettings: "Settings",
1130
+ blockNames: { text: "Text", heading: "Heading", button: "Button", image: "Image", divider: "Divider", spacer: "Spacer", social: "Social", html: "HTML", menu: "Menu" },
1131
+ emailSettings: "Email settings",
1132
+ rowSettings: "Row",
1133
+ columnSettings: "Column",
1134
+ columns: "Columns",
1135
+ back: "Back",
1136
+ duplicate: "Duplicate",
1137
+ remove: "Remove",
1138
+ moveUp: "Move up",
1139
+ moveDown: "Move down",
1140
+ addRow: "Add row",
1141
+ empty: "Drag a block here",
1142
+ dropRowHere: "Drop a row here",
1143
+ undo: "Undo",
1144
+ redo: "Redo",
1145
+ preview: "Preview",
1146
+ desktop: "Desktop",
1147
+ mobile: "Mobile",
1148
+ edit: "Edit",
1149
+ bold: "Bold",
1150
+ italic: "Italic",
1151
+ underline: "Underline",
1152
+ strike: "Strikethrough",
1153
+ link: "Link",
1154
+ linkUrl: "Paste a URL",
1155
+ bulletList: "Bulleted list",
1156
+ numberList: "Numbered list",
1157
+ clearFormat: "Clear formatting",
1158
+ insertField: "Field",
1159
+ align: "Alignment",
1160
+ fontSize: "Font size",
1161
+ textColor: "Text color",
1162
+ lineHeight: "Line height",
1163
+ level: "Level",
1164
+ buttonLabel: "Label",
1165
+ buttonHref: "Link",
1166
+ buttonBg: "Background",
1167
+ radius: "Corner radius",
1168
+ fullWidth: "Full width",
1169
+ imageUrl: "Image URL",
1170
+ imageAlt: "Alt text",
1171
+ imageHref: "Link (optional)",
1172
+ imageWidth: "Width (px)",
1173
+ imageWidthHint: "0 = full column width",
1174
+ uploadImage: "Upload image",
1175
+ uploading: "Uploading\u2026",
1176
+ dropImage: "Drop an image or click to upload",
1177
+ color: "Color",
1178
+ thickness: "Thickness",
1179
+ height: "Height",
1180
+ size: "Size",
1181
+ textEditHint: "Click the text on the canvas to edit it.",
1182
+ socialLinks: "Networks",
1183
+ socialAdd: "Add icons",
1184
+ socialNetwork: "Network",
1185
+ socialUrl: "URL",
1186
+ iconSpacing: "Icon spacing",
1187
+ htmlContent: "HTML",
1188
+ htmlHint: "Raw HTML \u2014 scripts are stripped.",
1189
+ menuItems: "Menu items",
1190
+ menuAdd: "Add item",
1191
+ layout: "Layout",
1192
+ horizontal: "Horizontal",
1193
+ vertical: "Vertical",
1194
+ itemGap: "Space between items",
1195
+ action: "Action",
1196
+ linkType: "Link type",
1197
+ openWebsite: "Open website",
1198
+ sendEmail: "Send email",
1199
+ callPhone: "Call phone",
1200
+ linkValue: "Value",
1201
+ rowBg: "Background",
1202
+ contentBg: "Content background",
1203
+ backgroundImage: "Background image",
1204
+ paddingY: "Vertical padding",
1205
+ paddingX: "Horizontal padding",
1206
+ blockGap: "Space between blocks",
1207
+ columnBg: "Background",
1208
+ containerPadding: "Container padding",
1209
+ iconStyle: "Icon shape",
1210
+ styleCircle: "Circle",
1211
+ styleRounded: "Rounded",
1212
+ styleSquare: "Square",
1213
+ pageBg: "Page background",
1214
+ contentWidth: "Content width",
1215
+ font: "Font",
1216
+ linkColor: "Link color",
1217
+ preheader: "Preheader",
1218
+ preheaderHint: "Preview line shown after the subject in the inbox."
1219
+ };
1220
+ function mapColumns(design, colId, fn) {
1221
+ return { ...design, rows: design.rows.map((r) => ({ ...r, columns: r.columns.map((c) => c.id === colId ? { ...c, blocks: fn(c.blocks) } : c) })) };
1222
+ }
1223
+ function locateBlock(design, blockId) {
1224
+ for (const r of design.rows) {
1225
+ for (const c of r.columns) {
1226
+ const index = c.blocks.findIndex((b) => b.id === blockId);
1227
+ if (index >= 0) return { colId: c.id, index, block: c.blocks[index] };
1228
+ }
1229
+ }
1230
+ return null;
1231
+ }
1232
+ var cloneBlock = (b) => ({ ...b, id: uid() });
1233
+ var cloneColumn = (c) => ({ ...c, id: uid(), blocks: c.blocks.map(cloneBlock) });
1234
+ var cloneRow = (r) => ({ ...r, id: uid(), columns: r.columns.map(cloneColumn) });
1235
+ function EmailBuilder({ value, onChange, labels: labelOverrides, uploadImage, socialIconBase, height = 640 }) {
1236
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides, blockNames: { ...DEFAULT_LABELS.blockNames, ...labelOverrides?.blockNames } };
1237
+ const [selectedBlockId, setSelectedBlockId] = useState(null);
1238
+ const [selectedRowId, setSelectedRowId] = useState(null);
1239
+ const [selectedColumnId, setSelectedColumnId] = useState(null);
1240
+ const [dragging, setDragging] = useState(false);
1241
+ const [preview, setPreview] = useState(null);
1242
+ const [past, setPast] = useState([]);
1243
+ const [future, setFuture] = useState([]);
1244
+ const commit = (next) => {
1245
+ setPast((p) => [...p, value].slice(-50));
1246
+ setFuture([]);
1247
+ onChange(next);
1248
+ };
1249
+ const undo = () => {
1250
+ if (!past.length) return;
1251
+ const prev = past[past.length - 1];
1252
+ setPast((p) => p.slice(0, -1));
1253
+ setFuture((f) => [value, ...f]);
1254
+ onChange(prev);
1255
+ };
1256
+ const redo = () => {
1257
+ if (!future.length) return;
1258
+ const nxt = future[0];
1259
+ setFuture((f) => f.slice(1));
1260
+ setPast((p) => [...p, value]);
1261
+ onChange(nxt);
1262
+ };
1263
+ const selectBlock = (blockId, rowId) => {
1264
+ setSelectedBlockId(blockId);
1265
+ setSelectedRowId(rowId);
1266
+ setSelectedColumnId(null);
1267
+ };
1268
+ const selectRow = (rowId) => {
1269
+ setSelectedBlockId(null);
1270
+ setSelectedColumnId(null);
1271
+ setSelectedRowId(rowId);
1272
+ };
1273
+ const selectColumn = (colId, rowId) => {
1274
+ setSelectedBlockId(null);
1275
+ setSelectedColumnId(colId);
1276
+ setSelectedRowId(rowId);
1277
+ };
1278
+ const selectNone = () => {
1279
+ setSelectedBlockId(null);
1280
+ setSelectedRowId(null);
1281
+ setSelectedColumnId(null);
1282
+ };
1283
+ const dropInColumn = (colId, index, data) => {
1284
+ if (data.kind === "new-block") {
1285
+ const block = makeBlock(data.blockType);
1286
+ commit(mapColumns(value, colId, (bs) => [...bs.slice(0, index), block, ...bs.slice(index)]));
1287
+ const row = value.rows.find((r) => r.columns.some((c) => c.id === colId));
1288
+ selectBlock(block.id, row?.id ?? "");
1289
+ return;
1290
+ }
1291
+ if (data.kind !== "move-block") return;
1292
+ const found = locateBlock(value, data.blockId);
1293
+ if (!found) return;
1294
+ let idx = index;
1295
+ if (found.colId === colId && found.index < index) idx -= 1;
1296
+ const removed = mapColumns(value, found.colId, (bs) => bs.filter((b) => b.id !== data.blockId));
1297
+ commit(mapColumns(removed, colId, (bs) => [...bs.slice(0, idx), found.block, ...bs.slice(idx)]));
1298
+ };
1299
+ const dropRow = (index, data) => {
1300
+ if (data.kind === "new-row") {
1301
+ const row = makeRow(data.weights);
1302
+ const rows2 = [...value.rows.slice(0, index), row, ...value.rows.slice(index)];
1303
+ commit({ ...value, rows: rows2 });
1304
+ selectRow(row.id);
1305
+ return;
1306
+ }
1307
+ if (data.kind !== "move-row") return;
1308
+ const from = value.rows.findIndex((r) => r.id === data.rowId);
1309
+ if (from < 0) return;
1310
+ let to = index;
1311
+ if (from < index) to -= 1;
1312
+ const rows = [...value.rows];
1313
+ const [moved] = rows.splice(from, 1);
1314
+ rows.splice(to, 0, moved);
1315
+ commit({ ...value, rows });
1316
+ };
1317
+ const blockAction = (blockId, action) => {
1318
+ const found = locateBlock(value, blockId);
1319
+ if (!found) return;
1320
+ if (action === "remove") {
1321
+ commit(mapColumns(value, found.colId, (bs) => bs.filter((b) => b.id !== blockId)));
1322
+ if (selectedBlockId === blockId) setSelectedBlockId(null);
1323
+ return;
1324
+ }
1325
+ commit(mapColumns(value, found.colId, (bs) => {
1326
+ const i = bs.findIndex((b) => b.id === blockId);
1327
+ const next = [...bs];
1328
+ if (action === "duplicate") next.splice(i + 1, 0, cloneBlock(bs[i]));
1329
+ else if (action === "up" && i > 0) {
1330
+ [next[i - 1], next[i]] = [next[i], next[i - 1]];
1331
+ } else if (action === "down" && i < bs.length - 1) {
1332
+ [next[i + 1], next[i]] = [next[i], next[i + 1]];
1333
+ }
1334
+ return next;
1335
+ }));
1336
+ };
1337
+ const changeBlock = (block) => {
1338
+ const found = locateBlock(value, block.id);
1339
+ if (found) commit(mapColumns(value, found.colId, (bs) => bs.map((b) => b.id === block.id ? block : b)));
1340
+ };
1341
+ const changeRow = (row) => commit({ ...value, rows: value.rows.map((r) => r.id === row.id ? row : r) });
1342
+ const changeColumn = (col) => commit({ ...value, rows: value.rows.map((r) => ({ ...r, columns: r.columns.map((c) => c.id === col.id ? col : c) })) });
1343
+ const changeRowLayout = (rowId, weights) => commit({ ...value, rows: value.rows.map((r) => r.id === rowId ? relayoutRow(r, weights) : r) });
1344
+ const changeSettings = (settings) => commit({ ...value, settings });
1345
+ const rowAction = (rowId, action) => {
1346
+ const i = value.rows.findIndex((r) => r.id === rowId);
1347
+ if (i < 0) return;
1348
+ const next = [...value.rows];
1349
+ if (action === "remove") {
1350
+ next.splice(i, 1);
1351
+ if (selectedRowId === rowId) selectNone();
1352
+ } else if (action === "duplicate") next.splice(i + 1, 0, cloneRow(value.rows[i]));
1353
+ commit({ ...value, rows: next });
1354
+ };
1355
+ const appendBlock = (type) => {
1356
+ const block = makeBlock(type);
1357
+ const colId = selectedColumnId ?? value.rows.find((r) => r.id === selectedRowId)?.columns[0].id ?? value.rows[value.rows.length - 1]?.columns[0].id;
1358
+ if (!colId) {
1359
+ const row2 = makeRow([1]);
1360
+ row2.columns[0].blocks = [block];
1361
+ commit({ ...value, rows: [row2] });
1362
+ selectBlock(block.id, row2.id);
1363
+ return;
1364
+ }
1365
+ commit(mapColumns(value, colId, (bs) => [...bs, block]));
1366
+ const row = value.rows.find((r) => r.columns.some((c) => c.id === colId));
1367
+ selectBlock(block.id, row?.id ?? "");
1368
+ };
1369
+ const appendRow = (weights) => {
1370
+ const row = makeRow(weights);
1371
+ commit({ ...value, rows: [...value.rows, row] });
1372
+ selectRow(row.id);
1373
+ };
1374
+ const onKeyDown = (e) => {
1375
+ const el = document.activeElement;
1376
+ if (el?.isContentEditable || el?.tagName === "INPUT" || el?.tagName === "TEXTAREA") return;
1377
+ const mod = e.metaKey || e.ctrlKey;
1378
+ if (mod && e.key.toLowerCase() === "z") {
1379
+ e.preventDefault();
1380
+ if (e.shiftKey) redo();
1381
+ else undo();
1382
+ }
1383
+ };
1384
+ const previewHtml = `<!doctype html><html><body style="margin:0;background:${value.settings.backgroundColor};">${previewFill(compileDesign(value, { socialIconBase }))}</body></html>`;
1385
+ const iconBtn = "rounded-md p-1.5 text-muted-foreground enabled:hover:bg-muted enabled:hover:text-foreground disabled:opacity-40";
1386
+ return /* @__PURE__ */ jsxs("div", { className: "relative flex flex-col overflow-hidden rounded-lg border border-border bg-background", style: { height }, onKeyDown, tabIndex: -1, children: [
1387
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-border bg-card px-3 py-1.5", children: [
1388
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
1389
+ /* @__PURE__ */ jsx("button", { type: "button", title: labels.undo, className: iconBtn, disabled: !past.length, onClick: undo, children: /* @__PURE__ */ jsx(Undo2, { size: 16 }) }),
1390
+ /* @__PURE__ */ jsx("button", { type: "button", title: labels.redo, className: iconBtn, disabled: !future.length, onClick: redo, children: /* @__PURE__ */ jsx(Redo2, { size: 16 }) })
1391
+ ] }),
1392
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => setPreview("desktop"), className: "flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1 text-xs font-medium hover:bg-muted", children: [
1393
+ /* @__PURE__ */ jsx(Eye, { size: 14 }),
1394
+ " ",
1395
+ labels.preview
1396
+ ] })
1397
+ ] }),
1398
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 overflow-hidden", children: [
1399
+ /* @__PURE__ */ jsx(
1400
+ Canvas,
1401
+ {
1402
+ design: value,
1403
+ labels,
1404
+ selectedBlockId,
1405
+ selectedRowId,
1406
+ selectedColumnId,
1407
+ dragging,
1408
+ socialIconBase,
1409
+ uploadImage,
1410
+ onSelectBlock: selectBlock,
1411
+ onSelectRow: selectRow,
1412
+ onSelectColumn: selectColumn,
1413
+ onSelectNone: selectNone,
1414
+ onBlockChange: changeBlock,
1415
+ onBlockAction: blockAction,
1416
+ onRowAction: rowAction,
1417
+ onDragStateChange: setDragging,
1418
+ onDropInColumn: dropInColumn,
1419
+ onDropRow: dropRow
1420
+ }
1421
+ ),
1422
+ /* @__PURE__ */ jsx(
1423
+ RightPanel,
1424
+ {
1425
+ design: value,
1426
+ labels,
1427
+ uploadImage,
1428
+ selectedBlockId,
1429
+ selectedRowId,
1430
+ selectedColumnId,
1431
+ onBlockChange: changeBlock,
1432
+ onRowChange: changeRow,
1433
+ onColumnChange: changeColumn,
1434
+ onRowLayout: changeRowLayout,
1435
+ onSettingsChange: changeSettings,
1436
+ onAppendBlock: appendBlock,
1437
+ onAppendRow: appendRow,
1438
+ onClearSelection: selectNone,
1439
+ onDragStateChange: setDragging
1440
+ }
1441
+ )
1442
+ ] }),
1443
+ preview && /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 z-50 flex flex-col bg-black/60", onClick: () => setPreview(null), children: [
1444
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 py-3", onClick: (e) => e.stopPropagation(), children: [
1445
+ /* @__PURE__ */ jsxs("div", { className: "inline-flex rounded-md bg-card p-0.5", children: [
1446
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => setPreview("desktop"), className: `flex items-center gap-1 rounded px-3 py-1 text-xs ${preview === "desktop" ? "bg-primary text-primary-foreground" : "text-muted-foreground"}`, children: [
1447
+ /* @__PURE__ */ jsx(Monitor, { size: 13 }),
1448
+ " ",
1449
+ labels.desktop
1450
+ ] }),
1451
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => setPreview("mobile"), className: `flex items-center gap-1 rounded px-3 py-1 text-xs ${preview === "mobile" ? "bg-primary text-primary-foreground" : "text-muted-foreground"}`, children: [
1452
+ /* @__PURE__ */ jsx(Smartphone, { size: 13 }),
1453
+ " ",
1454
+ labels.mobile
1455
+ ] })
1456
+ ] }),
1457
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPreview(null), className: "rounded-md bg-card p-1.5 text-muted-foreground hover:text-foreground", children: /* @__PURE__ */ jsx(X, { size: 16 }) })
1458
+ ] }),
1459
+ /* @__PURE__ */ jsx("div", { className: "flex flex-1 justify-center overflow-y-auto p-4", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
1460
+ "iframe",
1461
+ {
1462
+ title: labels.preview,
1463
+ sandbox: "",
1464
+ srcDoc: previewHtml,
1465
+ className: "h-full rounded-lg border-0 bg-white shadow-xl",
1466
+ style: { width: preview === "mobile" ? 375 : value.settings.contentWidth + 40 }
1467
+ }
1468
+ ) })
1469
+ ] })
1470
+ ] });
1471
+ }
1472
+
1473
+ export { DEFAULT_LABELS, EmailBuilder, MOBILE_STACK_CSS, blockCount, compileDesign, defaultDesign };
1474
+ //# sourceMappingURL=index.js.map
1475
+ //# sourceMappingURL=index.js.map