@pagenflow/email 1.4.2 → 1.4.3

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.
@@ -0,0 +1,119 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { memo } from "react";
3
+ import { arePropsEqual } from "../utils/memoUtils";
4
+ function getBorderStyle(border) {
5
+ if (!border)
6
+ return {};
7
+ const style = {};
8
+ if (border.width && border.style && border.color) {
9
+ style.border = `${border.width} ${border.style} ${border.color}`;
10
+ }
11
+ if (border.top)
12
+ style.borderTop = `${border.top.width} ${border.top.style} ${border.top.color}`;
13
+ if (border.right)
14
+ style.borderRight = `${border.right.width} ${border.right.style} ${border.right.color}`;
15
+ if (border.bottom)
16
+ style.borderBottom = `${border.bottom.width} ${border.bottom.style} ${border.bottom.color}`;
17
+ if (border.left)
18
+ style.borderLeft = `${border.left.width} ${border.left.style} ${border.left.color}`;
19
+ return style;
20
+ }
21
+ function getBorderStyleString(border) {
22
+ if (!border)
23
+ return "";
24
+ const styles = [];
25
+ // Standard shorthand
26
+ if (border.width && border.style && border.color) {
27
+ styles.push(`border:${border.width} ${border.style} ${border.color} !important;`);
28
+ }
29
+ else {
30
+ // If desktop had a border and mobile wants "none", we must explicitly kill it
31
+ styles.push(`border: none !important;`);
32
+ }
33
+ // Individual sides
34
+ if (border.top)
35
+ styles.push(`border-top:${border.top.width} ${border.top.style} ${border.top.color} !important;`);
36
+ if (border.right)
37
+ styles.push(`border-right:${border.right.width} ${border.right.style} ${border.right.color} !important;`);
38
+ if (border.bottom)
39
+ styles.push(`border-bottom:${border.bottom.width} ${border.bottom.style} ${border.bottom.color} !important;`);
40
+ if (border.left)
41
+ styles.push(`border-left:${border.left.width} ${border.left.style} ${border.left.color} !important;`);
42
+ return styles.join(" ");
43
+ }
44
+ function Image({ config, devNode, devMode }) {
45
+ var _a;
46
+ const { src, alt, href, target, mobile } = config;
47
+ const seed = src + (alt || "");
48
+ const instanceId = seed
49
+ .split("")
50
+ .reduce((acc, char) => acc + char.charCodeAt(0), 0)
51
+ .toString(36);
52
+ const imgClass = `img-${instanceId}`;
53
+ // 1. Desktop Dimensional Logic
54
+ const desktopWidth = config.width || "100%";
55
+ const isPercent = desktopWidth.includes("%");
56
+ const widthAttr = desktopWidth.replace("px", "");
57
+ const heightAttr = (_a = config.height) === null || _a === void 0 ? void 0 : _a.replace("px", "");
58
+ // Determine the table's "initial" width.
59
+ // If it's 300px, the table should be 300px, not 100%.
60
+ const tableWidth = isPercent ? desktopWidth : `${widthAttr}px`;
61
+ // 2. Mobile Overrides (Every property used)
62
+ let mobileCss = "";
63
+ if (mobile) {
64
+ mobileCss = `
65
+ @media screen and (max-width: 768px) {
66
+ .wrap-${imgClass} {
67
+ /* This breaks the px lock from desktop and makes it fluid */
68
+ width: ${mobile.width || "100%"} !important;
69
+ max-width: ${mobile.maxWidth || "100%"} !important;
70
+ min-width: 0 !important;
71
+ }
72
+ .td-${imgClass} {
73
+ padding: ${mobile.padding || "0"} !important;
74
+ background-color: ${mobile.backgroundColor || "transparent"} !important;
75
+ width: 100% !important;
76
+ }
77
+ .${imgClass} {
78
+ width: ${mobile.width || "100%"} !important;
79
+ height: ${mobile.height || "auto"} !important;
80
+ max-width: ${mobile.maxWidth || "100%"} !important;
81
+ max-height: ${mobile.maxHeight || "none"} !important;
82
+ border-radius: ${mobile.borderRadius || "0"} !important;
83
+ display: ${mobile.hidden ? "none" : "block"} !important;
84
+ object-fit: ${mobile.objectFit || "fill"} !important;
85
+ object-position: ${mobile.objectPosition || "center"} !important;
86
+ ${getBorderStyleString(mobile.border)}
87
+ }
88
+ }
89
+ `;
90
+ }
91
+ const imgStyle = {
92
+ display: "block",
93
+ width: isPercent ? "100%" : desktopWidth,
94
+ height: config.height || "auto",
95
+ maxWidth: config.maxWidth || "100%",
96
+ maxHeight: config.maxHeight || "none",
97
+ borderRadius: config.borderRadius || "0",
98
+ ...getBorderStyle(config.border),
99
+ outline: "none",
100
+ textDecoration: "none",
101
+ objectFit: config.objectFit,
102
+ objectPosition: config.objectPosition,
103
+ };
104
+ const imageElement = (_jsx("img", { src: src, alt: alt, width: !isPercent ? widthAttr : undefined, height: heightAttr !== "auto" ? heightAttr : undefined, className: imgClass, style: imgStyle }));
105
+ return (_jsxs(_Fragment, { 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
106
+ , style: {
107
+ width: tableWidth, // Fixed px here prevents the 100% "ghost space"
108
+ maxWidth: "100%",
109
+ borderCollapse: "collapse",
110
+ margin: "0 auto",
111
+ }, children: [_jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { className: `td-${imgClass}`, align: "center", style: {
112
+ padding: config.padding,
113
+ backgroundColor: config.backgroundColor,
114
+ fontSize: "0",
115
+ lineHeight: "0",
116
+ width: tableWidth, // Lock the cell as well
117
+ }, children: href && !devMode ? (_jsx("a", { href: href, target: target, style: { display: "block", width: "100%" }, children: imageElement })) : (imageElement) }) }) }), devMode && !!devNode && (_jsx("tfoot", { children: _jsx("tr", { children: _jsx("td", { children: devNode }) }) }))] })] }));
118
+ }
119
+ export default memo(Image, arePropsEqual);
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Content rendered by Outlook Classic only.
4
+ * Outputs: <!--[if mso]> ... <![endif]-->
5
+ */
6
+ export function MsoOnly({ html }) {
7
+ return (_jsx("td", { dangerouslySetInnerHTML: {
8
+ __html: `<!--[if mso]>${html}<![endif]-->`,
9
+ } }));
10
+ }
11
+ /**
12
+ * Content hidden from Outlook Classic, visible in all other clients.
13
+ * Outputs: <!--[if !mso]><!--> ... <!--<![endif]-->
14
+ */
15
+ export function NonMso({ html }) {
16
+ return (_jsx("td", { dangerouslySetInnerHTML: {
17
+ __html: `<!--[if !mso]><!-->${html}<!--<![endif]-->`,
18
+ } }));
19
+ }
@@ -0,0 +1,157 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment, memo } from "react";
3
+ import { arePropsEqual } from "../utils/memoUtils";
4
+ const justifyMap = {
5
+ start: "left",
6
+ center: "center",
7
+ end: "right",
8
+ };
9
+ const alignMap = {
10
+ start: "top",
11
+ center: "middle",
12
+ end: "bottom",
13
+ };
14
+ function getBorderStyle(border) {
15
+ if (!border)
16
+ return {};
17
+ const style = {};
18
+ // If a full border is specified, apply it
19
+ if (border.width && border.style && border.color) {
20
+ style.border = `${border.width} ${border.style} ${border.color}`;
21
+ }
22
+ else {
23
+ // If only individual borders are specified, explicitly set others to 'none'
24
+ // to prevent Outlook Classic from showing black borders
25
+ const hasIndividualBorders = border.top || border.right || border.bottom || border.left;
26
+ if (hasIndividualBorders) {
27
+ // Default all borders to none
28
+ style.borderTop = "none";
29
+ style.borderRight = "none";
30
+ style.borderBottom = "none";
31
+ style.borderLeft = "none";
32
+ }
33
+ }
34
+ // Override with specific borders if provided
35
+ if (border.top) {
36
+ style.borderTop = `${border.top.width} ${border.top.style} ${border.top.color}`;
37
+ }
38
+ if (border.right) {
39
+ style.borderRight = `${border.right.width} ${border.right.style} ${border.right.color}`;
40
+ }
41
+ if (border.bottom) {
42
+ style.borderBottom = `${border.bottom.width} ${border.bottom.style} ${border.bottom.color}`;
43
+ }
44
+ if (border.left) {
45
+ style.borderLeft = `${border.left.width} ${border.left.style} ${border.left.color}`;
46
+ }
47
+ return style;
48
+ }
49
+ function getHrefFromInnerLink(innerLink) {
50
+ if (!innerLink || innerLink.type === "none")
51
+ return undefined;
52
+ switch (innerLink.type) {
53
+ case "url":
54
+ return innerLink.url;
55
+ case "email":
56
+ return innerLink.email ? `mailto:${innerLink.email}` : undefined;
57
+ case "phone":
58
+ return innerLink.phone ? `tel:${innerLink.phone}` : undefined;
59
+ case "anchor":
60
+ return innerLink.anchor ? `#${innerLink.anchor}` : undefined;
61
+ case "page_top":
62
+ return "#";
63
+ case "page_bottom":
64
+ return "#bottom";
65
+ default:
66
+ return undefined;
67
+ }
68
+ }
69
+ function Row({ children, config, devNode, devMode }) {
70
+ var _a, _b, _c, _d, _e, _f, _g;
71
+ const childrenArray = (Array.isArray(children) ? children : [children]).filter((child) => child != null);
72
+ const numChildren = childrenArray.length;
73
+ const href = getHrefFromInnerLink(config.innerLink);
74
+ const target = (_a = config.innerLink) === null || _a === void 0 ? void 0 : _a.target;
75
+ // 1. Outer TD: Background, Border Radius, Width, Height.
76
+ const backgroundTdStyle = {
77
+ backgroundColor: config.backgroundColor,
78
+ borderRadius: config.borderRadius,
79
+ width: config.width || "100%",
80
+ height: config.height,
81
+ backgroundImage: config.backgroundImage
82
+ ? `url(${config.backgroundImage.src})`
83
+ : undefined,
84
+ backgroundRepeat: (_b = config.backgroundImage) === null || _b === void 0 ? void 0 : _b.repeat,
85
+ backgroundSize: (_c = config.backgroundImage) === null || _c === void 0 ? void 0 : _c.size,
86
+ backgroundPosition: (_d = config.backgroundImage) === null || _d === void 0 ? void 0 : _d.position,
87
+ ...(config.borderRadius && { overflow: "hidden" }),
88
+ };
89
+ // 2. Inner Table: Border and Border Radius.
90
+ const borderTableStyle = {
91
+ width: "100%",
92
+ height: "100%",
93
+ borderCollapse: "separate",
94
+ borderSpacing: 0,
95
+ borderRadius: config.borderRadius,
96
+ ...getBorderStyle(config.border),
97
+ };
98
+ // 3. Padding TD.
99
+ const paddingTdStyle = {
100
+ padding: config.padding,
101
+ width: "100%",
102
+ verticalAlign: "top",
103
+ };
104
+ // 4. Content Table.
105
+ //
106
+ // fillWidth: false/undefined (default) → width: "auto"
107
+ // Original behavior. Children shrink-wrap to their natural sizes.
108
+ // Use for icon rows, button rows, social link rows.
109
+ // Centering works via the Justification Wrapper TD (align + width="100%").
110
+ //
111
+ // fillWidth: true → width: "100%"
112
+ // Content table fills available space, giving Outlook Classic a hard
113
+ // boundary so text children get a constrained box and line wrapping
114
+ // triggers correctly. Use for rows containing text + image layouts.
115
+ const contentTableStyle = {
116
+ width: config.fillWidth ? "100%" : "auto",
117
+ height: "100%",
118
+ borderCollapse: "collapse",
119
+ minWidth: "1px",
120
+ ...(!config.fillWidth && { maxWidth: config.width || "100%" }),
121
+ };
122
+ // 5. Gap TD.
123
+ const gapTdStyle = {
124
+ width: config.gap || "0",
125
+ lineHeight: "1px",
126
+ fontSize: "1px",
127
+ };
128
+ const tdAlign = config.justifyContent
129
+ ? justifyMap[config.justifyContent]
130
+ : "left";
131
+ const tdValign = config.alignItems ? alignMap[config.alignItems] : "top";
132
+ const content = (_jsxs("table", { "aria-label": "Row Outer", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
133
+ position: "relative",
134
+ width: config.width || "100%",
135
+ height: config.height,
136
+ borderCollapse: "collapse",
137
+ }, ...(config.height && { height: config.height }), children: [_jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { style: backgroundTdStyle, ...(config.height && { height: config.height }), children: _jsx("table", { "aria-label": "Row Border Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: borderTableStyle, children: _jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { style: paddingTdStyle, children: _jsx("table", { "aria-label": "Row Justification Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
138
+ width: "100%",
139
+ height: "100%",
140
+ borderCollapse: "collapse",
141
+ }, 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: {
142
+ verticalAlign: tdValign,
143
+ textAlign: "left",
144
+ padding: "0",
145
+ margin: "0",
146
+ }, className: "child-cell", children: child }), index < numChildren - 1 &&
147
+ 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 }) }) }))] }));
148
+ if (href && !devMode) {
149
+ return (_jsx("a", { href: href, ...(target && { target }), style: {
150
+ textDecoration: "none",
151
+ color: "inherit",
152
+ display: "block",
153
+ }, children: content }));
154
+ }
155
+ return content;
156
+ }
157
+ export default memo(Row, arePropsEqual);
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { memo } from "react";
3
+ import { arePropsEqual } from "../utils/memoUtils";
4
+ function getBorderStyle(border) {
5
+ if (!border)
6
+ return {};
7
+ const style = {};
8
+ // If a full border is specified, apply it
9
+ if (border.width && border.style && border.color) {
10
+ style.border = `${border.width} ${border.style} ${border.color}`;
11
+ }
12
+ else {
13
+ // If only individual borders are specified, explicitly set others to 'none'
14
+ // to prevent Outlook Classic from showing black borders
15
+ const hasIndividualBorders = border.top || border.right || border.bottom || border.left;
16
+ if (hasIndividualBorders) {
17
+ // Default all borders to none
18
+ style.borderTop = "none";
19
+ style.borderRight = "none";
20
+ style.borderBottom = "none";
21
+ style.borderLeft = "none";
22
+ }
23
+ }
24
+ // Override with specific borders if provided
25
+ if (border.top) {
26
+ style.borderTop = `${border.top.width} ${border.top.style} ${border.top.color}`;
27
+ }
28
+ if (border.right) {
29
+ style.borderRight = `${border.right.width} ${border.right.style} ${border.right.color}`;
30
+ }
31
+ if (border.bottom) {
32
+ style.borderBottom = `${border.bottom.width} ${border.bottom.style} ${border.bottom.color}`;
33
+ }
34
+ if (border.left) {
35
+ style.borderLeft = `${border.left.width} ${border.left.style} ${border.left.color}`;
36
+ }
37
+ return style;
38
+ }
39
+ const Section = ({ config, children, devNode, }) => {
40
+ var _a, _b, _c;
41
+ const { sectionType, padding } = config;
42
+ return (_jsxs("table", { "aria-label": `Section |Table | ${sectionType}`, role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
43
+ position: "relative",
44
+ width: "100%",
45
+ backgroundColor: config.backgroundColor,
46
+ ...getBorderStyle(config.border),
47
+ backgroundImage: config.backgroundImage
48
+ ? `url(${config.backgroundImage.src})`
49
+ : undefined,
50
+ backgroundRepeat: (_a = config.backgroundImage) === null || _a === void 0 ? void 0 : _a.repeat,
51
+ backgroundSize: (_b = config.backgroundImage) === null || _b === void 0 ? void 0 : _b.size,
52
+ backgroundPosition: (_c = config.backgroundImage) === null || _c === void 0 ? void 0 : _c.position,
53
+ }, children: [_jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { style: {
54
+ padding: padding,
55
+ }, children: children }) }) }), devNode && (_jsx("tfoot", { children: _jsx("tr", { children: _jsxs("td", { children: [_jsxs("span", { style: {
56
+ backgroundColor: "black",
57
+ color: "white",
58
+ padding: "4px",
59
+ fontSize: "14px",
60
+ position: "absolute",
61
+ left: 0,
62
+ top: 0,
63
+ }, children: ["Section | ", sectionType] }), children] }) }) }))] }));
64
+ };
65
+ export default memo(Section, arePropsEqual);
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { memo } from "react";
3
+ import { arePropsEqual } from "../utils/memoUtils";
4
+ function Spacer({ config, devNode }) {
5
+ const { height, hideOnMobile } = config;
6
+ // 1. Spacer Table Style
7
+ const spacerTableStyle = {
8
+ // Crucial for compatibility: Ensures no background or border interference
9
+ backgroundColor: "transparent",
10
+ borderCollapse: "collapse",
11
+ border: "0",
12
+ width: "100%",
13
+ // Note the CSS standard dash convention: 'mso-table-lspace'
14
+ // ["mso-table-lspace" as string]: "0pt",
15
+ ["msoTableLspace"]: "0pt",
16
+ // ["mso-table-rspace" as string]: "0pt",
17
+ ["msoTableRspace"]: "0pt",
18
+ };
19
+ // 2. Spacer TD Style: The element that creates the actual vertical space
20
+ const spacerTdStyle = {
21
+ height: height,
22
+ // Critical: Suppress any vertical height created by text/font
23
+ fontSize: "0",
24
+ lineHeight: "0",
25
+ padding: "0",
26
+ };
27
+ // Parse height for the HTML attribute
28
+ const spacerHeightAttribute = parseInt(height, 10) || 1;
29
+ return (
30
+ // Outer table ensures the spacer spans the full width of its container
31
+ _jsxs("table", { "aria-label": "Vertical Spacer", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
32
+ // --- Start dev
33
+ position: "relative",
34
+ // --- End dev
35
+ ...spacerTableStyle,
36
+ }, ...{ height: spacerHeightAttribute }, className: hideOnMobile ? "hide-on-mobile" : undefined, children: [_jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { style: spacerTdStyle,
37
+ // Explicit height attribute
38
+ height: spacerHeightAttribute, children: "\u00A0" }) }) }), devNode && (_jsx("tfoot", { children: _jsx("tr", { children: _jsx("td", { children: devNode }) }) }))] }));
39
+ }
40
+ export default memo(Spacer, arePropsEqual);
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { memo } from "react";
3
+ import { arePropsEqual } from "../utils/memoUtils";
4
+ function Text({ config, devMode, children }) {
5
+ const { text, padding, color, textAlign, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, backgroundColor, opacity, whiteSpace, wordBreak = "break-all", maxWidth } = config;
6
+ // 1. TD Style: Where padding and background are reliably applied.
7
+ const tdStyle = {
8
+ padding: padding,
9
+ backgroundColor: backgroundColor,
10
+ width: "100%",
11
+ verticalAlign: "top",
12
+ };
13
+ // 2. Content Style: Applied directly to a wrapper element
14
+ const contentStyle = {
15
+ color: color,
16
+ textAlign: textAlign,
17
+ fontFamily: fontFamily || "Arial, Helvetica, sans-serif",
18
+ fontSize: fontSize,
19
+ fontWeight: fontWeight,
20
+ fontStyle: fontStyle,
21
+ lineHeight: lineHeight,
22
+ letterSpacing: letterSpacing,
23
+ textTransform: textTransform,
24
+ textDecoration: textDecoration,
25
+ direction: direction,
26
+ verticalAlign: verticalAlign,
27
+ opacity: opacity,
28
+ whiteSpace: whiteSpace,
29
+ wordBreak: wordBreak,
30
+ margin: "0",
31
+ padding: "0",
32
+ maxWidth
33
+ };
34
+ // Determine content to render
35
+ const content = text !== null && text !== void 0 ? text : children;
36
+ const isString = typeof content === "string";
37
+ return (_jsx("table", { "aria-label": "Text Block Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
38
+ width: "100%",
39
+ borderCollapse: "collapse",
40
+ }, children: _jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { style: tdStyle, align: textAlign, children: isString ? (_jsx("div", { style: contentStyle, dangerouslySetInnerHTML: { __html: content } })) : (_jsx("div", { style: contentStyle, children: content })) }) }) }) }));
41
+ }
42
+ export default memo(Text, arePropsEqual);