best-email-builder 0.1.1 → 0.3.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 (56) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +113 -1
  3. package/dist/builder.js +44 -0
  4. package/dist/builder.js.map +1 -0
  5. package/dist/{chunk-2DZ6BRIJ.js → chunk-3M36VVXS.js} +132 -119
  6. package/dist/chunk-3M36VVXS.js.map +1 -0
  7. package/dist/chunk-57CTQNTK.js +56 -0
  8. package/dist/chunk-57CTQNTK.js.map +1 -0
  9. package/dist/chunk-BVOVAEEJ.js +73 -0
  10. package/dist/chunk-BVOVAEEJ.js.map +1 -0
  11. package/dist/chunk-ILRAQTHS.js +3677 -0
  12. package/dist/chunk-ILRAQTHS.js.map +1 -0
  13. package/dist/chunk-JOC752I3.js +529 -0
  14. package/dist/chunk-JOC752I3.js.map +1 -0
  15. package/dist/chunk-ZSUHOCCE.js +1351 -0
  16. package/dist/chunk-ZSUHOCCE.js.map +1 -0
  17. package/dist/index.js +88 -4199
  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 +27 -23
  22. package/dist/server.js.map +1 -1
  23. package/dist/styles.css +1 -1
  24. package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +1 -1
  25. package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -14
  26. package/dist/types/blocks/columns/index.d.ts +3 -0
  27. package/dist/types/blocks/heading/index.d.ts +3 -0
  28. package/dist/types/blocks/hero/index.d.ts +3 -0
  29. package/dist/types/blocks/navbar/index.d.ts +6 -0
  30. package/dist/types/blocks/social/index.d.ts +13 -0
  31. package/dist/types/builder.d.ts +8 -0
  32. package/dist/types/core/EmailEditor.d.ts +12 -4
  33. package/dist/types/core/constants.d.ts +1 -1
  34. package/dist/types/core/scrollbars.d.ts +13 -0
  35. package/dist/types/defaultEditor.d.ts +8 -0
  36. package/dist/types/features/templates/TemplateGallery.d.ts +3 -0
  37. package/dist/types/features/templates/gallery.d.ts +48 -0
  38. package/dist/types/features/templates/index.d.ts +2 -0
  39. package/dist/types/features/templates/starter.d.ts +22 -0
  40. package/dist/types/index.d.ts +4 -8
  41. package/dist/types/library.d.ts +5 -0
  42. package/dist/types/server/compileMjml.d.ts +3 -3
  43. package/dist/types/server/render.d.ts +4 -4
  44. package/dist/types/server/templates.d.ts +2 -2
  45. package/dist/types/storage/client.d.ts +4 -0
  46. package/dist/types/ui/BlockSidebar.d.ts +2 -1
  47. package/dist/types/ui/Drawer.d.ts +19 -0
  48. package/dist/types/ui/EmailBuilder.d.ts +2 -0
  49. package/dist/types/ui/EmailStudio.d.ts +42 -0
  50. package/dist/types/ui/PropertiesPanel.d.ts +2 -1
  51. package/dist/types/ui/Toolbar.d.ts +9 -2
  52. package/dist/types/ui/library/TemplateLibrary.d.ts +41 -0
  53. package/dist/types/ui/primitives/button.d.ts +1 -1
  54. package/dist/types/ui/useLayoutWidth.d.ts +10 -0
  55. package/package.json +23 -6
  56. package/dist/chunk-2DZ6BRIJ.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,92 @@ 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
+ ## [0.3.0] — 2026-10-13
8
+
9
+ ### Added
10
+ - **`TemplateLibrary`** — the first screen: a template bank in three columns
11
+ (searchable list of your templates and of the ready-made bases, details of
12
+ the selected one, rendered preview on desktop / mobile). *Edit*, *New
13
+ template* and *Start from this base* hand the template to the host through
14
+ `onEdit` / `onCreate`; *Duplicate*, *Delete*, *Copy HTML* and *Download*
15
+ work in place. *Favorites*: the star of the details moves an entry to a
16
+ section at the top (`favoritesKey`). Search with Ctrl+K, Esc clears it.
17
+ Follows its own width down to phones. French included.
18
+ - **`EmailStudio`** — `TemplateLibrary` and `EmailBuilder` wired together.
19
+ Each screen still works alone.
20
+ The editor opened from the bank has no *Templates* button: the bases are
21
+ picked in the bank, and the gallery would replace the email being edited
22
+ (`builderProps.templates` brings it back).
23
+ - **Two entry points, one per block**: `best-email-builder/library` (block A,
24
+ the bank — no editor, no GrapesJS) and `best-email-builder/builder`
25
+ (block B, the editor — nothing of the bank). A unit test bundles each one
26
+ and fails if it reaches the other. The README shows block B under a bank
27
+ of your own (what to pass in, what comes back) and block A with your own
28
+ editor; a story drives block B from a home-made bank.
29
+ - Storage contract: optional `list()` and `remove(id)`, implemented by
30
+ `createHttpStorage()` (the server already had the routes) and
31
+ `createMemoryStorage()`.
32
+ - `withPlaceholderImages(html)` draws a grey frame for images not chosen yet
33
+ in compiled HTML.
34
+
35
+ ### Changed
36
+ - Demo, three pages: a landing page (a background that follows the pointer,
37
+ a marquee of the editors it can replace, a new mark), the template bank,
38
+ and the editor — each page has its own address (`#/templates`,
39
+ `#/editor/…`), so a reload keeps it. The bank and the editor share one
40
+ window of the same size, under a bar with the steps and a *Full screen* /
41
+ *Exit full screen* button. Type: Geist, Bricolage Grotesque for the title.
42
+ - Thin scrollbars everywhere in the builder and the
43
+ bank, inside the canvas and the preview frames (preview only, never the
44
+ saved HTML): a 5.6 px gutter, a thin thumb, no arrows, shown only while the
45
+ pointer is over the block. They also replace GrapesJS's own dark ones.
46
+ - The preview (in the bank and in the editor's *Preview*) switches device
47
+ with the editor toolbar's switch, and narrows and widens like the canvas
48
+ (350 ms).
49
+ - `types` and `typesVersions` in `package.json`: TypeScript projects still on
50
+ `moduleResolution: "node"` find the types of every entry (`.`, `/builder`,
51
+ `/library`, `/server`).
52
+ - `EmailBuilder` no longer imports the package entry
53
+ (`createDefaultEmailEditor` lives in its own module).
54
+
55
+ ### Fixed
56
+ - Demo: after a first save, switching the language reopened a blank editor
57
+ (or the base), and saving again created a duplicate; it now reopens the
58
+ saved template.
59
+
60
+ ## [0.2.0] — 2026-10-12
61
+
62
+ ### Added
63
+ - Blocks: **Heading**, **Social links** (Facebook, Instagram, LinkedIn, X,
64
+ YouTube, GitHub — each icon selectable, duplicable, its network and link
65
+ editable), **Menu** (a row of links, each label edited in the panel),
66
+ **4 columns**, **Image + text** and **Hero banner** (a section with a
67
+ background image under a title, a sentence and a button).
68
+ - Sections take a background image, with its size and position; the color
69
+ underneath stays for clients that block images.
70
+ - Text: a *Weight* property (normal / bold).
71
+ - **Template gallery**: a *Templates* button opens six ready-made emails
72
+ (blank, simple, newsletter, welcome, promotion, event invitation), shown as
73
+ rendered thumbnails. Picking one replaces the email (after confirmation if
74
+ it was modified) and fills the subject and preview text when empty.
75
+ `templates` prop to offer your own, `[]` to hide it; `TEMPLATE_GALLERY` and
76
+ `buildGallery` are exported.
77
+ - **Phones and tablets**: the layout follows the builder's own width. Below
78
+ 1180 px the properties open in a drawer and the toolbar takes two rows (its
79
+ actions scroll sideways); below 760 px the blocks move to a drawer too and a
80
+ bar under the canvas offers *Add a block* and *Edit: <element>*. Adding a
81
+ block opens its properties; saving without a name opens the settings.
82
+ - `editor.exportHtml(mjml)` compiles the given MJML with the canvas compiler
83
+ (display only — what is stored and sent is still compiled by the server).
84
+
85
+ ### Changed
86
+ - **Breaking:** `compileMjml`, `compileTemplate` and `renderEmail` from
87
+ `best-email-builder/server` now return a Promise — MJML 5 compiles
88
+ asynchronously. Add `await` where you call them.
89
+ - MJML 5.4 on the server (htmlnano/cssnano instead of html-minifier, stricter
90
+ includes), GrapesJS 0.23.6 in the canvas, lucide-react 1.x, radix-ui 1.8.
91
+ Templates saved with GrapesJS 0.22 open unchanged.
92
+
7
93
  ## [0.1.1] — 2026-10-12
8
94
 
9
95
  ### Fixed
package/README.md CHANGED
@@ -32,12 +32,18 @@ Best Email Builder is the second option done once, properly, and given away:
32
32
 
33
33
  - **MJML under the hood** — every block compiles to battle-tested, table-based
34
34
  email HTML. You never write a `<table>` by hand.
35
+ - **The blocks emails are made of**: heading, text, image, button, divider,
36
+ spacer, social links, menu, 1 to 4 columns, image + text, and a hero banner
37
+ with a background image — sections take a background image too.
35
38
  - **Your UI, your storage, your rendering.** The builder is a React component
36
39
  with its own interface; GrapesJS is only the internal canvas engine, hidden
37
40
  behind an adapter. Templates are stored by *your* backend, as a versioned JSON
38
41
  project (the source of truth) plus server-compiled HTML.
39
42
  - **Server-side rendering only.** The browser never sends HTML: the server
40
43
  validates the project and the MJML, compiles, and stores its own HTML.
44
+ - **A gallery of ready-made emails** — newsletter, welcome, promotion, event
45
+ invitation… each one with the sender's footer and the unsubscribe link, so
46
+ nobody starts from a blank page or forgets the legal mentions.
41
47
  - **Production features**: undo/redo, desktop/mobile canvas, autosave and local
42
48
  drafts, versions, conflict detection between concurrent editors, image
43
49
  uploads, server preview with quality checks (missing alt text, missing
@@ -89,11 +95,114 @@ export default function TemplateEditor({ templateId, onClose }) {
89
95
  }
90
96
  ```
91
97
 
92
- The builder fills its parent: give the parent a height. It is a client-only
98
+ The builder fills its parent: give the parent a height. It adapts to the
99
+ width it is given — not the window's: three columns from 1180 px, properties
100
+ in a drawer below that, and on a phone the canvas alone with *Add a block* /
101
+ *Edit* drawers. It is a client-only
93
102
  component (the canvas needs the DOM). With Next.js, load it with
94
103
  `dynamic(() => import(...), { ssr: false })` or render it from a
95
104
  `'use client'` component.
96
105
 
106
+ ### Two blocks, each one on its own
107
+
108
+ The package has two blocks. An application installs one, the other, or both,
109
+ and each has its own entry point, so one never brings the other's code:
110
+
111
+ | Block | Import | What it is |
112
+ |---|---|---|
113
+ | A — the bank | `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). *Edit*, *New template* and *Start from this base* only call `onEdit(template)` / `onCreate(start)` — it never mounts an editor, and does not load GrapesJS. |
114
+ | B — the editor | `import { EmailBuilder } from 'best-email-builder/builder'` | The editor (above). Opens whatever the host hands it; knows nothing of the bank. |
115
+ | A + B | `import { EmailStudio } from 'best-email-builder'` | The two wired together: the bank, then the editor, then back to the bank with the template just saved selected. The editor has no *Templates* button there — the bases are in the bank. |
116
+
117
+ `best-email-builder` (the main entry) still exports everything; the two
118
+ sub-entries are for an application that wants one block only. Both use the
119
+ same stylesheet, `best-email-builder/styles.css`.
120
+
121
+ #### A + B, nothing to wire
122
+
123
+ ```jsx
124
+ import { EmailStudio, createHttpStorage } from 'best-email-builder';
125
+
126
+ <EmailStudio storage={createHttpStorage()} locale="fr" sender={sender} />
127
+ ```
128
+
129
+ #### Block B with your own bank
130
+
131
+ Your bank (a table, a sidebar, a modal — anything) only decides what the
132
+ editor opens, and hears back when it saves or closes:
133
+
134
+ | Your bank wants to… | Pass to `EmailBuilder` |
135
+ |---|---|
136
+ | open a saved template | `templateId={id}` (the editor loads it from `storage`) |
137
+ | start from content it has (a base, an import, a copy) | `initialMjml={mjml}` and, optionally, `initialMeta={{ subject, preheader }}` |
138
+ | start from a blank email | neither |
139
+ | refresh its list after a save | `onSaved={(template) => …}` — `template.id` is set from the first save on |
140
+ | show itself again | `onClose={() => …}` (the *Back* button) |
141
+ | hide the editor's own gallery of bases | `templates={[]}` |
142
+
143
+ Give the editor a `key` that changes with what it opens, so switching from one
144
+ template to another starts a fresh editor.
145
+
146
+ ```jsx
147
+ import { useState } from 'react';
148
+ import { EmailBuilder, createHttpStorage } from 'best-email-builder/builder';
149
+
150
+ const storage = createHttpStorage();
151
+
152
+ function Emails() {
153
+ const [open, setOpen] = useState(null); // null = my bank
154
+
155
+ if (!open) {
156
+ return (
157
+ <MyTemplateList // your block A
158
+ onOpen={(id) => setOpen({ key: id, templateId: id })}
159
+ onNew={() => setOpen({ key: `new-${Date.now()}` })}
160
+ />
161
+ );
162
+ }
163
+ return (
164
+ <EmailBuilder
165
+ key={open.key}
166
+ storage={storage}
167
+ locale="fr"
168
+ templates={[]}
169
+ templateId={open.templateId ?? null}
170
+ initialMjml={open.mjml}
171
+ initialMeta={open.meta}
172
+ onSaved={() => refreshMyList()}
173
+ onClose={() => setOpen(null)}
174
+ />
175
+ );
176
+ }
177
+ ```
178
+
179
+ Your list can read the same templates with `storage.list()` (summaries, newest
180
+ first) or from your own API — the editor only needs the `id`.
181
+
182
+ #### Block A with your own editor
183
+
184
+ ```jsx
185
+ import { TemplateLibrary, createHttpStorage } from 'best-email-builder/library';
186
+
187
+ <TemplateLibrary
188
+ storage={createHttpStorage()}
189
+ locale="fr"
190
+ onEdit={(template) => router.push(`/emails/${template.id}`)}
191
+ onCreate={(start) => openMyEditor(start)} // start: null (new), or { baseId, mjml, meta: { subject, preheader } }
192
+ onClose={closeModal} // optional: shows a close button
193
+ />
194
+ ```
195
+
196
+ The library needs `storage.list()`; *Delete* appears when the storage has
197
+ `remove()`, the preview uses `render()` (else the HTML compiled at the last
198
+ save). Its props: `storage`, `locale`, `messages`, `onNotify`, `sender`,
199
+ `templates` (the bases, `[]` hides them), `title`, `description`,
200
+ `selectedId`, `favoritesKey` (the star of the details moves an entry to a
201
+ *Favorites* section at the top; kept in `localStorage` under this key, `null`
202
+ turns favorites off), `onEdit`, `onCreate`, `onClose`, `className`. Like the
203
+ builder, it fills its parent and follows its own width (three columns, then
204
+ two, then list and details as two screens).
205
+
97
206
  ### 2. The server routes
98
207
 
99
208
  ```js
@@ -170,6 +279,7 @@ With an email provider that does its own merge, map the tags instead:
170
279
  | `variables` | `DEFAULT_VARIABLES` | Merge tags offered in the *Variables* menu. Pass the same list to the server. |
171
280
  | `sender` | — | `{ name, address, logoUrl, logoWidth }` used by the starter template footer. |
172
281
  | `starterMjml` | — | Replace the starter template entirely. |
282
+ | `templates` | `TEMPLATE_GALLERY` | Ready-made emails offered by the *Templates* button (blank, simple, newsletter, welcome, promotion, event invitation). Pass your own `{ id, name, description, create({ sender, t }), meta?(t) }` entries, or `[]` to hide the gallery. |
173
283
  | `initialMjml`, `initialMeta` | — | Open a NEW template on given MJML and metadata (`{ name, subject, preheader, description }`) — e.g. a template converted from another format. Ignored with `templateId`. |
174
284
  | `testRecipients` | `[]` | Addresses pre-filled in the *Send test* dialog. |
175
285
  | `onClose` | — | Shows a *Back* button; called after confirming unsaved changes. |
@@ -184,7 +294,9 @@ or use `createHttpStorage()` with the server handler:
184
294
 
185
295
  ```ts
186
296
  interface TemplateStorage {
297
+ list?(): Promise<Template[]>; // TemplateLibrary: newest first, no document/MJML
187
298
  load(id): Promise<Template>;
299
+ remove?(id): Promise<void>; // TemplateLibrary: enables Delete
188
300
  create(payload): Promise<{ template; warnings }>;
189
301
  duplicate(id): Promise<{ template; warnings }>;
190
302
  save(id, payload): Promise<{ template; warnings; version }>;
@@ -0,0 +1,44 @@
1
+ import {
2
+ EmailBuilder,
3
+ EmailBuilderBoundary
4
+ } from "./chunk-ILRAQTHS.js";
5
+ import {
6
+ STARTER_MJML,
7
+ StorageError,
8
+ TEMPLATE_GALLERY,
9
+ buildGallery,
10
+ createHttpStorage,
11
+ createMemoryStorage,
12
+ createStarterMjml
13
+ } from "./chunk-ZSUHOCCE.js";
14
+ import "./chunk-57CTQNTK.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-3M36VVXS.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":[]}
@@ -1,5 +1,5 @@
1
1
  // src/core/constants.js
2
- var EMAIL_BUILDER_VERSION = "0.1.1";
2
+ var EMAIL_BUILDER_VERSION = "0.3.0";
3
3
  var EMAIL_DOCUMENT_SCHEMA_VERSION = 1;
4
4
  var EMAIL_DEVICES = ["desktop", "mobile"];
5
5
  var EMAIL_MOBILE_WIDTH = 375;
@@ -77,6 +77,132 @@ function serializeDocument(document) {
77
77
 
78
78
  // src/i18n/fr.js
79
79
  var fr = {
80
+ "1. Complete your profile": "1. Compl\xE9tez votre profil",
81
+ "2. Explore": "2. Explorez",
82
+ "3. Ask us anything": "3. Posez-nous vos questions",
83
+ "30% off everything": "-30 % sur tout",
84
+ "30% off this weekend only": "-30 % ce week-end seulement",
85
+ "4 columns": "4 colonnes",
86
+ "4-column section": "Section en 4 colonnes",
87
+ "A new feature": "Une nouveaut\xE9",
88
+ "A sale with a discount code and two products": "Une vente avec un code promo et deux produits",
89
+ "A tip": "Une astuce",
90
+ "Add a block": "Ajouter un bloc",
91
+ "Add to favorites": "Ajouter aux favoris",
92
+ "All templates": "Tous les mod\xE8les",
93
+ "All your templates are in the favorites.": "Tous vos mod\xE8les sont dans les favoris.",
94
+ "An empty page to build from scratch": "Une page vide pour partir de z\xE9ro",
95
+ "An evening of talks and drinks with the team and our customers.": "Une soir\xE9e de rencontres autour d'un verre avec l'\xE9quipe et nos clients.",
96
+ "An event": "Un \xE9v\xE9nement",
97
+ "Article {n}": "Article {n}",
98
+ "Background image": "Image de fond",
99
+ "Background image with a title and a button": "Image de fond avec un titre et un bouton",
100
+ "Base": "Base",
101
+ "Base \u2014 not editable: start from it to create your template.": "Base non modifiable \u2014 partez-en pour cr\xE9er votre mod\xE8le.",
102
+ "Bases": "Bases",
103
+ "Blank": "Vierge",
104
+ "Book my seat": "Je r\xE9serve ma place",
105
+ "Builder": "Constructeur",
106
+ "Buy": "Acheter",
107
+ "by {name}": "par {name}",
108
+ "Choose a template": "Choisissez un mod\xE8le",
109
+ "Clear the search": "Effacer la recherche",
110
+ "Copy HTML": "Copier le HTML",
111
+ "Could not load the templates.": "Impossible de charger les mod\xE8les.",
112
+ "Created": "Cr\xE9\xE9",
113
+ "Date, place and a button to book a seat": "Date, lieu et un bouton pour r\xE9server",
114
+ "Delete \u201C{name}\u201D?": "Supprimer \xAB {name} \xBB ?",
115
+ "Direction": "Disposition",
116
+ "Download": "T\xE9l\xE9charger",
117
+ "Edit: {element}": "Modifier : {element}",
118
+ "Email settings": "R\xE9glages de l'e-mail",
119
+ "Email template library": "Banque de mod\xE8les emails",
120
+ "Event invitation": "Invitation",
121
+ "Facebook": "Facebook",
122
+ "Favorites": "Favoris",
123
+ "Fill the section": "Remplir la section",
124
+ "Get started": "Commencer",
125
+ "GitHub": "GitHub",
126
+ "Greets a new customer and lists the first steps": "Accueille un nouveau client et liste les premi\xE8res \xE9tapes",
127
+ "Heading": "Titre",
128
+ "Hero banner": "Banni\xE8re",
129
+ "Horizontal": "Horizontale",
130
+ "HTML copied.": "HTML copi\xE9.",
131
+ "Icon background": "Fond de l'ic\xF4ne",
132
+ "Icon size": "Taille des ic\xF4nes",
133
+ "Icons linking to your profiles": "Ic\xF4nes vers vos profils",
134
+ "Image + text": "Image + texte",
135
+ "Image beside a title, text and button": "Image \xE0 c\xF4t\xE9 d'un titre, d'un texte et d'un bouton",
136
+ "Image position": "Position de l'image",
137
+ "Image size": "Taille de l'image",
138
+ "In brief": "En bref",
139
+ "Instagram": "Instagram",
140
+ "Large bold title": "Grand titre en gras",
141
+ "LinkedIn": "LinkedIn",
142
+ "Logo, headline, text and a button": "Logo, titre, texte et un bouton",
143
+ "Menu": "Menu",
144
+ "Menu link": "Lien du menu",
145
+ "Monthly news: banner, two articles, highlights": "Les nouvelles du mois : banni\xE8re, deux articles, br\xE8ves",
146
+ "Network": "R\xE9seau",
147
+ "Newsletter": "Newsletter",
148
+ "No preview: this storage cannot render emails.": "Pas d'aper\xE7u : ce stockage ne sait pas compiler les e-mails.",
149
+ "No template yet. Start from a base, or create a new one.": "Aucun mod\xE8le pour l\u2019instant. Partez d\u2019une base, ou cr\xE9ez-en un.",
150
+ "Nothing matches \u201C{query}\u201D.": "Aucun r\xE9sultat pour \xAB {query} \xBB.",
151
+ "Offer valid until Sunday midnight, while stocks last.": "Offre valable jusqu'\xE0 dimanche minuit, dans la limite des stocks.",
152
+ "One sentence about it.": "Une phrase pour en parler.",
153
+ "Origin": "Origine",
154
+ "Original size": "Taille d'origine",
155
+ "Our offices, 1 Main Street": "Nos bureaux, 1 rue Principale",
156
+ "Picking a template replaces the email being edited.": "Choisir un mod\xE8le remplace l'e-mail en cours de modification.",
157
+ "Product one": "Produit un",
158
+ "Product two": "Produit deux",
159
+ "Profile URL": "Adresse du profil",
160
+ "Promotion": "Promotion",
161
+ "Read more": "Lire la suite",
162
+ "Ready-made templates to copy into your campaigns.": "Mod\xE8les pr\xEAts \xE0 copier dans vos campagnes.",
163
+ "Remove from favorites": "Retirer des favoris",
164
+ "Replace": "Remplacer",
165
+ "Replace the current email?": "Remplacer l'e-mail actuel ?",
166
+ "Reply to this email: a person reads it.": "R\xE9pondez \xE0 cet e-mail : une personne le lit.",
167
+ "Row of links to your site": "Rang\xE9e de liens vers votre site",
168
+ "Save the date \u2014 seats are limited.": "Notez la date \u2014 les places sont limit\xE9es.",
169
+ "Search templates": "Rechercher un mod\xE8le",
170
+ "Seats are limited: book yours now, it takes ten seconds.": "Les places sont limit\xE9es : r\xE9servez la v\xF4tre, cela prend dix secondes.",
171
+ "Select a template on the left.": "S\xE9lectionnez un mod\xE8le \xE0 gauche.",
172
+ "Shop the sale": "Voir les offres",
173
+ "Simple": "Simple",
174
+ "So we can tailor what we send you.": "Pour adapter ce que nous vous envoyons.",
175
+ "Social link": "R\xE9seau social",
176
+ "Social links": "R\xE9seaux sociaux",
177
+ "Space around each icon": "Espace autour de chaque ic\xF4ne",
178
+ "Start from a template": "Partir d'un mod\xE8le",
179
+ "Start from this base": "Partir de cette base",
180
+ "Take a tour of what you can do.": "Faites le tour de ce que vous pouvez faire.",
181
+ "Template deleted.": "Mod\xE8le supprim\xE9.",
182
+ "Template duplicated.": "Mod\xE8le dupliqu\xE9.",
183
+ "Templates": "Mod\xE8les",
184
+ "Thank you for joining us. Here is how to get the most out of your account.": "Merci de nous avoir rejoints. Voici comment tirer le meilleur de votre compte.",
185
+ "The browser refused to copy.": "Le navigateur a refus\xE9 la copie.",
186
+ "The news of the month": "Les nouvelles du mois",
187
+ "The template and its versions will be deleted.": "Le mod\xE8le et ses versions seront supprim\xE9s.",
188
+ "The template preview appears here.": "L'aper\xE7u du mod\xE8le s'affiche ici.",
189
+ "This weekend only, with the code SPRING30.": "Ce week-end seulement, avec le code SPRING30.",
190
+ "Three steps to get started.": "Trois \xE9tapes pour bien commencer.",
191
+ "Thursday, 7 pm": "Jeudi, 19 h",
192
+ "Two or three sentences that sum up the story and make people want the rest.": "Deux ou trois phrases qui r\xE9sument l'article et donnent envie de lire la suite.",
193
+ "Updated": "Modifi\xE9",
194
+ "Vertical": "Verticale",
195
+ "Welcome": "Bienvenue",
196
+ "Welcome aboard!": "Bienvenue \xE0 bord !",
197
+ "Welcome, {name}!": "Bienvenue, {name} !",
198
+ "What changed, what is coming, and two stories worth your time.": "Ce qui a chang\xE9, ce qui arrive, et deux articles qui valent le d\xE9tour.",
199
+ "When": "Quand",
200
+ "Where": "O\xF9",
201
+ "Whole image": "Image enti\xE8re",
202
+ "With the code SPRING30, until Sunday midnight.": "Avec le code SPRING30, jusqu'\xE0 dimanche minuit.",
203
+ "X": "X",
204
+ "You're invited": "On vous invite !",
205
+ "YouTube": "YouTube",
80
206
  "{label} \u2014 {side}": "{label} \u2014 {side}",
81
207
  "{max} test addresses at most": "{max} adresses de test au maximum",
82
208
  "{message} Your version can be saved as a copy.": "{message} Votre version peut \xEAtre enregistr\xE9e comme copie.",
@@ -362,7 +488,9 @@ var fr = {
362
488
  "Width": "Largeur",
363
489
  "Your company": "Votre soci\xE9t\xE9",
364
490
  "Your headline": "Titre du message",
365
- "Your text.": "Votre texte."
491
+ "Your text.": "Votre texte.",
492
+ "\u201C{name}\u201D template applied.": "Mod\xE8le \xAB {name} \xBB appliqu\xE9.",
493
+ "\u20AC49 instead of \u20AC70": "49 \u20AC au lieu de 70 \u20AC"
366
494
  };
367
495
 
368
496
  // src/i18n/index.js
@@ -481,66 +609,6 @@ function applyServiceLinks(html, { unsubscribeUrl }) {
481
609
  return String(html || "").replace(UNSUBSCRIBE, () => escapeAttribute(unsubscribeUrl)).replace(MIRROR, "#");
482
610
  }
483
611
 
484
- // src/features/variables/service.js
485
- function escapeHtml(text) {
486
- return String(text ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
487
- }
488
- function decodeHtml(text) {
489
- return String(text ?? "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
490
- }
491
- var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
492
- function patternOf(key) {
493
- return new RegExp(`\\{\\{\\s*${escapeRegExp(key)}\\s*(?:\\|([^{}]*))?\\}\\}`, "g");
494
- }
495
- var ANY_TAG = /\{\{\s*([A-Za-z][\w.]*)\s*(?:\|[^{}]*)?\}\}/g;
496
- var textVariables = (variables) => variables.filter((v) => v.kind !== "link");
497
- var present = (value) => value !== null && value !== void 0 && String(value) !== "";
498
- function replace(text, values, variables, html) {
499
- let out = String(text || "");
500
- for (const variable of textVariables(variables)) {
501
- out = out.replace(patternOf(variable.key), (_, inline) => {
502
- const value = values?.[variable.key];
503
- if (present(value)) return html ? escapeHtml(value) : String(value);
504
- const fallback = inline !== void 0 ? (html ? decodeHtml(inline) : inline).trim() : variable.fallback || "";
505
- return html ? escapeHtml(fallback) : fallback;
506
- });
507
- }
508
- return out;
509
- }
510
- function applyVariables(html, values, variables = DEFAULT_VARIABLES) {
511
- return replace(html, values, variables, true);
512
- }
513
- function applyVariablesToText(text, values, variables = DEFAULT_VARIABLES) {
514
- return replace(text, values, variables, false);
515
- }
516
- function mapVariables(html, mapping, variables = DEFAULT_VARIABLES) {
517
- let out = String(html || "");
518
- for (const variable of textVariables(variables)) {
519
- if (!Object.prototype.hasOwnProperty.call(mapping, variable.key)) continue;
520
- out = out.replace(patternOf(variable.key), () => String(mapping[variable.key]));
521
- }
522
- return out;
523
- }
524
- function usedVariables(text, variables = DEFAULT_VARIABLES) {
525
- const source = String(text || "");
526
- return variables.filter((v) => patternOf(v.key).test(source)).map((v) => v.key);
527
- }
528
- function unknownVariables(text, variables = DEFAULT_VARIABLES, ignore = ["unsubscribe", "mirror"]) {
529
- const known = /* @__PURE__ */ new Set([...variables.map((v) => v.key), ...ignore]);
530
- const found = /* @__PURE__ */ new Set();
531
- for (const match of String(text || "").matchAll(ANY_TAG)) {
532
- if (!known.has(match[1])) found.add(match[1]);
533
- }
534
- return [...found];
535
- }
536
- function sampleValues(variables = DEFAULT_VARIABLES, overrides = {}) {
537
- const values = Object.fromEntries(textVariables(variables).map((v) => [v.key, v.sample]));
538
- for (const [key, value] of Object.entries(overrides)) {
539
- if (present(value)) values[key] = String(value);
540
- }
541
- return values;
542
- }
543
-
544
612
  // src/validation/images.js
545
613
  var MAX_IMAGE_BYTES = 2 * 1024 * 1024;
546
614
  var IMAGE_TYPES = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif" };
@@ -563,51 +631,6 @@ function imageUploadError(file, { t = identity } = {}) {
563
631
  return null;
564
632
  }
565
633
 
566
- // src/validation/urls.js
567
- var LINK_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "action", "formaction", "xlink:href"]);
568
- var IMAGE_ATTRIBUTES = /* @__PURE__ */ new Set([
569
- "src",
570
- "srcset",
571
- "background-url",
572
- "background",
573
- "poster",
574
- "icon-unwrapped-url",
575
- "icon-wrapped-url"
576
- ]);
577
- var NAMED_ENTITIES = {
578
- colon: ":",
579
- tab: " ",
580
- newline: "\n",
581
- lpar: "(",
582
- rpar: ")",
583
- amp: "&",
584
- quot: '"',
585
- apos: "'",
586
- lt: "<",
587
- gt: ">",
588
- nbsp: " "
589
- };
590
- function decodeEntities(value) {
591
- return String(value ?? "").replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);?/gi, (whole, body) => {
592
- const key = String(body).toLowerCase();
593
- if (key.startsWith("#x")) return String.fromCodePoint(Number.parseInt(key.slice(2), 16) || 0);
594
- if (key.startsWith("#")) return String.fromCodePoint(Number.parseInt(key.slice(1), 10) || 0);
595
- return NAMED_ENTITIES[key] ?? whole;
596
- });
597
- }
598
- function isMergeTag(value) {
599
- return /^\{\{\s*[a-zA-Z][\w.]*\s*\}\}$/.test(String(value || "").trim());
600
- }
601
- function isSafeUrl(raw, kind) {
602
- const value = decodeEntities(raw).trim();
603
- if (!value) return true;
604
- if (isMergeTag(value)) return true;
605
- const compact = value.replace(/[\s\u0000-\u001f\u007f]+/g, "").toLowerCase();
606
- if (kind === "image") return compact.startsWith("https://") || isAssetPath(value);
607
- if (compact.startsWith("#")) return true;
608
- return /^(https?:\/\/|mailto:|tel:)/.test(compact);
609
- }
610
-
611
634
  export {
612
635
  EMAIL_BUILDER_VERSION,
613
636
  EMAIL_DOCUMENT_SCHEMA_VERSION,
@@ -637,16 +660,6 @@ export {
637
660
  IMAGE_TYPES,
638
661
  ASSET_KEY_PATTERN,
639
662
  detectImage,
640
- imageUploadError,
641
- LINK_ATTRIBUTES,
642
- IMAGE_ATTRIBUTES,
643
- decodeEntities,
644
- isSafeUrl,
645
- applyVariables,
646
- applyVariablesToText,
647
- mapVariables,
648
- usedVariables,
649
- unknownVariables,
650
- sampleValues
663
+ imageUploadError
651
664
  };
652
- //# sourceMappingURL=chunk-2DZ6BRIJ.js.map
665
+ //# sourceMappingURL=chunk-3M36VVXS.js.map