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.
- package/CHANGELOG.md +86 -0
- package/README.md +113 -1
- package/dist/builder.js +44 -0
- package/dist/builder.js.map +1 -0
- package/dist/{chunk-2DZ6BRIJ.js → chunk-3M36VVXS.js} +132 -119
- package/dist/chunk-3M36VVXS.js.map +1 -0
- package/dist/chunk-57CTQNTK.js +56 -0
- package/dist/chunk-57CTQNTK.js.map +1 -0
- package/dist/chunk-BVOVAEEJ.js +73 -0
- package/dist/chunk-BVOVAEEJ.js.map +1 -0
- package/dist/chunk-ILRAQTHS.js +3677 -0
- package/dist/chunk-ILRAQTHS.js.map +1 -0
- package/dist/chunk-JOC752I3.js +529 -0
- package/dist/chunk-JOC752I3.js.map +1 -0
- package/dist/chunk-ZSUHOCCE.js +1351 -0
- package/dist/chunk-ZSUHOCCE.js.map +1 -0
- package/dist/index.js +88 -4199
- package/dist/index.js.map +1 -1
- package/dist/library.js +31 -0
- package/dist/library.js.map +1 -0
- package/dist/server.js +27 -23
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +1 -1
- package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -14
- package/dist/types/blocks/columns/index.d.ts +3 -0
- package/dist/types/blocks/heading/index.d.ts +3 -0
- package/dist/types/blocks/hero/index.d.ts +3 -0
- package/dist/types/blocks/navbar/index.d.ts +6 -0
- package/dist/types/blocks/social/index.d.ts +13 -0
- package/dist/types/builder.d.ts +8 -0
- package/dist/types/core/EmailEditor.d.ts +12 -4
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/core/scrollbars.d.ts +13 -0
- package/dist/types/defaultEditor.d.ts +8 -0
- package/dist/types/features/templates/TemplateGallery.d.ts +3 -0
- package/dist/types/features/templates/gallery.d.ts +48 -0
- package/dist/types/features/templates/index.d.ts +2 -0
- package/dist/types/features/templates/starter.d.ts +22 -0
- package/dist/types/index.d.ts +4 -8
- package/dist/types/library.d.ts +5 -0
- package/dist/types/server/compileMjml.d.ts +3 -3
- package/dist/types/server/render.d.ts +4 -4
- package/dist/types/server/templates.d.ts +2 -2
- package/dist/types/storage/client.d.ts +4 -0
- package/dist/types/ui/BlockSidebar.d.ts +2 -1
- package/dist/types/ui/Drawer.d.ts +19 -0
- package/dist/types/ui/EmailBuilder.d.ts +2 -0
- package/dist/types/ui/EmailStudio.d.ts +42 -0
- package/dist/types/ui/PropertiesPanel.d.ts +2 -1
- package/dist/types/ui/Toolbar.d.ts +9 -2
- package/dist/types/ui/library/TemplateLibrary.d.ts +41 -0
- package/dist/types/ui/primitives/button.d.ts +1 -1
- package/dist/types/ui/useLayoutWidth.d.ts +10 -0
- package/package.json +23 -6
- 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
|
|
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 }>;
|
package/dist/builder.js
ADDED
|
@@ -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.
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
487
|
-
}
|
|
488
|
-
function decodeHtml(text) {
|
|
489
|
-
return String(text ?? "").replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/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-
|
|
665
|
+
//# sourceMappingURL=chunk-3M36VVXS.js.map
|