@pagenflow/email 1.4.4 → 1.4.6

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.
Files changed (39) hide show
  1. package/dist/components/Button.d.ts +4 -0
  2. package/dist/components/Column.d.ts +1 -1
  3. package/dist/components/Container.d.ts +1 -1
  4. package/dist/components/Image.d.ts +8 -0
  5. package/dist/components/Row.d.ts +1 -1
  6. package/dist/components/Section.d.ts +1 -1
  7. package/dist/components/Text.d.ts +2 -1
  8. package/dist/components/utils/MiniDomParser.d.ts +23 -0
  9. package/dist/components/utils/injectLinkStyles.d.ts +1 -0
  10. package/dist/index.cjs.js +177 -132
  11. package/dist/index.cjs.js.map +1 -1
  12. package/dist/index.esm.js +177 -132
  13. package/dist/index.esm.js.map +1 -1
  14. package/dist/types/IInnerLink.d.ts +17 -0
  15. package/package.json +1 -1
  16. package/dist/components/Body.js +0 -57
  17. package/dist/components/BodyDev.js +0 -57
  18. package/dist/components/Button.js +0 -327
  19. package/dist/components/Column.js +0 -127
  20. package/dist/components/Container.js +0 -179
  21. package/dist/components/Divider.js +0 -41
  22. package/dist/components/Font.js +0 -44
  23. package/dist/components/Head.js +0 -134
  24. package/dist/components/HeadDev.js +0 -311
  25. package/dist/components/Heading.js +0 -46
  26. package/dist/components/Html.js +0 -20
  27. package/dist/components/Icon.js +0 -276
  28. package/dist/components/Image.js +0 -119
  29. package/dist/components/MsoConditional.js +0 -19
  30. package/dist/components/Row.js +0 -157
  31. package/dist/components/Section.js +0 -65
  32. package/dist/components/Spacer.js +0 -40
  33. package/dist/components/Text.js +0 -42
  34. package/dist/index.js +0 -17
  35. package/dist/types/IInnerLink.js +0 -1
  36. package/dist/types/ResolvedFont.js +0 -1
  37. package/dist/types/index.js +0 -1
  38. package/dist/utils/isEqual.js +0 -1439
  39. package/dist/utils/memoUtils.js +0 -55
@@ -1,311 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { Fragment, useEffect } from "react";
4
- /**
5
- * Dev variant of Head component for use in builder canvas.
6
- * Injects styles directly into document head to maintain email behavior during development.
7
- *
8
- * All styles — including @font-face declarations — are injected imperatively into
9
- * document.head via useEffect. This guarantees correct placement (always in <head>,
10
- * never in <body>) and correct declaration order (fonts before selectors).
11
- */
12
- export default function HeadDev({ children, backgroundColor = "#ffffff", title = "Email Preview", rowGaps = [], fonts = [], }) {
13
- // ── 1. MSO reset + global styles ─────────────────────────────────────────
14
- useEffect(() => {
15
- document.title = title;
16
- const msoResetStyles = `
17
- /* Forces Outlook to render 100% width and prevents line-height issues */
18
- .builder-canvas .ExternalClass { width: 100%; line-height: 100%; }
19
- .builder-canvas .ExternalClass p,
20
- .builder-canvas .ExternalClass span,
21
- .builder-canvas .ExternalClass font,
22
- .builder-canvas .ExternalClass td,
23
- .builder-canvas .ExternalClass div { line-height: 100%; }
24
-
25
- /* Reset tables for MSO and border issues */
26
- .builder-canvas table { mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0; }
27
- .builder-canvas td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
28
-
29
- /* Reset images */
30
- .builder-canvas img { border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; }
31
-
32
- /* Fix for Gmail image wrapping and blue links */
33
- .builder-canvas #MessageViewBody img { min-width: 100%; }
34
-
35
- /* Apple data-detector reset for blue links (Scoped to canvas) */
36
- .builder-canvas a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; }
37
-
38
- /* Apply background to builder canvas */
39
- .builder-canvas { background-color: ${backgroundColor !== null && backgroundColor !== void 0 ? backgroundColor : "transparent"} !important; }
40
-
41
- /* Disable browser default margin */
42
- .builder-canvas p { margin: 0; }
43
- `;
44
- const globalStyles = `
45
- /* Define builder-canvas as a container for container queries */
46
- .builder-canvas {
47
- container-name: builder-canvas;
48
- container-type: inline-size;
49
- }
50
-
51
- /* Prevents default blue color and underline for standard <a> tags */
52
- .builder-canvas .ql-snow .ql-editor a {
53
- color: inherit;
54
- text-decoration: none;
55
- cursor: default;
56
- }
57
-
58
- /* Responsive container styles - Using container query */
59
- @container builder-canvas (max-width: 768px) {
60
- .container-fixed-width {
61
- width: 100% !important;
62
- max-width: 100% !important;
63
- }
64
- }
65
-
66
- @container builder-canvas (max-width: 768px) {
67
- .hide-on-mobile {
68
- display: none !important;
69
- max-height: 0 !important;
70
- overflow: hidden !important;
71
- mso-hide: all;
72
- }
73
- }
74
-
75
- @container builder-canvas (min-width: 769px) {
76
- .hide-on-desktop {
77
- display: none !important;
78
- max-height: 0 !important;
79
- overflow: hidden !important;
80
- mso-hide: all;
81
- }
82
- }
83
-
84
- /* Stack columns on mobile - Using container query */
85
- @container builder-canvas (max-width: 768px) {
86
- .stack-td {
87
- width: 100% !important;
88
- display: block !important;
89
- float: left;
90
- clear: both;
91
- padding-left: 0 !important;
92
- padding-right: 0 !important;
93
- }
94
-
95
- .desktop-gap-column {
96
- width: 0 !important;
97
- display: none !important;
98
- }
99
-
100
- .mobile-gap-spacer {
101
- display: block !important;
102
- width: 100% !important;
103
- font-size: 1px !important;
104
- line-height: 1px !important;
105
- mso-line-height-rule: exactly;
106
- }
107
- }
108
-
109
- /* Row alignment on mobile - Using container query */
110
- @container builder-canvas (max-width: 768px) {
111
- .row-content-table[data-mobile-justify="center"] {
112
- margin: 0 auto !important;
113
- float: none !important;
114
- }
115
- .row-content-table[data-mobile-justify="start"] {
116
- margin: 0 !important;
117
- float: left !important;
118
- }
119
- .row-content-table[data-mobile-justify="end"] {
120
- margin: 0 0 0 auto !important;
121
- float: right !important;
122
- }
123
-
124
- .row-content-table[data-mobile-align="center"] .child-cell {
125
- vertical-align: middle !important;
126
- }
127
- .row-content-table[data-mobile-align="start"] .child-cell {
128
- vertical-align: top !important;
129
- }
130
- .row-content-table[data-mobile-align="end"] .child-cell {
131
- vertical-align: bottom !important;
132
- }
133
-
134
- /* Mobile Wrap - Pure CSS Solution */
135
- .row-content-table[data-mobile-wrap="true"] {
136
- width: 100% !important;
137
- max-width: 100% !important;
138
- }
139
-
140
- .row-content-table[data-mobile-wrap="true"] > tbody > .content-tr {
141
- display: block !important;
142
- }
143
-
144
- .row-content-table[data-mobile-wrap="true"] > tbody > .content-tr > .child-cell {
145
- display: block !important;
146
- width: 100% !important;
147
- box-sizing: border-box !important;
148
- }
149
-
150
- .row-content-table[data-mobile-wrap="true"] > tbody > .content-tr > .row-gap-td {
151
- display: none !important;
152
- width: 0 !important;
153
- height: 0 !important;
154
- }
155
-
156
- .row-content-table[data-mobile-wrap="true"] > tbody > .content-tr > .child-cell:not(:last-child) {
157
- margin-bottom: 20px !important;
158
- }
159
-
160
- /* Dynamic gap support - common values */
161
- ${["10px", "15px", "20px", "24px", "30px", "40px", ...rowGaps]
162
- .filter((gap, index, self) => self.indexOf(gap) === index)
163
- .map((gap) => `
164
- .row-content-table[data-mobile-wrap="true"][data-gap="${gap}"] > tbody > .content-tr > .child-cell:not(:last-child) {
165
- margin-bottom: ${gap} !important;
166
- }`)
167
- .join("\n")}
168
- }
169
-
170
- /* Heading style reset */
171
- h1, h2, h3, h4, h5, h6 {
172
- margin: 0;
173
- padding: 0;
174
- font-weight: inherit;
175
- }
176
- `;
177
- // Create or update MSO reset style tag
178
- let msoStyleTag = document.getElementById("email-mso-reset-styles");
179
- if (!msoStyleTag) {
180
- msoStyleTag = document.createElement("style");
181
- msoStyleTag.id = "email-mso-reset-styles";
182
- msoStyleTag.type = "text/css";
183
- document.head.appendChild(msoStyleTag);
184
- }
185
- msoStyleTag.textContent = msoResetStyles;
186
- // Create or update global styles tag
187
- let globalStyleTag = document.getElementById("email-global-styles");
188
- if (!globalStyleTag) {
189
- globalStyleTag = document.createElement("style");
190
- globalStyleTag.id = "email-global-styles";
191
- globalStyleTag.type = "text/css";
192
- document.head.appendChild(globalStyleTag);
193
- }
194
- globalStyleTag.textContent = globalStyles;
195
- // Apply background color to builder canvas element directly
196
- const builderCanvas = document.querySelector(".builder-canvas");
197
- if (builderCanvas instanceof HTMLElement) {
198
- builderCanvas.style.backgroundColor = backgroundColor;
199
- }
200
- }, [backgroundColor, title, rowGaps]);
201
- // ── 2. Font injection — always into document.head, always first ──────────
202
- //
203
- // @font-face rules MUST live in <head> and be declared BEFORE any selector
204
- // rules that reference the family name. Injecting them as the firstChild of
205
- // <head> guarantees both constraints, eliminating FOUT in the canvas and
206
- // ensuring the browser's font loader can resolve faces before first paint.
207
- useEffect(() => {
208
- var _a;
209
- if (!fonts.length) {
210
- // Clean up tag if all fonts were removed
211
- (_a = document.getElementById("email-font-faces")) === null || _a === void 0 ? void 0 : _a.remove();
212
- return;
213
- }
214
- // insert preload
215
- fonts.forEach((resolved) => {
216
- resolved.fontProps.forEach(({ webFont }) => {
217
- if (!webFont)
218
- return;
219
- if (document.querySelector(`link[data-font-preload="${webFont.url}"]`))
220
- return;
221
- const link = document.createElement("link");
222
- link.rel = "preload";
223
- link.as = "font";
224
- link.href = webFont.url;
225
- link.type = `font/${webFont.format}`;
226
- link.crossOrigin = "anonymous";
227
- link.dataset.fontPreload = webFont.url;
228
- document.head.insertBefore(link, document.head.firstChild);
229
- });
230
- });
231
- const fontFaceCss = fonts
232
- .flatMap((resolved) => resolved.fontProps.map(({ fontFamily, fontStyle = "normal", fontWeight = 400, webFont, fallbackFontFamily, }) => {
233
- if (!webFont)
234
- return "";
235
- const fallback = Array.isArray(fallbackFontFamily)
236
- ? fallbackFontFamily[0]
237
- : fallbackFontFamily;
238
- return `@font-face {
239
- font-family: '${fontFamily}';
240
- font-style: ${fontStyle};
241
- font-weight: ${fontWeight};
242
- font-display: swap;
243
- src: url('${webFont.url}') format('${webFont.format}');
244
- mso-font-alt: '${fallback !== null && fallback !== void 0 ? fallback : "sans-serif"}';
245
- }`;
246
- }))
247
- .filter(Boolean)
248
- .join("\n");
249
- let fontStyleTag = document.getElementById("email-font-faces");
250
- if (!fontStyleTag) {
251
- fontStyleTag = document.createElement("style");
252
- fontStyleTag.id = "email-font-faces";
253
- fontStyleTag.type = "text/css";
254
- // Prepend as the very first child so @font-face is resolved before
255
- // any other style rules that reference the font family.
256
- document.head.insertBefore(fontStyleTag, document.head.firstChild);
257
- }
258
- fontStyleTag.textContent = fontFaceCss;
259
- return () => {
260
- var _a;
261
- document
262
- .querySelectorAll("link[data-font-preload]")
263
- .forEach((el) => el.remove());
264
- (_a = document.getElementById("email-font-faces")) === null || _a === void 0 ? void 0 : _a.remove();
265
- };
266
- }, [fonts]);
267
- useEffect(() => {
268
- if (!fonts.length)
269
- return;
270
- const loadPromises = fonts.flatMap((resolved) => resolved.fontProps
271
- .filter((p) => !!p.webFont)
272
- .map(({ fontFamily, fontWeight = 400, fontStyle = "normal", webFont }) => {
273
- const face = new FontFace(fontFamily, `url('${webFont.url}') format('${webFont.format}')`, { weight: String(fontWeight), style: fontStyle });
274
- document.fonts.add(face);
275
- return face.load().catch(() => {
276
- // Silently ignore load failures (network offline, etc.)
277
- });
278
- }));
279
- // Once all faces load, force a re-render of the canvas text
280
- Promise.all(loadPromises).then(() => {
281
- document
282
- .querySelectorAll(".builder-canvas .ql-editor")
283
- .forEach((el) => {
284
- // Toggling a harmless style forces the browser to re-evaluate font matching
285
- el.style.visibility =
286
- el.style.visibility === "hidden" ? "" : el.style.visibility;
287
- });
288
- });
289
- }, [fonts]);
290
- // ── 3. Custom children (additional style tags, etc.) ─────────────────────
291
- useEffect(() => {
292
- if (!children)
293
- return;
294
- // Create a container for custom head elements
295
- let customHeadContainer = document.getElementById("email-custom-head-elements");
296
- if (!customHeadContainer) {
297
- customHeadContainer = document.createElement("div");
298
- customHeadContainer.id = "email-custom-head-elements";
299
- customHeadContainer.style.display = "none";
300
- document.head.appendChild(customHeadContainer);
301
- }
302
- return () => {
303
- // Cleanup custom elements on unmount
304
- if (customHeadContainer && customHeadContainer.parentNode) {
305
- customHeadContainer.parentNode.removeChild(customHeadContainer);
306
- }
307
- };
308
- }, [children]);
309
- // Nothing is rendered into the React tree — all injection is imperative.
310
- return _jsx(Fragment, { children: children });
311
- }
@@ -1,46 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { memo } from "react";
3
- import { arePropsEqual } from "../utils/memoUtils";
4
- function Heading({ config, devMode, children }) {
5
- const { text, level = "h1", padding, color, textAlign, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform, textDecoration, direction, verticalAlign, backgroundColor, wordBreak, whiteSpace, } = config;
6
- // Determine the content to render
7
- const content = text !== null && text !== void 0 ? text : children;
8
- const isString = typeof content === "string";
9
- // 1. TD Style: Where padding, background, width, and verticalAlign are applied.
10
- const tdStyle = {
11
- padding: padding,
12
- backgroundColor: backgroundColor,
13
- width: "100%",
14
- verticalAlign: verticalAlign || "top",
15
- };
16
- // 2. Heading Tag Style: Applied directly to the H tag.
17
- const headingStyle = {
18
- color: color,
19
- textAlign: textAlign,
20
- fontFamily: fontFamily || "Arial, Helvetica, sans-serif",
21
- fontSize: fontSize,
22
- fontWeight: fontWeight,
23
- fontStyle: fontStyle,
24
- lineHeight: lineHeight,
25
- letterSpacing: letterSpacing,
26
- textTransform: textTransform,
27
- textDecoration: textDecoration,
28
- direction: direction,
29
- wordBreak: wordBreak,
30
- whiteSpace: whiteSpace,
31
- // Critical: Remove default top/bottom margin from HTML heading tags
32
- margin: "0",
33
- padding: "0",
34
- // Outlook specific fixes (using string indexing)
35
- ["msoLineHeightRule"]: "exactly",
36
- };
37
- // Dynamically create the Heading element
38
- const HeadingTag = level;
39
- return (
40
- // Wrap the heading content in a table for padding/width/background management.
41
- _jsx("table", { "aria-label": "Heading Block Wrapper", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: {
42
- width: "100%",
43
- borderCollapse: "collapse",
44
- }, children: _jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { style: tdStyle, align: textAlign, children: isString ? (_jsx(HeadingTag, { style: headingStyle, dangerouslySetInnerHTML: { __html: content } })) : (_jsx(HeadingTag, { style: headingStyle, children: content })) }) }) }) }));
45
- }
46
- export default memo(Heading, arePropsEqual);
@@ -1,20 +0,0 @@
1
- import React from "react";
2
- export default function Html({ children, backgroundColor = "#ffffff", }) {
3
- const htmlAttributes = {
4
- // Standard xmlns attribute
5
- xmlns: "http://www.w3.org/1999/xhtml",
6
- // Namespaced attributes (with colons) are safe in the JS object
7
- "xmlns:v": "urn:schemas-microsoft-com:vml",
8
- "xmlns:o": "urn:schemas-microsoft-com:office:office",
9
- lang: "en",
10
- xmlLang: "en",
11
- // bgcolor attribute
12
- bgcolor: backgroundColor,
13
- // Note: The 'children' prop is passed as the third argument to createElement, not here.
14
- };
15
- // React.createElement avoids the JSX transpiler error by passing attributes as a JS object.
16
- return React.createElement('html', // The element tag name
17
- htmlAttributes, // The attributes/props object
18
- children // The content to be rendered inside the <html> tag
19
- );
20
- }
@@ -1,276 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { memo } from "react";
3
- import { arePropsEqual } from "../utils/memoUtils";
4
- // Map alignment to HTML 'align' attribute
5
- const justifyMap = {
6
- start: "left",
7
- center: "center",
8
- end: "right",
9
- };
10
- function getBorderStyle(border) {
11
- if (!border)
12
- return {};
13
- const style = {};
14
- // If a full border is specified, apply it
15
- if (border.width && border.style && border.color) {
16
- style.border = `${border.width} ${border.style} ${border.color}`;
17
- }
18
- else {
19
- // If only individual borders are specified, explicitly set others to 'none'
20
- // to prevent Outlook Classic from showing black borders
21
- const hasIndividualBorders = border.top || border.right || border.bottom || border.left;
22
- if (hasIndividualBorders) {
23
- // Default all borders to none
24
- style.borderTop = "none";
25
- style.borderRight = "none";
26
- style.borderBottom = "none";
27
- style.borderLeft = "none";
28
- }
29
- }
30
- // Override with specific borders if provided
31
- if (border.top) {
32
- style.borderTop = `${border.top.width} ${border.top.style} ${border.top.color}`;
33
- }
34
- if (border.right) {
35
- style.borderRight = `${border.right.width} ${border.right.style} ${border.right.color}`;
36
- }
37
- if (border.bottom) {
38
- style.borderBottom = `${border.bottom.width} ${border.bottom.style} ${border.bottom.color}`;
39
- }
40
- if (border.left) {
41
- style.borderLeft = `${border.left.width} ${border.left.style} ${border.left.color}`;
42
- }
43
- return style;
44
- }
45
- function getBorderStyleString(border) {
46
- if (!border)
47
- return "";
48
- const styles = [];
49
- // If a full border is specified, apply it
50
- if (border.width && border.style && border.color) {
51
- styles.push(`border: ${border.width} ${border.style} ${border.color};`);
52
- }
53
- else {
54
- // If only individual borders are specified
55
- const hasIndividualBorders = border.top || border.right || border.bottom || border.left;
56
- if (hasIndividualBorders) {
57
- // Default all borders to none
58
- styles.push("border-top: none;");
59
- styles.push("border-right: none;");
60
- styles.push("border-bottom: none;");
61
- styles.push("border-left: none;");
62
- }
63
- }
64
- // Override with specific borders if provided
65
- if (border.top) {
66
- styles.push(`border-top: ${border.top.width} ${border.top.style} ${border.top.color};`);
67
- }
68
- if (border.right) {
69
- styles.push(`border-right: ${border.right.width} ${border.right.style} ${border.right.color};`);
70
- }
71
- if (border.bottom) {
72
- styles.push(`border-bottom: ${border.bottom.width} ${border.bottom.style} ${border.bottom.color};`);
73
- }
74
- if (border.left) {
75
- styles.push(`border-left: ${border.left.width} ${border.left.style} ${border.left.color};`);
76
- }
77
- return styles.join(" ");
78
- }
79
- // Helper to build Iconify API URL
80
- function buildIconifyUrl(config) {
81
- const { iconIdentifier, height = 24, color = "000000", rotate = 0, rotateOrientation = "cw", } = config;
82
- if (!iconIdentifier)
83
- return null;
84
- // Parse height to extract numeric value
85
- const parseHeight = (h) => {
86
- if (typeof h === "number")
87
- return h;
88
- // Extract numeric value from string (e.g., "24px" -> 24)
89
- const match = String(h).match(/^(-?\d*\.?\d+)/);
90
- return match ? parseFloat(match[1]) : 24;
91
- };
92
- const numericHeight = parseHeight(height);
93
- // Remove # from color if present
94
- const cleanColor = color.replace("#", "");
95
- // Build URL from template
96
- const template = process.env.ICONIFY_API_IMAGE_URI ||
97
- "https://iconify.pagenflow.com/api/image/{{height}}/{{color}}/{{rotate}}-{{rotate-orientation}}/{{icon-full-name}}.png";
98
- return template
99
- .replace("{{height}}", String(numericHeight * 2))
100
- .replace("{{color}}", cleanColor)
101
- .replace("{{rotate}}", String(rotate))
102
- .replace("{{rotate-orientation}}", rotateOrientation)
103
- .replace("{{icon-full-name}}", iconIdentifier);
104
- }
105
- // Helper to build link href based on innerLink type
106
- function buildLinkHref(innerLink) {
107
- if (!innerLink || innerLink.type === "none")
108
- return null;
109
- switch (innerLink.type) {
110
- case "url":
111
- return innerLink.url || null;
112
- case "email":
113
- return innerLink.email ? `mailto:${innerLink.email}` : null;
114
- case "phone":
115
- return innerLink.phone ? `tel:${innerLink.phone}` : null;
116
- case "anchor":
117
- return innerLink.anchor ? `#${innerLink.anchor}` : null;
118
- case "page_top":
119
- return "#top";
120
- case "page_bottom":
121
- return "#bottom";
122
- default:
123
- return null;
124
- }
125
- }
126
- function Icon({ config, devNode, devMode, children }) {
127
- const {
128
- // base64Source,
129
- width, height, backgroundColor, padding = "0", borderRadius = "0", border, innerLink, justifyContent = "center", } = config;
130
- // Determine icon source
131
- const iconSrc = buildIconifyUrl(config);
132
- const href = buildLinkHref(innerLink);
133
- const target = (innerLink === null || innerLink === void 0 ? void 0 : innerLink.target) || "_self";
134
- const align = justifyMap[justifyContent];
135
- // Get border styles
136
- const borderStyle = getBorderStyle(border);
137
- const borderStyleString = getBorderStyleString(border);
138
- // Convert width/height to string with px if number
139
- const widthStr = typeof width === "number" ? `${width}px` : width;
140
- const heightStr = typeof height === "number" ? `${height}px` : height;
141
- // Parse numeric values for HTML attributes
142
- const widthNum = typeof width === "number"
143
- ? width
144
- : typeof width === "string" && width.endsWith("px")
145
- ? parseInt(width, 10)
146
- : undefined;
147
- const heightNum = typeof height === "number"
148
- ? height
149
- : typeof height === "string" && height.endsWith("px")
150
- ? parseInt(height, 10)
151
- : undefined;
152
- // 1. Image Style: Critical for compatibility
153
- const imgStyle = {
154
- display: "block", // Prevents extra vertical space
155
- border: 0, // No default border
156
- width: widthStr || "auto",
157
- height: heightStr || "auto",
158
- objectFit: "contain",
159
- };
160
- // 2. Link Style: No underline or color changes
161
- const linkStyle = {
162
- display: "block",
163
- textDecoration: "none",
164
- border: 0,
165
- outline: "none",
166
- };
167
- // 3. Outer TD Style: Background and border-radius wrapper with border
168
- const outerTdStyle = {
169
- backgroundColor: backgroundColor,
170
- borderRadius: borderRadius,
171
- overflow: "hidden",
172
- fontSize: "0",
173
- lineHeight: "0",
174
- };
175
- // 4. Inner Table Style: Apply border here with border-collapse: separate
176
- const innerTableStyle = {
177
- width: "100%",
178
- borderCollapse: "separate",
179
- borderSpacing: 0,
180
- borderRadius: borderRadius,
181
- ...borderStyle,
182
- };
183
- // 5. Inner TD Style: Padding
184
- const innerTdStyle = {
185
- padding: padding,
186
- fontSize: "0", // CRITICAL: Collapses extra space
187
- lineHeight: "0", // CRITICAL: Collapses extra space
188
- };
189
- // --- VML Calculation for Outlook Compatibility ---
190
- const numericPadding = parseInt(padding.split(" ")[0] || "0", 10);
191
- const vmlWidth = (widthNum || 24) + numericPadding * 2;
192
- const vmlHeight = (heightNum || 24) + numericPadding * 2;
193
- // VML colors must use full hex format
194
- const vmlFillColor = (backgroundColor === null || backgroundColor === void 0 ? void 0 : backgroundColor.startsWith("#"))
195
- ? backgroundColor
196
- : backgroundColor
197
- ? `#${backgroundColor}`
198
- : "#ffffff";
199
- // Calculate arcsize percentage for VML
200
- const numericBorderRadius = parseInt(borderRadius || "0", 10);
201
- const arcsize = numericBorderRadius > 0
202
- ? Math.min((numericBorderRadius / Math.min(vmlWidth, vmlHeight)) * 100, 100)
203
- : 0;
204
- // VML stroke color for border
205
- const vmlStrokeColor = (border === null || border === void 0 ? void 0 : border.color) || vmlFillColor;
206
- const vmlStrokeWeight = (border === null || border === void 0 ? void 0 : border.width) ? parseInt(border.width, 10) : 0;
207
- const hasVmlStroke = vmlStrokeWeight > 0;
208
- // Build VML code for Outlook
209
- const vmlIcon = backgroundColor && numericBorderRadius > 0
210
- ? `
211
- <!--[if mso]>
212
- <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" ${href && !devMode ? `href="${href}"` : ""} style="height:${vmlHeight}px;width:${vmlWidth}px;v-text-anchor:middle;" arcsize="${arcsize}%" ${hasVmlStroke ? `strokecolor="${vmlStrokeColor}" strokeweight="${vmlStrokeWeight}px"` : 'stroke="false"'} fillcolor="${vmlFillColor}">
213
- <w:anchorlock/>
214
- <v:textbox inset="0,0,0,0" style="text-align: center;">
215
- <center style="padding:${padding};">
216
- <img src="${iconSrc || ""}" alt="" width="${widthNum || 24}" height="${heightNum || 24}" border="0" style="display:block;border:0;object-fit:contain;" />
217
- </center>
218
- </v:textbox>
219
- </v:roundrect>
220
- <![endif]-->
221
- `
222
- : "";
223
- // If no icon source, return empty
224
- if (!iconSrc && !devMode) {
225
- return null;
226
- }
227
- // Icon image element
228
- const iconElement = devMode && !!children ? (children) : iconSrc ? (_jsx("img", { draggable: false, src: iconSrc, alt: "", style: imgStyle, width: widthNum, height: heightNum, border: 0 })) : (_jsx(_Fragment, {}));
229
- // Wrap in link if href exists and not in dev mode
230
- const content = href && !devMode ? (_jsx("a", { href: href, target: target, style: linkStyle, ...(target === "_blank" ? { rel: "noopener noreferrer" } : {}), children: iconElement })) : (iconElement);
231
- // Build the HTML content with VML support (only when NOT in dev mode)
232
- const useVML = !devMode && backgroundColor && numericBorderRadius > 0;
233
- const htmlContent = useVML
234
- ? `
235
- ${vmlIcon}
236
- <!--[if !mso]><!-->
237
- <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="border-collapse: collapse; width: 100%;">
238
- <tbody>
239
- <tr>
240
- <td style="background-color: ${backgroundColor}; border-radius: ${borderRadius}; overflow: hidden; font-size: 0; line-height: 0;">
241
- <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="border-collapse: separate; border-spacing: 0; border-radius: ${borderRadius}; width: 100%; ${borderStyleString}">
242
- <tbody>
243
- <tr>
244
- <td style="padding: ${padding}; font-size: 0; line-height: 0;">
245
- ${href
246
- ? `<a href="${href}" target="${target}" style="display:block;text-decoration:none;border:0;outline:none;" ${target === "_blank" ? 'rel="noopener noreferrer"' : ""}>
247
- <img draggable="false" src="${iconSrc}" alt="" width="${widthNum || 24}" height="${heightNum || 24}" border="0" style="display:block;border:0;width:${widthStr || "auto"};height:${heightStr || "auto"};object-fit:contain;" />
248
- </a>`
249
- : `<img draggable="false" src="${iconSrc}" alt="" width="${widthNum || 24}" height="${heightNum || 24}" border="0" style="display:block;border:0;width:${widthStr || "auto"};height:${heightStr || "auto"};object-fit:contain;" />`}
250
- </td>
251
- </tr>
252
- </tbody>
253
- </table>
254
- </td>
255
- </tr>
256
- </tbody>
257
- </table>
258
- <!--<![endif]-->
259
- `
260
- : null;
261
- return (_jsxs("table", { "aria-label": "Icon", role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, align: align, style: {
262
- // --- Start dev
263
- position: "relative",
264
- // --- End dev
265
- width: "auto",
266
- borderCollapse: "collapse",
267
- // base
268
- boxSizing: "border-box",
269
- border: 0,
270
- margin: 0,
271
- padding: 0,
272
- }, onClick: devMode ? (e) => e.preventDefault() : undefined, children: [_jsx("tbody", { children: _jsx("tr", { children: useVML ? (_jsx("td", { dangerouslySetInnerHTML: {
273
- __html: htmlContent !== null && htmlContent !== void 0 ? htmlContent : "",
274
- } })) : (_jsx("td", { style: outerTdStyle, align: align, children: _jsx("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, style: innerTableStyle, children: _jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { style: innerTdStyle, align: align, children: content }) }) }) }) })) }) }), devMode && !!devNode && (_jsx("tfoot", { children: _jsx("tr", { children: _jsx("td", { children: devNode }) }) }))] }));
275
- }
276
- export default memo(Icon, arePropsEqual);