better-react-email-editor 0.1.4 → 0.1.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.
- package/README.md +400 -395
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1256 -1052
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1724 -1643
- package/dist/types/components/BetterEmailEditor.d.ts +2 -0
- package/dist/types/components/BodyTab.d.ts +1 -1
- package/dist/types/components/Canvas.d.ts +3 -1
- package/dist/types/components/EmailHeader.d.ts +4 -0
- package/dist/types/components/fields.d.ts +9 -0
- package/dist/types/context.d.ts +7 -0
- package/dist/types/core/email-font.d.ts +5 -0
- package/dist/types/core/fonts.d.ts +13 -0
- package/dist/types/i18n/en.d.ts +10 -0
- package/dist/types/i18n/index.d.ts +20 -0
- package/dist/types/index.d.ts +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,395 +1,400 @@
|
|
|
1
|
-
<h1 align="center">Better React Email Editor</h1>
|
|
2
|
-
|
|
3
|
-
<p align="center">Open-source drag & drop email builder for React, built on <a href="https://www.npmjs.com/package/@react-email/editor">@react-email/editor</a>.</p>
|
|
4
|
-
|
|
5
|
-
<p align="center">
|
|
6
|
-
<a href="https://www.npmjs.com/package/better-react-email-editor"><img src="https://img.shields.io/npm/v/better-react-email-editor?color=0a7ea4" alt="npm version"></a>
|
|
7
|
-
<a href="./LICENSE"><img src="https://img.shields.io/npm/l/better-react-email-editor?color=4c9a2a" alt="MIT license"></a>
|
|
8
|
-
<a href="https://www.npmjs.com/package/better-react-email-editor"><img src="https://img.shields.io/npm/dm/better-react-email-editor?color=4c9a2a" alt="npm downloads"></a>
|
|
9
|
-
<a href="https://github.com/Gyom03/Better-react-email-editor"><img src="https://img.shields.io/github/stars/Gyom03/Better-react-email-editor?color=0a7ea4" alt="GitHub stars"></a>
|
|
10
|
-
</p>
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## Introduction
|
|
15
|
-
|
|
16
|
-
Add a visual email builder to your React app. Your users drag blocks into a layout, edit text in place and adjust styles from a side panel. Your app gets the email back as JSON, to save and reload, and as email-ready HTML (table layout, inline styles), to send.
|
|
17
|
-
|
|
18
|
-
It works like [Unlayer](https://unlayer.com), but it is open source and renders directly in your page: no iframe, no hosted service. Every part of the interface is a React component that you can restyle, replace or rearrange.
|
|
19
|
-
|
|
20
|
-
[](./docs/demo.mp4)
|
|
21
|
-
|
|
22
|
-
## Features
|
|
23
|
-
|
|
24
|
-
- **Drag & drop**: layouts of 1 to 4 columns and 12 content blocks (heading, text, button, image, divider, spacer, social links, menu, list, quote, HTML, card), plus prebuilt rows (header, hero, products, features, testimonial, footer).
|
|
25
|
-
- **Canvas**: hover and selection frames, a move handle, a toolbar (select parent, duplicate, delete) and keyboard shortcuts (`Esc` selects the parent, `Delete` removes the block).
|
|
26
|
-
- **Layers panel**: the email as a tree. You can select, reorder and nest blocks by drag & drop, with keyboard navigation.
|
|
27
|
-
- **Properties panel**: one panel per block type (typography, spacing, background, border, links, column widths, social networks, HTML…) and a breadcrumb of the hierarchy.
|
|
28
|
-
- **Inline text editing**: floating menu and `/` commands.
|
|
29
|
-
- **Copy / paste** of blocks and text, and undo / redo.
|
|
30
|
-
- **Export**: one button opens the rendered email: desktop and mobile preview, HTML (copy or download) and plain text.
|
|
31
|
-
- **Modular**: replace, restyle or rearrange any part, and add your own blocks, palette items, templates and properties panels.
|
|
32
|
-
- **i18n**: English and French are built in, and you can override any string.
|
|
33
|
-
|
|
34
|
-
## Installation
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npm install better-react-email-editor @react-email/editor react-email \
|
|
38
|
-
@tiptap/core @tiptap/pm @tiptap/react @tiptap/html @tiptap/extension-placeholder
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
`react` and `react-dom` (18 or 19), `@react-email/editor`, `react-email` and the `@tiptap/*` packages are peer dependencies. Your app and the editor must share one copy of each.
|
|
42
|
-
|
|
43
|
-
## Quick start
|
|
44
|
-
|
|
45
|
-
```tsx
|
|
46
|
-
import { BetterEmailEditor } from 'better-react-email-editor';
|
|
47
|
-
import '@react-email/editor/themes/default.css';
|
|
48
|
-
import 'better-react-email-editor/styles.css';
|
|
49
|
-
|
|
50
|
-
export function NewsletterEditor() {
|
|
51
|
-
return (
|
|
52
|
-
<div style={{ height: '100vh' }}>
|
|
53
|
-
<BetterEmailEditor
|
|
54
|
-
locale="en" // or "fr"
|
|
55
|
-
onChange={(value) => save(value)} // { doc, settings }
|
|
56
|
-
uploadImage={async (file) => ({ url: await uploadToS3(file) })}
|
|
57
|
-
/>
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
The editor fills its parent, so give the parent a height.
|
|
64
|
-
|
|
65
|
-
### Loading, saving and exporting
|
|
66
|
-
|
|
67
|
-
```tsx
|
|
68
|
-
const ref = useRef<EmailEditorHandle>(null);
|
|
69
|
-
|
|
70
|
-
<BetterEmailEditor ref={ref} defaultValue={savedValue} onChange={setDraft} />;
|
|
71
|
-
|
|
72
|
-
const value = ref.current.getValue(); // { doc, settings } — store it as JSON
|
|
73
|
-
ref.current.setValue(otherValue); // replace the content
|
|
74
|
-
const { html, text } = await ref.current.exportEmail(); // ready-to-send email
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
| `EmailEditorHandle` | Description |
|
|
78
|
-
| --- | --- |
|
|
79
|
-
| `editor` | The underlying Tiptap `Editor`. |
|
|
80
|
-
| `getValue()` / `setValue(value)` | Read or replace `{ doc, settings }`. `setValue` also accepts a bare Tiptap document. |
|
|
81
|
-
| `getJSON()` | The Tiptap document only. |
|
|
82
|
-
| `exportEmail()` | `Promise<{ html, text, unformattedHtml }>`, including the preview text. |
|
|
83
|
-
| `getHtml()` / `getText()` | Shortcuts for `exportEmail()`. |
|
|
84
|
-
| `insertBlocks(json[])` | Appends blocks at the end of the email. |
|
|
85
|
-
|
|
86
|
-
`
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
|
196
|
-
|
|
|
197
|
-
| `--bree-
|
|
198
|
-
| `--bree-
|
|
199
|
-
| `--bree-
|
|
200
|
-
| `--bree-
|
|
201
|
-
| `--bree-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
<BetterEmailEditor locale="
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
-
|
|
389
|
-
- `
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
1
|
+
<h1 align="center">Better React Email Editor</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">Open-source drag & drop email builder for React, built on <a href="https://www.npmjs.com/package/@react-email/editor">@react-email/editor</a>.</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/better-react-email-editor"><img src="https://img.shields.io/npm/v/better-react-email-editor?color=0a7ea4" alt="npm version"></a>
|
|
7
|
+
<a href="./LICENSE"><img src="https://img.shields.io/npm/l/better-react-email-editor?color=4c9a2a" alt="MIT license"></a>
|
|
8
|
+
<a href="https://www.npmjs.com/package/better-react-email-editor"><img src="https://img.shields.io/npm/dm/better-react-email-editor?color=4c9a2a" alt="npm downloads"></a>
|
|
9
|
+
<a href="https://github.com/Gyom03/Better-react-email-editor"><img src="https://img.shields.io/github/stars/Gyom03/Better-react-email-editor?color=0a7ea4" alt="GitHub stars"></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Introduction
|
|
15
|
+
|
|
16
|
+
Add a visual email builder to your React app. Your users drag blocks into a layout, edit text in place and adjust styles from a side panel. Your app gets the email back as JSON, to save and reload, and as email-ready HTML (table layout, inline styles), to send.
|
|
17
|
+
|
|
18
|
+
It works like [Unlayer](https://unlayer.com), but it is open source and renders directly in your page: no iframe, no hosted service. Every part of the interface is a React component that you can restyle, replace or rearrange.
|
|
19
|
+
|
|
20
|
+
[](./docs/demo.mp4)
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- **Drag & drop**: layouts of 1 to 4 columns and 12 content blocks (heading, text, button, image, divider, spacer, social links, menu, list, quote, HTML, card), plus prebuilt rows (header, hero, products, features, testimonial, footer).
|
|
25
|
+
- **Canvas**: hover and selection frames, a move handle, a toolbar (select parent, duplicate, delete) and keyboard shortcuts (`Esc` selects the parent, `Delete` removes the block).
|
|
26
|
+
- **Layers panel**: the email as a tree. You can select, reorder and nest blocks by drag & drop, with keyboard navigation.
|
|
27
|
+
- **Properties panel**: one panel per block type (typography, spacing, background, border, links, column widths, social networks, HTML…) and a breadcrumb of the hierarchy.
|
|
28
|
+
- **Inline text editing**: floating menu and `/` commands.
|
|
29
|
+
- **Copy / paste** of blocks and text, and undo / redo.
|
|
30
|
+
- **Export**: one button opens the rendered email: desktop and mobile preview, HTML (copy or download) and plain text.
|
|
31
|
+
- **Modular**: replace, restyle or rearrange any part, and add your own blocks, palette items, templates and properties panels.
|
|
32
|
+
- **i18n**: English and French are built in, and you can override any string.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install better-react-email-editor @react-email/editor react-email \
|
|
38
|
+
@tiptap/core @tiptap/pm @tiptap/react @tiptap/html @tiptap/extension-placeholder
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`react` and `react-dom` (18 or 19), `@react-email/editor`, `react-email` and the `@tiptap/*` packages are peer dependencies. Your app and the editor must share one copy of each.
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { BetterEmailEditor } from 'better-react-email-editor';
|
|
47
|
+
import '@react-email/editor/themes/default.css';
|
|
48
|
+
import 'better-react-email-editor/styles.css';
|
|
49
|
+
|
|
50
|
+
export function NewsletterEditor() {
|
|
51
|
+
return (
|
|
52
|
+
<div style={{ height: '100vh' }}>
|
|
53
|
+
<BetterEmailEditor
|
|
54
|
+
locale="en" // or "fr"
|
|
55
|
+
onChange={(value) => save(value)} // { doc, settings }
|
|
56
|
+
uploadImage={async (file) => ({ url: await uploadToS3(file) })}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The editor fills its parent, so give the parent a height.
|
|
64
|
+
|
|
65
|
+
### Loading, saving and exporting
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
const ref = useRef<EmailEditorHandle>(null);
|
|
69
|
+
|
|
70
|
+
<BetterEmailEditor ref={ref} defaultValue={savedValue} onChange={setDraft} />;
|
|
71
|
+
|
|
72
|
+
const value = ref.current.getValue(); // { doc, settings } — store it as JSON
|
|
73
|
+
ref.current.setValue(otherValue); // replace the content
|
|
74
|
+
const { html, text } = await ref.current.exportEmail(); // ready-to-send email
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| `EmailEditorHandle` | Description |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `editor` | The underlying Tiptap `Editor`. |
|
|
80
|
+
| `getValue()` / `setValue(value)` | Read or replace `{ doc, settings }`. `setValue` also accepts a bare Tiptap document. |
|
|
81
|
+
| `getJSON()` | The Tiptap document only. |
|
|
82
|
+
| `exportEmail()` | `Promise<{ html, text, unformattedHtml }>`, including the preview text. |
|
|
83
|
+
| `getHtml()` / `getText()` | Shortcuts for `exportEmail()`. |
|
|
84
|
+
| `insertBlocks(json[])` | Appends blocks at the end of the email. |
|
|
85
|
+
|
|
86
|
+
`settings` holds `subject` and `previewText`. The preview text is rendered into the HTML; the subject is not, so pass it to your email provider.
|
|
87
|
+
|
|
88
|
+
`defaultValue` is read on mount. The editor is uncontrolled, because re-applying a document on every keystroke would reset the cursor. Use `setValue` to replace the content later.
|
|
89
|
+
|
|
90
|
+
## Props
|
|
91
|
+
|
|
92
|
+
`BetterEmailEditor` accepts every `EditorRoot` prop, plus the layout props listed further down.
|
|
93
|
+
|
|
94
|
+
| Prop | Type | Default | Description |
|
|
95
|
+
| --- | --- | --- | --- |
|
|
96
|
+
| `defaultValue` | `EmailValue \| JSONContent` | empty email | Initial content, read on mount. |
|
|
97
|
+
| `onChange` | `(value: EmailValue) => void` | | Called on every document or settings change. Debounce it yourself if you save to a server. |
|
|
98
|
+
| `onReady` | `(handle) => void` | | Called once the editor exists. |
|
|
99
|
+
| `locale` | `'en' \| 'fr' \| string` | `'en'` | UI language. |
|
|
100
|
+
| `messages` | `PartialMessages` | | Overrides any UI string, deep-merged over the locale. |
|
|
101
|
+
| `uploadImage` | `(file) => Promise<{ url }>` | inline data URL | Stores uploaded or pasted images. |
|
|
102
|
+
| `nodes` | `NodeDefinition[]` | `defaultNodes` | Block types: label, icon, properties panel, layers preview, extensions. |
|
|
103
|
+
| `palette` | `PaletteItem[]` | `defaultPalette` | Tiles of the **Content** tab. |
|
|
104
|
+
| `paletteGroups` | `PaletteGroup[]` | Layout, Content | Groups of tiles. |
|
|
105
|
+
| `templates` | `TemplateItem[]` | `defaultTemplates` | Prebuilt rows of the **Blocks** tab. |
|
|
106
|
+
| `fonts` | `FontOption[]` | `defaultFonts` | Fonts of the font pickers (`{ label, value }`, `value` being a CSS `font-family` stack). Defaults to Arial, Verdana, Tahoma, Georgia and Times New Roman, safe in every email client. |
|
|
107
|
+
| `starterDocument` | `(i18n) => JSONContent` | demo email | Loaded by the top bar's **Template** button. |
|
|
108
|
+
| `extensions` | `Extensions` | | Extra Tiptap extensions, read on mount. |
|
|
109
|
+
| `theme` | `EditorThemeInput` | `defaultTheme` | Email theme for new documents, read on mount. |
|
|
110
|
+
| `editable` | `boolean` | `true` | |
|
|
111
|
+
| `device` / `defaultDevice` / `onDeviceChange` | `'desktop' \| 'mobile'` | `'desktop'` | Canvas width, controlled or not. |
|
|
112
|
+
| `layersOpen` / `defaultLayersOpen` / `onLayersOpenChange` | `boolean` | `true` | Layers panel visibility, controlled or not. |
|
|
113
|
+
| `className` / `style` | | | Applied to the root element. |
|
|
114
|
+
|
|
115
|
+
Layout props, `BetterEmailEditor` only:
|
|
116
|
+
|
|
117
|
+
| Prop | Description |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| `showTopBar`, `showLayers`, `showSidebar` | Hide a part (all `true` by default). |
|
|
120
|
+
| `showEmailHeader` | Show a card with the subject and preview text above the email (`false` by default). |
|
|
121
|
+
| `slotProps` | Props for each part, e.g. `{ sidebar: { className: 'my-sidebar' }, topBar: { actions: <MyButton /> } }`. |
|
|
122
|
+
| `components` | Replace a part entirely: `{ TopBar, LayersPanel, Canvas, Sidebar }`. |
|
|
123
|
+
|
|
124
|
+
## Components
|
|
125
|
+
|
|
126
|
+
Every component below accepts `className` and `style`, and must be rendered inside `EditorRoot` (or `BetterEmailEditor`).
|
|
127
|
+
|
|
128
|
+
| Component | What it is |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `EditorRoot` | Provider: creates the editor and shares it. Renders a `div.bree`. |
|
|
131
|
+
| `Canvas` | The editable email, with its overlay (frames, handle, toolbar, drop indicator). Props: `overlay`, `bubbleMenu`, `slashCommands`, `header` (subject and preview text card above the email). |
|
|
132
|
+
| `EmailHeader` | The subject and preview text card on its own, to place anywhere. |
|
|
133
|
+
| `TopBar` | Top bar: desktop / mobile toggle and the export button. Props: `brand` (left side, empty by default), `actions` (extra controls before the export button), or `children` to replace its content. |
|
|
134
|
+
| `DeviceToggle`, `ExportHtmlButton` | The default top bar controls. |
|
|
135
|
+
| `LayersToggle`, `UndoRedo`, `TemplateButton`, `ImportButton`, `ExportJsonButton`, `PreviewButton` | Optional controls, not shown by default: add them through `actions` or your own bar. |
|
|
136
|
+
| `LayersPanel` | Layers tree. Props: `title`, `onClose` to show a close button, and `expandCollapseButtons={false}` to hide the expand / collapse all buttons. |
|
|
137
|
+
| `Sidebar` | Right panel. Switches between three screens; override any of them with `renderDocument`, `renderNode` or `renderText`. |
|
|
138
|
+
| `ContentPanel` | Screen shown when nothing is selected: tabs. Prop `tabs` picks or reorders `'content' \| 'blocks' \| 'body'` and accepts your own tabs. |
|
|
139
|
+
| `ContentTab` / `PaletteTile` | Palette of draggable tiles. |
|
|
140
|
+
| `BlocksTab` / `TemplateCard` | Prebuilt rows. |
|
|
141
|
+
| `BodyTab` | Email-wide settings: background, width, default font and colors, preview text. |
|
|
142
|
+
| `PropertiesPanel` | Properties shell: header, close button, breadcrumb. |
|
|
143
|
+
| `BlockProperties` | Properties of the selected block, using the inspector registered for its type. |
|
|
144
|
+
| `TextProperties` | Properties of a text selection. |
|
|
145
|
+
| `PreviewDialog` | Preview and export modal. |
|
|
146
|
+
| `ImageInspector`, `ButtonInspector`, `HeadingInspector`, `SocialLinksInspector`, `ColumnsInspector`, `SpacerInspector`, `HtmlInspector`, `DividerInspector`, `ContainerInspector`, `DefaultInspector` | Properties panel of each block type. |
|
|
147
|
+
| `TypographyGroup`, `SpacingGroup`, `BoxGroup` | Reusable groups of controls. |
|
|
148
|
+
| `Group`, `Field`, `TextInput`, `TextArea`, `NumberInput`, `RangeInput`, `ColorInput`, `FontInput`, `Select`, `Segmented`, `AlignInput`, `PaddingInput` | Form controls, to build your own panels. |
|
|
149
|
+
|
|
150
|
+
Hooks:
|
|
151
|
+
|
|
152
|
+
- `useEmailEditor()` gives the editor, `t` (messages), `locale`, registries, `device`, `layersOpen`, `settings`, `handle`…
|
|
153
|
+
- `useI18n()` gives `{ locale, t }`.
|
|
154
|
+
- `useDraggableBlock(label, content)` turns any element into a source of blocks: drag it onto the canvas or click it.
|
|
155
|
+
- `useDeselect()` clears the selection.
|
|
156
|
+
|
|
157
|
+
### Custom layout
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
import { EditorRoot, Canvas, Sidebar, UndoRedo, DeviceToggle, ExportHtmlButton } from 'better-react-email-editor';
|
|
161
|
+
|
|
162
|
+
<EditorRoot locale="fr" defaultValue={value} onChange={save} className="my-editor">
|
|
163
|
+
<div style={{ display: 'flex', height: '100%' }}>
|
|
164
|
+
<main style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
|
165
|
+
<Canvas style={{ flex: 1 }} />
|
|
166
|
+
<footer>
|
|
167
|
+
<UndoRedo />
|
|
168
|
+
<DeviceToggle />
|
|
169
|
+
<ExportHtmlButton />
|
|
170
|
+
</footer>
|
|
171
|
+
</main>
|
|
172
|
+
<Sidebar style={{ width: 320 }} />
|
|
173
|
+
</div>
|
|
174
|
+
</EditorRoot>;
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+

|
|
178
|
+
|
|
179
|
+
The playground contains this example: `npm run dev`, then open `/?example=custom`.
|
|
180
|
+
|
|
181
|
+
## Styling
|
|
182
|
+
|
|
183
|
+
All classes are prefixed with `bree-`, and the stylesheet never touches global elements. Restyle a part through its `className` or `style`, or theme the whole editor with CSS custom properties:
|
|
184
|
+
|
|
185
|
+
```css
|
|
186
|
+
.my-editor {
|
|
187
|
+
--bree-accent: #7c3aed;
|
|
188
|
+
--bree-accent-hover: #6d28d9;
|
|
189
|
+
--bree-accent-soft: #f5f3ff;
|
|
190
|
+
--bree-font: 'Inter', sans-serif;
|
|
191
|
+
--bree-sidebar-width: 320px;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
| Variable | Default |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| `--bree-bg`, `--bree-bg-subtle` | `#ffffff`, `#f8fafc` |
|
|
198
|
+
| `--bree-border`, `--bree-border-strong` | `#e5e7eb`, `#d1d5db` |
|
|
199
|
+
| `--bree-text`, `--bree-text-secondary`, `--bree-muted` | `#111827`, `#334155`, `#6b7280` |
|
|
200
|
+
| `--bree-accent`, `--bree-accent-hover`, `--bree-accent-soft` | `#2563eb`, `#1d4ed8`, `#eff6ff` |
|
|
201
|
+
| `--bree-danger` | `#dc2626` |
|
|
202
|
+
| `--bree-radius` | `8px` |
|
|
203
|
+
| `--bree-frame-gap` (space between a block and its hover / selection frame) | `4px` |
|
|
204
|
+
| `--bree-canvas-bg` | `#eef1f5` |
|
|
205
|
+
| `--bree-topbar-height`, `--bree-sidebar-width`, `--bree-layers-width` | `56px`, `360px`, `270px` |
|
|
206
|
+
| `--bree-font` | Inter, system-ui… |
|
|
207
|
+
|
|
208
|
+
The default layout is a CSS grid on `.bree-layout`. The color picker popover is rendered in a portal; it carries the `bree` class, so the variables apply to it too. Its `popoverClassName` prop adds your own class.
|
|
209
|
+
|
|
210
|
+
## Languages
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
<BetterEmailEditor locale="fr" />
|
|
214
|
+
|
|
215
|
+
// Override a few strings
|
|
216
|
+
<BetterEmailEditor locale="en" messages={{ topBar: { exportHtml: 'Export' }, palette: { card: 'Callout' } }} />
|
|
217
|
+
|
|
218
|
+
// Add a language: provide every string, starting from `en`
|
|
219
|
+
import { en, type Messages } from 'better-react-email-editor';
|
|
220
|
+
const de: Messages = { ...en, topBar: { ...en.topBar, preview: 'Vorschau' /* … */ } };
|
|
221
|
+
<BetterEmailEditor locale="de" messages={de} />
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`en` and `fr` are exported. `messages` covers the UI, the default content inserted by the palette and templates, the placeholders and the `/` commands.
|
|
225
|
+
|
|
226
|
+
## Extending
|
|
227
|
+
|
|
228
|
+
### A palette item
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
import { content, defaultPalette, type PaletteItem } from 'better-react-email-editor';
|
|
232
|
+
|
|
233
|
+
const promo: PaletteItem = {
|
|
234
|
+
id: 'promo',
|
|
235
|
+
group: 'brand', // new group, or 'layout' / 'content'
|
|
236
|
+
label: ({ locale }) => (locale === 'fr' ? 'Bandeau promo' : 'Promo banner'),
|
|
237
|
+
icon: <PercentIcon />,
|
|
238
|
+
content: () => [content.section('padding:20px;background-color:#fef3c7', content.heading(2, '20% off'))],
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
<BetterEmailEditor
|
|
242
|
+
palette={[promo, ...defaultPalette]}
|
|
243
|
+
paletteGroups={[{ id: 'brand', title: 'My brand' }, ...defaultPaletteGroups]}
|
|
244
|
+
/>;
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`content` provides helpers to build the document JSON: `paragraph`, `heading`, `button`, `image`, `columns`, `column`, `sized`, `section`, `padding`, `link`, `text`, `doc` and `placeholderImage`.
|
|
248
|
+
|
|
249
|
+
### A template (prebuilt row)
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
const signature: TemplateItem = {
|
|
253
|
+
id: 'signature',
|
|
254
|
+
label: 'Signature',
|
|
255
|
+
description: 'Name, role and logo',
|
|
256
|
+
content: () => [content.paragraph('Jane Doe — CEO')],
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
<BetterEmailEditor templates={[...defaultTemplates, signature]} />;
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### A properties panel
|
|
263
|
+
|
|
264
|
+
Replace the panel of an existing block, or give one to a new block:
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
import { defaultNodes, Group, Field, TextInput, SpacingGroup, type InspectorProps } from 'better-react-email-editor';
|
|
268
|
+
|
|
269
|
+
function MyButtonInspector({ ctx, className, style }: InspectorProps) {
|
|
270
|
+
return (
|
|
271
|
+
<div className={className} style={style}>
|
|
272
|
+
<Group title="Link">
|
|
273
|
+
<Field label="URL" stacked>
|
|
274
|
+
<TextInput value={String(ctx.getAttr('href') ?? '')} onChange={(v) => ctx.setAttr('href', v)} />
|
|
275
|
+
</Field>
|
|
276
|
+
</Group>
|
|
277
|
+
<SpacingGroup ctx={ctx} />
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const nodes = defaultNodes.map((node) => (node.type === 'button' ? { ...node, inspector: MyButtonInspector } : node));
|
|
283
|
+
|
|
284
|
+
<BetterEmailEditor nodes={nodes} />;
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`ctx` is the `@react-email/editor` inspector context: `getAttr`, `setAttr`, `getStyle`, `setStyle`, `batchSetStyle`, `presetColors`, `nodeType` and `nodePos`.
|
|
288
|
+
|
|
289
|
+
### A new block type
|
|
290
|
+
|
|
291
|
+
Create the node with `EmailNode.create` from `@react-email/editor/core`. It needs an editor rendering and an email rendering (`renderToReactEmail`). Then register it:
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
const countdownNode: NodeDefinition = {
|
|
295
|
+
type: 'countdown',
|
|
296
|
+
label: ({ locale }) => (locale === 'fr' ? 'Compte à rebours' : 'Countdown'),
|
|
297
|
+
icon: TimerIcon,
|
|
298
|
+
inspector: CountdownInspector,
|
|
299
|
+
preview: (node) => node.attrs.date,
|
|
300
|
+
extensions: [Countdown], // your EmailNode
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
<BetterEmailEditor nodes={[...defaultNodes, countdownNode]} palette={[...defaultPalette, countdownTile]} />;
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
See `src/nodes/custom-nodes.tsx` for complete examples (spacer, social links, raw HTML).
|
|
307
|
+
|
|
308
|
+
### Adding or removing controls
|
|
309
|
+
|
|
310
|
+
The default top bar only has the device toggle and the export button. Add the optional controls back, or remove parts:
|
|
311
|
+
|
|
312
|
+
```tsx
|
|
313
|
+
import { UndoRedo, PreviewButton, ExportJsonButton, LayersToggle } from 'better-react-email-editor';
|
|
314
|
+
|
|
315
|
+
<BetterEmailEditor
|
|
316
|
+
slotProps={{
|
|
317
|
+
topBar: { brand: <img src="/logo.svg" alt="Acme" height={28} />, actions: <><UndoRedo /><ExportJsonButton /><PreviewButton /></> },
|
|
318
|
+
}}
|
|
319
|
+
/>
|
|
320
|
+
|
|
321
|
+
<BetterEmailEditor
|
|
322
|
+
palette={defaultPalette.filter((item) => item.id !== 'html')}
|
|
323
|
+
showLayers={false}
|
|
324
|
+
slotProps={{ sidebar: { renderDocument: (ctx) => <ContentPanel ctx={ctx} tabs={['content', 'body']} /> } }}
|
|
325
|
+
/>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
To let users hide and show the layers panel, add a `LayersToggle` and give the panel a close button:
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
const [layersOpen, setLayersOpen] = useState(true);
|
|
332
|
+
|
|
333
|
+
<BetterEmailEditor
|
|
334
|
+
layersOpen={layersOpen}
|
|
335
|
+
onLayersOpenChange={setLayersOpen}
|
|
336
|
+
slotProps={{ topBar: { actions: <LayersToggle /> }, layersPanel: { onClose: () => setLayersOpen(false) } }}
|
|
337
|
+
/>;
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Headless core
|
|
341
|
+
|
|
342
|
+
The drag & drop engine has no UI and can be reused: `findDropTarget`, `applyDrop`, `appendBlocks`, `unitAt`, `selectedUnit`, `selectUnit`, `duplicateUnit`, `deleteUnit`, `parentUnit`, `BLOCK_CONTAINERS` and `COLUMN_PARENTS`.
|
|
343
|
+
|
|
344
|
+
How it fits into ProseMirror:
|
|
345
|
+
|
|
346
|
+
- The model is the package's: `container` (the email) holds the blocks. The drop zones are `container`, `section` and `columnsColumn`. The rows `twoColumns`, `threeColumns` and `fourColumns` hold columns.
|
|
347
|
+
- `findDropTarget()` walks down from the container to the deepest zone under the pointer, then picks the gap between two children. The schema validates every drop (`node.canReplace`), so a block never lands somewhere invalid.
|
|
348
|
+
- `dragover` and `drop` are captured on the canvas before ProseMirror sees them. Native text drags stay with ProseMirror.
|
|
349
|
+
- Insertions and moves are plain transactions, so undo / redo work with no extra code.
|
|
350
|
+
|
|
351
|
+
## Development
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
npm install
|
|
355
|
+
npm run dev # playground on http://localhost:5173 (?lang=fr, ?example=custom)
|
|
356
|
+
npm run build # library → dist/ (ESM, CJS, types, styles.css)
|
|
357
|
+
npm run typecheck
|
|
358
|
+
npm run lint
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
src/
|
|
363
|
+
index.ts public API
|
|
364
|
+
context.tsx EditorRoot, hooks, imperative handle
|
|
365
|
+
components/ one file per screen: TopBar, LayersPanel, Canvas (+ CanvasOverlay),
|
|
366
|
+
Sidebar, ContentPanel, ContentTab, BlocksTab, BodyTab, PropertiesPanel,
|
|
367
|
+
BlockProperties, TextProperties, PreviewDialog, fields, inspectors/
|
|
368
|
+
registry/ default nodes, palette, templates and starter email
|
|
369
|
+
nodes/ custom email nodes (spacer, social links, HTML)
|
|
370
|
+
core/ headless drag & drop engine, paste handling, JSON helpers
|
|
371
|
+
i18n/ en.ts, fr.ts
|
|
372
|
+
styles.css
|
|
373
|
+
playground/ demo app consuming the library from its sources
|
|
374
|
+
scripts/ Playwright scenarios
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
The scenarios in `scripts/` drive the playground in French with Playwright. They use the Edge installed locally, through `playwright-core`.
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
npx vite --port 5179 &
|
|
381
|
+
node scripts/e2e.mjs # also e2e2 … e2e10, e2e-custom, check-padding; screenshots in ./screenshots
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Known limitations
|
|
385
|
+
|
|
386
|
+
These are workarounds for `@react-email/editor`:
|
|
387
|
+
|
|
388
|
+
- The package has no block drag & drop; this library adds it.
|
|
389
|
+
- The package's `Button.parseHTML` ignores the `data-href` and alignment it writes, so `EmailButton` (in `core/paste.ts`) reads them back. Without it, a copy-pasted button loses its link.
|
|
390
|
+
- Pasting with a block selected inserts **after** the block instead of replacing it.
|
|
391
|
+
- `Divider` rejects insertions while a divider is selected. The selection is released before a drop or a duplication.
|
|
392
|
+
- The package's floating menus for buttons and images duplicate the properties panel, so CSS hides them.
|
|
393
|
+
- `TrailingNode` leaves an empty paragraph after non-text blocks. It stays collapsed until the caret enters it.
|
|
394
|
+
- `EmailEditor` always places its editing area before its children. `EditorRoot` creates the editor itself, so `Canvas` can sit anywhere in your layout; it reproduces the package's paste sanitizer, which is not exported.
|
|
395
|
+
|
|
396
|
+
Further ideas: host PNG social icons (Gmail does not display SVG), add media queries to stack columns on mobile in the exported HTML, and add more blocks (video thumbnail, countdown).
|
|
397
|
+
|
|
398
|
+
## License
|
|
399
|
+
|
|
400
|
+
[MIT](./LICENSE)
|