@seliseblocks/mailcraft 0.2.5 → 0.2.7

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 (51) hide show
  1. package/DOCS.md +4 -3
  2. package/README.md +4 -1
  3. package/README.md.txt +4 -1
  4. package/dist/mailcraft-editor.bundle.js +57 -54
  5. package/dist/mailcraft-editor.bundle.js.map +3 -3
  6. package/examples/vanilla.html +19 -18
  7. package/package.json +1 -1
  8. package/src/core/brand.js +81 -0
  9. package/src/core/editor-core.js +41 -8
  10. package/src/core/export.js +8 -1
  11. package/src/core/i18n/ar.js +2 -2
  12. package/src/core/i18n/bg.js +2 -2
  13. package/src/core/i18n/bn.js +2 -2
  14. package/src/core/i18n/ca.js +2 -2
  15. package/src/core/i18n/cs.js +2 -2
  16. package/src/core/i18n/da.js +2 -2
  17. package/src/core/i18n/de-CH.js +2 -2
  18. package/src/core/i18n/de.js +2 -2
  19. package/src/core/i18n/dz.js +2 -2
  20. package/src/core/i18n/el.js +2 -2
  21. package/src/core/i18n/en.js +2 -2
  22. package/src/core/i18n/es.js +2 -2
  23. package/src/core/i18n/et.js +2 -2
  24. package/src/core/i18n/fi.js +2 -2
  25. package/src/core/i18n/fr.js +2 -2
  26. package/src/core/i18n/hr.js +2 -2
  27. package/src/core/i18n/hu.js +2 -2
  28. package/src/core/i18n/it.js +2 -2
  29. package/src/core/i18n/lt.js +2 -2
  30. package/src/core/i18n/lv.js +2 -2
  31. package/src/core/i18n/nb.js +2 -2
  32. package/src/core/i18n/nl.js +2 -2
  33. package/src/core/i18n/pl.js +2 -2
  34. package/src/core/i18n/pt.js +2 -2
  35. package/src/core/i18n/ro.js +2 -2
  36. package/src/core/i18n/ru.js +2 -2
  37. package/src/core/i18n/sk.js +2 -2
  38. package/src/core/i18n/sl.js +2 -2
  39. package/src/core/i18n/sv.js +2 -2
  40. package/src/core/i18n/tr.js +2 -2
  41. package/src/core/i18n/uk.js +2 -2
  42. package/src/core/icons.js +1 -0
  43. package/src/core/import-html.js +63 -9
  44. package/src/core/sanitize.js +34 -0
  45. package/src/core/theme.js +1 -1
  46. package/src/mailcraft-editor.js +20 -10
  47. package/src/render/block-body.js +12 -6
  48. package/src/render/canvas.js +7 -1
  49. package/src/render/fields.js +9 -1
  50. package/src/render/screenshot.js +13 -2
  51. package/src/render/style.js +14 -11
@@ -186,6 +186,8 @@ export function renderDoc(core, live) {
186
186
  // Same contract for the full content-area border: set only when asked,
187
187
  // so the chrome's own subtle frame keeps marking the sheet otherwise.
188
188
  border: borderW ? borderW + 'px ' + (theme.borderStyle || 'solid') + ' ' + (theme.borderColor || '#e2e2e5') : '',
189
+ // And for the canvas-wide drop shadow -- unset keeps the chrome's own.
190
+ boxShadow: theme.shadow || '',
189
191
  // Clipping the rows to that corner is right for the sent email (export
190
192
  // emits `overflow:hidden` alongside the radius) and for the static
191
193
  // preview, but not for the editable canvas: the sheet is also what the
@@ -229,7 +231,11 @@ export function renderDoc(core, live) {
229
231
  showLine(rowDropTracker, rowLines[index]);
230
232
  });
231
233
  page.addEventListener('dragleave', (e) => { if (e.target === root || e.target === page) hideActive(rowDropTracker); });
232
- page.addEventListener('drop', (e) => { hideActive(rowDropTracker); core.canvasDrop(e); });
234
+ // Measured against `root`, exactly as the line above it was: the slots are
235
+ // the sheet's children, not the page's, and deriving the index a second
236
+ // time from the event's own target is what used to send every dropped
237
+ // section to index 0 while the line sat where the pointer actually was.
238
+ page.addEventListener('drop', (e) => { hideActive(rowDropTracker); core.canvasDrop(e, core.indexFromPoint(root, e.clientY)); });
233
239
  }
234
240
 
235
241
  d.rows.forEach((r, ri) => {
@@ -1,5 +1,6 @@
1
1
  import { icon, brandIcon, socialKey, SOCIAL_BRAND, contrastInk } from '../core/icons.js';
2
2
  import { parseItems } from '../core/parse.js';
3
+ import { linkHref } from '../core/sanitize.js';
3
4
 
4
5
  function el(tag, style, attrs) {
5
6
  const node = document.createElement(tag);
@@ -226,7 +227,14 @@ function renderRichLinks(f) {
226
227
  const urlCommit = typeCommit((v) => change(i, (a) => a.setAttribute('href', v)));
227
228
  url.addEventListener('input', (e) => urlCommit.call(e.target.value));
228
229
  url.addEventListener('focus', () => { url.style.borderColor = 'var(--ed-accent)'; url.style.outline = 'none'; });
229
- url.addEventListener('blur', () => { url.style.borderColor = 'var(--ed-line)'; urlCommit.flush(); });
230
+ url.addEventListener('blur', () => {
231
+ url.style.borderColor = 'var(--ed-line)';
232
+ // Scheme normalization happens on the way out, never per keystroke: a
233
+ // half-typed "h" rewritten to "https://h" under the caret is unusable.
234
+ url.value = linkHref(url.value);
235
+ urlCommit.call(url.value);
236
+ urlCommit.flush();
237
+ });
230
238
 
231
239
  const targetLabel = el('label', { display: 'flex', alignItems: 'center', gap: '6px', marginTop: '7px', width: 'fit-content', color: 'var(--ed-muted)', cursor: 'pointer', fontFamily: 'var(--ed-font)', fontSize: '10.5px', fontWeight: '500' });
232
240
  const target = el('input', { accentColor: 'var(--ed-accent)', cursor: 'pointer' }, { type: 'checkbox', 'data-focus-key': `f${f.key}-link-target-${i}` });
@@ -124,9 +124,20 @@ export const captureTemplatePng = async function (core, mountInto, scale = 2) {
124
124
  img.onerror = () => reject(new Error('SVG rasterization failed'));
125
125
  img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
126
126
  });
127
+ // WebKit fires `load` on an SVG-as-image before it is reliably
128
+ // drawable, and drawing on that tick yields a blank or half-laid-out
129
+ // frame. Same `decode()` guard the inner images get above.
130
+ if (img.decode) await img.decode().catch(() => {});
131
+ // Safari caps total canvas area, and past the cap it hands back a
132
+ // *blank* canvas that still encodes successfully -- a silently empty
133
+ // PNG with nothing thrown to catch. Long newsletters reach it easily
134
+ // at 2x (a 680x4000 template is 10.9M device pixels). Scale the
135
+ // capture down to fit instead of returning an empty image.
136
+ const MAX_CANVAS_AREA = 16e6;
137
+ const fit = Math.min(scale, Math.sqrt(MAX_CANVAS_AREA / Math.max(1, w * h)));
127
138
  const canvas = document.createElement('canvas');
128
- canvas.width = Math.max(1, Math.round(w * scale));
129
- canvas.height = Math.max(1, Math.round(h * scale));
139
+ canvas.width = Math.max(1, Math.round(w * fit));
140
+ canvas.height = Math.max(1, Math.round(h * fit));
130
141
  const ctx = canvas.getContext('2d');
131
142
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
132
143
  return await new Promise((resolve, reject) => {
@@ -23,6 +23,10 @@ export const STYLE = `
23
23
  /* The accent as it must read on the email sheet, which is a white page in
24
24
  both themes -- so this family does NOT flip with the chrome (see the dark
25
25
  palette below, where it is deliberately not overridden). */
26
+ /* Logo ink. Deliberately literal rather than accent-derived: the host can
27
+ repaint the chrome through the brand attribute, and a logo that follows
28
+ it stops being the logo. Values are the official light artwork. */
29
+ --ed-logo-mark: #2D86C7; --ed-logo-text: #3C3939;
26
30
  --ed-accent-sheet: #0065b3; --ed-accent-sheet-strong: #004f8c; --ed-accent-sheet-ink: #ffffff;
27
31
  --ed-accent-sheet-line: rgba(0,101,179,0.5);
28
32
  --ed-grid: rgba(71,85,105,0.10);
@@ -43,6 +47,8 @@ export const STYLE = `
43
47
  --ed-panel-label: #e2e8f0; --ed-panel-value: #c0cad8; --ed-panel-meta: #8d9aae;
44
48
  --ed-accent: #58a8e3; --ed-accent-strong: #8fc4ec; --ed-accent-ink: #08111f; --ed-soft: rgba(88,168,227,0.13);
45
49
  --ed-accent-tint: #8fcdf2; --ed-glow: rgba(88,168,227,0.28); --ed-select: rgba(88,168,227,0.3);
50
+ /* The official dark-mode logo artwork: mark and wordmark both go light. */
51
+ --ed-logo-mark: #F2F2F2; --ed-logo-text: #EEEEEE;
46
52
  --ed-grid: rgba(148,163,184,0.08);
47
53
  --ed-danger: #f2777c; --ed-danger-soft: rgba(242,119,124,0.14);
48
54
  --ed-success: #4ade95; --ed-success-soft: rgba(74,222,149,0.14);
@@ -151,14 +157,9 @@ export const STYLE = `
151
157
  .mc-footer-link { cursor: pointer; transition: color 0.14s ease; }
152
158
  .mc-footer-link:hover { color: var(--ed-accent); text-decoration: underline; }
153
159
 
154
- /* Brand mark -- the small gradient envelope glyph in the header. */
155
- .mc-brand-mark {
156
- display: flex; align-items: center; justify-content: center; flex: none;
157
- width: 26px; height: 26px; border-radius: 8px;
158
- background: linear-gradient(145deg, var(--ed-accent-tint), var(--ed-accent) 55%, var(--ed-accent-strong));
159
- box-shadow: 0 5px 14px var(--ed-glow);
160
- color: #ffffff;
161
- }
160
+ /* The header logo. It is the official lockup artwork, so it gets no chip, no
161
+ gradient and no recolouring -- only a height. */
162
+ .mc-brand { display: flex; align-items: center; flex: none; }
162
163
 
163
164
  /*
164
165
  * Shared segmented-control shell -- one bordered/backed track that owns the
@@ -269,9 +270,7 @@ export const STYLE = `
269
270
  /* Opaque, no backdrop-filter: the header sits over the scrolling canvas, so a
270
271
  backdrop blur re-composites every scroll frame -- visible scroll jank. */
271
272
  #mc .mc-header { padding: 0 18px !important; background: var(--ed-panel) !important; }
272
- #mc .mc-brand-mark { width: 30px !important; height: 30px !important; border-radius: 9px; background: linear-gradient(145deg, var(--ed-accent-tint), var(--ed-accent) 56%, var(--ed-accent-strong)) !important; box-shadow: 0 7px 18px var(--ed-glow); }
273
- #mc .mc-brand-mark svg { width: 20px; height: 20px; display: block; color: #fff; }
274
- #mc .mc-brand-name { font-family: var(--ed-font) !important; font-size: 17px !important; letter-spacing: -.03em !important; }
273
+ #mc .mc-brand svg { height: 34px; width: auto; display: block; }
275
274
  #mc .mc-header button { font-family: var(--ed-font) !important; font-size: 10.5px !important; font-weight: 600; letter-spacing: .015em !important; text-transform: none !important; }
276
275
  #mc .mc-header > div { border-radius: 10px; }
277
276
  #mc .mc-segment { gap: 3px !important; padding: 3px !important; border: 0 !important; border-radius: 10px; background: var(--ed-panel-2) !important; }
@@ -304,6 +303,10 @@ export const STYLE = `
304
303
  #mc .mc-inspector .mc-section-body { padding: 12px 14px 16px !important; }
305
304
  #mc .mc-inspector .mc-section-body > :first-child { margin-top: 0 !important; }
306
305
  #mc .mc-field-list { display: grid; align-content: start; gap: 12px; }
306
+ /* The author display:grid above beats the UA rule [hidden]{display:none},
307
+ which is exactly how the accordion silently stopped collapsing: the bar
308
+ toggled the hidden attribute and the grid stayed painted. Hidden wins. */
309
+ #mc .mc-field-list[hidden], #mc .mc-section-body[hidden] { display: none !important; }
307
310
  #mc .mc-field-list .mc-field { min-width: 0; }
308
311
  #mc .mc-field-list .mc-field-row {
309
312
  display: grid !important; grid-template-columns: minmax(0, 1fr) 168px;