email-builder-utils 1.1.28 → 1.1.30

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.
@@ -31,6 +31,8 @@ interface IProps {
31
31
  youtubeVideoUrl?: string;
32
32
  thumbnailUrl?: string;
33
33
  shape?: string;
34
+ hideOnDesktop?: boolean;
35
+ hideOnMobile?: boolean;
34
36
  }
35
37
  interface IStyle {
36
38
  [key: string]: any;
@@ -1 +1 @@
1
- {"version":3,"file":"Template.d.ts","sourceRoot":"","sources":["../../src/types/Template.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS;IACnB,IAAI,SAAS;IACb,KAAK,UAAU;IACf,MAAM,WAAW;IACjB,IAAI,YAAY;IAChB,KAAK,UAAU;IACf,QAAQ,WAAW;IACnB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,WAAW,gBAAgB;IAC3B,KAAK,UAAU;IACf,KAAK,UAAU;IACf,QAAQ,aAAa;CACtB;AAED,oBAAY,UAAU;IACpB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,WAAW,gBAAgB;CAC5B;AAED,UAAU,MAAM;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,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;CAChB;AAED,UAAU,MAAM;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;CACH"}
1
+ {"version":3,"file":"Template.d.ts","sourceRoot":"","sources":["../../src/types/Template.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS;IACnB,IAAI,SAAS;IACb,KAAK,UAAU;IACf,MAAM,WAAW;IACjB,IAAI,YAAY;IAChB,KAAK,UAAU;IACf,QAAQ,WAAW;IACnB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,WAAW,gBAAgB;IAC3B,KAAK,UAAU;IACf,KAAK,UAAU;IACf,QAAQ,aAAa;CACtB;AAED,oBAAY,UAAU;IACpB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,WAAW,gBAAgB;CAC5B;AAED,UAAU,MAAM;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,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,MAAM;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;CACH"}
@@ -1,3 +1,7 @@
1
1
  export declare const extractYouTubeId: (url: string) => string | null;
2
2
  export declare const extractVimeoId: (url: string) => string | null;
3
+ export declare function getVisibilityClass(props?: {
4
+ hideOnDesktop?: boolean;
5
+ hideOnMobile?: boolean;
6
+ }): string;
3
7
  //# sourceMappingURL=common.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/utils/common.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAqBvD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAIrD,CAAC"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/utils/common.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAqBvD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAIrD,CAAC;AAGF,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,UASA"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractVimeoId = exports.extractYouTubeId = void 0;
4
+ exports.getVisibilityClass = getVisibilityClass;
4
5
  const extractYouTubeId = (url) => {
5
6
  try {
6
7
  const u = new URL(url);
@@ -31,3 +32,14 @@ const extractVimeoId = (url) => {
31
32
  return match ? match[1] : null;
32
33
  };
33
34
  exports.extractVimeoId = extractVimeoId;
35
+ function getVisibilityClass(props) {
36
+ if (!props)
37
+ return "";
38
+ const { hideOnDesktop, hideOnMobile } = props;
39
+ return [
40
+ hideOnMobile ? "hide-mobile" : "",
41
+ hideOnDesktop ? "hide-desktop" : "",
42
+ ]
43
+ .filter(Boolean)
44
+ .join(" ");
45
+ }
@@ -12,6 +12,8 @@ interface BlockJsonProps {
12
12
  youtubeVideoUrl?: string;
13
13
  thumbnailUrl?: string;
14
14
  shape?: string;
15
+ hideOnDesktop?: boolean;
16
+ hideOnMobile?: boolean;
15
17
  }
16
18
  interface IBlockData {
17
19
  type: BlockType;
@@ -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;AAUrC,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;CAChB;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;AA2DhF,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAwBtB;AAobD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBA8K5E"}
1
+ {"version":3,"file":"jsonToHTML.d.ts","sourceRoot":"","sources":["../../src/utils/jsonToHTML.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAUrC,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;AA2DhF,wBAAsB,aAAa,CACjC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,GAAG,EACb,aAAa,EAAE,MAAM,mBAwBtB;AAuiBD,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,mBAyL5E"}
@@ -84,34 +84,67 @@ async function convertToHtml(blockData, rootData, cellWidthInPx) {
84
84
  return "";
85
85
  }
86
86
  }
87
- function appendOutlookSupport(content, contentStyle) {
87
+ // function appendOutlookSupport(content: string, contentStyle: string) {
88
+ // return `
89
+ // <table width="100%" style="${tableCommonStyle}"><tr><td style="${contentStyle}">${content}</td></tr></table>
90
+ // `;
91
+ // }
92
+ function appendOutlookSupport(content, contentStyle, className) {
93
+ const visibilityClass = className || "";
94
+ const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
95
+ if (shouldHideInOutlook) {
96
+ return `
97
+ <!--[if !mso]><!-->
98
+ <table width="100%" style="${exports.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
99
+ <!--<![endif]-->
100
+ `;
101
+ }
88
102
  return `
89
- <table width="100%" style="${exports.tableCommonStyle}"><tr><td style="${contentStyle}">${content}</td></tr></table>
103
+ <table width="100%" style="${exports.tableCommonStyle}" class="${visibilityClass}"><tr><td style="${contentStyle}">${content}</td></tr></table>
90
104
  `;
91
105
  }
92
106
  function convertDividerBlockToHtml(blockData) {
93
- const { style } = blockData.data;
94
- const { thickness, dividerColor, ...rest } = style;
107
+ const { style, props } = blockData.data;
108
+ const { hideOnMobile, hideOnDesktop } = props;
109
+ const { thickness, dividerColor, width, ...rest } = style;
95
110
  const convertedStyle = buildStyles(rest, {
96
111
  perChanges: [],
97
112
  pxChanges: allPxAttributes,
98
113
  });
114
+ const dividerWidth = width || "100%";
115
+ // Build class name based on visibility
116
+ const visibilityClass = [
117
+ hideOnMobile ? "hide-mobile" : "",
118
+ hideOnDesktop ? "hide-desktop" : "",
119
+ ]
120
+ .filter(Boolean)
121
+ .join(" ");
99
122
  const dividerContent = `
100
- <table width="100%" cellpadding="0" cellspacing="0">
123
+ <table
124
+ width="${dividerWidth}%"
125
+ cellpadding="0"
126
+ cellspacing="0"
127
+ >
101
128
  <tr>
102
- <td height="${thickness}" style="font-size:1px; line-height:1px; background:${dividerColor};">&nbsp;</td>
129
+ <td
130
+ height="${thickness}"
131
+ style="font-size:1px; line-height:1px; background:${dividerColor}; width:${dividerWidth};"
132
+ >
133
+ &nbsp;
134
+ </td>
103
135
  </tr>
104
136
  </table>
105
137
  `;
106
- return appendOutlookSupport(dividerContent, convertedStyle);
138
+ return appendOutlookSupport(dividerContent, convertedStyle, visibilityClass);
107
139
  }
108
140
  function convertSpacerBlockToHtml(blockData) {
109
- const { style } = blockData.data;
141
+ const { style, props } = blockData.data;
142
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
110
143
  const styles = buildStyles(style, {
111
144
  perChanges: [],
112
145
  pxChanges: allPxAttributes,
113
146
  });
114
- return appendOutlookSupport(``, styles);
147
+ return appendOutlookSupport(``, styles, visibilityClass);
115
148
  }
116
149
  // function convertTextBlock(blockData: IBlockData) {
117
150
  // const { style, props } = blockData.data;
@@ -131,6 +164,7 @@ function convertSpacerBlockToHtml(blockData) {
131
164
  // }
132
165
  function convertTextBlock(blockData) {
133
166
  const { style, props } = blockData.data;
167
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
134
168
  const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, ...rest } = style;
135
169
  const textBoxStyle = {
136
170
  width,
@@ -158,9 +192,9 @@ function convertTextBlock(blockData) {
158
192
  .replaceAll(/<\/p>/g, "</div>");
159
193
  const navigateToUrl = props.navigateToUrl || "";
160
194
  const convertedTextBox = `<div style="display: inline-block; max-width: 100%; box-sizing: border-box; ${convertedTextStyle}">${sanitizedText.replaceAll(/\n/g, "<br>")}</div>`;
161
- const textContent = appendOutlookSupport(convertedTextBox, styles);
195
+ const textContent = appendOutlookSupport(convertedTextBox, styles, visibilityClass);
162
196
  return navigateToUrl
163
- ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit; text-decoration:none; cursor:pointer;">${textContent}</a>`
197
+ ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="color:inherit;text-decoration:none;cursor:pointer;">${textContent}</a>`
164
198
  : textContent;
165
199
  }
166
200
  async function appendOutlookForImage(content, outerContainerWidth, innerContainerWidth, imageUrl, style = {}) {
@@ -197,13 +231,20 @@ async function appendOutlookForImage(content, outerContainerWidth, innerContaine
197
231
  <!--<![endif]-->
198
232
  `;
199
233
  }
234
+ async function computeScaledDimensions(imageUrl, maxContainerWidthPx) {
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(maxContainerWidthPx / originalWidth, 1);
239
+ const scaledWidth = Math.round(originalWidth * widthScalingFactor);
240
+ const scaledHeight = Math.round(originalHeight * widthScalingFactor);
241
+ return { originalWidth, originalHeight, scaledWidth, scaledHeight };
242
+ }
200
243
  async function convertImageBlock(blockData, cellWidthInPx) {
201
244
  const { style, props } = blockData.data;
202
245
  const { altText, imageUrl, navigateToUrl } = props;
246
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
203
247
  const { width, height, objectFit, borderRadius, borderWidth, borderColor, borderStyle, ...containerStyle } = style;
204
- const image = await jimp_1.Jimp.read(imageUrl);
205
- const originalWidth = image.bitmap.width;
206
- const originalHeight = image.bitmap.height;
207
248
  // Ensure border styles are applied only to the container, not the image
208
249
  const imageStyle = {
209
250
  width,
@@ -212,27 +253,37 @@ async function convertImageBlock(blockData, cellWidthInPx) {
212
253
  borderStyle,
213
254
  borderRadius: borderRadius,
214
255
  borderColor,
215
- maxWidth: `${originalWidth}px`, // Limit to original size
216
- maxHeight: `${originalHeight}px`,
217
256
  };
218
257
  // Add border styles to container for fallback clients
219
258
  const containerStyles = buildStyles({
220
259
  ...containerStyle,
221
260
  }, { perChanges: [], pxChanges: addPxToAttributes });
222
- const imageTagStyles = buildStyles(imageStyle, {
223
- perChanges: addPxOrPerToAttributes,
224
- pxChanges: addPxToAttributes,
225
- });
226
- const imageElement = `<img src="${imageUrl}" alt="${altText}" style="${imageTagStyles}; max-width: ${originalWidth}px; max-height: ${originalHeight}px;" />`;
227
- const innerContainerWidth = ((typeof width === "string" ? parseInt(width.replace("%", "")) : width) /
261
+ const innerContainerWidth = (((typeof width === "string" ? parseInt(width.replace("%", "")) : width) ||
262
+ 100) /
228
263
  100) *
229
264
  (cellWidthInPx -
230
265
  (style?.padding?.left || 0) -
231
266
  (style?.padding?.right || 0));
232
- const outlookImage = await appendOutlookForImage(imageElement, cellWidthInPx, innerContainerWidth, imageUrl, style);
233
- const imageContent = appendOutlookSupport(outlookImage, containerStyles);
267
+ const { originalWidth, originalHeight, scaledWidth, scaledHeight } = await computeScaledDimensions(imageUrl, innerContainerWidth);
268
+ const imageTagStyles = buildStyles({
269
+ maxWidth: `${originalWidth}px`, // Limit to original size
270
+ maxHeight: `${originalHeight}px`,
271
+ ...imageStyle,
272
+ }, {
273
+ perChanges: addPxOrPerToAttributes,
274
+ pxChanges: addPxToAttributes,
275
+ });
276
+ 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;" />`;
277
+ const percentWidth = typeof width === "string" && width.endsWith("%")
278
+ ? width
279
+ : typeof width === "number"
280
+ ? `${width}%`
281
+ : "100%";
282
+ const nonMsoWrapper = `<div style="display:inline-block; width:${percentWidth}; max-width:${originalWidth}px;">${imageElement}</div>`;
283
+ const outlookImage = await appendOutlookForImage(nonMsoWrapper, cellWidthInPx, innerContainerWidth, imageUrl, style);
284
+ const imageContent = appendOutlookSupport(outlookImage, containerStyles, visibilityClass);
234
285
  return navigateToUrl
235
- ? `<a href="${navigateToUrl}" target="_blank" rel="noreferrer noopener" style="display:block; text-decoration:none; cursor:pointer;">${imageContent}</a>`
286
+ ? `<a href="${navigateToUrl}" target="_blank" rel="noreferrer noopener" style="display:block;">${imageContent}</a>`
236
287
  : imageContent;
237
288
  }
238
289
  function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
@@ -262,6 +313,7 @@ function appendOutlookForButton(content, buttonStyle, navigateToUrl, text) {
262
313
  function convertButtonBlock(blockData) {
263
314
  const { style, props } = blockData.data;
264
315
  const { text, navigateToUrl } = props;
316
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
265
317
  const { fontFamily, fontSize, fontWeight, borderColor, borderRadius, borderWidth, borderStyle, buttonPadding, color, buttonColor, width, height, ...rest } = style;
266
318
  const buttonStyle = {
267
319
  width,
@@ -286,44 +338,71 @@ function convertButtonBlock(blockData) {
286
338
  pxChanges: allPxAttributes,
287
339
  });
288
340
  const buttonElement = `<a href="${navigateToUrl}" rel="noreferrer noopener" style="display:inline-block; text-decoration:none; cursor:pointer;"><button style="${convertedButtonStyle}">${text}</button></a>`;
289
- const buttonContent = appendOutlookSupport(appendOutlookForButton(buttonElement, style, navigateToUrl, text), convertedStyles);
341
+ const buttonContent = appendOutlookSupport(appendOutlookForButton(buttonElement, style, navigateToUrl, text), convertedStyles, visibilityClass);
290
342
  return buttonContent;
291
343
  }
292
344
  async function convertGridBlock(blockData, rootData, cellWidthInPx) {
293
345
  const { style = {}, childrenIds = [], props } = blockData.data;
294
346
  const { columns = 1, cellWidths = [], responsive = true } = props;
295
347
  const { columnGap = 0, ...restStyle } = style;
348
+ const gridVisibilityClass = (0, common_1.getVisibilityClass)(props);
296
349
  const tableStyles = buildStyles(restStyle, {
297
350
  perChanges: [],
298
351
  pxChanges: allPxAttributes,
299
352
  });
300
353
  const total = childrenIds.length;
301
354
  const visualRows = Math.ceil(total / columns);
355
+ // Fix: Calculate visible cells per row to adjust widths
302
356
  let html = `
303
357
  <!--[if mso]>
304
- <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%" style="${exports.tableCommonStyle}border-collapse: separate;border-spacing:${columnGap}px;">
358
+ <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%"
359
+ style="${exports.tableCommonStyle}border-collapse: separate;border-spacing:${columnGap}px;" class="${gridVisibilityClass}">
305
360
  <![endif]-->
306
- <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%" role="presentation" style="${exports.tableCommonStyle} ${tableStyles}border-collapse: separate;border-spacing:${columnGap}px;">
361
+ <table border="0" cellpadding="0" cellspacing="${columnGap}" width="100%" role="presentation"
362
+ style="${exports.tableCommonStyle} ${tableStyles}border-collapse: separate;border-spacing:${columnGap}px;" class="${gridVisibilityClass}">
307
363
  `;
308
364
  for (let r = 0; r < visualRows; r++) {
309
365
  html += "<tr>";
366
+ let visibleCellsInRow = 0;
367
+ const rowCellVisibility = [];
368
+ for (let c = 0; c < columns; c++) {
369
+ const idx = r * columns + c;
370
+ const childId = childrenIds[idx];
371
+ if (childId) {
372
+ const child = rootData[childId];
373
+ const { props: childProps = {} } = child.data || {};
374
+ const isVisible = !childProps.hideOnDesktop;
375
+ rowCellVisibility.push(isVisible);
376
+ if (isVisible)
377
+ visibleCellsInRow++;
378
+ }
379
+ else {
380
+ rowCellVisibility.push(false);
381
+ }
382
+ }
383
+ const widthPerVisibleCell = visibleCellsInRow > 0 ? 100 / visibleCellsInRow : 100 / columns;
310
384
  for (let c = 0; c < columns; c++) {
311
385
  const idx = r * columns + c;
312
386
  const childId = childrenIds[idx];
313
- const widthPercent = cellWidths[c] ?? 100 / columns;
387
+ const widthPercent = cellWidths[c] ?? widthPerVisibleCell;
314
388
  if (childId) {
315
389
  const child = rootData[childId];
316
- const { style: cellStyle = {} } = child.data || {};
390
+ const { style: cellStyle = {}, props: childProps = {} } = child.data || {};
317
391
  const verticalAlign = cellStyle.verticalAlign || "top";
318
392
  const { html: childHtml, styles } = await convertGridCellBlock(child, rootData, widthPercent, cellWidthInPx);
319
- html += `
320
- <td
321
- width="${widthPercent}%"
322
- ${responsive ? 'class="stack-column"' : ""}
323
- style="vertical-align:${verticalAlign}; word-break:break-word; ${styles} "
324
- >
325
- ${childHtml}
326
- </td>`;
393
+ const cellVisibilityClass = (0, common_1.getVisibilityClass)(childProps);
394
+ if (!childProps.hideOnDesktop) {
395
+ html += `
396
+ <td
397
+ width="${widthPercent}%"
398
+ class="${[responsive ? "stack-column" : "", cellVisibilityClass]
399
+ .filter(Boolean)
400
+ .join(" ")}"
401
+ style="vertical-align:${verticalAlign}; word-break:break-word; ${styles}"
402
+ >
403
+ ${childHtml}
404
+ </td>`;
405
+ }
327
406
  }
328
407
  else {
329
408
  html += `<td width="${widthPercent}%" ${responsive ? 'class="stack-column"' : ""} style=""></td>`;
@@ -335,7 +414,7 @@ async function convertGridBlock(blockData, rootData, cellWidthInPx) {
335
414
  return html;
336
415
  }
337
416
  async function convertGridCellBlock(blockData, rootData, cellWidthPercent, parentCellWidthPx) {
338
- const { style = {}, childrenIds = [] } = blockData.data;
417
+ const { style = {}, childrenIds = [], props = {} } = blockData.data;
339
418
  const styles = buildStyles(style, {
340
419
  perChanges: [],
341
420
  pxChanges: allPxAttributes,
@@ -348,13 +427,17 @@ async function convertGridCellBlock(blockData, rootData, cellWidthPercent, paren
348
427
  innerHtmlParts.push(await convertToHtml(child, rootData, cellWidthPx));
349
428
  }
350
429
  }
430
+ const cellContent = innerHtmlParts.join("");
351
431
  return {
352
- html: innerHtmlParts.join(""),
432
+ html: cellContent,
353
433
  styles,
354
434
  };
355
435
  }
436
+ // Enhanced Video Block HTML Conversion with centered play button
356
437
  async function convertVideoBlock(blockData, cellWidthInPx) {
357
438
  const { style, props } = blockData.data;
439
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
440
+ const { hideOnDesktop } = props; // Get the hideOnDesktop prop
358
441
  const { videoUrl, youtubeVideoUrl, thumbnailUrl, altText } = props;
359
442
  const videoLink = youtubeVideoUrl || videoUrl || "#";
360
443
  let resolvedThumbnail = thumbnailUrl || "https://via.placeholder.com/480x360?text=No+Thumbnail";
@@ -386,10 +469,7 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
386
469
  else {
387
470
  percentWidth = "100%";
388
471
  }
389
- const innerContainerWidth = (parseFloat(percentWidth) / 100) *
390
- (cellWidthInPx -
391
- (style?.padding?.left || 0) -
392
- (style?.padding?.right || 0));
472
+ const innerContainerWidth = (parseFloat(percentWidth) / 100) * (cellWidthInPx - (style?.padding?.left || 0) - (style?.padding?.right || 0));
393
473
  const aspectRatio = 16 / 9;
394
474
  const calculatedHeight = innerContainerWidth / aspectRatio;
395
475
  const outerContainerStyles = buildStyles({
@@ -412,77 +492,99 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
412
492
  // VML centering math (for Outlook)
413
493
  const vmlLeft = innerContainerWidth / 2 - playIconWidth / 2;
414
494
  const vmlTop = calculatedHeight / 2 - playIconHeight / 2;
415
- const videoContent = `
416
- <!--[if mso]>
417
- <v:group xmlns:v="urn:schemas-microsoft-com:vml"
418
- coordsize="${innerContainerWidth},${calculatedHeight}"
419
- href="${videoLink}"
420
- style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
421
- <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px; stroked="t"
422
- strokeweight="${borderWidth}px"
423
- strokecolor="${borderColor}"
424
- ${borderRadius > 0
425
- ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"`
426
- : ""}
495
+ const shouldHideInOutlook = hideOnDesktop;
496
+ const outlookVideoContent = shouldHideInOutlook
497
+ ? `<!--[if !mso]><!-->
498
+ <v:group xmlns:v="urn:schemas-microsoft-com:vml"
499
+ coordsize="${innerContainerWidth},${calculatedHeight}"
500
+ href="${videoLink}"
501
+ style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
502
+ <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px; stroked="t"
503
+ strokeweight="${borderWidth}px"
504
+ strokecolor="${borderColor}"
505
+ ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
506
+ >
507
+ <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
508
+ </v:rect>
509
+ <v:shape type="#_x0000_t75"
510
+ style="position:absolute;
511
+ left:${vmlLeft.toFixed(1)}px;
512
+ top:${vmlTop.toFixed(1)}px;
513
+ width:${playIconWidth}px;
514
+ height:${playIconHeight}px;"
515
+ alt="Play" href="${videoLink}" title="${altText || "Video"}"
516
+ stroked="f" filled="t">
517
+ <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
518
+ </v:shape>
519
+ </v:group>
520
+ <!--<![endif]-->`
521
+ : `<!--[if mso]>
522
+ <v:group xmlns:v="urn:schemas-microsoft-com:vml"
523
+ coordsize="${innerContainerWidth},${calculatedHeight}"
524
+ href="${videoLink}"
525
+ style="width:${innerContainerWidth}px;height:${calculatedHeight}px;">
526
+ <v:rect fill="t" style="position:absolute;width:${innerContainerWidth}px;height:${calculatedHeight}px; stroked="t"
527
+ strokeweight="${borderWidth}px"
528
+ strokecolor="${borderColor}"
529
+ ${borderRadius > 0 ? `arcsize="${Math.min(borderRadius / calculatedHeight, 1).toFixed(2)}"` : ""}
530
+ >
531
+ <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
532
+ </v:rect>
533
+ <v:shape type="#_x0000_t75"
534
+ style="position:absolute;
535
+ left:${vmlLeft.toFixed(1)}px;
536
+ top:${vmlTop.toFixed(1)}px;
537
+ width:${playIconWidth}px;
538
+ height:${playIconHeight}px;"
539
+ alt="Play" href="${videoLink}" title="${altText || "Video"}"
540
+ stroked="f" filled="t">
541
+ <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
542
+ </v:shape>
543
+ </v:group>
544
+ <![endif]-->`;
545
+ const nonOutlookVideoContent = `<!--[if !mso]><!-->
546
+ <table
547
+ width="${innerContainerWidth}"
548
+ cellpadding="0"
549
+ cellspacing="0"
550
+ border="0"
551
+ role="presentation"
552
+ align="${style?.textAlign || "left"}"
553
+ style="
554
+ max-width: ${innerContainerWidth}px;
555
+ width: 100%;
556
+ height: ${calculatedHeight}px;
557
+ background-color: ${style?.backgroundColor || "#FFFFFF"};
558
+ background-image: url('${resolvedThumbnail}');
559
+ background-size: cover;
560
+ background-position: center;
561
+ background-repeat: no-repeat;
562
+ box-sizing: border-box;
563
+ border: ${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};
564
+ border-radius: ${borderRadius}px;
565
+ "
427
566
  >
428
- <v:fill src="${resolvedThumbnail}" type="frame" color="${style?.backgroundColor || "#FFFFFF"}"/>
429
- </v:rect>
430
- <v:shape type="#_x0000_t75"
431
- style="position:absolute;
432
- left:${vmlLeft.toFixed(1)}px;
433
- top:${vmlTop.toFixed(1)}px;
434
- width:${playIconWidth}px;
435
- height:${playIconHeight}px;"
436
- alt="Play" href="${videoLink}" title="${altText || "Video"}"
437
- stroked="f" filled="t">
438
- <v:imagedata src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png" />
439
- </v:shape>
440
- </v:group>
441
- <![endif]-->
442
-
443
- <!--[if !mso]><!-->
444
- <table
445
- width="${innerContainerWidth}"
446
- cellpadding="0"
447
- cellspacing="0"
448
- border="0"
449
- role="presentation"
450
- align="${style?.textAlign || "left"}"
451
- style="
452
- max-width: ${innerContainerWidth}px;
453
- width: 100%;
454
- height: ${calculatedHeight}px;
455
- background-color: ${style?.backgroundColor || "#FFFFFF"};
456
- background-image: url('${resolvedThumbnail}');
457
- background-size: cover;
458
- background-position: center;
459
- background-repeat: no-repeat;
460
- box-sizing: border-box;
461
- border: ${borderWidth}px ${style?.borderStyle || "solid"} ${borderColor};
462
- border-radius: ${borderRadius}px;
463
- "
464
- >
465
- <tr>
466
- <td style="padding: 0; height: ${calculatedHeight}px; text-align: center; vertical-align: middle;" valign="middle">
467
- <a href="${videoLink}" target="_blank" style="display:inline-block; border: 0; outline: none; text-decoration: none;">
468
- <img
469
- src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
470
- width="${playIconWidth}"
471
- alt="Play"
472
- style="display: block;
473
- border: 0;
474
- outline: none;
475
- text-decoration: none;
476
- height: auto;"
477
- />
478
- </a>
479
- </td>
480
- </tr>
481
- </table>
482
- <!--<![endif]-->
483
- `;
567
+ <tr>
568
+ <td style="padding: 0; height: ${calculatedHeight}px; text-align: center; vertical-align: middle;" valign="middle">
569
+ <a href="${videoLink}" target="_blank" style="display:inline-block; border: 0; outline: none; text-decoration: none;">
570
+ <img
571
+ src="https://app-rsrc.getbee.io/public/resources/components/widgetBar/video-content-icon-sets/light/type-01.png"
572
+ width="${playIconWidth}"
573
+ alt="Play"
574
+ style="display: block;
575
+ border: 0;
576
+ outline: none;
577
+ text-decoration: none;
578
+ height: auto;"
579
+ />
580
+ </a>
581
+ </td>
582
+ </tr>
583
+ </table>
584
+ <!--<![endif]-->`;
585
+ const videoContent = `${outlookVideoContent}${nonOutlookVideoContent}`;
484
586
  const wrapperHtml = `
485
- <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse;">
587
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0; padding:0; border-collapse: collapse;" class="${visibilityClass}">
486
588
  <tr>
487
589
  <td align="${style?.textAlign || "left"}" style="padding:0; ${outerContainerStyles}">
488
590
  <table border="0" cellpadding="0" cellspacing="0" role="presentation"
@@ -522,10 +624,158 @@ function computeArcSize(borderRadius, widthPx) {
522
624
  return Math.min(px / widthPx, 1).toFixed(2);
523
625
  }
524
626
  // ---------- Updated convertShapeBlock function ----------
627
+ // async function convertShapeBlock(blockData: IBlockData) {
628
+ // const { style, props } = blockData.data;
629
+ // const { shape, text, imageUrl } = props as any;
630
+ // const {
631
+ // width = "100",
632
+ // height = "150",
633
+ // padding = {},
634
+ // backgroundColor = "#2F80ED",
635
+ // borderRadius,
636
+ // borderWidth = 0,
637
+ // borderStyle = "solid",
638
+ // borderColor = "transparent",
639
+ // customCss,
640
+ // shapeColor,
641
+ // alignment = "left",
642
+ // msoBakeImageWithText,
643
+ // color = "#000000",
644
+ // fontSize = 14,
645
+ // verticalAlign = "center",
646
+ // } = style || {};
647
+ // const borderRadiusMap: Record<string, string> = {
648
+ // rectangle: "0",
649
+ // rounded: "10px",
650
+ // circle: "50%",
651
+ // oval: "50%",
652
+ // };
653
+ // let resolvedBorderRadius = borderRadius || borderRadiusMap[shape] || "0";
654
+ // let resolvedWidthPx =
655
+ // typeof width === "number"
656
+ // ? width
657
+ // : parseInt(width.toString().replace("px", ""), 10) || 100;
658
+ // let resolvedHeightPx =
659
+ // typeof height === "number"
660
+ // ? height
661
+ // : parseInt(height.toString().replace("px", ""), 10) || 150;
662
+ // // --- Shape specific constraints ---
663
+ // if (shape === "circle") {
664
+ // const side = Math.min(resolvedWidthPx, resolvedHeightPx);
665
+ // resolvedWidthPx = side;
666
+ // resolvedHeightPx = side;
667
+ // resolvedBorderRadius = "50%";
668
+ // } else if (shape === "oval") {
669
+ // resolvedBorderRadius = "50% / 50%";
670
+ // }
671
+ // const finalBackgroundColor = shapeColor || backgroundColor;
672
+ // const alignmentStyles = {
673
+ // left: "margin-right:auto;margin-left:0;",
674
+ // center: "margin-left:auto;margin-right:auto;",
675
+ // right: "margin-left:auto;margin-right:0;",
676
+ // };
677
+ // const alignmentStyle =
678
+ // alignmentStyles[alignment as keyof typeof alignmentStyles] || "";
679
+ // const verticalAlignStyles = {
680
+ // top: "align-items:flex-start;padding-top:8px;",
681
+ // center: "align-items:center;",
682
+ // bottom: "align-items:flex-end;padding-bottom:8px;",
683
+ // };
684
+ // const verticalAlignStyle =
685
+ // verticalAlignStyles[verticalAlign as keyof typeof verticalAlignStyles] ||
686
+ // verticalAlignStyles.center;
687
+ // // Text styling (safe across clients)
688
+ // const textSizeStyle = `font-size:${fontSize}px;line-height:1.3;word-break:break-word;overflow-wrap:break-word;text-align:center;color:${color};`;
689
+ // // ============================
690
+ // // Modern HTML (non-MSO)
691
+ // // ============================
692
+ // let nonMsoContent = "";
693
+ // if (imageUrl && text) {
694
+ // nonMsoContent = `
695
+ // <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
696
+ // border:${borderWidth}px ${borderStyle} ${borderColor};
697
+ // border-radius:${resolvedBorderRadius};
698
+ // background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
699
+ // overflow:hidden;${alignmentStyle}${customCss || ""}">
700
+ // <div style="width:100%;height:100%;display:flex;${verticalAlignStyle}justify-content:center;overflow:hidden;">
701
+ // <div style="${textSizeStyle}padding:6px;max-width:90%;-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden;">
702
+ // ${text}
703
+ // </div>
704
+ // </div>
705
+ // </div>`;
706
+ // } else if (imageUrl) {
707
+ // nonMsoContent = `
708
+ // <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
709
+ // border:${borderWidth}px ${borderStyle} ${borderColor};
710
+ // border-radius:${resolvedBorderRadius};
711
+ // overflow:hidden;${alignmentStyle}${customCss || ""}">
712
+ // <img src="${imageUrl}" alt="${text || "shape image"}"
713
+ // width="${resolvedWidthPx}" height="${resolvedHeightPx}"
714
+ // style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
715
+ // </div>`;
716
+ // } else {
717
+ // const circlePadding =
718
+ // shape === "circle" ? Math.round(resolvedHeightPx * 0.15) : 8;
719
+ // nonMsoContent = `
720
+ // <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
721
+ // background:${finalBackgroundColor};
722
+ // border:${borderWidth}px ${borderStyle} ${borderColor};
723
+ // border-radius:${resolvedBorderRadius};
724
+ // overflow:hidden;${alignmentStyle}${customCss || ""}">
725
+ // <div style="width:100%;height:100%;display:flex;${verticalAlignStyle}justify-content:center;padding:${circlePadding}px;box-sizing:border-box;">
726
+ // <div style="${textSizeStyle}max-width:90%;overflow:hidden;">
727
+ // ${text || ""}
728
+ // </div>
729
+ // </div>
730
+ // </div>`;
731
+ // }
732
+ // // ============================
733
+ // // Outlook (VML) version
734
+ // // ============================
735
+ // const outlookContent = await appendOutlookForShape(
736
+ // nonMsoContent,
737
+ // resolvedWidthPx,
738
+ // resolvedWidthPx,
739
+ // {
740
+ // shape,
741
+ // imageUrl,
742
+ // backgroundColor,
743
+ // shapeColor,
744
+ // borderWidth,
745
+ // borderColor,
746
+ // borderRadius: resolvedBorderRadius,
747
+ // heightPx: resolvedHeightPx,
748
+ // text,
749
+ // textColor: color,
750
+ // textSize: fontSize,
751
+ // verticalAlign,
752
+ // alignment,
753
+ // padding,
754
+ // msoBakeImageWithText,
755
+ // }
756
+ // );
757
+ // // ============================
758
+ // // Final combined block
759
+ // // ============================
760
+ // return `
761
+ // <table width="100%" style="border-collapse:collapse;table-layout:fixed;">
762
+ // <tr>
763
+ // <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${
764
+ // padding.bottom || 0
765
+ // }px ${padding.left || 0}px;text-align:${alignment};">
766
+ // ${outlookContent}
767
+ // <!--[if !mso]><!-->
768
+ // ${nonMsoContent}
769
+ // <!--<![endif]-->
770
+ // </td>
771
+ // </tr>
772
+ // </table>`;
773
+ // }
525
774
  async function convertShapeBlock(blockData) {
526
775
  const { style, props } = blockData.data;
527
776
  const { shape, text, imageUrl } = props;
528
- const { width = "100", height = "150", padding = {}, backgroundColor = "#2F80ED", borderRadius, borderWidth = 0, borderStyle = "solid", borderColor = "transparent", customCss, shapeColor, alignment = "left", msoBakeImageWithText, color = "#000000", fontSize = 14, verticalAlign = "center", } = style || {};
777
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
778
+ const { width = "100", height = "150", padding = {}, backgroundColor = "#2F80ED", borderRadius, borderWidth = 0, borderStyle = "solid", borderColor = "transparent", customCss, shapeColor, alignment = "left", msoBakeImageWithText, color = "#000000", fontSize = 14, textAlign = "center", verticalAlign = "middle", } = style || {};
529
779
  const borderRadiusMap = {
530
780
  rectangle: "0",
531
781
  rounded: "10px",
@@ -539,7 +789,7 @@ async function convertShapeBlock(blockData) {
539
789
  let resolvedHeightPx = typeof height === "number"
540
790
  ? height
541
791
  : parseInt(height.toString().replace("px", ""), 10) || 150;
542
- // --- Shape specific constraints ---
792
+ // --- Shape-specific constraints ---
543
793
  if (shape === "circle") {
544
794
  const side = Math.min(resolvedWidthPx, resolvedHeightPx);
545
795
  resolvedWidthPx = side;
@@ -550,25 +800,38 @@ async function convertShapeBlock(blockData) {
550
800
  resolvedBorderRadius = "50% / 50%";
551
801
  }
552
802
  const finalBackgroundColor = shapeColor || backgroundColor;
803
+ // --- Horizontal alignment for outer container ---
553
804
  const alignmentStyles = {
554
805
  left: "margin-right:auto;margin-left:0;",
555
806
  center: "margin-left:auto;margin-right:auto;",
556
807
  right: "margin-left:auto;margin-right:0;",
557
808
  };
558
809
  const alignmentStyle = alignmentStyles[alignment] || "";
559
- const verticalAlignStyles = {
560
- top: "align-items:flex-start;padding-top:8px;",
561
- center: "align-items:center;",
562
- bottom: "align-items:flex-end;padding-bottom:8px;",
810
+ // --- Text + vertical alignment maps ---
811
+ const textAlignMap = {
812
+ left: "left",
813
+ center: "center",
814
+ right: "right",
815
+ justify: "justify",
563
816
  };
564
- const verticalAlignStyle = verticalAlignStyles[verticalAlign] ||
565
- verticalAlignStyles.center;
566
- // Text styling (safe across clients)
567
- const textSizeStyle = `font-size:${fontSize}px;line-height:1.3;word-break:break-word;overflow-wrap:break-word;text-align:center;color:${color};`;
817
+ const textAlignStyle = textAlignMap[textAlign] || "center";
818
+ const flexJustify = textAlign === "left"
819
+ ? "flex-start"
820
+ : textAlign === "right"
821
+ ? "flex-end"
822
+ : "center";
823
+ const flexAlign = verticalAlign === "top"
824
+ ? "flex-start"
825
+ : verticalAlign === "bottom"
826
+ ? "flex-end"
827
+ : "center";
828
+ // --- Text styling ---
829
+ const textSizeStyle = `font-size:${fontSize}px;line-height:1.3;word-break:break-word;overflow-wrap:break-word;color:${color};`;
568
830
  // ============================
569
831
  // Modern HTML (non-MSO)
570
832
  // ============================
571
833
  let nonMsoContent = "";
834
+ // --- Case 1: Image + Text ---
572
835
  if (imageUrl && text) {
573
836
  nonMsoContent = `
574
837
  <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
@@ -576,13 +839,14 @@ async function convertShapeBlock(blockData) {
576
839
  border-radius:${resolvedBorderRadius};
577
840
  background:${finalBackgroundColor} url('${imageUrl}') center/cover no-repeat;
578
841
  overflow:hidden;${alignmentStyle}${customCss || ""}">
579
- <div style="width:100%;height:100%;display:flex;${verticalAlignStyle}justify-content:center;overflow:hidden;">
580
- <div style="${textSizeStyle}padding:6px;max-width:90%;-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden;">
842
+ <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};overflow:hidden;padding:6px;box-sizing:border-box;">
843
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
581
844
  ${text}
582
845
  </div>
583
846
  </div>
584
847
  </div>`;
585
848
  }
849
+ // --- Case 2: Image only ---
586
850
  else if (imageUrl) {
587
851
  nonMsoContent = `
588
852
  <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
@@ -594,24 +858,22 @@ async function convertShapeBlock(blockData) {
594
858
  style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
595
859
  </div>`;
596
860
  }
861
+ // --- Case 3: Text only ---
597
862
  else {
598
- const circlePadding = shape === "circle" ? Math.round(resolvedHeightPx * 0.15) : 8;
599
863
  nonMsoContent = `
600
864
  <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
601
865
  background:${finalBackgroundColor};
602
866
  border:${borderWidth}px ${borderStyle} ${borderColor};
603
867
  border-radius:${resolvedBorderRadius};
604
868
  overflow:hidden;${alignmentStyle}${customCss || ""}">
605
- <div style="width:100%;height:100%;display:flex;${verticalAlignStyle}justify-content:center;padding:${circlePadding}px;box-sizing:border-box;">
606
- <div style="${textSizeStyle}max-width:90%;overflow:hidden;">
869
+ <div style="width:100%;height:100%;display:flex;justify-content:${flexJustify};align-items:${flexAlign};padding:8px;box-sizing:border-box;">
870
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">
607
871
  ${text || ""}
608
872
  </div>
609
873
  </div>
610
874
  </div>`;
611
875
  }
612
- // ============================
613
- // Outlook (VML) version
614
- // ============================
876
+ // Outlook (VML) fallback
615
877
  const outlookContent = await appendOutlookForShape(nonMsoContent, resolvedWidthPx, resolvedWidthPx, {
616
878
  shape,
617
879
  imageUrl,
@@ -625,15 +887,14 @@ async function convertShapeBlock(blockData) {
625
887
  textColor: color,
626
888
  textSize: fontSize,
627
889
  verticalAlign,
890
+ textAlign, // ✅ added
628
891
  alignment,
629
892
  padding,
630
893
  msoBakeImageWithText,
631
- });
632
- // ============================
633
- // Final combined block
634
- // ============================
894
+ }, visibilityClass);
895
+ // Combine into table wrapper
635
896
  return `
636
- <table width="100%" style="border-collapse:collapse;table-layout:fixed;">
897
+ <table width="100%" style="border-collapse:collapse;table-layout:fixed;" class="${visibilityClass}">
637
898
  <tr>
638
899
  <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
639
900
  ${outlookContent}
@@ -644,15 +905,14 @@ async function convertShapeBlock(blockData) {
644
905
  </tr>
645
906
  </table>`;
646
907
  }
647
- // ---------- Updated VML builder with better text containment ----------
648
- function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, borderWidth, borderColor, borderRadius, text, textColor = "#000000", textSize = 14, verticalAlign = "center", msoHasBakedText = false, }) {
649
- // --- Basic setup ---
908
+ // ---------- Updated VML builder ----------
909
+ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, borderWidth, borderColor, borderRadius, text, textColor = "#000000", textSize = 14, verticalAlign = "middle", textAlign = "center", msoHasBakedText = false, }) {
650
910
  const bw = borderWidth || 0;
651
911
  const bc = borderColor || "transparent";
652
912
  const borderAttrs = bw > 0 ? `strokeweight="${bw}px" strokecolor="${bc}"` : `stroked="false"`;
653
913
  const fillColor = backgroundColor || "#2F80ED";
914
+ // Use frame for img fill so sizing is preserved
654
915
  const fillMarkup = `<v:fill ${imageUrl ? `src="${imageUrl}" type="frame" aspect="atleast"` : ""} color="${fillColor}" />`;
655
- // --- Shape tag ---
656
916
  let tag = "rect";
657
917
  let extraAttr = "";
658
918
  if (shape === "circle" || shape === "oval") {
@@ -662,15 +922,22 @@ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, bo
662
922
  tag = "roundrect";
663
923
  extraAttr = `arcsize="${computeArcSize(borderRadius, widthPx)}"`;
664
924
  }
665
- // --- Text alignment ---
666
- const vAlignMap = { top: "top", center: "middle", bottom: "bottom" };
925
+ // maps for vml
926
+ const vAlignMap = { top: "top", middle: "middle", bottom: "bottom" };
927
+ const hAlignMap = {
928
+ left: "left",
929
+ center: "center",
930
+ right: "right",
931
+ justify: "left",
932
+ }; // justify -> left fallback in VML
667
933
  const vAlign = vAlignMap[verticalAlign] || "middle";
668
- const safeFontSize = Math.max(textSize, 10);
669
- // --- Text inside shape ---
934
+ const hAlign = hAlignMap[textAlign] || "center";
935
+ const safeFontSize = Math.max(Math.round(textSize), 10);
936
+ // Build the textbox with table/cell for reliable vertical centering in Outlook
670
937
  const textboxMarkup = text && !msoHasBakedText
671
938
  ? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
672
939
  <div style="display:table;width:100%;height:100%;">
673
- <div style="display:table-cell;vertical-align:${vAlign};text-align:center;">
940
+ <div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
674
941
  <div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
675
942
  ${text}
676
943
  </div>
@@ -678,20 +945,17 @@ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, bo
678
945
  </div>
679
946
  </v:textbox>`
680
947
  : `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
681
- // --- Final shape markup ---
948
+ // Return VML shape
682
949
  return `
683
950
  <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
684
- style="width:${widthPx}px;height:${heightPx}px;
685
- mso-position-horizontal:center;
686
- mso-position-vertical:center;"
951
+ style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
687
952
  ${borderAttrs}
688
953
  fill="true" fillcolor="${fillColor}"${extraAttr}>
689
954
  ${fillMarkup}
690
955
  ${textboxMarkup}
691
956
  </v:${tag}>`;
692
957
  }
693
- // ---------- Updated appendOutlookForShape ----------
694
- async function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts) {
958
+ function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts, visibilityClass) {
695
959
  const widthPx = Math.round(Math.min(outerContainerWidth, innerContainerWidth));
696
960
  const heightPx = Math.max(1, Math.round(opts.heightPx));
697
961
  const vml = buildVMLShape({
@@ -706,14 +970,31 @@ async function appendOutlookForShape(content, outerContainerWidth, innerContaine
706
970
  text: opts.text,
707
971
  textColor: opts.textColor,
708
972
  textSize: opts.textSize,
973
+ verticalAlign: opts.verticalAlign,
974
+ textAlign: opts.textAlign,
709
975
  msoHasBakedText: Boolean(opts.msoBakeImageWithText),
710
976
  });
711
977
  const pad = opts.padding || {};
712
978
  const align = opts.alignment || "left";
713
979
  const valign = opts.verticalAlign || "middle";
980
+ const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
981
+ // Fix: Properly handle Outlook visibility with conditional comments
982
+ if (shouldHideInOutlook) {
983
+ return `<!--[if !mso]><!-->
984
+ <table align="${align}" border="0" cellpadding="0" cellspacing="0"
985
+ style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
986
+ <tr>
987
+ <td valign="${valign}"
988
+ style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
989
+ ${vml}
990
+ </td>
991
+ </tr>
992
+ </table>
993
+ <!--<![endif]-->`;
994
+ }
714
995
  return `<!--[if mso]>
715
996
  <table align="${align}" border="0" cellpadding="0" cellspacing="0"
716
- style="width:${widthPx}px;height:${heightPx}px;">
997
+ style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
717
998
  <tr>
718
999
  <td valign="${valign}"
719
1000
  style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
@@ -721,11 +1002,12 @@ async function appendOutlookForShape(content, outerContainerWidth, innerContaine
721
1002
  </td>
722
1003
  </tr>
723
1004
  </table>
724
- <![endif]-->`;
1005
+ <![endif]-->`;
725
1006
  }
726
1007
  function convertVerticalDividerBlockToHtml(blockData) {
727
- const { style } = blockData.data;
1008
+ const { style, props } = blockData.data;
728
1009
  const { width, height, dividerColor, ...rest } = style;
1010
+ const visibilityClass = (0, common_1.getVisibilityClass)(props);
729
1011
  // Convert other styles to inline-safe HTML attributes
730
1012
  const convertedStyle = buildStyles(rest, {
731
1013
  perChanges: [],
@@ -746,5 +1028,5 @@ function convertVerticalDividerBlockToHtml(blockData) {
746
1028
  </tr>
747
1029
  </table>
748
1030
  `;
749
- return appendOutlookSupport(dividerContent, convertedStyle);
1031
+ return appendOutlookSupport(dividerContent, convertedStyle, visibilityClass);
750
1032
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email-builder-utils",
3
- "version": "1.1.28",
3
+ "version": "1.1.30",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [