@seliseblocks/mailcraft 0.1.1 → 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.
Files changed (69) hide show
  1. package/DOCS.md +238 -10
  2. package/README.md +11 -3
  3. package/README.md.txt +134 -0
  4. package/dist/mailcraft-editor.bundle.js +51 -39
  5. package/dist/mailcraft-editor.bundle.js.map +3 -3
  6. package/examples/templates/order-confirmed.html +80 -80
  7. package/examples/vanilla.html +234 -22
  8. package/package.json +7 -2
  9. package/src/core/assets.js +10 -15
  10. package/src/core/binder.js +120 -118
  11. package/src/core/blocks.js +14 -1
  12. package/src/core/css-cascade.js +117 -117
  13. package/src/core/editor-core.js +1530 -1492
  14. package/src/core/export.js +142 -55
  15. package/src/core/i18n/ar.js +219 -177
  16. package/src/core/i18n/bg.js +196 -152
  17. package/src/core/i18n/bn.js +218 -176
  18. package/src/core/i18n/ca.js +196 -152
  19. package/src/core/i18n/cs.js +196 -152
  20. package/src/core/i18n/da.js +196 -152
  21. package/src/core/i18n/de-CH.js +196 -152
  22. package/src/core/i18n/de.js +196 -152
  23. package/src/core/i18n/dz.js +221 -179
  24. package/src/core/i18n/el.js +196 -152
  25. package/src/core/i18n/en.js +3 -10
  26. package/src/core/i18n/es.js +196 -152
  27. package/src/core/i18n/et.js +196 -152
  28. package/src/core/i18n/fi.js +196 -152
  29. package/src/core/i18n/fr.js +196 -152
  30. package/src/core/i18n/hr.js +196 -152
  31. package/src/core/i18n/hu.js +196 -152
  32. package/src/core/i18n/index.js +83 -83
  33. package/src/core/i18n/it.js +196 -152
  34. package/src/core/i18n/lt.js +196 -152
  35. package/src/core/i18n/lv.js +196 -152
  36. package/src/core/i18n/nb.js +196 -152
  37. package/src/core/i18n/nl.js +196 -152
  38. package/src/core/i18n/pl.js +196 -152
  39. package/src/core/i18n/pt.js +196 -152
  40. package/src/core/i18n/ro.js +196 -152
  41. package/src/core/i18n/ru.js +196 -152
  42. package/src/core/i18n/sk.js +196 -152
  43. package/src/core/i18n/sl.js +196 -152
  44. package/src/core/i18n/sv.js +196 -152
  45. package/src/core/i18n/tables.js +50 -50
  46. package/src/core/i18n/tr.js +196 -152
  47. package/src/core/i18n/uk.js +196 -152
  48. package/src/core/icons.js +237 -235
  49. package/src/core/ids.js +1 -1
  50. package/src/core/import-html.js +1025 -959
  51. package/src/core/layout-style.js +100 -100
  52. package/src/core/parse.js +10 -10
  53. package/src/core/placeholder.js +15 -15
  54. package/src/core/sanitize.js +141 -141
  55. package/src/core/storage-limits.js +184 -184
  56. package/src/core/storage.js +85 -85
  57. package/src/core/theme.js +1 -1
  58. package/src/core/variables.js +11 -11
  59. package/src/index.js +9 -9
  60. package/src/mailcraft-editor.js +26 -14
  61. package/src/render/block-body.js +49 -6
  62. package/src/render/canvas.js +31 -2
  63. package/src/render/fields.js +602 -588
  64. package/src/render/focus-preserve.js +158 -158
  65. package/src/render/rte.js +241 -212
  66. package/src/render/screenshot.js +132 -132
  67. package/src/render/story.js +415 -415
  68. package/src/render/style.js +8 -0
  69. 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 | ~553 KB minified, ~152 KB gzipped, one file |
48
+ | Size | ~639 KB minified, ~172 KB gzipped, one file |
49
49
  | Languages | 31, RTL automatic |
50
- | Content blocks | 18 — text, heading, image, button, divider, spacer, social, video, countdown, menu, list, table, box, html, css, code, svg, embed |
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 makes a network request
73
+ ### 2. The editor never talks to a backend of its own
74
74
 
75
- There is no `fetch` anywhere in the package. No base URL, no auth, no upload endpoint, no telemetry.
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:
@@ -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. Without them the library keeps its built-in demo files and every upload is refused.
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` puts the built-in demo library back, which is what an unconfigured editor shows.
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` restores the built-in library |
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,9 +773,9 @@ 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 # one devDependency (esbuild)
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 # storage, export, templates, toolbar, system no DOM needed
778
+ npm test # 13 suites core logic runs DOM-free, the editor/importer suites run on jsdom
551
779
  ```
552
780
 
553
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.
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. .storageProvider = null restores the demo library.
106
- The editor never makes a network request itself.
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