@seliseblocks/mailcraft 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/DOCS.md +243 -16
  2. package/README.md +15 -6
  3. package/README.md.txt +135 -0
  4. package/dist/mailcraft-editor.bundle.js +87 -47
  5. package/dist/mailcraft-editor.bundle.js.map +3 -3
  6. package/examples/templates/activate-your-account.html +1 -1
  7. package/examples/templates/back-in-stock.html +1 -1
  8. package/examples/templates/cart-left-behind.html +1 -1
  9. package/examples/templates/community-giveaway.html +1 -1
  10. package/examples/templates/frontend-futures-invite.html +1 -1
  11. package/examples/templates/give-25-get-25.html +1 -1
  12. package/examples/templates/invoice-paid.html +1 -1
  13. package/examples/templates/meet-nova-launch.html +1 -1
  14. package/examples/templates/mega-weekend-sale.html +1 -1
  15. package/examples/templates/order-confirmed.html +80 -80
  16. package/examples/templates/rate-your-headphones.html +1 -1
  17. package/examples/templates/reset-your-password.html +1 -1
  18. package/examples/templates/thankyou-promo-code.html +1 -1
  19. package/examples/templates/the-sunday-brief.html +1 -1
  20. package/examples/templates/welcome-to-your-workspace.html +1 -1
  21. package/examples/templates/your-order-shipped.html +1 -1
  22. package/examples/templates/your-password-was-changed.html +1 -1
  23. package/examples/templates/your-signin-code.html +1 -1
  24. package/examples/vanilla.html +261 -47
  25. package/package.json +7 -2
  26. package/src/core/assets.js +10 -15
  27. package/src/core/binder.js +140 -118
  28. package/src/core/blocks.js +14 -1
  29. package/src/core/css-cascade.js +117 -117
  30. package/src/core/editor-core.js +1540 -1492
  31. package/src/core/export.js +158 -55
  32. package/src/core/i18n/ar.js +218 -177
  33. package/src/core/i18n/bg.js +195 -152
  34. package/src/core/i18n/bn.js +217 -176
  35. package/src/core/i18n/ca.js +195 -152
  36. package/src/core/i18n/cs.js +195 -152
  37. package/src/core/i18n/da.js +195 -152
  38. package/src/core/i18n/de-CH.js +195 -152
  39. package/src/core/i18n/de.js +195 -152
  40. package/src/core/i18n/dz.js +220 -179
  41. package/src/core/i18n/el.js +195 -152
  42. package/src/core/i18n/en.js +3 -11
  43. package/src/core/i18n/es.js +195 -152
  44. package/src/core/i18n/et.js +195 -152
  45. package/src/core/i18n/fi.js +195 -152
  46. package/src/core/i18n/fr.js +195 -152
  47. package/src/core/i18n/hr.js +195 -152
  48. package/src/core/i18n/hu.js +195 -152
  49. package/src/core/i18n/index.js +83 -83
  50. package/src/core/i18n/it.js +195 -152
  51. package/src/core/i18n/lt.js +195 -152
  52. package/src/core/i18n/lv.js +195 -152
  53. package/src/core/i18n/nb.js +195 -152
  54. package/src/core/i18n/nl.js +195 -152
  55. package/src/core/i18n/pl.js +195 -152
  56. package/src/core/i18n/pt.js +195 -152
  57. package/src/core/i18n/ro.js +195 -152
  58. package/src/core/i18n/ru.js +195 -152
  59. package/src/core/i18n/sk.js +195 -152
  60. package/src/core/i18n/sl.js +195 -152
  61. package/src/core/i18n/sv.js +195 -152
  62. package/src/core/i18n/tables.js +50 -50
  63. package/src/core/i18n/tr.js +195 -152
  64. package/src/core/i18n/uk.js +195 -152
  65. package/src/core/icons.js +237 -235
  66. package/src/core/ids.js +1 -1
  67. package/src/core/import-html.js +1047 -959
  68. package/src/core/layout-style.js +100 -100
  69. package/src/core/parse.js +10 -10
  70. package/src/core/placeholder.js +15 -15
  71. package/src/core/sanitize.js +141 -141
  72. package/src/core/storage-limits.js +189 -184
  73. package/src/core/storage.js +85 -85
  74. package/src/core/theme.js +15 -1
  75. package/src/core/variables.js +11 -11
  76. package/src/index.js +9 -9
  77. package/src/mailcraft-editor.js +64 -25
  78. package/src/render/block-body.js +49 -6
  79. package/src/render/canvas.js +74 -8
  80. package/src/render/fields.js +619 -588
  81. package/src/render/focus-preserve.js +158 -158
  82. package/src/render/rte.js +241 -212
  83. package/src/render/screenshot.js +138 -132
  84. package/src/render/story.js +415 -415
  85. package/src/render/style.js +44 -8
  86. package/types/index.d.ts +422 -0
@@ -218,7 +218,7 @@
218
218
  <body style="margin:0; padding:0; background-color:#eef2f6;">
219
219
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#eef2f6;">
220
220
  <tr>
221
- <td align="center" style="padding:24px 12px;">
221
+ <td align="center" style="padding:0;">
222
222
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; overflow:hidden;">
223
223
  <tr>
224
224
  <td align="center" style="background-color:#0065b3; padding:22px 36px;">
@@ -274,7 +274,7 @@
274
274
  <body style="margin:0; padding:0; background-color:#faf7f2;">
275
275
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#faf7f2;">
276
276
  <tr>
277
- <td align="center" style="padding:24px 12px;">
277
+ <td align="center" style="padding:0;">
278
278
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; ">
279
279
  <tr>
280
280
  <td align="center" style="padding:36px 36px 8px;">
@@ -339,7 +339,7 @@
339
339
  <body style="margin:0; padding:0; background-color:#f6f6f8;">
340
340
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f6f6f8;">
341
341
  <tr>
342
- <td align="center" style="padding:24px 12px;">
342
+ <td align="center" style="padding:0;">
343
343
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; ">
344
344
  <tr>
345
345
  <td align="center" style="padding:34px 36px 8px;">
@@ -415,7 +415,7 @@
415
415
  <body style="margin:0; padding:0; background-color:#0c1226;">
416
416
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#0c1226;">
417
417
  <tr>
418
- <td align="center" style="padding:24px 12px;">
418
+ <td align="center" style="padding:0;">
419
419
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#131b36; ">
420
420
  <tr>
421
421
  <td align="center" style="height:10px; line-height:10px; font-size:0; padding:0;">
@@ -498,7 +498,7 @@
498
498
  <body style="margin:0; padding:0; background-color:#0e0c1a;">
499
499
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#0e0c1a;">
500
500
  <tr>
501
- <td align="center" style="padding:24px 12px;">
501
+ <td align="center" style="padding:0;">
502
502
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#171332; ">
503
503
  <tr>
504
504
  <td align="center" style="padding:12px 36px 0;">
@@ -569,7 +569,7 @@
569
569
  <body style="margin:0; padding:0; background-color:#0f1c17;">
570
570
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#0f1c17;">
571
571
  <tr>
572
- <td align="center" style="padding:24px 12px;">
572
+ <td align="center" style="padding:0;">
573
573
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#16352a; ">
574
574
  <tr>
575
575
  <td align="center" style="background-color:#1e7a4f; padding:30px 32px 26px; ">
@@ -640,7 +640,7 @@
640
640
  <body style="margin:0; padding:0; background-color:#f4f6f9;">
641
641
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f6f9;">
642
642
  <tr>
643
- <td align="center" style="padding:24px 12px;">
643
+ <td align="center" style="padding:0;">
644
644
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; ">
645
645
  <tr>
646
646
  <td style="padding:30px 36px 10px;">
@@ -719,7 +719,7 @@
719
719
  <body style="margin:0; padding:0; background-color:#0b1020;">
720
720
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#0b1020;">
721
721
  <tr>
722
- <td align="center" style="padding:24px 12px;">
722
+ <td align="center" style="padding:0;">
723
723
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#111832; ">
724
724
  <tr>
725
725
  <td align="center" style="background-color:#0065b3; padding:34px 32px 30px; ">
@@ -790,7 +790,7 @@
790
790
  <body style="margin:0; padding:0; background-color:#ffffff;">
791
791
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#ffffff;">
792
792
  <tr>
793
- <td align="center" style="padding:24px 12px;">
793
+ <td align="center" style="padding:0;">
794
794
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#101010;">
795
795
  <tr>
796
796
  <td style="height:6px; line-height:6px; font-size:0; background-color:#d9f24f;">&nbsp;</td>
@@ -849,7 +849,7 @@
849
849
  <body style="margin:0; padding:0; background-color:#f4f4f2;">
850
850
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f2;">
851
851
  <tr>
852
- <td align="center" style="padding:24px 12px;">
852
+ <td align="center" style="padding:0;">
853
853
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff;">
854
854
  <tr>
855
855
  <td style="padding:32px 36px 6px;">
@@ -931,7 +931,7 @@
931
931
  <body style="margin:0; padding:0; background-color:#f2f4f8;">
932
932
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f2f4f8;">
933
933
  <tr>
934
- <td align="center" style="padding:24px 12px;">
934
+ <td align="center" style="padding:0;">
935
935
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; ">
936
936
  <tr>
937
937
  <td align="center" style="padding:36px 36px 10px;">
@@ -1004,7 +1004,7 @@
1004
1004
  <body style="margin:0; padding:0; background-color:#eef2f6;">
1005
1005
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#eef2f6;">
1006
1006
  <tr>
1007
- <td align="center" style="padding:24px 12px;">
1007
+ <td align="center" style="padding:0;">
1008
1008
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; overflow:hidden;">
1009
1009
  <tr>
1010
1010
  <td align="center" style="background-color:#0065b3; padding:22px 36px;">
@@ -1060,7 +1060,7 @@
1060
1060
  <body style="margin:0; padding:0; background-color:#fdf8f3;">
1061
1061
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#fdf8f3;">
1062
1062
  <tr>
1063
- <td align="center" style="padding:24px 12px;">
1063
+ <td align="center" style="padding:0;">
1064
1064
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; ">
1065
1065
  <tr>
1066
1066
  <td align="center" style="padding:38px 36px 8px;">
@@ -1130,7 +1130,7 @@
1130
1130
  <body style="margin:0; padding:0; background-color:#f1f0ec;">
1131
1131
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f1f0ec;">
1132
1132
  <tr>
1133
- <td align="center" style="padding:24px 12px;">
1133
+ <td align="center" style="padding:0;">
1134
1134
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff;">
1135
1135
  <tr>
1136
1136
  <td style="background-color:#1c1b18; padding:26px 32px;">
@@ -1179,7 +1179,7 @@
1179
1179
  <body style="margin:0; padding:0; background-color:#eef2f6;">
1180
1180
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#eef2f6;">
1181
1181
  <tr>
1182
- <td align="center" style="padding:24px 12px;">
1182
+ <td align="center" style="padding:0;">
1183
1183
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; ">
1184
1184
  <tr>
1185
1185
  <td style="height:5px; line-height:5px; font-size:0; background-color:#0065b3; ">&nbsp;</td>
@@ -1243,7 +1243,7 @@
1243
1243
  <body style="margin:0; padding:0; background-color:#eef2f6;">
1244
1244
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#eef2f6;">
1245
1245
  <tr>
1246
- <td align="center" style="padding:24px 12px;">
1246
+ <td align="center" style="padding:0;">
1247
1247
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; overflow:hidden;">
1248
1248
  <tr>
1249
1249
  <td align="center" style="background-color:#0065b3; padding:22px 36px;">
@@ -1319,7 +1319,7 @@
1319
1319
  <body style="margin:0; padding:0; background-color:#eef2f6;">
1320
1320
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#eef2f6;">
1321
1321
  <tr>
1322
- <td align="center" style="padding:24px 12px;">
1322
+ <td align="center" style="padding:0;">
1323
1323
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff; overflow:hidden;">
1324
1324
  <tr>
1325
1325
  <td align="center" style="background-color:#0065b3; padding:22px 36px;">
@@ -1379,7 +1379,7 @@
1379
1379
  <body style="margin:0; padding:0; background-color:#10141c;">
1380
1380
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#10141c;">
1381
1381
  <tr>
1382
- <td align="center" style="padding:24px 12px;">
1382
+ <td align="center" style="padding:0;">
1383
1383
  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#1a1f2b; ">
1384
1384
  <tr>
1385
1385
  <td align="center" style="padding:36px 36px 0;">
@@ -1519,9 +1519,11 @@
1519
1519
 
1520
1520
  // ---- storage ------------------------------------------------------------
1521
1521
  //
1522
- // What may be uploaded is the host app's call, so the editor ships no
1523
- // defaults: with a provider wired and no limits set, uploads are refused.
1524
- // The values below are a reasonable email starting point, not a rule --
1522
+ // What may be uploaded is the host app's call, so sizes ship undefaulted:
1523
+ // with a provider wired and no `maxBytes`, uploads are refused. Formats are
1524
+ // the exception -- every image type is allowed unless `accept` narrows it,
1525
+ // which is why no list appears below. Add one when your audience's mail
1526
+ // clients demand it --
1525
1527
  //
1526
1528
  // * JPEG/PNG/GIF render everywhere. WebP does not: Outlook on Windows
1527
1529
  // draws mail with the Word engine and shows nothing at all. AVIF is
@@ -1531,11 +1533,11 @@
1531
1533
  // * Email bodies are 600px wide, so 1600px leaves room for a retina
1532
1534
  // asset without letting a 6000px camera original through.
1533
1535
  //
1534
- // SVG is missing on purpose. It is a script host, and it renders inside the
1535
- // editor's own DOM as soon as it appears as a library tile -- listing it in
1536
- // `accept` is not enough on its own, it also needs `allowSvg: true`.
1536
+ // SVG is excluded from the default. It is a script host, and it renders
1537
+ // inside the editor's own DOM as soon as it appears as a library tile --
1538
+ // listing it in `accept` is not enough on its own, it also needs
1539
+ // `allowSvg: true`.
1537
1540
  editor.storageLimits = {
1538
- accept: ['image/jpeg', 'image/png', 'image/gif'],
1539
1541
  maxBytes: 2 * 1024 * 1024,
1540
1542
  maxWidth: 1600,
1541
1543
  maxHeight: 1600,
@@ -1548,32 +1550,244 @@
1548
1550
  // calls below at S3, a DMS, a CDN or your own API and the editor is
1549
1551
  // unchanged.
1550
1552
  //
1551
- // Left commented out, this demo keeps its seeded placeholder files.
1553
+ // This demo wires a real one. There is no server behind a static page, so
1554
+ // the "backend" is IndexedDB in your own browser — but everything the editor
1555
+ // is allowed to see about it behaves like a service: round-trip latency,
1556
+ // folders and search resolved on the far side, cursor-paged listings,
1557
+ // uploads that are still there after a reload, deletes that stay deleted,
1558
+ // and cancellation honoured. Replace `demoBackend` with `fetch` calls to
1559
+ // your API and the provider underneath it does not change a line.
1552
1560
  //
1553
- // editor.storageProvider = {
1554
- // async list({ folderId, cursor, query }) {
1555
- // const r = await myApi.listImages({ folder: folderId, cursor, q: query });
1556
- // return {
1557
- // items: r.files.map((f) => ({
1558
- // id: f.id, name: f.name, url: f.publicUrl,
1559
- // folder: f.folderName, folderId: f.folderId,
1560
- // w: f.width, ht: f.height, size: f.bytes,
1561
- // })),
1562
- // cursor: r.nextCursor ?? null,
1563
- // };
1564
- // },
1565
- // async upload(file, { folderId, width, height, signal }) {
1566
- // const saved = await myApi.upload(file, folderId, { signal });
1567
- // return { id: saved.id, name: file.name, url: saved.publicUrl,
1568
- // folder: '', folderId, w: width, ht: height, size: file.size };
1569
- // },
1570
- // async folders() { return myApi.folders(); }, // optional
1571
- // async remove(asset) { await myApi.delete(asset.id); }, // optional
1572
- // };
1561
+ // The editor ships no files of its own: with nothing wired here the library
1562
+ // opens empty and stays that way until someone drops something in. Every
1563
+ // file in the Assets modal is defined below — host content, in host code,
1564
+ // exactly where a real integration keeps it.
1565
+
1566
+ // ---- the pretend backend ------------------------------------------------
1567
+
1568
+ const demoBackend = (() => {
1569
+ const DB_NAME = 'mailcraft-demo-assets';
1570
+ const STORE = 'files';
1571
+ const PAGE_SIZE = 8; // small on purpose: it makes "Load more" real
1572
+ const LATENCY = 240; // roughly what one round trip to a DMS costs
1573
+
1574
+ // Folders are the backend's, not the editor's. Ids are what travel over
1575
+ // the wire; the names are only ever display strings.
1576
+ const FOLDERS = [
1577
+ { id: 'brand', name: 'Brand' },
1578
+ { id: 'product', name: 'Product' },
1579
+ { id: 'photography', name: 'Photography' },
1580
+ { id: 'uploads', name: 'Uploads' },
1581
+ ];
1582
+ const folderName = (id) => (FOLDERS.find((f) => f.id === id) || {}).name || '';
1583
+
1584
+ // The demo's own placeholder images, as SVG data URIs so the catalogue
1585
+ // needs no binaries next to this file. A real backend hands back durable
1586
+ // https URLs instead — see the note on `url` at the bottom.
1587
+ const ph = (label, w, ht) => 'data:image/svg+xml;utf8,' + encodeURIComponent(
1588
+ '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + ht + '">'
1589
+ + '<defs><pattern id="p" width="9" height="9" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">'
1590
+ + '<rect width="9" height="9" fill="#ececed"/><line x1="0" y1="0" x2="0" y2="9" stroke="#cfd3d8" stroke-width="3"/></pattern></defs>'
1591
+ + '<rect width="100%" height="100%" fill="url(#p)"/>'
1592
+ + '<rect x="0.5" y="0.5" width="' + (w - 1) + '" height="' + (ht - 1) + '" fill="none" stroke="#9aa2ab"/>'
1593
+ + '<text x="50%" y="50%" dy="4" text-anchor="middle" font-family="ui-monospace,monospace" font-size="'
1594
+ + Math.max(11, Math.round(w / 40)) + '" fill="#5b6672">' + label + '</text></svg>',
1595
+ ).replace(/\(/g, '%28').replace(/\)/g, '%29');
1596
+
1597
+ // Fourteen files across three folders: enough that a listing pages, and
1598
+ // enough that a search narrows something.
1599
+ const CATALOGUE = [
1600
+ ['logo-lockup.png', 'brand', 480, 180],
1601
+ ['logo-mark.png', 'brand', 240, 240],
1602
+ ['logo-wordmark.png', 'brand', 520, 140],
1603
+ ['jacket-03-front.jpg', 'product', 520, 360],
1604
+ ['jacket-03-detail.jpg', 'product', 520, 360],
1605
+ ['jacket-03-lining.jpg', 'product', 520, 360],
1606
+ ['boots-pair.jpg', 'product', 520, 360],
1607
+ ['boots-sole.jpg', 'product', 520, 360],
1608
+ ['satchel-tan.jpg', 'product', 520, 360],
1609
+ ['hero-workshop.jpg', 'photography', 600, 340],
1610
+ ['hero-storefront.jpg', 'photography', 600, 340],
1611
+ ['texture-waxed.jpg', 'photography', 600, 240],
1612
+ ['texture-canvas.jpg', 'photography', 600, 240],
1613
+ ['team-bench.jpg', 'photography', 600, 400],
1614
+ ];
1615
+
1616
+ // IndexedDB is unavailable on a `file://` page in Chrome, and this file is
1617
+ // meant to survive being double-clicked. The in-memory shim keeps every
1618
+ // path below identical; the only thing lost is "survives a reload".
1619
+ let memory = null;
1620
+ let dbPromise = null;
1621
+ function db() {
1622
+ if (memory) return Promise.resolve(null);
1623
+ if (!dbPromise) {
1624
+ dbPromise = new Promise((resolve) => {
1625
+ let req;
1626
+ try { req = indexedDB.open(DB_NAME, 1); } catch (e) { memory = new Map(); return resolve(null); }
1627
+ req.onupgradeneeded = () => req.result.createObjectStore(STORE, { keyPath: 'id' });
1628
+ req.onsuccess = () => resolve(req.result);
1629
+ req.onerror = () => { memory = new Map(); resolve(null); };
1630
+ });
1631
+ }
1632
+ return dbPromise;
1633
+ }
1634
+
1635
+ async function readAll() {
1636
+ const conn = await db();
1637
+ if (!conn) return Array.from(memory.values());
1638
+ return new Promise((resolve, reject) => {
1639
+ const req = conn.transaction(STORE, 'readonly').objectStore(STORE).getAll();
1640
+ req.onsuccess = () => resolve(req.result || []);
1641
+ req.onerror = () => reject(req.error);
1642
+ });
1643
+ }
1644
+
1645
+ async function write(rec) {
1646
+ const conn = await db();
1647
+ if (!conn) { memory.set(rec.id, rec); return rec; }
1648
+ return new Promise((resolve, reject) => {
1649
+ const t = conn.transaction(STORE, 'readwrite');
1650
+ t.objectStore(STORE).put(rec);
1651
+ t.oncomplete = () => resolve(rec);
1652
+ t.onerror = () => reject(t.error);
1653
+ });
1654
+ }
1655
+
1656
+ async function drop(id) {
1657
+ const conn = await db();
1658
+ if (!conn) { memory.delete(id); return; }
1659
+ return new Promise((resolve, reject) => {
1660
+ const t = conn.transaction(STORE, 'readwrite');
1661
+ t.objectStore(STORE).delete(id);
1662
+ t.oncomplete = () => resolve();
1663
+ t.onerror = () => reject(t.error);
1664
+ });
1665
+ }
1666
+
1667
+ // First run only: a real backend already has the customer's files in it.
1668
+ let seeded = null;
1669
+ function seed() {
1670
+ if (!seeded) {
1671
+ seeded = readAll().then(async (rows) => {
1672
+ if (rows.length) return;
1673
+ const t0 = Date.now() - CATALOGUE.length * 1000;
1674
+ for (let i = 0; i < CATALOGUE.length; i++) {
1675
+ const name = CATALOGUE[i][0];
1676
+ const folderId = CATALOGUE[i][1];
1677
+ const w = CATALOGUE[i][2];
1678
+ const ht = CATALOGUE[i][3];
1679
+ const url = ph(name.replace(/\.[a-z]+$/i, '').replace(/-/g, ' '), w, ht);
1680
+ await write({ id: 'seed-' + i, name: name, folderId: folderId, url: url, w: w, ht: ht, size: url.length, createdAt: t0 + i * 1000 });
1681
+ }
1682
+ });
1683
+ }
1684
+ return seeded;
1685
+ }
1686
+
1687
+ /** Latency you can cancel: an aborted request rejects the way `fetch` does. */
1688
+ function nap(ms, signal) {
1689
+ return new Promise((resolve, reject) => {
1690
+ if (signal && signal.aborted) return reject(new DOMException('Aborted', 'AbortError'));
1691
+ const id = setTimeout(resolve, ms);
1692
+ if (signal) signal.addEventListener('abort', () => { clearTimeout(id); reject(new DOMException('Aborted', 'AbortError')); }, { once: true });
1693
+ });
1694
+ }
1695
+
1696
+ const readAsDataUrl = (file) => new Promise((resolve, reject) => {
1697
+ const fr = new FileReader();
1698
+ fr.onload = () => resolve(fr.result);
1699
+ fr.onerror = () => reject(new Error('the file could not be read'));
1700
+ fr.readAsDataURL(file);
1701
+ });
1702
+
1703
+ return {
1704
+ folders: async () => { await nap(LATENCY / 2); return FOLDERS.map((f) => ({ id: f.id, name: f.name })); },
1705
+
1706
+ // Folder, search and paging are all resolved here, on the far side of the
1707
+ // wire — which is exactly why the editor stops filtering locally the
1708
+ // moment a provider is assigned. A cursor is opaque to it: this one is an
1709
+ // offset, a real one is usually a continuation token.
1710
+ async list(q) {
1711
+ await seed();
1712
+ await nap(LATENCY, q.signal);
1713
+ const term = (q.query || '').trim().toLowerCase();
1714
+ const rows = (await readAll())
1715
+ .filter((r) => (!q.folderId || r.folderId === q.folderId) && (!term || r.name.toLowerCase().includes(term)))
1716
+ .sort((a, b) => b.createdAt - a.createdAt);
1717
+ const from = Number(q.cursor) || 0;
1718
+ return {
1719
+ items: rows.slice(from, from + PAGE_SIZE).map((r) => ({
1720
+ id: r.id, name: r.name, url: r.url,
1721
+ folder: folderName(r.folderId), folderId: r.folderId,
1722
+ w: r.w, ht: r.ht, size: r.size,
1723
+ })),
1724
+ cursor: from + PAGE_SIZE < rows.length ? String(from + PAGE_SIZE) : null,
1725
+ };
1726
+ },
1727
+
1728
+ // The editor has already validated the file against `storageLimits` and
1729
+ // measured it, so `width`/`height` arrive filled in. Uploading from "All
1730
+ // files" means no folder was chosen — a backend still has to put the
1731
+ // bytes somewhere, and "Uploads" is this one's answer.
1732
+ async upload(file, opts) {
1733
+ const url = await readAsDataUrl(file);
1734
+ await nap(LATENCY * 2, opts.signal);
1735
+ const target = opts.folderId || 'uploads';
1736
+ const rec = {
1737
+ id: 'up-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 7),
1738
+ name: file.name, folderId: target, url: url,
1739
+ w: opts.width || 0, ht: opts.height || 0, size: file.size, createdAt: Date.now(),
1740
+ };
1741
+ await write(rec);
1742
+ return { id: rec.id, name: rec.name, url: rec.url, folder: folderName(target), folderId: target, w: rec.w, ht: rec.ht, size: rec.size };
1743
+ },
1744
+
1745
+ async remove(asset) {
1746
+ await nap(LATENCY);
1747
+ await drop(asset.id);
1748
+ },
1749
+
1750
+ /** Not part of the contract — a console escape hatch for this demo. */
1751
+ async reset() {
1752
+ const rows = await readAll();
1753
+ for (let i = 0; i < rows.length; i++) await drop(rows[i].id);
1754
+ seeded = null;
1755
+ await seed();
1756
+ },
1757
+ };
1758
+ })();
1759
+
1760
+ // The provider itself: four thin methods over whatever the backend is. This
1761
+ // is the entire integration — no editor internals, no imports, no build step.
1762
+ const demoStorage = {
1763
+ folders: () => demoBackend.folders(),
1764
+ list: (q) => demoBackend.list(q),
1765
+ upload: (file, opts) => demoBackend.upload(file, opts),
1766
+ remove: (asset) => demoBackend.remove(asset),
1767
+ };
1768
+ editor.storageProvider = demoStorage;
1769
+
1770
+ // Handy while poking at the demo: `mailcraftDemo.reset()` empties the
1771
+ // library, re-seeds it, and re-lists it. Re-assigning the provider is how a
1772
+ // host tells the editor to start over — the same call the line above makes.
1773
+ window.mailcraftDemo = {
1774
+ reset: () => demoBackend.reset().then(() => { editor.storageProvider = demoStorage; }),
1775
+ };
1776
+
1777
+ // Against a real API the same four methods are usually one-liners:
1778
+ //
1779
+ // list: ({ folderId, cursor, query, signal }) => myApi.listImages({ folder: folderId, cursor, q: query, signal })
1780
+ // upload: (file, { folderId, width, height, signal }) => myApi.upload(file, folderId, { signal })
1781
+ // folders: () => myApi.folders(), // optional
1782
+ // remove: (asset) => myApi.delete(asset.id), // optional
1783
+ //
1784
+ // — each response mapped onto `{ id, name, url, folder, folderId, w, ht, size }`.
1573
1785
  //
1574
1786
  // The one hard requirement is on `url`: it has to still resolve after the
1575
1787
  // campaign is sent. A URL that expires in an hour produces mail whose images
1576
- // are already broken when it lands.
1788
+ // are already broken when it lands. This demo cheats it with data URIs, which
1789
+ // no email client will render — a real integration returns the durable https
1790
+ // URL its CDN serves.
1577
1791
  //
1578
1792
  // README.md has the full contract.
1579
1793
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seliseblocks/mailcraft",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Framework-agnostic drag-and-drop email template editor, packaged as a zero-dependency Web Component.",
5
5
  "license": "MIT",
6
6
  "author": "SELISE Digital Platforms",
@@ -30,10 +30,14 @@
30
30
  ],
31
31
  "main": "./src/index.js",
32
32
  "module": "./src/index.js",
33
+ "types": "./types/index.d.ts",
33
34
  "unpkg": "./dist/mailcraft-editor.bundle.js",
34
35
  "jsdelivr": "./dist/mailcraft-editor.bundle.js",
35
36
  "exports": {
36
- ".": "./src/index.js",
37
+ ".": {
38
+ "types": "./types/index.d.ts",
39
+ "default": "./src/index.js"
40
+ },
37
41
  "./dist/*": "./dist/*",
38
42
  "./src/*": "./src/*",
39
43
  "./package.json": "./package.json"
@@ -41,6 +45,7 @@
41
45
  "files": [
42
46
  "src",
43
47
  "dist",
48
+ "types",
44
49
  "examples",
45
50
  "README.md",
46
51
  "DOCS.md",
@@ -1,15 +1,10 @@
1
- import { uid } from './ids.js';
2
- import { PH } from './placeholder.js';
3
-
4
- export const FOLDERS = ['All files', 'Brand', 'Product', 'Photography', 'Uploads'];
5
-
6
- export const seedAssets = () => [
7
- { id: uid(), name: 'logo-lockup.png', url: PH('logo lockup', 480, 180), folder: 'Brand', w: 480, ht: 180, size: 18400 },
8
- { id: uid(), name: 'hero-workshop.jpg', url: PH('workshop hero', 600, 340), folder: 'Photography', w: 600, ht: 340, size: 142000 },
9
- { id: uid(), name: 'jacket-03-front.jpg', url: PH('jacket 03 front', 520, 360), folder: 'Product', w: 520, ht: 360, size: 98000 },
10
- { id: uid(), name: 'jacket-03-detail.jpg', url: PH('jacket 03 detail', 520, 360), folder: 'Product', w: 520, ht: 360, size: 91000 },
11
- { id: uid(), name: 'boots-pair.jpg', url: PH('boots pair', 520, 360), folder: 'Product', w: 520, ht: 360, size: 88000 },
12
- { id: uid(), name: 'texture-waxed.jpg', url: PH('waxed texture', 600, 240), folder: 'Photography', w: 600, ht: 240, size: 64000 },
13
- ];
14
-
15
- export const KB = (n) => (n > 1048576 ? (n / 1048576).toFixed(1) + ' MB' : Math.round(n / 1024) + ' KB');
1
+ /**
2
+ * Asset-size formatting.
3
+ *
4
+ * No file list lives here. The library is the host's: with a `storageProvider`
5
+ * wired the files come from the backend, and without one it starts empty and
6
+ * holds only what the user drops in this session. Shipping a fabricated
7
+ * catalogue of "brand" and "product" files meant every integrator's first run
8
+ * showed six files nobody in their organisation had ever uploaded.
9
+ */
10
+ export const KB = (n) => (n > 1048576 ? (n / 1048576).toFixed(1) + ' MB' : Math.round(n / 1024) + ' KB');