@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.
- package/DOCS.md +4 -3
- package/README.md +4 -1
- package/README.md.txt +4 -1
- package/dist/mailcraft-editor.bundle.js +57 -54
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/examples/vanilla.html +19 -18
- package/package.json +1 -1
- package/src/core/brand.js +81 -0
- package/src/core/editor-core.js +41 -8
- package/src/core/export.js +8 -1
- package/src/core/i18n/ar.js +2 -2
- package/src/core/i18n/bg.js +2 -2
- package/src/core/i18n/bn.js +2 -2
- package/src/core/i18n/ca.js +2 -2
- package/src/core/i18n/cs.js +2 -2
- package/src/core/i18n/da.js +2 -2
- package/src/core/i18n/de-CH.js +2 -2
- package/src/core/i18n/de.js +2 -2
- package/src/core/i18n/dz.js +2 -2
- package/src/core/i18n/el.js +2 -2
- package/src/core/i18n/en.js +2 -2
- package/src/core/i18n/es.js +2 -2
- package/src/core/i18n/et.js +2 -2
- package/src/core/i18n/fi.js +2 -2
- package/src/core/i18n/fr.js +2 -2
- package/src/core/i18n/hr.js +2 -2
- package/src/core/i18n/hu.js +2 -2
- package/src/core/i18n/it.js +2 -2
- package/src/core/i18n/lt.js +2 -2
- package/src/core/i18n/lv.js +2 -2
- package/src/core/i18n/nb.js +2 -2
- package/src/core/i18n/nl.js +2 -2
- package/src/core/i18n/pl.js +2 -2
- package/src/core/i18n/pt.js +2 -2
- package/src/core/i18n/ro.js +2 -2
- package/src/core/i18n/ru.js +2 -2
- package/src/core/i18n/sk.js +2 -2
- package/src/core/i18n/sl.js +2 -2
- package/src/core/i18n/sv.js +2 -2
- package/src/core/i18n/tr.js +2 -2
- package/src/core/i18n/uk.js +2 -2
- package/src/core/icons.js +1 -0
- package/src/core/import-html.js +63 -9
- package/src/core/sanitize.js +34 -0
- package/src/core/theme.js +1 -1
- package/src/mailcraft-editor.js +20 -10
- package/src/render/block-body.js +12 -6
- package/src/render/canvas.js +7 -1
- package/src/render/fields.js +9 -1
- package/src/render/screenshot.js +13 -2
- package/src/render/style.js +14 -11
package/src/render/canvas.js
CHANGED
|
@@ -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
|
-
|
|
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) => {
|
package/src/render/fields.js
CHANGED
|
@@ -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', () => {
|
|
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}` });
|
package/src/render/screenshot.js
CHANGED
|
@@ -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 *
|
|
129
|
-
canvas.height = Math.max(1, Math.round(h *
|
|
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) => {
|
package/src/render/style.js
CHANGED
|
@@ -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
|
-
/*
|
|
155
|
-
.
|
|
156
|
-
|
|
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
|
|
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;
|