email-builder-utils 1.1.35 → 1.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,85 +12,85 @@ const convertJsonToHtml = async (jsonData) => {
12
12
  }
13
13
  const { fontFamily, canvasColor, textColor, padding = {}, borderColor, borderRadius, borderWidth, borderStyle, } = rootData.style || {};
14
14
  const { top = 0, right = 0, bottom = 0, left = 0 } = padding;
15
- const rawHtml = `<!DOCTYPE html>
16
- <html lang="en">
17
- <head>
18
- <meta charset="UTF-8" />
19
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
- <meta name="x-apple-disable-message-reformatting" />
21
- <style>
22
- .responsive-table {
23
- width: 100%;
24
- max-width: 600px;
25
- }
26
- @media only screen and (max-width: 600px) {
27
- .responsive-table {
28
- width: 100% !important;
29
- }
30
- .stack-column,
31
- .stack-column td {
32
- display: block !important;
33
- width: 100% !important;
34
- max-width: 100% !important;
35
- }
36
- }
37
- .hide-mobile {
38
- display: block !important;
39
- mso-hide: all !important; /* Hide in Outlook */
40
- }
41
-
42
- .hide-desktop {
43
- display: block !important;
44
- mso-hide: all !important; /* Hide in Outlook */
45
- }
46
-
47
- @media only screen and (max-width: 600px) {
48
- .hide-mobile {
49
- display: none !important;
50
- max-height: 0 !important;
51
- overflow: hidden !important;
52
- mso-hide: all !important;
53
- }
54
- }
55
-
56
- @media only screen and (min-width: 601px) {
57
- .hide-desktop {
58
- display: none !important;
59
- max-height: 0 !important;
60
- overflow: hidden !important;
61
- mso-hide: all !important;
62
- }
63
- }
64
-
65
- </style>
66
- </head>
67
- <body>
68
- <center>
69
- <table
70
- class="responsive-table"
71
- bgcolor="${canvasColor}"
72
- style="
73
- font-family: ${fontFamily};
74
- margin: 0 auto;
75
- table-layout:fixed;
76
- width:600px;
77
- max-width:600px;
78
- background-color: ${canvasColor};
79
- color: ${textColor};
80
- padding: ${top}px ${right}px ${bottom}px ${left}px;
81
- border: ${borderWidth}px ${borderStyle} ${borderColor};
82
- border-radius: ${borderRadius}px; "
83
- >
84
- <tbody>
85
- <tr>
86
- <td style="padding: 0;">
87
- ${blocksHtml.join("")}
88
- </td>
89
- </tr>
90
- </tbody>
91
- </table>
92
- </center>
93
- </body>
15
+ const rawHtml = `<!DOCTYPE html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="UTF-8" />
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
+ <meta name="x-apple-disable-message-reformatting" />
21
+ <style>
22
+ .responsive-table {
23
+ width: 100%;
24
+ max-width: 600px;
25
+ }
26
+ @media only screen and (max-width: 600px) {
27
+ .responsive-table {
28
+ width: 100% !important;
29
+ }
30
+ .stack-column,
31
+ .stack-column td {
32
+ display: block !important;
33
+ width: 100% !important;
34
+ max-width: 100% !important;
35
+ }
36
+ }
37
+ .hide-mobile {
38
+ display: block !important;
39
+ mso-hide: all !important; /* Hide in Outlook */
40
+ }
41
+
42
+ .hide-desktop {
43
+ display: block !important;
44
+ mso-hide: all !important; /* Hide in Outlook */
45
+ }
46
+
47
+ @media only screen and (max-width: 600px) {
48
+ .hide-mobile {
49
+ display: none !important;
50
+ max-height: 0 !important;
51
+ overflow: hidden !important;
52
+ mso-hide: all !important;
53
+ }
54
+ }
55
+
56
+ @media only screen and (min-width: 601px) {
57
+ .hide-desktop {
58
+ display: none !important;
59
+ max-height: 0 !important;
60
+ overflow: hidden !important;
61
+ mso-hide: all !important;
62
+ }
63
+ }
64
+
65
+ </style>
66
+ </head>
67
+ <body>
68
+ <center>
69
+ <table
70
+ class="responsive-table"
71
+ bgcolor="${canvasColor}"
72
+ style="
73
+ font-family: ${fontFamily};
74
+ margin: 0 auto;
75
+ table-layout:fixed;
76
+ width:600px;
77
+ max-width:600px;
78
+ background-color: ${canvasColor};
79
+ color: ${textColor};
80
+ padding: ${top}px ${right}px ${bottom}px ${left}px;
81
+ border: ${borderWidth}px ${borderStyle} ${borderColor};
82
+ border-radius: ${borderRadius}px; "
83
+ >
84
+ <tbody>
85
+ <tr>
86
+ <td style="padding: 0;">
87
+ ${blocksHtml.join("")}
88
+ </td>
89
+ </tr>
90
+ </tbody>
91
+ </table>
92
+ </center>
93
+ </body>
94
94
  </html>`;
95
95
  return rawHtml;
96
96
  };
@@ -1 +1 @@
1
- {"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAGrC,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,cAAc,CAAC;QACtB,KAAK,EAAE,GAAG,CAAC;QACX,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;KAC7B,CAAC;CACH;AAYD,eAAO,MAAM,gBAAgB,kDAAkD,CAAC;AAyFhF,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAwBtB;AAmkBD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAyL5E"}
1
+ {"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAGrC,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,cAAc,CAAC;QACtB,KAAK,EAAE,GAAG,CAAC;QACX,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;KAC7B,CAAC;CACH;AAYD,eAAO,MAAM,gBAAgB,kDAAkD,CAAC;AAwFhF,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAwBtB;AA4qBD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAyL5E"}
@@ -127,14 +127,17 @@ function appendOutlookSupport(content, contentStyle, className) {
127
127
  const visibilityClass = className || "";
128
128
  const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
129
129
  if (shouldHideInOutlook) {
130
- return `
131
- <!--[if !mso]><!-->
132
- <table width="100%" style="${exports.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
133
- <!--<![endif]-->
130
+ return `
131
+ <!--[if !mso]><!-->
132
+ <table width="100%" style="${exports.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
133
+ <!--<![endif]-->
134
134
  `;
135
135
  }
136
- return `
137
- <table width="100%" style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
136
+ // Use a single table that works for both MSO and non-MSO
137
+ // The content inside already has its own MSO conditional comments
138
+ // width="100%" works for modern clients, Outlook will use the td width from nested tables
139
+ return `
140
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}"><tr><td width="100%" style="${contentStyle}">${content}</td></tr></table>
138
141
  `;
139
142
  }
140
143
  function convertDividerBlockToHtml(blockData) {
@@ -153,21 +156,21 @@ function convertDividerBlockToHtml(blockData) {
153
156
  ]
154
157
  .filter(Boolean)
155
158
  .join(" ");
156
- const dividerContent = `
157
- <table
158
- width="${dividerWidth}%"
159
- cellpadding="0"
160
- cellspacing="0"
161
- >
162
- <tr>
163
- <td
164
- height="${thickness}"
165
- style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth};"
166
- >
167
- &nbsp;
168
- </td>
169
- </tr>
170
- </table>
159
+ const dividerContent = `
160
+ <table
161
+ width="${dividerWidth}%"
162
+ cellpadding="0"
163
+ cellspacing="0"
164
+ >
165
+ <tr>
166
+ <td
167
+ height="${thickness}"
168
+ style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth};"
169
+ >
170
+ &nbsp;
171
+ </td>
172
+ </tr>
173
+ </table>
171
174
  `;
172
175
  return appendOutlookSupport(dividerContent, convertedStyle, visibilityClass);
173
176
  }
@@ -231,38 +234,79 @@ function convertTextBlock(blockData) {
231
234
  ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit;text-decoration:none;cursor:pointer;">${textContent}</a>`
232
235
  : textContent;
233
236
  }
234
- async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}) {
235
- const image = await jimp_1.Jimp.read(imageUrl);
236
- const originalWidth = image.bitmap.width;
237
- const originalHeight = image.bitmap.height;
238
- const widthScalingFactor = Math.min(outerContainerWidth / originalWidth, innerContainerWidth / originalWidth);
239
- const scaledWidth = Math.round(originalWidth * widthScalingFactor);
240
- const scaledHeight = Math.round(originalHeight * widthScalingFactor);
237
+ async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}, finalWidth, finalHeight) {
238
+ // OUTLOOK FIX: Use provided dimensions or calculate from image
239
+ let vmlWidth;
240
+ let vmlHeight;
241
+ if (finalWidth && finalHeight) {
242
+ // Use pre-calculated dimensions (preferred for accuracy)
243
+ vmlWidth = finalWidth;
244
+ vmlHeight = finalHeight;
245
+ }
246
+ else {
247
+ // Fallback: calculate from image
248
+ const image = await jimp_1.Jimp.read(imageUrl);
249
+ const originalWidth = image.bitmap.width;
250
+ const originalHeight = image.bitmap.height;
251
+ const widthScalingFactor = Math.min(outerContainerWidth / originalWidth, innerContainerWidth / originalWidth, 1 // Never scale up
252
+ );
253
+ vmlWidth = Math.round(originalWidth * widthScalingFactor);
254
+ vmlHeight = Math.round(originalHeight * widthScalingFactor);
255
+ }
241
256
  const borderWidth = parseInt(style?.borderWidth) || 0;
242
257
  const borderColor = style?.borderColor || "transparent";
243
258
  const borderRadius = parseInt(style?.borderRadius) || 0;
244
259
  const useRoundRect = borderRadius > 0;
245
260
  const arcsize = useRoundRect
246
- ? Math.min(borderRadius / scaledHeight, 1).toFixed(2)
261
+ ? Math.min(borderRadius / vmlHeight, 1).toFixed(2)
247
262
  : "";
248
263
  const borderAttributes = borderWidth > 0
249
264
  ? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"`
250
265
  : `stroked="false"`;
251
- const outlookImage = `<!--[if mso]>
252
- <v:${useRoundRect ? "roundrect" : "rect"} xmlns:v="urn:schemas-microsoft-com:vml"
253
- style="width:${scaledWidth}px;height:${scaledHeight}px;"
254
- ${borderAttributes}
255
- ${useRoundRect ? `arcsize="${arcsize}"` : ""}
256
- fill="true" fillcolor="none">
257
- <v:fill src="${imageUrl}" type="frame" />
258
- <v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>
259
- </v:${useRoundRect ? "roundrect" : "rect"}>
266
+ // OUTLOOK FIX: For Outlook 2019+ (version 2512), VML type="frame" causes stretching
267
+ // Solution: Use simple IMG tag with fixed dimensions for Outlook, only use VML for border radius
268
+ let outlookImage;
269
+ if (useRoundRect && borderRadius > 0) {
270
+ // Use VML for border radius - wrap in table to constrain width for Old Outlook (Word engine)
271
+ // Use aspect="atmost" to prevent image from stretching beyond its bounds
272
+ outlookImage = `<!--[if mso]>
273
+ <table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
274
+ <tr>
275
+ <td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
276
+ <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
277
+ style="width:${vmlWidth}px;height:${vmlHeight}px;"
278
+ ${borderAttributes}
279
+ arcsize="${arcsize}"
280
+ fill="true" fillcolor="none">
281
+ <v:fill src="${imageUrl}" type="tile" aspect="atmost" />
282
+ <v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>
283
+ </v:roundrect>
284
+ </td>
285
+ </tr>
286
+ </table>
287
+ <![endif]-->`;
288
+ }
289
+ else {
290
+ // For images without border radius, wrap in a table with explicit width for Old Outlook (Word engine)
291
+ // This prevents stretching/overflow in Outlook 2007-2019 and Outlook Classic
292
+ const borderStyleAttr = borderWidth > 0
293
+ ? `border: ${borderWidth}px solid ${borderColor};`
294
+ : '';
295
+ outlookImage = `<!--[if mso]>
296
+ <table border="0" cellpadding="0" cellspacing="0" width="${vmlWidth}" style="width:${vmlWidth}px;">
297
+ <tr>
298
+ <td align="center" valign="top" width="${vmlWidth}" style="width:${vmlWidth}px;">
299
+ <img src="${imageUrl}" alt="Image" border="0" width="${vmlWidth}" height="${vmlHeight}" style="display:block; width:${vmlWidth}px; height:${vmlHeight}px; max-width:${vmlWidth}px; ${borderStyleAttr}" />
300
+ </td>
301
+ </tr>
302
+ </table>
260
303
  <![endif]-->`;
261
- return `
262
- ${outlookImage}
263
- <!--[if !mso]><!-->
264
- ${content}
265
- <!--<![endif]-->
304
+ }
305
+ return `
306
+ ${outlookImage}
307
+ <!--[if !mso]><!-->
308
+ ${content}
309
+ <!--<![endif]-->
266
310
  `;
267
311
  }
268
312
  async function computeScaledDimensions(imageUrl, maxContainerWidthPx) {
@@ -279,42 +323,52 @@ async function convertImageBlock(blockData, cellWidthInPx) {
279
323
  const { altText, imageUrl, navigateToUrl } = props;
280
324
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
281
325
  const { width, height, objectFit, borderRadius, borderWidth, borderColor, borderStyle, ...containerStyle } = style;
282
- // Ensure border styles are applied only to the container, not the image
283
- const imageStyle = {
284
- width,
285
- height,
286
- objectFit,
287
- borderStyle,
288
- borderRadius: borderRadius,
289
- borderColor,
290
- };
291
326
  // Add border styles to container for fallback clients
292
327
  const containerStyles = buildStyles({
293
328
  ...containerStyle,
294
329
  }, { perChanges: [], pxChanges: addPxToAttributes });
295
- const innerContainerWidth = (((typeof width === "string" ? parseInt(width.replace("%", "")) : width) ||
296
- 100) /
297
- 100) *
298
- (cellWidthInPx -
299
- (style?.padding?.left || 0) -
300
- (style?.padding?.right || 0));
330
+ // OUTLOOK FIX: Ensure cellWidthInPx never exceeds 600px
331
+ const safeCellWidth = Math.min(cellWidthInPx, 600);
332
+ // Parse width percentage (default 100%)
333
+ const widthPercent = typeof width === "string" && width.includes("%")
334
+ ? parseInt(width.replace("%", ""))
335
+ : typeof width === "number"
336
+ ? width
337
+ : 100;
338
+ // OUTLOOK FIX: Calculate inner container width based on safe cell width
339
+ const paddingLeft = style?.padding?.left || 0;
340
+ const paddingRight = style?.padding?.right || 0;
341
+ const availableWidth = safeCellWidth - paddingLeft - paddingRight;
342
+ const innerContainerWidth = Math.round((widthPercent / 100) * availableWidth);
343
+ // Get image dimensions and calculate scaled sizes
301
344
  const { originalWidth, originalHeight, scaledWidth, scaledHeight } = await computeScaledDimensions(imageUrl, innerContainerWidth);
345
+ // OUTLOOK FIX: For Outlook, we need exact pixel dimensions
346
+ // Calculate final dimensions that respect both original size and container
347
+ const finalWidth = Math.min(scaledWidth, innerContainerWidth, originalWidth);
348
+ const finalHeight = Math.round((finalWidth / originalWidth) * originalHeight);
349
+ // Build image styles for modern email clients (non-Outlook)
302
350
  const imageTagStyles = buildStyles({
303
- maxWidth: `${originalWidth}px`, // Limit to original size
304
- maxHeight: `${originalHeight}px`,
305
- ...imageStyle,
351
+ borderStyle,
352
+ borderRadius: borderRadius,
353
+ borderColor,
354
+ borderWidth,
306
355
  }, {
307
- perChanges: addPxOrPerToAttributes,
356
+ perChanges: [],
308
357
  pxChanges: addPxToAttributes,
309
358
  });
310
- const imageElement = `<img src="${imageUrl}" alt="${altText}" width="${scaledWidth}" height="${scaledHeight}" style="${imageTagStyles}; width:100%; height:auto; max-width:${originalWidth}px; max-height:${originalHeight}px;" />`;
359
+ // OUTLOOK FIX: Image element with explicit dimensions
360
+ // Outlook will use width/height attributes, modern clients use CSS
361
+ // Use max-width instead of width:100% to prevent stretching
362
+ const imageElement = `<img src="${imageUrl}" alt="${altText || "Image"}" border="0" width="${finalWidth}" height="${finalHeight}" style="${imageTagStyles}; display:block; max-width:100%; height:auto;" />`;
311
363
  const percentWidth = typeof width === "string" && width.endsWith("%")
312
364
  ? width
313
365
  : typeof width === "number"
314
366
  ? `${width}%`
315
367
  : "100%";
368
+ // Non-MSO wrapper for responsive behavior
316
369
  const nonMsoWrapper = `<div style="display:inline-block; width:${percentWidth}; max-width:${originalWidth}px;">${imageElement}</div>`;
317
- const outlookImage = await appendOutlookForImage(nonMsoWrapper, cellWidthInPx, innerContainerWidth, imageUrl, style);
370
+ // OUTLOOK FIX: Generate VML with corrected dimensions
371
+ const outlookImage = await appendOutlookForImage(nonMsoWrapper, safeCellWidth, innerContainerWidth, imageUrl, style, finalWidth, finalHeight);
318
372
  const imageContent = appendOutlookSupport(outlookImage, containerStyles, visibilityClass);
319
373
  return navigateToUrl
320
374
  ? `<a href="${navigateToUrl}" target="_blank" rel="noreferrer noopener" style="display:block;">${imageContent}</a>`
@@ -325,23 +379,23 @@ function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
325
379
  const borderAttributes = borderWidth > 0
326
380
  ? `strokeweight="${borderWidth}px" strokecolor="${borderColor}"`
327
381
  : `stroked="false"`;
328
- return `
329
- <!--[if mso]>
330
- <v:${borderRadius ? "roundrect" : "rect"} xmlns:v="urn:schemas-microsoft-com:vml" href="${navigateToUrl}"
331
- style="height:${height}px;v-text-anchor:middle;width:${width}px;"
332
- arcsize="${borderRadius / height}" ${borderAttributes}
333
- fillcolor="${buttonColor}">
334
- <w:anchorlock/>
335
- <v:textbox inset="${buttonPadding.top}px,${buttonPadding.left}px,${buttonPadding.bottom}px,${buttonPadding.right}px">
336
- <center style="font-family:${fontFamily};font-size:${fontSize}px;font-weight:${fontWeight};color:${color};">
337
- ${text}
338
- </center>
339
- </v:textbox>
340
- </v:${borderRadius ? "roundrect" : "rect"}>
341
- <![endif]-->
342
- <!--[if !mso]><!-->
343
- ${content}
344
- <!--<![endif]-->
382
+ return `
383
+ <!--[if mso]>
384
+ <v:${borderRadius ? "roundrect" : "rect"} xmlns:v="urn:schemas-microsoft-com:vml" href="${navigateToUrl}"
385
+ style="height:${height}px;v-text-anchor:middle;width:${width}px;"
386
+ arcsize="${borderRadius / height}" ${borderAttributes}
387
+ fillcolor="${buttonColor}">
388
+ <w:anchorlock/>
389
+ <v:textbox inset="${buttonPadding.top}px,${buttonPadding.left}px,${buttonPadding.bottom}px,${buttonPadding.right}px">
390
+ <center style="font-family:${fontFamily};font-size:${fontSize}px;font-weight:${fontWeight};color:${color};">
391
+ ${text}
392
+ </center>
393
+ </v:textbox>
394
+ </v:${borderRadius ? "roundrect" : "rect"}>
395
+ <![endif]-->
396
+ <!--[if !mso]><!-->
397
+ ${content}
398
+ <!--<![endif]-->
345
399
  `;
346
400
  }
347
401
  function convertButtonBlock(blockData) {
@@ -387,16 +441,20 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
387
441
  });
388
442
  const total = childrenIds.length;
389
443
  const visualRows = Math.ceil(total / columns);
390
- let html = `
391
- <!--[if mso]>
392
- <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%"
393
- style="border-collapse:separate;border-spacing:${columnGap}px;"
394
- class="${gridVisibilityClass}">
395
- <![endif]-->
396
- <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%"
397
- role="presentation"
398
- style="border-collapse:separate;border-spacing:${columnGap}px; ${tableStyles}; max-width:600px;"
399
- class="${gridVisibilityClass}">
444
+ // OUTLOOK FIX: Use explicit pixel width for Old Outlook (Word engine)
445
+ const msoTableWidth = Math.min(cellWidthInPx, 600);
446
+ let html = `
447
+ <!--[if mso]>
448
+ <table border="0" cellpadding="0" cellspacing="${columnGap}" width="${msoTableWidth}"
449
+ style="border-collapse:separate;border-spacing:${columnGap}px;width:${msoTableWidth}px;"
450
+ class="${gridVisibilityClass}">
451
+ <![endif]-->
452
+ <!--[if !mso]><!-->
453
+ <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%"
454
+ role="presentation"
455
+ style="border-collapse:separate;border-spacing:${columnGap}px; ${tableStyles}; max-width:600px;"
456
+ class="${gridVisibilityClass}">
457
+ <!--<![endif]-->
400
458
  `;
401
459
  for (let r = 0; r < visualRows; r++) {
402
460
  html += "<tr>";
@@ -414,18 +472,38 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
414
472
  visibleCells++;
415
473
  }
416
474
  }
417
- // FIX: fallback safe-width
418
- const safeWidth = visibleCells > 0 ? Math.min(100 / visibleCells, 50) : Math.min(100 / columns, 50);
475
+ // OUTLOOK FIX: Calculate safe width based on visible cells
476
+ // If we have visible cells, distribute 100% width evenly
477
+ const safeWidth = visibleCells > 0 ? 100 / visibleCells : 100 / columns;
478
+ // OUTLOOK FIX: Calculate total width used by all cells
479
+ let totalWidth = 0;
480
+ const cellWidthPercents = [];
419
481
  for (let c = 0; c < columns; c++) {
420
- const idx = r * columns + c;
421
482
  const id = rowIds[c];
422
483
  let widthPercent = cellWidths[c] ?? safeWidth;
423
- // FIX: never exceed reasonable width
484
+ // Validate width
424
485
  if (widthPercent <= 0 || widthPercent > 100) {
425
486
  widthPercent = safeWidth;
426
487
  }
427
- // FIX: Cap width percent to prevent overflow in Outlook
428
- widthPercent = Math.min(widthPercent, 50);
488
+ cellWidthPercents.push(widthPercent);
489
+ if (id) {
490
+ const child = rootData[id];
491
+ const isHidden = child?.data?.props?.hideOnDesktop;
492
+ if (!isHidden) {
493
+ totalWidth += widthPercent;
494
+ }
495
+ }
496
+ }
497
+ // OUTLOOK FIX: If total width < 100%, scale up to fill container
498
+ // This prevents orphaned space that Outlook handles poorly
499
+ const scaleFactor = totalWidth > 0 && totalWidth < 100 ? 100 / totalWidth : 1;
500
+ for (let c = 0; c < columns; c++) {
501
+ const idx = r * columns + c;
502
+ const id = rowIds[c];
503
+ // OUTLOOK FIX: Scale width to ensure cells fill 100% of container
504
+ let widthPercent = cellWidthPercents[c] * scaleFactor;
505
+ // OUTLOOK FIX: Ensure width doesn't exceed 100% after scaling
506
+ widthPercent = Math.min(widthPercent, 100);
429
507
  if (id) {
430
508
  const child = rootData[id];
431
509
  const { style: cellStyle = {}, props: childProps = {} } = child.data;
@@ -435,31 +513,43 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
435
513
  // Only render if visible
436
514
  if (childVisible) {
437
515
  const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, cellWidthInPx);
438
- html += `
439
- <td
440
- width="${Math.round(widthPercent)}%"
516
+ // OUTLOOK FIX: Calculate pixel width for Outlook
517
+ const cellWidthPx = Math.round((widthPercent / 100) * cellWidthInPx);
518
+ html += `
519
+ <td
520
+ width="${Math.round(widthPercent)}%"
441
521
  class="${[
442
522
  responsive ? "stack-column" : "",
443
523
  visibilityClass,
444
- ].filter(Boolean).join(" ")}"
445
- style="vertical-align:${verticalAlign};word-break:break-word;${styles}"
446
- >
447
- ${childHtml}
524
+ ].filter(Boolean).join(" ")}"
525
+ style="width:${cellWidthPx}px;vertical-align:${verticalAlign};word-break:break-word;${styles}"
526
+ >
527
+ ${childHtml}
448
528
  </td>`;
449
529
  }
450
530
  }
451
531
  else {
452
532
  // SAFE empty cell (keeps layout stable)
453
- html += `
454
- <td width="${Math.round(widthPercent)}%"
455
- ${responsive ? 'class="stack-column"' : ""}
456
- style="vertical-align:top;">
533
+ // OUTLOOK FIX: Calculate pixel width for Outlook
534
+ const cellWidthPx = Math.round((widthPercent / 100) * cellWidthInPx);
535
+ html += `
536
+ <td width="${Math.round(widthPercent)}%"
537
+ ${responsive ? 'class="stack-column"' : ""}
538
+ style="width:${cellWidthPx}px;vertical-align:top;">
457
539
  </td>`;
458
540
  }
459
541
  }
460
542
  html += "</tr>";
461
543
  }
462
- html += "</table><!--[if mso]></table><![endif]-->";
544
+ // Close both MSO and non-MSO tables
545
+ html += `
546
+ <!--[if mso]>
547
+ </table>
548
+ <![endif]-->
549
+ <!--[if !mso]><!-->
550
+ </table>
551
+ <!--<![endif]-->
552
+ `;
463
553
  return html;
464
554
  }
465
555
  async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx) {
@@ -470,8 +560,11 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
470
560
  pxChanges: allPxAttributes,
471
561
  });
472
562
  const parts = [];
473
- // FIX: do NOT re-calc px based on parent causes shrinking
474
- const safeCellWidthPx = Math.max(parentCellWidthPx, 20);
563
+ // OUTLOOK FIX: Calculate the actual cell width in pixels based on percentage
564
+ // If parent is 600px and cell is 50%, cell width should be 300px, not 600px
565
+ const cellWidthPx = Math.round((cellWidthPercent / 100) * parentCellWidthPx);
566
+ // OUTLOOK FIX: Ensure cell width is reasonable and capped at 600px
567
+ const safeCellWidthPx = Math.min(Math.max(cellWidthPx, 20), 600);
475
568
  for (const childId of childrenIds) {
476
569
  const child = rootData[childId];
477
570
  if (child) {
@@ -544,116 +637,116 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
544
637
  const vmlTop = calculatedHeight / 2 - playIconHeight / 2;
545
638
  const shouldHideInOutlook = hideOnDesktop;
546
639
  const outlookVideoContent = shouldHideInOutlook
547
- ? `<!--[if !mso]><!-->
548
- <v:group xmlns:v="urn:schemas-microsoft-com:vml"
549
- coordsize="${innerContainerWidth},${calculatedHeight}"
550
- href="${videoLink}"
551
- style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
552
- <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;" stroked="t"
553
- strokeweight="${borderWidth}px"
554
- strokecolor="${borderColor}"
555
- ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
556
- >
557
- <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
558
- </v:rect>
559
- <v:shape type="#_x0000_t75"
560
- style="position:absolute;
561
- left:${vmlLeft.toFixed(1)}px;
562
- top:${vmlTop.toFixed(1)}px;
563
- width:${playIconWidth}px;
564
- height:${playIconHeight}px;"
565
- alt="Play" href="${videoLink}" title="${altText || "Video"}"
566
- stroked="f" filled="t">
567
- <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
568
- </v:shape>
569
- </v:group>
640
+ ? `<!--[if !mso]><!-->
641
+ <v:group xmlns:v="urn:schemas-microsoft-com:vml"
642
+ coordsize="${innerContainerWidth},${calculatedHeight}"
643
+ href="${videoLink}"
644
+ style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
645
+ <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px;" stroked="t"
646
+ strokeweight="${borderWidth}px"
647
+ strokecolor="${borderColor}"
648
+ ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
649
+ >
650
+ <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
651
+ </v:rect>
652
+ <v:shape type="#_x0000_t75"
653
+ style="position:absolute;
654
+ left:${vmlLeft.toFixed(1)}px;
655
+ top:${vmlTop.toFixed(1)}px;
656
+ width:${playIconWidth}px;
657
+ height:${playIconHeight}px;"
658
+ alt="Play" href="${videoLink}" title="${altText || "Video"}"
659
+ stroked="f" filled="t">
660
+ <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
661
+ </v:shape>
662
+ </v:group>
570
663
  <!--<![endif]-->`
571
- : `<!--[if mso]>
572
- <v:group xmlns:v="urn:schemas-microsoft-com:vml"
573
- coordsize="${innerContainerWidth},${calculatedHeight}"
574
- href="${videoLink}"
575
- style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
576
- <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px; stroked="t"
577
- strokeweight="${borderWidth}px"
578
- strokecolor="${borderColor}"
579
- ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
580
- >
581
- <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
582
- </v:rect>
583
- <v:shape type="#_x0000_t75"
584
- style="position:absolute;
585
- left:${vmlLeft.toFixed(1)}px;
586
- top:${vmlTop.toFixed(1)}px;
587
- width:${playIconWidth}px;
588
- height:${playIconHeight}px;"
589
- alt="Play" href="${videoLink}" title="${altText || "Video"}"
590
- stroked="f" filled="t">
591
- <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
592
- </v:shape>
593
- </v:group>
664
+ : `<!--[if mso]>
665
+ <v:group xmlns:v="urn:schemas-microsoft-com:vml"
666
+ coordsize="${innerContainerWidth},${calculatedHeight}"
667
+ href="${videoLink}"
668
+ style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
669
+ <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px; stroked="t"
670
+ strokeweight="${borderWidth}px"
671
+ strokecolor="${borderColor}"
672
+ ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
673
+ >
674
+ <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
675
+ </v:rect>
676
+ <v:shape type="#_x0000_t75"
677
+ style="position:absolute;
678
+ left:${vmlLeft.toFixed(1)}px;
679
+ top:${vmlTop.toFixed(1)}px;
680
+ width:${playIconWidth}px;
681
+ height:${playIconHeight}px;"
682
+ alt="Play" href="${videoLink}" title="${altText || "Video"}"
683
+ stroked="f" filled="t">
684
+ <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
685
+ </v:shape>
686
+ </v:group>
594
687
  <![endif]-->`;
595
- const nonOutlookVideoContent = `<!--[if !mso]><!-->
596
- <table
597
- width="${innerContainerWidth}"
598
- cellpadding="0"
599
- cellspacing="0"
600
- border="0"
601
- role="presentation"
602
- align="${style?.textAlign || "left"}"
603
- style="
604
- max-width: ${innerContainerWidth}px;
605
- width: 100%;
606
- height: ${calculatedHeight}px;
607
- background-color: ${style?.backgroundColor || "#FFFFFF"};
608
- background-image: url('${resolvedThumbnail}');
609
- background-size: contain;
610
- background-position: center;
611
- background-repeat: no-repeat;
612
- box-sizing: border-box;
613
- border: ${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};
614
- border-radius: ${borderRadius}px;
615
- "
616
- >
617
- <tr>
618
- <td style="padding: 0; height: ${calculatedHeight}px; text-align: center; vertical-align: middle;" valign="middle">
619
- <a href="${videoLink}" target="_blank" style="display:inline-block; border: 0; outline: none; text-decoration: none;">
620
- <img
621
- src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
622
- width="${playIconWidth}"
623
- alt="Play"
624
- style="display: block;
625
- border: 0;
626
- outline: none;
627
- text-decoration: none;
628
- height: auto;"
629
- />
630
- </a>
631
- </td>
632
- </tr>
633
- </table>
688
+ const nonOutlookVideoContent = `<!--[if !mso]><!-->
689
+ <table
690
+ width="${innerContainerWidth}"
691
+ cellpadding="0"
692
+ cellspacing="0"
693
+ border="0"
694
+ role="presentation"
695
+ align="${style?.textAlign || "left"}"
696
+ style="
697
+ max-width: ${innerContainerWidth}px;
698
+ width: 100%;
699
+ height: ${calculatedHeight}px;
700
+ background-color: ${style?.backgroundColor || "#FFFFFF"};
701
+ background-image: url('${resolvedThumbnail}');
702
+ background-size: contain;
703
+ background-position: center;
704
+ background-repeat: no-repeat;
705
+ box-sizing: border-box;
706
+ border: ${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};
707
+ border-radius: ${borderRadius}px;
708
+ "
709
+ >
710
+ <tr>
711
+ <td style="padding: 0; height: ${calculatedHeight}px; text-align: center; vertical-align: middle;" valign="middle">
712
+ <a href="${videoLink}" target="_blank" style="display:inline-block; border: 0; outline: none; text-decoration: none;">
713
+ <img
714
+ src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
715
+ width="${playIconWidth}"
716
+ alt="Play"
717
+ style="display: block;
718
+ border: 0;
719
+ outline: none;
720
+ text-decoration: none;
721
+ height: auto;"
722
+ />
723
+ </a>
724
+ </td>
725
+ </tr>
726
+ </table>
634
727
  <!--<![endif]-->`;
635
728
  const videoContent = `${outlookVideoContent}${nonOutlookVideoContent}`;
636
- const wrapperHtml = `
637
- <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}">
638
- <tr>
639
- <td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
640
- <table border="0" cellpadding="0" cellspacing="0" role="presentation"
641
- align="${style?.textAlign || "left"}"
642
- style="
643
- margin:0;
644
- max-width:${cellWidthInPx}px;
645
- width:${percentWidth};
646
- border-collapse:collapse;
647
- ">
648
- <tr>
649
- <td align="${style?.textAlign || "left"}" style="text-align:${style?.textAlign || "left"}; padding:0;">
650
- ${videoContent}
651
- </td>
652
- </tr>
653
- </table>
654
- </td>
655
- </tr>
656
- </table>
729
+ const wrapperHtml = `
730
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}">
731
+ <tr>
732
+ <td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
733
+ <table border="0" cellpadding="0" cellspacing="0" role="presentation"
734
+ align="${style?.textAlign || "left"}"
735
+ style="
736
+ margin:0;
737
+ max-width:${cellWidthInPx}px;
738
+ width:${percentWidth};
739
+ border-collapse:collapse;
740
+ ">
741
+ <tr>
742
+ <td align="${style?.textAlign || "left"}" style="text-align:${style?.textAlign || "left"}; padding:0;">
743
+ ${videoContent}
744
+ </td>
745
+ </tr>
746
+ </table>
747
+ </td>
748
+ </tr>
749
+ </table>
657
750
  `;
658
751
  return wrapperHtml;
659
752
  }
@@ -735,44 +828,44 @@ async function convertShapeBlock(blockData) {
735
828
  let nonMsoContent = "";
736
829
  // --- Case 1: Image + Text ---
737
830
  if (imageUrl && text) {
738
- nonMsoContent = `
739
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
740
- border:${borderWidth}px ${borderStyle} ${borderColor};
741
- border-radius:${resolvedBorderRadius};
742
- background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
743
- overflow:hidden;${alignmentStyle}${customCss || ""}">
744
- <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};overflow:hidden;padding:6px;box-sizing:border-box;">
745
- <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
746
- ${text}
747
- </div>
748
- </div>
831
+ nonMsoContent = `
832
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
833
+ border:${borderWidth}px ${borderStyle} ${borderColor};
834
+ border-radius:${resolvedBorderRadius};
835
+ background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
836
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
837
+ <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};overflow:hidden;padding:6px;box-sizing:border-box;">
838
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
839
+ ${text}
840
+ </div>
841
+ </div>
749
842
  </div>`;
750
843
  }
751
844
  // --- Case 2: Image only ---
752
845
  else if (imageUrl) {
753
- nonMsoContent = `
754
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
755
- border:${borderWidth}px ${borderStyle} ${borderColor};
756
- border-radius:${resolvedBorderRadius};
757
- overflow:hidden;${alignmentStyle}${customCss || ""}">
758
- <img src="${imageUrl}" alt="${text || "shape image"}"
759
- width="${resolvedWidthPx}" height="${resolvedHeightPx}"
760
- style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
846
+ nonMsoContent = `
847
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
848
+ border:${borderWidth}px ${borderStyle} ${borderColor};
849
+ border-radius:${resolvedBorderRadius};
850
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
851
+ <img src="${imageUrl}" alt="${text || "shape image"}"
852
+ width="${resolvedWidthPx}" height="${resolvedHeightPx}"
853
+ style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
761
854
  </div>`;
762
855
  }
763
856
  // --- Case 3: Text only ---
764
857
  else {
765
- nonMsoContent = `
766
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
767
- background:${finalBackgroundColor};
768
- border:${borderWidth}px ${borderStyle} ${borderColor};
769
- border-radius:${resolvedBorderRadius};
770
- overflow:hidden;${alignmentStyle}${customCss || ""}">
771
- <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};padding:8px;box-sizing:border-box;">
772
- <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
773
- ${text || ""}
774
- </div>
775
- </div>
858
+ nonMsoContent = `
859
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
860
+ background:${finalBackgroundColor};
861
+ border:${borderWidth}px ${borderStyle} ${borderColor};
862
+ border-radius:${resolvedBorderRadius};
863
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
864
+ <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};padding:8px;box-sizing:border-box;">
865
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
866
+ ${text || ""}
867
+ </div>
868
+ </div>
776
869
  </div>`;
777
870
  }
778
871
  // Outlook (VML) fallback
@@ -795,16 +888,16 @@ async function convertShapeBlock(blockData) {
795
888
  msoBakeImageWithText,
796
889
  }, visibilityClass);
797
890
  // Combine into table wrapper
798
- return `
799
- <table width="100%" style="border-collapse:collapse;table-layout:fixed;max-width:600px;" class="${visibilityClass}">
800
- <tr>
801
- <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
802
- ${outlookContent}
803
- <!--[if !mso]><!-->
804
- ${nonMsoContent}
805
- <!--<![endif]-->
806
- </td>
807
- </tr>
891
+ return `
892
+ <table width="100%" style="border-collapse:collapse;table-layout:fixed;max-width:600px;" class="${visibilityClass}">
893
+ <tr>
894
+ <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
895
+ ${outlookContent}
896
+ <!--[if !mso]><!-->
897
+ ${nonMsoContent}
898
+ <!--<![endif]-->
899
+ </td>
900
+ </tr>
808
901
  </table>`;
809
902
  }
810
903
  // ---------- Updated VML builder ----------
@@ -837,24 +930,24 @@ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, bo
837
930
  const safeFontSize = Math.max(Math.round(textSize), 10);
838
931
  // Build the textbox with table/cell for reliable vertical centering in Outlook
839
932
  const textboxMarkup = text && !msoHasBakedText
840
- ? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
841
- <div style="display:table;width:100%;height:100%;">
842
- <div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
843
- <div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
844
- ${text}
845
- </div>
846
- </div>
847
- </div>
933
+ ? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
934
+ <div style="display:table;width:100%;height:100%;">
935
+ <div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
936
+ <div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
937
+ ${text}
938
+ </div>
939
+ </div>
940
+ </div>
848
941
  </v:textbox>`
849
942
  : `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
850
943
  // Return VML shape
851
- return `
852
- <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
853
- style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
854
- ${borderAttrs}
855
- fill="true" fillcolor="${fillColor}"${extraAttr}>
856
- ${fillMarkup}
857
- ${textboxMarkup}
944
+ return `
945
+ <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
946
+ style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
947
+ ${borderAttrs}
948
+ fill="true" fillcolor="${fillColor}"${extraAttr}>
949
+ ${fillMarkup}
950
+ ${textboxMarkup}
858
951
  </v:${tag}>`;
859
952
  }
860
953
  function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts, visibilityClass) {
@@ -882,28 +975,28 @@ function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth
882
975
  const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
883
976
  // Fix: Properly handle Outlook visibility with conditional comments
884
977
  if (shouldHideInOutlook) {
885
- return `<!--[if !mso]><!-->
886
- <table align="${align}" border="0" cellpadding="0" cellspacing="0"
887
- style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
888
- <tr>
889
- <td valign="${valign}"
890
- style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
891
- ${vml}
892
- </td>
893
- </tr>
894
- </table>
978
+ return `<!--[if !mso]><!-->
979
+ <table align="${align}" border="0" cellpadding="0" cellspacing="0"
980
+ style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
981
+ <tr>
982
+ <td valign="${valign}"
983
+ style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
984
+ ${vml}
985
+ </td>
986
+ </tr>
987
+ </table>
895
988
  <!--<![endif]-->`;
896
989
  }
897
- return `<!--[if mso]>
898
- <table align="${align}" border="0" cellpadding="0" cellspacing="0"
899
- style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
900
- <tr>
901
- <td valign="${valign}"
902
- style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
903
- ${vml}
904
- </td>
905
- </tr>
906
- </table>
990
+ return `<!--[if mso]>
991
+ <table align="${align}" border="0" cellpadding="0" cellspacing="0"
992
+ style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
993
+ <tr>
994
+ <td valign="${valign}"
995
+ style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
996
+ ${vml}
997
+ </td>
998
+ </tr>
999
+ </table>
907
1000
  <![endif]-->`;
908
1001
  }
909
1002
  function convertVerticalDividerBlockToHtml(blockData) {
@@ -916,19 +1009,19 @@ function convertVerticalDividerBlockToHtml(blockData) {
916
1009
  pxChanges: allPxAttributes,
917
1010
  });
918
1011
  // Outlook-safe vertical divider
919
- const dividerContent = `
920
- <table cellpadding="0" cellspacing="0" border="0" align="center" style="width:auto; ${convertedStyle}">
921
- <tr>
922
- <td style="vertical-align: middle; text-align: center;">
923
- <!--[if mso | IE]>
924
- <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="${dividerColor}" style="width:${width}px;height:${height}px;" stroke="f"></v:rect>
925
- <![endif]-->
926
- <!--[if !mso]><!-- -->
927
- <div style="display:inline-block;width:${width}px;height:${height}px;background:${dividerColor};line-height:0;font-size:0;">&nbsp;</div>
928
- <!--<![endif]-->
929
- </td>
930
- </tr>
931
- </table>
1012
+ const dividerContent = `
1013
+ <table cellpadding="0" cellspacing="0" border="0" align="center" style="width:auto; ${convertedStyle}">
1014
+ <tr>
1015
+ <td style="vertical-align: middle; text-align: center;">
1016
+ <!--[if mso | IE]>
1017
+ <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="${dividerColor}" style="width:${width}px;height:${height}px;" stroke="f"></v:rect>
1018
+ <![endif]-->
1019
+ <!--[if !mso]><!-- -->
1020
+ <div style="display:inline-block;width:${width}px;height:${height}px;background:${dividerColor};line-height:0;font-size:0;">&nbsp;</div>
1021
+ <!--<![endif]-->
1022
+ </td>
1023
+ </tr>
1024
+ </table>
932
1025
  `;
933
1026
  return appendOutlookSupport(dividerContent, convertedStyle, visibilityClass);
934
1027
  }
package/package.json CHANGED
@@ -1,33 +1,33 @@
1
- {
2
- "name": "email-builder-utils",
3
- "version": "1.1.35",
4
- "main": "dist/index.js",
5
- "types": "dist/index.d.ts",
6
- "files": [
7
- "dist"
8
- ],
9
- "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1",
11
- "build": "tsc",
12
- "start": "npm run build && node dist/index.js"
13
- },
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://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@github.com/Biztecno-Infra/email-builder-utils.git"
17
- },
18
- "author": "",
19
- "license": "ISC",
20
- "bugs": {
21
- "url": "https://github.com/Biztecno-Infra/email-builder-utils/issues"
22
- },
23
- "homepage": "https://github.com/Biztecno-Infra/email-builder-utils#readme",
24
- "description": "",
25
- "devDependencies": {
26
- "@types/node": "^22.13.10",
27
- "@types/pngjs": "^6.0.5",
28
- "typescript": "^5.8.2"
29
- },
30
- "dependencies": {
31
- "jimp": "^1.6.0"
32
- }
33
- }
1
+ {
2
+ "name": "email-builder-utils",
3
+ "version": "1.1.40",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "build": "tsc",
12
+ "start": "npm run build && node dist/index.js"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://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@github.com/Biztecno-Infra/email-builder-utils.git"
17
+ },
18
+ "author": "",
19
+ "license": "ISC",
20
+ "bugs": {
21
+ "url": "https://github.com/Biztecno-Infra/email-builder-utils/issues"
22
+ },
23
+ "homepage": "https://github.com/Biztecno-Infra/email-builder-utils#readme",
24
+ "description": "",
25
+ "devDependencies": {
26
+ "@types/node": "^22.13.10",
27
+ "@types/pngjs": "^6.0.5",
28
+ "typescript": "^5.8.2"
29
+ },
30
+ "dependencies": {
31
+ "jimp": "^1.6.0"
32
+ }
33
+ }