@pagenflow/email 1.4.0 → 1.4.2

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.
@@ -4,6 +4,7 @@ export interface GlobalConfig {
4
4
  fontSize?: string;
5
5
  backgroundColor?: string;
6
6
  lineHeight?: string;
7
+ fontFamily?: string;
7
8
  backgroundImage?: {
8
9
  src?: string;
9
10
  repeat?: string;
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from "react";
2
- import type { ResolvedFont } from "@/lib/email-fonts/loadEmailFonts";
2
+ import ResolvedFont from "../types/ResolvedFont";
3
3
  export interface HeadProps {
4
4
  /** Additional elements like custom <style> blocks, <title>, etc. */
5
5
  children?: ReactNode;
@@ -16,7 +16,7 @@ export interface HeadProps {
16
16
  *
17
17
  * Example:
18
18
  * const { resolved } = await loadEmailFonts(
19
- * extractFontFamilies(tree)
19
+ * extractFontFamilies(doc)
20
20
  * );
21
21
  * <Head fonts={resolved} ... />
22
22
  */
@@ -1,5 +1,5 @@
1
- import { ResolvedFont } from "@/lib/email-fonts/loadEmailFonts";
2
1
  import { ReactNode } from "react";
2
+ import ResolvedFont from "../types/ResolvedFont";
3
3
  export interface HeadDevProps {
4
4
  /** Additional elements like custom <style> blocks, <title>, etc. */
5
5
  children?: ReactNode;
@@ -1,6 +1,5 @@
1
- import { ReactNode } from "react";
1
+ import { CSSProperties, ReactNode } from "react";
2
2
  import { BorderConfig } from "../types";
3
- /** Style-only mobile overrides. Content props (src, alt, href, target) are excluded. */
4
3
  export interface ImageMobileConfig {
5
4
  width?: string;
6
5
  height?: string;
@@ -10,7 +9,8 @@ export interface ImageMobileConfig {
10
9
  padding?: string;
11
10
  borderRadius?: string;
12
11
  border?: BorderConfig;
13
- /** When true, the mobile version of the image is not rendered at all. */
12
+ objectFit?: CSSProperties["objectFit"];
13
+ objectPosition?: string;
14
14
  hidden?: boolean;
15
15
  }
16
16
  export interface ImageConfig {
@@ -26,17 +26,15 @@ export interface ImageConfig {
26
26
  border?: BorderConfig;
27
27
  href?: string;
28
28
  target?: string;
29
- /**
30
- * Mobile-specific style overrides.
31
- * Only explicitly set properties override the desktop value on mobile.
32
- * Unset properties fall back to the desktop value.
33
- */
29
+ objectFit?: CSSProperties["objectFit"];
30
+ objectPosition?: string;
34
31
  mobile?: ImageMobileConfig;
35
32
  }
36
33
  export type ImageProps = {
37
34
  config: ImageConfig;
38
35
  devNode?: ReactNode;
39
36
  devMode?: boolean;
37
+ previewMode?: boolean;
40
38
  };
41
39
  declare function Image({ config, devNode, devMode }: ImageProps): import("react/jsx-runtime").JSX.Element;
42
40
  declare const _default: import("react").MemoExoticComponent<typeof Image>;
@@ -13,6 +13,16 @@ export interface RowConfig {
13
13
  alignItems?: AlignItems;
14
14
  width?: string;
15
15
  height?: string;
16
+ /**
17
+ * When true, the content table uses width:100% so Outlook Classic has a
18
+ * hard boundary and text children can wrap correctly.
19
+ * Use this for rows that contain text blocks alongside images.
20
+ *
21
+ * When false/undefined (default), the content table uses width:auto so
22
+ * children shrink-wrap to their natural sizes — preserving the original
23
+ * behavior. Use this for icon rows, button rows, social link rows.
24
+ */
25
+ fillWidth?: boolean;
16
26
  padding?: string;
17
27
  backgroundColor?: string;
18
28
  backgroundImage?: BackgroundImageType;
@@ -36,6 +36,7 @@ export interface TextConfig {
36
36
  whiteSpace?: string;
37
37
  /** Word break behavior (e.g., 'break-all', 'break-word', 'keep-all', 'normal'). */
38
38
  wordBreak?: string;
39
+ maxWidth?: string;
39
40
  }
40
41
  export type TextProps = {
41
42
  config: TextConfig;
package/dist/index.cjs.js CHANGED
@@ -10,20 +10,14 @@ function Body({ children, config = {} }) {
10
10
  const globalFontSize = config.fontSize || "16px";
11
11
  const globalBackgroundColor = config.backgroundColor || "#ffffff";
12
12
  const globalLineHeight = config.lineHeight || "1.4";
13
+ const globalFontFamily = config.fontFamily || "Arial, Helvetica, sans-serif";
13
14
  // Background image properties
14
15
  const bgImage = ((_a = config.backgroundImage) === null || _a === void 0 ? void 0 : _a.src) || "";
15
16
  const bgRepeat = ((_b = config.backgroundImage) === null || _b === void 0 ? void 0 : _b.repeat) || "no-repeat";
16
17
  const bgSize = ((_c = config.backgroundImage) === null || _c === void 0 ? void 0 : _c.size) || "cover";
17
18
  const bgPosition = ((_d = config.backgroundImage) === null || _d === void 0 ? void 0 : _d.position) || "center";
18
19
  // 1. Style for the <body> tag inline
19
- const bodyStyle = Object.assign({ backgroundColor: globalBackgroundColor, color: globalColor, fontSize: globalFontSize, lineHeight: globalLineHeight, padding: "0", margin: "0", WebkitTextSizeAdjust: "100%", overflowX: "hidden",
20
- // ✅ FIX 1: Use string indexing for MSO property
21
- // ['ms-text-size-adjust' as string]: '100%',
22
- ["msTextSizeAdjust"]: "100%",
23
- // ["mso-line-height-rule" as string]: "exactly",
24
- ["msoLineHeightRule"]: "exactly",
25
- // Base font for body
26
- fontFamily: "Arial, Helvetica, sans-serif" }, (bgImage && {
20
+ const bodyStyle = Object.assign({ backgroundColor: globalBackgroundColor, color: globalColor, fontSize: globalFontSize, lineHeight: globalLineHeight, padding: "0", margin: "0", WebkitTextSizeAdjust: "100%", overflowX: "hidden", ["msTextSizeAdjust"]: "100%", ["msoLineHeightRule"]: "exactly", fontFamily: globalFontFamily }, (bgImage && {
27
21
  backgroundImage: `url(${bgImage})`,
28
22
  backgroundRepeat: bgRepeat,
29
23
  backgroundSize: bgSize,
@@ -32,14 +26,10 @@ function Body({ children, config = {} }) {
32
26
  // 2. Style for the top-level <table> wrapper
33
27
  const outerTableStyle = {
34
28
  width: "100%",
35
- // ✅ FIX 1 (on table): Use string indexing for MSO property
36
29
  ["msoLineHeightRule"]: "exactly",
37
- // ['mso-line-height-rule' as string]: 'exactly',
38
30
  borderCollapse: "collapse",
39
31
  };
40
- return (
41
- // The <body> tag with inline styles
42
- jsxRuntime.jsxs("body", { style: bodyStyle, children: [jsxRuntime.jsx("center", { style: Object.assign({ width: "100%", background: globalBackgroundColor }, (bgImage && {
32
+ return (jsxRuntime.jsxs("body", { style: bodyStyle, children: [jsxRuntime.jsx("center", { style: Object.assign({ width: "100%", background: globalBackgroundColor }, (bgImage && {
43
33
  backgroundImage: `url(${bgImage})`,
44
34
  backgroundRepeat: bgRepeat,
45
35
  backgroundSize: bgSize,
@@ -1681,9 +1671,12 @@ function Button({ config, devMode }) {
1681
1671
  else {
1682
1672
  vmlAlignStyle = `text-align:${textAlign};`;
1683
1673
  }
1674
+ // Border radius is intentionally omitted (arcsize="0%") for Outlook Classic.
1675
+ // Outlook Classic does not reliably support rounded corners and the result
1676
+ // is inconsistent, so we render sharp corners there instead.
1684
1677
  vmlButton = `
1685
1678
  <!--[if mso]>
1686
- <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${href}" style="height:${vmlHeight}px;width:${vmlWidth}px;" arcsize="${Math.min((parseInt(borderRadius) / vmlHeight) * 100, 50)}%" strokecolor="${vmlStrokeColor}" ${hasVmlStroke ? `strokeweight="${vmlStrokeWeight}px"` : 'stroke="f"'} fillcolor="${vmlFillColor}">
1679
+ <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${href}" style="height:${vmlHeight}px;width:${vmlWidth}px;" arcsize="0%" strokecolor="${vmlStrokeColor}" ${hasVmlStroke ? `strokeweight="${vmlStrokeWeight}px"` : 'stroke="f"'} fillcolor="${vmlFillColor}">
1687
1680
  <w:anchorlock/>
1688
1681
  <v:textbox inset="${horizontalPaddingValue}px,${numericPadding}px,${horizontalPaddingValue}px,${numericPadding}px">
1689
1682
  <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="border-collapse:collapse;">
@@ -1716,6 +1709,9 @@ function Button({ config, devMode }) {
1716
1709
  const directionProp = direction ? `direction: ${direction};` : "";
1717
1710
  const opacityProp = opacity !== undefined ? `opacity: ${opacity};` : "";
1718
1711
  const wordBreakProp = wordBreak !== "break-word" ? `word-break: ${wordBreak};` : "";
1712
+ // Border radius is intentionally omitted from the Outlook Classic table cell.
1713
+ // Outlook Classic ignores border-radius on table cells anyway, and including it
1714
+ // can cause unexpected rendering artifacts, so we explicitly leave it out.
1719
1715
  simpleOutlookButton = `
1720
1716
  <!--[if mso]>
1721
1717
  <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="border-collapse: collapse;">
@@ -1723,7 +1719,7 @@ function Button({ config, devMode }) {
1723
1719
  <td align="${align}" style="padding: 0;">
1724
1720
  <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="${width || "auto"}" style="border-collapse: collapse;">
1725
1721
  <tr>
1726
- <td bgcolor="${backgroundColor}" align="${textAlign}" style="padding: ${padding}; text-align: ${textAlign}; border-radius: ${borderRadius}; ${borderStyleString}">
1722
+ <td bgcolor="${backgroundColor}" align="${textAlign}" style="padding: ${padding}; text-align: ${textAlign}; ${borderStyleString}">
1727
1723
  <a href="${href}" target="_blank" rel="noopener noreferrer" style="color: ${color}; ${textDecorationStyle} display: block; font-family: ${safeFontFamily}; font-size: ${fontSize}; font-weight: ${fontWeight}; ${fontStyleProp} line-height: ${lineHeight}; ${letterSpacingProp} ${textTransformProp} text-align: ${textAlign}; ${whiteSpaceProp} ${directionProp} ${opacityProp} ${wordBreakProp} mso-line-height-rule: exactly;">
1728
1724
  ${typeof children === "string" ? children : ""}
1729
1725
  </a>
@@ -2352,28 +2348,6 @@ function Html({ children, backgroundColor = "#ffffff", }) {
2352
2348
  );
2353
2349
  }
2354
2350
 
2355
- /**
2356
- * Content rendered by Outlook Classic only.
2357
- * Outputs: <!--[if mso]> ... <![endif]-->
2358
- */
2359
- function MsoOnly({ html }) {
2360
- return (jsxRuntime.jsx("td", { dangerouslySetInnerHTML: {
2361
- __html: `<!--[if mso]>${html}<![endif]-->`,
2362
- } }));
2363
- }
2364
- /**
2365
- * Content hidden from Outlook Classic, visible in all other clients.
2366
- * Outputs: <!--[if !mso]><!--> ... <!--<![endif]-->
2367
- */
2368
- function NonMso({ html }) {
2369
- return (jsxRuntime.jsx("td", { dangerouslySetInnerHTML: {
2370
- __html: `<!--[if !mso]><!-->${html}<!--<![endif]-->`,
2371
- } }));
2372
- }
2373
-
2374
- // ---------------------------------------------------------------------------
2375
- // Helpers
2376
- // ---------------------------------------------------------------------------
2377
2351
  function getBorderStyle$3(border) {
2378
2352
  if (!border)
2379
2353
  return {};
@@ -2381,15 +2355,6 @@ function getBorderStyle$3(border) {
2381
2355
  if (border.width && border.style && border.color) {
2382
2356
  style.border = `${border.width} ${border.style} ${border.color}`;
2383
2357
  }
2384
- else {
2385
- const hasIndividual = border.top || border.right || border.bottom || border.left;
2386
- if (hasIndividual) {
2387
- style.borderTop = "none";
2388
- style.borderRight = "none";
2389
- style.borderBottom = "none";
2390
- style.borderLeft = "none";
2391
- }
2392
- }
2393
2358
  if (border.top)
2394
2359
  style.borderTop = `${border.top.width} ${border.top.style} ${border.top.color}`;
2395
2360
  if (border.right)
@@ -2404,143 +2369,87 @@ function getBorderStyleString$1(border) {
2404
2369
  if (!border)
2405
2370
  return "";
2406
2371
  const styles = [];
2372
+ // Standard shorthand
2407
2373
  if (border.width && border.style && border.color) {
2408
- styles.push(`border:${border.width} ${border.style} ${border.color};`);
2374
+ styles.push(`border:${border.width} ${border.style} ${border.color} !important;`);
2409
2375
  }
2410
2376
  else {
2411
- const hasIndividual = border.top || border.right || border.bottom || border.left;
2412
- if (hasIndividual) {
2413
- styles.push("border-top:none;", "border-right:none;", "border-bottom:none;", "border-left:none;");
2414
- }
2377
+ // If desktop had a border and mobile wants "none", we must explicitly kill it
2378
+ styles.push(`border: none !important;`);
2415
2379
  }
2380
+ // Individual sides
2416
2381
  if (border.top)
2417
- styles.push(`border-top:${border.top.width} ${border.top.style} ${border.top.color};`);
2382
+ styles.push(`border-top:${border.top.width} ${border.top.style} ${border.top.color} !important;`);
2418
2383
  if (border.right)
2419
- styles.push(`border-right:${border.right.width} ${border.right.style} ${border.right.color};`);
2384
+ styles.push(`border-right:${border.right.width} ${border.right.style} ${border.right.color} !important;`);
2420
2385
  if (border.bottom)
2421
- styles.push(`border-bottom:${border.bottom.width} ${border.bottom.style} ${border.bottom.color};`);
2386
+ styles.push(`border-bottom:${border.bottom.width} ${border.bottom.style} ${border.bottom.color} !important;`);
2422
2387
  if (border.left)
2423
- styles.push(`border-left:${border.left.width} ${border.left.style} ${border.left.color};`);
2388
+ styles.push(`border-left:${border.left.width} ${border.left.style} ${border.left.color} !important;`);
2424
2389
  return styles.join(" ");
2425
2390
  }
2426
- // ---------------------------------------------------------------------------
2427
- // Merged styles helper — applies mobile overrides on top of desktop values
2428
- // ---------------------------------------------------------------------------
2429
- function mergeConfig(config, overrides) {
2430
- var _a, _b, _c, _d, _e, _f, _g, _h;
2431
- return {
2432
- width: (_a = overrides === null || overrides === void 0 ? void 0 : overrides.width) !== null && _a !== void 0 ? _a : config.width,
2433
- height: (_b = overrides === null || overrides === void 0 ? void 0 : overrides.height) !== null && _b !== void 0 ? _b : config.height,
2434
- maxWidth: (_c = overrides === null || overrides === void 0 ? void 0 : overrides.maxWidth) !== null && _c !== void 0 ? _c : config.maxWidth,
2435
- maxHeight: (_d = overrides === null || overrides === void 0 ? void 0 : overrides.maxHeight) !== null && _d !== void 0 ? _d : config.maxHeight,
2436
- backgroundColor: (_e = overrides === null || overrides === void 0 ? void 0 : overrides.backgroundColor) !== null && _e !== void 0 ? _e : config.backgroundColor,
2437
- padding: (_f = overrides === null || overrides === void 0 ? void 0 : overrides.padding) !== null && _f !== void 0 ? _f : config.padding,
2438
- borderRadius: (_g = overrides === null || overrides === void 0 ? void 0 : overrides.borderRadius) !== null && _g !== void 0 ? _g : config.borderRadius,
2439
- border: (_h = overrides === null || overrides === void 0 ? void 0 : overrides.border) !== null && _h !== void 0 ? _h : config.border,
2440
- };
2441
- }
2442
- // ---------------------------------------------------------------------------
2443
- // Desktop table — JSX (same as original)
2444
- // ---------------------------------------------------------------------------
2445
- function renderDesktopTable({ config, className, devNode, devMode, }) {
2446
- const { src, alt, href, target } = config;
2447
- const { width, height, maxWidth, maxHeight, backgroundColor, padding, borderRadius, border, } = mergeConfig(config);
2448
- const borderStyle = getBorderStyle$3(border);
2449
- const imgStyle = Object.assign({ display: "block", objectFit: "cover", width: width || "100%", height: height || "auto", maxWidth: maxWidth || "100%", maxHeight: maxHeight, border: "0", borderRadius: borderRadius }, borderStyle);
2450
- const linkStyle = {
2451
- display: "block",
2452
- textDecoration: "none",
2453
- border: "0",
2454
- outline: "none",
2455
- };
2456
- const tdStyle = {
2457
- padding: padding,
2458
- backgroundColor: backgroundColor,
2459
- fontSize: "0",
2460
- lineHeight: "0",
2461
- };
2462
- const widthNum = (width === null || width === void 0 ? void 0 : width.endsWith("px")) ? parseInt(width, 10) : undefined;
2463
- const maxWidthNum = (maxWidth === null || maxWidth === void 0 ? void 0 : maxWidth.endsWith("px"))
2464
- ? parseInt(maxWidth, 10)
2465
- : undefined;
2466
- const heightNum = (height === null || height === void 0 ? void 0 : height.endsWith("px")) ? parseInt(height, 10) : undefined;
2467
- const imageElement = (jsxRuntime.jsx("img", { draggable: false, src: src, alt: alt, style: imgStyle, width: widthNum && maxWidthNum
2468
- ? Math.min(widthNum, maxWidthNum)
2469
- : widthNum || maxWidthNum, height: heightNum, border: 0 }));
2470
- const content = href && !devMode ? (jsxRuntime.jsx("a", Object.assign({ href: href, target: target, style: linkStyle }, (target === "_blank" ? { rel: "noopener noreferrer" } : {}), { children: imageElement }))) : (imageElement);
2471
- return (jsxRuntime.jsxs("table", { "aria-label": `Image Wrapper for: ${alt}`, role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, className: className, style: {
2472
- position: "relative",
2473
- width: width || "100%",
2474
- borderCollapse: "collapse",
2475
- }, onClick: devMode ? (e) => e.preventDefault() : undefined, children: [jsxRuntime.jsx("tbody", { children: jsxRuntime.jsx("tr", { children: jsxRuntime.jsx("td", { style: tdStyle, align: "center", children: content }) }) }), devMode && !!devNode && (jsxRuntime.jsx("tfoot", { children: jsxRuntime.jsx("tr", { children: jsxRuntime.jsx("td", { children: devNode }) }) }))] }));
2476
- }
2477
- // ---------------------------------------------------------------------------
2478
- // Mobile table — HTML string (injected via NonMso, same pattern as Icon VML)
2479
- // ---------------------------------------------------------------------------
2480
- function buildMobileTableHTML({ config, overrides, className, }) {
2481
- const { src, alt, href, target } = config;
2482
- const { width, height, maxWidth, maxHeight, backgroundColor, padding, borderRadius, border, } = mergeConfig(config, overrides);
2483
- const borderStyleStr = getBorderStyleString$1(border);
2484
- const widthNum = (width === null || width === void 0 ? void 0 : width.endsWith("px")) ? parseInt(width, 10) : undefined;
2485
- const maxWidthNum = (maxWidth === null || maxWidth === void 0 ? void 0 : maxWidth.endsWith("px"))
2486
- ? parseInt(maxWidth, 10)
2487
- : undefined;
2488
- const heightNum = (height === null || height === void 0 ? void 0 : height.endsWith("px")) ? parseInt(height, 10) : undefined;
2489
- const resolvedWidth = widthNum && maxWidthNum
2490
- ? Math.min(widthNum, maxWidthNum)
2491
- : widthNum || maxWidthNum;
2492
- const imgTag = `<img
2493
- draggable="false"
2494
- src="${src}"
2495
- alt="${alt}"
2496
- ${resolvedWidth ? `width="${resolvedWidth}"` : ""}
2497
- ${heightNum ? `height="${heightNum}"` : ""}
2498
- border="0"
2499
- style="display:block;object-fit:cover;width:${width || "100%"};height:${height || "auto"};max-width:${maxWidth || "100%"};${maxHeight ? `max-height:${maxHeight};` : ""}border:0;${borderRadius ? `border-radius:${borderRadius};` : ""}${borderStyleStr}"
2500
- />`;
2501
- const content = href
2502
- ? `<a href="${href}" target="${target || "_self"}" style="display:block;text-decoration:none;border:0;outline:none;"${target === "_blank" ? ' rel="noopener noreferrer"' : ""}>${imgTag}</a>`
2503
- : imgTag;
2504
- return `
2505
- <table
2506
- aria-label="Image Wrapper for: ${alt}"
2507
- role="presentation"
2508
- cellpadding="0"
2509
- cellspacing="0"
2510
- border="0"
2511
- class="${className}"
2512
- style="position:relative;width:${width || "100%"};border-collapse:collapse;"
2513
- >
2514
- <tbody>
2515
- <tr>
2516
- <td
2517
- align="center"
2518
- style="padding:${padding || ""};background-color:${backgroundColor || ""};font-size:0;line-height:0;"
2519
- >
2520
- ${content}
2521
- </td>
2522
- </tr>
2523
- </tbody>
2524
- </table>
2525
- `;
2526
- }
2527
- // ---------------------------------------------------------------------------
2528
- // Component
2529
- // ---------------------------------------------------------------------------
2530
2391
  function Image({ config, devNode, devMode }) {
2531
- const { mobile } = config;
2532
- const hasMobileOverrides = !!mobile && !mobile.hidden;
2533
- const isHiddenOnMobile = !!(mobile === null || mobile === void 0 ? void 0 : mobile.hidden);
2534
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderDesktopTable({
2535
- config,
2536
- className: hasMobileOverrides || isHiddenOnMobile ? "hide-on-mobile" : undefined,
2537
- devNode,
2538
- devMode,
2539
- }), hasMobileOverrides && !devMode && (jsxRuntime.jsx("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: { width: "100%", borderCollapse: "collapse" }, children: jsxRuntime.jsx("tbody", { children: jsxRuntime.jsx("tr", { children: jsxRuntime.jsx(NonMso, { html: buildMobileTableHTML({
2540
- config,
2541
- overrides: mobile,
2542
- className: "hide-on-desktop",
2543
- }) }) }) }) }))] }));
2392
+ var _a;
2393
+ const { src, alt, href, target, mobile } = config;
2394
+ const seed = src + (alt || "");
2395
+ const instanceId = seed
2396
+ .split("")
2397
+ .reduce((acc, char) => acc + char.charCodeAt(0), 0)
2398
+ .toString(36);
2399
+ const imgClass = `img-${instanceId}`;
2400
+ // 1. Desktop Dimensional Logic
2401
+ const desktopWidth = config.width || "100%";
2402
+ const isPercent = desktopWidth.includes("%");
2403
+ const widthAttr = desktopWidth.replace("px", "");
2404
+ const heightAttr = (_a = config.height) === null || _a === void 0 ? void 0 : _a.replace("px", "");
2405
+ // Determine the table's "initial" width.
2406
+ // If it's 300px, the table should be 300px, not 100%.
2407
+ const tableWidth = isPercent ? desktopWidth : `${widthAttr}px`;
2408
+ // 2. Mobile Overrides (Every property used)
2409
+ let mobileCss = "";
2410
+ if (mobile) {
2411
+ mobileCss = `
2412
+ @media screen and (max-width: 768px) {
2413
+ .wrap-${imgClass} {
2414
+ /* This breaks the px lock from desktop and makes it fluid */
2415
+ width: ${mobile.width || "100%"} !important;
2416
+ max-width: ${mobile.maxWidth || "100%"} !important;
2417
+ min-width: 0 !important;
2418
+ }
2419
+ .td-${imgClass} {
2420
+ padding: ${mobile.padding || "0"} !important;
2421
+ background-color: ${mobile.backgroundColor || "transparent"} !important;
2422
+ width: 100% !important;
2423
+ }
2424
+ .${imgClass} {
2425
+ width: ${mobile.width || "100%"} !important;
2426
+ height: ${mobile.height || "auto"} !important;
2427
+ max-width: ${mobile.maxWidth || "100%"} !important;
2428
+ max-height: ${mobile.maxHeight || "none"} !important;
2429
+ border-radius: ${mobile.borderRadius || "0"} !important;
2430
+ display: ${mobile.hidden ? "none" : "block"} !important;
2431
+ object-fit: ${mobile.objectFit || "fill"} !important;
2432
+ object-position: ${mobile.objectPosition || "center"} !important;
2433
+ ${getBorderStyleString$1(mobile.border)}
2434
+ }
2435
+ }
2436
+ `;
2437
+ }
2438
+ const imgStyle = Object.assign(Object.assign({ display: "block", width: isPercent ? "100%" : desktopWidth, height: config.height || "auto", maxWidth: config.maxWidth || "100%", maxHeight: config.maxHeight || "none", borderRadius: config.borderRadius || "0" }, getBorderStyle$3(config.border)), { outline: "none", textDecoration: "none", objectFit: config.objectFit, objectPosition: config.objectPosition });
2439
+ const imageElement = (jsxRuntime.jsx("img", { src: src, alt: alt, width: !isPercent ? widthAttr : undefined, height: heightAttr !== "auto" ? heightAttr : undefined, className: imgClass, style: imgStyle }));
2440
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [mobile && jsxRuntime.jsx("style", { dangerouslySetInnerHTML: { __html: mobileCss } }), jsxRuntime.jsxs("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, className: `wrap-${imgClass}`, align: "center" // Ensures a 300px image stays centered in its parent
2441
+ , style: {
2442
+ width: tableWidth, // Fixed px here prevents the 100% "ghost space"
2443
+ maxWidth: "100%",
2444
+ borderCollapse: "collapse",
2445
+ margin: "0 auto",
2446
+ }, children: [jsxRuntime.jsx("tbody", { children: jsxRuntime.jsx("tr", { children: jsxRuntime.jsx("td", { className: `td-${imgClass}`, align: "center", style: {
2447
+ padding: config.padding,
2448
+ backgroundColor: config.backgroundColor,
2449
+ fontSize: "0",
2450
+ lineHeight: "0",
2451
+ width: tableWidth, // Lock the cell as well
2452
+ }, children: href && !devMode ? (jsxRuntime.jsx("a", { href: href, target: target, style: { display: "block", width: "100%" }, children: imageElement })) : (imageElement) }) }) }), devMode && !!devNode && (jsxRuntime.jsx("tfoot", { children: jsxRuntime.jsx("tr", { children: jsxRuntime.jsx("td", { children: devNode }) }) }))] })] }));
2544
2453
  }
2545
2454
  var Image_default = React.memo(Image, arePropsEqual);
2546
2455
 
@@ -2615,35 +2524,31 @@ function Row({ children, config, devNode, devMode }) {
2615
2524
  const numChildren = childrenArray.length;
2616
2525
  const href = getHrefFromInnerLink(config.innerLink);
2617
2526
  const target = (_a = config.innerLink) === null || _a === void 0 ? void 0 : _a.target;
2618
- // 1. Outer TD for Background and Border Radius (no border here).
2619
- // height declared here is the *total* outer height.
2620
- const backgroundTdStyle = Object.assign({ backgroundColor: config.backgroundColor, borderRadius: config.borderRadius, width: config.width || "100%", height: config.height,
2621
- // Background Image styles
2622
- backgroundImage: config.backgroundImage
2527
+ // 1. Outer TD: Background, Border Radius, Width, Height.
2528
+ const backgroundTdStyle = Object.assign({ backgroundColor: config.backgroundColor, borderRadius: config.borderRadius, width: config.width || "100%", height: config.height, backgroundImage: config.backgroundImage
2623
2529
  ? `url(${config.backgroundImage.src})`
2624
2530
  : undefined, backgroundRepeat: (_b = config.backgroundImage) === null || _b === void 0 ? void 0 : _b.repeat, backgroundSize: (_c = config.backgroundImage) === null || _c === void 0 ? void 0 : _c.size, backgroundPosition: (_d = config.backgroundImage) === null || _d === void 0 ? void 0 : _d.position }, (config.borderRadius && { overflow: "hidden" }));
2625
- // 2. Inner Table for Border and Border Radius.
2626
- // height: 100% so it stretches to fill the outer TD.
2531
+ // 2. Inner Table: Border and Border Radius.
2627
2532
  const borderTableStyle = Object.assign({ width: "100%", height: "100%", borderCollapse: "separate", borderSpacing: 0, borderRadius: config.borderRadius }, getBorderStyle$2(config.border));
2628
- // 3. TD for Padding only — no height.
2629
- // The outer TD owns the total height; setting height here would cause
2630
- // browsers/email clients to treat it as content-box height and add
2631
- // padding on top, making the row taller than the declared height.
2533
+ // 3. Padding TD.
2632
2534
  const paddingTdStyle = {
2633
2535
  padding: config.padding,
2634
2536
  width: "100%",
2635
- // height intentionally omitted — padding must be inner, not additive
2636
2537
  verticalAlign: "top",
2637
2538
  };
2638
- // 4. Content Table - horizontal layout
2639
- const contentTableStyle = {
2640
- width: "auto",
2641
- height: "100%",
2642
- borderCollapse: "collapse",
2643
- minWidth: "1px",
2644
- maxWidth: config.width || "100%",
2645
- };
2646
- // 5. Gap styles for horizontal spacing between children
2539
+ // 4. Content Table.
2540
+ //
2541
+ // fillWidth: false/undefined (default) → width: "auto"
2542
+ // Original behavior. Children shrink-wrap to their natural sizes.
2543
+ // Use for icon rows, button rows, social link rows.
2544
+ // Centering works via the Justification Wrapper TD (align + width="100%").
2545
+ //
2546
+ // fillWidth: true → width: "100%"
2547
+ // Content table fills available space, giving Outlook Classic a hard
2548
+ // boundary so text children get a constrained box and line wrapping
2549
+ // triggers correctly. Use for rows containing text + image layouts.
2550
+ const contentTableStyle = Object.assign({ width: config.fillWidth ? "100%" : "auto", height: "100%", borderCollapse: "collapse", minWidth: "1px" }, (!config.fillWidth && { maxWidth: config.width || "100%" }));
2551
+ // 5. Gap TD.
2647
2552
  const gapTdStyle = {
2648
2553
  width: config.gap || "0",
2649
2554
  lineHeight: "1px",
@@ -2653,7 +2558,6 @@ function Row({ children, config, devNode, devMode }) {
2653
2558
  ? justifyMap$1[config.justifyContent]
2654
2559
  : "left";
2655
2560
  const tdValign = config.alignItems ? alignMap[config.alignItems] : "top";
2656
- // Content to render - wrapped in anchor if innerLink is defined
2657
2561
  const content = (jsxRuntime.jsxs("table", Object.assign({ "aria-label": "Row Outer", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
2658
2562
  position: "relative",
2659
2563
  width: config.width || "100%",
@@ -2663,14 +2567,13 @@ function Row({ children, config, devNode, devMode }) {
2663
2567
  width: "100%",
2664
2568
  height: "100%",
2665
2569
  borderCollapse: "collapse",
2666
- }, children: jsxRuntime.jsx("tbody", { children: jsxRuntime.jsx("tr", { children: jsxRuntime.jsx("td", { align: tdAlign, children: jsxRuntime.jsx("table", Object.assign({ "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: jsxRuntime.jsx("tbody", { children: jsxRuntime.jsx("tr", { className: "content-tr", children: childrenArray.map((child, index) => (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx("td", { valign: tdValign, style: {
2570
+ }, children: jsxRuntime.jsx("tbody", { children: jsxRuntime.jsx("tr", { children: jsxRuntime.jsx("td", { align: tdAlign, width: "100%", style: { width: "100%" }, children: jsxRuntime.jsx("table", Object.assign({ "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: jsxRuntime.jsx("tbody", { children: jsxRuntime.jsx("tr", { className: "content-tr", children: childrenArray.map((child, index) => (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx("td", { valign: tdValign, style: {
2667
2571
  verticalAlign: tdValign,
2668
2572
  textAlign: "left",
2669
2573
  padding: "0",
2670
2574
  margin: "0",
2671
2575
  }, className: "child-cell", children: child }), index < numChildren - 1 &&
2672
2576
  config.gap && (jsxRuntime.jsx("td", { width: config.gap, style: gapTdStyle, className: "row-gap-td", children: "\u00A0" }, `row-gap-${index}`))] }, `row-child-${index}`))) }) }) })) }) }) }) }) }) }) }) }) })) }) }), devNode && (jsxRuntime.jsx("tfoot", { children: jsxRuntime.jsx("tr", { children: jsxRuntime.jsx("td", { children: devNode }) }) }))] })));
2673
- // Wrap in anchor tag if innerLink is defined and NOT in dev mode
2674
2577
  if (href && !devMode) {
2675
2578
  return (jsxRuntime.jsx("a", Object.assign({ href: href }, (target && { target }), { style: {
2676
2579
  textDecoration: "none",
@@ -2772,7 +2675,7 @@ function Spacer({ config, devNode }) {
2772
2675
  var Spacer_default = React.memo(Spacer, arePropsEqual);
2773
2676
 
2774
2677
  function Text({ config, devMode, children }) {
2775
- const { text, padding, color, textAlign, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, backgroundColor, opacity, whiteSpace, wordBreak = "break-all", } = config;
2678
+ const { text, padding, color, textAlign, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, backgroundColor, opacity, whiteSpace, wordBreak = "break-all", maxWidth } = config;
2776
2679
  // 1. TD Style: Where padding and background are reliably applied.
2777
2680
  const tdStyle = {
2778
2681
  padding: padding,
@@ -2799,6 +2702,7 @@ function Text({ config, devMode, children }) {
2799
2702
  wordBreak: wordBreak,
2800
2703
  margin: "0",
2801
2704
  padding: "0",
2705
+ maxWidth
2802
2706
  };
2803
2707
  // Determine content to render
2804
2708
  const content = text !== null && text !== void 0 ? text : children;
@@ -3078,6 +2982,25 @@ function Icon({ config, devNode, devMode, children }) {
3078
2982
  }
3079
2983
  var Icon_default = React.memo(Icon, arePropsEqual);
3080
2984
 
2985
+ /**
2986
+ * Content rendered by Outlook Classic only.
2987
+ * Outputs: <!--[if mso]> ... <![endif]-->
2988
+ */
2989
+ function MsoOnly({ html }) {
2990
+ return (jsxRuntime.jsx("td", { dangerouslySetInnerHTML: {
2991
+ __html: `<!--[if mso]>${html}<![endif]-->`,
2992
+ } }));
2993
+ }
2994
+ /**
2995
+ * Content hidden from Outlook Classic, visible in all other clients.
2996
+ * Outputs: <!--[if !mso]><!--> ... <!--<![endif]-->
2997
+ */
2998
+ function NonMso({ html }) {
2999
+ return (jsxRuntime.jsx("td", { dangerouslySetInnerHTML: {
3000
+ __html: `<!--[if !mso]><!-->${html}<!--<![endif]-->`,
3001
+ } }));
3002
+ }
3003
+
3081
3004
  exports.Body = Body;
3082
3005
  exports.Button = Button_default;
3083
3006
  exports.Column = Column_default;