@pagenflow/email 1.4.7 → 1.4.9

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",
@@ -1739,6 +1790,15 @@ function Button({ config, devMode }) {
1739
1790
  }
1740
1791
  var Button_default = memo(Button, arePropsEqual);
1741
1792
 
1793
+ function isGapZero(gap) {
1794
+ if (!gap)
1795
+ return true;
1796
+ // Remove whitespace and convert to lowercase
1797
+ const trimmedGap = gap.trim().toLowerCase();
1798
+ // Check for exact zero matches (0, 0px, 0%, 0em, 0rem, etc.)
1799
+ return /^0(px|%|em|rem|ex|ch|vw|vh|vmin|vmax)?$/.test(trimmedGap);
1800
+ }
1801
+
1742
1802
  // Helper for vertical alignment
1743
1803
  const vAlignMap = {
1744
1804
  start: "top",
@@ -1787,7 +1847,7 @@ function getBorderStyle$5(border) {
1787
1847
  }
1788
1848
  return style;
1789
1849
  }
1790
- function Column({ children, config, devNode }) {
1850
+ function Column({ children, config, devNode, bindings }) {
1791
1851
  var _a, _b, _c;
1792
1852
  // Process children array for gap support
1793
1853
  const childrenArray = (Array.isArray(children) ? children : [children]).filter((child) => child != null);
@@ -1839,6 +1899,7 @@ function Column({ children, config, devNode }) {
1839
1899
  // treat it as content-box height and add padding on top, causing the
1840
1900
  // total to exceed the declared height in preview mode.
1841
1901
  verticalAlign: config.alignItems ? alignMap$2[config.alignItems] : "top",
1902
+ background: "transparent",
1842
1903
  };
1843
1904
  // 4. Gap spacer style (used between children)
1844
1905
  const gapSpacerStyle = {
@@ -1846,6 +1907,7 @@ function Column({ children, config, devNode }) {
1846
1907
  lineHeight: "1px",
1847
1908
  fontSize: "1px",
1848
1909
  width: "100%",
1910
+ background: "transparent",
1849
1911
  };
1850
1912
  // 5. maxWidth constraining table style (modern clients).
1851
1913
  // The `width` HTML attribute on this table is what Outlook Classic
@@ -1860,10 +1922,10 @@ function Column({ children, config, devNode }) {
1860
1922
  borderCollapse: "collapse",
1861
1923
  };
1862
1924
  // Main content rendering
1863
- 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: {
1925
+ 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: {
1864
1926
  width: "100%",
1865
1927
  borderCollapse: "collapse",
1866
- }, children: jsx("tbody", { children: childrenArray.map((child, index) => (jsxs(Fragment, { children: [jsx("tr", { children: jsx("td", { style: {
1928
+ }, children: jsx("tbody", { ...listBindingProps(bindings), children: childrenArray.map((child, index) => (jsxs(Fragment, { children: [jsx("tr", { children: jsx("td", { style: {
1867
1929
  verticalAlign: config.alignItems
1868
1930
  ? alignMap$2[config.alignItems]
1869
1931
  : "top",
@@ -1871,8 +1933,8 @@ function Column({ children, config, devNode }) {
1871
1933
  ? vAlignMap[config.justifyContent]
1872
1934
  : "top", align: config.alignItems
1873
1935
  ? alignMap$2[config.alignItems]
1874
- : "left", children: child }) }), index < numChildren - 1 && (jsx("tr", { children: jsx("td", { style: gapSpacerStyle, children: "\u00A0" }) }))] }, `col-child-${index}`))) }) })) : (children) }) }) }) }));
1875
- return (jsxs("table", { "aria-label": "Column Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
1936
+ : "left", children: child }) }), index < numChildren - 1 && !isGapZero(config.gap) && (jsx("tr", { children: jsx("td", { style: gapSpacerStyle, children: "\u00A0" }) }))] }, `col-child-${index}`))) }) })) : (children) }) }) }) }));
1937
+ return (jsxs("table", { "aria-label": "Column Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
1876
1938
  position: "relative",
1877
1939
  ...outerTableStyle,
1878
1940
  }, ...(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 ? (
@@ -1941,7 +2003,7 @@ function getBorderStyle$4(border) {
1941
2003
  }
1942
2004
  return style;
1943
2005
  }
1944
- function Container({ children, config, devMode, devNode }) {
2006
+ function Container({ children, config, bindings, devMode, devNode, }) {
1945
2007
  var _a, _b, _c;
1946
2008
  const { widthType, childrenConstraints } = config;
1947
2009
  const childrenArray = (Array.isArray(children) ? children : [children]).filter((child) => child != null);
@@ -1959,7 +2021,7 @@ function Container({ children, config, devMode, devNode }) {
1959
2021
  return 0;
1960
2022
  })();
1961
2023
  const getChildWidths = (() => {
1962
- const { widthDistributionType } = childrenConstraints;
2024
+ const { widthDistributionType } = childrenConstraints !== null && childrenConstraints !== void 0 ? childrenConstraints : {};
1963
2025
  const totalGapSpace = gapWidthPx * (numChildren > 1 ? numChildren - 1 : 0);
1964
2026
  const remainingContentSpace = containerWidthPx - totalGapSpace;
1965
2027
  switch (widthDistributionType) {
@@ -2033,6 +2095,7 @@ function Container({ children, config, devMode, devNode }) {
2033
2095
  width: config.gap || "0",
2034
2096
  lineHeight: "1px",
2035
2097
  fontSize: "1px",
2098
+ background: "transparent",
2036
2099
  };
2037
2100
  const justifyAlign = config.justifyContent
2038
2101
  ? justifyMap$2[config.justifyContent]
@@ -2048,19 +2111,20 @@ function Container({ children, config, devMode, devNode }) {
2048
2111
  textAlign: "left",
2049
2112
  };
2050
2113
  if (config.gap && index < numChildren - 1) {
2051
- return (jsxs(Fragment, { children: [jsxs("td", { className: isStacking ? "stack-td" : undefined, width: getChildWidths[index], style: childTdStyle, children: [child, isStacking && (jsx("div", { className: "mobile-gap-spacer", style: {
2114
+ return (jsxs(Fragment, { children: [jsxs("td", { className: isStacking ? "stack-td" : undefined, width: getChildWidths[index], style: childTdStyle, children: [child, isStacking && !isGapZero(config.gap) && (jsx("div", { className: "mobile-gap-spacer", style: {
2052
2115
  display: "none",
2053
2116
  fontSize: "0",
2054
2117
  lineHeight: "0",
2055
2118
  height: config.gap,
2056
- }, children: "\u00A0" }))] }, `child-${index}`), jsx("td", { className: isStacking ? "desktop-gap-column" : undefined, width: config.gap, style: gapTdStyle, children: "\u00A0" }, `gap-${index}`)] }, `ctn:${index}`));
2119
+ background: "transparent",
2120
+ }, children: "\u00A0" }))] }, `child-${index}`), !isGapZero(config.gap) && (jsx("td", { className: isStacking ? "desktop-gap-column" : undefined, width: config.gap, style: gapTdStyle, children: "\u00A0" }, `gap-${index}`))] }, `ctn:${index}`));
2057
2121
  }
2058
2122
  return (jsx("td", { className: isStacking ? "stack-td" : undefined, width: getChildWidths[index], style: childTdStyle, children: child }, `child-${index}`));
2059
2123
  });
2060
2124
  return (jsx("table", { "aria-label": `Container | Table Outer`, cellPadding: 0, cellSpacing: 0, role: "presentation", border: 0, style: {
2061
2125
  position: "relative",
2062
2126
  ...outerTableStyle,
2063
- }, children: jsx("tbody", { children: jsx("tr", { children: jsxs("td", { align: justifyAlign, children: [jsx("div", { dangerouslySetInnerHTML: { __html: msoFixedWrapper } }), jsxs("table", { className: [
2127
+ }, ...rootBindingProps(bindings), children: jsx("tbody", { children: jsx("tr", { children: jsxs("td", { align: justifyAlign, children: [jsx("div", { dangerouslySetInnerHTML: { __html: msoFixedWrapper } }), jsxs("table", { className: [
2064
2128
  widthType === "fixed" ? "container-fixed-width" : undefined,
2065
2129
  devMode ? "main-wrapper relative" : undefined,
2066
2130
  ]
@@ -2069,22 +2133,22 @@ function Container({ children, config, devMode, devNode }) {
2069
2133
  width: "100%",
2070
2134
  maxWidth: widthType === "fixed" ? config.width || "600px" : undefined,
2071
2135
  borderCollapse: "collapse",
2072
- }, 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 } })] }) }) }) }));
2136
+ }, 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 } })] }) }) }) }));
2073
2137
  }
2074
2138
  var Container_default = memo(Container, arePropsEqual);
2075
2139
 
2076
- function Divider({ config, devNode }) {
2140
+ function Divider({ config, devNode, bindings }) {
2077
2141
  const { height = "1px", color = "#cccccc", width = "100%", margin = "20px 0", align = "center", hideOnMobile, } = config;
2078
2142
  const heightPx = parseInt(height, 10) || 1;
2079
2143
  // Parse margin into paddingTop / paddingBottom for the outer TD.
2080
2144
  // Outlook ignores shorthand "20px 0" on TDs — must be explicit.
2081
- const [marginTopRaw = "0", marginRightRaw = "0", marginBottomRaw, marginLeftRaw] = margin.trim().split(/\s+/);
2145
+ const [marginTopRaw = "0", marginRightRaw = "0", marginBottomRaw, marginLeftRaw,] = margin.trim().split(/\s+/);
2082
2146
  const marginTop = marginTopRaw;
2083
2147
  const marginBottom = marginBottomRaw !== null && marginBottomRaw !== void 0 ? marginBottomRaw : marginTopRaw; // "20px 0" → top=20px, bottom=20px
2084
2148
  // Outlook requires align on the outer TD to correctly position
2085
2149
  // a fixed-width inner table (e.g. width="300px").
2086
2150
  const alignAttr = align === "left" ? "left" : align === "right" ? "right" : "center";
2087
- return (jsxs("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
2151
+ return (jsxs("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
2088
2152
  position: "relative", // dev overlay anchor
2089
2153
  width: "100%",
2090
2154
  borderCollapse: "collapse",
@@ -2181,7 +2245,13 @@ function Head({ children, backgroundColor = "#ffffff", title = "Email Preview",
2181
2245
  line-height: inherit !important;
2182
2246
  }
2183
2247
  body { background-color: ${backgroundColor} !important; }
2184
- p { margin: 0; }
2248
+ p {
2249
+ margin: 0;
2250
+ margin-block-start: 0px;
2251
+ margin-block-end: 0px;
2252
+ margin-inline-start: 0px;
2253
+ margin-inline-end: 0px;
2254
+ }
2185
2255
  `;
2186
2256
  const globalStyles = `
2187
2257
  @media screen and (max-width: 768px) {
@@ -2403,8 +2473,56 @@ function injectLinkStyles(html, fallback) {
2403
2473
  return result;
2404
2474
  }
2405
2475
 
2406
- function Heading({ config, devMode, children }) {
2407
- const { text, level = "h1", padding, color, textAlign, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, backgroundColor, wordBreak, whiteSpace, } = config;
2476
+ // injectParagraphReset.ts
2477
+ const P_RESET_STYLE = [
2478
+ "margin:0",
2479
+ "margin-block-start:0px",
2480
+ "margin-block-end:0px",
2481
+ "margin-inline-start:0px",
2482
+ "margin-inline-end:0px",
2483
+ ].join(";");
2484
+ function injectParagraphReset(html) {
2485
+ if (!html || !html.includes("<p"))
2486
+ return html;
2487
+ return html.replace(/<p(\s[^>]*)?>/gi, (_, attrs = "") => {
2488
+ var _a, _b;
2489
+ const existingStyle = (_b = (_a = /style\s*=\s*"([^"]*)"/i.exec(attrs)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : "";
2490
+ const mergedStyle = existingStyle
2491
+ ? `${existingStyle};${P_RESET_STYLE}`
2492
+ : P_RESET_STYLE;
2493
+ const cleanAttrs = attrs.replace(/\s*style\s*=\s*"[^"]*"/i, "").trim();
2494
+ return cleanAttrs
2495
+ ? `<p ${cleanAttrs} style="${mergedStyle}">`
2496
+ : `<p style="${mergedStyle}">`;
2497
+ });
2498
+ }
2499
+
2500
+ // Helper to build link href based on innerLink type
2501
+ function buildLinkHref$3(innerLink) {
2502
+ if (!innerLink || innerLink.type === "none")
2503
+ return null;
2504
+ switch (innerLink.type) {
2505
+ case "url":
2506
+ return innerLink.url || null;
2507
+ case "email":
2508
+ return innerLink.email ? `mailto:${innerLink.email}` : null;
2509
+ case "phone":
2510
+ return innerLink.phone ? `tel:${innerLink.phone}` : null;
2511
+ case "anchor":
2512
+ return innerLink.anchor ? `#${innerLink.anchor}` : null;
2513
+ case "page_top":
2514
+ return "#top";
2515
+ case "page_bottom":
2516
+ return "#bottom";
2517
+ default:
2518
+ return null;
2519
+ }
2520
+ }
2521
+ function Heading({ config, devMode, children, bindings }) {
2522
+ const { text, level = "h1", padding, color, textAlign, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, backgroundColor, wordBreak, whiteSpace, innerLink, } = config;
2523
+ // Resolve href and target from innerLink
2524
+ const href = buildLinkHref$3(innerLink);
2525
+ const target = (innerLink === null || innerLink === void 0 ? void 0 : innerLink.target) || "_blank";
2408
2526
  // Determine the content to render
2409
2527
  const content = text !== null && text !== void 0 ? text : children;
2410
2528
  const isString = typeof content === "string";
@@ -2437,16 +2555,21 @@ function Heading({ config, devMode, children }) {
2437
2555
  ["msoLineHeightRule"]: "exactly",
2438
2556
  };
2439
2557
  const processedHtml = isString
2440
- ? injectLinkStyles(content, headingStyle)
2558
+ ? injectParagraphReset(injectLinkStyles(content, headingStyle))
2441
2559
  : "";
2442
2560
  // Dynamically create the Heading element
2443
2561
  const HeadingTag = level;
2562
+ const headingElement = isString ? (jsx(HeadingTag, { style: headingStyle, dangerouslySetInnerHTML: { __html: processedHtml } })) : (jsx(HeadingTag, { style: headingStyle, children: content }));
2444
2563
  return (
2445
2564
  // Wrap the heading content in a table for padding/width/background management.
2446
- jsx("table", { "aria-label": "Heading Block Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
2565
+ jsx("table", { "aria-label": "Heading Block Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
2447
2566
  width: "100%",
2448
2567
  borderCollapse: "collapse",
2449
- }, 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 })) }) }) }) }));
2568
+ }, 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: {
2569
+ display: "block",
2570
+ textDecoration: "none",
2571
+ color: "inherit",
2572
+ }, children: headingElement })) : (headingElement) }) }) }) }));
2450
2573
  }
2451
2574
  var Heading_default = memo(Heading, arePropsEqual);
2452
2575
 
@@ -2471,7 +2594,7 @@ function Html({ children, backgroundColor = "#ffffff", }) {
2471
2594
  }
2472
2595
 
2473
2596
  // Helper to build link href based on innerLink type
2474
- function buildLinkHref$1(innerLink) {
2597
+ function buildLinkHref$2(innerLink) {
2475
2598
  if (!innerLink || innerLink.type === "none")
2476
2599
  return null;
2477
2600
  switch (innerLink.type) {
@@ -2531,11 +2654,15 @@ function getBorderStyleString$1(border) {
2531
2654
  styles.push(`border-left:${border.left.width} ${border.left.style} ${border.left.color} !important;`);
2532
2655
  return styles.join(" ");
2533
2656
  }
2534
- function Image({ config, devNode, devMode }) {
2657
+ function Image({ config, devNode, devMode, bindings }) {
2535
2658
  var _a, _b;
2536
- const { src, alt, innerLink, mobile } = config;
2659
+ const { src: originalSrc, alt, innerLink, mobile } = config;
2660
+ // In dev mode, if there's no src, use the placeholder
2661
+ const src = devMode && !originalSrc
2662
+ ? "https://placehold.co/300x200?text=select+an+image&font=poppins"
2663
+ : originalSrc;
2537
2664
  // Resolve href and target from innerLink
2538
- const href = buildLinkHref$1(innerLink);
2665
+ const href = buildLinkHref$2(innerLink);
2539
2666
  const target = (innerLink === null || innerLink === void 0 ? void 0 : innerLink.target) || "_blank";
2540
2667
  const seed = src + (alt || "");
2541
2668
  const instanceId = seed
@@ -2549,43 +2676,37 @@ function Image({ config, devNode, devMode }) {
2549
2676
  const widthAttr = desktopWidth.replace("px", "");
2550
2677
  const heightAttr = (_a = config.height) === null || _a === void 0 ? void 0 : _a.replace("px", "");
2551
2678
  // Determine the table's "initial" width.
2552
- // If it's 300px, the table should be 300px, not 100%.
2553
2679
  const tableWidth = isPercent ? desktopWidth : `${widthAttr}px`;
2554
- // When width is a percentage, Outlook ignores CSS and renders the image at
2555
- // its intrinsic pixel size. Setting a concrete `width` HTML attribute gives
2556
- // Outlook a value to constrain against while modern clients continue to use
2557
- // the CSS `width: 100%` for fluid rendering.
2558
- //
2559
- // If `maxWidth` is a pixel value (e.g. "600px"), we extract the number and
2560
- // use it as the HTML `width` attribute so Outlook enforces that cap.
2561
- // Other clients ignore the attribute and rely on CSS styles instead.
2562
- // If `maxWidth` is not set or is not a pixel value (e.g. "100%"), we fall
2563
- // back to the original behaviour (numeric string for px widths, undefined
2564
- // for % widths).
2565
- const maxWidthPx = ((_b = config.maxWidth) === null || _b === void 0 ? void 0 : _b.endsWith("px"))
2566
- ? parseInt(config.maxWidth, 10)
2567
- : undefined;
2568
- const imgWidthAttr = isPercent ? (maxWidthPx !== null && maxWidthPx !== void 0 ? maxWidthPx : undefined) : widthAttr;
2569
- // 2. Mobile Overrides only emit CSS properties that are explicitly set,
2570
- // so unspecified properties are left untouched (no forced defaults).
2680
+ // Calculate width attribute for Outlook
2681
+ let imgWidthAttr;
2682
+ if (config.outlookWidth) {
2683
+ // Use explicit outlookWidth if provided
2684
+ imgWidthAttr = parseInt(config.outlookWidth, 10);
2685
+ }
2686
+ else if (isPercent) {
2687
+ // For percentage widths, use maxWidth as fallback for Outlook
2688
+ const maxWidthPx = ((_b = config.maxWidth) === null || _b === void 0 ? void 0 : _b.endsWith("px"))
2689
+ ? parseInt(config.maxWidth, 10)
2690
+ : undefined;
2691
+ imgWidthAttr = maxWidthPx !== null && maxWidthPx !== void 0 ? maxWidthPx : undefined;
2692
+ }
2693
+ else {
2694
+ // For fixed pixel widths, use that value
2695
+ imgWidthAttr = widthAttr ? parseInt(widthAttr, 10) : undefined;
2696
+ }
2697
+ // 2. Mobile Overrides
2571
2698
  let mobileCss = "";
2572
2699
  if (mobile) {
2573
- // .wrap-${imgClass} rules
2574
- const wrapRules = [
2575
- // Always reset min-width so the px lock from desktop can be overridden
2576
- "min-width: 0 !important;",
2577
- ];
2700
+ const wrapRules = ["min-width: 0 !important;"];
2578
2701
  if (mobile.width !== undefined)
2579
2702
  wrapRules.push(`width: ${mobile.width} !important;`);
2580
2703
  if (mobile.maxWidth !== undefined)
2581
2704
  wrapRules.push(`max-width: ${mobile.maxWidth} !important;`);
2582
- // .td-${imgClass} rules
2583
2705
  const tdRules = [];
2584
2706
  if (mobile.padding !== undefined)
2585
2707
  tdRules.push(`padding: ${mobile.padding} !important;`);
2586
2708
  if (mobile.backgroundColor !== undefined)
2587
2709
  tdRules.push(`background-color: ${mobile.backgroundColor} !important;`);
2588
- // .${imgClass} rules
2589
2710
  const imgRules = [];
2590
2711
  if (mobile.width !== undefined)
2591
2712
  imgRules.push(`width: ${mobile.width} !important;`);
@@ -2608,7 +2729,6 @@ function Image({ config, devNode, devMode }) {
2608
2729
  mobileCss = `
2609
2730
  @media screen and (max-width: 768px) {
2610
2731
  .wrap-${imgClass} {
2611
- /* This breaks the px lock from desktop and makes it fluid */
2612
2732
  ${wrapRules.join("\n ")}
2613
2733
  }
2614
2734
  .td-${imgClass} {
@@ -2634,9 +2754,27 @@ function Image({ config, devNode, devMode }) {
2634
2754
  objectPosition: config.objectPosition,
2635
2755
  };
2636
2756
  const imageElement = (jsx("img", { src: src, alt: alt, width: imgWidthAttr, height: heightAttr !== "auto" ? heightAttr : undefined, className: imgClass, style: imgStyle, draggable: !devMode }));
2637
- 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
2638
- , style: {
2639
- width: tableWidth, // Fixed px here prevents the 100% "ghost space"
2757
+ // Outlook Classic wrapper - only applied when outlookWidth is explicitly set
2758
+ // OR when we need to constrain a percentage-width image in Outlook
2759
+ const needsOutlookWrapper = config.outlookWidth || (isPercent && imgWidthAttr);
2760
+ const finalImageElement = needsOutlookWrapper ? (jsxs(Fragment$1, { children: [jsx("style", { dangerouslySetInnerHTML: {
2761
+ __html: `</style>` +
2762
+ `<!--[if mso]>` +
2763
+ `<table cellpadding="0" cellspacing="0" border="0" style="width: ${imgWidthAttr}px;">` +
2764
+ `<tr>` +
2765
+ `<td style="padding: 0; margin: 0;" width="${imgWidthAttr}">` +
2766
+ `<img src="${src}" alt="${alt || ""}" width="${imgWidthAttr}" height="${heightAttr !== "auto" ? heightAttr : ""}" style="display: block; width: 100%; height: auto;" />` +
2767
+ `</td>` +
2768
+ `</tr>` +
2769
+ `</table>` +
2770
+ `<![endif]-->` +
2771
+ `<!--[if !mso]><!-->` +
2772
+ `<style>`,
2773
+ } }), imageElement, jsx("style", { dangerouslySetInnerHTML: {
2774
+ __html: `</style>` + `<!--<![endif]-->` + `<style>`,
2775
+ } })] })) : (imageElement);
2776
+ 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: {
2777
+ width: tableWidth,
2640
2778
  maxWidth: "100%",
2641
2779
  borderCollapse: "collapse",
2642
2780
  margin: "0 auto",
@@ -2645,10 +2783,10 @@ function Image({ config, devNode, devMode }) {
2645
2783
  backgroundColor: config.backgroundColor,
2646
2784
  fontSize: "0",
2647
2785
  lineHeight: "0",
2648
- width: tableWidth, // Lock the cell as well
2786
+ width: tableWidth,
2649
2787
  }, children: href && !devMode ? (jsx("a", { href: href, target: target, ...(target === "_blank"
2650
2788
  ? { rel: "noopener noreferrer" }
2651
- : {}), style: { display: "block", width: "100%" }, children: imageElement })) : (imageElement) }) }) }), devMode && !!devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] })] }));
2789
+ : {}), style: { display: "block", width: "100%" }, children: finalImageElement })) : (finalImageElement) }) }) }), devMode && !!devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] })] }));
2652
2790
  }
2653
2791
  var Image_default = memo(Image, arePropsEqual);
2654
2792
 
@@ -2717,7 +2855,21 @@ function getHrefFromInnerLink(innerLink) {
2717
2855
  return undefined;
2718
2856
  }
2719
2857
  }
2720
- function Row({ children, config, devNode, devMode }) {
2858
+ /**
2859
+ * Resolves the width for a child <td> at the given index based on layoutColumns.
2860
+ *
2861
+ * - "equal" → equal percentage share across all children
2862
+ * - string[] → explicit value at the matching index (px, %, or mixed)
2863
+ * - undefined → undefined, so no width attribute is set (retrocompat)
2864
+ */
2865
+ function resolveChildColumnWidth(layoutColumns, index, numChildren) {
2866
+ if (!layoutColumns)
2867
+ return undefined;
2868
+ if (layoutColumns === "equal")
2869
+ return `${100 / numChildren}%`;
2870
+ return layoutColumns[index];
2871
+ }
2872
+ function Row({ children, config, devNode, devMode, bindings }) {
2721
2873
  var _a, _b, _c, _d, _e, _f;
2722
2874
  const childrenArray = (Array.isArray(children) ? children : [children]).filter((child) => child != null);
2723
2875
  const numChildren = childrenArray.length;
@@ -2728,6 +2880,9 @@ function Row({ children, config, devNode, devMode }) {
2728
2880
  // / mobile-gap-spacer class names so that stacking works via non-@media CSS
2729
2881
  // rules that survive Gmail's stylesheet stripping.
2730
2882
  const isStacking = ((_b = config.mobile) === null || _b === void 0 ? void 0 : _b.wrap) === true && numChildren > 1;
2883
+ // Whether layoutColumns is active. When true, table-layout:fixed is applied
2884
+ // to the content table so Outlook Classic honours the declared column widths.
2885
+ const hasLayoutColumns = config.layoutColumns !== undefined;
2731
2886
  // 1. Outer TD: Background, Border Radius, Width, Height.
2732
2887
  const backgroundTdStyle = {
2733
2888
  backgroundColor: config.backgroundColor,
@@ -2768,24 +2923,34 @@ function Row({ children, config, devNode, devMode }) {
2768
2923
  // Content table fills available space, giving Outlook Classic a hard
2769
2924
  // boundary so text children get a constrained box and line wrapping
2770
2925
  // triggers correctly. Use for rows containing text + image layouts.
2926
+ //
2927
+ // layoutColumns (any value) → additionally applies table-layout: fixed
2928
+ // so Outlook Classic honours the per-child width declarations.
2929
+ // Compatible with both fillWidth modes.
2771
2930
  const contentTableStyle = {
2772
- width: config.fillWidth ? "100%" : "auto",
2931
+ // When layoutColumns is active, force 100% so Outlook Classic has a
2932
+ // concrete boundary to resolve percentage column widths against.
2933
+ // table-layout:fixed is meaningless without a fixed reference width.
2934
+ width: hasLayoutColumns || config.fillWidth ? "100%" : "auto",
2773
2935
  height: "100%",
2774
2936
  borderCollapse: "collapse",
2775
2937
  minWidth: "1px",
2776
- ...(!config.fillWidth && { maxWidth: config.width || "100%" }),
2938
+ ...(!config.fillWidth &&
2939
+ !hasLayoutColumns && { maxWidth: config.width || "100%" }),
2940
+ ...(hasLayoutColumns && { tableLayout: "fixed" }),
2777
2941
  };
2778
2942
  // 5. Gap TD.
2779
2943
  const gapTdStyle = {
2780
2944
  width: config.gap || "0",
2781
2945
  lineHeight: "1px",
2782
2946
  fontSize: "1px",
2947
+ background: "transparent",
2783
2948
  };
2784
2949
  const tdAlign = config.justifyContent
2785
2950
  ? justifyMap$1[config.justifyContent]
2786
2951
  : "left";
2787
2952
  const tdValign = config.alignItems ? alignMap[config.alignItems] : "top";
2788
- const content = (jsxs("table", { "aria-label": "Row Outer", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
2953
+ const content = (jsxs("table", { "aria-label": "Row Outer", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...(!href ? rootBindingProps(bindings) : {}), style: {
2789
2954
  position: "relative",
2790
2955
  width: config.width || "100%",
2791
2956
  height: config.height,
@@ -2794,32 +2959,44 @@ function Row({ children, config, devNode, devMode }) {
2794
2959
  width: "100%",
2795
2960
  height: "100%",
2796
2961
  borderCollapse: "collapse",
2797
- }, 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", children: childrenArray.map((child, index) => (jsxs(Fragment, { children: [jsxs("td", { align: tdAlign, style: {
2798
- verticalAlign: tdValign,
2799
- textAlign: tdAlign,
2800
- padding: "0",
2801
- margin: "0",
2802
- },
2803
- // Mirror of Container's stack-td pattern: when isStacking,
2804
- // the non-@media .stack-td rule forces display:block +
2805
- // width:100% on each child, which survives Gmail's
2806
- // @media stripping and achieves true mobile stacking.
2807
- className: `child-cell${isStacking ? " stack-td" : ""}`, children: [child, isStacking &&
2808
- index < numChildren - 1 &&
2809
- config.gap && (jsx("div", { className: "mobile-gap-spacer", style: {
2810
- display: "none",
2811
- fontSize: "0",
2812
- lineHeight: "0",
2813
- height: config.gap,
2814
- }, children: "\u00A0" }))] }), index < numChildren - 1 &&
2815
- config.gap && (jsx("td", { width: config.gap, style: gapTdStyle,
2816
- // Mirror of Container's desktop-gap-column pattern:
2817
- // when isStacking, the non-@media .desktop-gap-column
2818
- // rule collapses the between-column gap td so it does
2819
- // not create phantom space while children are stacked.
2820
- className: `row-gap-td${isStacking ? " desktop-gap-column" : ""}`, children: "\u00A0" }, `row-gap-${index}`))] }, `row-child-${index}`))) }) }) }) }) }) }) }) }) }) }) }) }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] }));
2962
+ }, 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) => {
2963
+ // Resolve the column width for this child based on
2964
+ // layoutColumns. undefined when layoutColumns is not set,
2965
+ // preserving the original behaviour (retrocompat).
2966
+ const columnWidth = resolveChildColumnWidth(config.layoutColumns, index, numChildren);
2967
+ return (jsxs(Fragment, { children: [jsxs("td", { align: tdAlign, ...(columnWidth && {
2968
+ width: columnWidth,
2969
+ }), style: {
2970
+ verticalAlign: tdValign,
2971
+ textAlign: tdAlign,
2972
+ padding: "0",
2973
+ margin: "0",
2974
+ ...(columnWidth && {
2975
+ width: columnWidth,
2976
+ }),
2977
+ },
2978
+ // Mirror of Container's stack-td pattern: when isStacking,
2979
+ // the non-@media .stack-td rule forces display:block +
2980
+ // width:100% on each child, which survives Gmail's
2981
+ // @media stripping and achieves true mobile stacking.
2982
+ className: `child-cell${isStacking ? " stack-td" : ""}`, children: [child, isStacking &&
2983
+ index < numChildren - 1 &&
2984
+ config.gap && (jsx("div", { className: "mobile-gap-spacer", style: {
2985
+ display: "none",
2986
+ fontSize: "0",
2987
+ lineHeight: "0",
2988
+ height: config.gap,
2989
+ background: "transparent",
2990
+ }, children: "\u00A0" }))] }), index < numChildren - 1 &&
2991
+ config.gap && (jsx("td", { width: config.gap, style: gapTdStyle,
2992
+ // Mirror of Container's desktop-gap-column pattern:
2993
+ // when isStacking, the non-@media .desktop-gap-column
2994
+ // rule collapses the between-column gap td so it does
2995
+ // not create phantom space while children are stacked.
2996
+ className: `row-gap-td${isStacking ? " desktop-gap-column" : ""}`, children: "\u00A0" }, `row-gap-${index}`))] }, `row-child-${index}`));
2997
+ }) }) }) }) }) }) }) }) }) }) }) }) }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] }));
2821
2998
  if (href && !devMode) {
2822
- return (jsx("a", { href: href, ...({ target }), style: {
2999
+ return (jsx("a", { href: href, ...({ target }), ...rootBindingProps(bindings), style: {
2823
3000
  textDecoration: "none",
2824
3001
  color: "inherit",
2825
3002
  display: "block",
@@ -2864,10 +3041,10 @@ function getBorderStyle$1(border) {
2864
3041
  }
2865
3042
  return style;
2866
3043
  }
2867
- const Section = ({ config, children, devNode, }) => {
3044
+ const Section = ({ config, children, devNode, bindings, }) => {
2868
3045
  var _a, _b, _c;
2869
3046
  const { sectionType, padding } = config;
2870
- return (jsxs("table", { "aria-label": `Section |Table | ${sectionType}`, role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
3047
+ return (jsxs("table", { "aria-label": `Section |Table | ${sectionType}`, role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
2871
3048
  position: "relative",
2872
3049
  width: "100%",
2873
3050
  backgroundColor: config.backgroundColor,
@@ -2880,58 +3057,84 @@ const Section = ({ config, children, devNode, }) => {
2880
3057
  backgroundPosition: (_c = config.backgroundImage) === null || _c === void 0 ? void 0 : _c.position,
2881
3058
  }, children: [jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: {
2882
3059
  padding: padding,
2883
- }, children: children }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsxs("td", { children: [jsxs("span", { style: {
2884
- backgroundColor: "black",
2885
- color: "white",
2886
- padding: "4px",
2887
- fontSize: "14px",
2888
- position: "absolute",
2889
- left: 0,
2890
- top: 0,
2891
- }, children: ["Section | ", sectionType] }), children] }) }) }))] }));
3060
+ }, ...listBindingProps(bindings), children: children }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: jsxs("span", { style: {
3061
+ backgroundColor: "black",
3062
+ color: "white",
3063
+ padding: "4px",
3064
+ fontSize: "14px",
3065
+ position: "absolute",
3066
+ left: 0,
3067
+ top: 0,
3068
+ }, children: ["Section | ", sectionType] }) }) }) }))] }));
2892
3069
  };
2893
3070
  var Section_default = memo(Section, arePropsEqual);
2894
3071
 
2895
- function Spacer({ config, devNode }) {
2896
- const { height, hideOnMobile } = config;
3072
+ function Spacer({ config, devNode, bindings }) {
3073
+ var _a, _b, _c;
3074
+ const { height, hideOnMobile, backgroundColor, backgroundImage } = config;
2897
3075
  // 1. Spacer Table Style
2898
3076
  const spacerTableStyle = {
2899
- // Crucial for compatibility: Ensures no background or border interference
2900
- backgroundColor: "transparent",
2901
3077
  borderCollapse: "collapse",
2902
- border: "0",
2903
3078
  width: "100%",
2904
- // Note the CSS standard dash convention: 'mso-table-lspace'
2905
- // ["mso-table-lspace" as string]: "0pt",
2906
- ["msoTableLspace"]: "0pt",
2907
- // ["mso-table-rspace" as string]: "0pt",
2908
- ["msoTableRspace"]: "0pt",
2909
3079
  };
2910
3080
  // 2. Spacer TD Style: The element that creates the actual vertical space
2911
3081
  const spacerTdStyle = {
2912
3082
  height: height,
2913
- // Critical: Suppress any vertical height created by text/font
3083
+ maxHeight: height,
2914
3084
  fontSize: "0",
2915
3085
  lineHeight: "0",
2916
3086
  padding: "0",
3087
+ margin: "0",
3088
+ border: "none",
3089
+ color: "transparent",
3090
+ // Background applied at TD level for Outlook Classic compatibility
3091
+ ...(backgroundColor && { backgroundColor }),
3092
+ ...(backgroundImage && {
3093
+ backgroundImage: `url(${backgroundImage.src})`,
3094
+ backgroundRepeat: (_a = backgroundImage.repeat) !== null && _a !== void 0 ? _a : "no-repeat",
3095
+ backgroundSize: (_b = backgroundImage.size) !== null && _b !== void 0 ? _b : "cover",
3096
+ backgroundPosition: (_c = backgroundImage.position) !== null && _c !== void 0 ? _c : "center",
3097
+ }),
2917
3098
  };
2918
3099
  // Parse height for the HTML attribute
2919
3100
  const spacerHeightAttribute = parseInt(height, 10) || 1;
2920
- return (
2921
- // Outer table ensures the spacer spans the full width of its container
2922
- jsxs("table", { "aria-label": "Vertical Spacer", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
3101
+ return (jsxs("table", { "aria-label": "Vertical Spacer", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
2923
3102
  // --- Start dev
2924
3103
  position: "relative",
2925
3104
  // --- End dev
2926
3105
  ...spacerTableStyle,
2927
- }, ...{ height: spacerHeightAttribute }, className: hideOnMobile ? "hide-on-mobile" : undefined, children: [jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: spacerTdStyle,
2928
- // Explicit height attribute
2929
- height: spacerHeightAttribute, children: "\u00A0" }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] }));
3106
+ }, ...{ height: spacerHeightAttribute }, className: hideOnMobile ? "hide-on-mobile" : undefined, children: [jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: spacerTdStyle, height: spacerHeightAttribute, ...(backgroundColor && !backgroundImage
3107
+ ? { bgcolor: backgroundColor }
3108
+ : {}) }) }) }), devNode && (jsx("tfoot", { children: jsx("tr", { children: jsx("td", { children: devNode }) }) }))] }));
2930
3109
  }
2931
3110
  var Spacer_default = memo(Spacer, arePropsEqual);
2932
3111
 
2933
- function Text({ config, devMode, children }) {
2934
- const { text, padding, color, textAlign, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, backgroundColor, opacity, whiteSpace, wordBreak = "break-all", maxWidth, } = config;
3112
+ // Helper to build link href based on innerLink type
3113
+ function buildLinkHref$1(innerLink) {
3114
+ if (!innerLink || innerLink.type === "none")
3115
+ return null;
3116
+ switch (innerLink.type) {
3117
+ case "url":
3118
+ return innerLink.url || null;
3119
+ case "email":
3120
+ return innerLink.email ? `mailto:${innerLink.email}` : null;
3121
+ case "phone":
3122
+ return innerLink.phone ? `tel:${innerLink.phone}` : null;
3123
+ case "anchor":
3124
+ return innerLink.anchor ? `#${innerLink.anchor}` : null;
3125
+ case "page_top":
3126
+ return "#top";
3127
+ case "page_bottom":
3128
+ return "#bottom";
3129
+ default:
3130
+ return null;
3131
+ }
3132
+ }
3133
+ function Text({ config, devMode, children, bindings }) {
3134
+ const { text, padding, color, textAlign, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, backgroundColor, opacity, whiteSpace, wordBreak = "break-all", maxWidth, innerLink, } = config;
3135
+ // Resolve href and target from innerLink
3136
+ const href = buildLinkHref$1(innerLink);
3137
+ const target = (innerLink === null || innerLink === void 0 ? void 0 : innerLink.target) || "_blank";
2935
3138
  // 1. TD Style: Where padding and background are reliably applied.
2936
3139
  // When maxWidth is set, this TD stays at width: 100% so it always fills
2937
3140
  // its parent — the inner maxWidth table (see below) does the actual
@@ -2984,26 +3187,31 @@ function Text({ config, devMode, children }) {
2984
3187
  ? injectLinkStyles(content, contentStyle)
2985
3188
  : "";
2986
3189
  const innerContent = isString ? (jsx("div", { style: contentStyle, dangerouslySetInnerHTML: { __html: processedHtml } })) : (jsx("div", { style: contentStyle, children: content }));
2987
- return (jsx("table", { "aria-label": "Text Block Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
3190
+ const wrappedContent = maxWidth ? (
3191
+ /*
3192
+ * maxWidth wrapper — Outlook Classic compatibility pattern:
3193
+ *
3194
+ * <center> instructs the Word rendering engine to horizontally
3195
+ * centre its child block, equivalent to margin: 0 auto in CSS.
3196
+ *
3197
+ * The inner table carries the `width` HTML attribute set to the
3198
+ * maxWidth value. Outlook Classic reads `width` as a hard pixel
3199
+ * cap; it has no concept of max-width so this is the only lever
3200
+ * available. Modern clients receive the CSS max-width on the
3201
+ * same table and behave correctly.
3202
+ *
3203
+ * The outer TD remains at width: 100% so it always fills its
3204
+ * parent cell in every client — only the inner content is capped.
3205
+ */
3206
+ 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);
3207
+ return (jsx("table", { "aria-label": "Text Block Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, ...rootBindingProps(bindings), style: {
2988
3208
  width: "100%",
2989
3209
  borderCollapse: "collapse",
2990
- }, children: jsx("tbody", { children: jsx("tr", { children: jsx("td", { style: tdStyle, align: textAlign, children: maxWidth ? (
2991
- /*
2992
- * maxWidth wrapper — Outlook Classic compatibility pattern:
2993
- *
2994
- * <center> instructs the Word rendering engine to horizontally
2995
- * centre its child block, equivalent to margin: 0 auto in CSS.
2996
- *
2997
- * The inner table carries the `width` HTML attribute set to the
2998
- * maxWidth value. Outlook Classic reads `width` as a hard pixel
2999
- * cap; it has no concept of max-width so this is the only lever
3000
- * available. Modern clients receive the CSS max-width on the
3001
- * same table and behave correctly.
3002
- *
3003
- * The outer TD remains at width: 100% so it always fills its
3004
- * parent cell in every client — only the inner content is capped.
3005
- */
3006
- 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) }) }) }) }));
3210
+ }, 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: {
3211
+ display: "block",
3212
+ textDecoration: "none",
3213
+ color: "inherit",
3214
+ }, children: wrappedContent })) : (wrappedContent) }) }) }) }));
3007
3215
  }
3008
3216
  var Text_default = memo(Text, arePropsEqual);
3009
3217
 
@@ -3129,7 +3337,7 @@ function buildLinkHref(innerLink) {
3129
3337
  return null;
3130
3338
  }
3131
3339
  }
3132
- function Icon({ config, devNode, devMode, children }) {
3340
+ function Icon({ config, devNode, devMode, children, bindings }) {
3133
3341
  const {
3134
3342
  // base64Source,
3135
3343
  width, height, backgroundColor, padding = "0", borderRadius = "0", border, innerLink, justifyContent = "center", } = config;
@@ -3264,7 +3472,7 @@ function Icon({ config, devNode, devMode, children }) {
3264
3472
  <!--<![endif]-->
3265
3473
  `
3266
3474
  : null;
3267
- return (jsxs("table", { "aria-label": "Icon", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, align: align, style: {
3475
+ return (jsxs("table", { "aria-label": "Icon", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, align: align, ...rootBindingProps(bindings), style: {
3268
3476
  // --- Start dev
3269
3477
  position: "relative",
3270
3478
  // --- End dev