@seliseblocks/mailcraft 0.2.16 → 0.2.19

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 (78) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/DOCS.md +70 -67
  3. package/README.md +8 -8
  4. package/README.md.txt +8 -8
  5. package/dist/mailcraft-editor.bundle.js +89 -76
  6. package/dist/mailcraft-editor.bundle.js.map +3 -3
  7. package/examples/templates/activate-your-account.html +1 -1
  8. package/examples/templates/back-in-stock.html +4 -4
  9. package/examples/templates/cart-left-behind.html +3 -3
  10. package/examples/templates/community-giveaway.html +3 -3
  11. package/examples/templates/frontend-futures-invite.html +3 -3
  12. package/examples/templates/give-25-get-25.html +2 -2
  13. package/examples/templates/invoice-paid.html +3 -3
  14. package/examples/templates/meet-nova-launch.html +7 -7
  15. package/examples/templates/mega-weekend-sale.html +1 -1
  16. package/examples/templates/order-confirmed.html +6 -6
  17. package/examples/templates/rate-your-headphones.html +3 -3
  18. package/examples/templates/reset-your-password.html +1 -1
  19. package/examples/templates/thankyou-promo-code.html +3 -3
  20. package/examples/templates/the-sunday-brief.html +4 -4
  21. package/examples/templates/welcome-to-your-workspace.html +4 -4
  22. package/examples/templates/your-order-shipped.html +2 -2
  23. package/examples/templates/your-password-was-changed.html +2 -2
  24. package/examples/templates/your-signin-code.html +1 -1
  25. package/examples/vanilla.html +66 -66
  26. package/package.json +1 -1
  27. package/src/core/binder.js +10 -0
  28. package/src/core/blocks.js +3 -3
  29. package/src/core/editor-core.js +195 -6
  30. package/src/core/export.js +311 -22
  31. package/src/core/i18n/ar.js +2 -0
  32. package/src/core/i18n/bg.js +2 -0
  33. package/src/core/i18n/bn.js +2 -0
  34. package/src/core/i18n/ca.js +2 -0
  35. package/src/core/i18n/cs.js +2 -0
  36. package/src/core/i18n/da.js +2 -0
  37. package/src/core/i18n/de-CH.js +2 -0
  38. package/src/core/i18n/de.js +2 -0
  39. package/src/core/i18n/dz.js +2 -0
  40. package/src/core/i18n/el.js +2 -0
  41. package/src/core/i18n/en.js +2 -0
  42. package/src/core/i18n/es.js +2 -0
  43. package/src/core/i18n/et.js +2 -0
  44. package/src/core/i18n/fi.js +2 -0
  45. package/src/core/i18n/fr.js +2 -0
  46. package/src/core/i18n/hr.js +2 -0
  47. package/src/core/i18n/hu.js +2 -0
  48. package/src/core/i18n/index.js +83 -83
  49. package/src/core/i18n/it.js +2 -0
  50. package/src/core/i18n/lt.js +2 -0
  51. package/src/core/i18n/lv.js +2 -0
  52. package/src/core/i18n/nb.js +2 -0
  53. package/src/core/i18n/nl.js +2 -0
  54. package/src/core/i18n/pl.js +2 -0
  55. package/src/core/i18n/pt.js +2 -0
  56. package/src/core/i18n/ro.js +2 -0
  57. package/src/core/i18n/ru.js +2 -0
  58. package/src/core/i18n/sk.js +2 -0
  59. package/src/core/i18n/sl.js +2 -0
  60. package/src/core/i18n/sv.js +2 -0
  61. package/src/core/i18n/tr.js +2 -0
  62. package/src/core/i18n/uk.js +2 -0
  63. package/src/core/icons.js +17 -43
  64. package/src/core/ids.js +1 -1
  65. package/src/core/import-html.js +390 -20
  66. package/src/core/layout-style.js +25 -0
  67. package/src/core/parse.js +10 -10
  68. package/src/core/placeholder.js +15 -15
  69. package/src/core/sanitize.js +4 -1
  70. package/src/core/theme.js +22 -1
  71. package/src/core/variables.js +11 -11
  72. package/src/mailcraft-editor.js +11 -1
  73. package/src/render/block-body.js +129 -17
  74. package/src/render/canvas.js +35 -6
  75. package/src/render/fields.js +19 -0
  76. package/src/render/focus-preserve.js +158 -158
  77. package/src/render/screenshot.js +26 -3
  78. package/src/render/style.js +6 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,53 @@ All notable changes to `@seliseblocks/mailcraft` are documented here.
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.2.19] — 2026-09-13
8
+
9
+ A housekeeping release with one theme: the package stops shipping other people's marks, and stops reaching the network on its own.
10
+
11
+ ### Changed
12
+ - **Social icons are drawn here now, not borrowed.** The social block rendered eleven platforms as official filled brand glyphs lifted from Simple Icons. Registered logo artwork is not ours to redistribute inside a template builder, whatever the icon set's own licence says about the SVG — the mark underneath belongs to the platform, and every downstream host was inheriting that call from us. Every social key resolves into the same in-house thin-stroke set as the rest of the chrome: geometric marks that say where a link goes without reproducing anyone's logo. `brandIcon()` keeps its signature and its `currentColor` behaviour, so the palette and contrast logic and the export round-trip are untouched; a host that wants the real logos has always been able to supply them through `iconProvider`, and that is now the only way they get there.
13
+ - **The editor fetches no webfont.** `render/style.js` opened with an `@import` of Manrope from the Google Fonts CDN, which meant dropping the component into a page made a third-party request and disclosed the visitor's IP to a font host — and left the chrome unstyled on a `file://` page with the network off, which is a setting this component is explicitly meant to survive. The import is gone. `'Manrope'` stays at the head of `--ed-font` so a host that loads it still gets it, with the platform UI face behind it; `ui-font` overrides the stack as before.
14
+ - **The default footer and the shipped templates lost their borrowed details.** The footer group's placeholder carried a fictional company at a real-looking Portland street address; the order-shipped template named a specific courier and carried a tracking number in that courier's live format, which is the kind of thing that gets pasted into a real tracking form. Both are now plainly generic.
15
+
16
+ ### Notes for hosts
17
+ - **Upgrading changes how existing templates look.** Any document with a social block will render its icons in the stroke set after this release — same positions, same colours, same links, different glyphs. Nothing needs migrating and no export breaks; it is a visual change, and worth a look before you ship it to your own users. Pass `iconProvider` to keep the previous artwork.
18
+ - `window.claude.complete` no longer appears in the `aiProvider` documentation. Nothing changed in behaviour — the AI features have always stayed hidden until a host sets a provider, and the package ships no model client and talks to no model service of its own — but the old wording implied a specific host environment that was never a requirement.
19
+ - The published sourcemap now matches the bundle it describes. `dist/mailcraft-editor.bundle.js` is byte-identical to 0.2.18's; only the stale `sourcesContent` embedded in the `.map` is corrected.
20
+
21
+ ## [0.2.18] — 2026-09-10
22
+
23
+ ### Added
24
+ - **A background image behind one column.** The two-up "photo beside the copy" band that a row-level image cannot express: each column of a multi-column section now takes its own image with fit, position and a darken tint, the same vocabulary a section has. It paints on the wrapper `<div>` rather than the `<td>`, because the cell also carries the gutter and an image there runs under the gap between the columns — so, like a section using outside margins, it is CSS plus VML rather than CSS plus the `background=` attribute pair. Classic Outlook gets a `v:rect` wherever the section is not already painting one of its own: nesting VML shapes is where Word's layout stops being predictable, and a section image plus a column image is a design that has already chosen which it wants behind the text.
25
+ - **Image widths in pixels, not only percentages.** A percentage cannot say "88px". An 88px logo in a 600px column rounded to 15%, lost five pixels, and then crept wider on every save, because the next percentage was derived from the already-rounded pixels — no template with a fixed-size logo was a save fixed point. The inspector now offers Percent or Pixels, converts between them in one undo step so nothing jumps at the switch, and an import pins the exact number wherever the source declared one. `width:100%` beside the pixel `width` attribute this exporter writes for Word still reads as fluid, so no responsive hero freezes at the width it happened to be exported from.
26
+ - **Retina sources and tooltips survive an import.** `srcset`, `sizes` and `title` were dropped on the floor, so a template opened for a copy edit came back out having quietly lost its 2× artwork.
27
+ - **A blocked image holds its box.** Where the aspect ratio is known — the source carried both dimensions, or the artwork came from the library, which knows its own — the export writes a `height` beside the `width`. With images blocked (Outlook's default, and Gmail's for an unknown sender) the image reserves its real space instead of collapsing to a one-line strip and shoving the layout down when it finally loads. The CSS `height:auto` still wins wherever CSS is read.
28
+ - **A warning for sources that will not render when sent.** A placeholder, any other `data:` URI and a `cid:` attachment all draw perfectly on the canvas and arrive as an empty box in Gmail and Outlook, which is exactly why they ship unnoticed. The inspector now says so on the selected image, and opening the export dialog counts them for the whole document. Named, never blocked — `cid:` is legitimate for a host that attaches the file at send time. The built-in placeholder is told apart from real `data:` artwork by the hatched pattern its generator draws, so an intentional inline illustration is not mislabelled.
29
+ - **A background image for the full-width page**, alongside the content area's, each with its own fit, position and repeat. The page image rides `<body>` and the full-width wrapper `<table>` together (Gmail discards `<body>` outright) and ships a `v:background` for Word, which takes no dimensions and so needs no height estimate.
30
+ - **A content-area background image, preview text and a document reading direction** in Settings. The content column is the right home for a document background: it is already a `<table>`, the one element every client paints a background on.
31
+
32
+ ### Fixed
33
+ - **Flex and grid sections shipped their columns bare.** The canvas paints a column's background, image, border, radius and padding for every layout — the exporter only did it for table sections, so a two-card flex row was designed as cards and arrived as plain unstyled text. All three layouts now share one painter. It stays CSS-only for flex and grid: a grid column is an equal fraction and a flex column can wrap, so neither offers the fixed pixel width VML demands, and Word still receives the ghost-cell table those layouts already emit.
34
+ - **A flex or grid section that was the only section in the document lost its layout on reload**, along with its spans and any column paint, coming back as one plain row per column. A content table with a single `<tr>` is treated as scaffolding and its cell's children walked as content, which swallowed the layout marker; a cell whose only child carries that marker is now recognised as a row, exactly as a dynamic-content marker row already was. With two or more sections the content table was never scaffolding, which is why this needed a single-section document to show itself.
35
+ - **A screenshot dropped the darken tint.** Inlining the photo replaced the whole `background-image`, taking the gradient layer with it, so every tinted hero came out of the capture lighter than the canvas it was captured from. Only the `url()` layer is rewritten now.
36
+ - **A section background image is one band, not one per row.** A multi-row table stamped its photo onto every row as it built them, so a hero section that walked into six rows got six copies, each repainting from its own top edge and tiling the image down the email. The image is applied once, after the rows exist. A nested multi-row section — a hero or card stack inside the content column — is walked as rows too, instead of hitting the never-drop-content floor and importing as one opaque, uneditable block.
37
+ - **Images, spacers, dividers and video hardened for Outlook and Gmail.** `<img>` ships a pixel `width` attribute (Word cannot resolve a percentage and fell back to the file's intrinsic size, which is how a 1200px hero tore the layout open) and a real `font-size`, so alt text is visible with images blocked instead of inheriting the wrapper's `0px`. A spacer takes mj-spacer's shape, since an empty div has no height in Word. A divider ships both as the div every other client renders and as a `<td>` border Word paints. Video no longer positions its play badge — Gmail strips `position` and Word ignores it — so the thumbnail is the cell background and the badge is centred content.
38
+ - **Imported backgrounds that were previously invisible are read**: a VML-only image inside an MSO conditional, a declaration the CSSOM could not digest (falling back to the raw style text), and a foreign builder's dark rgba tint, which folds onto the Darken slider. A 1×1 tracking pixel passes through as raw HTML rather than becoming a visible block.
39
+
40
+ ### Notes for hosts
41
+ - New message keys `toast.exportImageOne` and `toast.exportImageMany` carry the export preflight. Every shipped locale has them; a host with its own message table falls back to English until it adds them.
42
+ - Four of the example templates render their product image 10–13% larger than before. That is the pixel-width fix landing: those images were authored at a fixed size and had been rounded down through a percentage. Nothing else in the shipped templates changes.
43
+
44
+ ## [0.2.17] — 2026-09-09
45
+
46
+ ### Fixed
47
+ - **A section background image reaches New Outlook.** A row's photo shipped as one layered declaration — `background-image: linear-gradient(rgba(20,22,24,α),…), url(…)`, the tint in front of the image — and outlook.com's sanitiser, which is what both New Outlook for Windows and Outlook on the web run, drops a declaration it cannot fully parse rather than salvaging the layers it understands. So the tint took the photo down with it and the row fell back to flat colour, while an ordinary `<img>` in the same email rendered fine. The hero section sets a 46% tint by default, so every hero was affected. The image now ships as unlayered longhands with the tint as its own `rgba()` box (which also carries the row padding, so it covers the whole band rather than just the content area), the url is unquoted, and `background=`/`bgcolor=` repeat both values as HTML attributes for anything that drops CSS wholesale.
48
+ - **Classic Outlook gets the photo too.** The same rows now emit a VML `v:rect`/`v:fill` behind the content for the Word engine, which has never read a CSS background. `v:rect` needs pixel dimensions and a row's height is content-driven, so the height is an estimate (padding plus a nominal allowance per block) that `mso-fit-shape-to-text` treats as a floor — Word grows the shape to whatever the content actually needs. The VML namespace is declared on `<html>` only when a row actually emitted VML. The tint is the one thing Classic still does not get: it understands neither the CSS background nor `rgba()`.
49
+ - **A Section box keeps its background colour when it also has an image.** The box wrote both through the `background` shorthand, so setting an image made the colour unreachable — there was nothing left underneath when a client refused the image — and the shorthand opened with a no-op `linear-gradient()`, the same construct the sanitiser rejects. Colour and image are separate longhands now, and the box runs its image url through `cssUrl` like everywhere else.
50
+
51
+ ### Notes for hosts
52
+ - Templates exported by earlier versions still load unchanged: the importer folds the current tint box back into the old layered shape before anything walks it, so both eras read identically and the overlay percentage survives a save/reload either way.
53
+
7
54
  ## [0.2.16] — 2026-09-05
8
55
 
9
56
  ### Fixed
package/DOCS.md CHANGED
@@ -1,4 +1,4 @@
1
- # Blocks MailCraft product & integration guide
1
+ # Blocks MailCraft: product & integration guide
2
2
 
3
3
  [README.md](README.md) is the five-minute version. This is the rest: what it is, why it works the way it does, and how to wire it into a real application.
4
4
 
@@ -41,13 +41,13 @@ A user drags rows and blocks onto a canvas, edits text in place, styles it in an
41
41
 
42
42
  | | |
43
43
  |---|---|
44
- | Distribution | npm `@seliseblocks/mailcraft` or one `<script>` tag |
44
+ | Distribution | npm (`@seliseblocks/mailcraft`) or one `<script>` tag |
45
45
  | Runtime dependencies | none |
46
- | Framework | none a custom element works in React, Angular, Vue, Svelte and plain HTML |
46
+ | Framework | none; a custom element works in React, Angular, Vue, Svelte and plain HTML |
47
47
  | Isolation | Shadow DOM; the editor's CSS and the host's cannot reach each other |
48
48
  | Size | ~683 KB minified, ~189 KB gzipped, one file |
49
49
  | Languages | 31, RTL automatic |
50
- | Content blocks | 19 text, heading, image, button, divider, spacer, social, video, countdown, menu, list, table, box, html, css, code, svg, condition, loop |
50
+ | Content blocks | 19: text, heading, image, button, divider, spacer, social, video, countdown, menu, list, table, box, html, css, code, svg, condition, loop |
51
51
 
52
52
  ---
53
53
 
@@ -55,7 +55,7 @@ A user drags rows and blocks onto a canvas, edits text in place, styles it in an
55
55
 
56
56
  Four decisions shape the whole API. Each is a constraint the host would otherwise inherit.
57
57
 
58
- ### 1. HTML in, HTML out there is no document format
58
+ ### 1. HTML in, HTML out: there is no document format
59
59
 
60
60
  The obvious design is to expose the editor's internal document as JSON and let hosts store that. It is also the one that ages worst: the moment a host persists that JSON, its shape becomes a public contract. Every new block type, every renamed prop, every changed default becomes a migration the host has to run against rows in its own database.
61
61
 
@@ -66,13 +66,13 @@ editor.loadTemplate({ name: 'Welcome', html });
66
66
  const html = editor.exportHtml();
67
67
  ```
68
68
 
69
- Exported HTML is valid input to the importer, so **saving the export is saving the work**. The host stores a string in a column it already understands, and can send it, diff it, preview it, or open it in any other tool none of which is true of a private JSON shape.
69
+ Exported HTML is valid input to the importer, so **saving the export is saving the work**. The host stores a string in a column it already understands, and can send it, diff it, preview it, or open it in any other tool. None of that is true of a private JSON shape.
70
70
 
71
71
  The internal document still exists. It is simply not yours to hold.
72
72
 
73
73
  ### 2. The editor never talks to a backend of its own
74
74
 
75
- No base URL, no auth, no upload endpoint, no telemetry. The one `fetch` in the package is the screenshot capture inlining the images *your template already shows* so they can be drawn into the PNG best-effort and CORS-bound, and an image the remote server refuses degrades to a blank pixel rather than a failed capture.
75
+ No base URL, no auth, no upload endpoint, no telemetry. The one `fetch` in the package is the screenshot capture inlining the images *your template already shows* so they can be drawn into the PNG. That fetch is best-effort and CORS-bound, and an image the remote server refuses degrades to a blank pixel rather than a failed capture.
76
76
 
77
77
  Image storage is a plain object the host assigns:
78
78
 
@@ -84,24 +84,24 @@ You write those functions next to the auth and base URL you already own, and poi
84
84
 
85
85
  The same applies to AI (`.aiProvider` is one `async (prompt) => text` function) and to social icons (`.iconProvider`).
86
86
 
87
- ### 3. Upload policy belongs to the host formats excepted
87
+ ### 3. Upload policy belongs to the host: formats excepted
88
88
 
89
- Sizes and counts ship undefaulted: what an email may carry depends on the sending platform's caps and the host's own product rules none of which this package can know. With a provider wired and no `maxBytes` declared, uploads are refused rather than waved through.
89
+ Sizes and counts ship undefaulted: what an email may carry depends on the sending platform's caps and the host's own product rules, none of which this package can know. With a provider wired and no `maxBytes` declared, uploads are refused rather than waved through.
90
90
 
91
- Formats are the one default: **every image type is allowed** JPEG, PNG, GIF, WebP, BMP, TIFF, ICO, AVIF, HEIC unless `accept` lists a narrower set. SVG keeps its own gate below.
91
+ Formats are the one default: **every image type is allowed** (JPEG, PNG, GIF, WebP, BMP, TIFF, ICO, AVIF, HEIC) unless `accept` lists a narrower set. SVG keeps its own gate below.
92
92
 
93
93
  Two supporting details:
94
94
 
95
95
  - **Formats are decided by reading the file's leading bytes**, not `file.type`. The browser fills `file.type` in from the extension, so renaming `payload.svg` to `photo.png` would otherwise walk a script-bearing document straight into the editor's own DOM, where the library tile renders it.
96
96
  - **SVG needs a second, explicit opt-in** (`allowSvg: true`) even when listed in `accept`. It is the one image type that is also a script host, and no one should enable it by pasting a permissive MIME list.
97
97
 
98
- Validation runs *before* the provider is called, so a rejected file never reaches your backend which matters for any store where minting an upload URL also creates the file record.
98
+ Validation runs *before* the provider is called, so a rejected file never reaches your backend, which matters for any store where minting an upload URL also creates the file record.
99
99
 
100
100
  ### 4. The editor's chrome is the host's decision
101
101
 
102
102
  An editor embedded in a product that already has a header, a breadcrumb and a Save button ends up with two bars and two logos stacked on each other. So the top bar is switchable down to the individual control, and can be removed entirely.
103
103
 
104
- Most of what the bar does survives without it. Undo, redo and export are element methods; the screenshot methods never needed the bar at all; and the keyboard shortcuts are bound at the window level, not on the bar, so `Ctrl/Cmd+Z`, `Shift+Ctrl/Cmd+Z` and `Ctrl/Cmd+E` keep working either way. Three panels are opened only from the bar, though the live **preview** overlay, the **Code** modal and the **AI draft** panel so dropping those parts does take them out of reach. The table under [Choose what the top bar shows](#choose-what-the-top-bar-shows) says which is which.
104
+ Most of what the bar does survives without it. Undo, redo and export are element methods; the screenshot methods never needed the bar at all; and the keyboard shortcuts are bound at the window level, not on the bar, so `Ctrl/Cmd+Z`, `Shift+Ctrl/Cmd+Z` and `Ctrl/Cmd+E` keep working either way. Three panels are opened only from the bar, though (the live **preview** overlay, the **Code** modal and the **AI draft** panel), so dropping those parts does take them out of reach. The table under [Choose what the top bar shows](#choose-what-the-top-bar-shows) says which is which.
105
105
 
106
106
  ---
107
107
 
@@ -119,13 +119,13 @@ import '@seliseblocks/mailcraft'; // side effect: registers <mailcraft-editor>
119
119
 
120
120
  Named exports (`MailCraftEditor`, `EditorCore`, `LOCALES`, `validateFiles`, …) come from the same entry.
121
121
 
122
- **TypeScript** works out of the box the package ships declarations (`types/index.d.ts`) for every named export, and registers `mailcraft-editor` in `HTMLElementTagNameMap`, so `document.createElement('mailcraft-editor')` and `querySelector('mailcraft-editor')` come back typed. The contract types (`StorageProvider`, `StorageLimits`, `Asset`, `ToolbarOption`, `FooterOption`, `CreateEditorOptions`, …) are exported for your own signatures:
122
+ **TypeScript** works out of the box: the package ships declarations (`types/index.d.ts`) for every named export, and registers `mailcraft-editor` in `HTMLElementTagNameMap`, so `document.createElement('mailcraft-editor')` and `querySelector('mailcraft-editor')` come back typed. The contract types (`StorageProvider`, `StorageLimits`, `Asset`, `ToolbarOption`, `FooterOption`, `CreateEditorOptions`, …) are exported for your own signatures:
123
123
 
124
124
  ```ts
125
125
  import type { StorageProvider, StorageLimits } from '@seliseblocks/mailcraft';
126
126
  ```
127
127
 
128
- **Server-side rendering:** the module defines a custom element, so import it in the browser only inside a `useEffect`, an `onMounted`, or a `dynamic(..., { ssr: false })` component.
128
+ **Server-side rendering:** the module defines a custom element, so import it in the browser only: inside a `useEffect`, an `onMounted`, or a `dynamic(..., { ssr: false })` component.
129
129
 
130
130
  Without a bundler:
131
131
 
@@ -141,7 +141,7 @@ Without a bundler:
141
141
  <mailcraft-editor id="editor" toolbar="none"></mailcraft-editor>
142
142
  ```
143
143
 
144
- **Into a container from code**, the shape most JS widgets ship with you hand it a target and options, and get a handle back:
144
+ **Into a container from code**, the shape most JS widgets ship with: you hand it a target and options, and get a handle back:
145
145
 
146
146
  ```js
147
147
  import { createEditor } from '@seliseblocks/mailcraft';
@@ -169,17 +169,17 @@ Every option, in full:
169
169
  | option | what it does |
170
170
  |---|---|
171
171
  | `html` | initial content, applied through the importer as an undoable edit |
172
- | `name` | the template name that content is loaded under only read alongside `html`, and defaults to `''` |
172
+ | `name` | the template name that content is loaded under, only read alongside `html`, and defaults to `''` |
173
173
  | `variables`, `locale`, `dir`, `theme`, `uiFont`, `accent` | the attributes, under their property spellings (`uiFont` is `ui-font` in markup) |
174
- | `toolbar`, `footer` | as documented below objects and `false` included, since these are set as properties |
174
+ | `toolbar`, `footer` | as documented below: objects and `false` included, since these are set as properties |
175
175
  | `storageProvider`, `storageLimits`, `aiProvider`, `iconProvider`, `messages` | the property-only options |
176
176
  | `height` | sets the *container's* height; a number is treated as `px`. Omit it and your CSS decides |
177
177
  | `replace` | empty the container first (default: append) |
178
178
  | `onChange(doc)`, `onExport(html)` | the two events, as callbacks. `destroy()` detaches them |
179
179
 
180
- It is a wrapper, not a second implementation it creates the same element and sets the same attributes and properties. The only thing it adds is not having to know which options are attributes (strings) and which must be properties (objects and functions).
180
+ It is a wrapper, not a second implementation: it creates the same element and sets the same attributes and properties. The only thing it adds is not having to know which options are attributes (strings) and which must be properties (objects and functions).
181
181
 
182
- **The container supplies the height.** The editor is `display: block; height: 100%`, so `#mail { height: 560px }` in your CSS, a flex/grid cell, or `{ height: 560px }` in the options all work. Nothing is guessed for you.
182
+ **The container supplies the height.** The editor is `display: block; height: 100%`, so `#mail { height: 560px }` in your CSS, a flex/grid cell, or `{ height: 560px }` in the options: all work. Nothing is guessed for you.
183
183
 
184
184
  By default the editor is appended, so existing content in the container survives; pass `{ replace: true }` to empty it first.
185
185
 
@@ -187,7 +187,7 @@ By default the editor is appended, so existing content in the container survives
187
187
 
188
188
  The element is framework-agnostic; only the plumbing differs.
189
189
 
190
- **React** attributes carry strings, so set objects and functions as properties on a ref:
190
+ **React**: attributes carry strings, so set objects and functions as properties on a ref:
191
191
 
192
192
  ```jsx
193
193
  import { useEffect, useRef } from 'react';
@@ -213,9 +213,9 @@ export function EmailEditor({ html, onSave }) {
213
213
  }
214
214
  ```
215
215
 
216
- **Angular** add `CUSTOM_ELEMENTS_SCHEMA` to the module, then drive it from a `ViewChild` the same way.
216
+ **Angular**: add `CUSTOM_ELEMENTS_SCHEMA` to the module, then drive it from a `ViewChild` the same way.
217
217
 
218
- **Vue / Svelte** both set DOM properties for non-string bindings automatically, so `<mailcraft-editor :toolbar="cfg">` works as written.
218
+ **Vue / Svelte**: both set DOM properties for non-string bindings automatically, so `<mailcraft-editor :toolbar="cfg">` works as written.
219
219
 
220
220
  ## Save and restore
221
221
 
@@ -227,19 +227,19 @@ await api.saveDraft(id, editor.exportHtml());
227
227
  editor.loadTemplate({ name: 'Draft', html: await api.loadDraft(id) });
228
228
  ```
229
229
 
230
- Editing continues where it left off. Anything the importer cannot classify into an editable block survives as a raw-HTML block rendered and exported, never dropped.
230
+ Editing continues where it left off. Anything the importer cannot classify into an editable block survives as a raw-HTML block: rendered and exported, never dropped.
231
231
 
232
232
  The editor also autosaves to `localStorage`, scoped **per browser tab**, so two tabs are two independent documents. That is a convenience for reload, not your persistence layer.
233
233
 
234
234
  ## Load templates
235
235
 
236
- Templates are host content **and** host UI. The editor ships no catalogue and has no Templates tab you render your own picker and push the choice in:
236
+ Templates are host content **and** host UI. The editor ships no catalogue and has no Templates tab; you render your own picker and push the choice in:
237
237
 
238
238
  ```js
239
239
  editor.loadTemplate({ name: 'Welcome', html });
240
240
  ```
241
241
 
242
- Applying one is a normal undoable edit with a toast, and your string is never mutated. Ready-made examples ship in the package under `examples/templates/`, with a working picker in [`examples/vanilla.html`](examples/vanilla.html) both host-app content, shipped in the package for you to lift but not part of its API.
242
+ Applying one is a normal undoable edit with a toast, and your string is never mutated. Ready-made examples ship in the package under `examples/templates/`, with a working picker in [`examples/vanilla.html`](examples/vanilla.html): both host-app content, shipped in the package for you to lift but not part of its API.
243
243
 
244
244
  ## Add merge variables
245
245
 
@@ -251,9 +251,9 @@ Users insert them from the toolbar; they export as `{{first_name}}`.
251
251
 
252
252
  ### Conditions and loops
253
253
 
254
- **Condition** and **Loop** are blocks in the palette. Dropping one inserts a *pair* of markers a start band and an end band — and everything the user drags between them is what the logic applies to. Dropped inside a column, the pair wraps blocks; dropped onto the canvas between sections, each marker gets a slim row of its own, so whole sections can be moved between them. From then on each half is an ordinary block: moved, duplicated or deleted independently.
254
+ **Condition** and **Loop** are blocks in the palette. Dropping one inserts a *pair* of markers, a start band and an end band; everything the user drags between them is what the logic applies to. Dropped inside a column, the pair wraps blocks; dropped onto the canvas between sections, each marker gets a slim row of its own, so whole sections can be moved between them. From then on each half is an ordinary block: moved, duplicated or deleted independently.
255
255
 
256
- Like merge tags, the editor never evaluates the expressions the export emits literal Handlebars-style tags at the markers' positions for your templating engine to run at send time:
256
+ Like merge tags, the editor never evaluates the expressions: the export emits literal Handlebars-style tags at the markers' positions for your templating engine to run at send time:
257
257
 
258
258
  ```html
259
259
  {{#if has_order}}
@@ -263,13 +263,13 @@ Like merge tags, the editor never evaluates the expressions — the export emits
263
263
  {{/if}}
264
264
  ```
265
265
 
266
- The exporter balances the document as a whole: a stray end marker emits nothing, and a start whose end was deleted is auto-closed after the last row, so the output template is always well-formed. Marker-only rows emit just the tag no empty `<tr>` band reaches a recipient and inside a loop, item-scoped merge tags (`{{ this.name }}`, `{{ price }}`) pass through like any other token. The markers render as dashed teal `SHOW IF` / violet `REPEAT EACH` bands on the canvas, in the preview, and in the code view's live pane every editor surface shows the template the way it shows merge tokens while a recipient sees nothing but the engine's output. Exported tags round-trip: re-importing the HTML restores them as marker blocks in the same positions.
266
+ The exporter balances the document as a whole: a stray end marker emits nothing, and a start whose end was deleted is auto-closed after the last row, so the output template is always well-formed. Marker-only rows emit just the tag (no empty `<tr>` band reaches a recipient), and inside a loop, item-scoped merge tags (`{{ this.name }}`, `{{ price }}`) pass through like any other token. The markers render as dashed teal `SHOW IF` / violet `REPEAT EACH` bands on the canvas, in the preview, and in the code view's live pane; every editor surface shows the template the way it shows merge tokens, while a recipient sees nothing but the engine's output. Exported tags round-trip: re-importing the HTML restores them as marker blocks in the same positions.
267
267
 
268
268
  A start marker's expression field suggests the host's `variables` in a dropdown but stays free text, since conditions and loops routinely reference names that aren't inline tokens (`order.items`, a boolean flag, whatever your engine understands).
269
269
 
270
270
  ## Accept image uploads
271
271
 
272
- A provider and a `maxBytes` ceiling are both required. The package ships no files of its own: with no provider the library opens empty and holds only what is dropped into it data URIs, kept in the local draft, which no email client renders. With a provider but no `maxBytes` every upload is refused.
272
+ A provider and a `maxBytes` ceiling are both required. The package ships no files of its own: with no provider the library opens empty and holds only what is dropped into it: data URIs, kept in the local draft, which no email client renders. With a provider but no `maxBytes` every upload is refused.
273
273
 
274
274
  ```js
275
275
  editor.storageLimits = {
@@ -305,7 +305,7 @@ editor.storageProvider = {
305
305
 
306
306
  Why the shape is what it is:
307
307
 
308
- - `cursor` is opaque whatever you returned last, handed back for the next page. Folder and search are the backend's job; re-filtering the current page client-side would hide matches on the next one.
308
+ - `cursor` is opaque: whatever you returned last, handed back for the next page. Folder and search are the backend's job; re-filtering the current page client-side would hide matches on the next one.
309
309
  - `signal` is an `AbortSignal`. Listings and uploads get **separate** abort scopes: changing folder cancels the listing it supersedes, but must never kill uploads already in flight.
310
310
  - Without `remove`, the library's delete only drops the tile from view.
311
311
  - **`url` must still resolve after the email is sent.** A URL that expires in an hour produces mail whose images are already broken when it lands.
@@ -316,11 +316,11 @@ A provider may also carry its own `limits` object, for a backend that knows its
316
316
  editor.storageProvider = { list, upload, limits: { accept: [...], maxBytes: 5e6 } };
317
317
  ```
318
318
 
319
- The two are merged **per key**, with `editor.storageLimits` winning so a provider can ship sane defaults and the host can still tighten one number without restating the rest. Either source satisfies the "`maxBytes` is required" rule; only a file that passes the merged result reaches `upload`.
319
+ The two are merged **per key**, with `editor.storageLimits` winning, so a provider can ship sane defaults and the host can still tighten one number without restating the rest. Either source satisfies the "`maxBytes` is required" rule; only a file that passes the merged result reaches `upload`.
320
320
 
321
321
  Setting `editor.storageProvider = null` drops back to that empty local library.
322
322
 
323
- `examples/vanilla.html` wires a working provider over IndexedDB latency, cursor paging, server-side folders and search, uploads that survive a reload, deletes that stay deleted. It is the shape of a real integration with `fetch` swapped out, and it is where every file in the live demo's Assets modal comes from.
323
+ `examples/vanilla.html` wires a working provider over IndexedDB: latency, cursor paging, server-side folders and search, uploads that survive a reload, deletes that stay deleted. It is the shape of a real integration with `fetch` swapped out, and it is where every file in the live demo's Assets modal comes from.
324
324
 
325
325
  ## Choose what the top bar shows
326
326
 
@@ -338,7 +338,7 @@ Parts: `logo`, `status`, `device`, `undo`, `redo`, `theme`, `ai`, `code`, `previ
338
338
 
339
339
  The **attribute names what to keep**; the **property names what to drop**. Markup has only strings to work with, and an allow-list reads better there than spelling out the seven things you did not want. Switching every part off collapses to no bar rather than an empty strip.
340
340
 
341
- `none` is the documented spelling for "no bar"; `hidden`, `off` and `false` are accepted as the same thing, and `toolbar="all"` is the explicit form of the default. The property takes `false` for no bar, and an object where only the keys set to `false` do anything unlisted parts stay on.
341
+ `none` is the documented spelling for "no bar"; `hidden`, `off` and `false` are accepted as the same thing, and `toolbar="all"` is the explicit form of the default. The property takes `false` for no bar, and an object where only the keys set to `false` do anything; unlisted parts stay on.
342
342
 
343
343
  ### The cost of a hidden part
344
344
 
@@ -346,15 +346,15 @@ The **attribute names what to keep**; the **property names what to drop**. Marku
346
346
  |---|---|
347
347
  | `undo` / `redo` | `editor.undo()`, `editor.redo()`, plus `Ctrl/Cmd+Z` and `Shift+Ctrl/Cmd+Z` |
348
348
  | `export` | `editor.exportHtml()` returns the HTML; `Ctrl/Cmd+E` still opens the export dialog, and the Screenshot button lives inside it |
349
- | `preview` | **bar-only** the live desktop/mobile overlay has no method and no shortcut |
350
- | `code` | **bar-only** the import/export-HTML modal. `editor.importHtml(html)` covers the import half |
351
- | `ai` | **bar-only** the draft panel. `.aiProvider` is your function, so a host can call it directly |
349
+ | `preview` | **bar-only**: the live desktop/mobile overlay has no method and no shortcut |
350
+ | `code` | **bar-only**: the import/export-HTML modal. `editor.importHtml(html)` covers the import half |
351
+ | `ai` | **bar-only**: the draft panel. `.aiProvider` is your function, so a host can call it directly |
352
352
  | `theme` | set the `theme` attribute yourself; while it is set the toggle is hidden anyway |
353
353
  | `logo`, `status`, `device` | display and view state only, nothing to lose |
354
354
 
355
355
  Screenshots are unaffected by any of this: `screenshotPng()`, `previewScreenshot()`, `downloadScreenshot()` and `copyScreenshot()` are element methods.
356
356
 
357
- `editor.core` reaches the rest (`core.openCode()`, `core.setState({ aiOpen: true })`, `core.setState({ previewOpen: true })`), but `EditorCore` is internal its shape is free to change between versions. Keep `code`, `ai` or `preview` in the bar if you need them.
357
+ `editor.core` reaches the rest (`core.openCode()`, `core.setState({ aiOpen: true })`, `core.setState({ previewOpen: true })`), but `EditorCore` is internal: its shape is free to change between versions. Keep `code`, `ai` or `preview` in the bar if you need them.
358
358
 
359
359
  ## Use the keyboard shortcuts
360
360
 
@@ -366,11 +366,11 @@ Bound at the window level while the editor is connected, so they work with any b
366
366
  | `Ctrl/Cmd` + `Z` | undo |
367
367
  | `Shift` + `Ctrl/Cmd` + `Z` | redo |
368
368
  | `Ctrl/Cmd` + `E` | open the export dialog |
369
- | `Ctrl/Cmd` + `K` | link the selected text only while editing text |
369
+ | `Ctrl/Cmd` + `K` | link the selected text, only while editing text |
370
370
  | `Ctrl/Cmd` + `D` | duplicate the selected row or block |
371
371
  | `Backspace` / `Delete` | delete the selected row or block |
372
372
 
373
- Typing is never hijacked: inside a form field, `Ctrl/Cmd+Z` is the browser's own field undo and `Delete` deletes a character, not the block. Rich-text blocks are the exception for undo their edits commit on blur, so document undo is what you want there. The screenshot viewer claims `Esc`, the arrow keys and `Space` while it is open.
373
+ Typing is never hijacked: inside a form field, `Ctrl/Cmd+Z` is the browser's own field undo and `Delete` deletes a character, not the block. Rich-text blocks are the exception for undo: their edits commit on blur, so document undo is what you want there. The screenshot viewer claims `Esc`, the arrow keys and `Space` while it is open.
374
374
 
375
375
  One caveat if your app binds the same keys: the handler asks whether the event came from a text field, not whether it came from inside the editor. `Ctrl/Cmd+E` anywhere on the page opens the export dialog, and `Backspace` on a non-field element elsewhere in your UI deletes the editor's selected block. An editor kept on a route of its own never notices; one sitting beside your own keyboard-driven UI might, and the fix is to `stopPropagation()` on the keydowns you own before they reach `window`.
376
376
 
@@ -380,7 +380,7 @@ The editor carries a one-line attribution along the bottom of the shell:
380
380
 
381
381
  > Powered by SELISE Blocks © 2026
382
382
 
383
- It is configurable the same way the top bar is replace the line, point it
383
+ It is configurable the same way the top bar is: replace the line, point it
384
384
  somewhere, or remove it:
385
385
 
386
386
  ```html
@@ -399,7 +399,7 @@ editor.footer = { show: false }; // no strip, f
399
399
 
400
400
  A link opens in a new tab (`rel="noopener noreferrer"`) so a click never carries
401
401
  unsaved work out of the editor; pass `target` to override. Schemes are
402
- allowlisted to `http(s)`, `mailto` and relative paths the strip renders inside
402
+ allowlisted to `http(s)`, `mailto` and relative paths; the strip renders inside
403
403
  the editor's own DOM.
404
404
 
405
405
  The default line is a translated string, not baked-in text, so it follows
@@ -409,7 +409,7 @@ The default line is a translated string, not baked-in text, so it follows
409
409
  editor.messages = { 'footer.poweredBy': 'Powered by Acme' };
410
410
  ```
411
411
 
412
- Hiding the strip collapses its row the canvas keeps every pixel it had.
412
+ Hiding the strip collapses its row; the canvas keeps every pixel it had.
413
413
 
414
414
  ## Set language, direction and theme
415
415
 
@@ -436,7 +436,7 @@ Every key, with its English default, is listed in [Every message key](#every-mes
436
436
  <mailcraft-editor ui-font="'IBM Plex Sans', Arial, sans-serif"></mailcraft-editor>
437
437
  ```
438
438
 
439
- Editor chrome only never the fonts inside the email being edited.
439
+ Editor chrome only; never the fonts inside the email being edited.
440
440
 
441
441
  ## Match your brand color
442
442
 
@@ -446,7 +446,7 @@ Editor chrome only — never the fonts inside the email being edited.
446
446
  <mailcraft-editor accent="inherit"></mailcraft-editor>
447
447
  ```
448
448
 
449
- One color repaints every accented pixel in the editor there is no second
449
+ One color repaints every accented pixel in the editor; there is no second
450
450
  place to set. It reaches:
451
451
 
452
452
  | | |
@@ -464,8 +464,8 @@ place to set. It reaches:
464
464
  Contrast is corrected per surface, not just picked. The editor paints on two:
465
465
  the **panels**, which follow the light/dark theme, and the **email sheet**,
466
466
  which is a white page in both. A brand color is fitted separately against each
467
- darkened where it would wash out on white, lightened where it would vanish on
468
- the dark panels, each only as far as WCAG AA needs so the grip badge on the
467
+ (darkened where it would wash out on white, lightened where it would vanish on
468
+ the dark panels, each only as far as WCAG AA needs), so the grip badge on the
469
469
  page never inherits the pale accent the dark chrome needs. Text drawn *on* the
470
470
  accent flips between white and near-black to stay legible. A brand color that
471
471
  already passes is used exactly as given. An unusable value is ignored, with a
@@ -480,7 +480,7 @@ editor.accent = 'var(--brand)'; // same string: re-reads the token
480
480
  editor.accent = next; // ...or just hand over the literal
481
481
  ```
482
482
 
483
- Editor chrome only colors inside the email being edited belong to the
483
+ Editor chrome only; colors inside the email being edited belong to the
484
484
  template, not to your app.
485
485
 
486
486
  ---
@@ -493,7 +493,7 @@ template, not to your app.
493
493
  |---|---|
494
494
  | `variables` | comma-separated merge tags |
495
495
  | `locale` | any of the 31 shipped tags |
496
- | `dir` | `ltr` / `rtl` defaults from `locale` |
496
+ | `dir` | `ltr` / `rtl`: defaults from `locale` |
497
497
  | `theme` | `light` / `dark` |
498
498
  | `ui-font` | `inherit` or a CSS font-family stack |
499
499
  | `accent` | a CSS color, `var(--your-token)`, or `inherit` (the host's `accent-color`) |
@@ -510,10 +510,11 @@ template, not to your app.
510
510
  | `.uiFont` | string |
511
511
  | `.accent` | string |
512
512
  | `.messages` | `{ key: string }` |
513
- | `.storageProvider` | `{ list, upload, folders?, remove?, limits? }` `null` drops back to the empty local library |
513
+ | `.storageProvider` | `{ list, upload, folders?, remove?, limits? }` (or `null`, which drops back to the empty local library) |
514
+
514
515
  | `.storageLimits` | `{ accept?, maxBytes, maxWidth?, maxHeight?, maxFilesPerDrop?, allowSvg? }`, merged over `provider.limits` per key |
515
516
  | `.aiProvider` | `async (prompt) => text` |
516
- | `.iconProvider` | `(platformKey, { label, size, color }) => Node` social-icon override; falls back to the built-in icon when it is unset, throws, or returns a non-node |
517
+ | `.iconProvider` | `(platformKey, { label, size, color }) => Node`: social-icon override; falls back to the built-in icon when it is unset, throws, or returns a non-node |
517
518
 
518
519
  ### Package exports
519
520
 
@@ -523,13 +524,13 @@ template, not to your app.
523
524
  | `isReady()` | whether the custom element is registered |
524
525
  | `MailCraftEditor` | the element class |
525
526
  | `LOCALES`, `LOCALE_TABLES`, `createTranslator` | i18n: the shipped tags, their tables, and the translator the editor uses |
526
- | `LOCALE_LOADERS`, `loadLocale(tag)` | lazy per-locale table loading what the `locale` attribute resolves through, so a bundled app ships only the locales it uses. `loadLocale` prefetches one (e.g. before flipping `locale` at runtime) |
527
- | `defineMessages(base, overrides)` | merge a shipped locale with your own overrides the supported way to build a `.messages` value |
527
+ | `LOCALE_LOADERS`, `loadLocale(tag)` | lazy per-locale table loading: what the `locale` attribute resolves through, so a bundled app ships only the locales it uses. `loadLocale` prefetches one (e.g. before flipping `locale` at runtime) |
528
+ | `defineMessages(base, overrides)` | merge a shipped locale with your own overrides, the supported way to build a `.messages` value |
528
529
  | `missingKeys(locale, base)` | keys `base` has that `locale` does not translate. What a translator has left to do |
529
- | `EN`, `MESSAGE_KEYS` | the English table and every key in it listed in [Every message key](#every-message-key) |
530
- | `isRtl(tag)` | whether a locale tag is right-to-left. Metadata `dir` is what actually flips the layout |
530
+ | `EN`, `MESSAGE_KEYS` | the English table and every key in it, listed in [Every message key](#every-message-key) |
531
+ | `isRtl(tag)` | whether a locale tag is right-to-left. Metadata: `dir` is what actually flips the layout |
531
532
  | `validateFiles`, `acceptAttribute`, `sanitizeName` | upload validation, reusable outside the editor |
532
- | `limitsProblem(limits)` | what a limits object is missing, if anything the check that refuses uploads |
533
+ | `limitsProblem(limits)` | what a limits object is missing, if anything: the check that refuses uploads |
533
534
  | `resolveLimits(hostLimits, providerLimits)` | the per-key merge the editor applies to the two limit sources |
534
535
  | `normalizeAsset(raw, probe)`, `ALL_FOLDER_ID` | coerce a provider's item into the library's asset shape; the id of the synthetic "all files" folder (`''`) |
535
536
  | `EditorCore`, `renderDoc`, `BLOCKS`, `GROUPS`, `LAYOUTS`, `PALETTE` | internals, for building your own UI on top |
@@ -538,28 +539,28 @@ template, not to your app.
538
539
 
539
540
  | method | returns |
540
541
  |---|---|
541
- | `exportHtml(options?)` | send-ready email HTML. `{ markers: false }` omits the `data-mc*` fidelity markers for pristine output countdown, video, section box, code and raw-CSS blocks, flex/grid rows and “Keep columns” then reload lossily (content always survives) |
542
+ | `exportHtml(options?)` | send-ready email HTML. `{ markers: false }` omits the `data-mc*` fidelity markers for pristine output: countdown, video, section box, code and raw-CSS blocks, flex/grid rows and “Keep columns” then reload lossily (content always survives) |
542
543
  | `importHtml(html)` | number of rows produced |
543
544
  | `loadTemplate({ name, html })` | — |
544
545
  | `undo()` / `redo()` | — |
545
- | `screenshotPng(options?)` | full template as an image `Blob`. PNG by default (lossless); `{ format: 'jpeg' \| 'webp', quality: 0–1 }` compresses it to a fraction of the size, and `scale` (default 2) trades resolution for bytes. Read the returned `blob.type` a browser without a WebP encoder hands back PNG |
546
+ | `screenshotPng(options?)` | full template as an image `Blob`. PNG by default (lossless); `{ format: 'jpeg' \| 'webp', quality: 0–1 }` compresses it to a fraction of the size, and `scale` (default 2) trades resolution for bytes. Read the returned `blob.type`: a browser without a WebP encoder hands back PNG |
546
547
  | `previewScreenshot()` | opens the story-style viewer, with its own PNG / JPG / WebP download toggle |
547
- | `downloadScreenshot(blob?, options?)` / `copyScreenshot(blob?)` | save / clipboard captures first (with `options`) if no blob is passed; the filename extension follows the blob's actual type |
548
+ | `downloadScreenshot(blob?, options?)` / `copyScreenshot(blob?)` | save / clipboard: captures first (with `options`) if no blob is passed; the filename extension follows the blob's actual type |
548
549
 
549
550
  ### Events
550
551
 
551
552
  | event | `detail` |
552
553
  |---|---|
553
- | `change` | the internal document for dirty-tracking, not persistence |
554
+ | `change` | the internal document, for dirty-tracking, not persistence |
554
555
  | `export` | the exported HTML string |
555
556
 
556
557
  ### Not public API
557
558
 
558
- `getContent()` / `setContent(doc)` expose the internal document. They exist because undo, autosave and the test suite need them. The shape is free to change between versions store `exportHtml()` instead.
559
+ `getContent()` / `setContent(doc)` expose the internal document. They exist because undo, autosave and the test suite need them. The shape is free to change between versions; store `exportHtml()` instead.
559
560
 
560
561
  ### Every message key
561
562
 
562
- The complete catalog of UI strings that `.messages` accepts every key the editor ever asks for, with its built-in English text. In code the same catalog is `EN` (keys with values) and `MESSAGE_KEYS` (just the keys), and `missingKeys(yourTable)` diffs a custom table against it.
563
+ The complete catalog of UI strings that `.messages` accepts: every key the editor ever asks for, with its built-in English text. In code the same catalog is `EN` (keys with values) and `MESSAGE_KEYS` (just the keys), and `missingKeys(yourTable)` diffs a custom table against it.
563
564
 
564
565
  <!-- message-keys:begin — generated by build.js from src/core/i18n/en.js; edit en.js and run `node build.js`, never this table. -->
565
566
 
@@ -735,6 +736,8 @@ The complete catalog of UI strings that `.messages` accepts — every key the ed
735
736
  | `toast.assetDeleted` | {name} deleted |
736
737
  | `toast.buttonAdded` | Button added |
737
738
  | `toast.duplicated` | Duplicated |
739
+ | `toast.exportImageMany` | {count} images will not display when sent — placeholder or data:/cid: sources |
740
+ | `toast.exportImageOne` | 1 image will not display when sent — placeholder or data:/cid: source |
738
741
  | `toast.fileUploadedMany` | {count} files uploaded |
739
742
  | `toast.fileUploadedOne` | 1 file uploaded |
740
743
  | `toast.headingAdded` | Heading added |
@@ -772,22 +775,22 @@ The complete catalog of UI strings that `.messages` accepts — every key the ed
772
775
 
773
776
  Useful when debugging an integration, or before changing the source.
774
777
 
775
- **Core / render split.** `src/core/` holds state and logic and never touches the DOM; `src/render/` builds all of it. The canvas is torn down and rebuilt on every state change no diffing and focus and caret survive through `data-focus-key` and `render/focus-preserve.js`.
778
+ **Core / render split.** `src/core/` holds state and logic and never touches the DOM; `src/render/` builds all of it. The canvas is torn down and rebuilt on every state change (no diffing), and focus and caret survive through `data-focus-key` and `render/focus-preserve.js`.
776
779
 
777
- **Shadow DOM everywhere.** The editor's styles and the host's cannot reach each other. Two consequences if you script against it: `window.getSelection()` does not see inside, and window-level listeners see a retargeted `event.target` the host element, not the real node.
780
+ **Shadow DOM everywhere.** The editor's styles and the host's cannot reach each other. Two consequences if you script against it: `window.getSelection()` does not see inside, and window-level listeners see a retargeted `event.target`: the host element, not the real node.
778
781
 
779
782
  **Rows → columns → blocks.** A document is rows, each holding columns, each holding blocks. Older documents stay valid because every renderer falls back when a prop is missing, and a normalization pass fills the gaps on the way in.
780
783
 
781
- **Import.** Real-world email HTML inline and class styles, builder scaffolding, per-side borders, card columns, social strips becomes native blocks wherever the shape is recognizable. Nested grids and `rowspan`/`colspan` survive as raw-HTML blocks: rendered and exported, not block-editable.
784
+ **Import.** Real-world email HTML (inline and class styles, builder scaffolding, per-side borders, card columns, social strips) becomes native blocks wherever the shape is recognizable. Nested grids and `rowspan`/`colspan` survive as raw-HTML blocks: rendered and exported, not block-editable.
782
785
 
783
- **Export** reads back the rendered DOM, so what the user sees is what ships. Import and export stay round-trip compatible: re-importing an export restores every inspector setting, and exporting again reproduces the same bytes. A few blocks render into markup that cannot be read back (a countdown bakes its digits, a video is a linked image), so the export stamps a compact `data-mc*` attribute layer that the importer trusts and mail clients ignore `exportHtml({ markers: false })` omits it for hosts that want pristine HTML, trading a lossy (but content-preserving) reload.
786
+ **Export** reads back the rendered DOM, so what the user sees is what ships. Import and export stay round-trip compatible: re-importing an export restores every inspector setting, and exporting again reproduces the same bytes. A few blocks render into markup that cannot be read back (a countdown bakes its digits, a video is a linked image), so the export stamps a compact `data-mc*` attribute layer that the importer trusts and mail clients ignore; `exportHtml({ markers: false })` omits it for hosts that want pristine HTML, trading a lossy (but content-preserving) reload.
784
787
 
785
- **Build.** `build.js` is a zero-dependency bundler that turns the ESM sources into one plain `<script>`, then minifies through esbuild with a sourcemap alongside. esbuild is a dev dependency only consumers install nothing transitive, and a clone with no `node_modules` still produces a working bundle.
788
+ **Build.** `build.js` is a zero-dependency bundler that turns the ESM sources into one plain `<script>`, then minifies through esbuild with a sourcemap alongside. esbuild is a dev dependency only: consumers install nothing transitive, and a clone with no `node_modules` still produces a working bundle.
786
789
 
787
790
  ```sh
788
791
  npm install # devDependencies only: esbuild (minify), jsdom (DOM tests), c8 (coverage)
789
792
  node build.js # rebuild dist/ after any change under src/
790
- npm test # 14 suites core logic runs DOM-free; the editor, importer and round-trip suites run on jsdom
793
+ npm test # 14 suites: core logic runs DOM-free; the editor, importer and round-trip suites run on jsdom
791
794
  ```
792
795
 
793
796
  Deeper notes for coding agents and anyone changing the source live in `AGENTS.md` in the repository.
package/README.md CHANGED
@@ -31,7 +31,7 @@ editor.exportHtml();
31
31
  editor.destroy();
32
32
  ```
33
33
 
34
- That is a working editor. It speaks HTML in both directions there is no document format to store or migrate:
34
+ That is a working editor. It speaks HTML in both directions; there is no document format to store or migrate:
35
35
 
36
36
  ```js
37
37
  editor.loadTemplate({ name: 'Welcome', html }); // give it HTML
@@ -45,7 +45,7 @@ Exported HTML is valid input to `loadTemplate`, so saving the export *is* saving
45
45
  | | |
46
46
  |---|---|
47
47
  | `variables="first_name,company"` | merge tags, exported as `{{first_name}}` |
48
- | Condition / Loop blocks | dynamic content exported as `{{#if …}}` / `{{#each …}}` for your sending engine |
48
+ | Condition / Loop blocks | dynamic content, exported as `{{#if …}}` / `{{#each …}}` for your sending engine |
49
49
  | `locale="de"` | 31 languages, RTL automatic |
50
50
  | `theme="light" \| "dark"` | host owns light/dark; hides the built-in toggle |
51
51
  | `ui-font="inherit"` | match your app's font |
@@ -64,7 +64,7 @@ Every attribute is also a property. TypeScript declarations ship with the packag
64
64
  Everything an assistant needs to wire this up correctly:
65
65
 
66
66
  ```
67
- PACKAGE @seliseblocks/mailcraft Web Component, zero runtime deps
67
+ PACKAGE @seliseblocks/mailcraft: Web Component, zero runtime deps
68
68
  IMPORT import '@seliseblocks/mailcraft' (side effect: registers the element)
69
69
  Safe to import under SSR; the element itself renders in a browser only.
70
70
  TYPES TypeScript declarations ship in the package (types/index.d.ts) for every
@@ -117,18 +117,18 @@ UPLOADS A provider plus `maxBytes` are required, or every upload is refused;
117
117
  limits? }
118
118
  provider.limits is merged per key under .storageLimits, and satisfies
119
119
  the requirement on its own. With no provider the library is empty and
120
- local to the draft the package ships no files of its own.
120
+ local to the draft; the package ships no files of its own.
121
121
  The editor never talks to a backend of its own; its only fetch is the
122
122
  screenshot capture inlining the template's own images.
123
123
 
124
124
  TOOLBAR Parts: logo status device undo redo theme ai code preview export
125
125
  Attribute = allow-list (keep these). Property = { part: false } (drop these).
126
126
  none|hidden|off|false = no bar at all; all = the default.
127
- Hidden undo/redo/export stay reachable methods, plus Ctrl/Cmd+Z, +E.
127
+ Hidden undo/redo/export stay reachable: methods, plus Ctrl/Cmd+Z, +E.
128
128
  preview, code and ai are bar-only: hiding them removes the panel.
129
129
 
130
- DO NOT Do not use getContent()/setContent() internal, shape may change.
131
- Do not expect a campaign/title option there is none; <title> is "Email".
130
+ DO NOT Do not use getContent()/setContent(): internal, shape may change.
131
+ Do not expect a campaign/title option; there is none. <title> is "Email".
132
132
  ```
133
133
 
134
134
  ## Develop
@@ -137,6 +137,6 @@ DO NOT Do not use getContent()/setContent() — internal, shape may change.
137
137
  npm install && node build.js && npm test
138
138
  ```
139
139
 
140
- `examples/vanilla.html` is a complete host page open it directly, no server needed. It ships in the npm package too, so it is there after an install; the [hosted copy](https://mailcraft.seliseblocks.com/examples/vanilla.html) is the same file.
140
+ `examples/vanilla.html` is a complete host page: open it directly, no server needed. It ships in the npm package too, so it is there after an install; the [hosted copy](https://mailcraft.seliseblocks.com/examples/vanilla.html) is the same file.
141
141
 
142
142
  MIT