@seliseblocks/mailcraft 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +240 -12
- package/README.md +11 -3
- package/README.md.txt +134 -0
- package/dist/mailcraft-editor.bundle.js +51 -39
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/examples/templates/order-confirmed.html +80 -80
- package/examples/vanilla.html +242 -23
- package/package.json +7 -2
- package/src/core/assets.js +10 -15
- package/src/core/binder.js +120 -118
- package/src/core/blocks.js +14 -1
- package/src/core/css-cascade.js +117 -117
- package/src/core/editor-core.js +1530 -1492
- package/src/core/export.js +142 -55
- package/src/core/i18n/ar.js +219 -177
- package/src/core/i18n/bg.js +196 -152
- package/src/core/i18n/bn.js +218 -176
- package/src/core/i18n/ca.js +196 -152
- package/src/core/i18n/cs.js +196 -152
- package/src/core/i18n/da.js +196 -152
- package/src/core/i18n/de-CH.js +196 -152
- package/src/core/i18n/de.js +196 -152
- package/src/core/i18n/dz.js +221 -179
- package/src/core/i18n/el.js +196 -152
- package/src/core/i18n/en.js +3 -10
- package/src/core/i18n/es.js +196 -152
- package/src/core/i18n/et.js +196 -152
- package/src/core/i18n/fi.js +196 -152
- package/src/core/i18n/fr.js +196 -152
- package/src/core/i18n/hr.js +196 -152
- package/src/core/i18n/hu.js +196 -152
- package/src/core/i18n/index.js +83 -83
- package/src/core/i18n/it.js +196 -152
- package/src/core/i18n/lt.js +196 -152
- package/src/core/i18n/lv.js +196 -152
- package/src/core/i18n/nb.js +196 -152
- package/src/core/i18n/nl.js +196 -152
- package/src/core/i18n/pl.js +196 -152
- package/src/core/i18n/pt.js +196 -152
- package/src/core/i18n/ro.js +196 -152
- package/src/core/i18n/ru.js +196 -152
- package/src/core/i18n/sk.js +196 -152
- package/src/core/i18n/sl.js +196 -152
- package/src/core/i18n/sv.js +196 -152
- package/src/core/i18n/tables.js +50 -50
- package/src/core/i18n/tr.js +196 -152
- package/src/core/i18n/uk.js +196 -152
- package/src/core/icons.js +237 -235
- package/src/core/ids.js +1 -1
- package/src/core/import-html.js +1025 -959
- package/src/core/layout-style.js +100 -100
- package/src/core/parse.js +10 -10
- package/src/core/placeholder.js +15 -15
- package/src/core/sanitize.js +141 -141
- package/src/core/storage-limits.js +184 -184
- package/src/core/storage.js +85 -85
- package/src/core/theme.js +1 -1
- package/src/core/variables.js +11 -11
- package/src/index.js +9 -9
- package/src/mailcraft-editor.js +26 -14
- package/src/render/block-body.js +49 -6
- package/src/render/canvas.js +31 -2
- package/src/render/fields.js +602 -588
- package/src/render/focus-preserve.js +158 -158
- package/src/render/rte.js +241 -212
- package/src/render/screenshot.js +132 -132
- package/src/render/story.js +415 -415
- package/src/render/style.js +8 -0
- package/types/index.d.ts +419 -0
package/DOCS.md
CHANGED
|
@@ -45,9 +45,9 @@ A user drags rows and blocks onto a canvas, edits text in place, styles it in an
|
|
|
45
45
|
| Runtime dependencies | none |
|
|
46
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
|
-
| Size | ~
|
|
48
|
+
| Size | ~639 KB minified, ~172 KB gzipped, one file |
|
|
49
49
|
| Languages | 31, RTL automatic |
|
|
50
|
-
| Content blocks |
|
|
50
|
+
| Content blocks | 20 — text, heading, image, button, divider, spacer, social, video, countdown, menu, list, table, box, html, css, code, svg, embed, condition, loop |
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
@@ -70,9 +70,9 @@ Exported HTML is valid input to the importer, so **saving the export is saving t
|
|
|
70
70
|
|
|
71
71
|
The internal document still exists. It is simply not yours to hold.
|
|
72
72
|
|
|
73
|
-
### 2. The editor never
|
|
73
|
+
### 2. The editor never talks to a backend of its own
|
|
74
74
|
|
|
75
|
-
|
|
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.
|
|
76
76
|
|
|
77
77
|
Image storage is a plain object the host assigns:
|
|
78
78
|
|
|
@@ -119,6 +119,12 @@ 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:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import type { StorageProvider, StorageLimits } from '@seliseblocks/mailcraft';
|
|
126
|
+
```
|
|
127
|
+
|
|
122
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.
|
|
123
129
|
|
|
124
130
|
Without a bundler:
|
|
@@ -233,7 +239,7 @@ Templates are host content **and** host UI. The editor ships no catalogue and ha
|
|
|
233
239
|
editor.loadTemplate({ name: 'Welcome', html });
|
|
234
240
|
```
|
|
235
241
|
|
|
236
|
-
Applying one is a normal undoable edit with a toast, and your string is never mutated. Ready-made examples
|
|
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.
|
|
237
243
|
|
|
238
244
|
## Merge variables
|
|
239
245
|
|
|
@@ -243,9 +249,27 @@ Applying one is a normal undoable edit with a toast, and your string is never mu
|
|
|
243
249
|
|
|
244
250
|
Users insert them from the toolbar; they export as `{{first_name}}`.
|
|
245
251
|
|
|
252
|
+
### Conditions and loops
|
|
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.
|
|
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:
|
|
257
|
+
|
|
258
|
+
```html
|
|
259
|
+
{{#if has_order}}
|
|
260
|
+
{{#each order.items}}
|
|
261
|
+
…whatever sits between the markers, once per item…
|
|
262
|
+
{{/each}}
|
|
263
|
+
{{/if}}
|
|
264
|
+
```
|
|
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.
|
|
267
|
+
|
|
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
|
+
|
|
246
270
|
## Image uploads
|
|
247
271
|
|
|
248
|
-
Both properties are required.
|
|
272
|
+
Both properties are 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 limits every upload is refused.
|
|
249
273
|
|
|
250
274
|
```js
|
|
251
275
|
editor.storageLimits = {
|
|
@@ -294,7 +318,9 @@ editor.storageProvider = { list, upload, limits: { accept: [...], maxBytes: 5e6
|
|
|
294
318
|
|
|
295
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 "limits are required" rule; only a file that passes the merged result reaches `upload`.
|
|
296
320
|
|
|
297
|
-
Setting `editor.storageProvider = null`
|
|
321
|
+
Setting `editor.storageProvider = null` drops back to that empty local library.
|
|
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.
|
|
298
324
|
|
|
299
325
|
## Choosing what the top bar shows
|
|
300
326
|
|
|
@@ -399,6 +425,8 @@ Override any individual string:
|
|
|
399
425
|
editor.messages = { 'action.export': 'Send to campaign' };
|
|
400
426
|
```
|
|
401
427
|
|
|
428
|
+
Every key, with its English default, is listed in [Every message key](#every-message-key). English is always the floor: a key your table leaves out shows the built-in English rather than a gap, and `missingKeys(yourTable)` tells you what a full replacement still lacks.
|
|
429
|
+
|
|
402
430
|
## Matching your app's typography
|
|
403
431
|
|
|
404
432
|
```html
|
|
@@ -480,7 +508,7 @@ template, not to your app.
|
|
|
480
508
|
| `.uiFont` | string |
|
|
481
509
|
| `.accent` | string |
|
|
482
510
|
| `.messages` | `{ key: string }` |
|
|
483
|
-
| `.storageProvider` | `{ list, upload, folders?, remove?, limits? }` — `null`
|
|
511
|
+
| `.storageProvider` | `{ list, upload, folders?, remove?, limits? }` — `null` drops back to the empty local library |
|
|
484
512
|
| `.storageLimits` | `{ accept, maxBytes, maxWidth?, maxHeight?, maxFilesPerDrop?, allowSvg? }`, merged over `provider.limits` per key |
|
|
485
513
|
| `.aiProvider` | `async (prompt) => text` |
|
|
486
514
|
| `.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 |
|
|
@@ -495,7 +523,7 @@ template, not to your app.
|
|
|
495
523
|
| `LOCALES`, `LOCALE_TABLES`, `createTranslator` | i18n: the shipped tags, their tables, and the translator the editor uses |
|
|
496
524
|
| `defineMessages(base, overrides)` | merge a shipped locale with your own overrides — the supported way to build a `.messages` value |
|
|
497
525
|
| `missingKeys(locale, base)` | keys `base` has that `locale` does not translate. What a translator has left to do |
|
|
498
|
-
| `EN`, `MESSAGE_KEYS` | the English table and every key in it |
|
|
526
|
+
| `EN`, `MESSAGE_KEYS` | the English table and every key in it — listed in [Every message key](#every-message-key) |
|
|
499
527
|
| `isRtl(tag)` | whether a locale tag is right-to-left. Metadata — `dir` is what actually flips the layout |
|
|
500
528
|
| `validateFiles`, `acceptAttribute`, `sanitizeName` | upload validation, reusable outside the editor |
|
|
501
529
|
| `limitsProblem(limits)` | what a limits object is missing, if anything — the check that refuses uploads |
|
|
@@ -526,6 +554,206 @@ template, not to your app.
|
|
|
526
554
|
|
|
527
555
|
`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.
|
|
528
556
|
|
|
557
|
+
### Every message key
|
|
558
|
+
|
|
559
|
+
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.
|
|
560
|
+
|
|
561
|
+
<!-- message-keys:begin — generated by build.js from src/core/i18n/en.js; edit en.js and run `node build.js`, never this table. -->
|
|
562
|
+
|
|
563
|
+
| key | English default |
|
|
564
|
+
|---|---|
|
|
565
|
+
| `action.aiDraft` | Draft |
|
|
566
|
+
| `action.aiDraftHint` | Draft email content with AI |
|
|
567
|
+
| `action.applyToCanvas` | Apply to canvas |
|
|
568
|
+
| `action.applyToCanvasHint` | Parse the source back into editable rows |
|
|
569
|
+
| `action.chromeHint` | Light / dark editor chrome |
|
|
570
|
+
| `action.chromeHintToDark` | Dark mode |
|
|
571
|
+
| `action.chromeHintToLight` | Light mode |
|
|
572
|
+
| `action.chromeToDark` | Dark |
|
|
573
|
+
| `action.chromeToLight` | Light |
|
|
574
|
+
| `action.close` | Close |
|
|
575
|
+
| `action.closeWithoutApplyingHint` | Close without applying |
|
|
576
|
+
| `action.code` | Code |
|
|
577
|
+
| `action.codeHint` | Edit the raw HTML with a live preview |
|
|
578
|
+
| `action.deleteHint` | Delete — ⌫ |
|
|
579
|
+
| `action.download` | Download |
|
|
580
|
+
| `action.downloadPng` | Download PNG |
|
|
581
|
+
| `action.duplicateHint` | Duplicate — ⌘D |
|
|
582
|
+
| `action.export` | Export |
|
|
583
|
+
| `action.exportHint` | Export your email |
|
|
584
|
+
| `action.preview` | Preview |
|
|
585
|
+
| `action.previewHint` | Preview the email |
|
|
586
|
+
| `action.redoHint` | Redo — ⌘⇧Z |
|
|
587
|
+
| `action.reload` | Reload |
|
|
588
|
+
| `action.reloadHint` | Reload the source from the canvas |
|
|
589
|
+
| `action.screenshot` | Screenshot |
|
|
590
|
+
| `action.screenshotHint` | Preview the full template as an image, then download it |
|
|
591
|
+
| `action.undoHint` | Undo — ⌘Z |
|
|
592
|
+
| `action.upload` | Upload |
|
|
593
|
+
| `ai.actionInsertButton` | Insert as button |
|
|
594
|
+
| `ai.actionInsertHeading` | Insert as heading |
|
|
595
|
+
| `ai.actionInsertText` | Insert as text block |
|
|
596
|
+
| `ai.briefLabel` | What is this email about? |
|
|
597
|
+
| `ai.briefPlaceholder` | e.g. Autumn outerwear drop, 15% for subscribers, free shipping over $150 |
|
|
598
|
+
| `ai.disclosure` | AI creates suggestions from your brief. Review and edit the result before sending. |
|
|
599
|
+
| `ai.generate` | Generate draft with AI |
|
|
600
|
+
| `ai.goal` | Goal |
|
|
601
|
+
| `ai.kindBody` | Body copy |
|
|
602
|
+
| `ai.kindButton` | Button label |
|
|
603
|
+
| `ai.kindHeadline` | Headline |
|
|
604
|
+
| `ai.tone` | Tone |
|
|
605
|
+
| `ai.writing` | Writing… |
|
|
606
|
+
| `blocks.count` | {count} blocks |
|
|
607
|
+
| `blocks.dragHint` | Drag a block onto the canvas |
|
|
608
|
+
| `canvas.meta` | {rows} rows · {blocks} blocks · {width}px |
|
|
609
|
+
| `code.applyNote` | Apply parses each top-level table row back into a canvas row you can still select, reorder and delete — hand-written markup survives the round trip. |
|
|
610
|
+
| `code.liveHtmlPreviewTitle` | Live HTML preview |
|
|
611
|
+
| `code.livePreview` | Live preview |
|
|
612
|
+
| `code.meta` | {kb} KB · {lines} lines |
|
|
613
|
+
| `code.source` | Source |
|
|
614
|
+
| `code.statusEdited` | Code view — edited — preview live, not yet applied |
|
|
615
|
+
| `code.statusSynced` | Code view — in sync with the canvas |
|
|
616
|
+
| `device.desktop` | Desktop |
|
|
617
|
+
| `device.desktopHint` | Desktop width |
|
|
618
|
+
| `device.mobile` | Mobile |
|
|
619
|
+
| `device.mobileHint` | Mobile width |
|
|
620
|
+
| `export.copied` | Copied |
|
|
621
|
+
| `export.copy` | Copy email |
|
|
622
|
+
| `export.meta` | {kb} KB · ready to copy or download |
|
|
623
|
+
| `files.dragToCanvas` | Drag any file straight onto the canvas to place it as an image block. |
|
|
624
|
+
| `files.libraryCount` | Library — {count} files |
|
|
625
|
+
| `files.openManager` | Open file manager |
|
|
626
|
+
| `footer.poweredBy` | Powered by SELISE Blocks © 2026 |
|
|
627
|
+
| `inspector.blockProperties` | Block properties |
|
|
628
|
+
| `inspector.emptyBody` | Pick a row or block on the canvas to style it here. Drag blocks in from the <strong style="color: var(--ed-text); font-weight: 600;">Content blocks</strong> tab; double-click any text to edit it in place. |
|
|
629
|
+
| `inspector.nothingSelected` | Nothing selected |
|
|
630
|
+
| `inspector.sectionLabel` | Section |
|
|
631
|
+
| `inspector.sectionProperties` | Section properties |
|
|
632
|
+
| `inspector.shortcuts` | Shortcuts |
|
|
633
|
+
| `inspector.title` | Inspector |
|
|
634
|
+
| `layers.blockCount` | {count} blocks |
|
|
635
|
+
| `layers.column` | Column {index} |
|
|
636
|
+
| `layers.sectionMulti` | Section · {count} columns |
|
|
637
|
+
| `layers.sectionSingle` | Section |
|
|
638
|
+
| `layers.structure` | Structure |
|
|
639
|
+
| `library.allFiles` | All files |
|
|
640
|
+
| `library.clickToPlace` | Click a file to place it · drag a file onto the canvas |
|
|
641
|
+
| `library.clickToReplace` | Click a file to replace the selected image |
|
|
642
|
+
| `library.del` | DEL |
|
|
643
|
+
| `library.deleteFileHint` | Delete file |
|
|
644
|
+
| `library.dropHint` | Drop image files here to upload — or use the Upload button. |
|
|
645
|
+
| `library.empty` | No files here yet. |
|
|
646
|
+
| `library.folders` | Folders |
|
|
647
|
+
| `library.loadMore` | Load more |
|
|
648
|
+
| `library.searchPlaceholder` | Search files… |
|
|
649
|
+
| `library.storageLabel` | {count} files · {size} |
|
|
650
|
+
| `modal.aiDraft` | AI writing assistant |
|
|
651
|
+
| `modal.aiDraftTitle` | Draft your email with AI |
|
|
652
|
+
| `modal.assetsTitle` | Assets |
|
|
653
|
+
| `modal.export` | Export |
|
|
654
|
+
| `modal.exportTitle` | Your email is ready |
|
|
655
|
+
| `modal.fileManager` | File manager |
|
|
656
|
+
| `modal.rawHtmlTitle` | Raw HTML, live preview |
|
|
657
|
+
| `modal.replaceImage` | Replace image |
|
|
658
|
+
| `preview.kickerDesktop` | Preview — desktop {width}px |
|
|
659
|
+
| `preview.kickerMobile` | Preview — mobile 375px |
|
|
660
|
+
| `rows.canvasModel` | Canvas model |
|
|
661
|
+
| `rows.customMarkup` | Custom markup |
|
|
662
|
+
| `rows.hintRows` | Section based: drop a layout first, then drop blocks into its columns — every section carries its own background, border, flex or grid settings. |
|
|
663
|
+
| `rows.hintStack` | Free stack: skip layouts — every block you drop becomes its own full-width section, stacked in order. |
|
|
664
|
+
| `rows.htmlRowHint` | Drag in, or click to append — a full-bleed row you fill with raw HTML |
|
|
665
|
+
| `rows.modeFreeStack` | Free stack |
|
|
666
|
+
| `rows.modeRowBased` | Row based |
|
|
667
|
+
| `rows.rawHtmlDesc` | Full-bleed section, your markup, still draggable |
|
|
668
|
+
| `rows.rawHtmlSection` | Raw HTML section |
|
|
669
|
+
| `rows.sectionLayouts` | Section layouts |
|
|
670
|
+
| `rte.clearFormatting` | Clear formatting |
|
|
671
|
+
| `rte.highlightColor` | Highlight color |
|
|
672
|
+
| `rte.mergeTags` | Merge Tags |
|
|
673
|
+
| `rte.noMergeTags` | No merge tags available |
|
|
674
|
+
| `rte.removeHighlight` | Remove highlight |
|
|
675
|
+
| `rte.textColor` | Text color |
|
|
676
|
+
| `rte.textStyle` | Text style |
|
|
677
|
+
| `shortcut.delete` | ⌫ — delete selection |
|
|
678
|
+
| `shortcut.duplicate` | ⌘D — duplicate selection |
|
|
679
|
+
| `shortcut.escape` | ESC — deselect, close |
|
|
680
|
+
| `shortcut.export` | ⌘E — export email |
|
|
681
|
+
| `shortcut.undoRedo` | ⌘Z / ⌘⇧Z — undo, redo |
|
|
682
|
+
| `status.autosaveOn` | autosave on |
|
|
683
|
+
| `status.saveFailed` | not saved — storage full |
|
|
684
|
+
| `status.saved` | saved {time} |
|
|
685
|
+
| `storage.errDeleteFailed` | {name} could not be deleted — {reason} |
|
|
686
|
+
| `storage.errDimensions` | {name} is {w}×{ht}px — the limit is {maxW}×{maxH}px. |
|
|
687
|
+
| `storage.errFormat` | {name} is a {type} file, which is not allowed here. |
|
|
688
|
+
| `storage.errLoadFailed` | The file library could not be loaded — {reason} |
|
|
689
|
+
| `storage.errNoAccept` | Uploads are switched off: no file formats have been allowed. |
|
|
690
|
+
| `storage.errNoLimits` | Uploads are switched off: this app has not set any upload limits. |
|
|
691
|
+
| `storage.errNoMaxBytes` | Uploads are switched off: no maximum file size has been set. |
|
|
692
|
+
| `storage.errSvg` | {name} is an SVG, which this app does not accept. |
|
|
693
|
+
| `storage.errTooLarge` | {name} is {size} — the limit is {max}. |
|
|
694
|
+
| `storage.errTooMany` | Only {max} files can be uploaded at once. |
|
|
695
|
+
| `storage.errUnreadable` | {name} is not a readable image. |
|
|
696
|
+
| `storage.errUploadFailed` | {name} could not be uploaded — {reason} |
|
|
697
|
+
| `storage.loading` | Loading files… |
|
|
698
|
+
| `storage.uploading` | Uploading {count}… |
|
|
699
|
+
| `story.copy` | Copy |
|
|
700
|
+
| `story.failed` | Screenshot failed |
|
|
701
|
+
| `story.failedHint` | An image in the template could not be loaded for capture. |
|
|
702
|
+
| `story.kicker` | Screenshot |
|
|
703
|
+
| `story.meta` | {w}×{h} · screen {i} of {n} |
|
|
704
|
+
| `story.pause` | Pause |
|
|
705
|
+
| `story.play` | Play |
|
|
706
|
+
| `story.rendering` | Rendering the screenshot |
|
|
707
|
+
| `story.renderingHint` | Painting every block at 2× — this takes a moment on a long template. |
|
|
708
|
+
| `story.replay` | Replay |
|
|
709
|
+
| `story.retry` | Try again |
|
|
710
|
+
| `tab.blocks` | Content blocks |
|
|
711
|
+
| `tab.blocksHint` | Content blocks and variables |
|
|
712
|
+
| `tab.data` | Variables |
|
|
713
|
+
| `tab.dataHint` | Variables provided by your code |
|
|
714
|
+
| `tab.design` | Design |
|
|
715
|
+
| `tab.designHint` | Style the selection |
|
|
716
|
+
| `tab.files` | Assets |
|
|
717
|
+
| `tab.filesHint` | Asset library |
|
|
718
|
+
| `tab.layers` | Layers |
|
|
719
|
+
| `tab.layersHint` | Document structure |
|
|
720
|
+
| `tab.rows` | Sections |
|
|
721
|
+
| `tab.rowsHint` | Section layouts and canvas model |
|
|
722
|
+
| `tab.settings` | Settings |
|
|
723
|
+
| `tab.settingsHint` | Email settings |
|
|
724
|
+
| `toast.assetDeleted` | {name} deleted |
|
|
725
|
+
| `toast.buttonAdded` | Button added |
|
|
726
|
+
| `toast.duplicated` | Duplicated |
|
|
727
|
+
| `toast.fileUploadedMany` | {count} files uploaded |
|
|
728
|
+
| `toast.fileUploadedOne` | 1 file uploaded |
|
|
729
|
+
| `toast.headingAdded` | Heading added |
|
|
730
|
+
| `toast.htmlCopied` | HTML copied to clipboard |
|
|
731
|
+
| `toast.imageAdded` | Image added to canvas |
|
|
732
|
+
| `toast.imageReplaced` | Image replaced |
|
|
733
|
+
| `toast.parseError` | Could not parse that HTML |
|
|
734
|
+
| `toast.pngCopied` | Screenshot copied to clipboard |
|
|
735
|
+
| `toast.pngCopyFailed` | This browser would not accept an image on the clipboard |
|
|
736
|
+
| `toast.pngFailed` | Screenshot failed — an external image could not be loaded |
|
|
737
|
+
| `toast.pngSaved` | Screenshot downloaded |
|
|
738
|
+
| `toast.snippetCopied` | {name} copied — select a text block to insert it |
|
|
739
|
+
| `toast.snippetDefaultLabel` | Snippet |
|
|
740
|
+
| `toast.snippetInserted` | {name} inserted |
|
|
741
|
+
| `toast.sourceAppliedMany` | Source applied — {rows} rows back on the canvas |
|
|
742
|
+
| `toast.sourceAppliedOne` | Source applied — 1 row back on the canvas |
|
|
743
|
+
| `toast.sourceReloaded` | Source reloaded from canvas |
|
|
744
|
+
| `toast.templateLoaded` | {name} loaded |
|
|
745
|
+
| `toast.textBlockAdded` | Text block added |
|
|
746
|
+
| `vars.declaredNote` | Declared by your application — the canvas always shows the token, never a value. |
|
|
747
|
+
| `vars.filterPlaceholder` | Filter variables… |
|
|
748
|
+
| `vars.fromCode` | {count} from your code |
|
|
749
|
+
| `vars.insert` | insert |
|
|
750
|
+
| `vars.insertHint` | Click to insert into the selected text, heading or button |
|
|
751
|
+
| `vars.noMatch` | No variable matches that filter. |
|
|
752
|
+
| `vars.noneDeclared` | Your application has not declared any variables yet. |
|
|
753
|
+
| `vars.rteHint` | While editing text you can also pick a variable straight from the inline toolbar’s dropdown. |
|
|
754
|
+
| `vars.title` | Variables |
|
|
755
|
+
<!-- message-keys:end -->
|
|
756
|
+
|
|
529
757
|
---
|
|
530
758
|
|
|
531
759
|
# How it works inside
|
|
@@ -545,12 +773,12 @@ Useful when debugging an integration, and required reading before contributing.
|
|
|
545
773
|
**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.
|
|
546
774
|
|
|
547
775
|
```sh
|
|
548
|
-
npm install #
|
|
776
|
+
npm install # devDependencies only: esbuild (minify), jsdom (DOM tests), c8 (coverage)
|
|
549
777
|
node build.js # rebuild dist/ after any change under src/
|
|
550
|
-
npm test #
|
|
778
|
+
npm test # 13 suites — core logic runs DOM-free, the editor/importer suites run on jsdom
|
|
551
779
|
```
|
|
552
780
|
|
|
553
|
-
Deeper notes for contributors and coding agents live in
|
|
781
|
+
Deeper notes for contributors and coding agents live in `AGENTS.md` in the repository, alongside [CONTRIBUTING](https://mailcraft.seliseblocks.com/CONTRIBUTING.html), which is also on the docs site.
|
|
554
782
|
|
|
555
783
|
---
|
|
556
784
|
|
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ Exported HTML is valid input to `loadTemplate`, so saving the export *is* saving
|
|
|
42
42
|
| | |
|
|
43
43
|
|---|---|
|
|
44
44
|
| `variables="first_name,company"` | merge tags, exported as `{{first_name}}` |
|
|
45
|
+
| Condition / Loop blocks | dynamic content — exported as `{{#if …}}` / `{{#each …}}` for your sending engine |
|
|
45
46
|
| `locale="de"` | 31 languages, RTL automatic |
|
|
46
47
|
| `theme="light" \| "dark"` | host owns light/dark; hides the built-in toggle |
|
|
47
48
|
| `ui-font="inherit"` | match your app's font |
|
|
@@ -53,7 +54,7 @@ Exported HTML is valid input to `loadTemplate`, so saving the export *is* saving
|
|
|
53
54
|
| `.aiProvider` | `async (prompt) => text`, powers the AI draft panel |
|
|
54
55
|
| `.messages` | override any UI string |
|
|
55
56
|
|
|
56
|
-
Every attribute is also a property. Full reference and integration recipes: **[DOCS.md](DOCS.md)**.
|
|
57
|
+
Every attribute is also a property. TypeScript declarations ship with the package. Full reference and integration recipes: **[DOCS.md](DOCS.md)**.
|
|
57
58
|
|
|
58
59
|
## For AI agents
|
|
59
60
|
|
|
@@ -63,6 +64,8 @@ Everything an assistant needs to wire this up correctly:
|
|
|
63
64
|
PACKAGE @seliseblocks/mailcraft — Web Component, zero runtime deps
|
|
64
65
|
IMPORT import '@seliseblocks/mailcraft' (side effect: registers the element)
|
|
65
66
|
Safe to import under SSR; the element itself renders in a browser only.
|
|
67
|
+
TYPES TypeScript declarations ship in the package (types/index.d.ts) for every
|
|
68
|
+
named export; 'mailcraft-editor' is registered in HTMLElementTagNameMap.
|
|
66
69
|
ELEMENT <mailcraft-editor id="editor"></mailcraft-editor>
|
|
67
70
|
MOUNT createEditor(target, options) -> handle (no tag needed)
|
|
68
71
|
target = CSS selector or Element; throws if it matches nothing
|
|
@@ -79,6 +82,9 @@ DATA HTML in, HTML out. There is no JSON document format in the public API.
|
|
|
79
82
|
editor.exportHtml() -> string send-ready email HTML
|
|
80
83
|
Persist by storing exportHtml() and passing it back to loadTemplate().
|
|
81
84
|
Unclassifiable markup survives as a raw-HTML block. Nothing is dropped.
|
|
85
|
+
Condition/Loop blocks export literal {{#if expr}}/{{#each expr}} tags
|
|
86
|
+
(always balanced) for the host's templating engine; never evaluated
|
|
87
|
+
by the editor, and re-import restores them as blocks.
|
|
82
88
|
|
|
83
89
|
EVENTS 'change' detail = internal doc (do not persist this)
|
|
84
90
|
'export' detail = HTML string
|
|
@@ -102,8 +108,10 @@ UPLOADS Limits and a provider are both required, or every upload is refused.
|
|
|
102
108
|
editor.storageProvider = { list(q), upload(file, o), folders?(), remove?(a),
|
|
103
109
|
limits? }
|
|
104
110
|
provider.limits is merged per key under .storageLimits, and satisfies
|
|
105
|
-
the requirement on its own.
|
|
106
|
-
|
|
111
|
+
the requirement on its own. With no provider the library is empty and
|
|
112
|
+
local to the draft — the package ships no files of its own.
|
|
113
|
+
The editor never talks to a backend of its own; its only fetch is the
|
|
114
|
+
screenshot capture inlining the template's own images.
|
|
107
115
|
|
|
108
116
|
TOOLBAR Parts: logo status device undo redo theme ai code preview export
|
|
109
117
|
Attribute = allow-list (keep these). Property = { part: false } (drop these).
|
package/README.md.txt
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# MailCraft
|
|
2
|
+
|
|
3
|
+
Drag-and-drop email editor as a Web Component. No dependencies, no framework, no build step.
|
|
4
|
+
|
|
5
|
+
**[Live demo](https://mailcraft.seliseblocks.com/examples/vanilla.html)** · **[Documentation](https://mailcraft.seliseblocks.com/DOCS.html)**
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @seliseblocks/mailcraft
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Use
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import '@seliseblocks/mailcraft'; // registers <mailcraft-editor>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<mailcraft-editor id="editor"></mailcraft-editor>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or mount it into a container from code, without writing the tag:
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { createEditor } from '@seliseblocks/mailcraft';
|
|
25
|
+
|
|
26
|
+
const editor = createEditor('#mail', { html, toolbar: false });
|
|
27
|
+
editor.exportHtml();
|
|
28
|
+
editor.destroy();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That is a working editor. It speaks HTML in both directions — there is no document format to store or migrate:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
editor.loadTemplate({ name: 'Welcome', html }); // give it HTML
|
|
35
|
+
const html = editor.exportHtml(); // get send-ready HTML back
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Exported HTML is valid input to `loadTemplate`, so saving the export *is* saving the user's work.
|
|
39
|
+
|
|
40
|
+
## Configure
|
|
41
|
+
|
|
42
|
+
| | |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `variables="first_name,company"` | merge tags, exported as `{{first_name}}` |
|
|
45
|
+
| Condition / Loop blocks | dynamic content — exported as `{{#if …}}` / `{{#each …}}` for your sending engine |
|
|
46
|
+
| `locale="de"` | 31 languages, RTL automatic |
|
|
47
|
+
| `theme="light" \| "dark"` | host owns light/dark; hides the built-in toggle |
|
|
48
|
+
| `ui-font="inherit"` | match your app's font |
|
|
49
|
+
| `accent="#e11d48"` | your brand color, everywhere the editor is accented; also `var(--brand)` or `inherit` |
|
|
50
|
+
| `toolbar="none"` | hide the editor's own top bar |
|
|
51
|
+
| `footer="none"` | drop the "Powered by SELISE Blocks © 2026" strip, or pass your own line |
|
|
52
|
+
| `toolbar="undo,redo,export"` | …or keep only these parts |
|
|
53
|
+
| `.storageProvider` + `.storageLimits` | image uploads to your backend |
|
|
54
|
+
| `.aiProvider` | `async (prompt) => text`, powers the AI draft panel |
|
|
55
|
+
| `.messages` | override any UI string |
|
|
56
|
+
|
|
57
|
+
Every attribute is also a property. TypeScript declarations ship with the package. Full reference and integration recipes: **[DOCS.md](DOCS.md)**.
|
|
58
|
+
|
|
59
|
+
## For AI agents
|
|
60
|
+
|
|
61
|
+
Everything an assistant needs to wire this up correctly:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
PACKAGE @seliseblocks/mailcraft — Web Component, zero runtime deps
|
|
65
|
+
IMPORT import '@seliseblocks/mailcraft' (side effect: registers the element)
|
|
66
|
+
Safe to import under SSR; the element itself renders in a browser only.
|
|
67
|
+
TYPES TypeScript declarations ship in the package (types/index.d.ts) for every
|
|
68
|
+
named export; 'mailcraft-editor' is registered in HTMLElementTagNameMap.
|
|
69
|
+
ELEMENT <mailcraft-editor id="editor"></mailcraft-editor>
|
|
70
|
+
MOUNT createEditor(target, options) -> handle (no tag needed)
|
|
71
|
+
target = CSS selector or Element; throws if it matches nothing
|
|
72
|
+
options = html, name, variables, locale, dir, theme, uiFont, accent, toolbar,
|
|
73
|
+
footer, storageProvider, storageLimits, aiProvider, iconProvider,
|
|
74
|
+
messages, height, replace, onChange(doc), onExport(html)
|
|
75
|
+
name = template name for `html`; read only alongside it
|
|
76
|
+
handle = the METHODS below + .element + .destroy()
|
|
77
|
+
The container supplies the height; the editor fills it.
|
|
78
|
+
|
|
79
|
+
DATA HTML in, HTML out. There is no JSON document format in the public API.
|
|
80
|
+
editor.loadTemplate({ name, html }) apply HTML (undoable, deep-copied)
|
|
81
|
+
editor.importHtml(html) apply HTML (same importer)
|
|
82
|
+
editor.exportHtml() -> string send-ready email HTML
|
|
83
|
+
Persist by storing exportHtml() and passing it back to loadTemplate().
|
|
84
|
+
Unclassifiable markup survives as a raw-HTML block. Nothing is dropped.
|
|
85
|
+
Condition/Loop blocks export literal {{#if expr}}/{{#each expr}} tags
|
|
86
|
+
(always balanced) for the host's templating engine; never evaluated
|
|
87
|
+
by the editor, and re-import restores them as blocks.
|
|
88
|
+
|
|
89
|
+
EVENTS 'change' detail = internal doc (do not persist this)
|
|
90
|
+
'export' detail = HTML string
|
|
91
|
+
|
|
92
|
+
ATTRS variables="a,b,c" | locale="de" | theme="dark" | dir="rtl"
|
|
93
|
+
ui-font="inherit" | accent="#e11d48" | accent="var(--brand)"
|
|
94
|
+
toolbar="none" | toolbar="undo,redo,export" | footer="none"
|
|
95
|
+
PROPS .variables .toolbar .footer .uiFont .accent .messages .aiProvider .iconProvider
|
|
96
|
+
.storageProvider .storageLimits
|
|
97
|
+
METHODS exportHtml() importHtml(html) loadTemplate(tpl) undo() redo()
|
|
98
|
+
screenshotPng() previewScreenshot() downloadScreenshot() copyScreenshot()
|
|
99
|
+
|
|
100
|
+
KEYS Bound on window, so they survive toolbar="none":
|
|
101
|
+
Esc (leave field / deselect + close), Ctrl/Cmd+Z, Shift+Ctrl/Cmd+Z,
|
|
102
|
+
Ctrl/Cmd+E (export dialog), Ctrl/Cmd+K (link, while editing),
|
|
103
|
+
Ctrl/Cmd+D (duplicate), Backspace|Delete (delete selection).
|
|
104
|
+
|
|
105
|
+
UPLOADS Limits and a provider are both required, or every upload is refused.
|
|
106
|
+
editor.storageLimits = { accept:['image/jpeg','image/png','image/gif'],
|
|
107
|
+
maxBytes: 2*1024*1024 }
|
|
108
|
+
editor.storageProvider = { list(q), upload(file, o), folders?(), remove?(a),
|
|
109
|
+
limits? }
|
|
110
|
+
provider.limits is merged per key under .storageLimits, and satisfies
|
|
111
|
+
the requirement on its own. With no provider the library is empty and
|
|
112
|
+
local to the draft — the package ships no files of its own.
|
|
113
|
+
The editor never talks to a backend of its own; its only fetch is the
|
|
114
|
+
screenshot capture inlining the template's own images.
|
|
115
|
+
|
|
116
|
+
TOOLBAR Parts: logo status device undo redo theme ai code preview export
|
|
117
|
+
Attribute = allow-list (keep these). Property = { part: false } (drop these).
|
|
118
|
+
none|hidden|off|false = no bar at all; all = the default.
|
|
119
|
+
Hidden undo/redo/export stay reachable — methods, plus Ctrl/Cmd+Z, +E.
|
|
120
|
+
preview, code and ai are bar-only: hiding them removes the panel.
|
|
121
|
+
|
|
122
|
+
DO NOT Do not use getContent()/setContent() — internal, shape may change.
|
|
123
|
+
Do not expect a campaign/title option — there is none; <title> is "Email".
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Develop
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
npm install && node build.js && npm test
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`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.
|
|
133
|
+
|
|
134
|
+
MIT
|