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 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.0** — export uses **`rows[]`** + **`_reactEmailStudio`** (LMS shape); palette blocks add as root sections; section reorder via drag handle or inspector.
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 framework setup (Next.js client/SSR), props tables, and troubleshooting, see **[TUTORIAL.md](./TUTORIAL.md)**.
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 content = typeof p.content === "string" ? p.content : "";
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
  );