best-email-builder 0.2.0 → 0.4.0

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/README.md +309 -12
  3. package/dist/builder.js +44 -0
  4. package/dist/builder.js.map +1 -0
  5. package/dist/chunk-AT73Y7JA.js +1427 -0
  6. package/dist/chunk-AT73Y7JA.js.map +1 -0
  7. package/dist/chunk-HHLOE6OZ.js +1682 -0
  8. package/dist/chunk-HHLOE6OZ.js.map +1 -0
  9. package/dist/chunk-JNHFZXDL.js +1776 -0
  10. package/dist/chunk-JNHFZXDL.js.map +1 -0
  11. package/dist/chunk-MY6WPDT4.js +6110 -0
  12. package/dist/chunk-MY6WPDT4.js.map +1 -0
  13. package/dist/chunk-NAEGU6PR.js +538 -0
  14. package/dist/chunk-NAEGU6PR.js.map +1 -0
  15. package/dist/chunk-TGDZ4NFI.js +114 -0
  16. package/dist/chunk-TGDZ4NFI.js.map +1 -0
  17. package/dist/index.js +124 -4866
  18. package/dist/index.js.map +1 -1
  19. package/dist/library.js +31 -0
  20. package/dist/library.js.map +1 -0
  21. package/dist/server.js +554 -210
  22. package/dist/server.js.map +1 -1
  23. package/dist/styles.css +1 -1
  24. package/dist/types/blocks/shared.d.ts +20 -0
  25. package/dist/types/builder.d.ts +8 -0
  26. package/dist/types/core/EmailEditor.d.ts +72 -19
  27. package/dist/types/core/constants.d.ts +1 -1
  28. package/dist/types/core/mjmlDefaults.d.ts +13 -0
  29. package/dist/types/core/scrollbars.d.ts +13 -0
  30. package/dist/types/defaultEditor.d.ts +11 -0
  31. package/dist/types/engine/NativeAdapter.d.ts +28 -0
  32. package/dist/types/engine/canvas/CanvasView.d.ts +88 -0
  33. package/dist/types/engine/canvas/canvasStyles.d.ts +3 -0
  34. package/dist/types/engine/canvas/icons.d.ts +14 -0
  35. package/dist/types/engine/canvas/morph.d.ts +21 -0
  36. package/dist/types/engine/canvasMjml.d.ts +30 -0
  37. package/dist/types/engine/mjml.d.ts +31 -0
  38. package/dist/types/engine/model.d.ts +130 -0
  39. package/dist/types/engine/placement.d.ts +69 -0
  40. package/dist/types/engine/richText.d.ts +15 -0
  41. package/dist/types/engine/unwrapModule.d.ts +29 -0
  42. package/dist/types/features/brand/BrandButton.d.ts +4 -0
  43. package/dist/types/features/brand/brand.d.ts +108 -0
  44. package/dist/types/features/brand/extract.d.ts +61 -0
  45. package/dist/types/features/brand/index.d.ts +2 -0
  46. package/dist/types/features/brand/messages.d.ts +13 -0
  47. package/dist/types/features/brand/useBrand.d.ts +22 -0
  48. package/dist/types/features/generate/GenerateButton.d.ts +3 -0
  49. package/dist/types/features/generate/RewriteText.d.ts +5 -0
  50. package/dist/types/features/generate/SubjectIdeas.d.ts +16 -0
  51. package/dist/types/features/generate/chip.d.ts +1 -0
  52. package/dist/types/features/generate/claude.d.ts +21 -0
  53. package/dist/types/features/generate/draft.d.ts +122 -0
  54. package/dist/types/features/generate/generator.d.ts +118 -0
  55. package/dist/types/features/generate/index.d.ts +2 -0
  56. package/dist/types/features/generate/prompt.d.ts +76 -0
  57. package/dist/types/features/generate/purposes.d.ts +48 -0
  58. package/dist/types/features/generate/rewrite.d.ts +83 -0
  59. package/dist/types/features/generate/subjects.d.ts +83 -0
  60. package/dist/types/features/preview/PreviewButton.d.ts +1 -1
  61. package/dist/types/features/preview/darkMode.d.ts +48 -0
  62. package/dist/types/features/templates/gallery.d.ts +7 -0
  63. package/dist/types/index.d.ts +14 -10
  64. package/dist/types/library.d.ts +5 -0
  65. package/dist/types/server/brand.d.ts +32 -0
  66. package/dist/types/server/brandImport.d.ts +93 -0
  67. package/dist/types/server/compileMjml.d.ts +2 -0
  68. package/dist/types/server/handler.d.ts +25 -0
  69. package/dist/types/server/index.d.ts +11 -0
  70. package/dist/types/storage/client.d.ts +79 -3
  71. package/dist/types/ui/EmailStudio.d.ts +42 -0
  72. package/dist/types/ui/fields.d.ts +2 -1
  73. package/dist/types/ui/library/TemplateLibrary.d.ts +41 -0
  74. package/dist/types/ui/primitives/button.d.ts +2 -2
  75. package/dist/types/ui/primitives/color-picker.d.ts +2 -1
  76. package/dist/types/ui/useLayoutWidth.d.ts +1 -1
  77. package/package.json +27 -4
  78. package/dist/chunk-MTALS5GC.js +0 -747
  79. package/dist/chunk-MTALS5GC.js.map +0 -1
  80. package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +0 -33
  81. package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -162
  82. package/dist/types/adapters/grapesjs/unwrapModule.d.ts +0 -18
package/CHANGELOG.md CHANGED
@@ -4,6 +4,192 @@ All notable changes are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [0.4.0] — 2026-10-15
10
+
11
+ ### Changed — the builder's own engine (breaking)
12
+ - **GrapesJS is gone.** The interface, the canvas, the drag and drop and the
13
+ text editing are the builder's own (`src/engine/`); the email is a tree of
14
+ MJML elements, and MJML alone turns it into HTML. The canvas shows the email
15
+ compiled by `mjml-browser` 5.4.1 (the same MJML version as the server): what
16
+ is seen is what is sent. The package lost GrapesJS and grapesjs-mjml from its
17
+ dependencies, and its stylesheet their CSS.
18
+ - Stored documents are written by the engine `beb-mjml` (project: the MJML
19
+ tree). **Templates saved before** (engine `grapesjs-mjml`) open from their
20
+ stored MJML, versions included, and are saved in the new format next time;
21
+ a local draft of the old format is dropped.
22
+ - `editor.exportHtml()` returns a Promise (MJML 5 compiles asynchronously).
23
+ - `createGrapesJsAdapter` is replaced by `createNativeAdapter`; `parseMjml`,
24
+ `parseMjmlFragment` and `serializeMjml` are exported.
25
+ - `createEmailEditor({ …, t })` takes the interface's translator: the texts a
26
+ block brings and the names in the canvas follow the language. A block's
27
+ `mjml` / `sectionMjml` may be a function of `t`.
28
+
29
+ ### Added
30
+ - **Generate an email with AI.** A *Generate* button opens a dialog with 68
31
+ purposes as badges in 8 groups (sales, holidays, events, news, account,
32
+ orders, relationship, organization), a search field and an optional line of
33
+ details. The email is written in the interface's language with the
34
+ builder's sender and variables, cleaned for the builder (longhand padding,
35
+ sections and columns only, no stock photos, unknown variables as visible
36
+ placeholders), validated like a saved template, and opens in the editor with
37
+ its subject and preview text. Server: `createEmailGenerator({ model })`,
38
+ `claudeEmailModel({ client })` (Claude Opus 5.5, structured output,
39
+ streamed), the `generator` option of the handler (`POST /generate`, 30 per
40
+ user per hour); client: `createHttpStorage({ generate: true })`. Any other
41
+ provider plugs in as a function. The live demo generates with the visitor's
42
+ own Anthropic key.
43
+ - **Rewrite with AI.** Selecting a text or a button shows, in its properties,
44
+ *Shorter*, *Longer*, *Fix mistakes*, *Friendlier*, *More formal*,
45
+ *Translate* (13 languages) and a field to say what to change. The text is
46
+ replaced in place, in one step of the history (Ctrl+Z). Links and variables
47
+ must survive and the HTML must pass the validator, or the model gets one more
48
+ turn and then nothing changes; a button label stays plain text. Server:
49
+ `generator.rewrite` (`createEmailGenerator` returns it), `POST /rewrite`
50
+ (200 per user per hour, `rateLimits.rewritePerHour`), `claudeEmailModel`'s
51
+ `rewriteEffort` (`'low'`); client: `storage.rewrite`, given by
52
+ `createHttpStorage({ generate: true })`. `editor.getElement(id).html` returns
53
+ the stored HTML of a text block.
54
+ - **Your brand.** A *Brand* button opens one dialog: the company's website
55
+ first (*Get my brand* reads its logo, main color, headings color, font and
56
+ name), then every field by hand, with a live preview. Saving keeps the
57
+ brand for the workspace and repaints the email being edited in one step
58
+ (Ctrl+Z); new emails, gallery templates, inserted blocks and generated
59
+ emails start in it. Emails are repainted by role (filled buttons and
60
+ colored banners, bold headings, colored links, fonts; gray footers and body
61
+ text kept), and the text on a colored background switches to what reads.
62
+ Server: `brandStore` (`createMemoryBrandRepository`), `brandImporter`
63
+ (`createBrandImporter`: public http(s) addresses on ports 80/443 only,
64
+ private, link-local, metadata and IPv4-in-IPv6 addresses refused at the
65
+ DNS lookup and on every redirect, size and time limits), routes
66
+ `GET/PUT /brand` and `POST /brand/import` (20 per user per hour,
67
+ `rateLimits.brandImportsPerHour`); client: `createHttpStorage({ brand: true })`,
68
+ `storage.loadBrand / saveBrand / importBrand`. `applyBrand`, `cleanBrand`,
69
+ `brandedSender`, `isPublicAddress`, `editor.setBrand` and `editor.setMjml`
70
+ (replace the email in one undoable step) are exported. The live demo keeps
71
+ the brand in `localStorage`.
72
+ - **Suggest subjects.** Under the subject, one click gives three other
73
+ subject lines drawn from the words of the email, each from its own angle.
74
+ They are a list to pick from, the user's own subject first to come back to;
75
+ *More ideas* asks for three new ones. Plain lines, distinct, in the email's
76
+ language, merge tags only from the catalog (one repair turn, fewer than
77
+ three still shown). Server: `generator.suggestSubjects`, `POST /subjects`
78
+ (100 per user per hour, `rateLimits.subjectsPerHour`); client:
79
+ `storage.suggestSubjects`, given by `createHttpStorage({ generate: true })`.
80
+ `emailWords` and `SUBJECTS_SYSTEM_PROMPT` are exported.
81
+ - **Dark mode in the preview.** A sun / moon switch next to Desktop / Mobile
82
+ shows the email as the apps that repaint it in dark mode do (Gmail and
83
+ Outlook apps, Outlook.com): light backgrounds and borders turn dark of the
84
+ same hue, dark text turns light, brand colors, light text and images stay —
85
+ a dark logo on a transparent background disappears there as it does in
86
+ those apps. Simulated on the compiled HTML, without script: the frame stays
87
+ sandboxed. In the editor's preview and in the template library.
88
+ `simulateDarkMode(html)` is exported.
89
+ - Drag and drop with pointer events: a line shows where the block lands
90
+ (between two elements of a column, or between two sections — a content block
91
+ then gets a section of its own), the canvas scrolls near its edges, Esc
92
+ cancels. Sections and elements move by the ✥ handle of their toolbar.
93
+ - On the selection: its name, and a toolbar — select the parent, move,
94
+ duplicate, delete. Keyboard: Delete, Ctrl+Z / Ctrl+Y, Ctrl+C / Ctrl+V,
95
+ Ctrl+D, Escape (parent), Enter (edit the text).
96
+ - Ctrl+A selects every block of the email (each section outlined, "All
97
+ blocks" in the panel); Delete — or the panel's delete button — removes them
98
+ in one step, Ctrl+Z brings them back. Inside a text being edited or a field,
99
+ Ctrl+A keeps selecting the text. `editor.selectAll()` does the same.
100
+ - The toolbar's first button names where it goes ("⤴ Column", "⤴ 2 columns"):
101
+ it selects the parent element; a bare up arrow read as "move up".
102
+ - Panel features for well-made emails: **column widths** (1/4 to 3/4; the
103
+ other columns of the section share the rest, "Equal" resets), a section
104
+ **background across the whole screen**, an image **full width on mobile**,
105
+ menu links **as typed or in capitals** (MJML capitalised them; new menus
106
+ are written as typed).
107
+ - The server's HTML gets `x-apple-disable-message-reformatting` and
108
+ `format-detection` (Apple Mail does not rescale the email, iOS does not turn
109
+ dates, addresses and phone numbers into blue links).
110
+ - Email-client checks on the compiled HTML of every template and of an email
111
+ holding every block (tests/unit/email-clients.test.mjs): Outlook's MSO
112
+ tables and VML backgrounds, Gmail's 102 KB clipping and dropped CSS, images
113
+ (alt, width, no border), mobile media queries and the fallback without
114
+ `<style>`, links, and the same body from the canvas compiler and the
115
+ server's. And rendered in a browser at 640 px, 375 px and without `<style>`
116
+ (e2e/rendering.stories.spec.js).
117
+ - The template library is called a library everywhere ("Bibliothèque de
118
+ modèles e-mail" in French, instead of "Banque").
119
+ - Text edited in place (double-click, or a click on a selected text) with a
120
+ formatting bar: bold, italic, underline, strike, link, unlink, clear. Pasted
121
+ content arrives as plain text, Enter is a line break, the HTML is reduced to
122
+ what an email accepts.
123
+ - The panel shows MJML's own default in an empty field (a section's padding:
124
+ 20 px above and below) and keeps it for the sides not edited.
125
+ - New layouts start with empty columns showing "Drop a block here" (canvas
126
+ only); an image without file shows a frame sized to its column.
127
+
128
+ ### Fixed
129
+ - Dropping an image opened an empty "Select an image" dialog of the old engine.
130
+ - A 2-column layout was named "Section" in the canvas and the panel; it is now
131
+ "2 columns", and every element carries its block's name, in both languages.
132
+ - Blocks inserted English texts ("Your text.", "Column 1", "Learn more"…) in a
133
+ French interface; their texts now follow the interface's language.
134
+ - An empty padding showed 0 while MJML renders 20 px (sections) or 10 px
135
+ (texts, top and bottom); editing one side set the others to 0.
136
+ - The button's "Label" field was not translated.
137
+ - The server-side project check now also reads the new project's `tag` field
138
+ (an `mj-raw` or `script` element in a tampered project is refused).
139
+
140
+ ## [0.3.0] — 2026-10-13
141
+
142
+ ### Added
143
+ - **`TemplateLibrary`** — the first screen: a template bank in three columns
144
+ (searchable list of your templates and of the ready-made bases, details of
145
+ the selected one, rendered preview on desktop / mobile). *Edit*, *New
146
+ template* and *Start from this base* hand the template to the host through
147
+ `onEdit` / `onCreate`; *Duplicate*, *Delete*, *Copy HTML* and *Download*
148
+ work in place. *Favorites*: the star of the details moves an entry to a
149
+ section at the top (`favoritesKey`). Search with Ctrl+K, Esc clears it.
150
+ Follows its own width down to phones. French included.
151
+ - **`EmailStudio`** — `TemplateLibrary` and `EmailBuilder` wired together.
152
+ Each screen still works alone.
153
+ The editor opened from the bank has no *Templates* button: the bases are
154
+ picked in the bank, and the gallery would replace the email being edited
155
+ (`builderProps.templates` brings it back).
156
+ - **Two entry points, one per block**: `best-email-builder/library` (block A,
157
+ the bank — no editor, no GrapesJS) and `best-email-builder/builder`
158
+ (block B, the editor — nothing of the bank). A unit test bundles each one
159
+ and fails if it reaches the other. The README shows block B under a bank
160
+ of your own (what to pass in, what comes back) and block A with your own
161
+ editor; a story drives block B from a home-made bank.
162
+ - Storage contract: optional `list()` and `remove(id)`, implemented by
163
+ `createHttpStorage()` (the server already had the routes) and
164
+ `createMemoryStorage()`.
165
+ - `withPlaceholderImages(html)` draws a grey frame for images not chosen yet
166
+ in compiled HTML.
167
+
168
+ ### Changed
169
+ - Demo, three pages: a landing page (a background that follows the pointer,
170
+ a marquee of the editors it can replace, a new mark), the template bank,
171
+ and the editor — each page has its own address (`#/templates`,
172
+ `#/editor/…`), so a reload keeps it. The bank and the editor share one
173
+ window of the same size, under a bar with the steps and a *Full screen* /
174
+ *Exit full screen* button. Type: Geist, Bricolage Grotesque for the title.
175
+ - Thin scrollbars everywhere in the builder and the
176
+ bank, inside the canvas and the preview frames (preview only, never the
177
+ saved HTML): a 5.6 px gutter, a thin thumb, no arrows, shown only while the
178
+ pointer is over the block. They also replace GrapesJS's own dark ones.
179
+ - The preview (in the bank and in the editor's *Preview*) switches device
180
+ with the editor toolbar's switch, and narrows and widens like the canvas
181
+ (350 ms).
182
+ - `types` and `typesVersions` in `package.json`: TypeScript projects still on
183
+ `moduleResolution: "node"` find the types of every entry (`.`, `/builder`,
184
+ `/library`, `/server`).
185
+ - `EmailBuilder` no longer imports the package entry
186
+ (`createDefaultEmailEditor` lives in its own module).
187
+
188
+ ### Fixed
189
+ - Demo: after a first save, switching the language reopened a blank editor
190
+ (or the base), and saving again created a duplicate; it now reopens the
191
+ saved template.
192
+
7
193
  ## [0.2.0] — 2026-10-12
8
194
 
9
195
  ### Added
package/README.md CHANGED
@@ -21,8 +21,9 @@ canvas you have to wire to MJML, storage, validation and sending yourself.
21
21
  Best Email Builder is the second option done once, properly, and given away:
22
22
 
23
23
  - **Free and MIT-licensed** — use it in commercial products, fork it, ship it.
24
- - **No account, no API key, no vendor.** It is a React component and a server
25
- handler you run in your own app, against your own database.
24
+ - **No account, no vendor.** It is a React component and a server handler
25
+ you run in your own app, against your own database. The AI generation is
26
+ optional and runs on your server, with your own key.
26
27
  - **Readable from end to end.** The engine is hidden behind one adapter, every
27
28
  block and feature is a registry entry, and the rules a template must obey
28
29
  live in one validation folder — so it is easy to change what you need.
@@ -31,23 +32,52 @@ Best Email Builder is the second option done once, properly, and given away:
31
32
  oddly are all welcome — see [Contributing](#contributing).
32
33
 
33
34
  - **MJML under the hood** — every block compiles to battle-tested, table-based
34
- email HTML. You never write a `<table>` by hand.
35
+ email HTML. You never write a `<table>` by hand. Every template is checked
36
+ against what Outlook (MSO tables, VML backgrounds), Gmail (102 KB clipping,
37
+ dropped CSS) and Apple Mail need, and rendered at desktop and phone widths
38
+ in the test suite.
35
39
  - **The blocks emails are made of**: heading, text, image, button, divider,
36
40
  spacer, social links, menu, 1 to 4 columns, image + text, and a hero banner
37
41
  with a background image — sections take a background image too.
38
- - **Your UI, your storage, your rendering.** The builder is a React component
39
- with its own interface; GrapesJS is only the internal canvas engine, hidden
40
- behind an adapter. Templates are stored by *your* backend, as a versioned JSON
41
- project (the source of truth) plus server-compiled HTML.
42
+ Columns take a share of the row (1/4 to 3/4), a section's background can
43
+ span the whole screen, an image can take the full width on phones.
44
+ - **Its own editor, end to end.** The interface, the canvas and the drag and
45
+ drop are the builder's own; the email is a tree of MJML elements, and MJML
46
+ alone turns it into HTML. The canvas shows the real compiled email — what you
47
+ see is what Outlook, Gmail and Apple Mail receive. Templates are stored by
48
+ *your* backend, as a versioned JSON project (the source of truth) plus
49
+ server-compiled HTML.
50
+ - **Editing that behaves**: drag a block, a line shows exactly where it will
51
+ land; double-click a text to type in place, with bold, italic, underline,
52
+ links; every element has its name, its toolbar (parent, move, duplicate,
53
+ delete) and its keyboard shortcuts; an empty field of the panel shows the
54
+ value MJML will really use.
42
55
  - **Server-side rendering only.** The browser never sends HTML: the server
43
56
  validates the project and the MJML, compiles, and stores its own HTML.
57
+ - **An email written from scratch by AI** — the user picks what it is for
58
+ among 68 purposes (sale, Christmas, password reset, meeting reminder, order
59
+ shipped…), adds a line of details if they want, and a complete email opens in
60
+ the editor, subject and preview text included, ready to edit and save. The
61
+ model's MJML is cleaned and checked like any template before it gets there.
62
+ Claude out of the box; any other model in a few lines.
63
+ - **Any text rewritten in one click** — select a text or a button: *Shorter*,
64
+ *Longer*, *Fix mistakes*, *Friendlier*, *More formal*, *Translate* into 13
65
+ languages, or say what to change in your own words. The text changes in
66
+ place, links and variables kept; Ctrl+Z brings the previous one back.
67
+ - **Three subject lines on demand** — drawn from the words of the email, each
68
+ from its own angle; a click puts one in the subject, your own stays in the
69
+ list to come back to.
70
+ - **Your brand in every email** — give your website: the logo, colors and
71
+ font are read from it (or set by hand), and new emails, templates and
72
+ blocks start in them.
44
73
  - **A gallery of ready-made emails** — newsletter, welcome, promotion, event
45
74
  invitation… each one with the sender's footer and the unsubscribe link, so
46
75
  nobody starts from a blank page or forgets the legal mentions.
47
76
  - **Production features**: undo/redo, desktop/mobile canvas, autosave and local
48
77
  drafts, versions, conflict detection between concurrent editors, image
49
78
  uploads, server preview with quality checks (missing alt text, missing
50
- unsubscribe link, unknown variables, Gmail clipping…), test sends.
79
+ unsubscribe link, unknown variables, Gmail clipping…) and a dark-mode view
80
+ (as the Gmail and Outlook apps repaint emails), test sends.
51
81
  - **Merge-tag variables your users create themselves** — searchable menu,
52
82
  insertion at the caret, in the subject line too, sample values for the
53
83
  preview and fallbacks for recipients without a value.
@@ -56,9 +86,10 @@ Best Email Builder is the second option done once, properly, and given away:
56
86
  - **English and French** built in, any other language through a dictionary.
57
87
 
58
88
  ```
59
- React UI ─► EmailEditor ─► GrapesJsAdapter ─► GrapesJS + grapesjs-mjml
60
- (ui/) (core/) (adapters/) internal engine
61
- │ project JSON + MJML
89
+ React UI ─► EmailEditor ─► NativeAdapter ─► the email: an MJML tree
90
+ (ui/) (core/) (engine/) │ canvas = the email compiled
91
+ │ by mjml-browser
92
+ │ project JSON + MJML
62
93
  Server: validation ─► MJML compilation ─► variables ─► links ─► final HTML
63
94
  ```
64
95
 
@@ -103,6 +134,106 @@ component (the canvas needs the DOM). With Next.js, load it with
103
134
  `dynamic(() => import(...), { ssr: false })` or render it from a
104
135
  `'use client'` component.
105
136
 
137
+ ### Two blocks, each one on its own
138
+
139
+ The package has two blocks. An application installs one, the other, or both,
140
+ and each has its own entry point, so one never brings the other's code:
141
+
142
+ | Block | Import | What it is |
143
+ |---|---|---|
144
+ | A — the library | `import { TemplateLibrary } from 'best-email-builder/library'` | Your templates and the ready-made bases in a searchable list, the selected one's details, its rendered preview (desktop / mobile, light / dark). *Edit*, *New template* and *Start from this base* only call `onEdit(template)` / `onCreate(start)` — it never mounts an editor, and loads neither the editor's engine nor its MJML compiler. |
145
+ | B — the editor | `import { EmailBuilder } from 'best-email-builder/builder'` | The editor (above). Opens whatever the host hands it; knows nothing of the library. |
146
+ | A + B | `import { EmailStudio } from 'best-email-builder'` | The two wired together: the library, then the editor, then back to the library with the template just saved selected. The editor has no *Templates* button there — the bases are in the library. |
147
+
148
+ `best-email-builder` (the main entry) still exports everything; the two
149
+ sub-entries are for an application that wants one block only. Both use the
150
+ same stylesheet, `best-email-builder/styles.css`.
151
+
152
+ #### A + B, nothing to wire
153
+
154
+ ```jsx
155
+ import { EmailStudio, createHttpStorage } from 'best-email-builder';
156
+
157
+ <EmailStudio storage={createHttpStorage()} locale="fr" sender={sender} />
158
+ ```
159
+
160
+ #### Block B with your own library
161
+
162
+ Your library (a table, a sidebar, a modal — anything) only decides what the
163
+ editor opens, and hears back when it saves or closes:
164
+
165
+ | Your library wants to… | Pass to `EmailBuilder` |
166
+ |---|---|
167
+ | open a saved template | `templateId={id}` (the editor loads it from `storage`) |
168
+ | start from content it has (a base, an import, a copy) | `initialMjml={mjml}` and, optionally, `initialMeta={{ subject, preheader }}` |
169
+ | start from a blank email | neither |
170
+ | refresh its list after a save | `onSaved={(template) => …}` — `template.id` is set from the first save on |
171
+ | show itself again | `onClose={() => …}` (the *Back* button) |
172
+ | hide the editor's own gallery of bases | `templates={[]}` |
173
+
174
+ Give the editor a `key` that changes with what it opens, so switching from one
175
+ template to another starts a fresh editor.
176
+
177
+ ```jsx
178
+ import { useState } from 'react';
179
+ import { EmailBuilder, createHttpStorage } from 'best-email-builder/builder';
180
+
181
+ const storage = createHttpStorage();
182
+
183
+ function Emails() {
184
+ const [open, setOpen] = useState(null); // null = my library
185
+
186
+ if (!open) {
187
+ return (
188
+ <MyTemplateList // your block A
189
+ onOpen={(id) => setOpen({ key: id, templateId: id })}
190
+ onNew={() => setOpen({ key: `new-${Date.now()}` })}
191
+ />
192
+ );
193
+ }
194
+ return (
195
+ <EmailBuilder
196
+ key={open.key}
197
+ storage={storage}
198
+ locale="fr"
199
+ templates={[]}
200
+ templateId={open.templateId ?? null}
201
+ initialMjml={open.mjml}
202
+ initialMeta={open.meta}
203
+ onSaved={() => refreshMyList()}
204
+ onClose={() => setOpen(null)}
205
+ />
206
+ );
207
+ }
208
+ ```
209
+
210
+ Your list can read the same templates with `storage.list()` (summaries, newest
211
+ first) or from your own API — the editor only needs the `id`.
212
+
213
+ #### Block A with your own editor
214
+
215
+ ```jsx
216
+ import { TemplateLibrary, createHttpStorage } from 'best-email-builder/library';
217
+
218
+ <TemplateLibrary
219
+ storage={createHttpStorage()}
220
+ locale="fr"
221
+ onEdit={(template) => router.push(`/emails/${template.id}`)}
222
+ onCreate={(start) => openMyEditor(start)} // start: null (new), or { baseId, mjml, meta: { subject, preheader } }
223
+ onClose={closeModal} // optional: shows a close button
224
+ />
225
+ ```
226
+
227
+ The library needs `storage.list()`; *Delete* appears when the storage has
228
+ `remove()`, the preview uses `render()` (else the HTML compiled at the last
229
+ save). Its props: `storage`, `locale`, `messages`, `onNotify`, `sender`,
230
+ `templates` (the bases, `[]` hides them), `title`, `description`,
231
+ `selectedId`, `favoritesKey` (the star of the details moves an entry to a
232
+ *Favorites* section at the top; kept in `localStorage` under this key, `null`
233
+ turns favorites off), `onEdit`, `onCreate`, `onClose`, `className`. Like the
234
+ builder, it fills its parent and follows its own width (three columns, then
235
+ two, then list and details as two screens).
236
+
106
237
  ### 2. The server routes
107
238
 
108
239
  ```js
@@ -167,6 +298,150 @@ const subject = applyVariablesToText(template.subject, values, variables);
167
298
  With an email provider that does its own merge, map the tags instead:
168
299
  `mapVariables(html, { 'contact.firstname': '{{ contact.FIRSTNAME }}' })`.
169
300
 
301
+ ### 4. Generating emails with AI (optional)
302
+
303
+ The *Generate* button asks one thing: what the email is for (a badge among
304
+ 68, grouped: sales, holidays, events, news, account, orders, relationship,
305
+ organization), with an optional line of details. The email is written in the
306
+ interface's language, with the builder's sender and variables, and replaces
307
+ the one being edited — the dialog says so before, not after.
308
+
309
+ The model runs on the server, where its key is:
310
+
311
+ ```js
312
+ import Anthropic from '@anthropic-ai/sdk'; // npm install @anthropic-ai/sdk
313
+ import {
314
+ createEmailBuilderHandler,
315
+ createEmailGenerator,
316
+ claudeEmailModel,
317
+ compileMjml,
318
+ } from 'best-email-builder/server';
319
+
320
+ const handler = createEmailBuilderHandler({
321
+ /* … */
322
+ generator: createEmailGenerator({
323
+ model: claudeEmailModel({ client: new Anthropic() }), // ANTHROPIC_API_KEY
324
+ compile: compileMjml, // MJML's own warnings go back to the model
325
+ }),
326
+ rateLimits: { generatePerHour: 30, rewritePerHour: 200, subjectsPerHour: 100 }, // per user (defaults)
327
+ });
328
+ ```
329
+
330
+ ```jsx
331
+ <EmailBuilder storage={createHttpStorage({ generate: true })} />
332
+ ```
333
+
334
+ - `claudeEmailModel({ client, model, effort, maxTokens })` — `claude-opus-5-5`
335
+ by default, `effort: 'medium'`; the answer is structured output (a JSON
336
+ schema: name, subject, preheader, MJML) and is streamed, so a long email does
337
+ not hit a request timeout. A generation takes from 20 seconds to a minute or
338
+ two: give the route that long (`maxDuration` on Vercel, the proxy's timeout).
339
+ - **Another provider** is a function `({ system, messages, schema, signal, task }) =>
340
+ Promise<string>` returning the JSON text — pass it as `model`.
341
+ - What comes out is never trusted as is: shorthand padding is expanded,
342
+ elements the builder does not edit (`mj-hero`, `mj-wrapper`, `mj-head`…)
343
+ are turned into sections and columns, stock photos are removed (the user
344
+ picks real images; the alt text stays), unknown variables become visible
345
+ `[placeholders]`, and the result must pass the same validation as a saved
346
+ template. When a problem remains, the model gets one more turn with the list.
347
+ - The prompt (`SYSTEM_PROMPT`, `buildUserPrompt`) and the purposes
348
+ (`EMAIL_PURPOSES`) are exported, to read or to build on.
349
+
350
+ **Rewrite with AI.** The same generator rewrites one text: with it, selecting
351
+ a text block or a button shows a *Rewrite with AI* section in its properties —
352
+ *Shorter*, *Longer*, *Fix mistakes*, *Friendlier*, *More formal*, *Translate*
353
+ (13 languages, under their own names) and a field to say what to change. The
354
+ answer replaces the text at once, in one step of the history: Ctrl+Z brings
355
+ the previous text back, so there is nothing to accept or compare. Selecting
356
+ another element cancels a rewrite in progress; if the text was edited
357
+ meanwhile, the user's words win and the answer is dropped.
358
+
359
+ - It is `POST {base}/rewrite`, enabled with `createHttpStorage({ generate: true })`
360
+ like generation; a generator without `rewrite` answers 501.
361
+ - Every link (the same `href`, none added) and every variable (the same keys,
362
+ none added; a fallback after `|` may be translated) must survive, and the
363
+ HTML must pass the validator — otherwise the model gets one more turn, then
364
+ nothing is changed. A button label comes back as plain words.
365
+ - `claudeEmailModel` asks a rewrite for `rewriteEffort: 'low'` (a few seconds);
366
+ another provider gets `task: 'rewrite'` in its request to tell the two apart.
367
+ - `REWRITE_ACTIONS`, `REWRITE_LANGUAGES` and `REWRITE_SYSTEM_PROMPT` are
368
+ exported.
369
+
370
+ **Suggest subjects.** Under the subject of the email, *Suggest subjects* asks
371
+ for three other subject lines, drawn from the words of the email, each from
372
+ its own angle (clear, curious, personal or timely). They come as a list to
373
+ pick from, with the user's own subject first: a click puts one in the field,
374
+ another click another one, the first row brings the user's subject back.
375
+ *More ideas* asks for three new ones, *Done* closes the list.
376
+
377
+ - It is `POST {base}/subjects` (`generator.suggestSubjects`), enabled with
378
+ `createHttpStorage({ generate: true })` too; 100 per user per hour.
379
+ - The subjects are plain lines, different from the current subject and from
380
+ each other, in the language of the email; a merge tag is allowed only from
381
+ the server's catalog. Otherwise the model gets one more turn; fewer than
382
+ three good ones are still shown.
383
+ - `claudeEmailModel` uses `rewriteEffort` for them as well (`task: 'subjects'`).
384
+ `emailWords(mjml)` (the words a reader sees) and `SUBJECTS_SYSTEM_PROMPT`
385
+ are exported.
386
+
387
+ The live demo has no server: there, *Generate*, *Rewrite with AI* and
388
+ *Suggest subjects* ask for the visitor's own Anthropic key, keep it in the
389
+ browser and call Anthropic directly.
390
+
391
+ ### 5. Your brand (optional)
392
+
393
+ The *Brand* button opens one dialog. Its first field is the company's
394
+ website: *Get my brand* reads its logo, main color, headings color, font and
395
+ name, and fills the fields below. Everything can then be changed by hand
396
+ (upload a logo, pick a color), and a small preview follows every change.
397
+ *Save* keeps the brand for the whole workspace:
398
+
399
+ - the email being edited is repainted (the box *Also apply to this email* is
400
+ ticked by default) — one step of the history, Ctrl+Z brings the previous
401
+ look back;
402
+ - new emails, gallery templates, inserted blocks and AI-generated emails
403
+ start in the brand: the logo (or the name) at the top, the name in the
404
+ footer, buttons and banners in the main color, headings in their color.
405
+
406
+ Emails are repainted by **role**, not by exact color, so any email works —
407
+ a template, a generated one, one built by hand: filled buttons and colored
408
+ banners take the main color, and their text switches to white or dark
409
+ whichever reads; bold headings take the headings color; colored links take
410
+ the main color (or the headings color when the main one is too light to read
411
+ on white); gray footers and body text keep theirs.
412
+
413
+ ```js
414
+ import {
415
+ createBrandImporter,
416
+ createMemoryBrandRepository,
417
+ } from 'best-email-builder/server';
418
+
419
+ const handler = createEmailBuilderHandler({
420
+ // …
421
+ brandStore: createMemoryBrandRepository(), // one row: { get(), save(brand) }
422
+ brandImporter: createBrandImporter(), // optional: "Get my brand"
423
+ rateLimits: { brandImportsPerHour: 20 }, // per user (default)
424
+ });
425
+
426
+ // In the browser:
427
+ const storage = createHttpStorage({ brand: true }); // { brand: { import: false } } without an importer
428
+ ```
429
+
430
+ - Routes: `GET {base}/brand`, `PUT {base}/brand` (write permission),
431
+ `POST {base}/brand/import` with `{ url }` (write permission). The import
432
+ saves nothing: the user reviews the result first. A logo found on the site
433
+ is stored like an upload (PNG, JPEG or GIF; SVG and WebP logos are skipped,
434
+ since most email apps do not show them — the dialog says so).
435
+ - Without `brandImporter` the dialog has no website field; without
436
+ `brandStore` there is no *Brand* button.
437
+ - Some websites refuse automated readers (bot protection answers 403): the
438
+ dialog says so, and the brand is filled in by hand.
439
+ - `applyBrand(mjml, brand)` and `cleanBrand(brand)` are exported for your
440
+ own pipeline (e.g. rebranding stored templates).
441
+
442
+ The live demo keeps the brand in `localStorage` and has no website field
443
+ (reading a website needs a server).
444
+
170
445
  ## Component props
171
446
 
172
447
  | Prop | Default | Description |
@@ -194,7 +469,9 @@ or use `createHttpStorage()` with the server handler:
194
469
 
195
470
  ```ts
196
471
  interface TemplateStorage {
472
+ list?(): Promise<Template[]>; // TemplateLibrary: newest first, no document/MJML
197
473
  load(id): Promise<Template>;
474
+ remove?(id): Promise<void>; // TemplateLibrary: enables Delete
198
475
  create(payload): Promise<{ template; warnings }>;
199
476
  duplicate(id): Promise<{ template; warnings }>;
200
477
  save(id, payload): Promise<{ template; warnings; version }>;
@@ -202,10 +479,16 @@ interface TemplateStorage {
202
479
  getVersion(id, versionId): Promise<Version & { document; mjml }>;
203
480
  uploadImage(file): Promise<{ path }>;
204
481
  render?(input): Promise<{ html; warnings; quality }>; // enables Preview
482
+ generate?(input, { signal }): Promise<{ name; subject; preheader; mjml; warnings }>; // enables Generate
483
+ rewrite?(input, { signal }): Promise<{ text }>; // enables Rewrite with AI
484
+ suggestSubjects?(input, { signal }): Promise<{ subjects }>; // enables Suggest subjects
205
485
  sendTest?(input): Promise<{ sent; failed; invalid; results }>; // enables Send test
206
486
  listVariables?(): Promise<Variable[]>; // enable user-created variables
207
487
  saveVariable?(variable, { create }): Promise<Variable>;
208
488
  deleteVariable?(key): Promise<void>;
489
+ loadBrand?(): Promise<Brand>; // enable the Brand button
490
+ saveBrand?(brand): Promise<Brand>;
491
+ importBrand?(url, { signal }): Promise<{ brand; notes }>; // enables "Get my brand"
209
492
  }
210
493
  ```
211
494
 
@@ -294,6 +577,20 @@ touch the rest of your page, whether or not you use Tailwind yourself.
294
577
  `nosniff` and a sandboxing CSP.
295
578
  - Previews are rendered in an iframe with an empty `sandbox`.
296
579
  - Test sends are limited (10 addresses, 20 sends per user per hour by default).
580
+ - Reading a brand from a website is a request your server makes to an address
581
+ a user typed, so it is fenced: http(s) on ports 80 and 443 only, no
582
+ credentials in the address, and every address the name resolves to must be
583
+ public — loopback, private networks, link-local and cloud metadata
584
+ (`169.254.169.254`), carrier-grade NAT, multicast and IPv4 hidden in IPv6
585
+ (mapped, NAT64, 6to4) are refused, at the lookup itself so a DNS answer
586
+ cannot slip through. Redirects (4 at most) are checked the same way. Pages,
587
+ stylesheets and logos have size limits after decompression, the whole
588
+ reading 8 seconds, and imports are limited per user (20 per hour).
589
+ - Generated emails pass the same MJML validation as saved ones; generation is
590
+ limited per user (30 per hour by default) and needs the `write` permission.
591
+ A rewritten text keeps its links and variables (none added) and passes the
592
+ same validation; rewrites are limited too (200 per user per hour), and so
593
+ are subject ideas (100), whose merge tags come from the server's catalog.
297
594
 
298
595
  ## Development
299
596
 
@@ -332,7 +629,7 @@ channel described in [SECURITY.md](SECURITY.md).
332
629
 
333
630
  ## License
334
631
 
335
- [MIT](LICENSE). GrapesJS and grapesjs-mjml are BSD-3-Clause, MJML is MIT.
632
+ [MIT](LICENSE). MJML and mjml-browser are MIT.
336
633
 
337
634
  ## Author
338
635
 
@@ -0,0 +1,44 @@
1
+ import {
2
+ EmailBuilder,
3
+ EmailBuilderBoundary
4
+ } from "./chunk-MY6WPDT4.js";
5
+ import {
6
+ STARTER_MJML,
7
+ StorageError,
8
+ TEMPLATE_GALLERY,
9
+ buildGallery,
10
+ createHttpStorage,
11
+ createMemoryStorage,
12
+ createStarterMjml
13
+ } from "./chunk-AT73Y7JA.js";
14
+ import "./chunk-JNHFZXDL.js";
15
+ import {
16
+ CUSTOM_GROUP,
17
+ DEFAULT_VARIABLES,
18
+ EMAIL_BUILDER_VERSION,
19
+ LOCALES,
20
+ createTranslator,
21
+ defineVariables,
22
+ mergeVariables,
23
+ translatorFor
24
+ } from "./chunk-HHLOE6OZ.js";
25
+ export {
26
+ CUSTOM_GROUP,
27
+ DEFAULT_VARIABLES,
28
+ EMAIL_BUILDER_VERSION,
29
+ EmailBuilder,
30
+ EmailBuilderBoundary,
31
+ LOCALES,
32
+ STARTER_MJML,
33
+ StorageError,
34
+ TEMPLATE_GALLERY,
35
+ buildGallery,
36
+ createHttpStorage,
37
+ createMemoryStorage,
38
+ createStarterMjml,
39
+ createTranslator,
40
+ defineVariables,
41
+ mergeVariables,
42
+ translatorFor
43
+ };
44
+ //# sourceMappingURL=builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}