email-builder-utils 1.1.49 → 1.1.52

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.
@@ -6,18 +6,22 @@ const buildStyles_1 = require("../buildStyles");
6
6
  const gradientUtils_1 = require("../gradientUtils");
7
7
  const common_1 = require("../common");
8
8
  const jsonToHTML_1 = require("../jsonToHTML");
9
- const outlookSupport_1 = require("../outlookSupport");
10
9
  async function convertGridBlock(blockData, rootData, cellWidthInPx) {
11
10
  const { style = {}, childrenIds = [], props } = blockData.data;
12
11
  const { columns = 1, cellWidths = [], responsive = true } = props;
13
12
  const { columnGap = 0, backgroundImage, backgroundColor, ...restStyle } = style;
14
13
  const gridVisibilityClass = (0, common_1.getVisibilityClass)(props);
14
+ // Detect gradient — check both backgroundImage prop and customCss (gradient may land in
15
+ // customCss when the block was built via CSS shorthand or custom CSS input).
15
16
  const bgImageStr = typeof backgroundImage === "string" ? backgroundImage : '';
16
17
  const customCssStr = restStyle.customCss || '';
18
+ // Extract gradient string from customCss if not already in backgroundImage
17
19
  const gradientInCustomCss = !bgImageStr.includes('gradient(') && customCssStr.includes('gradient(')
18
20
  ? (customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || '')
19
21
  : '';
20
- const effectiveGradient = bgImageStr.includes('gradient(') ? bgImageStr : gradientInCustomCss;
22
+ const effectiveGradient = bgImageStr.includes('gradient(')
23
+ ? bgImageStr
24
+ : gradientInCustomCss;
21
25
  const isGradient = Boolean(effectiveGradient);
22
26
  const parsedGradient = isGradient ? (0, gradientUtils_1.parseGradient)(effectiveGradient) : null;
23
27
  const fallbackBgColor = backgroundColor ||
@@ -27,12 +31,18 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
27
31
  const rawBgImageUrl = !isGradient && bgImageStr
28
32
  ? bgImageStr.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
29
33
  : null;
34
+ // When gradient came from customCss, strip background-image from customCss so it
35
+ // doesn't duplicate into the inner table style (the outer <td> wrapper carries it).
30
36
  const innerCustomCss = gradientInCustomCss
31
37
  ? customCssStr.replace(/background-image\s*:[^;]+;?/gi, '').trim()
32
38
  : customCssStr;
39
+ // Build inner table styles — when gradient/bg-image is on the outer wrapper, strip
40
+ // background props from the inner table so the outer <td> background shows through.
33
41
  const innerRestStyleRaw = (rawBgImageUrl || isGradient)
34
42
  ? { ...restStyle, customCss: innerCustomCss, backgroundSize: undefined, backgroundPosition: undefined, backgroundRepeat: undefined }
35
43
  : { ...restStyle, customCss: innerCustomCss };
44
+ // Extract border/radius props — applied via a div wrapper for non-MSO clients so that
45
+ // border-radius is honoured (Gmail/Outlook compose strip border-radius from <table>).
36
46
  const { borderRadius, border, borderColor, borderWidth, borderStyle: borderStyleProp, ...innerRestStyle } = innerRestStyleRaw;
37
47
  const divBorderParts = [];
38
48
  if (borderRadius)
@@ -51,36 +61,52 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
51
61
  ? 'transparent'
52
62
  : ((rawBgImageUrl || isGradient) ? undefined : backgroundColor);
53
63
  const tableStyles = (0, buildStyles_1.buildStyles)({ backgroundColor: tableBgForNonMso, ...innerRestStyle }, {
54
- perChanges: [], pxChanges: buildStyles_1.allPxAttributes,
64
+ perChanges: [],
65
+ pxChanges: buildStyles_1.allPxAttributes,
55
66
  });
56
67
  const total = childrenIds.length;
57
68
  const visualRows = Math.ceil(total / columns);
69
+ // OUTLOOK FIX: Use explicit pixel width for Old Outlook (Word engine)
58
70
  const msoTableWidth = Math.min(cellWidthInPx, 600);
59
- const msoBgColor = !rawBgImageUrl && !isGradient ? (backgroundColor || '') : '';
60
- const msoBgAttr = msoBgColor ? ` bgcolor="${(0, outlookSupport_1.toOutlookBgColor)(msoBgColor)}"` : '';
71
+ // When a background image/gradient is present, the background is applied on an outer
72
+ // wrapper <td> (see bottom of function). The inner grid tables must be clean.
73
+ // When no background, the MSO table gets bgcolor for solid-color sections.
74
+ const msoBgColor = !rawBgImageUrl && !isGradient
75
+ ? (backgroundColor || '')
76
+ : '';
77
+ const msoBgAttr = (0, common_1.buildOutlookBgAttr)(msoBgColor);
61
78
  const msoBgStyle = msoBgColor ? `background-color:${msoBgColor};` : '';
62
- const innerBgTransparent = (rawBgImageUrl || isGradient) ? 'background-color:transparent;' : '';
63
- const nonMsoBgAttr = !rawBgImageUrl && !isGradient && backgroundColor && !divBorderStyle ? ` bgcolor="${(0, outlookSupport_1.toOutlookBgColor)(backgroundColor)}"` : '';
79
+ // Inner tables must be explicitly transparent when outer <td> carries the background.
80
+ const innerBgTransparent = (rawBgImageUrl || isGradient)
81
+ ? 'background-color:transparent;'
82
+ : '';
83
+ const nonMsoBgAttr = !rawBgImageUrl && !isGradient && !divBorderStyle
84
+ ? (0, common_1.buildOutlookBgAttr)(backgroundColor || '')
85
+ : '';
86
+ // When divBorderStyle is set the non-MSO <table> is transparent, so the Grid's
87
+ // backgroundColor must move onto the div wrapper — otherwise it vanishes in modern clients.
88
+ // Skip this for bg-image/gradient blocks; they apply their background via a separate wrapper.
64
89
  const divWrapBg = divBorderStyle && backgroundColor && !rawBgImageUrl && !isGradient
65
90
  ? ` background-color:${backgroundColor};`
66
91
  : '';
67
92
  const divWrapOpen = divBorderStyle ? `<div style="${divBorderStyle}${divWrapBg}">` : '';
68
93
  const divWrapClose = divBorderStyle ? `</div>` : '';
69
- let html = `
70
- <!--[if mso]>
71
- <table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}"${msoBgAttr}
72
- style="border-collapse:collapse;width:${msoTableWidth}px;${msoBgStyle}${innerBgTransparent}"
73
- class="${gridVisibilityClass}">
74
- <![endif]-->
75
- <!--[if !mso]><!-->
76
- <table border="0" cellpadding="0" cellspacing="0" width="100%"
77
- role="presentation"${nonMsoBgAttr}
78
- style="border-collapse:collapse; ${innerBgTransparent}${tableStyles}; max-width:600px;"
79
- class="${gridVisibilityClass}">
80
- <!--<![endif]-->
94
+ let html = `
95
+ <!--[if mso]>
96
+ <table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}"${msoBgAttr}
97
+ style="border-collapse:collapse;width:${msoTableWidth}px;${msoBgStyle}${innerBgTransparent}"
98
+ class="${gridVisibilityClass}">
99
+ <![endif]-->
100
+ <!--[if !mso]><!-->
101
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" align="center"
102
+ role="presentation"${nonMsoBgAttr}
103
+ style="border-collapse:collapse; table-layout:fixed; ${innerBgTransparent}${tableStyles}; max-width:600px;"
104
+ class="${gridVisibilityClass}">
105
+ <!--<![endif]-->
81
106
  `;
82
107
  for (let r = 0; r < visualRows; r++) {
83
108
  html += "<tr>";
109
+ // COUNT visible cells and find last visible column index
84
110
  let visibleCells = 0;
85
111
  let lastVisibleCol = 0;
86
112
  const rowIds = [];
@@ -89,12 +115,14 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
89
115
  const id = childrenIds[idx] ?? null;
90
116
  rowIds.push(id);
91
117
  const child = id ? rootData[id] : null;
92
- if (!child?.data?.props?.hideOnDesktop) {
118
+ const isHidden = child?.data?.props?.hideOnDesktop;
119
+ if (!isHidden) {
93
120
  visibleCells++;
94
121
  lastVisibleCol = c;
95
122
  }
96
123
  }
97
124
  const safeWidth = visibleCells > 0 ? 100 / visibleCells : 100 / columns;
125
+ // Reserve pixel space for spacer tds between visible cells (N-1 gaps for N visible cells)
98
126
  const totalGapPx = columnGap * Math.max(visibleCells - 1, 0);
99
127
  const adjustedTableWidth = Math.max(msoTableWidth - totalGapPx, 1);
100
128
  let totalWidth = 0;
@@ -102,19 +130,24 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
102
130
  for (let c = 0; c < columns; c++) {
103
131
  const id = rowIds[c];
104
132
  let widthPercent = cellWidths[c] ?? safeWidth;
105
- if (widthPercent <= 0 || widthPercent > 100)
133
+ if (widthPercent <= 0 || widthPercent > 100) {
106
134
  widthPercent = safeWidth;
135
+ }
107
136
  cellWidthPercents.push(widthPercent);
108
137
  if (id) {
109
138
  const child = rootData[id];
110
- if (!child?.data?.props?.hideOnDesktop)
139
+ const isHidden = child?.data?.props?.hideOnDesktop;
140
+ if (!isHidden) {
111
141
  totalWidth += widthPercent;
142
+ }
112
143
  }
113
144
  }
114
145
  const scaleFactor = totalWidth > 0 && totalWidth < 100 ? 100 / totalWidth : 1;
115
146
  for (let c = 0; c < columns; c++) {
116
147
  const id = rowIds[c];
117
- let widthPercent = Math.min(cellWidthPercents[c] * scaleFactor, 100);
148
+ let widthPercent = cellWidthPercents[c] * scaleFactor;
149
+ widthPercent = Math.min(widthPercent, 100);
150
+ // Cell pixel width is a share of the gap-adjusted table width
118
151
  const cellWidthPx = Math.round((widthPercent / 100) * adjustedTableWidth);
119
152
  if (id) {
120
153
  const child = rootData[id];
@@ -124,42 +157,73 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
124
157
  const visibilityClass = (0, common_1.getVisibilityClass)(childProps);
125
158
  if (childVisible) {
126
159
  const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth, Boolean(divBorderStyle));
160
+ // bgcolor on the cell <td> for Old Outlook (Word engine).
161
+ // When the cell has no border-radius and the grid has no border, use the grid's
162
+ // backgroundColor as the fallback so the grid background shows through empty cells.
163
+ // Skipped for border-radius cells — bgcolor is rectangular and would bleed through
164
+ // the div's rounded-corner clip in CSS-capable clients (corner squares).
127
165
  const cellBgColor = cellStyle.backgroundColor || '';
128
- const cellBgAttr = cellBgColor ? ` bgcolor="${(0, outlookSupport_1.toOutlookBgColor)(cellBgColor)}"` : '';
129
- html += `
130
- <td
131
- width="${cellWidthPx}"${cellBgAttr}
132
- class="${[responsive ? "stack-column" : "", visibilityClass].filter(Boolean).join(" ")}"
133
- style="width:${cellWidthPx}px;vertical-align:${verticalAlign};word-break:break-word;${styles}"
134
- >
135
- ${childHtml}
166
+ const cellHasBorderRadius = Boolean(cellStyle.borderRadius);
167
+ const canApplyGridBgFallback = !cellHasBorderRadius && !divBorderStyle;
168
+ const effectiveCellBg = cellBgColor || (canApplyGridBgFallback ? msoBgColor : '');
169
+ const cellBgAttr = (0, common_1.buildOutlookBgAttr)(effectiveCellBg);
170
+ html += `
171
+ <td
172
+ width="${Math.max(1, Math.round(widthPercent))}%"${cellBgAttr}
173
+ class="${[responsive ? "stack-column" : "", visibilityClass].filter(Boolean).join(" ")}"
174
+ style="width:${widthPercent}%;vertical-align:${verticalAlign};word-break:break-word;${styles}"
175
+ >
176
+ ${childHtml}
136
177
  </td>`;
178
+ // Spacer td between columns — uses width attribute only (no inline style width) so
179
+ // Outlook mobile (which strips <style>) treats it proportionally, not as a fixed blocker.
180
+ // col-gap-spacer class hides it when columns stack via CSS media query.
137
181
  if (columnGap > 0 && c !== lastVisibleCol) {
138
- html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
182
+ const gapBgAttr = divBorderStyle ? '' : (0, common_1.buildOutlookBgAttr)(msoBgColor);
183
+ html += `<td width="${columnGap}"${gapBgAttr} class="col-gap-spacer" style="font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
139
184
  }
140
185
  }
141
186
  }
142
187
  else {
143
- html += `
144
- <td width="${cellWidthPx}"
145
- ${responsive ? 'class="stack-column"' : ""}
146
- style="width:${cellWidthPx}px;vertical-align:top;">
188
+ const emptyBgAttr = divBorderStyle ? '' : (0, common_1.buildOutlookBgAttr)(msoBgColor);
189
+ html += `
190
+ <td width="${Math.max(1, Math.round(widthPercent))}%"
191
+ ${emptyBgAttr}
192
+ ${responsive ? 'class="stack-column"' : ""}
193
+ style="width:${widthPercent}%;vertical-align:top;">
147
194
  </td>`;
148
195
  if (columnGap > 0 && c !== lastVisibleCol) {
149
- html += `<td width="${columnGap}" style="width:${columnGap}px;font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
196
+ const gapBgAttr = divBorderStyle ? '' : (0, common_1.buildOutlookBgAttr)(msoBgColor);
197
+ html += `<td width="${columnGap}"${gapBgAttr} class="col-gap-spacer" style="font-size:0;line-height:0;padding:0;" aria-hidden="true"></td>`;
150
198
  }
151
199
  }
152
200
  }
153
201
  html += "</tr>";
154
202
  }
155
- html += `
156
- <!--[if mso]>
157
- </table>
158
- <![endif]-->
159
- <!--[if !mso]><!-->
160
- </table>
161
- <!--<![endif]-->
203
+ // Close both MSO and non-MSO tables
204
+ html += `
205
+ <!--[if mso]>
206
+ </table>
207
+ <![endif]-->
208
+ <!--[if !mso]><!-->
209
+ </table>
210
+ <!--<![endif]-->
162
211
  `;
212
+ // ── Background image: canonical multi-client approach ────────────────────
213
+ //
214
+ // Problem: `background-image` on a <table> element is stripped by:
215
+ // • New Outlook Mac / Windows (Chromium-based app)
216
+ // • Outlook.com
217
+ // • Old Outlook (Word engine) — ignores CSS entirely
218
+ //
219
+ // Solution: wrap the grid in an outer <table><tr><td> where the <td> carries
220
+ // the background. Different clients pick it up via different mechanisms:
221
+ //
222
+ // background="" attribute on <td> → Yahoo Mail, older webmail
223
+ // CSS background-image on <td> → Gmail, Apple Mail, new Outlook Mac ✓
224
+ // VML v:rect inside the <td> → Old Outlook (Word engine) ✓
225
+ //
226
+ // The inner grid tables have NO background so the outer <td> bg shows through.
163
227
  if (rawBgImageUrl || isGradient) {
164
228
  const vmlFill = isGradient
165
229
  ? (() => {
@@ -169,89 +233,99 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
169
233
  return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
170
234
  })()
171
235
  : `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
172
- html = `
173
- <table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
174
- style="border-collapse:collapse;width:100%;max-width:${msoTableWidth}px;">
175
- <tr>
176
- <td width="100%" bgcolor="${fallbackBgColor}" valign="top"
177
- ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
178
- style="
179
- width:100%;max-width:${msoTableWidth}px;
180
- background-color:${fallbackBgColor};
181
- ${isGradient ? `background:${effectiveGradient};` : `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`}
182
- ">
183
-
184
- <!--[if gte mso 9]>
185
- <v:rect xmlns:v="urn:schemas-microsoft-com:vml"
186
- fill="true" stroke="false"
187
- style="width:${msoTableWidth}px;">
188
- ${vmlFill}
189
- <v:textbox inset="0,0,0,0">
190
- <![endif]-->
191
-
192
- ${html}
193
-
194
- <!--[if gte mso 9]>
195
- </v:textbox>
196
- </v:rect>
197
- <![endif]-->
198
-
199
- </td>
200
- </tr>
236
+ html = `
237
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
238
+ style="border-collapse:collapse;table-layout:fixed;width:100%;max-width:${msoTableWidth}px;">
239
+ <tr>
240
+ <td width="100%"${(0, common_1.buildOutlookBgAttr)(fallbackBgColor)} valign="top"
241
+ ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
242
+ style="
243
+ width:100%;max-width:${msoTableWidth}px;
244
+ background-color:${fallbackBgColor};
245
+ ${isGradient ? `background:${effectiveGradient};` : `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`}
246
+ ">
247
+
248
+ <!--[if gte mso 9]>
249
+ <v:rect xmlns:v="urn:schemas-microsoft-com:vml"
250
+ fill="true" stroke="false"
251
+ style="width:${msoTableWidth}px;">
252
+ ${vmlFill}
253
+ <v:textbox inset="0,0,0,0">
254
+ <![endif]-->
255
+
256
+ ${html}
257
+
258
+ <!--[if gte mso 9]>
259
+ </v:textbox>
260
+ </v:rect>
261
+ <![endif]-->
262
+
263
+ </td>
264
+ </tr>
201
265
  </table>`;
202
266
  }
267
+ // Wrap the entire grid (including any bg-image outer table) in a div when the block
268
+ // has border/radius. An unconditional <div> is used — not gated behind <!--[if !mso]>-->
269
+ // — so Gmail compose paste renders the border-radius reliably. Old Outlook ignores
270
+ // border-radius on <div> but still shows the rectangular border; new Outlook works fully.
203
271
  if (divBorderStyle)
204
272
  html = `${divWrapOpen}${html}${divWrapClose}`;
205
273
  return html;
206
274
  }
207
275
  async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx, parentGridHasBorder = false) {
208
276
  const { style = {}, childrenIds = [], props = {} } = blockData.data;
277
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
278
+ // Extract border + radius from style so they move to the div wrapper (not the <td>).
279
+ // Gmail strips border-radius from <td> but honours it on <div>. By putting border and
280
+ // radius on the same unconditional <div>, the rounded card border renders in all clients.
281
+ // The <td> keeps bgcolor (via attribute) for Old Outlook background fallback.
209
282
  const { borderRadius: cellBorderRadius, borderWidth: cellBorderWidth, borderStyle: cellBorderStyleProp, borderColor: cellBorderColor, border: cellBorderShorthand, ...styleWithoutBorder } = style;
283
+ // backgroundColor must stay on the div wrapper (not the <td>) in two cases:
284
+ // 1. Cell has its own border-radius — the div's overflow:hidden clips the background.
285
+ // 2. Parent grid has a border div (divBorderStyle) — the grid's overflow:hidden clips it.
286
+ // In both cases, the rectangular <td> background bleeds through rounded corners if kept
287
+ // in CSS, creating visible corner squares. The bgcolor attribute stays for Outlook fallback.
210
288
  const stripBgFromTd = Boolean(cellBorderRadius) || parentGridHasBorder;
211
- // Omit backgroundColor entirely rather than setting 'transparent' — an explicit
212
- // background-color:transparent can defeat the bgcolor HTML attribute in Old Outlook
213
- // (Word engine), leaving the cell with no background at all.
289
+ // When stripping bg from <td>, omit backgroundColor entirely rather than setting 'transparent'.
290
+ // An explicit background-color:transparent in CSS can defeat the bgcolor HTML attribute in
291
+ // Old Outlook (Word engine), leaving the cell with no background at all.
214
292
  const styleForTd = stripBgFromTd
215
293
  ? { ...styleWithoutBorder, backgroundColor: undefined }
216
294
  : styleWithoutBorder;
217
- const styles = (0, buildStyles_1.buildStyles)(styleForTd, { perChanges: [], pxChanges: buildStyles_1.allPxAttributes });
295
+ // Outlook treats <td width="..."> as content width and then adds horizontal padding,
296
+ // which can push the total row width beyond the parent table and shift columns.
297
+ // Keep sizing styles on <td>, but move padding into the inner div wrapper.
298
+ const styleForTdWithoutPadding = { ...styleForTd, padding: undefined };
299
+ const styles = (0, buildStyles_1.buildStyles)(styleForTdWithoutPadding, {
300
+ perChanges: [],
301
+ pxChanges: buildStyles_1.allPxAttributes,
302
+ });
218
303
  const parts = [];
304
+ // OUTLOOK FIX: Calculate the actual cell width in pixels based on percentage
305
+ // If parent is 600px and cell is 50%, cell width should be 300px, not 600px
219
306
  const cellWidthPx = Math.round((cellWidthPercent / 100) * parentCellWidthPx);
220
- const rawPad = styleWithoutBorder?.padding;
221
- let cellPadLeft = 0;
222
- let cellPadRight = 0;
223
- if (rawPad && typeof rawPad === 'object') {
224
- cellPadLeft = Number.isFinite(rawPad.left) ? rawPad.left : 0;
225
- cellPadRight = Number.isFinite(rawPad.right) ? rawPad.right : 0;
226
- }
227
- else if (typeof rawPad === 'string') {
228
- const parts = rawPad.trim().split(/\s+/).map(v => parseFloat(v) || 0);
229
- if (parts.length >= 4) {
230
- cellPadRight = parts[1];
231
- cellPadLeft = parts[3];
232
- }
233
- else if (parts.length === 3) {
234
- cellPadRight = parts[1];
235
- cellPadLeft = parts[1];
236
- }
237
- else if (parts.length === 2) {
238
- cellPadRight = parts[1];
239
- cellPadLeft = parts[1];
240
- }
241
- else if (parts.length === 1) {
242
- cellPadRight = parts[0];
243
- cellPadLeft = parts[0];
244
- }
245
- }
307
+ // Subtract the cell's own padding so children receive the actual content-area width.
308
+ // Old Outlook honours explicit img/table width attributes — if a child is sized to the
309
+ // full column width (ignoring padding) it overflows and expands the column.
310
+ const cellPad = styleWithoutBorder?.padding || {};
311
+ const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
312
+ const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
246
313
  const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
314
+ // OUTLOOK FIX: Ensure cell width is reasonable and capped at 600px
247
315
  const safeCellWidthPx = Math.min(contentWidthPx, 600);
248
316
  for (const childId of childrenIds) {
249
317
  const child = rootData[childId];
250
- if (child)
318
+ if (child) {
251
319
  parts.push(await (0, jsonToHTML_1.convertToHtml)(child, rootData, safeCellWidthPx));
320
+ }
252
321
  }
253
322
  const borderRadius = cellBorderRadius || 0;
254
323
  const bgColor = styleWithoutBorder?.backgroundColor || "transparent";
324
+ // Build border CSS for the div wrapper.
325
+ // When the parent grid already has a divBorderStyle wrapper (border + border-radius +
326
+ // overflow:hidden), the cell must NOT duplicate the same border/radius — that causes
327
+ // two concentric borders of the same colour (double-border). The grid's wrapper div
328
+ // already provides the visual container; the cell div only needs background-color.
255
329
  const cellDivBorderParts = [];
256
330
  if (!parentGridHasBorder) {
257
331
  if (borderRadius)
@@ -267,9 +341,21 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
267
341
  }
268
342
  }
269
343
  const cellDivBorderStyle = cellDivBorderParts.join(' ');
344
+ // Unconditional div — visible to all clients (Gmail, Outlook new/old, Apple Mail).
345
+ // background-color on the div covers modern clients; bgcolor on <td> covers Old Outlook.
270
346
  const divStyleParts = [`background-color:${bgColor};`];
347
+ const formatPad = (value) => (typeof value === 'number' ? `${value}px` : (value || '0'));
348
+ const cellPadTop = formatPad(cellPad.top);
349
+ const cellPadBottom = formatPad(cellPad.bottom);
350
+ const cellPadLeftCss = formatPad(cellPad.left);
351
+ const cellPadRightCss = formatPad(cellPad.right);
352
+ divStyleParts.push(`padding:${cellPadTop} ${cellPadRightCss} ${cellPadBottom} ${cellPadLeftCss};`);
271
353
  if (cellDivBorderStyle)
272
354
  divStyleParts.push(cellDivBorderStyle);
273
- const wrapped = `<div style="${divStyleParts.join(' ')}">${parts.join("")}</div>`;
274
- return { html: wrapped, styles };
355
+ const divStyleStr = divStyleParts.join(' ');
356
+ const wrapped = `<div style="${divStyleStr}">${parts.join("")}</div>`;
357
+ return {
358
+ html: wrapped,
359
+ styles,
360
+ };
275
361
  }
@@ -1 +1 @@
1
- {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/image.ts"],"names":[],"mappings":"AAIA,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM;;;;;GAiB1F;AAED,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBA2D5E"}
1
+ {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/image.ts"],"names":[],"mappings":"AAIA,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,MAAM;;;;;GAoB5B;AAED,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAiH5E"}
@@ -29,27 +29,65 @@ async function convertImageBlock(blockData, cellWidthInPx) {
29
29
  const { altText, imageUrl, navigateToUrl } = props;
30
30
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
31
31
  const { width, height, objectFit, borderRadius, borderWidth, borderColor, borderStyle, ...containerStyle } = style;
32
- const containerStyles = (0, buildStyles_1.buildStyles)({ ...containerStyle }, { perChanges: [], pxChanges: buildStyles_1.addPxToAttributes });
32
+ // Add border styles to container for fallback clients
33
+ const containerStyles = (0, buildStyles_1.buildStyles)({
34
+ ...containerStyle,
35
+ }, { perChanges: [], pxChanges: buildStyles_1.addPxToAttributes });
36
+ // OUTLOOK FIX: Ensure cellWidthInPx never exceeds 600px
33
37
  const safeCellWidth = Math.min(cellWidthInPx, 600);
38
+ // Parse width percentage (default 100%)
34
39
  const widthPercent = typeof width === "string" && width.includes("%")
35
40
  ? parseInt(width.replace("%", ""))
36
- : typeof width === "number" ? width : 100;
41
+ : typeof width === "number"
42
+ ? width
43
+ : 100;
44
+ // OUTLOOK FIX: Calculate inner container width based on safe cell width
37
45
  const paddingLeft = style?.padding?.left || 0;
38
46
  const paddingRight = style?.padding?.right || 0;
39
47
  const availableWidth = safeCellWidth - paddingLeft - paddingRight;
40
48
  const innerContainerWidth = Math.round((widthPercent / 100) * availableWidth);
49
+ // Get image dimensions and calculate scaled sizes
41
50
  const { originalWidth, originalHeight, scaledWidth, scaledHeight } = await computeScaledDimensions(imageUrl, innerContainerWidth);
51
+ // OUTLOOK FIX: For Outlook, we need exact pixel dimensions
52
+ // Calculate final dimensions that respect both original size and container
42
53
  const finalWidth = Math.min(scaledWidth, innerContainerWidth, originalWidth);
43
54
  const finalHeight = Math.round((finalWidth / originalWidth) * originalHeight);
44
- const imageTagStyles = (0, buildStyles_1.buildStyles)({ borderStyle, borderRadius, borderColor, borderWidth }, { perChanges: [], pxChanges: buildStyles_1.addPxToAttributes });
55
+ // Build image styles for modern email clients (non-Outlook)
56
+ const imageTagStyles = (0, buildStyles_1.buildStyles)({
57
+ borderStyle,
58
+ borderRadius: borderRadius,
59
+ borderColor,
60
+ borderWidth,
61
+ }, {
62
+ perChanges: [],
63
+ pxChanges: buildStyles_1.addPxToAttributes,
64
+ });
65
+ // OUTLOOK RENDERING CONTRACT
66
+ // width/height HTML attributes → Old Outlook Word engine (CSS is lower priority in Word)
67
+ // width/height inline CSS → New Outlook, OWA, and all CSS-capable clients
68
+ // max-width:100% → prevents overflow in any client that ignores pixel constraints
69
+ // height:auto !important → @media query (convertJsonToHtml.ts) overrides this on mobile
70
+ // so proportional scaling is preserved on small screens
71
+ // -ms-interpolation-mode → inline fallback; the <style> block is stripped by
72
+ // Outlook during forward/reply MIME rewriting
45
73
  const imageElement = `<img src="${imageUrl}" alt="${altText || "Image"}" border="0" width="${finalWidth}" height="${finalHeight}" style="${imageTagStyles}; display:block; width:${finalWidth}px; height:${finalHeight}px; max-width:100%; line-height:0; -ms-interpolation-mode:bicubic;" />`;
46
74
  const percentWidth = typeof width === "string" && width.endsWith("%")
47
75
  ? width
48
- : typeof width === "number" ? `${width}%` : "100%";
76
+ : typeof width === "number"
77
+ ? `${width}%`
78
+ : "100%";
79
+ // Non-MSO wrapper: display:block removes the phantom inline-baseline gap that
80
+ // display:inline-block creates in Gmail / Apple Mail / Yahoo between images.
81
+ // margin handles alignment since text-align won't move block elements.
49
82
  const imgTextAlign = containerStyle.textAlign || "left";
50
83
  const imgMargin = imgTextAlign === "center" ? "margin:0 auto;" :
51
84
  imgTextAlign === "right" ? "margin-left:auto; margin-right:0;" : "";
85
+ // OUTLOOK FIX: Use finalWidth (the actual displayed size) as max-width so the div
86
+ // doesn't claim more space than the image occupies. originalWidth is the natural
87
+ // image size (e.g. 636px for the Beefree logo rendered at 35px) which was
88
+ // misleadingly large and could confuse some rendering engines.
52
89
  const nonMsoWrapper = `<div style="display:block; width:${percentWidth}; max-width:${finalWidth}px; line-height:0; font-size:0; ${imgMargin}">${imageElement}</div>`;
90
+ // OUTLOOK FIX: Generate VML with corrected dimensions
53
91
  const outlookImage = await (0, outlookSupport_1.appendOutlookForImage)(nonMsoWrapper, safeCellWidth, innerContainerWidth, imageUrl, style, finalWidth, finalHeight);
54
92
  const imageContent = (0, outlookSupport_1.appendOutlookSupport)(outlookImage, containerStyles, visibilityClass, safeCellWidth);
55
93
  return navigateToUrl