react-email-studio 3.8.1 → 3.8.2
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/CHANGELOG.md +10 -0
- package/README.md +3 -2
- package/RELEASE.md +137 -0
- package/dist/index.cjs +7 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.8.2] - 2026-05-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added **`RELEASE.md`** as a single-file release tutorial and included it in the published npm package.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Text block newline handling** is now consistent in canvas preview and exported HTML: plain text line breaks render as `<br/>`, while HTML content remains passthrough.
|
|
17
|
+
|
|
8
18
|
## [3.8.0] - 2026-05-15
|
|
9
19
|
|
|
10
20
|
### Changed
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
## Release notes
|
|
9
9
|
|
|
10
|
-
**Latest: 3.8.
|
|
10
|
+
**Latest: 3.8.2** — includes a single-file **`RELEASE.md`** npm tutorial and fixes plain text newline rendering parity between canvas preview and exported HTML.
|
|
11
11
|
|
|
12
12
|
Version history and migration hints: **[CHANGELOG.md](./CHANGELOG.md)** (also included in the published npm tarball under `node_modules/react-email-studio/CHANGELOG.md`).
|
|
13
13
|
|
|
@@ -121,7 +121,8 @@ Implement **`onUpload`** so image/video uploads return URLs your recipients can
|
|
|
121
121
|
| `ReactEmailEditorOptions`, `JsonToHtmlOptions`, `EmailHtmlOptions` | Editor options and HTML generation options. |
|
|
122
122
|
| `EmailDocument`, `EmailDocumentSettings`, … | JSON schema types for stored designs. |
|
|
123
123
|
|
|
124
|
-
For
|
|
124
|
+
For a single-file release tutorial, see **[RELEASE.md](./RELEASE.md)**.
|
|
125
|
+
For extended framework setup (Next.js client/SSR), props tables, and troubleshooting, see **[TUTORIAL.md](./TUTORIAL.md)**.
|
|
125
126
|
|
|
126
127
|
---
|
|
127
128
|
|
package/RELEASE.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# react-email-studio release tutorial (single file)
|
|
2
|
+
|
|
3
|
+
Use this file as the one-stop guide for npm integration, save/load, HTML export, and API usage.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install react-email-studio
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Install peer dependencies in your app:
|
|
12
|
+
|
|
13
|
+
- `react`, `react-dom`
|
|
14
|
+
- `lucide-react`
|
|
15
|
+
- TipTap v3:
|
|
16
|
+
- `@tiptap/react`
|
|
17
|
+
- `@tiptap/core`
|
|
18
|
+
- `@tiptap/starter-kit`
|
|
19
|
+
- `@tiptap/extension-link`
|
|
20
|
+
- `@tiptap/extension-placeholder`
|
|
21
|
+
- `@tiptap/extension-text-align`
|
|
22
|
+
- `@tiptap/extension-text-style`
|
|
23
|
+
- `@tiptap/extension-underline`
|
|
24
|
+
|
|
25
|
+
## Minimal integration
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { useRef, useCallback } from "react";
|
|
29
|
+
import {
|
|
30
|
+
ReactEmailEditor,
|
|
31
|
+
type ReactEmailEditorRef,
|
|
32
|
+
jsonToHtml,
|
|
33
|
+
} from "react-email-studio";
|
|
34
|
+
|
|
35
|
+
export function MailStudioPage() {
|
|
36
|
+
const ref = useRef<ReactEmailEditorRef>(null);
|
|
37
|
+
|
|
38
|
+
const save = useCallback(() => {
|
|
39
|
+
ref.current?.exportJson((jsonString) => {
|
|
40
|
+
void fetch("/api/email-designs", {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "Content-Type": "application/json" },
|
|
43
|
+
body: JSON.stringify({ design: jsonString }),
|
|
44
|
+
});
|
|
45
|
+
}, true);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<button type="button" onClick={save}>Save design</button>
|
|
51
|
+
<ReactEmailEditor
|
|
52
|
+
ref={ref}
|
|
53
|
+
hideTemplates
|
|
54
|
+
onUpload={uploadImageAndReturnUrl}
|
|
55
|
+
onReady={(api) => {
|
|
56
|
+
// api.loadJson(savedDesignJsonOrObject);
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function uploadImageAndReturnUrl(file: File): Promise<string> {
|
|
64
|
+
const body = new FormData();
|
|
65
|
+
body.append("file", file);
|
|
66
|
+
const res = await fetch("/api/upload", { method: "POST", body });
|
|
67
|
+
const { url } = await res.json();
|
|
68
|
+
return url as string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function htmlFromStoredDesign(designJson: string): string {
|
|
72
|
+
return jsonToHtml(designJson);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Important: `onUpload` must return public HTTPS URLs that recipients can access.
|
|
77
|
+
|
|
78
|
+
## Next.js note
|
|
79
|
+
|
|
80
|
+
The editor is client-side. Use:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
"use client";
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If needed:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import dynamic from "next/dynamic";
|
|
90
|
+
const ReactEmailEditor = dynamic(
|
|
91
|
+
() => import("react-email-studio").then((m) => m.ReactEmailEditor),
|
|
92
|
+
{ ssr: false },
|
|
93
|
+
);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Core API
|
|
97
|
+
|
|
98
|
+
- `ref.loadJson(input)`:
|
|
99
|
+
- Accepts JSON string or object.
|
|
100
|
+
- Replaces editor content.
|
|
101
|
+
- `ref.exportJson(cb, pretty?)`:
|
|
102
|
+
- Returns canonical `email_document` JSON.
|
|
103
|
+
- `jsonToHtml(design, opts?)`:
|
|
104
|
+
- Converts design JSON to full HTML document.
|
|
105
|
+
- `htmlToJson(html, pretty?)`:
|
|
106
|
+
- Converts pasted HTML to `email_document` JSON.
|
|
107
|
+
|
|
108
|
+
## Exported helpers
|
|
109
|
+
|
|
110
|
+
- `htmlToEmailDesignTemplate`
|
|
111
|
+
- `extractHtmlForDesign`
|
|
112
|
+
- `canonicalizeEmailDocument`
|
|
113
|
+
- `utf8ToBase64`, `base64ToUtf8`
|
|
114
|
+
- `EmailPreviewModal`, `emailPreviewDevices`
|
|
115
|
+
|
|
116
|
+
## JSON storage shape
|
|
117
|
+
|
|
118
|
+
Persist `exportJson` output as your source of truth:
|
|
119
|
+
|
|
120
|
+
- `type: "email_document"`
|
|
121
|
+
- `settings`
|
|
122
|
+
- `rows[]` (preferred export shape)
|
|
123
|
+
- row `layout`, `styles`, `columns[].blocks[]`
|
|
124
|
+
- `_reactEmailStudio` metadata for editor round-trip
|
|
125
|
+
|
|
126
|
+
Import supports legacy `blocks[]` and older styles, but export writes `rows[]`.
|
|
127
|
+
|
|
128
|
+
## Troubleshooting
|
|
129
|
+
|
|
130
|
+
- Missing module errors: install all peers with compatible versions.
|
|
131
|
+
- Blank HTML from `jsonToHtml`: invalid/empty design input.
|
|
132
|
+
- Broken email images: `onUpload` URL is private, local, or expired.
|
|
133
|
+
- SSR errors: ensure client boundary (`"use client"` or dynamic `ssr: false`).
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -872,6 +872,11 @@ function escHtmlAttr(s) {
|
|
|
872
872
|
function escHtml(s) {
|
|
873
873
|
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
874
874
|
}
|
|
875
|
+
function textBlockContentToHtml(content) {
|
|
876
|
+
const raw = typeof content === "string" ? content : "";
|
|
877
|
+
if (/^\s*<[^>]+>/.test(raw)) return raw;
|
|
878
|
+
return escHtml(raw).replace(/\r?\n/g, "<br/>");
|
|
879
|
+
}
|
|
875
880
|
function utf8ToBase64(raw) {
|
|
876
881
|
const bytes = new TextEncoder().encode(raw);
|
|
877
882
|
let bin = "";
|
|
@@ -1212,8 +1217,7 @@ function blockToHtml(cb) {
|
|
|
1212
1217
|
case "text": {
|
|
1213
1218
|
const shell = emailSurfaceBgCss(p);
|
|
1214
1219
|
const inner = `font-size:${lenPx(p.fontSize)};color:${p.color};text-align:${p.align};font-weight:${p.fontWeight || 400};font-style:${p.italic ? "italic" : "normal"};text-decoration:${p.underline ? "underline" : "none"};line-height:${lh(p.lineHeight)};letter-spacing:${lenPx(p.letterSpacing)};font-family:${p.fontFamily || "Georgia,serif"}`;
|
|
1215
|
-
const
|
|
1216
|
-
const body = /^\s*<[^>]+>/.test(content) ? content : escHtml(content).replace(/\r?\n/g, "<br/>");
|
|
1220
|
+
const body = textBlockContentToHtml(p.content);
|
|
1217
1221
|
return `<div style="${pd(p.padding)};${marginCss()}${shell}"><div style="${inner}">${body}</div></div>`;
|
|
1218
1222
|
}
|
|
1219
1223
|
case "html": {
|
|
@@ -3052,7 +3056,7 @@ function ContentBlock({ block, selected, onClick, preview, C }) {
|
|
|
3052
3056
|
"div",
|
|
3053
3057
|
{
|
|
3054
3058
|
style: { fontSize: p.fontSize, color: p.color, textAlign: p.align, fontWeight: p.fontWeight || (p.bold ? 700 : 400), fontStyle: p.italic ? "italic" : "normal", textDecoration: p.underline ? "underline" : "none", lineHeight: p.lineHeight || 1.65, letterSpacing: `${p.letterSpacing || 0}px`, fontFamily: p.fontFamily || "Georgia,serif" },
|
|
3055
|
-
dangerouslySetInnerHTML: { __html: p.content }
|
|
3059
|
+
dangerouslySetInnerHTML: { __html: textBlockContentToHtml(p.content) }
|
|
3056
3060
|
}
|
|
3057
3061
|
)
|
|
3058
3062
|
);
|