email-builder-utils 1.1.44 → 1.1.46

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.
@@ -23,7 +23,7 @@ interface IBlockData {
23
23
  childrenIds?: Array<string>;
24
24
  };
25
25
  }
26
- export declare const tableCommonStyle = "border-collapse:collapse; table-layout:fixed;";
26
+ export declare const tableCommonStyle = "border-collapse:collapse; table-layout:fixed";
27
27
  export declare function convertToHtml(blockData: IBlockData, rootData: any, cellWidthInPx: number): Promise<string>;
28
28
  export declare function convertVideoBlock(blockData: any, cellWidthInPx: number): Promise<string>;
29
29
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAIrC,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;AA2GhF,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAwBtB;AA2/BD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBA+K5E"}
1
+ {"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAIrC,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,iDAAiD,CAAC;AA2I/E,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAwBtB;AAqoCD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAwM5E"}
@@ -8,13 +8,12 @@ const common_1 = require("./common");
8
8
  const fontFallback_1 = require("./fontFallback");
9
9
  const addPxToAttributes = [
10
10
  "fontSize",
11
- "lineHeight",
12
11
  "borderRadius",
13
12
  "borderWidth",
14
13
  ];
15
14
  const addPxOrPerToAttributes = ["width", "height"];
16
15
  const allPxAttributes = [...addPxToAttributes, ...addPxOrPerToAttributes];
17
- exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed;";
16
+ exports.tableCommonStyle = "border-collapse:collapse; table-layout:fixed";
18
17
  function encodeBlockProps(props) {
19
18
  return JSON.stringify(props)
20
19
  .replace(/&/g, '&amp;')
@@ -28,6 +27,38 @@ async function loadImageNaturalDimensions(imageUrl) {
28
27
  img.src = imageUrl;
29
28
  });
30
29
  }
30
+ const GENERIC_FONT_FAMILIES = new Set([
31
+ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
32
+ 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace',
33
+ 'ui-rounded', 'emoji', 'math', 'fangsong',
34
+ ]);
35
+ /**
36
+ * Normalises a CSS font-family string so every multi-word family name is wrapped
37
+ * in single quotes — safe inside double-quoted HTML style attributes.
38
+ * Handles already-quoted names (single or double), generic keywords, and any
39
+ * number of comma-separated families.
40
+ */
41
+ function sanitizeFontFamily(fontFamily) {
42
+ if (!fontFamily)
43
+ return fontFamily;
44
+ return fontFamily
45
+ .split(',')
46
+ .map(font => {
47
+ const trimmed = font.trim();
48
+ // Strip any surrounding quotes (single or double) from either end
49
+ const unquoted = trimmed.replace(/^["']|["']$/g, '').trim();
50
+ if (!unquoted)
51
+ return '';
52
+ // Generic families and single-token names need no quotes
53
+ if (GENERIC_FONT_FAMILIES.has(unquoted.toLowerCase()) || !/\s/.test(unquoted)) {
54
+ return unquoted;
55
+ }
56
+ // Multi-word font name: wrap in single quotes, escaping any embedded single quotes
57
+ return `'${unquoted.replace(/'/g, "\\'")}'`;
58
+ })
59
+ .filter(Boolean)
60
+ .join(', ');
61
+ }
31
62
  function buildStyles(style, { pxChanges, perChanges }) {
32
63
  if (!style)
33
64
  style = {};
@@ -64,7 +95,7 @@ function buildStyles(style, { pxChanges, perChanges }) {
64
95
  value = `${safePad.top}px ${safePad.right}px ${safePad.bottom}px ${safePad.left}px`;
65
96
  }
66
97
  if (key === "fontFamily" && typeof value === "string") {
67
- value = (0, fontFallback_1.withFontFallback)(value).replace(/"/g, "'");
98
+ value = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(value));
68
99
  }
69
100
  // Wrap backgroundImage values in url() if not already wrapped — skip gradients
70
101
  if (key === "backgroundImage" && typeof value === "string"
@@ -170,12 +201,18 @@ ${content}
170
201
  function convertDividerBlockToHtml(blockData) {
171
202
  const { style, props } = blockData.data;
172
203
  const { hideOnMobile, hideOnDesktop } = props;
173
- const { thickness, dividerColor, width, ...rest } = style;
204
+ const { thickness, dividerColor, width, alignment, ...rest } = style;
174
205
  const convertedStyle = buildStyles(rest, {
175
206
  perChanges: [],
176
207
  pxChanges: allPxAttributes,
177
208
  });
178
- const dividerWidth = width || "100%";
209
+ const dividerWidth = width || 100;
210
+ const alignAttr = alignment === 'center' ? 'center' : alignment === 'right' ? 'right' : 'left';
211
+ const alignMargin = alignAttr === 'center' ? '0 auto' : alignAttr === 'right' ? '0 0 0 auto' : '0 auto 0 0';
212
+ // Append text-align so the import parser can recover alignment via inheritance
213
+ const contentStyle = convertedStyle
214
+ ? `${convertedStyle}; text-align:${alignAttr};`
215
+ : `text-align:${alignAttr};`;
179
216
  // Build class name based on visibility
180
217
  const visibilityClass = [
181
218
  hideOnMobile ? "hide-mobile" : "",
@@ -185,21 +222,23 @@ function convertDividerBlockToHtml(blockData) {
185
222
  .join(" ");
186
223
  const dividerContent = `
187
224
  <table
225
+ align="${alignAttr}"
188
226
  width="${dividerWidth}%"
189
227
  cellpadding="0"
190
228
  cellspacing="0"
229
+ style="margin:${alignMargin};"
191
230
  >
192
231
  <tr>
193
232
  <td
194
233
  height="${thickness}"
195
- style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth};"
234
+ style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth}%;"
196
235
  >
197
236
  &nbsp;
198
237
  </td>
199
238
  </tr>
200
239
  </table>
201
240
  `;
202
- return appendOutlookSupport(dividerContent, convertedStyle, visibilityClass);
241
+ return appendOutlookSupport(dividerContent, contentStyle, visibilityClass);
203
242
  }
204
243
  function convertSpacerBlockToHtml(blockData) {
205
244
  const { style, props } = blockData.data;
@@ -213,8 +252,27 @@ function convertSpacerBlockToHtml(blockData) {
213
252
  function convertTextBlock(blockData, cellWidthInPx) {
214
253
  const { style, props } = blockData.data;
215
254
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
216
- const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, fontSize, whiteSpace: _whiteSpace, // strip from outer td — pre-wrap on a td preserves editor whitespace
255
+ const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, fontSize, backgroundImage, whiteSpace: _whiteSpace, // strip from outer td — pre-wrap on a td preserves editor whitespace
217
256
  ...rest } = style;
257
+ // Detect background image or gradient (may live in backgroundImage or customCss).
258
+ // Same multi-client approach as Grid block: outer wrapper <td> carries the background
259
+ // via CSS (Gmail/New Outlook), background attribute (Yahoo), and VML (Old Outlook).
260
+ const bgImageStr = typeof backgroundImage === 'string' ? backgroundImage : '';
261
+ const customCssStr = rest.customCss || '';
262
+ const gradientInCustomCss = !bgImageStr.includes('gradient(') && customCssStr.includes('gradient(')
263
+ ? (customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || '')
264
+ : '';
265
+ const effectiveGradient = bgImageStr.includes('gradient(') ? bgImageStr : gradientInCustomCss;
266
+ const isGradient = Boolean(effectiveGradient);
267
+ const parsedGradient = isGradient ? parseGradient(effectiveGradient) : null;
268
+ const rawBgImageUrl = !isGradient && bgImageStr
269
+ ? bgImageStr.replace(/^url\(['"]?/, '').replace(/['"]?\)$/, '')
270
+ : null;
271
+ const hasBgImage = Boolean(rawBgImageUrl || isGradient);
272
+ const fallbackBgColor = textContainerBackgroundColor ||
273
+ parsedGradient?.fallback ||
274
+ extractCssFallbackColor(customCssStr) ||
275
+ '#ffffff';
218
276
  // Text box decoration styles (border, background, padding) — no width
219
277
  const textBoxStyle = {
220
278
  backgroundColor,
@@ -228,11 +286,17 @@ function convertTextBlock(blockData, cellWidthInPx) {
228
286
  perChanges: [],
229
287
  pxChanges: allPxAttributes,
230
288
  });
231
- // Outer td styles: layout only, no typography, no white-space
289
+ // Strip gradient from customCss when it is hoisted to the outer bg wrapper.
290
+ const innerCustomCss = gradientInCustomCss
291
+ ? customCssStr.replace(/background-image\s*:[^;]+;?/gi, '').trim()
292
+ : customCssStr;
293
+ const restForStyles = gradientInCustomCss ? { ...rest, customCss: innerCustomCss } : rest;
294
+ // Outer td styles: strip container background when a bg-image wrapper is present
295
+ // so the outer wrapper's background is not double-applied.
232
296
  const styles = buildStyles({
233
297
  padding: textContainerPadding,
234
- backgroundColor: textContainerBackgroundColor,
235
- ...rest,
298
+ backgroundColor: hasBgImage ? undefined : textContainerBackgroundColor,
299
+ ...restForStyles,
236
300
  }, {
237
301
  perChanges: [],
238
302
  pxChanges: allPxAttributes,
@@ -242,13 +306,72 @@ function convertTextBlock(blockData, cellWidthInPx) {
242
306
  .replace(/<\/p>/gi, "</div>");
243
307
  const navigateToUrl = props.navigateToUrl || "";
244
308
  const fontSizeStyle = fontSize != null ? `font-size:${fontSize}px;` : "";
309
+ // Email clients apply `a { color: blue }` which overrides inherited color.
310
+ // Inject the block color directly onto <a> tags that don't already have one.
311
+ const blockTextColor = rest.color;
312
+ const processedText = blockTextColor
313
+ ? sanitizedText.replace(/<a(\s[^>]*)?>/gi, (match, attrs = '') => {
314
+ if (/style\s*=\s*["'][^"']*\bcolor\s*:/i.test(attrs))
315
+ return match;
316
+ if (/\bstyle\s*=/i.test(attrs)) {
317
+ return `<a${attrs.replace(/(\bstyle\s*=\s*["'])/, `$1color:${blockTextColor};`)}>`;
318
+ }
319
+ return `<a${attrs} style="color:${blockTextColor};">`;
320
+ })
321
+ : sanitizedText;
322
+ const colorStyle = blockTextColor ? `color:${blockTextColor};` : '';
245
323
  // Use display:block + width:100% so text fills the column naturally.
246
324
  // display:inline-block with a pixel width (e.g. 400px) breaks narrow grid cells.
247
- const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${fontSizeStyle}${convertedTextStyle}">${sanitizedText.replaceAll(/\n/g, "<br>")}</div>`;
325
+ const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${colorStyle}${fontSizeStyle}${convertedTextStyle}">${processedText.replaceAll(/\n/g, "<br>")}</div>`;
248
326
  const safeCellWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : undefined;
249
- const textContent = appendOutlookSupport(convertedTextBox, styles, visibilityClass, safeCellWidth);
327
+ // When a bg-image wrapper is present, visibilityClass moves to the outer table.
328
+ const textContent = appendOutlookSupport(convertedTextBox, styles, hasBgImage ? '' : visibilityClass, safeCellWidth);
329
+ const linkColorStyle = blockTextColor ? `color:${blockTextColor};` : 'color:inherit;';
330
+ if (hasBgImage) {
331
+ const msoWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : 600;
332
+ const vmlFill = isGradient
333
+ ? (() => {
334
+ const vmlAngle = cssAngleToVml(parsedGradient?.angle || 180);
335
+ const c1 = parsedGradient?.fallback || '#ffffff';
336
+ const c2 = parsedGradient?.colors[parsedGradient.colors.length - 1] || c1;
337
+ return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
338
+ })()
339
+ : `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
340
+ const bgCss = isGradient
341
+ ? `background:${effectiveGradient};`
342
+ : `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`;
343
+ const wrappedContent = `
344
+ <table border="0" cellpadding="0" cellspacing="0" width="${msoWidth}" role="presentation"
345
+ style="border-collapse:collapse;width:${msoWidth}px;" class="${visibilityClass}">
346
+ <tr>
347
+ <td width="${msoWidth}" bgcolor="${fallbackBgColor}" valign="top"
348
+ ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ''}
349
+ style="width:${msoWidth}px;background-color:${fallbackBgColor};${bgCss}">
350
+
351
+ <!--[if gte mso 9]>
352
+ <v:rect xmlns:v="urn:schemas-microsoft-com:vml"
353
+ fill="true" stroke="false"
354
+ style="width:${msoWidth}px;">
355
+ ${vmlFill}
356
+ <v:textbox inset="0,0,0,0">
357
+ <![endif]-->
358
+
359
+ ${textContent}
360
+
361
+ <!--[if gte mso 9]>
362
+ </v:textbox>
363
+ </v:rect>
364
+ <![endif]-->
365
+
366
+ </td>
367
+ </tr>
368
+ </table>`;
369
+ return navigateToUrl
370
+ ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="${linkColorStyle}text-decoration:none;cursor:pointer;">${wrappedContent}</a>`
371
+ : wrappedContent;
372
+ }
250
373
  return navigateToUrl
251
- ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit;text-decoration:none;cursor:pointer;">${textContent}</a>`
374
+ ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="${linkColorStyle}text-decoration:none;cursor:pointer;">${textContent}</a>`
252
375
  : textContent;
253
376
  }
254
377
  async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}, finalWidth, finalHeight) {
@@ -416,161 +539,138 @@ async function convertImageBlock(blockData, cellWidthInPx) {
416
539
  ? `<a href="${navigateToUrl}" target="_blank" rel="noreferrer noopener" style="display:block;">${imageContent}</a>`
417
540
  : imageContent;
418
541
  }
419
- function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
420
- const pad = buttonStyle.buttonPadding || {};
542
+ function appendOutlookForButton(buttonData) {
543
+ const { style, text, navigateToUrl } = buttonData;
544
+ const pad = style.buttonPadding || {};
421
545
  const padTop = Number.isFinite(pad.top) ? pad.top : 10;
422
546
  const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
423
547
  const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
424
548
  const padRight = Number.isFinite(pad.right) ? pad.right : 20;
425
- const fontSize = buttonStyle.fontSize || 16;
426
- const height = typeof buttonStyle.height === "number" && buttonStyle.height > 0
427
- ? buttonStyle.height
428
- : null;
429
- // prevent layout break
430
- const minHeight = padTop + padBottom + fontSize;
431
- const finalHeight = height ? Math.max(height, minHeight) : null;
432
- const borderRadius = buttonStyle.borderRadius || 0;
433
- const borderColor = buttonStyle.borderColor || "transparent";
434
- const borderWidth = buttonStyle.borderWidth || 0;
435
- const borderStyle = buttonStyle.borderStyle || "solid";
436
- const bgColor = buttonStyle.buttonColor || "transparent";
437
- const color = buttonStyle.color || "#ffffff";
438
- const fontFamily = (0, fontFallback_1.withFontFallback)(buttonStyle.fontFamily).replace(/"/g, "'");
439
- const fontWeight = buttonStyle.fontWeight || 400;
440
- const width = typeof buttonStyle.width === "number"
441
- ? `width="${buttonStyle.width}"`
442
- : "";
443
- return `<!--[if mso]>
444
- <table role="presentation" cellspacing="0" cellpadding="0" border="0" style="display:inline-table;">
549
+ const fs = style.fontSize || 16;
550
+ const minHeight = padTop + padBottom + fs;
551
+ const finalHeight = typeof style.height === "number" && style.height > 0
552
+ ? Math.max(style.height, minHeight)
553
+ : minHeight;
554
+ const safeFF = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(style.fontFamily));
555
+ const safeColor = style.color || "#ffffff";
556
+ const bgColor = style.buttonColor || "transparent";
557
+ const bdColor = style.borderColor || "transparent";
558
+ const bdStyle = style.borderStyle || "solid";
559
+ const bw = typeof style.borderWidth === "number" ? style.borderWidth : 0;
560
+ const br = typeof style.borderRadius === "number" ? style.borderRadius : 0;
561
+ const fontWeight = style.fontWeight || 400;
562
+ const containerAlign = style.alignment || style.textAlign || "left";
563
+ const explicitWidth = typeof style.width === "number" && style.width > 0 ? style.width : 0;
564
+ const borderCss = bw > 0 ? `border:${bw}px ${bdStyle} ${bdColor};` : "";
565
+ const widthCss = explicitWidth ? `width:${explicitWidth}px;` : "";
566
+ // ── Non-MSO: <a display:inline-block> — border-radius works in modern clients ──
567
+ const nonMsoAnchor = `<!--[if !mso]><!-->
568
+ <a href="${navigateToUrl}"
569
+ target="_blank" rel="noreferrer noopener"
570
+ style="display:inline-block;background-color:${bgColor};border-radius:${br}px;${borderCss}color:${safeColor};font-family:${safeFF};font-size:${fs}px;font-weight:${fontWeight};text-decoration:none;padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;line-height:${fs}px;text-align:center;white-space:nowrap;-webkit-text-size-adjust:none;box-sizing:border-box;${widthCss}mso-hide:all;">${text}</a>
571
+ <!--<![endif]-->`;
572
+ // ── MSO: table-based bulletproof button.
573
+ // <v:roundrect arcsize="50%"> renders its half-circle arcs as visible bracket shapes
574
+ // in Outlook, so we use a plain <table>+<td> instead. bgcolor on <td> is reliable in
575
+ // all classic Outlook versions; border-radius is not (square corners in Outlook).
576
+ const tdStyleParts = [`padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px`];
577
+ if (bw > 0)
578
+ tdStyleParts.push(`border:${bw}px ${bdStyle} ${bdColor}`);
579
+ if (explicitWidth)
580
+ tdStyleParts.push(`width:${explicitWidth}px`);
581
+ const tdStyle = tdStyleParts.join(";");
582
+ const msoButton = `<!--[if mso]>
583
+ <table cellspacing="0" cellpadding="0" border="0">
445
584
  <tr>
446
- <td align="center"
447
- valign="middle"
448
- ${width}
449
- ${finalHeight ? `height="${finalHeight}"` : ""}
450
- bgcolor="${bgColor}"
451
- style="
452
- ${finalHeight ? `height:${finalHeight}px;` : ""}
453
- background-color:${bgColor};
454
- border-radius:${borderRadius}px;
455
- border:${borderWidth}px ${borderStyle} ${borderColor};
456
- overflow:hidden;
457
- mso-line-height-rule:exactly;
458
- ">
459
-
460
- <table role="presentation" cellspacing="0" cellpadding="0" border="0">
461
- <tr>
462
- <td align="center" valign="middle"
463
- style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
464
-
465
- <a href="${navigateToUrl}"
466
- style="
467
- display:inline-block;
468
- color:${color};
469
- text-decoration:none;
470
- font-family:${fontFamily};
471
- font-size:${fontSize}px;
472
- font-weight:${fontWeight};
473
- line-height:normal;
474
- ">
475
- ${text}
476
- </a>
477
-
478
- </td>
479
- </tr>
480
- </table>
481
-
585
+ <td bgcolor="${bgColor}" style="${tdStyle};">
586
+ <a href="${navigateToUrl}" target="_blank" rel="noreferrer noopener"
587
+ style="color:${safeColor};font-family:${safeFF};font-size:${fs}px;font-weight:${fontWeight};text-decoration:none;display:inline-block;line-height:${fs}px;">${text}</a>
482
588
  </td>
483
589
  </tr>
484
590
  </table>
485
- <![endif]-->
486
- <!--[if !mso]><!-->
487
- ${content}
488
- <!--<![endif]-->`;
591
+ <![endif]-->`;
592
+ const innerContent = containerAlign === "center"
593
+ ? `<center>${msoButton}${nonMsoAnchor}</center>`
594
+ : `<div style="text-align:${containerAlign};">${msoButton}${nonMsoAnchor}</div>`;
595
+ return {
596
+ innerContent,
597
+ computed: {
598
+ fs,
599
+ fontWeight,
600
+ containerAlign,
601
+ padTop,
602
+ padRight,
603
+ padBottom,
604
+ padLeft,
605
+ explicitWidth,
606
+ safeColor,
607
+ bgColor,
608
+ safeFF,
609
+ finalHeight,
610
+ bw,
611
+ br,
612
+ bdColor,
613
+ bdStyle,
614
+ },
615
+ };
489
616
  }
490
617
  function convertButtonBlock(blockData) {
491
618
  const { style, props } = blockData.data;
492
619
  const { text, navigateToUrl } = props;
493
- const { fontFamily, fontSize, fontWeight, textAlign, borderColor, borderRadius, borderWidth, borderStyle, buttonPadding, color, buttonColor, width, height, alignment, padding, backgroundColor: containerBg, margin, } = style;
494
- const pad = buttonPadding || {};
495
- const padTop = Number.isFinite(pad.top) ? pad.top : 10;
496
- const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
497
- const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
498
- const padRight = Number.isFinite(pad.right) ? pad.right : 20;
499
- const fs = fontSize || 16;
500
- // prevent layout break
501
- const minHeight = padTop + padBottom + fs;
502
- const finalHeight = typeof height === "number" && height > 0
503
- ? Math.max(height, minHeight)
504
- : null;
505
- const safeFF = (0, fontFallback_1.withFontFallback)(fontFamily).replace(/"/g, "'");
506
- const safeColor = color || "#ffffff";
507
- const bgColor = buttonColor || "transparent";
508
- const bdColor = borderColor || "transparent";
509
- const bdStyle = borderStyle || "solid";
510
- const bw = borderWidth || 0;
511
- const br = borderRadius || 0;
512
- const containerAlign = alignment || textAlign || "left";
513
- const widthAttr = typeof width === "number"
514
- ? `width="${width}"`
515
- : "";
516
- // ✅ FIX: no width=100% anywhere
517
- const buttonTable = `
518
- <table role="presentation" cellspacing="0" cellpadding="0" border="0"
519
- style="display:inline-table; border-collapse:separate;"
520
- ${widthAttr}>
521
- <tr>
522
- <td
523
- align="center"
524
- valign="middle"
525
- ${finalHeight ? `height="${finalHeight}"` : ""}
526
- style="
527
- ${finalHeight ? `height:${finalHeight}px;` : ""}
528
- background-color:${bgColor};
529
- border-radius:${br}px;
530
- border:${bw}px ${bdStyle} ${bdColor};
531
- overflow:hidden;
532
- mso-line-height-rule:exactly;
533
- "
534
- >
535
-
536
- <table role="presentation" cellspacing="0" cellpadding="0" border="0">
537
- <tr>
538
- <td align="center" valign="middle"
539
- style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
540
-
541
- <a href="${navigateToUrl}"
542
- style="
543
- display:inline-block;
544
- color:${safeColor};
545
- text-decoration:none;
546
- font-family:${safeFF};
547
- font-size:${fs}px;
548
- font-weight:${fontWeight || 400};
549
- line-height:normal;
550
- white-space:nowrap;
551
- ">
552
- ${text}
553
- </a>
554
-
555
- </td>
556
- </tr>
557
- </table>
558
-
559
- </td>
560
- </tr>
561
- </table>
562
- `;
563
- const aligned = containerAlign === "center"
564
- ? `<center>${buttonTable}</center>`
565
- : `<div style="text-align:${containerAlign};">${buttonTable}</div>`;
566
- const buttonWithOutlook = appendOutlookForButton(aligned, style, navigateToUrl, text);
620
+ const { fontFamily, fontSize, fontWeight, textAlign, borderColor, borderRadius, borderWidth, borderStyle, buttonPadding, color, buttonColor, width, height, alignment, padding, backgroundColor: containerBg, } = style;
621
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
622
+ const { innerContent, computed } = appendOutlookForButton({
623
+ style: {
624
+ fontFamily,
625
+ fontSize,
626
+ fontWeight,
627
+ textAlign,
628
+ borderColor,
629
+ borderRadius,
630
+ borderWidth,
631
+ borderStyle,
632
+ buttonPadding,
633
+ color,
634
+ buttonColor,
635
+ width,
636
+ height,
637
+ alignment,
638
+ },
639
+ text: text || "",
640
+ navigateToUrl: navigateToUrl || "",
641
+ });
642
+ const buttonBlockProps = encodeBlockProps({
643
+ buttonText: text || '',
644
+ navigateToUrl: navigateToUrl || '',
645
+ buttonColor: computed.bgColor,
646
+ color: computed.safeColor,
647
+ fontFamily: fontFamily || '',
648
+ fontSize: computed.fs,
649
+ fontWeight: computed.fontWeight,
650
+ alignment: computed.containerAlign,
651
+ padding: {
652
+ top: padding?.top || 0,
653
+ right: padding?.right || 0,
654
+ bottom: padding?.bottom || 0,
655
+ left: padding?.left || 0,
656
+ },
657
+ buttonPadding: { top: computed.padTop, right: computed.padRight, bottom: computed.padBottom, left: computed.padLeft },
658
+ width: computed.explicitWidth,
659
+ height: typeof height === 'number' && height > 0 ? height : 0,
660
+ backgroundColor: containerBg || '',
661
+ borderRadius: computed.br,
662
+ borderColor: computed.bdColor,
663
+ borderWidth: computed.bw,
664
+ borderStyle: computed.bdStyle,
665
+ hideOnDesktop: Boolean(props.hideOnDesktop),
666
+ hideOnMobile: Boolean(props.hideOnMobile),
667
+ });
567
668
  return `
568
- <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
669
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" class="${visibilityClass}" data-block-type="button" data-block-props="${buttonBlockProps}">
569
670
  <tr>
570
- <td align="${containerAlign}"
571
- style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;
572
- background-color:${containerBg || "transparent"};">
573
- ${buttonWithOutlook}
671
+ <td align="${computed.containerAlign}"
672
+ style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;background-color:${containerBg || 'transparent'};">
673
+ ${innerContent}
574
674
  </td>
575
675
  </tr>
576
676
  </table>
@@ -684,10 +784,29 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
684
784
  : customCssStr;
685
785
  // Build inner table styles — when gradient/bg-image is on the outer wrapper, strip
686
786
  // background props from the inner table so the outer <td> background shows through.
687
- const innerRestStyle = (rawBgImageUrl || isGradient)
787
+ const innerRestStyleRaw = (rawBgImageUrl || isGradient)
688
788
  ? { ...restStyle, customCss: innerCustomCss, backgroundSize: undefined, backgroundPosition: undefined, backgroundRepeat: undefined }
689
789
  : { ...restStyle, customCss: innerCustomCss };
690
- const tableStyles = buildStyles({ backgroundColor: (rawBgImageUrl || isGradient) ? undefined : backgroundColor, ...innerRestStyle }, {
790
+ // Extract border/radius props applied via a div wrapper for non-MSO clients so that
791
+ // border-radius is honoured (Gmail/Outlook compose strip border-radius from <table>).
792
+ const { borderRadius, border, borderColor, borderWidth, borderStyle: borderStyleProp, ...innerRestStyle } = innerRestStyleRaw;
793
+ const divBorderParts = [];
794
+ if (borderRadius)
795
+ divBorderParts.push(`border-radius:${typeof borderRadius === 'number' ? borderRadius + 'px' : borderRadius};`, `overflow:hidden;`);
796
+ if (border) {
797
+ divBorderParts.push(`border:${border};`);
798
+ }
799
+ else if (borderWidth || borderColor || borderStyleProp) {
800
+ const bw = borderWidth ? (typeof borderWidth === 'number' ? borderWidth + 'px' : borderWidth) : '1px';
801
+ const bs = borderStyleProp || 'solid';
802
+ const bc = borderColor || '#000000';
803
+ divBorderParts.push(`border:${bw} ${bs} ${bc};`);
804
+ }
805
+ const divBorderStyle = divBorderParts.join(' ');
806
+ const tableBgForNonMso = divBorderStyle
807
+ ? 'transparent'
808
+ : ((rawBgImageUrl || isGradient) ? undefined : backgroundColor);
809
+ const tableStyles = buildStyles({ backgroundColor: tableBgForNonMso, ...innerRestStyle }, {
691
810
  perChanges: [],
692
811
  pxChanges: allPxAttributes,
693
812
  });
@@ -707,7 +826,15 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
707
826
  const innerBgTransparent = (rawBgImageUrl || isGradient)
708
827
  ? 'background-color:transparent;'
709
828
  : '';
710
- const nonMsoBgAttr = !rawBgImageUrl && !isGradient && backgroundColor ? ` bgcolor="${backgroundColor}"` : '';
829
+ const nonMsoBgAttr = !rawBgImageUrl && !isGradient && backgroundColor && !divBorderStyle ? ` bgcolor="${backgroundColor}"` : '';
830
+ // When divBorderStyle is set the non-MSO <table> is transparent, so the Grid's
831
+ // backgroundColor must move onto the div wrapper — otherwise it vanishes in modern clients.
832
+ // Skip this for bg-image/gradient blocks; they apply their background via a separate wrapper.
833
+ const divWrapBg = divBorderStyle && backgroundColor && !rawBgImageUrl && !isGradient
834
+ ? ` background-color:${backgroundColor};`
835
+ : '';
836
+ const divWrapOpen = divBorderStyle ? `<div style="${divBorderStyle}${divWrapBg}">` : '';
837
+ const divWrapClose = divBorderStyle ? `</div>` : '';
711
838
  let html = `
712
839
  <!--[if mso]>
713
840
  <table border="0" cellpadding="0" cellspacing="0" width="${msoTableWidth}"${msoBgAttr}
@@ -773,7 +900,7 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
773
900
  const childVisible = !childProps.hideOnDesktop;
774
901
  const visibilityClass = (0, common_1.getVisibilityClass)(childProps);
775
902
  if (childVisible) {
776
- const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth);
903
+ const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, adjustedTableWidth, Boolean(divBorderStyle));
777
904
  // bgcolor on the cell <td> ensures background-color survives Outlook
778
905
  // compose paste (Word/Web editors strip CSS but keep bgcolor attribute).
779
906
  const cellBgColor = cellStyle.backgroundColor || '';
@@ -843,10 +970,11 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
843
970
  style="border-collapse:collapse;width:${msoTableWidth}px;">
844
971
  <tr>
845
972
  <td width="${msoTableWidth}" bgcolor="${fallbackBgColor}" valign="top"
973
+ ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
846
974
  style="
847
975
  width:${msoTableWidth}px;
848
976
  background-color:${fallbackBgColor};
849
- ${isGradient ? `background:${effectiveGradient};` : `background:url('${rawBgImageUrl}') center/cover no-repeat;`}
977
+ ${isGradient ? `background:${effectiveGradient};` : `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`}
850
978
  ">
851
979
 
852
980
  <!--[if gte mso 9]>
@@ -868,12 +996,32 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
868
996
  </tr>
869
997
  </table>`;
870
998
  }
999
+ // Wrap the entire grid (including any bg-image outer table) in a div when the block
1000
+ // has border/radius. An unconditional <div> is used — not gated behind <!--[if !mso]>-->
1001
+ // — so Gmail compose paste renders the border-radius reliably. Old Outlook ignores
1002
+ // border-radius on <div> but still shows the rectangular border; new Outlook works fully.
1003
+ if (divBorderStyle)
1004
+ html = `${divWrapOpen}${html}${divWrapClose}`;
871
1005
  return html;
872
1006
  }
873
- async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx) {
1007
+ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx, parentGridHasBorder = false) {
874
1008
  const { style = {}, childrenIds = [], props = {} } = blockData.data;
875
1009
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
876
- const styles = buildStyles(style, {
1010
+ // Extract border + radius from style so they move to the div wrapper (not the <td>).
1011
+ // Gmail strips border-radius from <td> but honours it on <div>. By putting border and
1012
+ // radius on the same unconditional <div>, the rounded card border renders in all clients.
1013
+ // The <td> keeps bgcolor (via attribute) for Old Outlook background fallback.
1014
+ const { borderRadius: cellBorderRadius, borderWidth: cellBorderWidth, borderStyle: cellBorderStyleProp, borderColor: cellBorderColor, border: cellBorderShorthand, ...styleWithoutBorder } = style;
1015
+ // backgroundColor must stay on the div wrapper (not the <td>) in two cases:
1016
+ // 1. Cell has its own border-radius — the div's overflow:hidden clips the background.
1017
+ // 2. Parent grid has a border div (divBorderStyle) — the grid's overflow:hidden clips it.
1018
+ // In both cases, the rectangular <td> background bleeds through rounded corners if kept
1019
+ // in CSS, creating visible corner squares. The bgcolor attribute stays for Outlook fallback.
1020
+ const stripBgFromTd = Boolean(cellBorderRadius) || parentGridHasBorder;
1021
+ const styleForTd = stripBgFromTd
1022
+ ? { ...styleWithoutBorder, backgroundColor: 'transparent' }
1023
+ : styleWithoutBorder;
1024
+ const styles = buildStyles(styleForTd, {
877
1025
  perChanges: [],
878
1026
  pxChanges: allPxAttributes,
879
1027
  });
@@ -884,7 +1032,7 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
884
1032
  // Subtract the cell's own padding so children receive the actual content-area width.
885
1033
  // Old Outlook honours explicit img/table width attributes — if a child is sized to the
886
1034
  // full column width (ignoring padding) it overflows and expands the column.
887
- const cellPad = style?.padding || {};
1035
+ const cellPad = styleWithoutBorder?.padding || {};
888
1036
  const cellPadLeft = Number.isFinite(cellPad.left) ? cellPad.left : 0;
889
1037
  const cellPadRight = Number.isFinite(cellPad.right) ? cellPad.right : 0;
890
1038
  const contentWidthPx = Math.max(cellWidthPx - cellPadLeft - cellPadRight, 20);
@@ -896,22 +1044,35 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
896
1044
  parts.push(await convertToHtml(child, rootData, safeCellWidthPx));
897
1045
  }
898
1046
  }
899
- const borderRadius = style?.borderRadius || 0;
900
- const bgColor = style?.backgroundColor || "transparent";
901
- // IMPORTANT: radius only for non-Outlook
902
- const wrapped = `
903
- <!--[if !mso]><!-->
904
- <div style="
905
- border-radius:${borderRadius}px;
906
- overflow:hidden;
907
- background-color:${bgColor};
908
- ">
909
- <!--<![endif]-->
910
- ${parts.join("")}
911
- <!--[if !mso]><!-->
912
- </div>
913
- <!--<![endif]-->
914
- `;
1047
+ const borderRadius = cellBorderRadius || 0;
1048
+ const bgColor = styleWithoutBorder?.backgroundColor || "transparent";
1049
+ // Build border CSS for the div wrapper.
1050
+ // When the parent grid already has a divBorderStyle wrapper (border + border-radius +
1051
+ // overflow:hidden), the cell must NOT duplicate the same border/radius — that causes
1052
+ // two concentric borders of the same colour (double-border). The grid's wrapper div
1053
+ // already provides the visual container; the cell div only needs background-color.
1054
+ const cellDivBorderParts = [];
1055
+ if (!parentGridHasBorder) {
1056
+ if (borderRadius)
1057
+ cellDivBorderParts.push(`border-radius:${typeof borderRadius === 'number' ? borderRadius + 'px' : borderRadius};`, `overflow:hidden;`);
1058
+ if (cellBorderShorthand) {
1059
+ cellDivBorderParts.push(`border:${cellBorderShorthand};`);
1060
+ }
1061
+ else if (cellBorderWidth || cellBorderColor || cellBorderStyleProp) {
1062
+ const bw = cellBorderWidth ? (typeof cellBorderWidth === 'number' ? cellBorderWidth + 'px' : cellBorderWidth) : '1px';
1063
+ const bs = cellBorderStyleProp || 'solid';
1064
+ const bc = cellBorderColor || '#000000';
1065
+ cellDivBorderParts.push(`border:${bw} ${bs} ${bc};`);
1066
+ }
1067
+ }
1068
+ const cellDivBorderStyle = cellDivBorderParts.join(' ');
1069
+ // Unconditional div — visible to all clients (Gmail, Outlook new/old, Apple Mail).
1070
+ // background-color on the div covers modern clients; bgcolor on <td> covers Old Outlook.
1071
+ const divStyleParts = [`background-color:${bgColor};`];
1072
+ if (cellDivBorderStyle)
1073
+ divStyleParts.push(cellDivBorderStyle);
1074
+ const divStyleStr = divStyleParts.join(' ');
1075
+ const wrapped = `<div style="${divStyleStr}">${parts.join("")}</div>`;
915
1076
  return {
916
1077
  html: wrapped,
917
1078
  styles,
@@ -1028,16 +1189,28 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
1028
1189
  <![endif]-->`;
1029
1190
  // Non-Outlook: use a real <img> for the thumbnail so it renders in Gmail / Yahoo / webmail.
1030
1191
  // background-image on <table> is stripped by virtually every email client.
1031
- // position:absolute for the play-button overlay is safe here because this block
1032
- // is already inside <!--[if !mso]> — Outlook is handled separately via VML above.
1033
1192
  const thumbnailW = Math.round(innerContainerWidth);
1034
1193
  const thumbnailH = Math.round(calculatedHeight);
1035
- const playMarginTop = -Math.round(playIconHeight / 2);
1036
- const playMarginLeft = -Math.round(playIconWidth / 2);
1037
1194
  const borderAttr = borderWidth > 0 ? `border:${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};` : "border:0;";
1038
1195
  const radiusAttr = borderRadius > 0 ? `border-radius:${borderRadius}px; overflow:hidden;` : "";
1196
+ // Overlay the play button using negative margin-top + line-height centering.
1197
+ // This avoids both position:absolute (stripped by Gmail/Yahoo) and
1198
+ // height:0/overflow:visible (clipped by New Outlook at <td> boundaries).
1199
+ // The play button <a> is pulled up by margin-top:-thumbnailH to sit over the thumbnail,
1200
+ // then line-height:thumbnailH + vertical-align:middle centres the icon vertically.
1201
+ // Elements later in DOM flow render on top of earlier ones, so the play icon overlays the image.
1202
+ const playButtonHtml = `<a href="${videoLink}" target="_blank" data-play-button="true"
1203
+ style="display:block; margin-top:-${thumbnailH}px; text-align:center; line-height:${thumbnailH}px; font-size:0; text-decoration:none; border:0; outline:none;">
1204
+ <img
1205
+ src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
1206
+ width="${playIconWidth}"
1207
+ height="${playIconHeight}"
1208
+ alt="Play"
1209
+ style="display:inline-block; vertical-align:middle; border:0; outline:none;"
1210
+ />
1211
+ </a>`;
1039
1212
  const nonOutlookVideoContent = `<!--[if !mso]><!-->
1040
- <div style="display:block; width:100%; max-width:${thumbnailW}px; position:relative; line-height:0; font-size:0; ${borderAttr}${radiusAttr}">
1213
+ <div style="display:block; width:100%; max-width:${thumbnailW}px; line-height:0; font-size:0; ${borderAttr}${radiusAttr}">
1041
1214
  <a href="${videoLink}" target="_blank" style="display:block; text-decoration:none; line-height:0; font-size:0;">
1042
1215
  <img
1043
1216
  src="${resolvedThumbnail}"
@@ -1046,21 +1219,32 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
1046
1219
  alt="${altText || "Video"}"
1047
1220
  style="display:block; width:100%; max-width:${thumbnailW}px; height:auto; border:0;"
1048
1221
  />
1049
- <img
1050
- src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
1051
- width="${playIconWidth}"
1052
- alt="Play"
1053
- style="display:block; position:absolute; top:50%; left:50%; margin-top:${playMarginTop}px; margin-left:${playMarginLeft}px; border:0; outline:none;"
1054
- />
1055
1222
  </a>
1223
+ ${playButtonHtml}
1056
1224
  </div>
1057
1225
  <!--<![endif]-->`;
1058
1226
  const videoContent = `${outlookVideoContent}${nonOutlookVideoContent}`;
1227
+ const videoBlockProps = encodeBlockProps({
1228
+ videoUrl: videoUrl || '',
1229
+ youtubeVideoUrl: youtubeVideoUrl || '',
1230
+ thumbnailUrl: thumbnailUrl || resolvedThumbnail,
1231
+ altText: altText || '',
1232
+ width: parseFloat(percentWidth) || 100,
1233
+ padding: style?.padding || { top: 0, right: 0, bottom: 0, left: 0 },
1234
+ backgroundColor: style?.backgroundColor || '',
1235
+ textAlign: style?.textAlign || 'left',
1236
+ borderRadius: style?.borderRadius || 0,
1237
+ borderColor: style?.borderColor || '',
1238
+ borderWidth: style?.borderWidth || 0,
1239
+ borderStyle: style?.borderStyle || 'none',
1240
+ hideOnDesktop: Boolean(props.hideOnDesktop),
1241
+ hideOnMobile: Boolean(props.hideOnMobile),
1242
+ });
1059
1243
  const wrapperHtml = `
1060
- <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}">
1244
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse; max-width:600px;" class="${visibilityClass}" data-block-type="video" data-block-props="${videoBlockProps}">
1061
1245
  <tr>
1062
1246
  <td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
1063
- <table border="0" cellpadding="0" cellspacing="0" role="presentation"
1247
+ <table border="0" cellpadding="0" cellspacing="0" role="presentation"
1064
1248
  align="${style?.textAlign || "left"}"
1065
1249
  style="
1066
1250
  margin:0;
@@ -1140,16 +1324,6 @@ async function convertShapeBlock(blockData) {
1140
1324
  justify: "justify",
1141
1325
  };
1142
1326
  const textAlignStyle = textAlignMap[textAlign] || "center";
1143
- const flexJustify = textAlign === "left"
1144
- ? "flex-start"
1145
- : textAlign === "right"
1146
- ? "flex-end"
1147
- : "center";
1148
- const flexAlign = verticalAlign === "top"
1149
- ? "flex-start"
1150
- : verticalAlign === "bottom"
1151
- ? "flex-end"
1152
- : "center";
1153
1327
  // --- Text styling ---
1154
1328
  const textSizeStyle = `font-size:${fontSize}px;line-height:1.3;word-break:break-word;overflow-wrap:break-word;color:${color};`;
1155
1329
  // ============================
@@ -1162,13 +1336,22 @@ async function convertShapeBlock(blockData) {
1162
1336
  <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
1163
1337
  border:${borderWidth}px ${borderStyle} ${borderColor};
1164
1338
  border-radius:${resolvedBorderRadius};
1165
- background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
1339
+ background-color:${finalBackgroundColor};
1340
+ background-image:url('${imageUrl}');
1341
+ background-position:center center;
1342
+ background-size:cover;
1343
+ background-repeat:no-repeat;
1166
1344
  overflow:hidden;${alignmentStyle}${customCss || ""}">
1167
- <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};overflow:hidden;padding:6px;box-sizing:border-box;">
1168
- <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
1169
- ${text}
1170
- </div>
1171
- </div>
1345
+ <table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
1346
+ style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
1347
+ <tr>
1348
+ <td align="${textAlignStyle}" valign="${verticalAlign}"
1349
+ width="${resolvedWidthPx}" height="${resolvedHeightPx}"
1350
+ style="padding:6px;vertical-align:${verticalAlign};text-align:${textAlignStyle};overflow:hidden;box-sizing:border-box;">
1351
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text}</div>
1352
+ </td>
1353
+ </tr>
1354
+ </table>
1172
1355
  </div>`;
1173
1356
  }
1174
1357
  // --- Case 2: Image only ---
@@ -1187,15 +1370,20 @@ async function convertShapeBlock(blockData) {
1187
1370
  else {
1188
1371
  nonMsoContent = `
1189
1372
  <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
1190
- background:${finalBackgroundColor};
1373
+ background-color:${finalBackgroundColor};
1191
1374
  border:${borderWidth}px ${borderStyle} ${borderColor};
1192
1375
  border-radius:${resolvedBorderRadius};
1193
1376
  overflow:hidden;${alignmentStyle}${customCss || ""}">
1194
- <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};padding:8px;box-sizing:border-box;">
1195
- <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
1196
- ${text || ""}
1197
- </div>
1198
- </div>
1377
+ <table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
1378
+ style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
1379
+ <tr>
1380
+ <td align="${textAlignStyle}" valign="${verticalAlign}"
1381
+ width="${resolvedWidthPx}" height="${resolvedHeightPx}"
1382
+ style="padding:8px;vertical-align:${verticalAlign};text-align:${textAlignStyle};box-sizing:border-box;">
1383
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text || ""}</div>
1384
+ </td>
1385
+ </tr>
1386
+ </table>
1199
1387
  </div>`;
1200
1388
  }
1201
1389
  // Outlook (VML) fallback
@@ -1377,7 +1565,7 @@ function convertVerticalDividerBlockToHtml(blockData) {
1377
1565
  });
1378
1566
  return `
1379
1567
  <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
1380
- style="${exports.tableCommonStyle} max-width:600px;" class="${visibilityClass}" data-block-type="vdivider" data-block-props="${vDividerProps}">
1568
+ style="${exports.tableCommonStyle}; max-width:600px;" class="${visibilityClass}" data-block-type="vdivider" data-block-props="${vDividerProps}">
1381
1569
  <tr>
1382
1570
  <td style="${outerStyles}; text-align:center; vertical-align:middle;">
1383
1571
  <!--[if mso | IE]>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email-builder-utils",
3
- "version": "1.1.44",
3
+ "version": "1.1.46",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [