best-email-builder 0.3.0 → 0.5.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 (112) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +290 -20
  3. package/dist/ai-server.js +68 -0
  4. package/dist/ai-server.js.map +1 -0
  5. package/dist/ai.js +852 -0
  6. package/dist/ai.js.map +1 -0
  7. package/dist/builder.js +16 -8
  8. package/dist/chunk-2RZHGAH5.js +64 -0
  9. package/dist/chunk-2RZHGAH5.js.map +1 -0
  10. package/dist/chunk-3LKF4QBL.js +595 -0
  11. package/dist/chunk-3LKF4QBL.js.map +1 -0
  12. package/dist/chunk-5SASKCUC.js +251 -0
  13. package/dist/chunk-5SASKCUC.js.map +1 -0
  14. package/dist/{chunk-JOC752I3.js → chunk-6D3SLT5W.js} +27 -29
  15. package/dist/chunk-6D3SLT5W.js.map +1 -0
  16. package/dist/chunk-7U6BAKM7.js +119 -0
  17. package/dist/chunk-7U6BAKM7.js.map +1 -0
  18. package/dist/chunk-DFNHICTF.js +1359 -0
  19. package/dist/chunk-DFNHICTF.js.map +1 -0
  20. package/dist/chunk-ECHGJZ2W.js +327 -0
  21. package/dist/chunk-ECHGJZ2W.js.map +1 -0
  22. package/dist/chunk-KXJJSVN5.js +98 -0
  23. package/dist/chunk-KXJJSVN5.js.map +1 -0
  24. package/dist/{chunk-ZSUHOCCE.js → chunk-LXR4QTGP.js} +123 -579
  25. package/dist/chunk-LXR4QTGP.js.map +1 -0
  26. package/dist/{chunk-BVOVAEEJ.js → chunk-LYMJ2TBX.js} +2 -2
  27. package/dist/chunk-MPX5CWTZ.js +1687 -0
  28. package/dist/chunk-MPX5CWTZ.js.map +1 -0
  29. package/dist/chunk-PB7YZ3WP.js +5635 -0
  30. package/dist/chunk-PB7YZ3WP.js.map +1 -0
  31. package/dist/chunk-UFKCLU2D.js +255 -0
  32. package/dist/chunk-UFKCLU2D.js.map +1 -0
  33. package/dist/index.js +51 -20
  34. package/dist/index.js.map +1 -1
  35. package/dist/library.js +12 -7
  36. package/dist/server.js +513 -214
  37. package/dist/server.js.map +1 -1
  38. package/dist/styles.css +1 -1
  39. package/dist/types/ai/ConnectAi.d.ts +12 -0
  40. package/dist/types/ai/GenerateButton.d.ts +4 -0
  41. package/dist/types/ai/RewriteText.d.ts +5 -0
  42. package/dist/types/ai/SubjectIdeas.d.ts +25 -0
  43. package/dist/types/ai/chip.d.ts +1 -0
  44. package/dist/types/ai/claude.d.ts +21 -0
  45. package/dist/types/ai/createAi.d.ts +66 -0
  46. package/dist/types/ai/draft.d.ts +163 -0
  47. package/dist/types/ai/generator.d.ts +123 -0
  48. package/dist/types/ai/i18n/fr.d.ts +2 -0
  49. package/dist/types/ai/i18n/index.d.ts +2 -0
  50. package/dist/types/ai/index.d.ts +9 -0
  51. package/dist/types/ai/keys.d.ts +46 -0
  52. package/dist/types/ai/prompt.d.ts +76 -0
  53. package/dist/types/ai/providers.d.ts +67 -0
  54. package/dist/types/ai/purposes.d.ts +48 -0
  55. package/dist/types/ai/rewrite.d.ts +83 -0
  56. package/dist/types/ai/server.d.ts +9 -0
  57. package/dist/types/ai/subjects.d.ts +97 -0
  58. package/dist/types/blocks/shared.d.ts +20 -0
  59. package/dist/types/core/EmailEditor.d.ts +72 -19
  60. package/dist/types/core/constants.d.ts +1 -1
  61. package/dist/types/core/mjmlDefaults.d.ts +13 -0
  62. package/dist/types/core/scrollbars.d.ts +1 -1
  63. package/dist/types/defaultEditor.d.ts +4 -1
  64. package/dist/types/engine/NativeAdapter.d.ts +28 -0
  65. package/dist/types/engine/canvas/CanvasView.d.ts +88 -0
  66. package/dist/types/engine/canvas/canvasStyles.d.ts +3 -0
  67. package/dist/types/engine/canvas/icons.d.ts +14 -0
  68. package/dist/types/engine/canvas/morph.d.ts +21 -0
  69. package/dist/types/engine/canvasMjml.d.ts +30 -0
  70. package/dist/types/engine/mjml.d.ts +31 -0
  71. package/dist/types/engine/model.d.ts +130 -0
  72. package/dist/types/engine/placement.d.ts +69 -0
  73. package/dist/types/engine/richText.d.ts +15 -0
  74. package/dist/types/engine/unwrapModule.d.ts +29 -0
  75. package/dist/types/features/brand/BrandButton.d.ts +4 -0
  76. package/dist/types/features/brand/brand.d.ts +108 -0
  77. package/dist/types/features/brand/extract.d.ts +61 -0
  78. package/dist/types/features/brand/index.d.ts +2 -0
  79. package/dist/types/features/brand/messages.d.ts +13 -0
  80. package/dist/types/features/brand/useBrand.d.ts +22 -0
  81. package/dist/types/features/preview/PreviewButton.d.ts +1 -1
  82. package/dist/types/features/preview/darkMode.d.ts +65 -0
  83. package/dist/types/features/variables/VariablePicker.d.ts +3 -2
  84. package/dist/types/i18n/index.d.ts +8 -0
  85. package/dist/types/index.d.ts +5 -2
  86. package/dist/types/server/brand.d.ts +32 -0
  87. package/dist/types/server/brandImport.d.ts +93 -0
  88. package/dist/types/server/compileMjml.d.ts +2 -0
  89. package/dist/types/server/handler.d.ts +26 -0
  90. package/dist/types/server/index.d.ts +5 -1
  91. package/dist/types/storage/client.d.ts +57 -3
  92. package/dist/types/ui/EmailBuilder.d.ts +5 -1
  93. package/dist/types/ui/EmailStudio.d.ts +6 -1
  94. package/dist/types/ui/PropertiesPanel.d.ts +2 -1
  95. package/dist/types/ui/fields.d.ts +2 -1
  96. package/dist/types/ui/primitives/button.d.ts +1 -1
  97. package/dist/types/ui/primitives/color-picker.d.ts +2 -1
  98. package/dist/types/ui/primitives/searchShortcut.d.ts +16 -0
  99. package/dist/types/ui/useLayoutWidth.d.ts +1 -1
  100. package/package.json +27 -7
  101. package/dist/chunk-3M36VVXS.js +0 -665
  102. package/dist/chunk-3M36VVXS.js.map +0 -1
  103. package/dist/chunk-57CTQNTK.js +0 -56
  104. package/dist/chunk-57CTQNTK.js.map +0 -1
  105. package/dist/chunk-ILRAQTHS.js +0 -3677
  106. package/dist/chunk-ILRAQTHS.js.map +0 -1
  107. package/dist/chunk-JOC752I3.js.map +0 -1
  108. package/dist/chunk-ZSUHOCCE.js.map +0 -1
  109. package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +0 -33
  110. package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -148
  111. package/dist/types/adapters/grapesjs/unwrapModule.d.ts +0 -18
  112. /package/dist/{chunk-BVOVAEEJ.js.map → chunk-LYMJ2TBX.js.map} +0 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The words of an email, as a reader sees them: the head, the styles and the
3
+ * markup left out.
4
+ * @param {string} mjml
5
+ */
6
+ export function emailWords(mjml: string): string;
7
+ /**
8
+ * The request as the model sees it, or an error when there is nothing to
9
+ * draw subjects from.
10
+ * @param {SubjectsInput} input
11
+ */
12
+ export function cleanSubjectsInput(input: SubjectsInput): {
13
+ subject: string;
14
+ preheader: string;
15
+ text: string;
16
+ language: string;
17
+ variables: {
18
+ key: string;
19
+ label: string;
20
+ }[];
21
+ avoid: string[];
22
+ };
23
+ /** @param {ReturnType<typeof cleanSubjectsInput>} request */
24
+ export function buildSubjectsPrompt({ subject, preheader, text, language, variables, avoid }: ReturnType<typeof cleanSubjectsInput>): string;
25
+ /**
26
+ * The subjects in a model's answer: the strings of its JSON field
27
+ * `subjects`; an answer that is no JSON at all, one per line (list marks,
28
+ * quotes and introductions left out). JSON without that field gives none —
29
+ * the model is asked again.
30
+ * @param {string} answer @param {string} [language] the email's, for the capital letter
31
+ */
32
+ export function parseSubjects(answer: string, language?: string): string[];
33
+ /**
34
+ * The usable subjects, and what is wrong with the others (for the model).
35
+ * Left out: a repeat (of the current subject, the preview text, an idea
36
+ * already shown or another one), a merge tag the list does not have or one
37
+ * written for another tool, a word in capitals the email does not have, more
38
+ * than one "!", a subject longer than an inbox shows.
39
+ * @param {string[]} subjects @param {ReturnType<typeof cleanSubjectsInput>} request
40
+ */
41
+ export function reviewSubjects(subjects: string[], request: ReturnType<typeof cleanSubjectsInput>): {
42
+ usable: string[];
43
+ problems: string[];
44
+ };
45
+ /**
46
+ * @param {{model: import('./generator.js').EmailModel, maxAttempts?: number}} options
47
+ * @returns {(input: SubjectsInput, options?: {signal?: AbortSignal}) => Promise<{subjects: string[], attempts: number}>}
48
+ */
49
+ export function createSubjectSuggester({ model, maxAttempts }: {
50
+ model: import("./generator.js").EmailModel;
51
+ maxAttempts?: number;
52
+ }): (input: SubjectsInput, options?: {
53
+ signal?: AbortSignal;
54
+ }) => Promise<{
55
+ subjects: string[];
56
+ attempts: number;
57
+ }>;
58
+ export const SUBJECT_COUNT: 3;
59
+ export const MAX_SUBJECT_LENGTH: 150;
60
+ export const MAX_EMAIL_WORDS: 4000;
61
+ export namespace SUBJECTS_SCHEMA {
62
+ let type: string;
63
+ namespace properties {
64
+ namespace subjects {
65
+ let type_1: string;
66
+ export { type_1 as type };
67
+ export let description: string;
68
+ export namespace items {
69
+ let type_2: string;
70
+ export { type_2 as type };
71
+ }
72
+ }
73
+ }
74
+ let required: string[];
75
+ let additionalProperties: boolean;
76
+ }
77
+ export const SUBJECTS_SYSTEM_PROMPT: "You write email subject lines. You get an email (its current subject, its preview text and its words) and answer three NEW subject lines for it, in the JSON field \"subjects\".\n\nEach subject:\n- tells what the email brings, specific to its content: under 50 characters when possible, 70 at most;\n- takes its own angle: one clear and direct, one that makes the reader curious, one personal or timely;\n- is written in the language of the email;\n- invents no facts: no price, discount, date, number, name or promise the email does not contain;\n- is plain text: no HTML, no quotation marks around it, no line break;\n- has no word in capitals, at most one \"!\", no spam wording (\"free!!!\", \"act now\", \"100% guaranteed\");\n- has no emoji, unless the current subject has one;\n- differs from the current subject and from the two others, and does not repeat the preview text.\n\nMerge tags: you may personalise ONE of the three with a merge tag from the list you are given, written exactly as listed, e.g. {{contact.firstname}}. No other merge tag.";
78
+ /** Longer than any inbox shows. */
79
+ export const MAX_SUBJECT_SHOWN: 90;
80
+ /**
81
+ * `text`: the words of the email (`emailWords`); `language`: the one of the
82
+ * interface, used when the email has no words yet and its subject does not
83
+ * tell; `variables`: a link variable (`kind: 'link'`) is never offered;
84
+ * `avoid`: ideas already shown ("More ideas" asks for new ones).
85
+ */
86
+ export type SubjectsInput = {
87
+ subject?: string;
88
+ preheader?: string;
89
+ text?: string;
90
+ language?: string;
91
+ variables?: {
92
+ key: string;
93
+ label?: string;
94
+ kind?: string;
95
+ }[];
96
+ avoid?: string[];
97
+ };
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Building blocks shared by all blocks: palette, fonts, recurring properties.
3
+ *
4
+ * -- DEFAULT PALETTE ---------------------------------------------------------
5
+ * Plain hex values: email clients load no stylesheet and understand neither
6
+ * `var()` nor modern color functions, so every value ends up inline in a
7
+ * `style=""` attribute.
8
+ *
9
+ * -- EVERY BLOCK SETS ITS OWN TEXT ATTRIBUTES --------------------------------
10
+ * Font, size, line height and color are written on the element itself, never
11
+ * left to MJML defaults (the "Ubuntu" font loaded from Google, line height 1):
12
+ * the canvas and the sent email then render the same thing, and Outlook gets
13
+ * explicit values.
14
+ *
15
+ * Fonts: only web-safe stacks, installed everywhere and understood by
16
+ * Outlook - a web font only renders in a minority of email clients.
17
+ */
18
+ /** @typedef {import('../core/EmailEditor.js').BlockProperty} BlockProperty */
19
+ /** Text for an MJML attribute or element. @param {string} text */
20
+ export function escapeXml(text: string): string;
1
21
  /**
2
22
  * White one-column section: the wrapper of a content block dropped outside
3
23
  * any column (MJML does not accept text directly inside the body).
@@ -27,7 +27,9 @@
27
27
  * `structure` : body, section, column — selectable, not addable.
28
28
  *
29
29
  * `mjml` is the inserted fragment; `sectionMjml` the same wrapped in a
30
- * section, for when it must go outside any column; `tagName` identifies the
30
+ * section, for when it must go outside any column. Either may be a function
31
+ * of the translator, so the texts a block brings arrive in the interface's
32
+ * language; `tagName` identifies the
31
33
  * block of a selected element (`mj-text` → text).
32
34
  * `editableContent`: the content is editable (canvas for text, panel for a
33
35
  * button label).
@@ -43,8 +45,8 @@
43
45
  * kind: 'content'|'layout'|'structure',
44
46
  * tagName: string,
45
47
  * order?: number,
46
- * mjml?: string,
47
- * sectionMjml?: string,
48
+ * mjml?: string | ((t: import('../i18n/index.js').Translate) => string),
49
+ * sectionMjml?: string | ((t: import('../i18n/index.js').Translate) => string),
48
50
  * properties: BlockProperty[],
49
51
  * editableContent?: boolean,
50
52
  * insertOnly?: boolean,
@@ -59,20 +61,30 @@
59
61
  * tagName: string,
60
62
  * attributes: Record<string, string>,
61
63
  * content: string|null,
64
+ * html?: string|null,
62
65
  * canRemove: boolean,
63
66
  * canDuplicate: boolean,
64
67
  * canMoveUp: boolean,
65
68
  * canMoveDown: boolean,
69
+ * columnCount?: number,
70
+ * multiple?: number,
66
71
  * }} EngineSelection
72
+ * `content`: the label of a button or a menu link, as plain text.
73
+ * `html`: the HTML of a text (`mj-text`), as stored.
74
+ * `columnCount`: for a section, how many columns it holds (its name).
75
+ * `multiple`: the whole email is selected (Ctrl+A) — how many sections;
76
+ * the selection is then described as the body, and only removal applies.
67
77
  */
68
78
  /**
69
- * Selected element, in the builder's vocabulary: its block and its property
70
- * values (padding recombined, see mjmlAttributes).
79
+ * Selected element, in the builder's vocabulary: its block, its property
80
+ * values (padding recombined, see mjmlAttributes) and, for the properties left
81
+ * empty, what MJML renders instead (`defaults`, see mjmlDefaults).
71
82
  *
72
83
  * @typedef {EngineSelection & {
73
84
  * blockId: string|null,
74
85
  * label: string,
75
86
  * values: Record<string, string>,
87
+ * defaults: Record<string, string>,
76
88
  * }} EmailSelection
77
89
  */
78
90
  /**
@@ -98,6 +110,8 @@
98
110
  * blocks: EmailBlock[],
99
111
  * document: EmailDocument|null,
100
112
  * onEvent: (type: string, payload?: any) => void,
113
+ * t?: import('../i18n/index.js').Translate,
114
+ * labelOf?: (selection: EngineSelection) => string,
101
115
  * }) => Promise<void>,
102
116
  * destroy: () => void,
103
117
  * addBlock: (block: EmailBlock, options?: {targetId?: string|null}) => string|null,
@@ -120,13 +134,15 @@
120
134
  * getProject: () => object,
121
135
  * loadProject: (project: object) => void,
122
136
  * loadMjml: (mjml: string) => void,
137
+ * setMjml?: (mjml: string) => void,
123
138
  * exportMjml: () => string,
124
- * exportHtml: (mjml?: string|null) => HtmlExport,
139
+ * exportHtml: (mjml?: string|null) => Promise<HtmlExport>,
125
140
  * setDevice: (device: EmailDevice) => void,
126
141
  * getDevice: () => EmailDevice,
127
142
  * startBlockDrag: (block: EmailBlock, event: Event) => void,
128
143
  * endBlockDrag: (cancel?: boolean) => void,
129
144
  * addShortcut: (id: string, keys: string, handler: () => void) => (() => void),
145
+ * selectAll?: () => void,
130
146
  * }} EmailEditorAdapter
131
147
  */
132
148
  /**
@@ -163,6 +179,7 @@
163
179
  * duplicate: (id?: string|null) => string|null,
164
180
  * move: (direction: -1|1, id?: string|null) => boolean,
165
181
  * select: (id: string|null) => void,
182
+ * selectAll: () => void,
166
183
  * getSelection: () => EmailSelection|null,
167
184
  * getElement: (id: string) => EmailSelection|null,
168
185
  * getRootId: () => string|null,
@@ -175,10 +192,12 @@
175
192
  * canUndo: () => boolean,
176
193
  * canRedo: () => boolean,
177
194
  * getDocument: () => EmailDocument,
178
- * loadDocument: (document: unknown) => void,
195
+ * loadDocument: (document: unknown, options?: {mjml?: string|null}) => void,
179
196
  * loadMjml: (mjml: string) => void,
197
+ * setMjml: (mjml: string) => void,
180
198
  * exportMjml: () => string,
181
- * exportHtml: (mjml?: string|null) => HtmlExport,
199
+ * exportHtml: (mjml?: string|null) => Promise<HtmlExport>,
200
+ * setBrand: (brand: import('../features/brand/brand.js').Brand|null) => void,
182
201
  * setDevice: (device: EmailDevice) => void,
183
202
  * getDevice: () => EmailDevice,
184
203
  * startBlockDrag: (blockId: string, event: Event) => void,
@@ -187,18 +206,33 @@
187
206
  * on: (event: string, listener: (payload?: any) => void) => () => void,
188
207
  * }} EmailEditor
189
208
  */
209
+ /**
210
+ * A block with its MJML (and section MJML) as strings, its texts in the
211
+ * translator's language.
212
+ * @param {EmailBlock} block
213
+ * @param {import('../i18n/index.js').Translate} [t]
214
+ * @returns {EmailBlock & {mjml?: string, sectionMjml?: string}}
215
+ */
216
+ export function resolveBlock(block: EmailBlock, t?: import("../i18n/index.js").Translate): EmailBlock & {
217
+ mjml?: string;
218
+ sectionMjml?: string;
219
+ };
190
220
  /**
191
221
  * @param {{
192
222
  * adapter: EmailEditorAdapter,
193
223
  * blocks: import('./registry.js').Registry<EmailBlock>,
194
224
  * features?: import('./registry.js').Registry<EmailBuilderFeature> | null,
225
+ * t?: import('../i18n/index.js').Translate,
195
226
  * }} options
227
+ * `t`: the interface's translator — texts of inserted blocks, names shown
228
+ * in the canvas.
196
229
  * @returns {EmailEditor}
197
230
  */
198
- export function createEmailEditor({ adapter, blocks, features }: {
231
+ export function createEmailEditor({ adapter, blocks, features, t }: {
199
232
  adapter: EmailEditorAdapter;
200
233
  blocks: import("./registry.js").Registry<EmailBlock>;
201
234
  features?: import("./registry.js").Registry<EmailBuilderFeature> | null;
235
+ t?: import("../i18n/index.js").Translate;
202
236
  }): EmailEditor;
203
237
  export type EmailDevice = import("./constants.js").EmailDevice;
204
238
  export type EmailDocument = import("./document.js").EmailDocument;
@@ -229,7 +263,9 @@ export type BlockProperty = {
229
263
  * `structure` : body, section, column — selectable, not addable.
230
264
  *
231
265
  * `mjml` is the inserted fragment; `sectionMjml` the same wrapped in a
232
- * section, for when it must go outside any column; `tagName` identifies the
266
+ * section, for when it must go outside any column. Either may be a function
267
+ * of the translator, so the texts a block brings arrive in the interface's
268
+ * language; `tagName` identifies the
233
269
  * block of a selected element (`mj-text` → text).
234
270
  * `editableContent`: the content is editable (canvas for text, panel for a
235
271
  * button label).
@@ -245,34 +281,42 @@ export type EmailBlock = {
245
281
  kind: "content" | "layout" | "structure";
246
282
  tagName: string;
247
283
  order?: number;
248
- mjml?: string;
249
- sectionMjml?: string;
284
+ mjml?: string | ((t: import("../i18n/index.js").Translate) => string);
285
+ sectionMjml?: string | ((t: import("../i18n/index.js").Translate) => string);
250
286
  properties: BlockProperty[];
251
287
  editableContent?: boolean;
252
288
  insertOnly?: boolean;
253
289
  };
254
290
  /**
255
- * Selected element as described by the ENGINE: MJML tag and raw attributes,
256
- * nothing more.
291
+ * `content`: the label of a button or a menu link, as plain text.
292
+ * `html`: the HTML of a text (`mj-text`), as stored.
293
+ * `columnCount`: for a section, how many columns it holds (its name).
294
+ * `multiple`: the whole email is selected (Ctrl+A) — how many sections;
295
+ * the selection is then described as the body, and only removal applies.
257
296
  */
258
297
  export type EngineSelection = {
259
298
  id: string;
260
299
  tagName: string;
261
300
  attributes: Record<string, string>;
262
301
  content: string | null;
302
+ html?: string | null;
263
303
  canRemove: boolean;
264
304
  canDuplicate: boolean;
265
305
  canMoveUp: boolean;
266
306
  canMoveDown: boolean;
307
+ columnCount?: number;
308
+ multiple?: number;
267
309
  };
268
310
  /**
269
- * Selected element, in the builder's vocabulary: its block and its property
270
- * values (padding recombined, see mjmlAttributes).
311
+ * Selected element, in the builder's vocabulary: its block, its property
312
+ * values (padding recombined, see mjmlAttributes) and, for the properties left
313
+ * empty, what MJML renders instead (`defaults`, see mjmlDefaults).
271
314
  */
272
315
  export type EmailSelection = EngineSelection & {
273
316
  blockId: string | null;
274
317
  label: string;
275
318
  values: Record<string, string>;
319
+ defaults: Record<string, string>;
276
320
  };
277
321
  export type HtmlExport = {
278
322
  html: string;
@@ -302,6 +346,8 @@ export type EmailEditorAdapter = {
302
346
  blocks: EmailBlock[];
303
347
  document: EmailDocument | null;
304
348
  onEvent: (type: string, payload?: any) => void;
349
+ t?: import("../i18n/index.js").Translate;
350
+ labelOf?: (selection: EngineSelection) => string;
305
351
  }) => Promise<void>;
306
352
  destroy: () => void;
307
353
  addBlock: (block: EmailBlock, options?: {
@@ -326,13 +372,15 @@ export type EmailEditorAdapter = {
326
372
  getProject: () => object;
327
373
  loadProject: (project: object) => void;
328
374
  loadMjml: (mjml: string) => void;
375
+ setMjml?: (mjml: string) => void;
329
376
  exportMjml: () => string;
330
- exportHtml: (mjml?: string | null) => HtmlExport;
377
+ exportHtml: (mjml?: string | null) => Promise<HtmlExport>;
331
378
  setDevice: (device: EmailDevice) => void;
332
379
  getDevice: () => EmailDevice;
333
380
  startBlockDrag: (block: EmailBlock, event: Event) => void;
334
381
  endBlockDrag: (cancel?: boolean) => void;
335
382
  addShortcut: (id: string, keys: string, handler: () => void) => (() => void);
383
+ selectAll?: () => void;
336
384
  };
337
385
  /**
338
386
  * Pluggable feature. `setup` receives the ready editor and may return a
@@ -369,6 +417,7 @@ export type EmailEditor = {
369
417
  duplicate: (id?: string | null) => string | null;
370
418
  move: (direction: -1 | 1, id?: string | null) => boolean;
371
419
  select: (id: string | null) => void;
420
+ selectAll: () => void;
372
421
  getSelection: () => EmailSelection | null;
373
422
  getElement: (id: string) => EmailSelection | null;
374
423
  getRootId: () => string | null;
@@ -381,10 +430,14 @@ export type EmailEditor = {
381
430
  canUndo: () => boolean;
382
431
  canRedo: () => boolean;
383
432
  getDocument: () => EmailDocument;
384
- loadDocument: (document: unknown) => void;
433
+ loadDocument: (document: unknown, options?: {
434
+ mjml?: string | null;
435
+ }) => void;
385
436
  loadMjml: (mjml: string) => void;
437
+ setMjml: (mjml: string) => void;
386
438
  exportMjml: () => string;
387
- exportHtml: (mjml?: string | null) => HtmlExport;
439
+ exportHtml: (mjml?: string | null) => Promise<HtmlExport>;
440
+ setBrand: (brand: import("../features/brand/brand.js").Brand | null) => void;
388
441
  setDevice: (device: EmailDevice) => void;
389
442
  getDevice: () => EmailDevice;
390
443
  startBlockDrag: (blockId: string, event: Event) => void;
@@ -6,7 +6,7 @@
6
6
  * project JSON). Both are written with every template, so a document read by
7
7
  * a newer version knows whether it must be migrated (see `core/document.js`).
8
8
  */
9
- export const EMAIL_BUILDER_VERSION: "0.3.0";
9
+ export const EMAIL_BUILDER_VERSION: "0.5.0";
10
10
  export const EMAIL_DOCUMENT_SCHEMA_VERSION: 1;
11
11
  /** @typedef {'desktop'|'mobile'} EmailDevice */
12
12
  /** @type {EmailDevice[]} */
@@ -0,0 +1,13 @@
1
+ /**
2
+ * MJML DEFAULTS — what MJML 5 renders when an attribute is not written.
3
+ *
4
+ * The properties panel shows them, greyed, in empty fields: an empty
5
+ * "Padding" of a section is not "0" — MJML gives it 20 px above and below,
6
+ * and the panel must say so (and start from there when one side is edited).
7
+ *
8
+ * Copied from the `defaultAttributes` of the mjml 5 components (the same
9
+ * version the server and the canvas compile with); the unit tests check them
10
+ * against the installed components.
11
+ */
12
+ /** @type {Record<string, Record<string, string>>} */
13
+ export const MJML_DEFAULTS: Record<string, Record<string, string>>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Scrollbars inside the builder's frames (the GrapesJS canvas, the preview),
2
+ * Scrollbars inside the builder's frames (the canvas, the preview),
3
3
  * the same as in the rest of the builder: a 5.6 px gutter, a 2.8 px thumb,
4
4
  * no arrows, shown only while the pointer is over the scrolling block. A frame is a
5
5
  * document of its own, so `styles.css` does not reach it — the same rules
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * An editor wired to the default engine and the built-in registries.
3
3
  *
4
- * @param {{locale?: string}} [options] `locale`: engine UI locale (`'en'` by default)
4
+ * @param {{locale?: string, t?: import('./i18n/index.js').Translate}} [options]
5
+ * `t`: the interface's translator (texts of inserted blocks, names in the
6
+ * canvas); by default the built-in one of `locale` (`'en'`).
5
7
  */
6
8
  export function createDefaultEmailEditor(options?: {
7
9
  locale?: string;
10
+ t?: import("./i18n/index.js").Translate;
8
11
  }): import("./core/EmailEditor.js").EmailEditor;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @param {{
3
+ * loadCompiler?: () => Promise<(mjml: string) => Promise<{html: string, errors: any[]}>>,
4
+ * }} [options]
5
+ * @returns {EmailEditorAdapter & {readonly view: any, readonly root: EmailNode|null}}
6
+ */
7
+ export function createNativeAdapter({ loadCompiler }?: {
8
+ loadCompiler?: () => Promise<(mjml: string) => Promise<{
9
+ html: string;
10
+ errors: any[];
11
+ }>>;
12
+ }): EmailEditorAdapter & {
13
+ readonly view: any;
14
+ readonly root: EmailNode | null;
15
+ };
16
+ /**
17
+ * @typedef {import('../core/EmailEditor.js').EmailEditorAdapter} EmailEditorAdapter
18
+ * @typedef {import('../core/EmailEditor.js').EmailBlock} EmailBlock
19
+ * @typedef {import('../core/EmailEditor.js').EngineSelection} EngineSelection
20
+ * @typedef {import('./model.js').EmailNode} EmailNode
21
+ */
22
+ export const ENGINE_NAME: "beb-mjml";
23
+ export const ENGINE_VERSION: "0.5.0";
24
+ export const EMPTY_MJML: "<mjml><mj-body background-color=\"#eceff4\" width=\"600px\"></mj-body></mjml>";
25
+ export type EmailEditorAdapter = import("../core/EmailEditor.js").EmailEditorAdapter;
26
+ export type EmailBlock = import("../core/EmailEditor.js").EmailBlock;
27
+ export type EngineSelection = import("../core/EmailEditor.js").EngineSelection;
28
+ export type EmailNode = import("./model.js").EmailNode;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @param {{
3
+ * container: HTMLElement,
4
+ * compile: (mjml: string) => Promise<{html: string, errors: any[]}>,
5
+ * getRoot: () => EmailNode,
6
+ * t: (key: string, values?: Record<string, any>) => string,
7
+ * info: (id: string) => {label: string, tag: string, canRemove: boolean, canDuplicate: boolean, canMove: boolean, hasParent: boolean, parentLabel: string}|null,
8
+ * onSelect: (id: string|null) => void,
9
+ * onAction: (action: 'parent'|'duplicate'|'remove', id: string) => void,
10
+ * onDrop: (payload: DragPayload, target: DropTarget) => void,
11
+ * onEditStart: (id: string) => void,
12
+ * onEditInput: (id: string, html: string) => void,
13
+ * onEditEnd: (id: string, html: string) => void,
14
+ * onKeyDown: (event: KeyboardEvent) => void,
15
+ * }} options
16
+ */
17
+ export function createCanvasView(options: {
18
+ container: HTMLElement;
19
+ compile: (mjml: string) => Promise<{
20
+ html: string;
21
+ errors: any[];
22
+ }>;
23
+ getRoot: () => EmailNode;
24
+ t: (key: string, values?: Record<string, any>) => string;
25
+ info: (id: string) => {
26
+ label: string;
27
+ tag: string;
28
+ canRemove: boolean;
29
+ canDuplicate: boolean;
30
+ canMove: boolean;
31
+ hasParent: boolean;
32
+ parentLabel: string;
33
+ } | null;
34
+ onSelect: (id: string | null) => void;
35
+ onAction: (action: "parent" | "duplicate" | "remove", id: string) => void;
36
+ onDrop: (payload: DragPayload, target: DropTarget) => void;
37
+ onEditStart: (id: string) => void;
38
+ onEditInput: (id: string, html: string) => void;
39
+ onEditEnd: (id: string, html: string) => void;
40
+ onKeyDown: (event: KeyboardEvent) => void;
41
+ }): {
42
+ frame: HTMLIFrameElement;
43
+ document: Document;
44
+ render: () => Promise<void>;
45
+ refresh: () => void;
46
+ /** @param {string|null} id */
47
+ setSelection(id: string | null): void;
48
+ /** Ctrl+A: boxes on every given element (empty array: none). @param {string[]} ids */
49
+ setMultiSelection(ids: string[]): void;
50
+ /** @param {'desktop'|'mobile'} next */
51
+ setDevice(next: "desktop" | "mobile"): void;
52
+ getDevice: () => "desktop" | "mobile";
53
+ startDrag: (start: {
54
+ payload: DragPayload;
55
+ label: string;
56
+ source: "host" | "frame";
57
+ pointerId: number;
58
+ captureTarget: Element;
59
+ x: number;
60
+ y: number;
61
+ }) => void;
62
+ cancelDrag: () => void;
63
+ isDragging: () => boolean;
64
+ /** @param {string} id @param {{x: number, y: number}|null} [at] */
65
+ startEditing: (id: string, at?: {
66
+ x: number;
67
+ y: number;
68
+ } | null) => void;
69
+ finishEditing: () => void;
70
+ editingId: () => string | null;
71
+ /** @param {string} text */
72
+ insertText(text: string): boolean;
73
+ /** @param {string} html */
74
+ insertHtml(html: string): boolean;
75
+ destroy(): void;
76
+ };
77
+ /** @typedef {import('../model.js').EmailNode} EmailNode */
78
+ /** @typedef {import('../placement.js').DropTarget} DropTarget */
79
+ /** @typedef {{kind: 'content'|'layout', nodeId?: string|null, block?: any}} DragPayload */
80
+ /** Elements edited as rich text in the canvas. */
81
+ export const EDITABLE_TAGS: Set<string>;
82
+ export type EmailNode = import("../model.js").EmailNode;
83
+ export type DropTarget = import("../placement.js").DropTarget;
84
+ export type DragPayload = {
85
+ kind: "content" | "layout";
86
+ nodeId?: string | null;
87
+ block?: any;
88
+ };
@@ -0,0 +1,3 @@
1
+ export const CANVAS_CSS: "\nhtml, body { min-height: 100%; }\nbody { margin: 0; }\n.eb-node { cursor: default; }\n.eb-node a { cursor: pointer; }\n[contenteditable=\"true\"] { outline: none; cursor: text; caret-color: #5b4ff0; }\ntd.eb-placeholder { outline: 1.5px dashed #c3cad6; outline-offset: -8px; }\n.eb-placeholder { user-select: none; }\n\n#eb-ui { position: absolute; top: 0; left: 0; width: 0; height: 0; z-index: 2147483000;\n font: 500 11px/1.2 system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; }\n#eb-ui * { box-sizing: border-box; }\n#eb-ui [hidden] { display: none !important; }\n.eb-ui-box { position: absolute; pointer-events: none; }\n.eb-ui-hover { box-shadow: inset 0 0 0 1px rgba(91, 79, 240, 0.55); }\n.eb-ui-select { box-shadow: inset 0 0 0 2px #5b4ff0; }\n.eb-ui-chip { position: absolute; pointer-events: none; white-space: nowrap; color: #fff; background: #5b4ff0;\n padding: 4px 7px; border-radius: 5px; font-weight: 600; letter-spacing: 0.01em; }\n.eb-ui-hover-chip { background: rgba(91, 79, 240, 0.85); }\n.eb-ui-tools { position: absolute; display: flex; gap: 1px; padding: 2px; border-radius: 6px; background: #5b4ff0;\n box-shadow: 0 4px 12px -4px rgba(40, 30, 120, 0.5); pointer-events: auto; }\n.eb-ui-tools button { all: unset; box-sizing: border-box; width: 26px; height: 24px; display: grid; place-items: center;\n color: #fff; border-radius: 4px; cursor: pointer; }\n.eb-ui-tools button:hover, .eb-ui-tools button:focus-visible { background: rgba(255, 255, 255, 0.2); }\n.eb-ui-tools button[data-action=\"move\"] { cursor: grab; touch-action: none; }\n.eb-ui-tools button.eb-ui-parent { width: auto; gap: 4px; padding: 0 7px 0 5px; display: inline-flex; align-items: center;\n font-weight: 600; white-space: nowrap; }\n.eb-ui-tools button.eb-ui-parent + button { margin-left: 2px; box-shadow: -1px 0 0 rgba(255, 255, 255, 0.25); }\n.eb-ui-drop { position: absolute; height: 4px; margin-top: -2px; border-radius: 2px; background: #5b4ff0;\n box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.85); pointer-events: none; }\n.eb-ui-drop::before, .eb-ui-drop::after { content: ''; position: absolute; top: -3px; width: 10px; height: 10px;\n border-radius: 50%; background: #5b4ff0; box-shadow: 0 0 0 2px #fff; }\n.eb-ui-drop::before { left: -5px; }\n.eb-ui-drop::after { right: -5px; }\n.eb-ui-text { position: absolute; display: flex; align-items: center; gap: 1px; padding: 3px; border-radius: 8px;\n background: #fff; border: 1px solid #dfe3ea; box-shadow: 0 8px 22px -10px rgba(16, 24, 40, 0.35); pointer-events: auto; }\n.eb-ui-text button { all: unset; box-sizing: border-box; width: 28px; height: 26px; display: grid; place-items: center;\n color: #2b3038; border-radius: 5px; cursor: pointer; }\n.eb-ui-text button:hover, .eb-ui-text button:focus-visible { background: #eef0f6; }\n.eb-ui-text button[aria-pressed=\"true\"] { background: #e8e6fd; color: #5b4ff0; }\n.eb-ui-text .eb-ui-sep { width: 1px; height: 18px; margin: 0 3px; background: #e3e6ec; }\n.eb-ui-text input { all: unset; box-sizing: border-box; width: 220px; height: 26px; padding: 0 8px; border-radius: 5px;\n border: 1px solid #dfe3ea; font: 400 12px/1 system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; color: #2b3038; }\n.eb-ui-text input:focus { border-color: #5b4ff0; }\n\n::-webkit-scrollbar{width:5.6px;height:5.6px;background:transparent}\n::-webkit-scrollbar-button{display:none;width:0;height:0}\n::-webkit-scrollbar-track,::-webkit-scrollbar-track-piece,::-webkit-scrollbar-corner{background:transparent;border:0;box-shadow:none}\n::-webkit-scrollbar-thumb{background-color:rgba(156,163,175,.45);border:1.4px solid transparent;background-clip:content-box;border-radius:9999px;min-height:2rem;min-width:2rem}\n::-webkit-scrollbar-thumb:hover{background-color:rgba(107,114,128,.7)}\n@supports not selector(::-webkit-scrollbar){*{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.45) transparent}}\n@media (hover:hover) and (pointer:fine){\n::-webkit-scrollbar-thumb{background-color:transparent}\n*:hover::-webkit-scrollbar-thumb{background-color:rgba(156,163,175,.45)}\n*:hover::-webkit-scrollbar-thumb:hover{background-color:rgba(107,114,128,.7)}\n@supports not selector(::-webkit-scrollbar){*{scrollbar-color:transparent transparent}*:hover{scrollbar-color:rgba(156,163,175,.45) transparent}}\n}\n";
2
+ /** The drag label, in the host page (outside the frame): inline styles only. */
3
+ export const GHOST_STYLE: string;
@@ -0,0 +1,14 @@
1
+ export namespace ICONS {
2
+ let parent: string;
3
+ let move: string;
4
+ let duplicate: string;
5
+ let remove: string;
6
+ let bold: string;
7
+ let italic: string;
8
+ let underline: string;
9
+ let strike: string;
10
+ let link: string;
11
+ let unlink: string;
12
+ let clear: string;
13
+ let check: string;
14
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * MORPH — updates the canvas DOM to a newly compiled email in place.
3
+ *
4
+ * Replacing the whole page at every change would reload the images (the
5
+ * email jumps while they come back), lose the scroll position and the caret
6
+ * of a text being typed. Nodes are compared position by position instead:
7
+ * same kind of node → its attributes and children are updated, otherwise it
8
+ * is replaced. An unchanged image keeps its element, so it never reloads.
9
+ *
10
+ * `skip(element)`: an element left exactly as it is, children included (the
11
+ * text being edited: the browser owns it until the edit ends).
12
+ */
13
+ /**
14
+ * @param {Element} from @param {Element} to
15
+ */
16
+ export function syncAttributes(from: Element, to: Element): void;
17
+ /**
18
+ * Makes `from`'s children match `to`'s.
19
+ * @param {Node} from @param {Node} to @param {(element: Element) => boolean} [skip]
20
+ */
21
+ export function morphChildren(from: Node, to: Node, skip?: (element: Element) => boolean): void;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Grey frame of an image without a file (an SVG data URL, canvas only),
3
+ * drawn at the width it will be shown so its text stays readable in a narrow
4
+ * column (it says where to choose the image only when there is room).
5
+ * @param {string} label @param {number} [width]
6
+ */
7
+ export function imagePlaceholder(label: string, width?: number): string;
8
+ /**
9
+ * @param {EmailNode} root
10
+ * @param {{t?: (key: string) => string}} [options]
11
+ * @returns {string}
12
+ */
13
+ export function canvasMjml(root: EmailNode, { t }?: {
14
+ t?: (key: string) => string;
15
+ }): string;
16
+ /**
17
+ * Id of the model element an HTML element belongs to (its own marker or its
18
+ * closest marked ancestor's), or null.
19
+ * @param {Element|null} element
20
+ * @returns {{id: string, element: Element}|null}
21
+ */
22
+ export function markedAncestor(element: Element | null): {
23
+ id: string;
24
+ element: Element;
25
+ } | null;
26
+ export const NODE_CLASS: "eb-node";
27
+ export const ID_CLASS_PREFIX: "eb-id-";
28
+ export const TAG_CLASS_PREFIX: "eb-tag-";
29
+ export const PLACEHOLDER_CLASS: "eb-placeholder";
30
+ export type EmailNode = import("./model.js").EmailNode;
@@ -0,0 +1,31 @@
1
+ /** @param {string} value */
2
+ export function decodeAttribute(value: string): string;
3
+ /** @param {string} value */
4
+ export function encodeAttribute(value: string): string;
5
+ /**
6
+ * MJML elements of a fragment (no `<mjml>` needed): `<mj-section>…</mj-section><mj-section>…`.
7
+ * @param {string} source
8
+ * @returns {EmailNode[]}
9
+ */
10
+ export function parseMjmlFragment(source: string): EmailNode[];
11
+ /**
12
+ * A whole email. Always returns an `mjml` root holding an `mj-body`: a bare
13
+ * fragment is wrapped, a missing body is added.
14
+ * @param {string} source
15
+ * @returns {EmailNode}
16
+ */
17
+ export function parseMjml(source: string): EmailNode;
18
+ /**
19
+ * The MJML of a tree. `pretty`: one element per line, indented (what is
20
+ * stored and downloaded); otherwise on one line.
21
+ * @param {EmailNode} node
22
+ * @param {{pretty?: boolean, attributes?: (node: EmailNode) => Record<string, string>, extraChildren?: (node: EmailNode) => string}} [options]
23
+ * `attributes`: replaces a node's attributes on write (the canvas adds its
24
+ * markers there); `extraChildren`: MJML appended inside a container.
25
+ */
26
+ export function serializeMjml(node: EmailNode, options?: {
27
+ pretty?: boolean;
28
+ attributes?: (node: EmailNode) => Record<string, string>;
29
+ extraChildren?: (node: EmailNode) => string;
30
+ }): string;
31
+ export type EmailNode = import("./model.js").EmailNode;