email-builder-utils 1.1.45 → 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.
@@ -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,iDAAiD,CAAC;AA2I/E,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAwBtB;AA0lCD,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,7 +8,6 @@ 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
  ];
@@ -209,6 +208,7 @@ function convertDividerBlockToHtml(blockData) {
209
208
  });
210
209
  const dividerWidth = width || 100;
211
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
212
  // Append text-align so the import parser can recover alignment via inheritance
213
213
  const contentStyle = convertedStyle
214
214
  ? `${convertedStyle}; text-align:${alignAttr};`
@@ -226,6 +226,7 @@ function convertDividerBlockToHtml(blockData) {
226
226
  width="${dividerWidth}%"
227
227
  cellpadding="0"
228
228
  cellspacing="0"
229
+ style="margin:${alignMargin};"
229
230
  >
230
231
  <tr>
231
232
  <td
@@ -251,8 +252,27 @@ function convertSpacerBlockToHtml(blockData) {
251
252
  function convertTextBlock(blockData, cellWidthInPx) {
252
253
  const { style, props } = blockData.data;
253
254
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
254
- 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
255
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';
256
276
  // Text box decoration styles (border, background, padding) — no width
257
277
  const textBoxStyle = {
258
278
  backgroundColor,
@@ -266,11 +286,17 @@ function convertTextBlock(blockData, cellWidthInPx) {
266
286
  perChanges: [],
267
287
  pxChanges: allPxAttributes,
268
288
  });
269
- // 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.
270
296
  const styles = buildStyles({
271
297
  padding: textContainerPadding,
272
- backgroundColor: textContainerBackgroundColor,
273
- ...rest,
298
+ backgroundColor: hasBgImage ? undefined : textContainerBackgroundColor,
299
+ ...restForStyles,
274
300
  }, {
275
301
  perChanges: [],
276
302
  pxChanges: allPxAttributes,
@@ -298,8 +324,52 @@ function convertTextBlock(blockData, cellWidthInPx) {
298
324
  // display:inline-block with a pixel width (e.g. 400px) breaks narrow grid cells.
299
325
  const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${colorStyle}${fontSizeStyle}${convertedTextStyle}">${processedText.replaceAll(/\n/g, "<br>")}</div>`;
300
326
  const safeCellWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : undefined;
301
- 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);
302
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
+ }
303
373
  return navigateToUrl
304
374
  ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="${linkColorStyle}text-decoration:none;cursor:pointer;">${textContent}</a>`
305
375
  : textContent;
@@ -469,161 +539,138 @@ async function convertImageBlock(blockData, cellWidthInPx) {
469
539
  ? `<a href="${navigateToUrl}" target="_blank" rel="noreferrer noopener" style="display:block;">${imageContent}</a>`
470
540
  : imageContent;
471
541
  }
472
- function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
473
- const pad = buttonStyle.buttonPadding || {};
542
+ function appendOutlookForButton(buttonData) {
543
+ const { style, text, navigateToUrl } = buttonData;
544
+ const pad = style.buttonPadding || {};
474
545
  const padTop = Number.isFinite(pad.top) ? pad.top : 10;
475
546
  const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
476
547
  const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
477
548
  const padRight = Number.isFinite(pad.right) ? pad.right : 20;
478
- const fontSize = buttonStyle.fontSize || 16;
479
- const height = typeof buttonStyle.height === "number" && buttonStyle.height > 0
480
- ? buttonStyle.height
481
- : null;
482
- // prevent layout break
483
- const minHeight = padTop + padBottom + fontSize;
484
- const finalHeight = height ? Math.max(height, minHeight) : null;
485
- const borderRadius = buttonStyle.borderRadius || 0;
486
- const borderColor = buttonStyle.borderColor || "transparent";
487
- const borderWidth = buttonStyle.borderWidth || 0;
488
- const borderStyle = buttonStyle.borderStyle || "solid";
489
- const bgColor = buttonStyle.buttonColor || "transparent";
490
- const color = buttonStyle.color || "#ffffff";
491
- const fontFamily = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(buttonStyle.fontFamily));
492
- const fontWeight = buttonStyle.fontWeight || 400;
493
- const width = typeof buttonStyle.width === "number"
494
- ? `width="${buttonStyle.width}"`
495
- : "";
496
- return `<!--[if mso]>
497
- <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">
498
584
  <tr>
499
- <td align="center"
500
- valign="middle"
501
- ${width}
502
- ${finalHeight ? `height="${finalHeight}"` : ""}
503
- bgcolor="${bgColor}"
504
- style="
505
- ${finalHeight ? `height:${finalHeight}px;` : ""}
506
- background-color:${bgColor};
507
- border-radius:${borderRadius}px;
508
- border:${borderWidth}px ${borderStyle} ${borderColor};
509
- overflow:hidden;
510
- mso-line-height-rule:exactly;
511
- ">
512
-
513
- <table role="presentation" cellspacing="0" cellpadding="0" border="0">
514
- <tr>
515
- <td align="center" valign="middle"
516
- style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
517
-
518
- <a href="${navigateToUrl}"
519
- style="
520
- display:inline-block;
521
- color:${color};
522
- text-decoration:none;
523
- font-family:${fontFamily};
524
- font-size:${fontSize}px;
525
- font-weight:${fontWeight};
526
- line-height:normal;
527
- ">
528
- ${text}
529
- </a>
530
-
531
- </td>
532
- </tr>
533
- </table>
534
-
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>
535
588
  </td>
536
589
  </tr>
537
590
  </table>
538
- <![endif]-->
539
- <!--[if !mso]><!-->
540
- ${content}
541
- <!--<![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
+ };
542
616
  }
543
617
  function convertButtonBlock(blockData) {
544
618
  const { style, props } = blockData.data;
545
619
  const { text, navigateToUrl } = props;
546
- const { fontFamily, fontSize, fontWeight, textAlign, borderColor, borderRadius, borderWidth, borderStyle, buttonPadding, color, buttonColor, width, height, alignment, padding, backgroundColor: containerBg, margin, } = style;
547
- const pad = buttonPadding || {};
548
- const padTop = Number.isFinite(pad.top) ? pad.top : 10;
549
- const padBottom = Number.isFinite(pad.bottom) ? pad.bottom : 10;
550
- const padLeft = Number.isFinite(pad.left) ? pad.left : 20;
551
- const padRight = Number.isFinite(pad.right) ? pad.right : 20;
552
- const fs = fontSize || 16;
553
- // prevent layout break
554
- const minHeight = padTop + padBottom + fs;
555
- const finalHeight = typeof height === "number" && height > 0
556
- ? Math.max(height, minHeight)
557
- : null;
558
- const safeFF = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(fontFamily));
559
- const safeColor = color || "#ffffff";
560
- const bgColor = buttonColor || "transparent";
561
- const bdColor = borderColor || "transparent";
562
- const bdStyle = borderStyle || "solid";
563
- const bw = borderWidth || 0;
564
- const br = borderRadius || 0;
565
- const containerAlign = alignment || textAlign || "left";
566
- const widthAttr = typeof width === "number"
567
- ? `width="${width}"`
568
- : "";
569
- // ✅ FIX: no width=100% anywhere
570
- const buttonTable = `
571
- <table role="presentation" cellspacing="0" cellpadding="0" border="0"
572
- style="display:inline-table; border-collapse:separate;"
573
- ${widthAttr}>
574
- <tr>
575
- <td
576
- align="center"
577
- valign="middle"
578
- ${finalHeight ? `height="${finalHeight}"` : ""}
579
- style="
580
- ${finalHeight ? `height:${finalHeight}px;` : ""}
581
- background-color:${bgColor};
582
- border-radius:${br}px;
583
- border:${bw}px ${bdStyle} ${bdColor};
584
- overflow:hidden;
585
- mso-line-height-rule:exactly;
586
- "
587
- >
588
-
589
- <table role="presentation" cellspacing="0" cellpadding="0" border="0">
590
- <tr>
591
- <td align="center" valign="middle"
592
- style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">
593
-
594
- <a href="${navigateToUrl}"
595
- style="
596
- display:inline-block;
597
- color:${safeColor};
598
- text-decoration:none;
599
- font-family:${safeFF};
600
- font-size:${fs}px;
601
- font-weight:${fontWeight || 400};
602
- line-height:normal;
603
- white-space:nowrap;
604
- ">
605
- ${text}
606
- </a>
607
-
608
- </td>
609
- </tr>
610
- </table>
611
-
612
- </td>
613
- </tr>
614
- </table>
615
- `;
616
- const aligned = containerAlign === "center"
617
- ? `<center>${buttonTable}</center>`
618
- : `<div style="text-align:${containerAlign};">${buttonTable}</div>`;
619
- 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
+ });
620
668
  return `
621
- <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}">
622
670
  <tr>
623
- <td align="${containerAlign}"
624
- style="padding:${padding?.top || 0}px ${padding?.right || 0}px ${padding?.bottom || 0}px ${padding?.left || 0}px;
625
- background-color:${containerBg || "transparent"};">
626
- ${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}
627
674
  </td>
628
675
  </tr>
629
676
  </table>
@@ -1142,16 +1189,28 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
1142
1189
  <![endif]-->`;
1143
1190
  // Non-Outlook: use a real <img> for the thumbnail so it renders in Gmail / Yahoo / webmail.
1144
1191
  // background-image on <table> is stripped by virtually every email client.
1145
- // position:absolute for the play-button overlay is safe here because this block
1146
- // is already inside <!--[if !mso]> — Outlook is handled separately via VML above.
1147
1192
  const thumbnailW = Math.round(innerContainerWidth);
1148
1193
  const thumbnailH = Math.round(calculatedHeight);
1149
- const playMarginTop = -Math.round(playIconHeight / 2);
1150
- const playMarginLeft = -Math.round(playIconWidth / 2);
1151
1194
  const borderAttr = borderWidth > 0 ? `border:${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};` : "border:0;";
1152
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>`;
1153
1212
  const nonOutlookVideoContent = `<!--[if !mso]><!-->
1154
- <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}">
1155
1214
  <a href="${videoLink}" target="_blank" style="display:block; text-decoration:none; line-height:0; font-size:0;">
1156
1215
  <img
1157
1216
  src="${resolvedThumbnail}"
@@ -1160,21 +1219,32 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
1160
1219
  alt="${altText || "Video"}"
1161
1220
  style="display:block; width:100%; max-width:${thumbnailW}px; height:auto; border:0;"
1162
1221
  />
1163
- <img
1164
- src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
1165
- width="${playIconWidth}"
1166
- alt="Play"
1167
- style="display:block; position:absolute; top:50%; left:50%; margin-top:${playMarginTop}px; margin-left:${playMarginLeft}px; border:0; outline:none;"
1168
- />
1169
1222
  </a>
1223
+ ${playButtonHtml}
1170
1224
  </div>
1171
1225
  <!--<![endif]-->`;
1172
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
+ });
1173
1243
  const wrapperHtml = `
1174
- <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}">
1175
1245
  <tr>
1176
1246
  <td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
1177
- <table border="0" cellpadding="0" cellspacing="0" role="presentation"
1247
+ <table border="0" cellpadding="0" cellspacing="0" role="presentation"
1178
1248
  align="${style?.textAlign || "left"}"
1179
1249
  style="
1180
1250
  margin:0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email-builder-utils",
3
- "version": "1.1.45",
3
+ "version": "1.1.46",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [