@unlayer/react-elements 0.1.16 → 0.1.17
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/README.md +20 -2
- package/dist/index.cjs +170 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -18
- package/dist/index.d.ts +47 -18
- package/dist/index.js +170 -25
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -47,6 +47,16 @@ function WelcomeEmail() {
|
|
|
47
47
|
}
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Using with AI agents (Claude, Cursor, etc.)
|
|
51
|
+
|
|
52
|
+
Assembling these templates in code is something AI coding agents do well. The official **`unlayer-elements`** skill teaches them the strict structure, the canonical prop shapes, the image-sizing model, and the patterns that make generated emails actually look good:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx skills add unlayer/unlayer-skills
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This installs the `unlayer-elements` skill (alongside the other Unlayer skills). Your agent then knows how to build emails, pages, and documents with these components and render them via `renderToHtml` / `renderToJson`. See [unlayer/unlayer-skills](https://github.com/unlayer/unlayer-skills).
|
|
59
|
+
|
|
50
60
|
## Critical Rules
|
|
51
61
|
|
|
52
62
|
These props have non-obvious shapes that **must** be followed exactly:
|
|
@@ -293,7 +303,7 @@ Each wrapper threads the correct mode to all children automatically.
|
|
|
293
303
|
|
|
294
304
|
## renderToHtml
|
|
295
305
|
|
|
296
|
-
Render any element tree to a
|
|
306
|
+
Render any element tree to a complete, ready-to-use HTML document — from `<!DOCTYPE ...>` to `</html>` — with no React hydration markers. Perfect for email sending and PDF generation:
|
|
297
307
|
|
|
298
308
|
```tsx
|
|
299
309
|
import { renderToHtml, Email, Row, Column, ColumnLayouts, Paragraph, Button } from '@unlayer/react-elements';
|
|
@@ -311,10 +321,18 @@ const html = renderToHtml(
|
|
|
311
321
|
</Button>
|
|
312
322
|
</Column>
|
|
313
323
|
</Row>
|
|
314
|
-
</Email
|
|
324
|
+
</Email>,
|
|
325
|
+
{ title: "Welcome" }
|
|
315
326
|
);
|
|
316
327
|
```
|
|
317
328
|
|
|
329
|
+
The document shell matches the mode: `<Email>` gets the XHTML transitional doctype plus the MSO conditional comments Outlook needs, `<Page>` gets a standard HTML5 shell, and `<Document>` gets a print/PDF-friendly XHTML shell. Options accept config overrides plus:
|
|
330
|
+
|
|
331
|
+
- `title` — document `<title>`
|
|
332
|
+
- `fonts` — web-font stylesheet URLs to load via `<link>` tags, e.g. `[{ url: "https://fonts.googleapis.com/css2?family=Inter" }]`
|
|
333
|
+
|
|
334
|
+
If your app owns the document shell (an existing page template, an ESP template, an iframe `srcdoc`), use `renderToHtmlParts` instead — it returns `{ head, body }` chunks for you to place yourself.
|
|
335
|
+
|
|
318
336
|
## UnlayerProvider
|
|
319
337
|
|
|
320
338
|
Configure global settings like CDN base URL, merge tags, text direction, and rendering mode:
|
package/dist/index.cjs
CHANGED
|
@@ -198,6 +198,50 @@ function textNodeToHtml(node) {
|
|
|
198
198
|
}
|
|
199
199
|
return html;
|
|
200
200
|
}
|
|
201
|
+
var NUMERIC_ALIGN = {
|
|
202
|
+
1: "left",
|
|
203
|
+
2: "center",
|
|
204
|
+
3: "right",
|
|
205
|
+
4: "justify"
|
|
206
|
+
};
|
|
207
|
+
function dirAttribute(direction) {
|
|
208
|
+
return direction === "ltr" || direction === "rtl" ? ` dir="${direction}"` : "";
|
|
209
|
+
}
|
|
210
|
+
function paragraphAlign(format) {
|
|
211
|
+
if (typeof format === "string") {
|
|
212
|
+
return ["left", "center", "right", "justify"].includes(format) ? format : void 0;
|
|
213
|
+
}
|
|
214
|
+
if (typeof format === "number") {
|
|
215
|
+
return NUMERIC_ALIGN[format];
|
|
216
|
+
}
|
|
217
|
+
return void 0;
|
|
218
|
+
}
|
|
219
|
+
function paragraphNodeToHtml(node, childrenHtml) {
|
|
220
|
+
const isInlineTool = !!node.isInlineTool;
|
|
221
|
+
const tag = isInlineTool ? "span" : "p";
|
|
222
|
+
const dir = dirAttribute(node.direction);
|
|
223
|
+
const styleParts = [];
|
|
224
|
+
if (node.style) {
|
|
225
|
+
styleParts.push(String(node.style).replace(/;+\s*$/, ""));
|
|
226
|
+
}
|
|
227
|
+
if (!isInlineTool) {
|
|
228
|
+
styleParts.push(
|
|
229
|
+
"margin: 0px",
|
|
230
|
+
"margin-block-start: 0px",
|
|
231
|
+
"margin-block-end: 0px"
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const indent = Number(node.indent) || 0;
|
|
235
|
+
if (indent > 0) {
|
|
236
|
+
styleParts.push(`padding-inline-start: ${indent * 40}px`);
|
|
237
|
+
}
|
|
238
|
+
const align = paragraphAlign(node.format);
|
|
239
|
+
if (align) {
|
|
240
|
+
styleParts.push(`text-align: ${align}`);
|
|
241
|
+
}
|
|
242
|
+
const style = styleParts.length > 0 ? ` style="${escapeHtml(styleParts.join("; ") + ";")}"` : "";
|
|
243
|
+
return `<${tag}${dir}${style}>${childrenHtml || "<br>"}</${tag}>`;
|
|
244
|
+
}
|
|
201
245
|
function nodeToHtml(node) {
|
|
202
246
|
if (!node || typeof node !== "object") return "";
|
|
203
247
|
const type = node.type;
|
|
@@ -212,14 +256,11 @@ function nodeToHtml(node) {
|
|
|
212
256
|
case "root":
|
|
213
257
|
return childrenHtml;
|
|
214
258
|
case "paragraph":
|
|
215
|
-
case "extended-paragraph":
|
|
216
|
-
|
|
217
|
-
const dir = node.direction ? ` dir="${node.direction}"` : "";
|
|
218
|
-
return `<p${dir}>${childrenHtml}</p>`;
|
|
219
|
-
}
|
|
259
|
+
case "extended-paragraph":
|
|
260
|
+
return paragraphNodeToHtml(node, childrenHtml);
|
|
220
261
|
case "heading": {
|
|
221
|
-
const tag = node.tag
|
|
222
|
-
const dir = node.direction
|
|
262
|
+
const tag = /^h[1-6]$/.test(node.tag) ? node.tag : "h1";
|
|
263
|
+
const dir = dirAttribute(node.direction);
|
|
223
264
|
return `<${tag}${dir}>${childrenHtml}</${tag}>`;
|
|
224
265
|
}
|
|
225
266
|
case "link": {
|
|
@@ -1122,7 +1163,7 @@ function getWidthPercentages(cells) {
|
|
|
1122
1163
|
return { value, className };
|
|
1123
1164
|
});
|
|
1124
1165
|
}
|
|
1125
|
-
function generateGridCSS(cells, mode, contentWidth = 600, mobileBreakpoint =
|
|
1166
|
+
function generateGridCSS(cells, mode, contentWidth = 600, mobileBreakpoint = 480) {
|
|
1126
1167
|
const widths = getWidthPercentages(cells);
|
|
1127
1168
|
if (mode === "email") {
|
|
1128
1169
|
const minQuery = `@media only screen and (min-width: ${contentWidth + 20}px)`;
|
|
@@ -1159,21 +1200,14 @@ ${widths.map(({ value, className }) => ` .no-stack .u-col-${className} { width:
|
|
|
1159
1200
|
const columnCSS = widths.map(
|
|
1160
1201
|
({ value, className }) => `.u-row .u-col.u-col-${className} { flex: 0 0 ${value}%; max-width: ${value}%; }`
|
|
1161
1202
|
).join("\n");
|
|
1162
|
-
const responsiveCSS = `
|
|
1163
|
-
@media
|
|
1164
|
-
.
|
|
1165
|
-
.u-row
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
min-width: 320px !important;
|
|
1203
|
+
const responsiveCSS = mode === "document" ? "" : `
|
|
1204
|
+
@media (max-width: ${mobileBreakpoint}px) {
|
|
1205
|
+
.u_row .container { max-width: 100% !important; }
|
|
1206
|
+
.u-row:not(.no-stack) { flex-wrap: wrap; }
|
|
1207
|
+
.u-row:not(.no-stack) .u-col {
|
|
1208
|
+
flex: 0 0 100% !important;
|
|
1169
1209
|
max-width: 100% !important;
|
|
1170
1210
|
}
|
|
1171
|
-
.u-row .u-col > div { margin: 0 auto; }
|
|
1172
|
-
.no-stack .u-col {
|
|
1173
|
-
min-width: 0 !important;
|
|
1174
|
-
display: table-cell !important;
|
|
1175
|
-
}
|
|
1176
|
-
${widths.map(({ value, className }) => ` .no-stack .u-col-${className} { width: ${value}% !important; }`).join("\n")}
|
|
1177
1211
|
}`;
|
|
1178
1212
|
return baseCSS + "\n" + columnCSS + "\n" + responsiveCSS;
|
|
1179
1213
|
}
|
|
@@ -1674,8 +1708,81 @@ function extractHeadFromTree(element, options) {
|
|
|
1674
1708
|
};
|
|
1675
1709
|
}
|
|
1676
1710
|
|
|
1711
|
+
// src/utils/document-layouts.ts
|
|
1712
|
+
function emailLayout(args) {
|
|
1713
|
+
const { dir, titleTag, tagLines, styleTag, scriptTag, fontLinks, body } = args;
|
|
1714
|
+
const head = [
|
|
1715
|
+
titleTag,
|
|
1716
|
+
tagLines,
|
|
1717
|
+
styleTag,
|
|
1718
|
+
scriptTag,
|
|
1719
|
+
fontLinks && `<!--[if !mso]><!-->${fontLinks}<!--<![endif]-->`
|
|
1720
|
+
].filter(Boolean).join("\n ");
|
|
1721
|
+
return `<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
1722
|
+
<html${dir} xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
1723
|
+
<head>
|
|
1724
|
+
<!--[if gte mso 9]>
|
|
1725
|
+
<xml>
|
|
1726
|
+
<o:OfficeDocumentSettings>
|
|
1727
|
+
<o:AllowPNG/>
|
|
1728
|
+
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
1729
|
+
</o:OfficeDocumentSettings>
|
|
1730
|
+
</xml>
|
|
1731
|
+
<![endif]-->
|
|
1732
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
1733
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1734
|
+
<meta name="x-apple-disable-message-reformatting">
|
|
1735
|
+
<!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]-->
|
|
1736
|
+
${head}
|
|
1737
|
+
</head>
|
|
1738
|
+
${body}
|
|
1739
|
+
</html>
|
|
1740
|
+
`;
|
|
1741
|
+
}
|
|
1742
|
+
function documentLayout(args) {
|
|
1743
|
+
const { dir, titleTag, tagLines, styleTag, scriptTag, fontLinks, body } = args;
|
|
1744
|
+
const head = [titleTag, tagLines, styleTag, scriptTag, fontLinks].filter(Boolean).join("\n ");
|
|
1745
|
+
return `<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
1746
|
+
<html${dir} xmlns="http://www.w3.org/1999/xhtml">
|
|
1747
|
+
<head>
|
|
1748
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
1749
|
+
${head}
|
|
1750
|
+
</head>
|
|
1751
|
+
${body}
|
|
1752
|
+
</html>
|
|
1753
|
+
`;
|
|
1754
|
+
}
|
|
1755
|
+
function webLayout(args) {
|
|
1756
|
+
const { dir, titleTag, tagLines, styleTag, scriptTag, fontLinks, body } = args;
|
|
1757
|
+
const head = [titleTag, fontLinks, tagLines, styleTag, scriptTag].filter(Boolean).join("\n ");
|
|
1758
|
+
return `<!doctype html>
|
|
1759
|
+
<html${dir}>
|
|
1760
|
+
<head>
|
|
1761
|
+
<meta charset="utf-8">
|
|
1762
|
+
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
|
1763
|
+
<meta name="description" content="">
|
|
1764
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1765
|
+
${head}
|
|
1766
|
+
</head>
|
|
1767
|
+
<body>
|
|
1768
|
+
${body}
|
|
1769
|
+
</body>
|
|
1770
|
+
</html>
|
|
1771
|
+
`;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1677
1774
|
// src/utils/render-to-html.tsx
|
|
1678
|
-
|
|
1775
|
+
var MODE_BY_WRAPPER = {
|
|
1776
|
+
Email: "email",
|
|
1777
|
+
Page: "web",
|
|
1778
|
+
Document: "document"
|
|
1779
|
+
};
|
|
1780
|
+
function resolveDisplayMode(element, mergedConfig) {
|
|
1781
|
+
const displayName = element.type?.displayName;
|
|
1782
|
+
const wrapperMode = displayName ? MODE_BY_WRAPPER[displayName] : void 0;
|
|
1783
|
+
return wrapperMode ?? element.props.mode ?? mergedConfig.mode ?? "web";
|
|
1784
|
+
}
|
|
1785
|
+
function renderBody(element, config) {
|
|
1679
1786
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
1680
1787
|
try {
|
|
1681
1788
|
const enriched = React__default.default.cloneElement(element, { config: mergedConfig });
|
|
@@ -1688,14 +1795,52 @@ Tip: Ensure your tree uses Body > Row > Column > Item structure.`
|
|
|
1688
1795
|
);
|
|
1689
1796
|
}
|
|
1690
1797
|
}
|
|
1798
|
+
function stripOuterDiv(html) {
|
|
1799
|
+
const match = html.match(/^\s*<div[^>]*>([\s\S]*)<\/div>\s*$/);
|
|
1800
|
+
return match ? match[1] : html;
|
|
1801
|
+
}
|
|
1802
|
+
function escapeForHtml(text) {
|
|
1803
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1804
|
+
}
|
|
1805
|
+
function renderToHtml(element, options) {
|
|
1806
|
+
const { title, fonts = [], ...config } = options ?? {};
|
|
1807
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
1808
|
+
const displayMode = resolveDisplayMode(element, mergedConfig);
|
|
1809
|
+
const wrapperName = element.type?.displayName;
|
|
1810
|
+
const isUnlayerWrapper = wrapperName === "Body" || (wrapperName ? wrapperName in MODE_BY_WRAPPER : false);
|
|
1811
|
+
const rawBody = renderBody(element, config);
|
|
1812
|
+
const body = isUnlayerWrapper ? stripOuterDiv(rawBody) : rawBody;
|
|
1813
|
+
const { css, js, tags } = extractHeadFromTree(element, {
|
|
1814
|
+
displayMode,
|
|
1815
|
+
headConfig: mergedConfig.headConfig
|
|
1816
|
+
});
|
|
1817
|
+
const layoutArgs = {
|
|
1818
|
+
dir: mergedConfig.textDirection ? ` dir="${escapeForHtml(String(mergedConfig.textDirection))}"` : "",
|
|
1819
|
+
titleTag: title ? `<title>${escapeForHtml(title.trim())}</title>` : "",
|
|
1820
|
+
styleTag: css ? `<style type="text/css">
|
|
1821
|
+
${css}
|
|
1822
|
+
</style>` : "",
|
|
1823
|
+
scriptTag: js ? `<script type="application/javascript">
|
|
1824
|
+
${js}
|
|
1825
|
+
</script>` : "",
|
|
1826
|
+
tagLines: tags.join("\n"),
|
|
1827
|
+
fontLinks: fonts.filter((font) => font?.url).map(
|
|
1828
|
+
(font) => `<link href="${escapeForHtml(font.url)}" rel="stylesheet" type="text/css">`
|
|
1829
|
+
).join(""),
|
|
1830
|
+
body
|
|
1831
|
+
};
|
|
1832
|
+
if (displayMode === "email") return emailLayout(layoutArgs);
|
|
1833
|
+
if (displayMode === "document") return documentLayout(layoutArgs);
|
|
1834
|
+
return webLayout(layoutArgs);
|
|
1835
|
+
}
|
|
1691
1836
|
function renderToPlainText(element, config) {
|
|
1692
|
-
const html =
|
|
1837
|
+
const html = renderBody(element, config);
|
|
1693
1838
|
return htmlToPlainText(html);
|
|
1694
1839
|
}
|
|
1695
1840
|
function renderToHtmlParts(element, config) {
|
|
1696
|
-
const body =
|
|
1841
|
+
const body = renderBody(element, config);
|
|
1697
1842
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
1698
|
-
const displayMode = element
|
|
1843
|
+
const displayMode = resolveDisplayMode(element, mergedConfig);
|
|
1699
1844
|
const { css, js, tags } = extractHeadFromTree(element, {
|
|
1700
1845
|
displayMode,
|
|
1701
1846
|
headConfig: mergedConfig.headConfig
|