best-email-builder 0.5.0 → 0.5.1

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 (46) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +14 -3
  3. package/dist/ai-server.js +4 -4
  4. package/dist/ai.js +7 -7
  5. package/dist/builder.js +9 -9
  6. package/dist/{chunk-3LKF4QBL.js → chunk-2RKVJQSD.js} +4 -4
  7. package/dist/{chunk-6D3SLT5W.js → chunk-2Y7CDXZM.js} +4 -4
  8. package/dist/{chunk-ECHGJZ2W.js → chunk-4U7L2NYQ.js} +32 -3
  9. package/dist/chunk-4U7L2NYQ.js.map +1 -0
  10. package/dist/{chunk-PB7YZ3WP.js → chunk-7QQH6MTV.js} +19 -65
  11. package/dist/chunk-7QQH6MTV.js.map +1 -0
  12. package/dist/{chunk-MPX5CWTZ.js → chunk-BAA5XMWY.js} +100 -12
  13. package/dist/chunk-BAA5XMWY.js.map +1 -0
  14. package/dist/{chunk-LYMJ2TBX.js → chunk-CNJZLZRH.js} +2 -2
  15. package/dist/{chunk-7U6BAKM7.js → chunk-H4W65GBX.js} +3 -3
  16. package/dist/{chunk-7U6BAKM7.js.map → chunk-H4W65GBX.js.map} +1 -1
  17. package/dist/{chunk-DFNHICTF.js → chunk-JI36FNWE.js} +3 -1
  18. package/dist/chunk-JI36FNWE.js.map +1 -0
  19. package/dist/{chunk-LXR4QTGP.js → chunk-N7DEVAIP.js} +3 -3
  20. package/dist/{chunk-2RZHGAH5.js → chunk-OWVXKMLI.js} +59 -2
  21. package/dist/chunk-OWVXKMLI.js.map +1 -0
  22. package/dist/{chunk-KXJJSVN5.js → chunk-SAIW46IC.js} +2 -2
  23. package/dist/{chunk-5SASKCUC.js → chunk-W424KPCR.js} +2 -2
  24. package/dist/index.js +11 -11
  25. package/dist/library.js +5 -5
  26. package/dist/server.js +12 -29
  27. package/dist/server.js.map +1 -1
  28. package/dist/types/ai/draft.d.ts +12 -0
  29. package/dist/types/ai/providers.d.ts +10 -2
  30. package/dist/types/core/constants.d.ts +1 -1
  31. package/dist/types/engine/NativeAdapter.d.ts +1 -1
  32. package/dist/types/renderer/prepareMjml.d.ts +8 -2
  33. package/dist/types/server/index.d.ts +1 -1
  34. package/dist/types/server/render.d.ts +5 -2
  35. package/package.json +1 -1
  36. package/dist/chunk-2RZHGAH5.js.map +0 -1
  37. package/dist/chunk-DFNHICTF.js.map +0 -1
  38. package/dist/chunk-ECHGJZ2W.js.map +0 -1
  39. package/dist/chunk-MPX5CWTZ.js.map +0 -1
  40. package/dist/chunk-PB7YZ3WP.js.map +0 -1
  41. /package/dist/{chunk-3LKF4QBL.js.map → chunk-2RKVJQSD.js.map} +0 -0
  42. /package/dist/{chunk-6D3SLT5W.js.map → chunk-2Y7CDXZM.js.map} +0 -0
  43. /package/dist/{chunk-LYMJ2TBX.js.map → chunk-CNJZLZRH.js.map} +0 -0
  44. /package/dist/{chunk-LXR4QTGP.js.map → chunk-N7DEVAIP.js.map} +0 -0
  45. /package/dist/{chunk-KXJJSVN5.js.map → chunk-SAIW46IC.js.map} +0 -0
  46. /package/dist/{chunk-5SASKCUC.js.map → chunk-W424KPCR.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,37 @@ All notable changes are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.1] — 2026-10-15
10
+
11
+ ### Fixed
12
+ - **An image with no picture yet is left out of the email.** `<mj-image>`
13
+ without `src` (AI drafts leave them for the user, so do the gallery's
14
+ newsletter and promotion) compiled to `<img src="">` — a broken image in
15
+ every inbox, a red cross in Outlook. Saving, exporting and sending now leave
16
+ it out; the canvas and the preview (`/render`) still draw the frame to fill.
17
+ Saving says how many are left out. `prepareMjml(…, { emptyImages: 'keep' })`
18
+ keeps them; `countImagesWithoutPicture(mjml)` counts them.
19
+ - **Drafts through `openrouter/free` were cut short.** Most free models
20
+ reason first — 5 000 to 12 000 tokens before the email — and hit the limit
21
+ of 16 000 (`TRUNCATED`). OpenRouter now asks for less reasoning
22
+ (`reasoning: { effort: 'low' }`) and allows 32 000 tokens.
23
+ - **AI drafts, what free models get wrong, fixed without asking again:** an
24
+ `<mj-image>` put inside a text (dead HTML there) becomes an image block in
25
+ its place; attributes MJML reports as illegal (`align` on a column) are
26
+ removed after compiling; a button whose words do not read on its fill (a
27
+ text color given, the fill forgotten) gets white or ink words. The prompt
28
+ shows the greeting's default in three languages: a French email no longer
29
+ starts with "Hello … there".
30
+ - **A 403 was always the key.** One about the model (only for some apps, not
31
+ in this region), the content (moderation) or the money now says so
32
+ (`MODEL_ERROR`, `REFUSED`, `MODEL_CREDITS`), and `askKey` keeps the user's
33
+ key. Gemini's wrong key (a 400) is `KEY_REFUSED`.
34
+
35
+ ### Added
36
+ - `providerEmailModel({ body })` and `provider: { body, maxTokens }`: fields
37
+ of your own in each request (OpenRouter's `models` fallback list, `reasoning`)
38
+ and a provider's own output limit.
39
+
9
40
  ## [0.5.0] — 2026-10-15
10
41
 
11
42
  ### Changed — the AI is a module (breaking)
package/README.md CHANGED
@@ -373,8 +373,8 @@ only, the key is in the page's code: `createAi({ provider: 'groq', apiKey })`.
373
373
  #### Providers
374
374
 
375
375
  Every provider here speaks the OpenAI *chat completions* API; nothing to
376
- install. `provider` is an id below, or `{ id, model, baseUrl, headers }` to
377
- change the model or reach an endpoint of your own. `AI_PROVIDERS` lists them
376
+ install. `provider` is an id below, or `{ id, model, baseUrl, headers, maxTokens, body }`
377
+ to change the model or reach an endpoint of your own. `AI_PROVIDERS` lists them
378
378
  (name, address, default model, where to get a key, what is free).
379
379
 
380
380
  | `provider` | Default model | Free to start | Key |
@@ -396,6 +396,15 @@ providerEmailModel({ provider: { baseUrl: 'http://localhost:1234/v1', model: 'qw
396
396
 
397
397
  - Model names change often: the defaults were current at this release —
398
398
  pass `model` to be sure of yours.
399
+ - `openrouter/free` costs nothing and is slow: most free models reason before
400
+ they write, so an email takes two to six minutes, and some days a free
401
+ model is busy upstream. OpenRouter is asked for less reasoning
402
+ (`reasoning: { effort: 'low' }`) and given room for it (`maxTokens: 32000`;
403
+ 16 000 elsewhere). For drafts in seconds, use a free tier with a key of its
404
+ own (Gemini, Groq, Cerebras) or a paid model.
405
+ - `body` adds fields of your own to each request — OpenRouter's fallback list,
406
+ for one: `providerEmailModel({ provider: 'openrouter', apiKey, body: { models: [...] } })`.
407
+ It cannot change the model, the messages or the output limit.
399
408
  - The answer is asked as structured output (a JSON schema); a model that
400
409
  refuses it is asked for a JSON object, then for plain JSON — once, then
401
410
  remembered. A model whose structured output breaks the JSON (some do on the
@@ -403,7 +412,9 @@ providerEmailModel({ provider: { baseUrl: 'http://localhost:1234/v1', model: 'qw
403
412
  - The provider's failures become errors the interface explains, in its
404
413
  language: `KEY_MISSING`, `KEY_REFUSED`, `MODEL_CREDITS`, `MODEL_BUSY`,
405
414
  `MODEL_ERROR` (with the provider's own text in `detail`). None is a 401:
406
- the key is the AI's, not the user's session.
415
+ the key is the AI's, not the user's session. A 403 about the model (not
416
+ available here, in this region) or the content is not `KEY_REFUSED`: with
417
+ `askKey`, the user's key is not thrown away for it.
407
418
  - **Claude**: `claudeEmailModel({ client: new Anthropic() })` with the
408
419
  Anthropic SDK (`npm install @anthropic-ai/sdk`) — `claude-opus-5-5` by
409
420
  default, `effort: 'medium'`, structured output, streamed so a long email
package/dist/ai-server.js CHANGED
@@ -29,10 +29,10 @@ import {
29
29
  reviewDraft,
30
30
  reviewRewrite,
31
31
  reviewSubjects
32
- } from "./chunk-MPX5CWTZ.js";
33
- import "./chunk-2RZHGAH5.js";
34
- import "./chunk-5SASKCUC.js";
35
- import "./chunk-DFNHICTF.js";
32
+ } from "./chunk-BAA5XMWY.js";
33
+ import "./chunk-OWVXKMLI.js";
34
+ import "./chunk-W424KPCR.js";
35
+ import "./chunk-JI36FNWE.js";
36
36
  export {
37
37
  AI_MESSAGES,
38
38
  AI_PROVIDERS,
package/dist/ai.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  cleanRichText
3
- } from "./chunk-KXJJSVN5.js";
3
+ } from "./chunk-SAIW46IC.js";
4
4
  import {
5
5
  Button,
6
6
  Dialog,
@@ -23,8 +23,8 @@ import {
23
23
  createHttpCaller,
24
24
  useSearchShortcut,
25
25
  useUi
26
- } from "./chunk-LXR4QTGP.js";
27
- import "./chunk-7U6BAKM7.js";
26
+ } from "./chunk-N7DEVAIP.js";
27
+ import "./chunk-H4W65GBX.js";
28
28
  import {
29
29
  AI_MESSAGES,
30
30
  AI_PROVIDERS,
@@ -43,13 +43,13 @@ import {
43
43
  providerEmailModel,
44
44
  purposesByCategory,
45
45
  resolveProvider
46
- } from "./chunk-MPX5CWTZ.js";
47
- import "./chunk-2RZHGAH5.js";
48
- import "./chunk-5SASKCUC.js";
46
+ } from "./chunk-BAA5XMWY.js";
47
+ import "./chunk-OWVXKMLI.js";
48
+ import "./chunk-W424KPCR.js";
49
49
  import {
50
50
  brandTemplate,
51
51
  hasBrand
52
- } from "./chunk-DFNHICTF.js";
52
+ } from "./chunk-JI36FNWE.js";
53
53
 
54
54
  // src/ai/keys.js
55
55
  var STORAGE_PREFIX = "best-email-builder:ai-key:";
package/dist/builder.js CHANGED
@@ -1,26 +1,26 @@
1
1
  import {
2
2
  EmailBuilder,
3
3
  EmailBuilderBoundary
4
- } from "./chunk-PB7YZ3WP.js";
4
+ } from "./chunk-7QQH6MTV.js";
5
5
  import {
6
6
  STARTER_MJML,
7
7
  TEMPLATE_GALLERY,
8
8
  buildGallery,
9
9
  createStarterMjml
10
- } from "./chunk-3LKF4QBL.js";
11
- import "./chunk-ECHGJZ2W.js";
10
+ } from "./chunk-2RKVJQSD.js";
11
+ import "./chunk-4U7L2NYQ.js";
12
12
  import "./chunk-UFKCLU2D.js";
13
- import "./chunk-KXJJSVN5.js";
13
+ import "./chunk-SAIW46IC.js";
14
14
  import {
15
15
  StorageError,
16
16
  createHttpStorage,
17
17
  createMemoryStorage
18
- } from "./chunk-LXR4QTGP.js";
18
+ } from "./chunk-N7DEVAIP.js";
19
19
  import {
20
20
  EMAIL_BUILDER_VERSION
21
- } from "./chunk-7U6BAKM7.js";
22
- import "./chunk-2RZHGAH5.js";
23
- import "./chunk-5SASKCUC.js";
21
+ } from "./chunk-H4W65GBX.js";
22
+ import "./chunk-OWVXKMLI.js";
23
+ import "./chunk-W424KPCR.js";
24
24
  import {
25
25
  CUSTOM_GROUP,
26
26
  DEFAULT_VARIABLES,
@@ -29,7 +29,7 @@ import {
29
29
  defineVariables,
30
30
  mergeVariables,
31
31
  translatorFor
32
- } from "./chunk-DFNHICTF.js";
32
+ } from "./chunk-JI36FNWE.js";
33
33
  export {
34
34
  CUSTOM_GROUP,
35
35
  DEFAULT_VARIABLES,
@@ -15,10 +15,10 @@ import {
15
15
  TabsTrigger,
16
16
  cn,
17
17
  useUi
18
- } from "./chunk-LXR4QTGP.js";
18
+ } from "./chunk-N7DEVAIP.js";
19
19
  import {
20
20
  EMAIL_MOBILE_WIDTH
21
- } from "./chunk-7U6BAKM7.js";
21
+ } from "./chunk-H4W65GBX.js";
22
22
  import {
23
23
  DEFAULT_BUTTON_URL,
24
24
  FONT_STACK,
@@ -28,7 +28,7 @@ import {
28
28
  identity,
29
29
  inWhiteSection,
30
30
  prop
31
- } from "./chunk-DFNHICTF.js";
31
+ } from "./chunk-JI36FNWE.js";
32
32
 
33
33
  // src/features/templates/starter.js
34
34
  function escapeXml(text) {
@@ -592,4 +592,4 @@ export {
592
592
  useBrand,
593
593
  useLayoutWidth
594
594
  };
595
- //# sourceMappingURL=chunk-3LKF4QBL.js.map
595
+ //# sourceMappingURL=chunk-2RKVJQSD.js.map
@@ -5,7 +5,7 @@ import {
5
5
  useBrand,
6
6
  useLayoutWidth,
7
7
  withPlaceholderImages
8
- } from "./chunk-3LKF4QBL.js";
8
+ } from "./chunk-2RKVJQSD.js";
9
9
  import {
10
10
  Button,
11
11
  Input,
@@ -18,11 +18,11 @@ import {
18
18
  createHttpStorage,
19
19
  useSearchShortcut,
20
20
  useUi
21
- } from "./chunk-LXR4QTGP.js";
21
+ } from "./chunk-N7DEVAIP.js";
22
22
  import {
23
23
  brandTemplate,
24
24
  brandedSender
25
- } from "./chunk-DFNHICTF.js";
25
+ } from "./chunk-JI36FNWE.js";
26
26
 
27
27
  // src/ui/library/TemplateLibrary.jsx
28
28
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -524,4 +524,4 @@ function TemplateLibrary({ locale = "en", messages, onNotify, ...props }) {
524
524
  export {
525
525
  TemplateLibrary
526
526
  };
527
- //# sourceMappingURL=chunk-6D3SLT5W.js.map
527
+ //# sourceMappingURL=chunk-2Y7CDXZM.js.map
@@ -5,7 +5,34 @@ import {
5
5
  parseColor,
6
6
  toHex,
7
7
  toHsl
8
- } from "./chunk-DFNHICTF.js";
8
+ } from "./chunk-JI36FNWE.js";
9
+
10
+ // src/renderer/prepareMjml.js
11
+ var MJ_IMAGE = /<mj-image\b((?:[^>"']|"[^"]*"|'[^']*')*?)\s*(?:\/>|>\s*<\/mj-image\s*>)/gi;
12
+ var HAS_SRC = /\ssrc\s*=\s*(?:"\s*[^"\s][^"]*"|'\s*[^'\s][^']*')/i;
13
+ function countImagesWithoutPicture(mjml) {
14
+ return [...String(mjml || "").matchAll(MJ_IMAGE)].filter(([, attrs]) => !HAS_SRC.test(attrs)).length;
15
+ }
16
+ function escapeXml(text) {
17
+ return String(text ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18
+ }
19
+ function prepareMjml(mjml, { title = null, preheader = null, lang = "en", emptyImages = "drop" } = {}) {
20
+ let out = String(mjml || "").replace(/<mj-title(?:\s[^>]*)?>[\s\S]*?<\/mj-title>/gi, "").replace(/<mj-preview(?:\s[^>]*)?>[\s\S]*?<\/mj-preview>/gi, "");
21
+ if (emptyImages !== "keep") out = out.replace(MJ_IMAGE, (element, attrs) => HAS_SRC.test(attrs) ? element : "");
22
+ out = out.replace(
23
+ /<mjml(\s[^>]*)?>/i,
24
+ (tag, attrs = "") => /\slang\s*=/.test(attrs) ? tag : `<mjml lang="${escapeXml(lang)}"${attrs}>`
25
+ );
26
+ const head = [
27
+ title && String(title).trim() ? `<mj-title>${escapeXml(String(title).trim())}</mj-title>` : "",
28
+ preheader && String(preheader).trim() ? `<mj-preview>${escapeXml(String(preheader).trim())}</mj-preview>` : ""
29
+ ].join("");
30
+ if (!head) return out;
31
+ if (/<mj-head(\s[^>]*)?>/i.test(out)) {
32
+ return out.replace(/<mj-head(\s[^>]*)?>/i, (tag) => `${tag}${head}`);
33
+ }
34
+ return out.replace(/<mjml(\s[^>]*)?>/i, (tag) => `${tag}<mj-head>${head}</mj-head>`);
35
+ }
9
36
 
10
37
  // src/features/brand/extract.js
11
38
  function decodeEntities(text) {
@@ -322,6 +349,8 @@ export {
322
349
  imageSize,
323
350
  logoWidthFor,
324
351
  BRAND_IMPORT_ERRORS,
325
- BRAND_NOTES
352
+ BRAND_NOTES,
353
+ countImagesWithoutPicture,
354
+ prepareMjml
326
355
  };
327
- //# sourceMappingURL=chunk-ECHGJZ2W.js.map
356
+ //# sourceMappingURL=chunk-4U7L2NYQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/renderer/prepareMjml.js","../src/features/brand/extract.js","../src/features/brand/messages.js"],"sourcesContent":["// @ts-check\n/**\n * Prepares MJML before compilation — shared by the server preview, the test\n * send and saving.\n *\n * The subject and the preview text (preheader) are METADATA of the template,\n * typed outside the canvas: they do not live in the engine project. They are\n * written here, into `<mj-head>`, at compile time:\n *\n * `<mj-title>` document title (browser tab, some webmails);\n * `<mj-preview>` the grey text that follows the subject in the inbox.\n *\n * `lang` is set when missing: otherwise screen readers read the message with\n * the voice of the system language.\n *\n * An image with no picture yet (`src` empty or missing — the canvas shows a\n * grey frame, AI drafts leave them for the user) is left out: sent, it would\n * be a broken image in every inbox (a red cross in Outlook). A preview keeps\n * it (`emptyImages: 'keep'`) to draw the frame that is still to fill.\n */\n\n/** An `<mj-image>` element, self-closing or not; its attributes in group 1. */\nconst MJ_IMAGE = /<mj-image\\b((?:[^>\"']|\"[^\"]*\"|'[^']*')*?)\\s*(?:\\/>|>\\s*<\\/mj-image\\s*>)/gi;\nconst HAS_SRC = /\\ssrc\\s*=\\s*(?:\"\\s*[^\"\\s][^\"]*\"|'\\s*[^'\\s][^']*')/i;\n\n/**\n * How many images have no picture yet.\n * @param {string} mjml\n */\nexport function countImagesWithoutPicture(mjml) {\n return [...String(mjml || '').matchAll(MJ_IMAGE)].filter(([, attrs]) => !HAS_SRC.test(attrs)).length;\n}\n\nfunction escapeXml(text) {\n return String(text ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\n/**\n * @param {string} mjml\n * @param {{title?: string|null, preheader?: string|null, lang?: string, emptyImages?: 'drop'|'keep'}} [meta]\n * @returns {string}\n */\nexport function prepareMjml(mjml, { title = null, preheader = null, lang = 'en', emptyImages = 'drop' } = {}) {\n let out = String(mjml || '')\n .replace(/<mj-title(?:\\s[^>]*)?>[\\s\\S]*?<\\/mj-title>/gi, '')\n .replace(/<mj-preview(?:\\s[^>]*)?>[\\s\\S]*?<\\/mj-preview>/gi, '');\n if (emptyImages !== 'keep') out = out.replace(MJ_IMAGE, (element, attrs) => (HAS_SRC.test(attrs) ? element : ''));\n\n out = out.replace(/<mjml(\\s[^>]*)?>/i, (tag, attrs = '') =>\n /\\slang\\s*=/.test(attrs) ? tag : `<mjml lang=\"${escapeXml(lang)}\"${attrs}>`\n );\n\n const head = [\n title && String(title).trim() ? `<mj-title>${escapeXml(String(title).trim())}</mj-title>` : '',\n preheader && String(preheader).trim() ? `<mj-preview>${escapeXml(String(preheader).trim())}</mj-preview>` : '',\n ].join('');\n if (!head) return out;\n\n if (/<mj-head(\\s[^>]*)?>/i.test(out)) {\n return out.replace(/<mj-head(\\s[^>]*)?>/i, (tag) => `${tag}${head}`);\n }\n return out.replace(/<mjml(\\s[^>]*)?>/i, (tag) => `${tag}<mj-head>${head}</mj-head>`);\n}\n","// @ts-check\n/**\n * BRAND FROM A WEBSITE — what a company's home page says about its look:\n * its name, where its logo is, its main color, the color of its headings\n * and its font.\n *\n * Pure text work on what the server downloaded (src/server/brandImport.js\n * does the downloading, with its safety rules): no network, no DOM, so it\n * runs anywhere and is tested on fixtures.\n *\n * name og:site_name → JSON-LD Organization → application-name → the\n * part of <title> that matches the domain → the domain;\n * logo JSON-LD Organization logo → an <img> called \"logo\" (header\n * first) → apple-touch-icon; SVG and WebP are skipped later\n * (most email apps do not show them);\n * colors counted in the CSS, weighted by where they are used (buttons,\n * links, custom properties named primary/brand/accent, the\n * theme-color meta); grays, alert and social colors set aside;\n * font the body's, mapped to the nearest web-safe stack.\n */\n\nimport { FONT_OPTIONS, PALETTE } from '../../blocks/shared.js';\nimport { contrastRatio, parseColor, toHex, toHsl } from './brand.js';\n\n/**\n * @typedef {{\n * name: string,\n * logos: {url: string, from: 'schema'|'img'|'icon'}[],\n * stylesheets: string[],\n * css: string,\n * themeColor: string,\n * }} PageReading\n */\n\n/* ── HTML ───────────────────────────────────────────────────────────────── */\n\n/** @param {string} text */\nfunction decodeEntities(text) {\n return String(text ?? '')\n .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))\n .replace(/&#(\\d+);/g, (_, dec) => String.fromCodePoint(Number(dec)))\n .replace(/&quot;/g, '\"')\n .replace(/&#39;|&apos;/g, \"'\")\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&nbsp;/g, ' ')\n .replace(/&amp;/g, '&');\n}\n\n/**\n * The attributes of one start tag, names in lower case.\n * @param {string} tag `<img src=\"…\" class=logo>`\n * @returns {Record<string, string>}\n */\nexport function tagAttributes(tag) {\n /** @type {Record<string, string>} */\n const attributes = {};\n const inner = tag.replace(/^<[^\\s>/]+/, '').replace(/\\/?>$/, '');\n const pattern = /([^\\s\"'=<>/]+)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+)))?/g;\n let match;\n while ((match = pattern.exec(inner))) {\n const name = match[1].toLowerCase();\n if (!(name in attributes)) attributes[name] = decodeEntities(match[2] ?? match[3] ?? match[4] ?? '');\n }\n return attributes;\n}\n\n/** @param {string} html @param {string} name */\nconst startTags = (html, name) => html.match(new RegExp(`<${name}\\\\b[^>]*>`, 'gi')) || [];\n\n/** An http(s) address, resolved against the page; '' otherwise. @param {string|undefined} href @param {string} base */\nfunction resolve(href, base) {\n const value = String(href ?? '').trim();\n if (!value || /^(data|javascript|blob):/i.test(value)) return '';\n try {\n const url = new URL(value, base);\n return url.protocol === 'https:' || url.protocol === 'http:' ? url.href : '';\n } catch {\n return '';\n }\n}\n\n/** The largest image of a srcset (\"a.png 1x, a@2x.png 2x\"). @param {string} srcset */\nfunction largestOf(srcset) {\n let best = { url: '', size: 0 };\n for (const part of srcset.split(/,\\s+/)) {\n const [url, descriptor = '1x'] = part.trim().split(/\\s+/);\n const size = parseFloat(descriptor) * (descriptor.endsWith('w') ? 1 : 1000);\n if (url && size > best.size) best = { url, size };\n }\n return best.url;\n}\n\n/** Objects of a JSON-LD script, flattened (@graph, arrays). @param {unknown} value @returns {any[]} */\nfunction jsonLdNodes(value) {\n if (Array.isArray(value)) return value.flatMap(jsonLdNodes);\n if (!value || typeof value !== 'object') return [];\n const node = /** @type {any} */ (value);\n return [node, ...jsonLdNodes(node['@graph']), ...jsonLdNodes(node.publisher), ...jsonLdNodes(node.brand)];\n}\n\nconst ORGANIZATION = /organization|corporation|business|brand|store|restaurant|company|ngo|school|airline|bank|agency/i;\n\n/** The words of a host name that name the company: \"www.acme-corp.co.uk\" → \"acme corp\". @param {string} host */\nfunction hostWords(host) {\n const labels = host.toLowerCase().replace(/^www\\d?\\./, '').split('.');\n const main = labels.length > 2 && labels[labels.length - 2].length <= 3 ? labels[labels.length - 3] : labels[0];\n return main.replace(/[-_]+/g, ' ');\n}\n\n/** @param {string} text */\nconst squash = (text) => text.toLowerCase().replace(/[^a-z0-9À-￿]+/g, '');\n\n/** @param {unknown} value */\nconst cleanName = (value) =>\n decodeEntities(String(value ?? ''))\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, 80);\n\n/**\n * What a home page says about its brand.\n * @param {string} html\n * @param {string} pageUrl the address it was read from (after redirects)\n * @returns {PageReading}\n */\nexport function readPage(html, pageUrl) {\n const source = String(html ?? '');\n const head = source.slice(0, 400_000);\n const baseTag = startTags(head, 'base').map(tagAttributes).find((a) => a.href);\n const base = resolve(baseTag?.href, pageUrl) || pageUrl;\n const host = (() => {\n try {\n return new URL(pageUrl).hostname;\n } catch {\n return '';\n }\n })();\n\n /** @type {Record<string, string>} */\n const meta = {};\n for (const attributes of startTags(head, 'meta').map(tagAttributes)) {\n const key = (attributes.property || attributes.name || '').toLowerCase();\n if (key && attributes.content && !(key in meta)) meta[key] = attributes.content;\n }\n\n // JSON-LD: the organization's own name and logo, when the site states them.\n let schemaName = '';\n let schemaLogo = '';\n for (const [, body] of source.matchAll(/<script\\b[^>]*type\\s*=\\s*[\"']?application\\/ld\\+json[^>]*>([\\s\\S]*?)<\\/script>/gi)) {\n let data;\n try {\n data = JSON.parse(body.trim());\n } catch {\n continue;\n }\n for (const node of jsonLdNodes(data)) {\n const types = [].concat(node['@type'] || []).join(' ');\n if (!ORGANIZATION.test(types)) continue;\n if (!schemaName && typeof node.name === 'string') schemaName = node.name;\n const logo = typeof node.logo === 'string' ? node.logo : node.logo?.url || node.logo?.contentUrl || '';\n if (!schemaLogo && typeof logo === 'string') schemaLogo = resolve(logo, base);\n }\n }\n\n /* Name */\n const title = cleanName(head.match(/<title\\b[^>]*>([\\s\\S]*?)<\\/title>/i)?.[1] || '');\n const words = hostWords(host);\n const fromTitle =\n title\n .split(/\\s+[|–—:·•»-]\\s+/)\n .map((part) => part.trim())\n .find((part) => part && squash(part).length <= 40 && (squash(part).includes(squash(words)) || squash(words).includes(squash(part)))) || '';\n const fromHost = words.replace(/\\b\\p{L}/gu, (letter) => letter.toUpperCase());\n const name = cleanName(meta['og:site_name'] || schemaName || meta['application-name'] || fromTitle || fromHost);\n\n /* Logo candidates, best first */\n /** @type {PageReading['logos']} */\n const logos = [];\n const add = (/** @type {string} */ url, /** @type {PageReading['logos'][number]['from']} */ from) => {\n if (url && !logos.some((logo) => logo.url === url)) logos.push({ url, from });\n };\n add(schemaLogo, 'schema');\n /** @type {{url: string, score: number}[]} */\n const images = [];\n const headerEnd = (() => {\n const at = source.search(/<\\/header>/i);\n return at > 0 ? at : Math.min(source.length, 20_000);\n })();\n for (const match of source.matchAll(/<img\\b[^>]*>/gi)) {\n const attributes = tagAttributes(match[0]);\n const own = [attributes.class, attributes.id, attributes.alt, attributes.src, attributes['data-src']].join(' ').toLowerCase();\n const before = source.slice(Math.max(0, (match.index ?? 0) - 400), match.index).toLowerCase();\n const parent = /<(a|div|span|figure|picture)\\b[^>]*(class|id)\\s*=\\s*[\"'][^\"']*logo[^\"']*[\"'][^>]*>[^<]*(<source\\b[^>]*>\\s*)*$/i.test(before);\n if (!own.includes('logo') && !parent) continue;\n // Partner, client and payment logos are someone else's.\n if (/partner|client|customer|sponsor|award|payment|badge|trust|press|app-?store|google-?play/.test(own)) continue;\n const url = resolve(attributes.srcset ? largestOf(attributes.srcset) : attributes.src || attributes['data-src'] || '', base);\n if (!url) continue;\n let score = own.includes('logo') ? 3 : 2;\n if ((match.index ?? 0) < headerEnd) score += 3;\n if (name && squash(attributes.alt || '').includes(squash(name))) score += 2;\n if (/footer/.test(own)) score -= 2;\n images.push({ url, score: score - images.length * 0.01 });\n }\n images.sort((a, b) => b.score - a.score).forEach((image) => add(image.url, 'img'));\n /** @type {{url: string, size: number}[]} */\n const icons = [];\n for (const attributes of startTags(head, 'link').map(tagAttributes)) {\n const rel = (attributes.rel || '').toLowerCase();\n if (!/apple-touch-icon|icon/.test(rel)) continue;\n const size = parseInt(attributes.sizes || (rel.includes('apple') ? '180' : '0'), 10) || 0;\n if (size >= 96) icons.push({ url: resolve(attributes.href, base), size });\n }\n icons.sort((a, b) => b.size - a.size).forEach((icon) => add(icon.url, 'icon'));\n\n /* Styles */\n const stylesheets = startTags(head, 'link')\n .map(tagAttributes)\n .filter((a) => /(^|\\s)stylesheet(\\s|$)/i.test(a.rel || '') && !/^print$/i.test((a.media || '').trim()))\n .map((a) => resolve(a.href, base))\n .filter(Boolean)\n // Fonts and third-party widgets say nothing about the brand's colors.\n .filter((href) => !/fonts\\.googleapis|use\\.typekit|fontawesome|cookie|consent|onetrust/i.test(href));\n const css = [\n ...[...source.matchAll(/<style\\b[^>]*>([\\s\\S]*?)<\\/style>/gi)].map((m) => m[1]),\n // Inline styles of buttons and links count too: `<a class=\"btn\" style=\"…\">`.\n ...[...source.matchAll(/<(a|button)\\b[^>]*>/gi)]\n .map((m) => tagAttributes(m[0]))\n .filter((a) => a.style)\n .map((a) => `${/btn|button|cta/i.test(a.class || '') ? '.btn' : 'a'}{${a.style}}`),\n ].join('\\n');\n\n return {\n name,\n logos,\n stylesheets: [...new Set(stylesheets)],\n css,\n themeColor: meta['theme-color'] || meta['msapplication-tilecolor'] || '',\n };\n}\n\n/* ── CSS ────────────────────────────────────────────────────────────────── */\n\nconst PRIMARY_NAME = /primary|brand|accent|main|theme|highlight|cta/i;\nconst NOT_A_BRAND_NAME = /text|foreground|fg|border|muted|gr[ae]y|neutral|surface|shadow|overlay|success|error|danger|warning|info|disabled|placeholder/i;\nconst NOISE = /error|danger|success|warning|alert|invalid|valid\\b|notice|toast|badge|disabled|facebook|twitter|linkedin|instagram|youtube|whatsapp|pinterest|tiktok|social|cookie|consent|onetrust|ot-|cmp|gdpr|wp-block|has-[\\w-]+-(background-)?color|syntax|hljs|code/i;\nconst COLOR_IN_VALUE = /#[0-9a-f]{3,8}\\b|rgba?\\([^)]*\\)|hsla?\\([^)]*\\)/gi;\nconst COLOR_PROPERTY = /^(color|background|background-color|border|border-color|border-(top|bottom|left|right)(-color)?|fill|stroke|outline(-color)?|text-decoration-color|accent-color|caret-color)$/;\n\n/** @param {string} css */\nconst stripComments = (css) => css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n\n/** Rules as [selector, declarations], @media flattened. @param {string} css */\nfunction rulesOf(css) {\n /** @type {[string, [string, string][]][]} */\n const rules = [];\n for (const [, selector, body] of stripComments(css).matchAll(/([^{}]+)\\{([^{}]*)\\}/g)) {\n const declarations = body\n .split(';')\n .map((part) => {\n const colon = part.indexOf(':');\n return /** @type {[string, string]} */ ([part.slice(0, colon).trim().toLowerCase(), part.slice(colon + 1).trim()]);\n })\n .filter(([property, value]) => property && value);\n if (declarations.length) rules.push([selector.trim(), declarations]);\n }\n return rules;\n}\n\n/**\n * A custom property's value as a color: `#2977bc`, `rgb(…)`, and the bare\n * channels design systems keep there (`41 119 188`, `210 64% 45%`).\n * @param {string} value\n */\nfunction customColor(value) {\n const direct = parseColor(value.replace(/\\s*!important$/, ''));\n if (direct) return direct;\n if (/^\\d{1,3}(\\.\\d+)?\\s+\\d{1,3}(\\.\\d+)?\\s+\\d{1,3}(\\.\\d+)?$/.test(value)) return parseColor(`rgb(${value})`);\n if (/^\\d{1,3}(\\.\\d+)?(deg)?\\s+\\d{1,3}(\\.\\d+)?%\\s+\\d{1,3}(\\.\\d+)?%$/.test(value)) return parseColor(`hsl(${value.replace('deg', '')})`);\n return null;\n}\n\n/**\n * `var(--x, fallback)` replaced by what --x holds, a few levels deep.\n * @param {string} value @param {Map<string, string>} custom\n */\nfunction resolveVars(value, custom) {\n let out = value;\n for (let depth = 0; depth < 4 && out.includes('var('); depth += 1) {\n out = out.replace(/var\\(\\s*(--[\\w-]+)\\s*(?:,\\s*([^()]*(?:\\([^()]*\\))?[^()]*))?\\)/g, (_, name, fallback) => custom.get(name) ?? fallback ?? '');\n }\n return out;\n}\n\n/** @param {{r: number, g: number, b: number}} a @param {{r: number, g: number, b: number}} b */\nconst distance = (a, b) => Math.hypot(a.r - b.r, a.g - b.g, a.b - b.b);\n\n/** Colors of one value, as found (not resolved through var()). @param {string} value */\nconst colorsIn = (value) => (value.match(COLOR_IN_VALUE) || []).map((text) => parseColor(text)).filter((c) => c && c.a >= 0.6);\n\n/**\n * The web-safe stack nearest to a CSS font-family.\n * @param {string} family\n * @returns {string|undefined}\n */\nexport function nearestFont(family) {\n const stack = String(family ?? '')\n .toLowerCase()\n .replace(/[\"']/g, '');\n if (!stack.trim() || /^(inherit|initial|unset|revert)$/.test(stack.trim())) return undefined;\n const first = stack.split(',')[0].trim();\n const known = FONT_OPTIONS.find((option) => first.startsWith(option.label.toLowerCase()));\n if (known) return known.value;\n if (/^helvetica/.test(first)) return FONT_OPTIONS[0].value;\n if (/^times/.test(first)) return FONT_OPTIONS.find((o) => o.label === 'Times')?.value;\n if (/monospace|mono\\b|courier|consolas/.test(stack)) return FONT_OPTIONS.find((o) => o.label === 'Courier')?.value;\n // Web fonts: their family decides (serif, else sans-serif).\n if (/(^|,)\\s*serif\\s*(,|$)|garamond|playfair|merriweather|lora|baskerville|caslon|didot|bodoni|crimson|libre baskerville|source serif|noto serif|pt serif/.test(stack)) {\n return FONT_OPTIONS.find((o) => o.label === 'Georgia')?.value;\n }\n return FONT_OPTIONS[0].value;\n}\n\n/**\n * Main color, headings color and font of a site, from its CSS.\n * @param {{css: string, themeColor?: string}} input\n * @returns {{primary?: string, heading?: string, font?: string}}\n */\nexport function brandFromCss({ css, themeColor = '' }) {\n const rules = rulesOf(String(css ?? ''));\n\n /** @type {Map<string, string>} */\n const custom = new Map();\n for (const [selector, declarations] of rules) {\n // :root first: a theme's dark variant must not replace it.\n const root = /^(:root|html|body|\\*)(\\s*,|$)/i.test(selector);\n for (const [property, value] of declarations) {\n if (property.startsWith('--') && (root || !custom.has(property))) custom.set(property, value.replace(/\\s*!important$/, ''));\n }\n }\n\n /** @type {{color: {r: number, g: number, b: number}, weight: number}[]} */\n const found = [];\n /** @type {{color: {r: number, g: number, b: number}, weight: number}[]} */\n const darkText = [];\n /** @type {{font: string, weight: number}[]} */\n const fonts = [];\n const count = (/** @type {{r: number, g: number, b: number}} */ color, /** @type {number} */ weight) => found.push({ color, weight });\n\n for (const [selector, declarations] of rules) {\n if (selector.startsWith('@')) continue;\n const noise = NOISE.test(selector);\n const emphasis = /btn|button|cta|primary|brand|accent|hero|highlight/i.test(selector) ? 4 : /(^|[\\s,>+~])a([:.[\\s,]|$)|link/i.test(selector) ? 2 : 1;\n const isHeading = /(^|[\\s,>])h[1-3]\\b|title|heading|headline/i.test(selector);\n const isBody = /^(html|body|:root)(\\s*,|$)|(^|,)\\s*(html|body)\\s*(,|$)/i.test(selector);\n for (const [property, raw] of declarations) {\n if (property.startsWith('--')) {\n const color = customColor(resolveVars(raw, custom));\n if (color && color.a >= 0.6 && PRIMARY_NAME.test(property) && !NOT_A_BRAND_NAME.test(property)) count(color, 10);\n continue;\n }\n const value = resolveVars(raw, custom);\n if (property === 'font-family' || property === 'font') {\n const family = property === 'font' ? value.replace(/^.*?\\d[\\w%.]*(\\/[\\w%.]+)?\\s+/, '') : value;\n if (isBody || isHeading) fonts.push({ font: family, weight: isBody ? 2 : 1 });\n continue;\n }\n if (!COLOR_PROPERTY.test(property) || noise) continue;\n for (const color of colorsIn(value)) {\n if (!color) continue;\n count(color, emphasis * (property.startsWith('background') ? 1.5 : 1));\n if (property === 'color' && (isHeading || isBody)) darkText.push({ color, weight: isHeading ? 2 : 1 });\n }\n }\n }\n const theme = parseColor(themeColor);\n if (theme && theme.a >= 0.6) count(theme, 15);\n\n // Near colors are one color (hover shades, rounding).\n /** @type {{color: {r: number, g: number, b: number}, weight: number}[]} */\n const clusters = [];\n for (const entry of found) {\n const near = clusters.find((cluster) => distance(cluster.color, entry.color) < 24);\n if (near) near.weight += entry.weight;\n else clusters.push({ ...entry });\n }\n clusters.sort((a, b) => b.weight - a.weight);\n const primary = clusters.find(({ color }) => {\n const { s, l } = toHsl(color);\n return s >= 0.25 && l >= 0.15 && l <= 0.85;\n });\n\n /** @type {{primary?: string, heading?: string, font?: string}} */\n const result = {};\n if (primary) result.primary = toHex(primary.color);\n\n const dark = darkText\n .filter(({ color }) => toHsl(color).l < 0.3)\n .reduce((/** @type {Map<string, number>} */ map, { color, weight }) => map.set(toHex(color), (map.get(toHex(color)) || 0) + weight), new Map());\n const heading = [...dark.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];\n if (heading) result.heading = heading;\n else if (result.primary && contrastRatio(result.primary, PALETTE.white) >= 7) result.heading = result.primary;\n else result.heading = PALETTE.ink;\n\n const font = fonts.sort((a, b) => b.weight - a.weight).map(({ font }) => nearestFont(font)).find(Boolean);\n if (font) result.font = font;\n return result;\n}\n\n/* ── Images ─────────────────────────────────────────────────────────────── */\n\n/**\n * Width and height of a PNG, GIF or JPEG, from its header; null otherwise.\n * @param {Uint8Array} bytes\n * @returns {{width: number, height: number}|null}\n */\nexport function imageSize(bytes) {\n if (!bytes || bytes.length < 24) return null;\n const at = (/** @type {number} */ i) => bytes[i];\n // PNG: IHDR right after the signature.\n if (at(0) === 0x89 && at(1) === 0x50 && at(2) === 0x4e && at(3) === 0x47) {\n const read32 = (/** @type {number} */ i) => ((at(i) << 24) | (at(i + 1) << 16) | (at(i + 2) << 8) | at(i + 3)) >>> 0;\n return { width: read32(16), height: read32(20) };\n }\n // GIF: logical screen size.\n if (at(0) === 0x47 && at(1) === 0x49 && at(2) === 0x46) {\n return { width: at(6) | (at(7) << 8), height: at(8) | (at(9) << 8) };\n }\n // JPEG: the first start-of-frame marker.\n if (at(0) === 0xff && at(1) === 0xd8) {\n let i = 2;\n while (i + 9 < bytes.length) {\n if (at(i) !== 0xff) {\n i += 1;\n continue;\n }\n const marker = at(i + 1);\n if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0xff) {\n i += marker === 0xff ? 1 : 2;\n continue;\n }\n const length = (at(i + 2) << 8) | at(i + 3);\n if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {\n return { height: (at(i + 5) << 8) | at(i + 6), width: (at(i + 7) << 8) | at(i + 8) };\n }\n i += 2 + length;\n }\n }\n return null;\n}\n\n/**\n * The width a logo is shown at in an email: a wordmark wider, a square mark\n * smaller; never wider than the image itself (a blurry logo is worse than a\n * small one). Retina images (twice the size shown) are common: halved first.\n * @param {{width: number, height: number}} size\n */\nexport function logoWidthFor({ width, height }) {\n if (!width || !height) return 140;\n const ratio = width / height;\n const shown = width >= 240 ? width / 2 : width;\n const target = ratio >= 2.5 ? 180 : ratio >= 1.3 ? 140 : 72;\n return Math.max(24, Math.round(Math.min(target, shown)));\n}\n","// @ts-check\n/**\n * What the user is told about reading a brand from a website: why it failed\n * (the server's answer) and what it could not find (notes under the field).\n * English source strings, translated where they are shown.\n */\n\n/**\n * Why a website could not be read: [HTTP status, message], by\n * BrandImportError code (src/server/brandImport.js).\n * @type {Record<string, [number, string]>}\n */\nexport const BRAND_IMPORT_ERRORS = {\n BAD_URL: [400, 'Enter the address of a website, like acme.com'],\n PRIVATE_ADDRESS: [400, 'This address is not a public website'],\n UNREACHABLE: [422, 'The website could not be reached. Check the address.'],\n TIMEOUT: [422, 'The website took too long to answer'],\n HTTP_ERROR: [422, 'The website does not let the builder read it. Fill in your brand below.'],\n NOT_HTML: [422, 'This address is not a web page'],\n TOO_MANY_REDIRECTS: [422, 'The website redirects too many times'],\n TOO_LARGE: [422, 'The website sent too much data'],\n};\n\n/** What could not be read, by note code. @type {Record<string, string>} */\nexport const BRAND_NOTES = {\n LOGO_NOT_FOUND: 'No logo was found on the website. Upload it below.',\n LOGO_UNSUPPORTED: 'The logo on the website is an SVG or WebP image, which most email apps do not show. Upload a PNG or JPEG version.',\n LOGO_ICON: 'The website icon is used as the logo. Upload your full logo if you have one.',\n COLORS_NOT_FOUND: 'No main color was found on the website. Pick one below.',\n};\n"],"mappings":"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","names":["font"]}
@@ -10,16 +10,17 @@ import {
10
10
  useBrand,
11
11
  useLayoutWidth,
12
12
  withThumbnailImages
13
- } from "./chunk-3LKF4QBL.js";
13
+ } from "./chunk-2RKVJQSD.js";
14
14
  import {
15
15
  BRAND_NOTES,
16
+ countImagesWithoutPicture,
16
17
  logoWidthFor
17
- } from "./chunk-ECHGJZ2W.js";
18
+ } from "./chunk-4U7L2NYQ.js";
18
19
  import {
19
20
  cleanRichText,
20
21
  escapeText,
21
22
  textOfHtml
22
- } from "./chunk-KXJJSVN5.js";
23
+ } from "./chunk-SAIW46IC.js";
23
24
  import {
24
25
  Button,
25
26
  Dialog,
@@ -55,24 +56,25 @@ import {
55
56
  useSearchShortcut,
56
57
  useT,
57
58
  useUi
58
- } from "./chunk-LXR4QTGP.js";
59
+ } from "./chunk-N7DEVAIP.js";
59
60
  import {
60
61
  EMAIL_BUILDER_VERSION,
61
62
  EMAIL_DEVICES,
62
63
  createDocument,
63
64
  imageUploadError,
64
65
  readDocument
65
- } from "./chunk-7U6BAKM7.js";
66
+ } from "./chunk-H4W65GBX.js";
66
67
  import {
67
68
  LONGHAND_SPACING,
69
+ MJML_DEFAULTS,
68
70
  formatSpacing,
69
71
  parseSpacing,
70
72
  toAttributes,
71
73
  toValues
72
- } from "./chunk-2RZHGAH5.js";
74
+ } from "./chunk-OWVXKMLI.js";
73
75
  import {
74
76
  isSafeUrl
75
- } from "./chunk-5SASKCUC.js";
77
+ } from "./chunk-W424KPCR.js";
76
78
  import {
77
79
  CUSTOM_GROUP,
78
80
  DEFAULT_BUTTON_URL,
@@ -121,7 +123,7 @@ import {
121
123
  tokenOf,
122
124
  translatorFor,
123
125
  validateCustomVariable
124
- } from "./chunk-DFNHICTF.js";
126
+ } from "./chunk-JI36FNWE.js";
125
127
 
126
128
  // src/core/events.js
127
129
  function createEmitter({ onError } = {}) {
@@ -154,62 +156,6 @@ function createEmitter({ onError } = {}) {
154
156
  };
155
157
  }
156
158
 
157
- // src/core/mjmlDefaults.js
158
- var MJML_DEFAULTS = {
159
- "mj-body": { width: "600px" },
160
- "mj-section": {
161
- "background-repeat": "repeat",
162
- "background-size": "auto",
163
- "background-position": "top center",
164
- padding: "20px 0",
165
- "text-align": "center"
166
- },
167
- "mj-wrapper": { "background-repeat": "repeat", "background-size": "auto", "background-position": "top center", padding: "20px 0" },
168
- "mj-column": { "vertical-align": "top" },
169
- "mj-text": {
170
- align: "left",
171
- color: "#000000",
172
- "font-family": "Ubuntu, Helvetica, Arial, sans-serif",
173
- "font-size": "13px",
174
- "line-height": "1",
175
- padding: "10px 25px"
176
- },
177
- "mj-image": { align: "center", height: "auto", padding: "10px 25px" },
178
- "mj-button": {
179
- align: "center",
180
- "background-color": "#414141",
181
- "border-radius": "3px",
182
- color: "#ffffff",
183
- "font-family": "Ubuntu, Helvetica, Arial, sans-serif",
184
- "font-size": "13px",
185
- "font-weight": "normal",
186
- "inner-padding": "10px 25px",
187
- "line-height": "120%",
188
- padding: "10px 25px"
189
- },
190
- "mj-divider": { "border-color": "#000000", "border-style": "solid", "border-width": "4px", padding: "10px 25px", width: "100%", align: "center" },
191
- "mj-spacer": { height: "20px" },
192
- "mj-social": {
193
- align: "center",
194
- "border-radius": "3px",
195
- "icon-size": "20px",
196
- mode: "horizontal",
197
- padding: "10px 25px"
198
- },
199
- "mj-social-element": { "border-radius": "3px", padding: "4px" },
200
- "mj-navbar": { align: "center" },
201
- "mj-navbar-link": {
202
- color: "#000000",
203
- "font-family": "Ubuntu, Helvetica, Arial, sans-serif",
204
- "font-size": "13px",
205
- "font-weight": "normal",
206
- "line-height": "22px",
207
- padding: "15px 10px",
208
- "text-transform": "uppercase"
209
- },
210
- "mj-hero": { "background-color": "#ffffff", padding: "0px" }
211
- };
212
-
213
159
  // src/core/EmailEditor.js
214
160
  function resolveBlock(block, t = identity) {
215
161
  const text = (value) => typeof value === "function" ? String(value(t)) : value;
@@ -5274,11 +5220,19 @@ function Builder({
5274
5220
  if (!current.id) clearLocalDraft();
5275
5221
  latest.current.onSaved?.(result.template);
5276
5222
  const warnings = result.warnings || [];
5223
+ const withoutPicture = auto ? 0 : countImagesWithoutPicture(mjml6);
5277
5224
  if (warnings.length) {
5278
5225
  notify(
5279
5226
  "warning",
5280
5227
  t("Saved, with {count} MJML warning(s): {message}", { count: warnings.length, message: warnings[0].message })
5281
5228
  );
5229
+ } else if (withoutPicture) {
5230
+ notify(
5231
+ "warning",
5232
+ t("Saved. {count} image(s) have no picture yet: they are left out of the email until you add one.", {
5233
+ count: withoutPicture
5234
+ })
5235
+ );
5282
5236
  } else if (!auto) {
5283
5237
  notify(
5284
5238
  "success",
@@ -5632,4 +5586,4 @@ export {
5632
5586
  EmailBuilder,
5633
5587
  EmailBuilderBoundary
5634
5588
  };
5635
- //# sourceMappingURL=chunk-PB7YZ3WP.js.map
5589
+ //# sourceMappingURL=chunk-7QQH6MTV.js.map