@endora-commerce/email-components 0.100.0

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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/dist/components/email-branding-preview-context.d.ts +7 -0
  4. package/dist/components/email-branding-preview-context.d.ts.map +1 -0
  5. package/dist/components/email-branding-preview-context.js +8 -0
  6. package/dist/components/email-branding-preview-context.js.map +1 -0
  7. package/dist/components/email-embeds-context.d.ts +11 -0
  8. package/dist/components/email-embeds-context.d.ts.map +1 -0
  9. package/dist/components/email-embeds-context.js +15 -0
  10. package/dist/components/email-embeds-context.js.map +1 -0
  11. package/dist/config.d.ts +333 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +900 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/defaults/default-footer.d.ts +13 -0
  16. package/dist/defaults/default-footer.d.ts.map +1 -0
  17. package/dist/defaults/default-footer.js +42 -0
  18. package/dist/defaults/default-footer.js.map +1 -0
  19. package/dist/defaults/default-header.d.ts +5 -0
  20. package/dist/defaults/default-header.d.ts.map +1 -0
  21. package/dist/defaults/default-header.js +34 -0
  22. package/dist/defaults/default-header.js.map +1 -0
  23. package/dist/defaults/simple-email-body.d.ts +20 -0
  24. package/dist/defaults/simple-email-body.d.ts.map +1 -0
  25. package/dist/defaults/simple-email-body.js +41 -0
  26. package/dist/defaults/simple-email-body.js.map +1 -0
  27. package/dist/directives/directive-engine.d.ts +7 -0
  28. package/dist/directives/directive-engine.d.ts.map +1 -0
  29. package/dist/directives/directive-engine.js +144 -0
  30. package/dist/directives/directive-engine.js.map +1 -0
  31. package/dist/editor/email-row-layout-presets.d.ts +33 -0
  32. package/dist/editor/email-row-layout-presets.d.ts.map +1 -0
  33. package/dist/editor/email-row-layout-presets.js +145 -0
  34. package/dist/editor/email-row-layout-presets.js.map +1 -0
  35. package/dist/index.d.ts +18 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +24 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/render/escape-html.d.ts +4 -0
  40. package/dist/render/escape-html.d.ts.map +1 -0
  41. package/dist/render/escape-html.js +21 -0
  42. package/dist/render/escape-html.js.map +1 -0
  43. package/dist/render/order-labels.d.ts +23 -0
  44. package/dist/render/order-labels.d.ts.map +1 -0
  45. package/dist/render/order-labels.js +46 -0
  46. package/dist/render/order-labels.js.map +1 -0
  47. package/dist/render/render-email-html.d.ts +18 -0
  48. package/dist/render/render-email-html.d.ts.map +1 -0
  49. package/dist/render/render-email-html.js +659 -0
  50. package/dist/render/render-email-html.js.map +1 -0
  51. package/dist/render/render-email-text.d.ts +11 -0
  52. package/dist/render/render-email-text.d.ts.map +1 -0
  53. package/dist/render/render-email-text.js +167 -0
  54. package/dist/render/render-email-text.js.map +1 -0
  55. package/dist/render/sample-variables.d.ts +15 -0
  56. package/dist/render/sample-variables.d.ts.map +1 -0
  57. package/dist/render/sample-variables.js +43 -0
  58. package/dist/render/sample-variables.js.map +1 -0
  59. package/dist/render/sanitize-email-html.d.ts +9 -0
  60. package/dist/render/sanitize-email-html.d.ts.map +1 -0
  61. package/dist/render/sanitize-email-html.js +122 -0
  62. package/dist/render/sanitize-email-html.js.map +1 -0
  63. package/dist/render/social-icon-svg.d.ts +6 -0
  64. package/dist/render/social-icon-svg.d.ts.map +1 -0
  65. package/dist/render/social-icon-svg.js +41 -0
  66. package/dist/render/social-icon-svg.js.map +1 -0
  67. package/dist/schema/component-types.d.ts +269 -0
  68. package/dist/schema/component-types.d.ts.map +1 -0
  69. package/dist/schema/component-types.js +105 -0
  70. package/dist/schema/component-types.js.map +1 -0
  71. package/dist/schema/envelope.d.ts +21 -0
  72. package/dist/schema/envelope.d.ts.map +1 -0
  73. package/dist/schema/envelope.js +8 -0
  74. package/dist/schema/envelope.js.map +1 -0
  75. package/dist/tree/walk-embeds.d.ts +7 -0
  76. package/dist/tree/walk-embeds.d.ts.map +1 -0
  77. package/dist/tree/walk-embeds.js +115 -0
  78. package/dist/tree/walk-embeds.js.map +1 -0
  79. package/package.json +71 -0
  80. package/tailwind.css +14 -0
@@ -0,0 +1,659 @@
1
+ // Pure email-HTML renderer (feature 047 + email builder expansion).
2
+ // Table-based, inline-styled HTML. Directive markers left intact for the engine.
3
+ import { EMAIL_ORDER_BLOCK_MARGIN_DEFAULT, EMAIL_ORDER_LABELED_FIELDS, } from '../schema/component-types.js';
4
+ import { escapeAttr, escapeHtml } from './escape-html.js';
5
+ import { orderSummaryColumnLabels } from './order-labels.js';
6
+ import { emailHtmlToPlainText, sanitizeEmailHtml } from './sanitize-email-html.js';
7
+ import { EMAIL_SOCIAL_BRAND_COLORS, EMAIL_SOCIAL_LABELS, socialIconDataUri, } from './social-icon-svg.js';
8
+ const DEFAULT_ACCENT = '#1f2937';
9
+ const DEFAULT_MAX_DEPTH = 3;
10
+ function bool(props, key, fallback) {
11
+ const v = props?.[key];
12
+ if (typeof v === 'boolean')
13
+ return v;
14
+ // Puck radio fields sometimes persist Yes/No as strings.
15
+ if (v === 'true')
16
+ return true;
17
+ if (v === 'false')
18
+ return false;
19
+ return fallback;
20
+ }
21
+ function asNodes(list) {
22
+ if (!Array.isArray(list))
23
+ return [];
24
+ const out = [];
25
+ for (const raw of list) {
26
+ if (raw && typeof raw === 'object' && typeof raw.type === 'string') {
27
+ out.push(raw);
28
+ }
29
+ }
30
+ return out;
31
+ }
32
+ function treeContent(tree) {
33
+ return asNodes(tree?.content);
34
+ }
35
+ function str(props, key, fallback = '') {
36
+ const v = props?.[key];
37
+ return typeof v === 'string' ? v : fallback;
38
+ }
39
+ function num(props, key, fallback) {
40
+ const v = props?.[key];
41
+ return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
42
+ }
43
+ function align(props, fallback = 'left') {
44
+ const v = props?.['align'];
45
+ return v === 'left' || v === 'center' || v === 'right' ? v : fallback;
46
+ }
47
+ function multiline(text) {
48
+ return escapeHtml(text).replace(/\r?\n/g, '<br />');
49
+ }
50
+ /** Nested column content uses tighter horizontal padding than the outer mail shell. */
51
+ const cellPaddingStack = ['0 24px'];
52
+ function withCellPadding(padding, fn) {
53
+ cellPaddingStack.push(padding);
54
+ try {
55
+ return fn();
56
+ }
57
+ finally {
58
+ cellPaddingStack.pop();
59
+ }
60
+ }
61
+ function row(inner, padding, extraTdStyle = '') {
62
+ const pad = padding ?? cellPaddingStack[cellPaddingStack.length - 1] ?? '0 24px';
63
+ return `<tr><td style="padding:${pad};font-family:Arial,Helvetica,sans-serif;${extraTdStyle}">${inner}</td></tr>`;
64
+ }
65
+ function orderBlockPadding(marginTop, marginBottom) {
66
+ const top = Math.max(0, marginTop ?? EMAIL_ORDER_BLOCK_MARGIN_DEFAULT);
67
+ const bottom = Math.max(0, marginBottom ?? EMAIL_ORDER_BLOCK_MARGIN_DEFAULT);
68
+ return `${top}px 24px ${bottom}px 24px`;
69
+ }
70
+ /** Resolve Puck slot children from zones (`id:slot`) or inline props.content. */
71
+ function slotChildren(node, ctx, slotName = 'content') {
72
+ const id = node.props?.['id'];
73
+ if (typeof id === 'string' && ctx.tree.zones) {
74
+ const zone = ctx.tree.zones[`${id}:${slotName}`];
75
+ if (Array.isArray(zone) && zone.length > 0)
76
+ return asNodes(zone);
77
+ }
78
+ const inline = node.props?.[slotName];
79
+ if (Array.isArray(inline))
80
+ return asNodes(inline);
81
+ return [];
82
+ }
83
+ function renderHeading(p) {
84
+ const size = p.level === 'h1' ? '28px' : p.level === 'h2' ? '22px' : '18px';
85
+ return row(`<${p.level} style="margin:0;padding:12px 0;font-size:${size};line-height:1.3;color:#15202b;text-align:${p.align};font-weight:bold;">${multiline(p.text)}</${p.level}>`);
86
+ }
87
+ function renderText(p) {
88
+ return row(`<p style="margin:0;padding:8px 0;font-size:15px;line-height:1.5;color:#1f2937;text-align:${p.align};">${multiline(p.text)}</p>`);
89
+ }
90
+ function renderButton(p, accent) {
91
+ const bg = p.backgroundColor || accent;
92
+ const fg = p.textColor || '#ffffff';
93
+ const href = p.href || '#';
94
+ const btn = `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:8px 0;">` +
95
+ `<tr><td style="background-color:${escapeAttr(bg)};border-radius:6px;">` +
96
+ `<a href="${escapeAttr(href)}" target="_blank" rel="noreferrer" ` +
97
+ `style="display:inline-block;padding:12px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;` +
98
+ `font-weight:bold;color:${escapeAttr(fg)};text-decoration:none;">${escapeHtml(p.label)}</a>` +
99
+ `</td></tr></table>`;
100
+ return row(`<div style="text-align:${p.align};">${btn}</div>`);
101
+ }
102
+ function renderImage(p) {
103
+ if (!p.src)
104
+ return '';
105
+ const margin = p.align === 'center' ? '0 auto' : p.align === 'right' ? '0 0 0 auto' : '0';
106
+ const img = `<img src="${escapeAttr(p.src)}" alt="${escapeAttr(p.alt)}" width="${p.width}" ` +
107
+ `style="display:block;border:0;outline:none;max-width:100%;height:auto;width:${p.width}px;margin:${margin};" />`;
108
+ const wrapped = p.href
109
+ ? `<a href="${escapeAttr(p.href)}" target="_blank" rel="noreferrer" style="display:inline-block;margin:${margin};">${img}</a>`
110
+ : img;
111
+ // Table cell align is more reliable than text-align + display:block img.
112
+ return `<tr><td align="${p.align}" style="padding:0 24px;font-family:Arial,Helvetica,sans-serif;">${wrapped}</td></tr>`;
113
+ }
114
+ function renderLogo(p) {
115
+ return renderImage({
116
+ src: p.src,
117
+ alt: p.alt || 'Logo',
118
+ href: p.href,
119
+ width: p.width || 160,
120
+ align: p.align || 'center',
121
+ });
122
+ }
123
+ function renderSpacer(p) {
124
+ const h = Math.max(0, p.height);
125
+ return `<tr><td style="height:${h}px;line-height:${h}px;font-size:0;">&nbsp;</td></tr>`;
126
+ }
127
+ function renderDivider(p) {
128
+ const thickness = Math.min(8, Math.max(1, p.thickness || 1));
129
+ const color = p.color || '#e5e7eb';
130
+ return row(`<hr style="border:0;border-top:${thickness}px solid ${escapeAttr(color)};margin:8px 0;" />`);
131
+ }
132
+ function renderSection(p, innerRows) {
133
+ const bg = p.backgroundColor || 'transparent';
134
+ const py = Math.max(0, p.paddingY ?? 16);
135
+ const px = Math.max(0, p.paddingX ?? 24);
136
+ return (`<tr><td style="padding:0;background-color:${escapeAttr(bg)};">` +
137
+ `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">` +
138
+ `<tr><td style="padding:${py}px ${px}px;font-family:Arial,Helvetica,sans-serif;">` +
139
+ `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">${innerRows}</table>` +
140
+ `</td></tr></table></td></tr>`);
141
+ }
142
+ function wrapAsInnerTable(innerRows) {
143
+ if (!innerRows) {
144
+ return `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="font-size:0;line-height:0;">&nbsp;</td></tr></table>`;
145
+ }
146
+ return `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">${innerRows}</table>`;
147
+ }
148
+ function renderRow(p, columns) {
149
+ if (columns.length === 0)
150
+ return '';
151
+ const gap = Math.max(0, p.gap ?? 16);
152
+ const valign = p.verticalAlign === 'middle' || p.verticalAlign === 'bottom' ? p.verticalAlign : 'top';
153
+ const n = columns.length;
154
+ const spanTotal = columns.reduce((sum, col) => sum + Math.max(1, col.span || 1), 0) || n;
155
+ const halfGap = Math.floor(gap / 2);
156
+ const tds = columns
157
+ .map((col, i) => {
158
+ const span = Math.max(1, col.span || 1);
159
+ const widthPct = Math.max(1, Math.floor((100 * span) / spanTotal));
160
+ const padLeft = i === 0 ? 0 : halfGap;
161
+ const padRight = i === n - 1 ? 0 : gap - halfGap;
162
+ return (`<td width="${widthPct}%" valign="${valign}" ` +
163
+ `style="width:${widthPct}%;vertical-align:${valign};padding:0 ${padRight}px 0 ${padLeft}px;font-family:Arial,Helvetica,sans-serif;">` +
164
+ wrapAsInnerTable(col.innerRows) +
165
+ `</td>`);
166
+ })
167
+ .join('');
168
+ return row(`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;table-layout:fixed;">` +
169
+ `<tr>${tds}</tr></table>`);
170
+ }
171
+ function renderRichText(p) {
172
+ const safe = sanitizeEmailHtml(p.html || '');
173
+ if (!safe)
174
+ return '';
175
+ return row(`<div style="text-align:${p.align};font-size:15px;line-height:1.5;color:#1f2937;">${safe}</div>`);
176
+ }
177
+ function renderProductCard(p, accent) {
178
+ const showImage = p.showImage;
179
+ const showPrice = p.showPrice;
180
+ const showSku = p.showSku;
181
+ let img = '';
182
+ if (showImage) {
183
+ if (p.imageSrc) {
184
+ img = `<img src="${escapeAttr(p.imageSrc)}" alt="" width="120" style="display:block;border:0;max-width:120px;height:auto;" />`;
185
+ }
186
+ else {
187
+ img =
188
+ `<div style="width:120px;height:120px;background-color:#f3f4f6;border:1px solid #e5e7eb;` +
189
+ `font-family:Arial,Helvetica,sans-serif;font-size:11px;color:#9ca3af;text-align:center;line-height:120px;">No image</div>`;
190
+ }
191
+ }
192
+ const cta = p.href && p.ctaLabel
193
+ ? `<a href="${escapeAttr(p.href)}" target="_blank" rel="noreferrer" ` +
194
+ `style="display:inline-block;margin-top:8px;padding:10px 16px;background-color:${escapeAttr(accent)};` +
195
+ `color:#ffffff;text-decoration:none;font-weight:bold;border-radius:6px;font-size:13px;">${escapeHtml(p.ctaLabel)}</a>`
196
+ : '';
197
+ const body = `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>` +
198
+ (img ? `<td valign="top" style="width:128px;padding-right:16px;">${img}</td>` : '') +
199
+ `<td valign="top" style="font-family:Arial,Helvetica,sans-serif;font-size:15px;color:#1f2937;text-align:${p.align || 'left'};">` +
200
+ `<div style="font-weight:bold;margin-bottom:4px;">${multiline(p.title)}</div>` +
201
+ (showSku && p.sku ? `<div style="margin-bottom:4px;font-size:13px;color:#6b7280;">${multiline(p.sku)}</div>` : '') +
202
+ (showPrice && p.price ? `<div style="margin-bottom:4px;">${multiline(p.price)}</div>` : '') +
203
+ cta +
204
+ `</td></tr></table>`;
205
+ return row(body, '12px 24px');
206
+ }
207
+ function renderProductGrid(p, accent) {
208
+ const items = Array.isArray(p.items) ? p.items : [];
209
+ if (items.length === 0)
210
+ return '';
211
+ const columns = Math.min(3, Math.max(1, p.columns || 2));
212
+ const gap = Math.max(0, p.gap ?? 16);
213
+ const halfGap = Math.floor(gap / 2);
214
+ const showImage = p.showImage;
215
+ const showPrice = p.showPrice;
216
+ const showSku = p.showSku;
217
+ const ctaLabel = p.ctaLabel || 'View';
218
+ const widthPct = Math.floor(100 / columns);
219
+ const rowsHtml = [];
220
+ for (let i = 0; i < items.length; i += columns) {
221
+ const slice = items.slice(i, i + columns);
222
+ while (slice.length < columns)
223
+ slice.push(null);
224
+ const tds = slice
225
+ .map((item, ci) => {
226
+ const padLeft = ci === 0 ? 0 : halfGap;
227
+ const padRight = ci === columns - 1 ? 0 : gap - halfGap;
228
+ if (!item) {
229
+ return `<td width="${widthPct}%" style="width:${widthPct}%;padding:0 ${padRight}px 12px ${padLeft}px;"></td>`;
230
+ }
231
+ const img = showImage && item.imageSrc
232
+ ? `<img src="${escapeAttr(item.imageSrc)}" alt="" width="100%" style="display:block;border:0;max-width:100%;height:auto;margin-bottom:8px;" />`
233
+ : showImage
234
+ ? `<div style="width:100%;height:100px;background:#f3f4f6;margin-bottom:8px;"></div>`
235
+ : '';
236
+ const cta = item.href
237
+ ? `<a href="${escapeAttr(item.href)}" target="_blank" rel="noreferrer" style="display:inline-block;margin-top:8px;padding:8px 12px;background-color:${escapeAttr(accent)};color:#ffffff;text-decoration:none;font-size:12px;font-weight:bold;border-radius:4px;">${escapeHtml(ctaLabel)}</a>`
238
+ : '';
239
+ return (`<td width="${widthPct}%" valign="top" style="width:${widthPct}%;padding:0 ${padRight}px 12px ${padLeft}px;font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#1f2937;">` +
240
+ img +
241
+ `<div style="font-weight:bold;margin-bottom:4px;">${multiline(item.title)}</div>` +
242
+ (showSku && item.sku ? `<div style="font-size:12px;color:#6b7280;margin-bottom:4px;">${multiline(item.sku)}</div>` : '') +
243
+ (showPrice && item.price ? `<div style="margin-bottom:4px;">${multiline(item.price)}</div>` : '') +
244
+ cta +
245
+ `</td>`);
246
+ })
247
+ .join('');
248
+ rowsHtml.push(`<tr>${tds}</tr>`);
249
+ }
250
+ return row(`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;table-layout:fixed;">${rowsHtml.join('')}</table>`);
251
+ }
252
+ function renderCategoryGrid(p) {
253
+ const items = Array.isArray(p.items) ? p.items : [];
254
+ if (items.length === 0)
255
+ return '';
256
+ const columns = Math.min(3, Math.max(1, p.columns || 2));
257
+ const gap = Math.max(0, p.gap ?? 16);
258
+ const halfGap = Math.floor(gap / 2);
259
+ const showImage = p.showImage;
260
+ const widthPct = Math.floor(100 / columns);
261
+ const rowsHtml = [];
262
+ for (let i = 0; i < items.length; i += columns) {
263
+ const slice = items.slice(i, i + columns);
264
+ while (slice.length < columns)
265
+ slice.push(null);
266
+ const tds = slice
267
+ .map((item, ci) => {
268
+ const padLeft = ci === 0 ? 0 : halfGap;
269
+ const padRight = ci === columns - 1 ? 0 : gap - halfGap;
270
+ if (!item) {
271
+ return `<td width="${widthPct}%" style="width:${widthPct}%;padding:0 ${padRight}px 12px ${padLeft}px;"></td>`;
272
+ }
273
+ const img = showImage && item.imageSrc
274
+ ? `<img src="${escapeAttr(item.imageSrc)}" alt="" width="100%" style="display:block;border:0;max-width:100%;height:auto;margin-bottom:8px;" />`
275
+ : showImage
276
+ ? `<div style="width:100%;height:80px;background:#f3f4f6;margin-bottom:8px;"></div>`
277
+ : '';
278
+ const title = item.href
279
+ ? `<a href="${escapeAttr(item.href)}" target="_blank" rel="noreferrer" style="color:#1f2937;text-decoration:none;font-weight:bold;">${multiline(item.title)}</a>`
280
+ : `<div style="font-weight:bold;">${multiline(item.title)}</div>`;
281
+ return (`<td width="${widthPct}%" valign="top" style="width:${widthPct}%;padding:0 ${padRight}px 12px ${padLeft}px;font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#1f2937;">` +
282
+ img +
283
+ title +
284
+ `</td>`);
285
+ })
286
+ .join('');
287
+ rowsHtml.push(`<tr>${tds}</tr>`);
288
+ }
289
+ return row(`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;table-layout:fixed;">${rowsHtml.join('')}</table>`);
290
+ }
291
+ function renderOrderSummaryLegacy(title, body) {
292
+ return row(`<div style="font-weight:bold;margin-bottom:8px;font-size:16px;color:#15202b;">${multiline(title)}</div>` +
293
+ `<div style="font-size:14px;line-height:1.5;color:#1f2937;white-space:pre-wrap;">${multiline(body)}</div>`);
294
+ }
295
+ function renderOrderSummary(p, language) {
296
+ const title = p.title || 'Order summary';
297
+ const showSku = p.showSku === true;
298
+ const showName = p.showName !== false;
299
+ const showQty = p.showQuantity !== false;
300
+ const showPrice = p.showPrice !== false;
301
+ const showTotals = p.showTotals === true;
302
+ const col = orderSummaryColumnLabels(language);
303
+ const headers = [];
304
+ if (showSku)
305
+ headers.push(col.sku);
306
+ if (showName)
307
+ headers.push(col.item);
308
+ if (showQty)
309
+ headers.push(col.qty);
310
+ if (showPrice)
311
+ headers.push(col.price);
312
+ const headerRow = headers.length > 0
313
+ ? `<tr>${headers
314
+ .map((h) => `<th align="left" style="padding:6px 8px;border-bottom:1px solid #e5e7eb;font-size:12px;color:#6b7280;text-transform:uppercase;">${escapeHtml(h)}</th>`)
315
+ .join('')}</tr>`
316
+ : '';
317
+ const cells = [];
318
+ if (showSku) {
319
+ cells.push(`<td style="padding:8px;border-bottom:1px solid #f3f4f6;font-size:14px;color:#1f2937;">{{var item.sku}}</td>`);
320
+ }
321
+ if (showName) {
322
+ cells.push(`<td style="padding:8px;border-bottom:1px solid #f3f4f6;font-size:14px;color:#1f2937;">{{var item.name}}</td>`);
323
+ }
324
+ if (showQty) {
325
+ cells.push(`<td style="padding:8px;border-bottom:1px solid #f3f4f6;font-size:14px;color:#1f2937;">{{var item.quantity}}</td>`);
326
+ }
327
+ if (showPrice) {
328
+ cells.push(`<td style="padding:8px;border-bottom:1px solid #f3f4f6;font-size:14px;color:#1f2937;">{{var item.price}}</td>`);
329
+ }
330
+ const loop = cells.length > 0 ? `{{for item in order.items}}<tr>${cells.join('')}</tr>{{/for}}` : '';
331
+ const totals = showTotals
332
+ ? `<div style="margin-top:12px;font-size:14px;color:#1f2937;white-space:pre-wrap;">{{var order.summaryText}}</div>`
333
+ : '';
334
+ return row(`<div style="font-weight:bold;margin-bottom:8px;font-size:16px;color:#15202b;">${multiline(title)}</div>` +
335
+ `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">` +
336
+ headerRow +
337
+ loop +
338
+ `</table>` +
339
+ totals, orderBlockPadding(p.marginTop, p.marginBottom));
340
+ }
341
+ function renderOrderLabeledVar(p, varKey) {
342
+ const title = p.title?.trim() ?? '';
343
+ const heading = title
344
+ ? `<div style="font-weight:bold;margin-bottom:6px;font-size:14px;color:#15202b;">${multiline(title)}</div>`
345
+ : '';
346
+ return row(heading +
347
+ `<div style="font-size:14px;line-height:1.5;color:#1f2937;white-space:pre-wrap;">{{var ${varKey}}}</div>`, orderBlockPadding(p.marginTop, p.marginBottom));
348
+ }
349
+ function renderTable(p) {
350
+ const columns = Array.isArray(p.columns) ? p.columns : [];
351
+ const rows = Array.isArray(p.tableRows) ? p.tableRows : [];
352
+ if (columns.length === 0 && rows.length === 0)
353
+ return '';
354
+ const colCount = Math.max(1, columns.length);
355
+ const header = columns.length > 0
356
+ ? `<thead><tr>${columns
357
+ .map((c) => `<th align="left" style="padding:6px 8px;border-bottom:2px solid #e5e7eb;font-size:12px;color:#6b7280;text-transform:uppercase;">${escapeHtml(c?.label ?? '')}</th>`)
358
+ .join('')}</tr></thead>`
359
+ : '';
360
+ const body = rows
361
+ .map((rowData, ri) => {
362
+ const cells = [...(rowData.cells ?? [])];
363
+ while (cells.length < colCount)
364
+ cells.push({ value: '' });
365
+ const bg = p.striped && ri % 2 === 1 ? 'background-color:#f9fafb;' : '';
366
+ return `<tr>${cells
367
+ .slice(0, colCount)
368
+ .map((cell) => `<td style="padding:6px 8px;border-bottom:1px solid #f3f4f6;font-size:14px;color:#1f2937;${bg}">${multiline(cell?.value ?? '')}</td>`)
369
+ .join('')}</tr>`;
370
+ })
371
+ .join('');
372
+ return row(`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;width:100%;max-width:100%;">${header}<tbody>${body}</tbody></table>`);
373
+ }
374
+ function renderSocial(p) {
375
+ const links = Array.isArray(p.links) ? p.links : [];
376
+ const active = links.filter((l) => l?.enabled !== false && l?.href);
377
+ if (active.length === 0)
378
+ return '';
379
+ const showIcons = p.showIcons !== false;
380
+ const showLabels = p.showLabels === true;
381
+ const iconSize = Math.min(48, Math.max(16, p.iconSize || 28));
382
+ const useBrand = p.useBrandColors !== false;
383
+ const fallbackColor = p.color || '#1f2937';
384
+ const parts = active
385
+ .map((l) => {
386
+ const network = (l.network in EMAIL_SOCIAL_LABELS ? l.network : 'other');
387
+ const label = l.label || EMAIL_SOCIAL_LABELS[network];
388
+ const color = useBrand ? EMAIL_SOCIAL_BRAND_COLORS[network] : fallbackColor;
389
+ const icon = showIcons
390
+ ? `<img src="${escapeAttr(socialIconDataUri(network, color))}" width="${iconSize}" height="${iconSize}" ` +
391
+ `alt="" style="display:inline-block;border:0;outline:none;width:${iconSize}px;height:${iconSize}px;vertical-align:middle;" />`
392
+ : '';
393
+ const text = showLabels
394
+ ? `<span style="font-size:13px;color:${escapeAttr(color)};vertical-align:middle;${showIcons ? 'padding-left:6px;' : ''}">${escapeHtml(label)}</span>`
395
+ : '';
396
+ if (!icon && !text) {
397
+ return `<a href="${escapeAttr(l.href)}" target="_blank" rel="noreferrer" style="color:${escapeAttr(color)};text-decoration:underline;margin:0 8px;">${escapeHtml(label)}</a>`;
398
+ }
399
+ return (`<a href="${escapeAttr(l.href)}" target="_blank" rel="noreferrer" ` +
400
+ `style="display:inline-block;text-decoration:none;margin:0 8px;vertical-align:middle;" aria-label="${escapeAttr(label)}">` +
401
+ icon +
402
+ text +
403
+ `</a>`);
404
+ })
405
+ .join('');
406
+ return row(`<div style="text-align:${p.align};padding:8px 0;line-height:1;">${parts}</div>`);
407
+ }
408
+ function prepareFooterLegalHtml(text) {
409
+ let t = text || '';
410
+ const alreadyLinked = /href\s*=\s*["']\s*\{\{\s*var\s+unsubscribeUrl\s*\}\}/i.test(t);
411
+ if (!alreadyLinked && t.includes('{{var unsubscribeUrl}}')) {
412
+ if (/Unsubscribe:\s*\{\{\s*var\s+unsubscribeUrl\s*\}\}/i.test(t)) {
413
+ t = t.replace(/Unsubscribe:\s*\{\{\s*var\s+unsubscribeUrl\s*\}\}/gi, '<a href="{{var unsubscribeUrl}}">Unsubscribe</a>');
414
+ }
415
+ else {
416
+ t = t.replace(/\{\{\s*var\s+unsubscribeUrl\s*\}\}/g, '<a href="{{var unsubscribeUrl}}">Unsubscribe</a>');
417
+ }
418
+ }
419
+ return sanitizeEmailHtml(t.replace(/\r?\n/g, '<br />'));
420
+ }
421
+ function renderCallout(p) {
422
+ const bg = p.backgroundColor || '#f3f4f6';
423
+ const border = p.borderColor || '#d1d5db';
424
+ const textColor = p.textColor || '#1f2937';
425
+ return row(`<div style="background-color:${escapeAttr(bg)};border:1px solid ${escapeAttr(border)};border-radius:6px;` +
426
+ `padding:14px 16px;text-align:${p.align};font-size:15px;line-height:1.5;color:${escapeAttr(textColor)};">${multiline(p.text)}</div>`, '8px 24px');
427
+ }
428
+ function renderFooterLegal(p) {
429
+ const html = prepareFooterLegalHtml(p.text);
430
+ if (!html)
431
+ return '';
432
+ return row(`<div style="margin:0;padding:8px 0;font-size:12px;line-height:1.5;color:#6b7280;text-align:${p.align};">${html}</div>`);
433
+ }
434
+ function renderEmbed(kind, code, ctx) {
435
+ if (!code || ctx.depth >= ctx.maxDepth)
436
+ return '';
437
+ const tree = ctx.embeds[kind][code];
438
+ if (!tree)
439
+ return '';
440
+ return renderNodes(treeContent(tree), { ...ctx, tree, depth: ctx.depth + 1 });
441
+ }
442
+ function renderNode(node, ctx) {
443
+ const p = node.props ?? {};
444
+ switch (node.type) {
445
+ case 'transactional_emails.EmailHeading':
446
+ return renderHeading({
447
+ level: p['level'] ?? 'h2',
448
+ text: str(p, 'text'),
449
+ align: align(p),
450
+ });
451
+ case 'transactional_emails.EmailText':
452
+ return renderText({ text: str(p, 'text'), align: align(p) });
453
+ case 'transactional_emails.EmailButton':
454
+ return renderButton({
455
+ label: str(p, 'label', 'Button'),
456
+ href: str(p, 'href'),
457
+ align: align(p, 'left'),
458
+ backgroundColor: str(p, 'backgroundColor'),
459
+ textColor: str(p, 'textColor'),
460
+ }, ctx.accent);
461
+ case 'transactional_emails.EmailImage':
462
+ return renderImage({
463
+ src: str(p, 'src'),
464
+ alt: str(p, 'alt'),
465
+ href: str(p, 'href'),
466
+ width: num(p, 'width', 200),
467
+ align: align(p, 'center'),
468
+ ...(p['imageSource'] === 'library' || p['imageSource'] === 'url'
469
+ ? { imageSource: p['imageSource'] }
470
+ : {}),
471
+ ...(typeof p['assetId'] === 'string' ? { assetId: p['assetId'] } : {}),
472
+ });
473
+ case 'transactional_emails.EmailLogo':
474
+ return renderLogo({
475
+ src: str(p, 'src', '{{var branding.logoUrl}}'),
476
+ alt: str(p, 'alt', 'Logo'),
477
+ href: str(p, 'href'),
478
+ width: num(p, 'width', 160),
479
+ align: align(p, 'center'),
480
+ });
481
+ case 'transactional_emails.EmailDivider':
482
+ return renderDivider({
483
+ thickness: num(p, 'thickness', 1),
484
+ color: str(p, 'color', '#e5e7eb'),
485
+ });
486
+ case 'transactional_emails.EmailSpacer':
487
+ return renderSpacer({ height: num(p, 'height', 16) });
488
+ case 'transactional_emails.EmailTable':
489
+ return renderTable({
490
+ columns: Array.isArray(p['columns']) ? p['columns'] : [],
491
+ tableRows: Array.isArray(p['tableRows'])
492
+ ? p['tableRows']
493
+ : [],
494
+ striped: bool(p, 'striped', true),
495
+ });
496
+ case 'transactional_emails.EmailSection': {
497
+ const children = slotChildren(node, ctx, 'content');
498
+ const inner = renderNodes(children, ctx);
499
+ return renderSection({
500
+ backgroundColor: str(p, 'backgroundColor', '#ffffff'),
501
+ paddingY: num(p, 'paddingY', 16),
502
+ paddingX: num(p, 'paddingX', 24),
503
+ }, inner);
504
+ }
505
+ case 'transactional_emails.EmailRow': {
506
+ const columnNodes = slotChildren(node, ctx, 'content').filter((n) => n.type === 'transactional_emails.EmailColumn');
507
+ const equalFallback = columnNodes.length > 0 ? Math.floor(12 / columnNodes.length) : 6;
508
+ const columns = columnNodes.map((col) => ({
509
+ innerRows: withCellPadding('4px 0', () => renderNodes(slotChildren(col, ctx, 'content'), ctx)),
510
+ span: num(col.props, 'span', equalFallback),
511
+ }));
512
+ const valignRaw = str(p, 'verticalAlign', 'top');
513
+ const verticalAlign = valignRaw === 'middle' || valignRaw === 'bottom' ? valignRaw : 'top';
514
+ return renderRow({
515
+ gap: num(p, 'gap', 16),
516
+ verticalAlign,
517
+ }, columns);
518
+ }
519
+ case 'transactional_emails.EmailColumn': {
520
+ // Standalone column (should only live inside EmailRow) — stack children full-width.
521
+ return renderNodes(slotChildren(node, ctx, 'content'), ctx);
522
+ }
523
+ case 'transactional_emails.EmailRichText':
524
+ return renderRichText({
525
+ content: p['content'],
526
+ html: str(p, 'html'),
527
+ align: align(p),
528
+ });
529
+ case 'catalog.EmailProductCard':
530
+ return renderProductCard({
531
+ productSlug: str(p, 'productSlug'),
532
+ productId: str(p, 'productId'),
533
+ imageSrc: str(p, 'imageSrc'),
534
+ title: str(p, 'title', 'Product'),
535
+ sku: str(p, 'sku'),
536
+ price: str(p, 'price'),
537
+ href: str(p, 'href'),
538
+ ctaLabel: str(p, 'ctaLabel', 'View'),
539
+ showImage: bool(p, 'showImage', true),
540
+ showPrice: bool(p, 'showPrice', true),
541
+ showSku: bool(p, 'showSku', false),
542
+ align: align(p, 'left'),
543
+ }, ctx.accent);
544
+ case 'catalog.EmailProductGrid':
545
+ return renderProductGrid({
546
+ productSlugs: Array.isArray(p['productSlugs']) ? p['productSlugs'] : [],
547
+ columns: num(p, 'columns', 2),
548
+ gap: num(p, 'gap', 16),
549
+ showImage: bool(p, 'showImage', true),
550
+ showPrice: bool(p, 'showPrice', true),
551
+ showSku: bool(p, 'showSku', false),
552
+ ctaLabel: str(p, 'ctaLabel', 'View'),
553
+ items: Array.isArray(p['items']) ? p['items'] : [],
554
+ }, ctx.accent);
555
+ case 'catalog.EmailCategoryGrid':
556
+ return renderCategoryGrid({
557
+ categorySlugs: Array.isArray(p['categorySlugs']) ? p['categorySlugs'] : [],
558
+ columns: num(p, 'columns', 2),
559
+ gap: num(p, 'gap', 16),
560
+ showImage: bool(p, 'showImage', true),
561
+ items: Array.isArray(p['items']) ? p['items'] : [],
562
+ });
563
+ case 'orders.EmailOrderSummary': {
564
+ const rawBody = typeof p['body'] === 'string' ? p['body'] : '';
565
+ const hasNewFlags = 'showName' in p ||
566
+ 'showSku' in p ||
567
+ 'showQuantity' in p ||
568
+ 'showPrice' in p ||
569
+ 'showTotals' in p;
570
+ if (rawBody && !hasNewFlags) {
571
+ return renderOrderSummaryLegacy(str(p, 'title', 'Order summary'), rawBody);
572
+ }
573
+ return renderOrderSummary({
574
+ title: str(p, 'title', 'Order summary'),
575
+ showSku: p['showSku'] === true,
576
+ showName: p['showName'] !== false,
577
+ showQuantity: p['showQuantity'] !== false,
578
+ showPrice: p['showPrice'] !== false,
579
+ showTotals: p['showTotals'] === true,
580
+ marginTop: num(p, 'marginTop', EMAIL_ORDER_BLOCK_MARGIN_DEFAULT),
581
+ marginBottom: num(p, 'marginBottom', EMAIL_ORDER_BLOCK_MARGIN_DEFAULT),
582
+ }, ctx.language);
583
+ }
584
+ case 'orders.EmailOrderId':
585
+ case 'orders.EmailBillingAddress':
586
+ case 'orders.EmailShippingAddress':
587
+ case 'orders.EmailOrderTotals':
588
+ case 'orders.EmailAppliedDiscounts':
589
+ case 'orders.EmailDeliveryMethod':
590
+ case 'orders.EmailPaymentMethod': {
591
+ const meta = EMAIL_ORDER_LABELED_FIELDS[node.type];
592
+ return renderOrderLabeledVar({
593
+ title: str(p, 'title', meta.defaultTitle),
594
+ marginTop: num(p, 'marginTop', EMAIL_ORDER_BLOCK_MARGIN_DEFAULT),
595
+ marginBottom: num(p, 'marginBottom', EMAIL_ORDER_BLOCK_MARGIN_DEFAULT),
596
+ }, meta.varKey);
597
+ }
598
+ case 'transactional_emails.EmailSocial':
599
+ return renderSocial({
600
+ links: Array.isArray(p['links']) ? p['links'] : [],
601
+ align: align(p, 'center'),
602
+ showIcons: bool(p, 'showIcons', true),
603
+ showLabels: bool(p, 'showLabels', false),
604
+ iconSize: num(p, 'iconSize', 28),
605
+ useBrandColors: bool(p, 'useBrandColors', true),
606
+ color: str(p, 'color', '#1f2937'),
607
+ });
608
+ case 'transactional_emails.EmailCallout':
609
+ return renderCallout({
610
+ text: str(p, 'text'),
611
+ backgroundColor: str(p, 'backgroundColor', '#f3f4f6'),
612
+ borderColor: str(p, 'borderColor', '#d1d5db'),
613
+ textColor: str(p, 'textColor', '#1f2937'),
614
+ align: align(p),
615
+ });
616
+ case 'transactional_emails.EmailFooterLegal':
617
+ return renderFooterLegal({
618
+ text: str(p, 'text', 'You received this email because you are subscribed.\n<a href="{{var unsubscribeUrl}}">Unsubscribe</a>'),
619
+ align: align(p, 'center'),
620
+ });
621
+ case 'transactional_emails.EmailInsertBlock':
622
+ return renderEmbed('blocks', str(p, 'code'), ctx);
623
+ case 'transactional_emails.EmailInsertTemplate':
624
+ return renderEmbed('templates', str(p, 'code'), ctx);
625
+ default:
626
+ return '';
627
+ }
628
+ }
629
+ function renderNodes(nodes, ctx) {
630
+ return nodes.map((n) => renderNode(n, ctx)).join('');
631
+ }
632
+ function wrapDocument(body) {
633
+ return (`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8" />` +
634
+ `<meta name="viewport" content="width=device-width, initial-scale=1.0" />` +
635
+ `<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />` +
636
+ `</head>` +
637
+ `<body style="margin:0;padding:0;background-color:#f3f4f6;-webkit-text-size-adjust:100%;">` +
638
+ `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#f3f4f6;width:100%;">` +
639
+ `<tr><td align="center" style="padding:24px 12px;">` +
640
+ `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" ` +
641
+ `style="width:100%;max-width:600px;background-color:#ffffff;border-radius:8px;">` +
642
+ body +
643
+ `</table></td></tr></table></body></html>`);
644
+ }
645
+ export function renderEmailHtml(tree, opts = {}) {
646
+ const safeTree = tree ?? { root: { props: {} }, content: [], zones: {} };
647
+ const ctx = {
648
+ embeds: opts.embeds ?? { blocks: {}, templates: {} },
649
+ accent: opts.accentColor ?? DEFAULT_ACCENT,
650
+ depth: 0,
651
+ maxDepth: opts.maxDepth ?? DEFAULT_MAX_DEPTH,
652
+ tree: safeTree,
653
+ language: opts.language ?? 'en-US',
654
+ };
655
+ const body = renderNodes(treeContent(safeTree), ctx);
656
+ return opts.document === false ? body : wrapDocument(body);
657
+ }
658
+ export { emailHtmlToPlainText, sanitizeEmailHtml };
659
+ //# sourceMappingURL=render-email-html.js.map