@pagenflow/email 1.4.6 → 1.4.8

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.
package/dist/index.esm.js CHANGED
@@ -1531,6 +1531,57 @@ function arePropsEqual(prevProps, nextProps) {
1531
1531
  return isEqual(prevProps, nextProps);
1532
1532
  }
1533
1533
 
1534
+ /**
1535
+ * bindingAttribute.ts
1536
+ * -------------------
1537
+ * Serialises DataBindings into discrete HTML attributes:
1538
+ *
1539
+ * data-bind-if → visible condition → stamped on the root element
1540
+ * data-bind-list → repeater (dataList + itemAlias) → stamped on the
1541
+ * direct parent of the children loop, NOT the root
1542
+ * data-bind → propertyMap → stamped on the root element
1543
+ *
1544
+ * Keeping the three concerns in separate attributes lets the post-processor
1545
+ * handle them independently without ambiguity.
1546
+ */
1547
+ // ─── Root element props (visible + propertyMap) ───────────────────────────────
1548
+ /**
1549
+ * Props to spread onto the component's root element.
1550
+ * Carries `data-bind-if` and/or `data-bind` (propertyMap).
1551
+ */
1552
+ function rootBindingProps(bindings) {
1553
+ if (!bindings)
1554
+ return {};
1555
+ const props = {};
1556
+ if (bindings.visible) {
1557
+ props['data-bind-if'] = bindings.visible;
1558
+ }
1559
+ if (bindings.propertyMap && Object.keys(bindings.propertyMap).length) {
1560
+ props['data-bind'] = JSON.stringify({ propertyMap: bindings.propertyMap });
1561
+ }
1562
+ return props;
1563
+ }
1564
+ // ─── List wrapper props (dataList + itemAlias) ────────────────────────────────
1565
+ /**
1566
+ * Props to spread onto the *direct parent* of the children loop.
1567
+ * Carries `data-bind-list` only when a repeater is defined.
1568
+ *
1569
+ * Usage inside a component:
1570
+ * <tbody {...listBindingProps(bindings)}>
1571
+ * {children}
1572
+ * </tbody>
1573
+ */
1574
+ function listBindingProps(bindings) {
1575
+ if (!(bindings === null || bindings === void 0 ? void 0 : bindings.dataList))
1576
+ return {};
1577
+ return {
1578
+ 'data-bind-list': JSON.stringify({
1579
+ dataList: bindings.dataList,
1580
+ ...(bindings.itemAlias && { itemAlias: bindings.itemAlias }),
1581
+ }),
1582
+ };
1583
+ }
1584
+
1534
1585
  // Map alignment to HTML 'align' attribute
1535
1586
  const justifyMap$3 = {
1536
1587
  start: "left",
@@ -1538,7 +1589,7 @@ const justifyMap$3 = {
1538
1589
  end: "right",
1539
1590
  };
1540
1591
  // Helper to build link href based on innerLink type (mirrors Icon component)
1541
- function buildLinkHref$2(innerLink) {
1592
+ function buildLinkHref$4(innerLink) {
1542
1593
  if (!innerLink || innerLink.type === "none")
1543
1594
  return null;
1544
1595
  switch (innerLink.type) {
@@ -1592,10 +1643,10 @@ function getBorderStyleString$2(border) {
1592
1643
  }
1593
1644
  return styles.join(" ");
1594
1645
  }
1595
- function Button({ config, devMode }) {
1646
+ function Button({ config, devMode, bindings }) {
1596
1647
  const { innerLink, children, backgroundColor, color, padding, borderRadius, border, width, maxWidth, justifyContent, textAlign, fontSize, fontWeight, fontStyle, fontFamily, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, opacity, whiteSpace, wordBreak, } = config;
1597
1648
  // Resolve href from innerLink
1598
- const href = buildLinkHref$2(innerLink);
1649
+ const href = buildLinkHref$4(innerLink);
1599
1650
  const target = (innerLink === null || innerLink === void 0 ? void 0 : innerLink.target) || "_blank";
1600
1651
  // Sanitize fontFamily early so safeFontFamily is available for all paths below.
1601
1652
  const safeFontFamily = fontFamily
@@ -1679,7 +1730,7 @@ function Button({ config, devMode }) {
1679
1730
  .join(" ");
1680
1731
  return (
1681
1732
  // Wrapper table for alignment - maintains proper positioning for hover indicators
1682
- jsx("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
1733
+ jsx("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
1683
1734
  width: "100%",
1684
1735
  borderCollapse: "collapse",
1685
1736
  boxSizing: "border-box",
@@ -1787,7 +1838,7 @@ function getBorderStyle$5(border) {
1787
1838
  }
1788
1839
  return style;
1789
1840
  }
1790
- function Column({ children, config, devNode }) {
1841
+ function Column({ children, config, devNode, bindings }) {
1791
1842
  var _a, _b, _c;
1792
1843
  // Process children array for gap support
1793
1844
  const childrenArray = (Array.isArray(children) ? children : [children]).filter((child) => child != null);
@@ -1801,6 +1852,9 @@ function Column({ children, config, devNode }) {
1801
1852
  };
1802
1853
  // 2. Outer TD style: Background and Border Radius (no border here).
1803
1854
  // height is set so the TD occupies the full declared height.
1855
+ // When maxWidth is set, the outer TD stays at its normal width so it
1856
+ // always fills its parent — the inner maxWidth table (see below) does
1857
+ // the actual capping.
1804
1858
  const outerTdStyle = {
1805
1859
  width: config.width,
1806
1860
  height: config.height,
@@ -1836,6 +1890,7 @@ function Column({ children, config, devNode }) {
1836
1890
  // treat it as content-box height and add padding on top, causing the
1837
1891
  // total to exceed the declared height in preview mode.
1838
1892
  verticalAlign: config.alignItems ? alignMap$2[config.alignItems] : "top",
1893
+ background: "transparent",
1839
1894
  };
1840
1895
  // 4. Gap spacer style (used between children)
1841
1896
  const gapSpacerStyle = {
@@ -1843,12 +1898,25 @@ function Column({ children, config, devNode }) {
1843
1898
  lineHeight: "1px",
1844
1899
  fontSize: "1px",
1845
1900
  width: "100%",
1901
+ background: "transparent",
1902
+ };
1903
+ // 5. maxWidth constraining table style (modern clients).
1904
+ // The `width` HTML attribute on this table is what Outlook Classic
1905
+ // (Word engine) reads — it has no concept of max-width, but it does
1906
+ // honour the `width` attribute as a hard column cap.
1907
+ // The CSS max-width here handles modern web/email clients correctly.
1908
+ // <center> around it ensures the constrained block stays horizontally
1909
+ // centred in both the Word engine and standards-based renderers.
1910
+ const maxWidthTableStyle = {
1911
+ width: "100%",
1912
+ maxWidth: config.maxWidth,
1913
+ borderCollapse: "collapse",
1846
1914
  };
1847
1915
  // Main content rendering
1848
- const renderContent = () => (jsx("table", { "aria-label": "Column Padding", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: innerTableStyle, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: innerTdStyle, valign: config.justifyContent ? vAlignMap[config.justifyContent] : "top", align: config.alignItems ? alignMap$2[config.alignItems] : "left", children: config.gap && numChildren > 1 ? (jsx("table", { "aria-label": "Column Gap Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
1916
+ const renderContent = () => (jsx("table", { "aria-label": "Column Padding", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: innerTableStyle, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: innerTdStyle, valign: config.justifyContent ? vAlignMap[config.justifyContent] : "top", align: config.alignItems ? alignMap$2[config.alignItems] : "left", ...(numChildren > 1 ? {} : listBindingProps(bindings)), children: numChildren > 1 ? (jsx("table", { "aria-label": "Column Gap Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
1849
1917
  width: "100%",
1850
1918
  borderCollapse: "collapse",
1851
- }, children: jsx("tbody", { children: childrenArray.map((child, index) => (jsxs(Fragment, { children: [jsx("tr", { children: jsx("td", { style: {
1919
+ }, children: jsx("tbody", { ...listBindingProps(bindings), children: childrenArray.map((child, index) => (jsxs(Fragment, { children: [jsx("tr", { children: jsx("td", { style: {
1852
1920
  verticalAlign: config.alignItems
1853
1921
  ? alignMap$2[config.alignItems]
1854
1922
  : "top",
@@ -1857,10 +1925,27 @@ function Column({ children, config, devNode }) {
1857
1925
  : "top", align: config.alignItems
1858
1926
  ? alignMap$2[config.alignItems]
1859
1927
  : "left", children: child }) }), index < numChildren - 1 && (jsx("tr", { children: jsx("td", { style: gapSpacerStyle, children: "\u00A0" }) }))] }, `col-child-${index}`))) }) })) : (children) }) }) }) }));
1860
- return (jsxs("table", { "aria-label": "Column Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
1928
+ return (jsxs("table", { "aria-label": "Column Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
1861
1929
  position: "relative",
1862
1930
  ...outerTableStyle,
1863
- }, ...(config.height && { height: config.height }), children: [jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: outerTdStyle, ...(config.width && { width: config.width }), ...(config.height && { height: config.height }), children: renderContent() }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] }));
1931
+ }, ...(config.height && { height: config.height }), children: [jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: outerTdStyle, ...(config.width && { width: config.width }), ...(config.height && { height: config.height }), children: config.maxWidth ? (
1932
+ /*
1933
+ * maxWidth wrapper — Outlook Classic compatibility pattern:
1934
+ *
1935
+ * <center> instructs the Word rendering engine to horizontally
1936
+ * centre its child block, equivalent to margin: 0 auto in CSS.
1937
+ *
1938
+ * The inner table carries the `width` HTML attribute set to the
1939
+ * maxWidth value. Outlook Classic reads `width` as a hard pixel
1940
+ * cap; it has no concept of max-width so this is the only lever
1941
+ * available. Modern clients receive the CSS max-width on the
1942
+ * same table and behave correctly.
1943
+ *
1944
+ * The outer column remains at its normal width so it always
1945
+ * fills its parent cell in every client — only the inner
1946
+ * content is capped.
1947
+ */
1948
+ jsx("center", { children: jsx("table", { "aria-label": "Column Max Width Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, width: config.maxWidth, style: maxWidthTableStyle, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { children: renderContent() }) }) }) }) })) : (renderContent()) }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] }));
1864
1949
  }
1865
1950
  var Column_default = memo(Column, arePropsEqual);
1866
1951
 
@@ -1909,7 +1994,7 @@ function getBorderStyle$4(border) {
1909
1994
  }
1910
1995
  return style;
1911
1996
  }
1912
- function Container({ children, config, devMode, devNode }) {
1997
+ function Container({ children, config, bindings, devMode, devNode, }) {
1913
1998
  var _a, _b, _c;
1914
1999
  const { widthType, childrenConstraints } = config;
1915
2000
  const childrenArray = (Array.isArray(children) ? children : [children]).filter((child) => child != null);
@@ -1927,7 +2012,7 @@ function Container({ children, config, devMode, devNode }) {
1927
2012
  return 0;
1928
2013
  })();
1929
2014
  const getChildWidths = (() => {
1930
- const { widthDistributionType } = childrenConstraints;
2015
+ const { widthDistributionType } = childrenConstraints !== null && childrenConstraints !== void 0 ? childrenConstraints : {};
1931
2016
  const totalGapSpace = gapWidthPx * (numChildren > 1 ? numChildren - 1 : 0);
1932
2017
  const remainingContentSpace = containerWidthPx - totalGapSpace;
1933
2018
  switch (widthDistributionType) {
@@ -2001,6 +2086,7 @@ function Container({ children, config, devMode, devNode }) {
2001
2086
  width: config.gap || "0",
2002
2087
  lineHeight: "1px",
2003
2088
  fontSize: "1px",
2089
+ background: "transparent",
2004
2090
  };
2005
2091
  const justifyAlign = config.justifyContent
2006
2092
  ? justifyMap$2[config.justifyContent]
@@ -2021,6 +2107,7 @@ function Container({ children, config, devMode, devNode }) {
2021
2107
  fontSize: "0",
2022
2108
  lineHeight: "0",
2023
2109
  height: config.gap,
2110
+ background: "transparent",
2024
2111
  }, children: "\u00A0" }))] }, `child-${index}`), jsx("td", { className: isStacking ? "desktop-gap-column" : undefined, width: config.gap, style: gapTdStyle, children: "\u00A0" }, `gap-${index}`)] }, `ctn:${index}`));
2025
2112
  }
2026
2113
  return (jsx("td", { className: isStacking ? "stack-td" : undefined, width: getChildWidths[index], style: childTdStyle, children: child }, `child-${index}`));
@@ -2028,7 +2115,7 @@ function Container({ children, config, devMode, devNode }) {
2028
2115
  return (jsx("table", { "aria-label": `Container | Table Outer`, cellPadding: 0, cellSpacing: 0, role: "presentation", border: 0, style: {
2029
2116
  position: "relative",
2030
2117
  ...outerTableStyle,
2031
- }, children: jsx("tbody", { children: jsx("tr", { children: jsxs("td", { align: justifyAlign, children: [jsx("div", { dangerouslySetInnerHTML: { __html: msoFixedWrapper } }), jsxs("table", { className: [
2118
+ }, ...rootBindingProps(bindings), children: jsx("tbody", { children: jsx("tr", { children: jsxs("td", { align: justifyAlign, children: [jsx("div", { dangerouslySetInnerHTML: { __html: msoFixedWrapper } }), jsxs("table", { className: [
2032
2119
  widthType === "fixed" ? "container-fixed-width" : undefined,
2033
2120
  devMode ? "main-wrapper relative" : undefined,
2034
2121
  ]
@@ -2037,22 +2124,22 @@ function Container({ children, config, devMode, devNode }) {
2037
2124
  width: "100%",
2038
2125
  maxWidth: widthType === "fixed" ? config.width || "600px" : undefined,
2039
2126
  borderCollapse: "collapse",
2040
- }, width: containerWidthAttr, children: [jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: backgroundTdStyle, children: jsx("table", { "aria-label": `Container | Border Wrapper`, cellPadding: 0, cellSpacing: 0, role: "presentation", border: 0, style: borderTableStyle, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: innerTdStyle, children: jsx("table", { "aria-label": `Container | Content Table`, cellPadding: 0, cellSpacing: 0, role: "presentation", border: 0, style: contentTableStyle, children: jsx("tbody", { children: jsx("tr", { children: rowElements }) }) }) }) }) }) }) }) }) }), !!devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] }), jsx("div", { dangerouslySetInnerHTML: { __html: msoFixedFooter } })] }) }) }) }));
2127
+ }, width: containerWidthAttr, children: [jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: backgroundTdStyle, children: jsx("table", { "aria-label": `Container | Border Wrapper`, cellPadding: 0, cellSpacing: 0, role: "presentation", border: 0, style: borderTableStyle, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: innerTdStyle, children: jsx("table", { "aria-label": `Container | Content Table`, cellPadding: 0, cellSpacing: 0, role: "presentation", border: 0, style: contentTableStyle, children: jsx("tbody", { children: jsx("tr", { ...listBindingProps(bindings), children: rowElements }) }) }) }) }) }) }) }) }) }), !!devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] }), jsx("div", { dangerouslySetInnerHTML: { __html: msoFixedFooter } })] }) }) }) }));
2041
2128
  }
2042
2129
  var Container_default = memo(Container, arePropsEqual);
2043
2130
 
2044
- function Divider({ config, devNode }) {
2131
+ function Divider({ config, devNode, bindings }) {
2045
2132
  const { height = "1px", color = "#cccccc", width = "100%", margin = "20px 0", align = "center", hideOnMobile, } = config;
2046
2133
  const heightPx = parseInt(height, 10) || 1;
2047
2134
  // Parse margin into paddingTop / paddingBottom for the outer TD.
2048
2135
  // Outlook ignores shorthand "20px 0" on TDs — must be explicit.
2049
- const [marginTopRaw = "0", marginRightRaw = "0", marginBottomRaw, marginLeftRaw] = margin.trim().split(/\s+/);
2136
+ const [marginTopRaw = "0", marginRightRaw = "0", marginBottomRaw, marginLeftRaw,] = margin.trim().split(/\s+/);
2050
2137
  const marginTop = marginTopRaw;
2051
2138
  const marginBottom = marginBottomRaw !== null && marginBottomRaw !== void 0 ? marginBottomRaw : marginTopRaw; // "20px 0" → top=20px, bottom=20px
2052
2139
  // Outlook requires align on the outer TD to correctly position
2053
2140
  // a fixed-width inner table (e.g. width="300px").
2054
2141
  const alignAttr = align === "left" ? "left" : align === "right" ? "right" : "center";
2055
- return (jsxs("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
2142
+ return (jsxs("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
2056
2143
  position: "relative", // dev overlay anchor
2057
2144
  width: "100%",
2058
2145
  borderCollapse: "collapse",
@@ -2192,36 +2279,7 @@ function Head({ children, backgroundColor = "#ffffff", title = "Email Preview",
2192
2279
  mso-line-height-rule: exactly;
2193
2280
  }
2194
2281
  }
2195
- @media only screen and (max-width: 768px) {
2196
- .row-content-table[data-mobile-justify="center"] { margin: 0 auto !important; float: none !important; }
2197
- .row-content-table[data-mobile-justify="start"] { margin: 0 !important; float: left !important; }
2198
- .row-content-table[data-mobile-justify="end"] { margin: 0 0 0 auto !important; float: right !important; }
2199
- .row-content-table[data-mobile-align="center"] .child-cell { vertical-align: middle !important; }
2200
- .row-content-table[data-mobile-align="start"] .child-cell { vertical-align: top !important; }
2201
- .row-content-table[data-mobile-align="end"] .child-cell { vertical-align: bottom !important; }
2202
- .row-content-table[data-mobile-wrap="true"] { width: 100% !important; max-width: 100% !important; }
2203
- .row-content-table[data-mobile-wrap="true"] > tbody > .content-tr { display: block !important; }
2204
- .row-content-table[data-mobile-wrap="true"] > tbody > .content-tr > .child-cell {
2205
- display: block !important;
2206
- width: 100% !important;
2207
- box-sizing: border-box !important;
2208
- }
2209
- .row-content-table[data-mobile-wrap="true"] > tbody > .content-tr > .row-gap-td {
2210
- display: none !important;
2211
- width: 0 !important;
2212
- height: 0 !important;
2213
- }
2214
- .row-content-table[data-mobile-wrap="true"] > tbody > .content-tr > .child-cell:not(:last-child) {
2215
- margin-bottom: 20px !important;
2216
- }
2217
- ${["10px", "15px", "20px", "24px", "30px", "40px", ...rowGaps]
2218
- .filter((gap, index, self) => self.indexOf(gap) === index)
2219
- .map((gap) => `
2220
- .row-content-table[data-mobile-wrap="true"][data-gap="${gap}"] > tbody > .content-tr > .child-cell:not(:last-child) {
2221
- margin-bottom: ${gap} !important;
2222
- }`)
2223
- .join("\n")}
2224
- }
2282
+
2225
2283
  a { color: inherit; text-decoration: none; }
2226
2284
  ol, ul { margin: 0px; padding: 0px; list-style: none; }
2227
2285
  li {
@@ -2373,7 +2431,9 @@ function resolveAnchorStyles(anchor, fallback) {
2373
2431
  else if (fallback === null || fallback === void 0 ? void 0 : fallback[prop]) {
2374
2432
  resolved[prop] = fallback[prop];
2375
2433
  }
2376
- // if neither leave it out entirely, don't force inherit
2434
+ else if (prop === "text-decoration") {
2435
+ resolved[prop] = "none";
2436
+ }
2377
2437
  }
2378
2438
  return Object.entries(resolved)
2379
2439
  .map(([k, v]) => `${k}:${v}`)
@@ -2398,8 +2458,32 @@ function injectLinkStyles(html, fallback) {
2398
2458
  return result;
2399
2459
  }
2400
2460
 
2401
- function Heading({ config, devMode, children }) {
2402
- const { text, level = "h1", padding, color, textAlign, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, backgroundColor, wordBreak, whiteSpace, } = config;
2461
+ // Helper to build link href based on innerLink type
2462
+ function buildLinkHref$3(innerLink) {
2463
+ if (!innerLink || innerLink.type === "none")
2464
+ return null;
2465
+ switch (innerLink.type) {
2466
+ case "url":
2467
+ return innerLink.url || null;
2468
+ case "email":
2469
+ return innerLink.email ? `mailto:${innerLink.email}` : null;
2470
+ case "phone":
2471
+ return innerLink.phone ? `tel:${innerLink.phone}` : null;
2472
+ case "anchor":
2473
+ return innerLink.anchor ? `#${innerLink.anchor}` : null;
2474
+ case "page_top":
2475
+ return "#top";
2476
+ case "page_bottom":
2477
+ return "#bottom";
2478
+ default:
2479
+ return null;
2480
+ }
2481
+ }
2482
+ function Heading({ config, devMode, children, bindings }) {
2483
+ const { text, level = "h1", padding, color, textAlign, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, backgroundColor, wordBreak, whiteSpace, innerLink, } = config;
2484
+ // Resolve href and target from innerLink
2485
+ const href = buildLinkHref$3(innerLink);
2486
+ const target = (innerLink === null || innerLink === void 0 ? void 0 : innerLink.target) || "_blank";
2403
2487
  // Determine the content to render
2404
2488
  const content = text !== null && text !== void 0 ? text : children;
2405
2489
  const isString = typeof content === "string";
@@ -2436,12 +2520,17 @@ function Heading({ config, devMode, children }) {
2436
2520
  : "";
2437
2521
  // Dynamically create the Heading element
2438
2522
  const HeadingTag = level;
2523
+ const headingElement = isString ? (jsx(HeadingTag, { style: headingStyle, dangerouslySetInnerHTML: { __html: processedHtml } })) : (jsx(HeadingTag, { style: headingStyle, children: content }));
2439
2524
  return (
2440
2525
  // Wrap the heading content in a table for padding/width/background management.
2441
- jsx("table", { "aria-label": "Heading Block Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
2526
+ jsx("table", { "aria-label": "Heading Block Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
2442
2527
  width: "100%",
2443
2528
  borderCollapse: "collapse",
2444
- }, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: tdStyle, align: textAlign, children: isString ? (jsx(HeadingTag, { style: headingStyle, dangerouslySetInnerHTML: { __html: processedHtml } })) : (jsx(HeadingTag, { style: headingStyle, children: content })) }) }) }) }));
2529
+ }, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: tdStyle, align: textAlign, children: href && !devMode ? (jsx("a", { href: href, target: target, ...(target === "_blank" ? { rel: "noopener noreferrer" } : {}), style: {
2530
+ display: "block",
2531
+ textDecoration: "none",
2532
+ color: "inherit",
2533
+ }, children: headingElement })) : (headingElement) }) }) }) }));
2445
2534
  }
2446
2535
  var Heading_default = memo(Heading, arePropsEqual);
2447
2536
 
@@ -2466,7 +2555,7 @@ function Html({ children, backgroundColor = "#ffffff", }) {
2466
2555
  }
2467
2556
 
2468
2557
  // Helper to build link href based on innerLink type
2469
- function buildLinkHref$1(innerLink) {
2558
+ function buildLinkHref$2(innerLink) {
2470
2559
  if (!innerLink || innerLink.type === "none")
2471
2560
  return null;
2472
2561
  switch (innerLink.type) {
@@ -2526,11 +2615,15 @@ function getBorderStyleString$1(border) {
2526
2615
  styles.push(`border-left:${border.left.width} ${border.left.style} ${border.left.color} !important;`);
2527
2616
  return styles.join(" ");
2528
2617
  }
2529
- function Image({ config, devNode, devMode }) {
2618
+ function Image({ config, devNode, devMode, bindings }) {
2530
2619
  var _a, _b;
2531
- const { src, alt, innerLink, mobile } = config;
2620
+ const { src: originalSrc, alt, innerLink, mobile } = config;
2621
+ // In dev mode, if there's no src, use the placeholder
2622
+ const src = devMode && !originalSrc
2623
+ ? "https://placehold.co/300x200?text=select+an+image&font=poppins"
2624
+ : originalSrc;
2532
2625
  // Resolve href and target from innerLink
2533
- const href = buildLinkHref$1(innerLink);
2626
+ const href = buildLinkHref$2(innerLink);
2534
2627
  const target = (innerLink === null || innerLink === void 0 ? void 0 : innerLink.target) || "_blank";
2535
2628
  const seed = src + (alt || "");
2536
2629
  const instanceId = seed
@@ -2544,43 +2637,37 @@ function Image({ config, devNode, devMode }) {
2544
2637
  const widthAttr = desktopWidth.replace("px", "");
2545
2638
  const heightAttr = (_a = config.height) === null || _a === void 0 ? void 0 : _a.replace("px", "");
2546
2639
  // Determine the table's "initial" width.
2547
- // If it's 300px, the table should be 300px, not 100%.
2548
2640
  const tableWidth = isPercent ? desktopWidth : `${widthAttr}px`;
2549
- // When width is a percentage, Outlook ignores CSS and renders the image at
2550
- // its intrinsic pixel size. Setting a concrete `width` HTML attribute gives
2551
- // Outlook a value to constrain against while modern clients continue to use
2552
- // the CSS `width: 100%` for fluid rendering.
2553
- //
2554
- // If `maxWidth` is a pixel value (e.g. "600px"), we extract the number and
2555
- // use it as the HTML `width` attribute so Outlook enforces that cap.
2556
- // Other clients ignore the attribute and rely on CSS styles instead.
2557
- // If `maxWidth` is not set or is not a pixel value (e.g. "100%"), we fall
2558
- // back to the original behaviour (numeric string for px widths, undefined
2559
- // for % widths).
2560
- const maxWidthPx = ((_b = config.maxWidth) === null || _b === void 0 ? void 0 : _b.endsWith("px"))
2561
- ? parseInt(config.maxWidth, 10)
2562
- : undefined;
2563
- const imgWidthAttr = isPercent ? (maxWidthPx !== null && maxWidthPx !== void 0 ? maxWidthPx : undefined) : widthAttr;
2564
- // 2. Mobile Overrides only emit CSS properties that are explicitly set,
2565
- // so unspecified properties are left untouched (no forced defaults).
2641
+ // Calculate width attribute for Outlook
2642
+ let imgWidthAttr;
2643
+ if (config.outlookWidth) {
2644
+ // Use explicit outlookWidth if provided
2645
+ imgWidthAttr = parseInt(config.outlookWidth, 10);
2646
+ }
2647
+ else if (isPercent) {
2648
+ // For percentage widths, use maxWidth as fallback for Outlook
2649
+ const maxWidthPx = ((_b = config.maxWidth) === null || _b === void 0 ? void 0 : _b.endsWith("px"))
2650
+ ? parseInt(config.maxWidth, 10)
2651
+ : undefined;
2652
+ imgWidthAttr = maxWidthPx !== null && maxWidthPx !== void 0 ? maxWidthPx : undefined;
2653
+ }
2654
+ else {
2655
+ // For fixed pixel widths, use that value
2656
+ imgWidthAttr = widthAttr ? parseInt(widthAttr, 10) : undefined;
2657
+ }
2658
+ // 2. Mobile Overrides
2566
2659
  let mobileCss = "";
2567
2660
  if (mobile) {
2568
- // .wrap-${imgClass} rules
2569
- const wrapRules = [
2570
- // Always reset min-width so the px lock from desktop can be overridden
2571
- "min-width: 0 !important;",
2572
- ];
2661
+ const wrapRules = ["min-width: 0 !important;"];
2573
2662
  if (mobile.width !== undefined)
2574
2663
  wrapRules.push(`width: ${mobile.width} !important;`);
2575
2664
  if (mobile.maxWidth !== undefined)
2576
2665
  wrapRules.push(`max-width: ${mobile.maxWidth} !important;`);
2577
- // .td-${imgClass} rules
2578
2666
  const tdRules = [];
2579
2667
  if (mobile.padding !== undefined)
2580
2668
  tdRules.push(`padding: ${mobile.padding} !important;`);
2581
2669
  if (mobile.backgroundColor !== undefined)
2582
2670
  tdRules.push(`background-color: ${mobile.backgroundColor} !important;`);
2583
- // .${imgClass} rules
2584
2671
  const imgRules = [];
2585
2672
  if (mobile.width !== undefined)
2586
2673
  imgRules.push(`width: ${mobile.width} !important;`);
@@ -2603,7 +2690,6 @@ function Image({ config, devNode, devMode }) {
2603
2690
  mobileCss = `
2604
2691
  @media screen and (max-width: 768px) {
2605
2692
  .wrap-${imgClass} {
2606
- /* This breaks the px lock from desktop and makes it fluid */
2607
2693
  ${wrapRules.join("\n ")}
2608
2694
  }
2609
2695
  .td-${imgClass} {
@@ -2629,9 +2715,27 @@ function Image({ config, devNode, devMode }) {
2629
2715
  objectPosition: config.objectPosition,
2630
2716
  };
2631
2717
  const imageElement = (jsx("img", { src: src, alt: alt, width: imgWidthAttr, height: heightAttr !== "auto" ? heightAttr : undefined, className: imgClass, style: imgStyle, draggable: !devMode }));
2632
- return (jsxs(Fragment$1, { children: [mobile && jsx("style", { dangerouslySetInnerHTML: { __html: mobileCss } }), jsxs("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, className: `wrap-${imgClass}`, align: "center" // Ensures a 300px image stays centered in its parent
2633
- , style: {
2634
- width: tableWidth, // Fixed px here prevents the 100% "ghost space"
2718
+ // Outlook Classic wrapper - only applied when outlookWidth is explicitly set
2719
+ // OR when we need to constrain a percentage-width image in Outlook
2720
+ const needsOutlookWrapper = config.outlookWidth || (isPercent && imgWidthAttr);
2721
+ const finalImageElement = needsOutlookWrapper ? (jsxs(Fragment$1, { children: [jsx("style", { dangerouslySetInnerHTML: {
2722
+ __html: `</style>` +
2723
+ `<!--[if mso]>` +
2724
+ `<table cellpadding="0" cellspacing="0" border="0" style="width: ${imgWidthAttr}px;">` +
2725
+ `<tr>` +
2726
+ `<td style="padding: 0; margin: 0;" width="${imgWidthAttr}">` +
2727
+ `<img src="${src}" alt="${alt || ""}" width="${imgWidthAttr}" height="${heightAttr !== "auto" ? heightAttr : ""}" style="display: block; width: 100%; height: auto;" />` +
2728
+ `</td>` +
2729
+ `</tr>` +
2730
+ `</table>` +
2731
+ `<![endif]-->` +
2732
+ `<!--[if !mso]><!-->` +
2733
+ `<style>`,
2734
+ } }), imageElement, jsx("style", { dangerouslySetInnerHTML: {
2735
+ __html: `</style>` + `<!--<![endif]-->` + `<style>`,
2736
+ } })] })) : (imageElement);
2737
+ return (jsxs(Fragment$1, { children: [mobile && jsx("style", { dangerouslySetInnerHTML: { __html: mobileCss } }), jsxs("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, className: `wrap-${imgClass}`, align: "center", ...rootBindingProps(bindings), style: {
2738
+ width: tableWidth,
2635
2739
  maxWidth: "100%",
2636
2740
  borderCollapse: "collapse",
2637
2741
  margin: "0 auto",
@@ -2640,10 +2744,10 @@ function Image({ config, devNode, devMode }) {
2640
2744
  backgroundColor: config.backgroundColor,
2641
2745
  fontSize: "0",
2642
2746
  lineHeight: "0",
2643
- width: tableWidth, // Lock the cell as well
2747
+ width: tableWidth,
2644
2748
  }, children: href && !devMode ? (jsx("a", { href: href, target: target, ...(target === "_blank"
2645
2749
  ? { rel: "noopener noreferrer" }
2646
- : {}), style: { display: "block", width: "100%" }, children: imageElement })) : (imageElement) }) }) }), devMode && !!devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] })] }));
2750
+ : {}), style: { display: "block", width: "100%" }, children: finalImageElement })) : (finalImageElement) }) }) }), devMode && !!devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] })] }));
2647
2751
  }
2648
2752
  var Image_default = memo(Image, arePropsEqual);
2649
2753
 
@@ -2712,12 +2816,34 @@ function getHrefFromInnerLink(innerLink) {
2712
2816
  return undefined;
2713
2817
  }
2714
2818
  }
2715
- function Row({ children, config, devNode, devMode }) {
2716
- var _a, _b, _c, _d, _e, _f, _g;
2819
+ /**
2820
+ * Resolves the width for a child <td> at the given index based on layoutColumns.
2821
+ *
2822
+ * - "equal" → equal percentage share across all children
2823
+ * - string[] → explicit value at the matching index (px, %, or mixed)
2824
+ * - undefined → undefined, so no width attribute is set (retrocompat)
2825
+ */
2826
+ function resolveChildColumnWidth(layoutColumns, index, numChildren) {
2827
+ if (!layoutColumns)
2828
+ return undefined;
2829
+ if (layoutColumns === "equal")
2830
+ return `${100 / numChildren}%`;
2831
+ return layoutColumns[index];
2832
+ }
2833
+ function Row({ children, config, devNode, devMode, bindings }) {
2834
+ var _a, _b, _c, _d, _e, _f;
2717
2835
  const childrenArray = (Array.isArray(children) ? children : [children]).filter((child) => child != null);
2718
2836
  const numChildren = childrenArray.length;
2719
2837
  const href = getHrefFromInnerLink(config.innerLink);
2720
2838
  const target = ((_a = config.innerLink) === null || _a === void 0 ? void 0 : _a.target) || "_blank";
2839
+ // Whether children should stack on mobile.
2840
+ // Mirrors Container's isStacking pattern: drives stack-td / desktop-gap-column
2841
+ // / mobile-gap-spacer class names so that stacking works via non-@media CSS
2842
+ // rules that survive Gmail's stylesheet stripping.
2843
+ const isStacking = ((_b = config.mobile) === null || _b === void 0 ? void 0 : _b.wrap) === true && numChildren > 1;
2844
+ // Whether layoutColumns is active. When true, table-layout:fixed is applied
2845
+ // to the content table so Outlook Classic honours the declared column widths.
2846
+ const hasLayoutColumns = config.layoutColumns !== undefined;
2721
2847
  // 1. Outer TD: Background, Border Radius, Width, Height.
2722
2848
  const backgroundTdStyle = {
2723
2849
  backgroundColor: config.backgroundColor,
@@ -2727,9 +2853,9 @@ function Row({ children, config, devNode, devMode }) {
2727
2853
  backgroundImage: config.backgroundImage
2728
2854
  ? `url(${config.backgroundImage.src})`
2729
2855
  : undefined,
2730
- backgroundRepeat: (_b = config.backgroundImage) === null || _b === void 0 ? void 0 : _b.repeat,
2731
- backgroundSize: (_c = config.backgroundImage) === null || _c === void 0 ? void 0 : _c.size,
2732
- backgroundPosition: (_d = config.backgroundImage) === null || _d === void 0 ? void 0 : _d.position,
2856
+ backgroundRepeat: (_c = config.backgroundImage) === null || _c === void 0 ? void 0 : _c.repeat,
2857
+ backgroundSize: (_d = config.backgroundImage) === null || _d === void 0 ? void 0 : _d.size,
2858
+ backgroundPosition: (_e = config.backgroundImage) === null || _e === void 0 ? void 0 : _e.position,
2733
2859
  ...(config.borderRadius && { overflow: "hidden" }),
2734
2860
  };
2735
2861
  // 2. Inner Table: Border and Border Radius.
@@ -2758,24 +2884,34 @@ function Row({ children, config, devNode, devMode }) {
2758
2884
  // Content table fills available space, giving Outlook Classic a hard
2759
2885
  // boundary so text children get a constrained box and line wrapping
2760
2886
  // triggers correctly. Use for rows containing text + image layouts.
2887
+ //
2888
+ // layoutColumns (any value) → additionally applies table-layout: fixed
2889
+ // so Outlook Classic honours the per-child width declarations.
2890
+ // Compatible with both fillWidth modes.
2761
2891
  const contentTableStyle = {
2762
- width: config.fillWidth ? "100%" : "auto",
2892
+ // When layoutColumns is active, force 100% so Outlook Classic has a
2893
+ // concrete boundary to resolve percentage column widths against.
2894
+ // table-layout:fixed is meaningless without a fixed reference width.
2895
+ width: hasLayoutColumns || config.fillWidth ? "100%" : "auto",
2763
2896
  height: "100%",
2764
2897
  borderCollapse: "collapse",
2765
2898
  minWidth: "1px",
2766
- ...(!config.fillWidth && { maxWidth: config.width || "100%" }),
2899
+ ...(!config.fillWidth &&
2900
+ !hasLayoutColumns && { maxWidth: config.width || "100%" }),
2901
+ ...(hasLayoutColumns && { tableLayout: "fixed" }),
2767
2902
  };
2768
2903
  // 5. Gap TD.
2769
2904
  const gapTdStyle = {
2770
2905
  width: config.gap || "0",
2771
2906
  lineHeight: "1px",
2772
2907
  fontSize: "1px",
2908
+ background: "transparent",
2773
2909
  };
2774
2910
  const tdAlign = config.justifyContent
2775
2911
  ? justifyMap$1[config.justifyContent]
2776
2912
  : "left";
2777
2913
  const tdValign = config.alignItems ? alignMap[config.alignItems] : "top";
2778
- const content = (jsxs("table", { "aria-label": "Row Outer", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
2914
+ const content = (jsxs("table", { "aria-label": "Row Outer", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...(!href ? rootBindingProps(bindings) : {}), style: {
2779
2915
  position: "relative",
2780
2916
  width: config.width || "100%",
2781
2917
  height: config.height,
@@ -2784,15 +2920,44 @@ function Row({ children, config, devNode, devMode }) {
2784
2920
  width: "100%",
2785
2921
  height: "100%",
2786
2922
  borderCollapse: "collapse",
2787
- }, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { align: tdAlign, width: "100%", style: { width: "100%" }, children: jsx("table", { "aria-label": "Row Content", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: contentTableStyle, ...(config.height && { height: config.height }), className: "content-table row-content-table", "data-mobile-justify": (_e = config.mobile) === null || _e === void 0 ? void 0 : _e.justifyContent, "data-mobile-align": (_f = config.mobile) === null || _f === void 0 ? void 0 : _f.alignItems, "data-mobile-wrap": ((_g = config.mobile) === null || _g === void 0 ? void 0 : _g.wrap) ? "true" : undefined, "data-gap": config.gap, children: jsx("tbody", { children: jsx("tr", { className: "content-tr", children: childrenArray.map((child, index) => (jsxs(Fragment, { children: [jsx("td", { valign: tdValign, style: {
2788
- verticalAlign: tdValign,
2789
- textAlign: "left",
2790
- padding: "0",
2791
- margin: "0",
2792
- }, className: "child-cell", children: child }), index < numChildren - 1 &&
2793
- config.gap && (jsx("td", { width: config.gap, style: gapTdStyle, className: "row-gap-td", children: "\u00A0" }, `row-gap-${index}`))] }, `row-child-${index}`))) }) }) }) }) }) }) }) }) }) }) }) }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] }));
2923
+ }, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { align: tdAlign, width: "100%", style: { width: "100%" }, children: jsx("table", { "aria-label": "Row Content", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: contentTableStyle, ...(config.height && { height: config.height }), className: "content-table row-content-table", "data-mobile-wrap": ((_f = config.mobile) === null || _f === void 0 ? void 0 : _f.wrap) ? "true" : undefined, "data-gap": config.gap, children: jsx("tbody", { children: jsx("tr", { className: "content-tr", ...listBindingProps(bindings), children: childrenArray.map((child, index) => {
2924
+ // Resolve the column width for this child based on
2925
+ // layoutColumns. undefined when layoutColumns is not set,
2926
+ // preserving the original behaviour (retrocompat).
2927
+ const columnWidth = resolveChildColumnWidth(config.layoutColumns, index, numChildren);
2928
+ return (jsxs(Fragment, { children: [jsxs("td", { align: tdAlign, ...(columnWidth && {
2929
+ width: columnWidth,
2930
+ }), style: {
2931
+ verticalAlign: tdValign,
2932
+ textAlign: tdAlign,
2933
+ padding: "0",
2934
+ margin: "0",
2935
+ ...(columnWidth && {
2936
+ width: columnWidth,
2937
+ }),
2938
+ },
2939
+ // Mirror of Container's stack-td pattern: when isStacking,
2940
+ // the non-@media .stack-td rule forces display:block +
2941
+ // width:100% on each child, which survives Gmail's
2942
+ // @media stripping and achieves true mobile stacking.
2943
+ className: `child-cell${isStacking ? " stack-td" : ""}`, children: [child, isStacking &&
2944
+ index < numChildren - 1 &&
2945
+ config.gap && (jsx("div", { className: "mobile-gap-spacer", style: {
2946
+ display: "none",
2947
+ fontSize: "0",
2948
+ lineHeight: "0",
2949
+ height: config.gap,
2950
+ background: "transparent",
2951
+ }, children: "\u00A0" }))] }), index < numChildren - 1 &&
2952
+ config.gap && (jsx("td", { width: config.gap, style: gapTdStyle,
2953
+ // Mirror of Container's desktop-gap-column pattern:
2954
+ // when isStacking, the non-@media .desktop-gap-column
2955
+ // rule collapses the between-column gap td so it does
2956
+ // not create phantom space while children are stacked.
2957
+ className: `row-gap-td${isStacking ? " desktop-gap-column" : ""}`, children: "\u00A0" }, `row-gap-${index}`))] }, `row-child-${index}`));
2958
+ }) }) }) }) }) }) }) }) }) }) }) }) }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] }));
2794
2959
  if (href && !devMode) {
2795
- return (jsx("a", { href: href, ...({ target }), style: {
2960
+ return (jsx("a", { href: href, ...({ target }), ...rootBindingProps(bindings), style: {
2796
2961
  textDecoration: "none",
2797
2962
  color: "inherit",
2798
2963
  display: "block",
@@ -2837,10 +3002,10 @@ function getBorderStyle$1(border) {
2837
3002
  }
2838
3003
  return style;
2839
3004
  }
2840
- const Section = ({ config, children, devNode, }) => {
3005
+ const Section = ({ config, children, devNode, bindings, }) => {
2841
3006
  var _a, _b, _c;
2842
3007
  const { sectionType, padding } = config;
2843
- return (jsxs("table", { "aria-label": `Section |Table | ${sectionType}`, role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
3008
+ return (jsxs("table", { "aria-label": `Section |Table | ${sectionType}`, role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
2844
3009
  position: "relative",
2845
3010
  width: "100%",
2846
3011
  backgroundColor: config.backgroundColor,
@@ -2853,66 +3018,98 @@ const Section = ({ config, children, devNode, }) => {
2853
3018
  backgroundPosition: (_c = config.backgroundImage) === null || _c === void 0 ? void 0 : _c.position,
2854
3019
  }, children: [jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: {
2855
3020
  padding: padding,
2856
- }, children: children }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsxs("td", { children: [jsxs("span", { style: {
2857
- backgroundColor: "black",
2858
- color: "white",
2859
- padding: "4px",
2860
- fontSize: "14px",
2861
- position: "absolute",
2862
- left: 0,
2863
- top: 0,
2864
- }, children: ["Section | ", sectionType] }), children] }) }) }))] }));
3021
+ }, ...listBindingProps(bindings), children: children }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: jsxs("span", { style: {
3022
+ backgroundColor: "black",
3023
+ color: "white",
3024
+ padding: "4px",
3025
+ fontSize: "14px",
3026
+ position: "absolute",
3027
+ left: 0,
3028
+ top: 0,
3029
+ }, children: ["Section | ", sectionType] }) }) }) }))] }));
2865
3030
  };
2866
3031
  var Section_default = memo(Section, arePropsEqual);
2867
3032
 
2868
- function Spacer({ config, devNode }) {
2869
- const { height, hideOnMobile } = config;
3033
+ function Spacer({ config, devNode, bindings }) {
3034
+ var _a, _b, _c;
3035
+ const { height, hideOnMobile, backgroundColor, backgroundImage } = config;
2870
3036
  // 1. Spacer Table Style
2871
3037
  const spacerTableStyle = {
2872
- // Crucial for compatibility: Ensures no background or border interference
2873
- backgroundColor: "transparent",
2874
3038
  borderCollapse: "collapse",
2875
- border: "0",
2876
3039
  width: "100%",
2877
- // Note the CSS standard dash convention: 'mso-table-lspace'
2878
- // ["mso-table-lspace" as string]: "0pt",
2879
- ["msoTableLspace"]: "0pt",
2880
- // ["mso-table-rspace" as string]: "0pt",
2881
- ["msoTableRspace"]: "0pt",
2882
3040
  };
2883
3041
  // 2. Spacer TD Style: The element that creates the actual vertical space
2884
3042
  const spacerTdStyle = {
2885
3043
  height: height,
2886
- // Critical: Suppress any vertical height created by text/font
3044
+ maxHeight: height,
2887
3045
  fontSize: "0",
2888
3046
  lineHeight: "0",
2889
3047
  padding: "0",
3048
+ margin: "0",
3049
+ border: "none",
3050
+ color: "transparent",
3051
+ // Background applied at TD level for Outlook Classic compatibility
3052
+ ...(backgroundColor && { backgroundColor }),
3053
+ ...(backgroundImage && {
3054
+ backgroundImage: `url(${backgroundImage.src})`,
3055
+ backgroundRepeat: (_a = backgroundImage.repeat) !== null && _a !== void 0 ? _a : "no-repeat",
3056
+ backgroundSize: (_b = backgroundImage.size) !== null && _b !== void 0 ? _b : "cover",
3057
+ backgroundPosition: (_c = backgroundImage.position) !== null && _c !== void 0 ? _c : "center",
3058
+ }),
2890
3059
  };
2891
3060
  // Parse height for the HTML attribute
2892
3061
  const spacerHeightAttribute = parseInt(height, 10) || 1;
2893
- return (
2894
- // Outer table ensures the spacer spans the full width of its container
2895
- jsxs("table", { "aria-label": "Vertical Spacer", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
3062
+ return (jsxs("table", { "aria-label": "Vertical Spacer", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
2896
3063
  // --- Start dev
2897
3064
  position: "relative",
2898
3065
  // --- End dev
2899
3066
  ...spacerTableStyle,
2900
- }, ...{ height: spacerHeightAttribute }, className: hideOnMobile ? "hide-on-mobile" : undefined, children: [jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: spacerTdStyle,
2901
- // Explicit height attribute
2902
- height: spacerHeightAttribute, children: "\u00A0" }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] }));
3067
+ }, ...{ height: spacerHeightAttribute }, className: hideOnMobile ? "hide-on-mobile" : undefined, children: [jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: spacerTdStyle, height: spacerHeightAttribute, ...(backgroundColor && !backgroundImage
3068
+ ? { bgcolor: backgroundColor }
3069
+ : {}) }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] }));
2903
3070
  }
2904
3071
  var Spacer_default = memo(Spacer, arePropsEqual);
2905
3072
 
2906
- function Text({ config, devMode, children }) {
2907
- const { text, padding, color, textAlign, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, backgroundColor, opacity, whiteSpace, wordBreak = "break-all", maxWidth, } = config;
3073
+ // Helper to build link href based on innerLink type
3074
+ function buildLinkHref$1(innerLink) {
3075
+ if (!innerLink || innerLink.type === "none")
3076
+ return null;
3077
+ switch (innerLink.type) {
3078
+ case "url":
3079
+ return innerLink.url || null;
3080
+ case "email":
3081
+ return innerLink.email ? `mailto:${innerLink.email}` : null;
3082
+ case "phone":
3083
+ return innerLink.phone ? `tel:${innerLink.phone}` : null;
3084
+ case "anchor":
3085
+ return innerLink.anchor ? `#${innerLink.anchor}` : null;
3086
+ case "page_top":
3087
+ return "#top";
3088
+ case "page_bottom":
3089
+ return "#bottom";
3090
+ default:
3091
+ return null;
3092
+ }
3093
+ }
3094
+ function Text({ config, devMode, children, bindings }) {
3095
+ const { text, padding, color, textAlign, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, backgroundColor, opacity, whiteSpace, wordBreak = "break-all", maxWidth, innerLink, } = config;
3096
+ // Resolve href and target from innerLink
3097
+ const href = buildLinkHref$1(innerLink);
3098
+ const target = (innerLink === null || innerLink === void 0 ? void 0 : innerLink.target) || "_blank";
2908
3099
  // 1. TD Style: Where padding and background are reliably applied.
3100
+ // When maxWidth is set, this TD stays at width: 100% so it always fills
3101
+ // its parent — the inner maxWidth table (see below) does the actual
3102
+ // capping, keeping the outer layout intact in all clients.
2909
3103
  const tdStyle = {
2910
3104
  padding: padding,
2911
3105
  backgroundColor: backgroundColor,
2912
3106
  width: "100%",
2913
3107
  verticalAlign: "top",
2914
3108
  };
2915
- // 2. Content Style: Applied directly to a wrapper element
3109
+ // 2. Content Style: Applied directly to the inner div wrapper.
3110
+ // maxWidth is intentionally omitted here — putting it on a div has no
3111
+ // effect in Outlook Classic (Word engine ignores CSS on div elements).
3112
+ // The constraint is enforced at the table level instead (see below).
2916
3113
  const contentStyle = {
2917
3114
  color: color,
2918
3115
  textAlign: textAlign,
@@ -2931,7 +3128,18 @@ function Text({ config, devMode, children }) {
2931
3128
  wordBreak: wordBreak,
2932
3129
  margin: "0",
2933
3130
  padding: "0",
2934
- maxWidth,
3131
+ };
3132
+ // 3. maxWidth constraining table style (modern clients).
3133
+ // The `width` HTML attribute on this table is what Outlook Classic
3134
+ // (Word engine) reads — it has no concept of max-width, but it does
3135
+ // honour the `width` attribute as a hard column cap.
3136
+ // The CSS max-width here handles modern web/email clients correctly.
3137
+ // <center> around it ensures the constrained block stays horizontally
3138
+ // centred in both the Word engine and standards-based renderers.
3139
+ const maxWidthTableStyle = {
3140
+ width: "100%",
3141
+ maxWidth: maxWidth,
3142
+ borderCollapse: "collapse",
2935
3143
  };
2936
3144
  // Determine content to render
2937
3145
  const content = text !== null && text !== void 0 ? text : children;
@@ -2939,10 +3147,32 @@ function Text({ config, devMode, children }) {
2939
3147
  const processedHtml = isString
2940
3148
  ? injectLinkStyles(content, contentStyle)
2941
3149
  : "";
2942
- return (jsx("table", { "aria-label": "Text Block Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
3150
+ const innerContent = isString ? (jsx("div", { style: contentStyle, dangerouslySetInnerHTML: { __html: processedHtml } })) : (jsx("div", { style: contentStyle, children: content }));
3151
+ const wrappedContent = maxWidth ? (
3152
+ /*
3153
+ * maxWidth wrapper — Outlook Classic compatibility pattern:
3154
+ *
3155
+ * <center> instructs the Word rendering engine to horizontally
3156
+ * centre its child block, equivalent to margin: 0 auto in CSS.
3157
+ *
3158
+ * The inner table carries the `width` HTML attribute set to the
3159
+ * maxWidth value. Outlook Classic reads `width` as a hard pixel
3160
+ * cap; it has no concept of max-width so this is the only lever
3161
+ * available. Modern clients receive the CSS max-width on the
3162
+ * same table and behave correctly.
3163
+ *
3164
+ * The outer TD remains at width: 100% so it always fills its
3165
+ * parent cell in every client — only the inner content is capped.
3166
+ */
3167
+ jsx("center", { children: jsx("table", { "aria-label": "Text Max Width Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, width: maxWidth, style: maxWidthTableStyle, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { children: innerContent }) }) }) }) })) : (innerContent);
3168
+ return (jsx("table", { "aria-label": "Text Block Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
2943
3169
  width: "100%",
2944
3170
  borderCollapse: "collapse",
2945
- }, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: tdStyle, align: textAlign, children: isString ? (jsx("div", { style: contentStyle, dangerouslySetInnerHTML: { __html: processedHtml } })) : (jsx("div", { style: contentStyle, children: content })) }) }) }) }));
3171
+ }, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: tdStyle, align: textAlign, children: href && !devMode ? (jsx("a", { href: href, target: target, ...(target === "_blank" ? { rel: "noopener noreferrer" } : {}), style: {
3172
+ display: "block",
3173
+ textDecoration: "none",
3174
+ color: "inherit",
3175
+ }, children: wrappedContent })) : (wrappedContent) }) }) }) }));
2946
3176
  }
2947
3177
  var Text_default = memo(Text, arePropsEqual);
2948
3178
 
@@ -3068,7 +3298,7 @@ function buildLinkHref(innerLink) {
3068
3298
  return null;
3069
3299
  }
3070
3300
  }
3071
- function Icon({ config, devNode, devMode, children }) {
3301
+ function Icon({ config, devNode, devMode, children, bindings }) {
3072
3302
  const {
3073
3303
  // base64Source,
3074
3304
  width, height, backgroundColor, padding = "0", borderRadius = "0", border, innerLink, justifyContent = "center", } = config;
@@ -3203,7 +3433,7 @@ function Icon({ config, devNode, devMode, children }) {
3203
3433
  <!--<![endif]-->
3204
3434
  `
3205
3435
  : null;
3206
- return (jsxs("table", { "aria-label": "Icon", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, align: align, style: {
3436
+ return (jsxs("table", { "aria-label": "Icon", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, align: align, ...rootBindingProps(bindings), style: {
3207
3437
  // --- Start dev
3208
3438
  position: "relative",
3209
3439
  // --- End dev