@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.
Files changed (69) hide show
  1. package/DOCS.md +240 -12
  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 +242 -23
  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
@@ -1,80 +1,80 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Order confirmed</title>
6
- </head>
7
- <body style="margin:0; padding:0; background-color:#f4f4f2;">
8
- <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f2;">
9
- <tr>
10
- <td align="center" style="padding:24px 12px;">
11
- <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff;">
12
- <tr>
13
- <td style="padding:32px 36px 6px;">
14
- <p style="margin:0 0 8px; font-family:Arial, Helvetica, sans-serif; font-size:12px; font-weight:bold; letter-spacing:0.16em; text-transform:uppercase; color:#2f9e44;">Order confirmed</p>
15
- <h1 style="margin:0 0 10px; font-family:Arial, Helvetica, sans-serif; font-size:24px; line-height:1.25; color:#1a1a1a;">Thanks, {{ first_name }} &mdash; it&rsquo;s on its way</h1>
16
- <p style="margin:0; font-family:Arial, Helvetica, sans-serif; font-size:14px; line-height:1.6; color:#4a4a4a;">Order <strong>#48213</strong>, placed today. We&rsquo;ll email tracking as soon as the courier scans it &mdash; usually within one business day.</p>
17
- </td>
18
- </tr>
19
- <tr>
20
- <td style="padding:24px 36px 4px;">
21
- <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e6e6e2; ">
22
- <tr>
23
- <td style="padding:14px 18px; border-bottom:1px solid #e6e6e2;">
24
- <table role="presentation" width="100%" cellpadding="0" cellspacing="0">
25
- <tr>
26
- <td style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">Field jacket &mdash; olive, M</td>
27
- <td align="right" style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">$148.00</td>
28
- </tr>
29
- </table>
30
- </td>
31
- </tr>
32
- <tr>
33
- <td style="padding:14px 18px; border-bottom:1px solid #e6e6e2;">
34
- <table role="presentation" width="100%" cellpadding="0" cellspacing="0">
35
- <tr>
36
- <td style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">Merino crew &mdash; charcoal, M</td>
37
- <td align="right" style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">$85.00</td>
38
- </tr>
39
- </table>
40
- </td>
41
- </tr>
42
- <tr>
43
- <td style="padding:14px 18px; background-color:#fafaf8;">
44
- <table role="presentation" width="100%" cellpadding="0" cellspacing="0">
45
- <tr>
46
- <td style="font-family:Arial, Helvetica, sans-serif; font-size:13px; color:#4a4a4a;">Shipping</td>
47
- <td align="right" style="font-family:Arial, Helvetica, sans-serif; font-size:13px; color:#4a4a4a;">Free</td>
48
- </tr>
49
- <tr>
50
- <td style="padding-top:6px; font-family:Arial, Helvetica, sans-serif; font-size:15px; font-weight:bold; color:#1a1a1a;">Total</td>
51
- <td align="right" style="padding-top:6px; font-family:Arial, Helvetica, sans-serif; font-size:15px; font-weight:bold; color:#1a1a1a;">$233.00</td>
52
- </tr>
53
- </table>
54
- </td>
55
- </tr>
56
- </table>
57
- </td>
58
- </tr>
59
- <tr>
60
- <td align="center" style="padding:22px 36px 6px;">
61
- <a href="https://example.com/orders/48213" style="display:inline-block; background-color:#1a1a1a; color:#ffffff; font-family:Arial, Helvetica, sans-serif; font-size:14px; font-weight:bold; text-decoration:none; padding:12px 30px; ">View your order</a>
62
- </td>
63
- </tr>
64
- <tr>
65
- <td style="padding:24px 36px 8px;">
66
- <hr style="border:none; border-top:1px solid #e6e6e2; margin:0;" />
67
- </td>
68
- </tr>
69
- <tr>
70
- <td style="padding:10px 36px 30px;">
71
- <p style="margin:0 0 8px; font-family:Arial, Helvetica, sans-serif; font-size:13px; line-height:1.6; color:#4a4a4a;">Need a different size? Exchanges are free for 30 days &mdash; start one from the order page above.</p>
72
- <p style="margin:0; font-family:Arial, Helvetica, sans-serif; font-size:11.5px; line-height:1.6; color:#9a9a94;">This receipt was sent to {{ email }}. <a href="{{ unsubscribe_url }}" style="color:#9a9a94;">Unsubscribe from marketing</a> &mdash; you will still receive order emails.</p>
73
- </td>
74
- </tr>
75
- </table>
76
- </td>
77
- </tr>
78
- </table>
79
- </body>
80
- </html>
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Order confirmed</title>
6
+ </head>
7
+ <body style="margin:0; padding:0; background-color:#f4f4f2;">
8
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f2;">
9
+ <tr>
10
+ <td align="center" style="padding:24px 12px;">
11
+ <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background-color:#ffffff;">
12
+ <tr>
13
+ <td style="padding:32px 36px 6px;">
14
+ <p style="margin:0 0 8px; font-family:Arial, Helvetica, sans-serif; font-size:12px; font-weight:bold; letter-spacing:0.16em; text-transform:uppercase; color:#2f9e44;">Order confirmed</p>
15
+ <h1 style="margin:0 0 10px; font-family:Arial, Helvetica, sans-serif; font-size:24px; line-height:1.25; color:#1a1a1a;">Thanks, {{ first_name }} &mdash; it&rsquo;s on its way</h1>
16
+ <p style="margin:0; font-family:Arial, Helvetica, sans-serif; font-size:14px; line-height:1.6; color:#4a4a4a;">Order <strong>#48213</strong>, placed today. We&rsquo;ll email tracking as soon as the courier scans it &mdash; usually within one business day.</p>
17
+ </td>
18
+ </tr>
19
+ <tr>
20
+ <td style="padding:24px 36px 4px;">
21
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e6e6e2; ">
22
+ <tr>
23
+ <td style="padding:14px 18px; border-bottom:1px solid #e6e6e2;">
24
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0">
25
+ <tr>
26
+ <td style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">Field jacket &mdash; olive, M</td>
27
+ <td align="right" style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">$148.00</td>
28
+ </tr>
29
+ </table>
30
+ </td>
31
+ </tr>
32
+ <tr>
33
+ <td style="padding:14px 18px; border-bottom:1px solid #e6e6e2;">
34
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0">
35
+ <tr>
36
+ <td style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">Merino crew &mdash; charcoal, M</td>
37
+ <td align="right" style="font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1a1a1a;">$85.00</td>
38
+ </tr>
39
+ </table>
40
+ </td>
41
+ </tr>
42
+ <tr>
43
+ <td style="padding:14px 18px; background-color:#fafaf8;">
44
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0">
45
+ <tr>
46
+ <td style="font-family:Arial, Helvetica, sans-serif; font-size:13px; color:#4a4a4a;">Shipping</td>
47
+ <td align="right" style="font-family:Arial, Helvetica, sans-serif; font-size:13px; color:#4a4a4a;">Free</td>
48
+ </tr>
49
+ <tr>
50
+ <td style="padding-top:6px; font-family:Arial, Helvetica, sans-serif; font-size:15px; font-weight:bold; color:#1a1a1a;">Total</td>
51
+ <td align="right" style="padding-top:6px; font-family:Arial, Helvetica, sans-serif; font-size:15px; font-weight:bold; color:#1a1a1a;">$233.00</td>
52
+ </tr>
53
+ </table>
54
+ </td>
55
+ </tr>
56
+ </table>
57
+ </td>
58
+ </tr>
59
+ <tr>
60
+ <td align="center" style="padding:22px 36px 6px;">
61
+ <a href="https://example.com/orders/48213" style="display:inline-block; background-color:#1a1a1a; color:#ffffff; font-family:Arial, Helvetica, sans-serif; font-size:14px; font-weight:bold; text-decoration:none; padding:12px 30px; ">View your order</a>
62
+ </td>
63
+ </tr>
64
+ <tr>
65
+ <td style="padding:24px 36px 8px;">
66
+ <hr style="border:none; border-top:1px solid #e6e6e2; margin:0;" />
67
+ </td>
68
+ </tr>
69
+ <tr>
70
+ <td style="padding:10px 36px 30px;">
71
+ <p style="margin:0 0 8px; font-family:Arial, Helvetica, sans-serif; font-size:13px; line-height:1.6; color:#4a4a4a;">Need a different size? Exchanges are free for 30 days &mdash; start one from the order page above.</p>
72
+ <p style="margin:0; font-family:Arial, Helvetica, sans-serif; font-size:11.5px; line-height:1.6; color:#9a9a94;">This receipt was sent to {{ email }}. <a href="{{ unsubscribe_url }}" style="color:#9a9a94;">Unsubscribe from marketing</a> &mdash; you will still receive order emails.</p>
73
+ </td>
74
+ </tr>
75
+ </table>
76
+ </td>
77
+ </tr>
78
+ </table>
79
+ </body>
80
+ </html>
@@ -13,6 +13,9 @@
13
13
  palette (src/render/style.js) so the demo reads as one application rather
14
14
  than a widget dropped into a stranger's page. */
15
15
  :root {
16
+ /* Pinned per theme, not left at `light dark`: it is what decides whether
17
+ the browser paints this page a light or a dark scrollbar. */
18
+ color-scheme: light;
16
19
  --brand: #0065b3;
17
20
  --bg: #ffffff;
18
21
  --surface: #f8fafc;
@@ -27,6 +30,7 @@
27
30
  /* One attribute drives both: the same switch that sets the editor's `theme`
28
31
  repaints this page. Host-owned light/dark, demonstrated rather than
29
32
  described. */
33
+ :root:has(body[data-theme="dark"]) { color-scheme: dark; }
30
34
  body[data-theme="dark"] {
31
35
  --bg: #0b1120;
32
36
  --surface: #111827;
@@ -129,7 +133,8 @@
129
133
  <nav class="menu" aria-label="Primary">
130
134
  <a id="docs-link" href="../DOCS.md">Documentation</a>
131
135
  <a id="readme-link" href="../README.md">Overview</a>
132
- <a href="https://github.com/SELISEdigitalplatforms/blocks-mailcraft" rel="noopener">GitHub</a>
136
+ <!-- No GitHub link while the repository is private -- it 404s for anyone
137
+ outside the org. Restore it when the repo goes public. -->
133
138
  <a href="https://www.npmjs.com/package/@seliseblocks/mailcraft" rel="noopener">npm</a>
134
139
  </nav>
135
140
 
@@ -1449,6 +1454,8 @@
1449
1454
  const next = editor.getAttribute('theme') === 'dark' ? 'light' : 'dark';
1450
1455
  editor.setAttribute('theme', next);
1451
1456
  document.body.dataset.theme = next;
1457
+ // :has() covers modern browsers; this covers the rest, and costs one line.
1458
+ document.documentElement.style.colorScheme = next;
1452
1459
  themeBtn.textContent = next === 'dark' ? 'Switch to light' : 'Switch to dark';
1453
1460
  });
1454
1461
 
@@ -1541,32 +1548,244 @@
1541
1548
  // calls below at S3, a DMS, a CDN or your own API and the editor is
1542
1549
  // unchanged.
1543
1550
  //
1544
- // Left commented out, this demo keeps its seeded placeholder files.
1551
+ // This demo wires a real one. There is no server behind a static page, so
1552
+ // the "backend" is IndexedDB in your own browser — but everything the editor
1553
+ // is allowed to see about it behaves like a service: round-trip latency,
1554
+ // folders and search resolved on the far side, cursor-paged listings,
1555
+ // uploads that are still there after a reload, deletes that stay deleted,
1556
+ // and cancellation honoured. Replace `demoBackend` with `fetch` calls to
1557
+ // your API and the provider underneath it does not change a line.
1545
1558
  //
1546
- // editor.storageProvider = {
1547
- // async list({ folderId, cursor, query }) {
1548
- // const r = await myApi.listImages({ folder: folderId, cursor, q: query });
1549
- // return {
1550
- // items: r.files.map((f) => ({
1551
- // id: f.id, name: f.name, url: f.publicUrl,
1552
- // folder: f.folderName, folderId: f.folderId,
1553
- // w: f.width, ht: f.height, size: f.bytes,
1554
- // })),
1555
- // cursor: r.nextCursor ?? null,
1556
- // };
1557
- // },
1558
- // async upload(file, { folderId, width, height, signal }) {
1559
- // const saved = await myApi.upload(file, folderId, { signal });
1560
- // return { id: saved.id, name: file.name, url: saved.publicUrl,
1561
- // folder: '', folderId, w: width, ht: height, size: file.size };
1562
- // },
1563
- // async folders() { return myApi.folders(); }, // optional
1564
- // async remove(asset) { await myApi.delete(asset.id); }, // optional
1565
- // };
1559
+ // The editor ships no files of its own: with nothing wired here the library
1560
+ // opens empty and stays that way until someone drops something in. Every
1561
+ // file in the Assets modal is defined below — host content, in host code,
1562
+ // exactly where a real integration keeps it.
1563
+
1564
+ // ---- the pretend backend ------------------------------------------------
1565
+
1566
+ const demoBackend = (() => {
1567
+ const DB_NAME = 'mailcraft-demo-assets';
1568
+ const STORE = 'files';
1569
+ const PAGE_SIZE = 8; // small on purpose: it makes "Load more" real
1570
+ const LATENCY = 240; // roughly what one round trip to a DMS costs
1571
+
1572
+ // Folders are the backend's, not the editor's. Ids are what travel over
1573
+ // the wire; the names are only ever display strings.
1574
+ const FOLDERS = [
1575
+ { id: 'brand', name: 'Brand' },
1576
+ { id: 'product', name: 'Product' },
1577
+ { id: 'photography', name: 'Photography' },
1578
+ { id: 'uploads', name: 'Uploads' },
1579
+ ];
1580
+ const folderName = (id) => (FOLDERS.find((f) => f.id === id) || {}).name || '';
1581
+
1582
+ // The demo's own placeholder images, as SVG data URIs so the catalogue
1583
+ // needs no binaries next to this file. A real backend hands back durable
1584
+ // https URLs instead — see the note on `url` at the bottom.
1585
+ const ph = (label, w, ht) => 'data:image/svg+xml;utf8,' + encodeURIComponent(
1586
+ '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + ht + '">'
1587
+ + '<defs><pattern id="p" width="9" height="9" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">'
1588
+ + '<rect width="9" height="9" fill="#ececed"/><line x1="0" y1="0" x2="0" y2="9" stroke="#cfd3d8" stroke-width="3"/></pattern></defs>'
1589
+ + '<rect width="100%" height="100%" fill="url(#p)"/>'
1590
+ + '<rect x="0.5" y="0.5" width="' + (w - 1) + '" height="' + (ht - 1) + '" fill="none" stroke="#9aa2ab"/>'
1591
+ + '<text x="50%" y="50%" dy="4" text-anchor="middle" font-family="ui-monospace,monospace" font-size="'
1592
+ + Math.max(11, Math.round(w / 40)) + '" fill="#5b6672">' + label + '</text></svg>',
1593
+ ).replace(/\(/g, '%28').replace(/\)/g, '%29');
1594
+
1595
+ // Fourteen files across three folders: enough that a listing pages, and
1596
+ // enough that a search narrows something.
1597
+ const CATALOGUE = [
1598
+ ['logo-lockup.png', 'brand', 480, 180],
1599
+ ['logo-mark.png', 'brand', 240, 240],
1600
+ ['logo-wordmark.png', 'brand', 520, 140],
1601
+ ['jacket-03-front.jpg', 'product', 520, 360],
1602
+ ['jacket-03-detail.jpg', 'product', 520, 360],
1603
+ ['jacket-03-lining.jpg', 'product', 520, 360],
1604
+ ['boots-pair.jpg', 'product', 520, 360],
1605
+ ['boots-sole.jpg', 'product', 520, 360],
1606
+ ['satchel-tan.jpg', 'product', 520, 360],
1607
+ ['hero-workshop.jpg', 'photography', 600, 340],
1608
+ ['hero-storefront.jpg', 'photography', 600, 340],
1609
+ ['texture-waxed.jpg', 'photography', 600, 240],
1610
+ ['texture-canvas.jpg', 'photography', 600, 240],
1611
+ ['team-bench.jpg', 'photography', 600, 400],
1612
+ ];
1613
+
1614
+ // IndexedDB is unavailable on a `file://` page in Chrome, and this file is
1615
+ // meant to survive being double-clicked. The in-memory shim keeps every
1616
+ // path below identical; the only thing lost is "survives a reload".
1617
+ let memory = null;
1618
+ let dbPromise = null;
1619
+ function db() {
1620
+ if (memory) return Promise.resolve(null);
1621
+ if (!dbPromise) {
1622
+ dbPromise = new Promise((resolve) => {
1623
+ let req;
1624
+ try { req = indexedDB.open(DB_NAME, 1); } catch (e) { memory = new Map(); return resolve(null); }
1625
+ req.onupgradeneeded = () => req.result.createObjectStore(STORE, { keyPath: 'id' });
1626
+ req.onsuccess = () => resolve(req.result);
1627
+ req.onerror = () => { memory = new Map(); resolve(null); };
1628
+ });
1629
+ }
1630
+ return dbPromise;
1631
+ }
1632
+
1633
+ async function readAll() {
1634
+ const conn = await db();
1635
+ if (!conn) return Array.from(memory.values());
1636
+ return new Promise((resolve, reject) => {
1637
+ const req = conn.transaction(STORE, 'readonly').objectStore(STORE).getAll();
1638
+ req.onsuccess = () => resolve(req.result || []);
1639
+ req.onerror = () => reject(req.error);
1640
+ });
1641
+ }
1642
+
1643
+ async function write(rec) {
1644
+ const conn = await db();
1645
+ if (!conn) { memory.set(rec.id, rec); return rec; }
1646
+ return new Promise((resolve, reject) => {
1647
+ const t = conn.transaction(STORE, 'readwrite');
1648
+ t.objectStore(STORE).put(rec);
1649
+ t.oncomplete = () => resolve(rec);
1650
+ t.onerror = () => reject(t.error);
1651
+ });
1652
+ }
1653
+
1654
+ async function drop(id) {
1655
+ const conn = await db();
1656
+ if (!conn) { memory.delete(id); return; }
1657
+ return new Promise((resolve, reject) => {
1658
+ const t = conn.transaction(STORE, 'readwrite');
1659
+ t.objectStore(STORE).delete(id);
1660
+ t.oncomplete = () => resolve();
1661
+ t.onerror = () => reject(t.error);
1662
+ });
1663
+ }
1664
+
1665
+ // First run only: a real backend already has the customer's files in it.
1666
+ let seeded = null;
1667
+ function seed() {
1668
+ if (!seeded) {
1669
+ seeded = readAll().then(async (rows) => {
1670
+ if (rows.length) return;
1671
+ const t0 = Date.now() - CATALOGUE.length * 1000;
1672
+ for (let i = 0; i < CATALOGUE.length; i++) {
1673
+ const name = CATALOGUE[i][0];
1674
+ const folderId = CATALOGUE[i][1];
1675
+ const w = CATALOGUE[i][2];
1676
+ const ht = CATALOGUE[i][3];
1677
+ const url = ph(name.replace(/\.[a-z]+$/i, '').replace(/-/g, ' '), w, ht);
1678
+ await write({ id: 'seed-' + i, name: name, folderId: folderId, url: url, w: w, ht: ht, size: url.length, createdAt: t0 + i * 1000 });
1679
+ }
1680
+ });
1681
+ }
1682
+ return seeded;
1683
+ }
1684
+
1685
+ /** Latency you can cancel: an aborted request rejects the way `fetch` does. */
1686
+ function nap(ms, signal) {
1687
+ return new Promise((resolve, reject) => {
1688
+ if (signal && signal.aborted) return reject(new DOMException('Aborted', 'AbortError'));
1689
+ const id = setTimeout(resolve, ms);
1690
+ if (signal) signal.addEventListener('abort', () => { clearTimeout(id); reject(new DOMException('Aborted', 'AbortError')); }, { once: true });
1691
+ });
1692
+ }
1693
+
1694
+ const readAsDataUrl = (file) => new Promise((resolve, reject) => {
1695
+ const fr = new FileReader();
1696
+ fr.onload = () => resolve(fr.result);
1697
+ fr.onerror = () => reject(new Error('the file could not be read'));
1698
+ fr.readAsDataURL(file);
1699
+ });
1700
+
1701
+ return {
1702
+ folders: async () => { await nap(LATENCY / 2); return FOLDERS.map((f) => ({ id: f.id, name: f.name })); },
1703
+
1704
+ // Folder, search and paging are all resolved here, on the far side of the
1705
+ // wire — which is exactly why the editor stops filtering locally the
1706
+ // moment a provider is assigned. A cursor is opaque to it: this one is an
1707
+ // offset, a real one is usually a continuation token.
1708
+ async list(q) {
1709
+ await seed();
1710
+ await nap(LATENCY, q.signal);
1711
+ const term = (q.query || '').trim().toLowerCase();
1712
+ const rows = (await readAll())
1713
+ .filter((r) => (!q.folderId || r.folderId === q.folderId) && (!term || r.name.toLowerCase().includes(term)))
1714
+ .sort((a, b) => b.createdAt - a.createdAt);
1715
+ const from = Number(q.cursor) || 0;
1716
+ return {
1717
+ items: rows.slice(from, from + PAGE_SIZE).map((r) => ({
1718
+ id: r.id, name: r.name, url: r.url,
1719
+ folder: folderName(r.folderId), folderId: r.folderId,
1720
+ w: r.w, ht: r.ht, size: r.size,
1721
+ })),
1722
+ cursor: from + PAGE_SIZE < rows.length ? String(from + PAGE_SIZE) : null,
1723
+ };
1724
+ },
1725
+
1726
+ // The editor has already validated the file against `storageLimits` and
1727
+ // measured it, so `width`/`height` arrive filled in. Uploading from "All
1728
+ // files" means no folder was chosen — a backend still has to put the
1729
+ // bytes somewhere, and "Uploads" is this one's answer.
1730
+ async upload(file, opts) {
1731
+ const url = await readAsDataUrl(file);
1732
+ await nap(LATENCY * 2, opts.signal);
1733
+ const target = opts.folderId || 'uploads';
1734
+ const rec = {
1735
+ id: 'up-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 7),
1736
+ name: file.name, folderId: target, url: url,
1737
+ w: opts.width || 0, ht: opts.height || 0, size: file.size, createdAt: Date.now(),
1738
+ };
1739
+ await write(rec);
1740
+ return { id: rec.id, name: rec.name, url: rec.url, folder: folderName(target), folderId: target, w: rec.w, ht: rec.ht, size: rec.size };
1741
+ },
1742
+
1743
+ async remove(asset) {
1744
+ await nap(LATENCY);
1745
+ await drop(asset.id);
1746
+ },
1747
+
1748
+ /** Not part of the contract — a console escape hatch for this demo. */
1749
+ async reset() {
1750
+ const rows = await readAll();
1751
+ for (let i = 0; i < rows.length; i++) await drop(rows[i].id);
1752
+ seeded = null;
1753
+ await seed();
1754
+ },
1755
+ };
1756
+ })();
1757
+
1758
+ // The provider itself: four thin methods over whatever the backend is. This
1759
+ // is the entire integration — no editor internals, no imports, no build step.
1760
+ const demoStorage = {
1761
+ folders: () => demoBackend.folders(),
1762
+ list: (q) => demoBackend.list(q),
1763
+ upload: (file, opts) => demoBackend.upload(file, opts),
1764
+ remove: (asset) => demoBackend.remove(asset),
1765
+ };
1766
+ editor.storageProvider = demoStorage;
1767
+
1768
+ // Handy while poking at the demo: `mailcraftDemo.reset()` empties the
1769
+ // library, re-seeds it, and re-lists it. Re-assigning the provider is how a
1770
+ // host tells the editor to start over — the same call the line above makes.
1771
+ window.mailcraftDemo = {
1772
+ reset: () => demoBackend.reset().then(() => { editor.storageProvider = demoStorage; }),
1773
+ };
1774
+
1775
+ // Against a real API the same four methods are usually one-liners:
1776
+ //
1777
+ // list: ({ folderId, cursor, query, signal }) => myApi.listImages({ folder: folderId, cursor, q: query, signal })
1778
+ // upload: (file, { folderId, width, height, signal }) => myApi.upload(file, folderId, { signal })
1779
+ // folders: () => myApi.folders(), // optional
1780
+ // remove: (asset) => myApi.delete(asset.id), // optional
1781
+ //
1782
+ // — each response mapped onto `{ id, name, url, folder, folderId, w, ht, size }`.
1566
1783
  //
1567
1784
  // The one hard requirement is on `url`: it has to still resolve after the
1568
1785
  // campaign is sent. A URL that expires in an hour produces mail whose images
1569
- // are already broken when it lands.
1786
+ // are already broken when it lands. This demo cheats it with data URIs, which
1787
+ // no email client will render — a real integration returns the durable https
1788
+ // URL its CDN serves.
1570
1789
  //
1571
1790
  // README.md has the full contract.
1572
1791
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seliseblocks/mailcraft",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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');