@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,179 @@
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 alignMap = {
5
+ start: "top",
6
+ center: "middle",
7
+ end: "bottom",
8
+ };
9
+ const justifyMap = {
10
+ start: "left",
11
+ center: "center",
12
+ end: "right",
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 Container({ children, config, devMode, devNode }) {
50
+ var _a, _b, _c;
51
+ const { widthType, childrenConstraints } = config;
52
+ const childrenArray = (Array.isArray(children) ? children : [children]).filter((child) => child != null);
53
+ const numChildren = childrenArray.length;
54
+ const containerWidthPx = (() => {
55
+ if (widthType === "fixed" && config.width && config.width.endsWith("px")) {
56
+ return parseInt(config.width, 10);
57
+ }
58
+ return 600;
59
+ })();
60
+ const gapWidthPx = (() => {
61
+ if (config.gap && config.gap.endsWith("px")) {
62
+ return parseInt(config.gap, 10);
63
+ }
64
+ return 0;
65
+ })();
66
+ const getChildWidths = (() => {
67
+ const { widthDistributionType } = childrenConstraints;
68
+ const totalGapSpace = gapWidthPx * (numChildren > 1 ? numChildren - 1 : 0);
69
+ const remainingContentSpace = containerWidthPx - totalGapSpace;
70
+ switch (widthDistributionType) {
71
+ case "equals":
72
+ const equalContentWidth = remainingContentSpace / numChildren;
73
+ return Array(numChildren).fill(`${equalContentWidth}px`);
74
+ case "ratio": {
75
+ const { ratio } = childrenConstraints;
76
+ const { mainChildIndex, value: [numerator, denominator], } = ratio;
77
+ if (numChildren < 2 ||
78
+ mainChildIndex < 0 ||
79
+ mainChildIndex >= numChildren ||
80
+ denominator === 0) {
81
+ const equalFallbackWidth = remainingContentSpace / numChildren;
82
+ return Array(numChildren).fill(`${equalFallbackWidth}px`);
83
+ }
84
+ const mainChildWidth = (remainingContentSpace * numerator) / denominator;
85
+ const remainingWidth = remainingContentSpace - mainChildWidth;
86
+ const numOtherChildren = numChildren - 1;
87
+ const otherChildWidth = numOtherChildren > 0 ? remainingWidth / numOtherChildren : 0;
88
+ const widths = Array(numChildren).fill(`${otherChildWidth}px`);
89
+ widths[mainChildIndex] = `${mainChildWidth}px`;
90
+ return widths;
91
+ }
92
+ case "manual": {
93
+ const { widths } = childrenConstraints;
94
+ return widths.length === numChildren ? widths : [];
95
+ }
96
+ default:
97
+ return [];
98
+ }
99
+ })();
100
+ const outerTableStyle = {
101
+ width: "100%",
102
+ borderCollapse: "collapse",
103
+ };
104
+ // 1. Background TD Style - Background color, border radius, background image
105
+ const backgroundTdStyle = {
106
+ backgroundColor: config.backgroundColor,
107
+ borderRadius: config.borderRadius,
108
+ maxWidth: widthType === "fixed" ? config.width || "600px" : undefined,
109
+ backgroundImage: config.backgroundImage
110
+ ? `url(${config.backgroundImage.src})`
111
+ : undefined,
112
+ backgroundRepeat: (_a = config.backgroundImage) === null || _a === void 0 ? void 0 : _a.repeat,
113
+ backgroundSize: (_b = config.backgroundImage) === null || _b === void 0 ? void 0 : _b.size,
114
+ backgroundPosition: (_c = config.backgroundImage) === null || _c === void 0 ? void 0 : _c.position,
115
+ // Overflow hidden to clip background to border-radius
116
+ ...(config.borderRadius && { overflow: "hidden" }),
117
+ };
118
+ // 2. Border Table Style - Border and border radius
119
+ const borderTableStyle = {
120
+ width: "100%",
121
+ borderCollapse: "separate",
122
+ borderSpacing: 0,
123
+ borderRadius: config.borderRadius,
124
+ ...getBorderStyle(config.border),
125
+ };
126
+ // 3. Padding TD Style
127
+ const innerTdStyle = {
128
+ padding: config.padding,
129
+ width: "100%",
130
+ verticalAlign: config.alignItems ? alignMap[config.alignItems] : "top",
131
+ };
132
+ const contentTableStyle = {
133
+ width: "100%",
134
+ height: config.height,
135
+ borderCollapse: "collapse",
136
+ };
137
+ const gapTdStyle = {
138
+ width: config.gap || "0",
139
+ lineHeight: "1px",
140
+ fontSize: "1px",
141
+ };
142
+ const justifyAlign = config.justifyContent
143
+ ? justifyMap[config.justifyContent]
144
+ : "center";
145
+ const containerWidthAttr = widthType === "fixed" ? containerWidthPx : undefined;
146
+ const isStacking = config.shouldWrap && numChildren > 1;
147
+ const msoFixedWrapper = "";
148
+ const msoFixedFooter = "";
149
+ const rowElements = childrenArray.map((child, index) => {
150
+ const childTdStyle = {
151
+ width: getChildWidths[index],
152
+ verticalAlign: config.alignItems ? alignMap[config.alignItems] : "top",
153
+ textAlign: "left",
154
+ };
155
+ if (config.gap && index < numChildren - 1) {
156
+ 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: {
157
+ display: "none",
158
+ fontSize: "0",
159
+ lineHeight: "0",
160
+ height: config.gap,
161
+ }, children: "\u00A0" }))] }, `child-${index}`), _jsx("td", { className: isStacking ? "desktop-gap-column" : undefined, width: config.gap, style: gapTdStyle, children: "\u00A0" }, `gap-${index}`)] }, `ctn:${index}`));
162
+ }
163
+ return (_jsx("td", { className: isStacking ? "stack-td" : undefined, width: getChildWidths[index], style: childTdStyle, children: child }, `child-${index}`));
164
+ });
165
+ return (_jsx("table", { "aria-label": `Container | Table Outer`, cellPadding: 0, cellSpacing: 0, role: "presentation", border: 0, style: {
166
+ position: "relative",
167
+ ...outerTableStyle,
168
+ }, children: _jsx("tbody", { children: _jsx("tr", { children: _jsxs("td", { align: justifyAlign, children: [_jsx("div", { dangerouslySetInnerHTML: { __html: msoFixedWrapper } }), _jsxs("table", { className: [
169
+ widthType === "fixed" ? "container-fixed-width" : undefined,
170
+ devMode ? "main-wrapper relative" : undefined,
171
+ ]
172
+ .filter(Boolean)
173
+ .join(" "), "aria-label": `Container | Table Middle`, cellPadding: 0, cellSpacing: 0, role: "presentation", border: 0, align: justifyAlign, style: {
174
+ width: "100%",
175
+ maxWidth: widthType === "fixed" ? config.width || "600px" : undefined,
176
+ borderCollapse: "collapse",
177
+ }, 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 } })] }) }) }) }));
178
+ }
179
+ export default memo(Container, arePropsEqual);
@@ -0,0 +1,41 @@
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 Divider({ config, devNode }) {
5
+ const { height = "1px", color = "#cccccc", width = "100%", margin = "20px 0", align = "center", hideOnMobile, } = config;
6
+ // 1. Outer TD Style: Applies the vertical spacing (margin)
7
+ const outerTdStyle = {
8
+ padding: margin,
9
+ fontSize: "0",
10
+ lineHeight: "0",
11
+ width: "100%",
12
+ };
13
+ // 2. Divider Table Style: Applies the line properties
14
+ const dividerTableStyle = {
15
+ width: width,
16
+ height: height,
17
+ backgroundColor: color,
18
+ borderCollapse: "collapse",
19
+ border: "0",
20
+ // ✅ FIX 1: Use string literal indexing for MSO properties
21
+ // ["mso-table-lspace" as string]: "0pt",
22
+ ["msoTableLspace"]: "0pt",
23
+ // ["mso-table-rspace" as string]: "0pt",
24
+ ["msoTableRspace"]: "0pt",
25
+ };
26
+ // Parse height for the HTML attribute
27
+ const dividerHeightAttribute = parseInt(height, 10) || 1;
28
+ return (_jsxs("table", { "aria-label": "Divider Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
29
+ // --- Start dev
30
+ position: "relative",
31
+ // --- End dev
32
+ width: "100%",
33
+ borderCollapse: "collapse",
34
+ }, className: hideOnMobile ? "hide-on-mobile" : undefined, children: [_jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { style: outerTdStyle, align: align, children: _jsx("table", { "aria-label": "Divider Line", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, align: align, style: dividerTableStyle, ...{ height: dividerHeightAttribute }, children: _jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { style: {
35
+ height: height,
36
+ fontSize: "0",
37
+ lineHeight: "0",
38
+ padding: "0",
39
+ }, children: "\u00A0" }) }) }) }) }) }) }), devNode && (_jsx("tfoot", { children: _jsx("tr", { children: _jsx("td", { children: devNode }) }) }))] }));
40
+ }
41
+ export default memo(Divider, arePropsEqual);
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // ── Helpers ───────────────────────────────────────────────────────────────────
3
+ function normaliseFallbacks(fallbackFontFamily) {
4
+ if (!fallbackFontFamily)
5
+ return [];
6
+ return Array.isArray(fallbackFontFamily)
7
+ ? fallbackFontFamily
8
+ : [fallbackFontFamily];
9
+ }
10
+ /**
11
+ * Build the CSS font-stack string from the primary family + fallbacks.
12
+ * Wraps names that contain spaces in single quotes.
13
+ */
14
+ function buildFontStack(primary, fallbacks) {
15
+ const quote = (name) => (name.includes(" ") ? `'${name}'` : name);
16
+ return [primary, ...fallbacks].map(quote).join(", ");
17
+ }
18
+ // ── Component ─────────────────────────────────────────────────────────────────
19
+ export default function Font({ fontFamily, fallbackFontFamily, webFont, fontWeight = 400, fontStyle = "normal", }) {
20
+ var _a;
21
+ const fallbacks = normaliseFallbacks(fallbackFontFamily);
22
+ const fontFaceCss = webFont
23
+ ? `
24
+ @font-face {
25
+ font-family: '${fontFamily}';
26
+ font-style: ${fontStyle};
27
+ font-weight: ${fontWeight};
28
+ font-display: swap;
29
+ src: url('${webFont.url}') format('${webFont.format}');
30
+ mso-font-alt: '${(_a = fallbacks[0]) !== null && _a !== void 0 ? _a : "sans-serif"}';
31
+ }`.trim()
32
+ : null;
33
+ const msoComment = fallbacks.length > 0
34
+ ? `<!--[if mso]>\n<style type="text/css">\n .${cssClassName(fontFamily)} {\n font-family: ${buildFontStack(fallbacks[0], fallbacks.slice(1))}, sans-serif !important;\n }\n</style>\n<![endif]-->`
35
+ : null;
36
+ return (_jsxs(_Fragment, { children: [fontFaceCss && (_jsx("style", { type: "text/css", dangerouslySetInnerHTML: { __html: fontFaceCss } })), msoComment && (_jsx("style", { type: "text/css", dangerouslySetInnerHTML: {
37
+ __html: `</style>${msoComment}<style type="text/css">`,
38
+ } }))] }));
39
+ }
40
+ // ── Utility: generate a stable CSS class name from a font family name ─────────
41
+ // Used in the MSO conditional comment to scope the fallback rule.
42
+ export function cssClassName(fontFamily) {
43
+ return `font-${fontFamily.toLowerCase().replace(/\s+/g, "-")}`;
44
+ }
@@ -0,0 +1,134 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Font from "./Font";
3
+ export default function Head({ children, backgroundColor = "#ffffff", title = "Email Preview", rowGaps = [], fonts = [], }) {
4
+ const msoResetStyles = `
5
+ .ExternalClass { width: 100%; line-height: 100%; }
6
+ .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; }
7
+ table { mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0; }
8
+ td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
9
+ img { border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; }
10
+ #MessageViewBody img { min-width: 100%; }
11
+ a[x-apple-data-detectors] {
12
+ color: inherit !important;
13
+ text-decoration: none !important;
14
+ font-size: inherit !important;
15
+ font-family: inherit !important;
16
+ font-weight: inherit !important;
17
+ line-height: inherit !important;
18
+ }
19
+ body { background-color: ${backgroundColor} !important; }
20
+ p { margin: 0; }
21
+ `;
22
+ const globalStyles = `
23
+ @media screen and (max-width: 768px) {
24
+ .container-fixed-width {
25
+ width: 100% !important;
26
+ max-width: 100% !important;
27
+ }
28
+ }
29
+ @media screen and (max-width: 768px) {
30
+ .hide-on-mobile {
31
+ display: none !important;
32
+ max-height: 0 !important;
33
+ overflow: hidden !important;
34
+ mso-hide: all;
35
+ }
36
+ }
37
+ @media screen and (min-width: 769px) {
38
+ .hide-on-desktop {
39
+ display: none !important;
40
+ max-height: 0 !important;
41
+ overflow: hidden !important;
42
+ mso-hide: all;
43
+ }
44
+ }
45
+ @media screen and (max-width: 768px) {
46
+ .stack-td {
47
+ width: 100% !important;
48
+ display: block !important;
49
+ float: left;
50
+ clear: both;
51
+ padding-left: 0 !important;
52
+ padding-right: 0 !important;
53
+ }
54
+ .desktop-gap-column { width: 0 !important; display: none !important; }
55
+ .mobile-gap-spacer {
56
+ display: block !important;
57
+ width: 100% !important;
58
+ font-size: 1px !important;
59
+ line-height: 1px !important;
60
+ mso-line-height-rule: exactly;
61
+ }
62
+ }
63
+ @media only screen and (max-width: 768px) {
64
+ .row-content-table[data-mobile-justify="center"] { margin: 0 auto !important; float: none !important; }
65
+ .row-content-table[data-mobile-justify="start"] { margin: 0 !important; float: left !important; }
66
+ .row-content-table[data-mobile-justify="end"] { margin: 0 0 0 auto !important; float: right !important; }
67
+ .row-content-table[data-mobile-align="center"] .child-cell { vertical-align: middle !important; }
68
+ .row-content-table[data-mobile-align="start"] .child-cell { vertical-align: top !important; }
69
+ .row-content-table[data-mobile-align="end"] .child-cell { vertical-align: bottom !important; }
70
+ .row-content-table[data-mobile-wrap="true"] { width: 100% !important; max-width: 100% !important; }
71
+ .row-content-table[data-mobile-wrap="true"] > tbody > .content-tr { display: block !important; }
72
+ .row-content-table[data-mobile-wrap="true"] > tbody > .content-tr > .child-cell {
73
+ display: block !important;
74
+ width: 100% !important;
75
+ box-sizing: border-box !important;
76
+ }
77
+ .row-content-table[data-mobile-wrap="true"] > tbody > .content-tr > .row-gap-td {
78
+ display: none !important;
79
+ width: 0 !important;
80
+ height: 0 !important;
81
+ }
82
+ .row-content-table[data-mobile-wrap="true"] > tbody > .content-tr > .child-cell:not(:last-child) {
83
+ margin-bottom: 20px !important;
84
+ }
85
+ ${["10px", "15px", "20px", "24px", "30px", "40px", ...rowGaps]
86
+ .filter((gap, index, self) => self.indexOf(gap) === index)
87
+ .map((gap) => `
88
+ .row-content-table[data-mobile-wrap="true"][data-gap="${gap}"] > tbody > .content-tr > .child-cell:not(:last-child) {
89
+ margin-bottom: ${gap} !important;
90
+ }`)
91
+ .join("\n")}
92
+ }
93
+ a { color: inherit; text-decoration: none; }
94
+ ol, ul { margin: 0px; padding: 0px; list-style: none; }
95
+ li {
96
+ list-style-type: none !important;
97
+ list-style: none !important;
98
+ position: relative;
99
+ padding-left: 0px;
100
+ margin: 0px;
101
+ display: block !important;
102
+ }
103
+ li::marker {
104
+ content: "" !important;
105
+ font-size: 0px !important;
106
+ line-height: 0px !important;
107
+ color: transparent !important;
108
+ width: 0px !important;
109
+ }
110
+ li[data-list="bullet"] {
111
+ list-style-type: disc !important;
112
+ list-style-position: inside !important;
113
+ padding-left: 1.5em;
114
+ display: list-item !important;
115
+ }
116
+ li[data-list="ordered"] {
117
+ list-style-type: decimal !important;
118
+ list-style-position: inside !important;
119
+ padding-left: 1.5em;
120
+ display: list-item !important;
121
+ }
122
+ li[data-list="bullet"]::marker,
123
+ li[data-list="ordered"]::marker {
124
+ content: normal !important;
125
+ font-size: inherit !important;
126
+ color: inherit !important;
127
+ width: auto !important;
128
+ padding: 0 !important;
129
+ margin: 0 !important;
130
+ }
131
+ h1, h2, h3, h4, h5, h6 { margin: 0; padding: 0; font-weight: inherit; }
132
+ `;
133
+ return (_jsxs("head", { children: [_jsx("meta", { httpEquiv: "Content-Type", content: "text/html; charset=utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("meta", { httpEquiv: "X-UA-Compatible", content: "IE=edge" }), _jsx("title", { children: title }), fonts.flatMap((resolved) => resolved.fontProps.map((props, i) => (_jsx(Font, { ...props }, `${resolved.family}-${props.fontWeight}-${props.fontStyle}-${i}`)))), children, _jsx("style", { type: "text/css", dangerouslySetInnerHTML: { __html: msoResetStyles } }), _jsx("style", { type: "text/css", dangerouslySetInnerHTML: { __html: globalStyles } })] }));
134
+ }