@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.
- package/dist/components/Body.js +57 -0
- package/dist/components/BodyDev.js +57 -0
- package/dist/components/Button.js +327 -0
- package/dist/components/Column.js +127 -0
- package/dist/components/Container.js +179 -0
- package/dist/components/Divider.js +41 -0
- package/dist/components/Font.js +44 -0
- package/dist/components/Head.js +134 -0
- package/dist/components/HeadDev.js +311 -0
- package/dist/components/Heading.js +46 -0
- package/dist/components/Html.js +20 -0
- package/dist/components/Icon.js +276 -0
- package/dist/components/Image.js +119 -0
- package/dist/components/MsoConditional.js +19 -0
- package/dist/components/Row.js +157 -0
- package/dist/components/Section.js +65 -0
- package/dist/components/Spacer.js +40 -0
- package/dist/components/Text.js +42 -0
- package/dist/index.cjs.js +191 -89
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +191 -89
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +17 -0
- package/dist/types/IInnerLink.js +1 -0
- package/dist/types/ResolvedFont.js +1 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/isEqual.js +1439 -0
- package/dist/utils/memoUtils.js +55 -0
- package/package.json +1 -1
|
@@ -0,0 +1,311 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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);
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
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);
|