@seliseblocks/mailcraft 0.2.8 → 0.2.10
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/CHANGELOG.md +135 -0
- package/DOCS.md +32 -32
- package/README.md +4 -1
- package/README.md.txt +4 -1
- package/dist/mailcraft-editor.bundle.js +74 -57
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/package.json +3 -2
- package/src/core/blocks.js +30 -11
- package/src/core/css-cascade.js +4 -1
- package/src/core/editor-core.js +80 -16
- package/src/core/export.js +83 -10
- package/src/core/icons.js +0 -1
- package/src/core/import-html.js +694 -55
- package/src/core/sanitize.js +72 -1
- package/src/mailcraft-editor.js +44 -4
- package/src/render/block-body.js +17 -5
- package/src/render/canvas.js +3 -0
- package/src/render/fields.js +28 -2
- package/src/render/rte.js +6 -0
- package/src/render/style.js +7 -0
- package/types/index.d.ts +10 -3
package/src/core/sanitize.js
CHANGED
|
@@ -73,7 +73,8 @@ const IMPORT_STYLES = [
|
|
|
73
73
|
'width', 'max-width', 'height',
|
|
74
74
|
];
|
|
75
75
|
|
|
76
|
-
export
|
|
76
|
+
/** `dropProps` (optional): style properties to leave out of the kept whitelist -- the importer passes `['font-family']` when a run's family has been claimed as the block-level font, so the same declaration doesn't ship twice (and the renderer's overrideRichFont then has nothing to strip at render time, which keeps export -> import -> export byte-stable). */
|
|
77
|
+
export const cleanImportHtml = (html, dropProps, keepProps) => {
|
|
77
78
|
const doc = new DOMParser().parseFromString(String(html || ''), 'text/html');
|
|
78
79
|
doc.querySelectorAll('style,script,meta,link,title,head').forEach((n) => n.remove());
|
|
79
80
|
const walk = (node) => {
|
|
@@ -87,6 +88,15 @@ export const cleanImportHtml = (html) => {
|
|
|
87
88
|
}
|
|
88
89
|
const kept = [];
|
|
89
90
|
IMPORT_STYLES.forEach((p) => {
|
|
91
|
+
if (dropProps && dropProps.indexOf(p) > -1) return;
|
|
92
|
+
const v = el.style.getPropertyValue(p);
|
|
93
|
+
if (v) kept.push(p + ':' + v);
|
|
94
|
+
});
|
|
95
|
+
// `keepProps`: extra properties a specific caller vouches for beyond the
|
|
96
|
+
// shared whitelist -- the section-box reader passes the display/margin
|
|
97
|
+
// pair its own template writes (`<strong style="display:block;...">`),
|
|
98
|
+
// which the general list rightly refuses from arbitrary paste.
|
|
99
|
+
(keepProps || []).forEach((p) => {
|
|
90
100
|
const v = el.style.getPropertyValue(p);
|
|
91
101
|
if (v) kept.push(p + ':' + v);
|
|
92
102
|
});
|
|
@@ -102,6 +112,67 @@ export const cleanImportHtml = (html) => {
|
|
|
102
112
|
return doc.body.innerHTML.replace(/<!--[\s\S]*?-->/g, '').replace(/\s{2,}/g, ' ').trim();
|
|
103
113
|
};
|
|
104
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Mutation-time repairs for rich block HTML. Imported content keeps its
|
|
117
|
+
* per-element inline typography on purpose (`cleanImportHtml` above), but a
|
|
118
|
+
* descendant's own `font-size`/`color`/`line-height` always outranks the
|
|
119
|
+
* inherited value from the block wrapper -- so the inspector's Text size,
|
|
120
|
+
* Text color, Line spacing and Text weight controls were dead on any block
|
|
121
|
+
* whose paragraphs carried their own styles. The render-time strip that fixed
|
|
122
|
+
* the Font control (`overrideRichFont`, render/block-body.js) can't be reused
|
|
123
|
+
* here: `fontFamily` defaults to empty so "unset" means "leave the import
|
|
124
|
+
* alone", while `size`/`color`/`lh`/`weight` always hold a value -- stripping
|
|
125
|
+
* at render time would flatten every imported design on first paint. These
|
|
126
|
+
* run once, at the moment the user moves the control (core `setProp`), so an
|
|
127
|
+
* untouched document renders byte-identical and the rewrite lands in the same
|
|
128
|
+
* undo step as the prop change.
|
|
129
|
+
*
|
|
130
|
+
* Both return the input string untouched (same reference, no reparse churn)
|
|
131
|
+
* when there is nothing to rewrite.
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/** Drops trailing float noise without inventing integers: 60.75 stays, 78.00 becomes 78. */
|
|
135
|
+
const trimNum = (n) => String(Number(n.toFixed(2)));
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Multiplies every absolute inline font-size by `ratio`, so the block's Text
|
|
139
|
+
* size acts as a master scale and a 15/26/15px imported hierarchy survives a
|
|
140
|
+
* base change instead of being flattened. Only px/pt scale -- em/% are
|
|
141
|
+
* relative and already follow the wrapper. A px line-height sitting beside a
|
|
142
|
+
* scaled font-size scales with it, or the imported `line-height:22px` would
|
|
143
|
+
* strangle 45px glyphs.
|
|
144
|
+
*/
|
|
145
|
+
export const scaleInlineSizes = (html, ratio) => {
|
|
146
|
+
const src = String(html == null ? '' : html);
|
|
147
|
+
if (!src || !Number.isFinite(ratio) || ratio <= 0 || ratio === 1 || !/font-size/i.test(src)) return src;
|
|
148
|
+
const doc = new DOMParser().parseFromString(src, 'text/html');
|
|
149
|
+
let changed = false;
|
|
150
|
+
doc.body.querySelectorAll('[style]').forEach((node) => {
|
|
151
|
+
const size = /^([\d.]+)(px|pt)$/.exec(node.style.fontSize || '');
|
|
152
|
+
if (!size) return;
|
|
153
|
+
node.style.fontSize = trimNum(parseFloat(size[1]) * ratio) + size[2];
|
|
154
|
+
const lh = /^([\d.]+)px$/.exec(node.style.lineHeight || '');
|
|
155
|
+
if (lh) node.style.lineHeight = trimNum(parseFloat(lh[1]) * ratio) + 'px';
|
|
156
|
+
changed = true;
|
|
157
|
+
});
|
|
158
|
+
return changed ? doc.body.innerHTML : src;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** Removes one CSS property from every descendant's inline style -- the block-level control now owns it. The property name is exact (`color` never touches `background-color`). */
|
|
162
|
+
export const stripInlineStyle = (html, prop) => {
|
|
163
|
+
const src = String(html == null ? '' : html);
|
|
164
|
+
if (!src || src.indexOf(prop) === -1) return src;
|
|
165
|
+
const doc = new DOMParser().parseFromString(src, 'text/html');
|
|
166
|
+
let changed = false;
|
|
167
|
+
doc.body.querySelectorAll('[style]').forEach((node) => {
|
|
168
|
+
if (!node.style.getPropertyValue(prop)) return;
|
|
169
|
+
node.style.removeProperty(prop);
|
|
170
|
+
if (!node.getAttribute('style')) node.removeAttribute('style');
|
|
171
|
+
changed = true;
|
|
172
|
+
});
|
|
173
|
+
return changed ? doc.body.innerHTML : src;
|
|
174
|
+
};
|
|
175
|
+
|
|
105
176
|
/**
|
|
106
177
|
* Makes an image URL safe to drop inside `url("...")`.
|
|
107
178
|
*
|
package/src/mailcraft-editor.js
CHANGED
|
@@ -463,8 +463,9 @@ export class MailCraftEditor extends ElementBase {
|
|
|
463
463
|
*/
|
|
464
464
|
loadTemplate(tpl) { this.core.loadTemplate(tpl); }
|
|
465
465
|
|
|
466
|
-
|
|
467
|
-
|
|
466
|
+
/** `options.markers: false` omits the fidelity-marker attributes (`data-mc*`, the inert `mc-keep` class) for hosts that want pristine HTML -- at the price of a lossy reload for the few blocks whose rendered shape cannot be read back (countdown, video, section box, code, raw CSS, flex/grid rows). */
|
|
467
|
+
exportHtml(options) {
|
|
468
|
+
const html = this.core.buildHtml(options);
|
|
468
469
|
this.dispatchEvent(new CustomEvent('export', { detail: html }));
|
|
469
470
|
return html;
|
|
470
471
|
}
|
|
@@ -1039,6 +1040,22 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1039
1040
|
}
|
|
1040
1041
|
|
|
1041
1042
|
renderTabBody() {
|
|
1043
|
+
// The browser's native colour dialog is bound to the identity of its
|
|
1044
|
+
// <input type="color"> node: Chromium closes the dialog the moment that
|
|
1045
|
+
// node leaves the document -- even if the same node is spliced back into
|
|
1046
|
+
// the rebuilt tree within the same task (unlike the range slider's mouse
|
|
1047
|
+
// capture, which focus-preserve.js does save that way). The picker lives
|
|
1048
|
+
// in this panel, and its `input` events commit (debounced) to the doc, so
|
|
1049
|
+
// every colour the user tried used to rebuild the panel ~120ms later and
|
|
1050
|
+
// slam the dialog shut. While the picker holds focus -- the only time its
|
|
1051
|
+
// dialog can be open -- the panel therefore skips its rebuild: the canvas
|
|
1052
|
+
// has already re-rendered above (that is the live preview), and the pill
|
|
1053
|
+
// previews itself (fields.js mutates its swatch and hex text directly on
|
|
1054
|
+
// every `input`). Nothing else in the panel changes on a colour commit,
|
|
1055
|
+
// so nothing is stale; the moment focus moves anywhere else, the next
|
|
1056
|
+
// render rebuilds the panel as usual.
|
|
1057
|
+
const active = this.shadowRoot.activeElement;
|
|
1058
|
+
if (active && active.tagName === 'INPUT' && active.type === 'color' && this.tabBody.contains(active)) return;
|
|
1042
1059
|
this.tabBody.innerHTML = '';
|
|
1043
1060
|
const tab = this.core.state.tab;
|
|
1044
1061
|
if (tab === 'design') this.tabBody.appendChild(this.renderDesignTab());
|
|
@@ -1579,7 +1596,30 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1579
1596
|
this.codeStatusEl = elS('div', 'font-family: var(--ed-font); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ed-muted);', { class: 'mc-code-kicker' });
|
|
1580
1597
|
headText.append(this.codeStatusEl, elS('div', 'font-family: var(--ed-font); font-weight: 600; font-size: 15px; line-height: 1.2;', { text: t('modal.rawHtmlTitle'), 'data-i18n': 'modal.rawHtmlTitle', class: 'mc-code-heading' }));
|
|
1581
1598
|
this.codeWidthSeg = elS('div', '', { class: 'mc-segment' });
|
|
1582
|
-
|
|
1599
|
+
// Ghost-button chrome shared by the header's Copy and Reload.
|
|
1600
|
+
const GHOST_BTN = 'border: 1px solid var(--ed-line); background: transparent; color: var(--ed-text); cursor: pointer; height: 30px; padding: 0 11px; display: flex; align-items: center; gap: 6px; font-family: var(--ed-font); font-size: 11px; font-weight: 600; transition: border-color 0.16s, background 0.16s;';
|
|
1601
|
+
const copyBtn = elS('button', GHOST_BTN, { type: 'button', title: t('toast.htmlCopied'), 'data-i18n-title': 'toast.htmlCopied', class: 'mc-code-copy' });
|
|
1602
|
+
copyBtn.appendChild(icon('copy', 14));
|
|
1603
|
+
// Reuses keys every locale already carries (story.copy / export.copied)
|
|
1604
|
+
// rather than minting new ones across 32 translations.
|
|
1605
|
+
const copyLabel = elS('span', '', { text: t('story.copy'), 'data-i18n': 'story.copy' });
|
|
1606
|
+
copyBtn.appendChild(copyLabel);
|
|
1607
|
+
copyBtn.addEventListener('mouseenter', () => { copyBtn.style.borderColor = 'var(--ed-accent)'; copyBtn.style.background = 'var(--ed-soft)'; });
|
|
1608
|
+
copyBtn.addEventListener('mouseleave', () => { copyBtn.style.borderColor = 'var(--ed-line)'; copyBtn.style.background = 'transparent'; });
|
|
1609
|
+
copyBtn.addEventListener('click', () => {
|
|
1610
|
+
this.core.copyCode();
|
|
1611
|
+
// Momentary "Copied": swap the i18n key itself, not just the text --
|
|
1612
|
+
// refreshStrings relabels every [data-i18n] node on each render and
|
|
1613
|
+
// would otherwise revert the feedback mid-flash.
|
|
1614
|
+
copyLabel.setAttribute('data-i18n', 'export.copied');
|
|
1615
|
+
copyLabel.textContent = this.core.t('export.copied');
|
|
1616
|
+
clearTimeout(this._codeCopyTimer);
|
|
1617
|
+
this._codeCopyTimer = setTimeout(() => {
|
|
1618
|
+
copyLabel.setAttribute('data-i18n', 'story.copy');
|
|
1619
|
+
copyLabel.textContent = this.core.t('story.copy');
|
|
1620
|
+
}, 1600);
|
|
1621
|
+
});
|
|
1622
|
+
const reloadBtn = elS('button', GHOST_BTN, { type: 'button', title: t('action.reloadHint'), 'data-i18n-title': 'action.reloadHint' });
|
|
1583
1623
|
reloadBtn.appendChild(icon('refresh', 14));
|
|
1584
1624
|
reloadBtn.appendChild(elS('span', '', { text: t('action.reload'), 'data-i18n': 'action.reload' }));
|
|
1585
1625
|
reloadBtn.addEventListener('mouseenter', () => { reloadBtn.style.borderColor = 'var(--ed-accent)'; reloadBtn.style.background = 'var(--ed-soft)'; });
|
|
@@ -1601,7 +1641,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1601
1641
|
closeBtn.addEventListener('mouseleave', () => { closeBtn.style.borderColor = 'var(--ed-line)'; });
|
|
1602
1642
|
closeBtn.addEventListener('click', () => this.core.setState({ libraryOpen: false, exportOpen: false, aiOpen: false, codeOpen: false, assetTarget: null, libHot: false }));
|
|
1603
1643
|
tip(closeBtn, t('action.closeWithoutApplyingHint'), 'down', 'end');
|
|
1604
|
-
head.append(headIcon, headText, this.codeWidthSeg, reloadBtn, applyBtn, closeBtn);
|
|
1644
|
+
head.append(headIcon, headText, this.codeWidthSeg, copyBtn, reloadBtn, applyBtn, closeBtn);
|
|
1605
1645
|
overlay.appendChild(head);
|
|
1606
1646
|
|
|
1607
1647
|
const cols = elS('div', 'display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 12px; min-height: 0; background: var(--ed-work);', { class: 'mc-code-split' });
|
package/src/render/block-body.js
CHANGED
|
@@ -71,6 +71,20 @@ function overrideRichFont(root, fontFamily) {
|
|
|
71
71
|
root.querySelectorAll('[style]').forEach((node) => node.style.removeProperty('font-family'));
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** An anchor merely restating the document link color drops it, so the sheet rule -- and a later Link color edit -- owns it again. The exporter stamps `theme.link` on every colorless anchor (mail clients need the value inline), and an import keeps that stamp in the block's html; left in place it froze the link color at whatever the theme said on the day of the save. A genuinely different inline color is the user's and stays. Compared through the same rgb-vs-hex fold the importer uses, since CSSOM serializes one and pickers speak the other. */
|
|
75
|
+
function overrideLinkColor(root, link) {
|
|
76
|
+
if (!link) return;
|
|
77
|
+
const key = (v) => {
|
|
78
|
+
const m = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/.exec(String(v || '').trim());
|
|
79
|
+
const hex = m ? '#' + [m[1], m[2], m[3]].map((n) => Number(n).toString(16).padStart(2, '0')).join('') : String(v || '');
|
|
80
|
+
return hex.toLowerCase();
|
|
81
|
+
};
|
|
82
|
+
const want = key(link);
|
|
83
|
+
root.querySelectorAll('a[style]').forEach((a) => {
|
|
84
|
+
if (a.style.color && key(a.style.color) === want) a.style.removeProperty('color');
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
/**
|
|
75
89
|
* Ported from the original `blockBody(b, theme, live)`. `ctx` supplies the
|
|
76
90
|
* editing wiring that in the original lived on `this` (Component instance):
|
|
@@ -97,6 +111,7 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
97
111
|
// and other inline formatting remain intact, while the selected family
|
|
98
112
|
// now applies consistently in the canvas and exported read-back DOM.
|
|
99
113
|
overrideRichFont(content, p.fontFamily);
|
|
114
|
+
overrideLinkColor(content, t.link);
|
|
100
115
|
if (edit) wireEditable(content, b, 'html', ctx, false);
|
|
101
116
|
if (!live) return content;
|
|
102
117
|
return wrapWithRte(b, ctx, edit && ctx.editingId === b.id, content);
|
|
@@ -303,6 +318,7 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
303
318
|
list.appendChild(li);
|
|
304
319
|
});
|
|
305
320
|
overrideRichFont(list, p.fontFamily);
|
|
321
|
+
overrideLinkColor(list, t.link);
|
|
306
322
|
return list;
|
|
307
323
|
}
|
|
308
324
|
case 'table': {
|
|
@@ -351,11 +367,6 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
351
367
|
table.appendChild(tbody);
|
|
352
368
|
return table;
|
|
353
369
|
}
|
|
354
|
-
case 'embed': {
|
|
355
|
-
const wrap = el('div', { padding: p.py + 'px 0' }, attr);
|
|
356
|
-
wrap.appendChild(el('iframe', { width: '100%', height: p.height + 'px', border: '1px solid rgba(29,31,32,0.14)', background: '#fff', display: 'block' }, { src: p.src, title: p.label }));
|
|
357
|
-
return wrap;
|
|
358
|
-
}
|
|
359
370
|
case 'css': {
|
|
360
371
|
const wrap = el('div', { fontSize: '0', lineHeight: '0' }, attr);
|
|
361
372
|
wrap.appendChild(el('style', {}, { html: live ? ctx.scopeCss(p.code, '[data-mc-sheet]') : p.code }));
|
|
@@ -383,6 +394,7 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
383
394
|
boxShadow: p.shadow ? '0 10px 30px rgba(29,31,32,0.12)' : 'none',
|
|
384
395
|
fontFamily: t.font, color: t.text, fontSize: '15px', lineHeight: '1.6', ...FIT,
|
|
385
396
|
}, { ...attr, ...editableAttrs(edit), html: m(p.html), 'data-focus-key': edit ? 'block:' + b.id : undefined });
|
|
397
|
+
overrideLinkColor(box, t.link);
|
|
386
398
|
if (edit) wireEditable(box, b, 'html', ctx, false);
|
|
387
399
|
if (!live) return box;
|
|
388
400
|
return wrapWithRte(b, ctx, ctx.editingId === b.id, box);
|
package/src/render/canvas.js
CHANGED
|
@@ -196,6 +196,9 @@ export function renderDoc(core, live) {
|
|
|
196
196
|
overflow: !live && radius ? 'hidden' : '',
|
|
197
197
|
transition: 'width 0.28s cubic-bezier(0.22,0.61,0.36,1), background 0.2s, border-radius 0.2s',
|
|
198
198
|
}, { 'data-mc-sheet': '1', dir: 'ltr' });
|
|
199
|
+
// A custom property cannot ride Object.assign(style, ...) -- '--mc-link'
|
|
200
|
+
// is not a CSSOM member -- so it is set the one way that works.
|
|
201
|
+
root.style.setProperty('--mc-link', theme.link || '');
|
|
199
202
|
|
|
200
203
|
/*
|
|
201
204
|
* The page: the full-width section the email sits on, painted from the
|
package/src/render/fields.js
CHANGED
|
@@ -12,6 +12,23 @@ function el(tag, style, attrs) {
|
|
|
12
12
|
return node;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The native `<input type="color">` accepts ONLY `#rrggbb` -- handed an
|
|
17
|
+
* imported `rgba(...)`, `rgb(...)`, `#abc` or a keyword it silently stays at
|
|
18
|
+
* black, so opening the picker on such a value read as "the color was not
|
|
19
|
+
* picked up". Normalizes what can be normalized (alpha is dropped -- the
|
|
20
|
+
* picker cannot represent it; the text field beside it still holds the exact
|
|
21
|
+
* value); anything else returns '' and the picker keeps its default.
|
|
22
|
+
*/
|
|
23
|
+
function pickerHex(v) {
|
|
24
|
+
const s = String(v == null ? '' : v).trim();
|
|
25
|
+
if (/^#[0-9a-f]{6}$/i.test(s)) return s;
|
|
26
|
+
if (/^#[0-9a-f]{3}$/i.test(s)) return '#' + s.slice(1).split('').map((c) => c + c).join('');
|
|
27
|
+
const m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(s);
|
|
28
|
+
if (m) return '#' + [m[1], m[2], m[3]].map((n) => Math.min(255, Number(n)).toString(16).padStart(2, '0')).join('');
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
/**
|
|
16
33
|
* Trailing debounce for free-typing inputs. Every commit rebuilds the whole
|
|
17
34
|
* canvas and panel (full re-render, no diffing) -- fine per click, but per
|
|
@@ -570,11 +587,20 @@ export function renderField(f) {
|
|
|
570
587
|
});
|
|
571
588
|
}
|
|
572
589
|
const picker = el('input', { position: 'absolute', inset: '0', width: '100%', height: '100%', opacity: '0', cursor: 'pointer', padding: '0', border: '0' }, { type: 'color' });
|
|
573
|
-
|
|
590
|
+
const ph = pickerHex(f.swatch);
|
|
591
|
+
if (ph) picker.value = ph;
|
|
574
592
|
// The native picker fires `input` continuously while dragging the hue
|
|
575
593
|
// wheel -- committed raw, that is a full re-render per mouse move.
|
|
576
594
|
const pickCommit = typeCommit(f.onChange);
|
|
577
|
-
picker.addEventListener('input', (e) =>
|
|
595
|
+
picker.addEventListener('input', (e) => {
|
|
596
|
+
// Self-preview: while this picker's dialog is open the inspector skips
|
|
597
|
+
// its rebuilds (renderTabBody, mailcraft-editor.js) so the dialog's
|
|
598
|
+
// host node survives -- which also means no rebuild will repaint this
|
|
599
|
+
// pill. Mutate it directly so swatch and hex track the dialog live.
|
|
600
|
+
swatch.style.background = e.target.value;
|
|
601
|
+
hex.value = e.target.value;
|
|
602
|
+
pickCommit.call(e.target.value);
|
|
603
|
+
});
|
|
578
604
|
picker.addEventListener('change', () => pickCommit.flush());
|
|
579
605
|
swatch.appendChild(picker);
|
|
580
606
|
const hex = el('input', { width: '82px', boxSizing: 'border-box', background: 'transparent', border: '0', outline: 'none', color: 'var(--ed-text)', fontFamily: 'ui-monospace, monospace', fontSize: '11px', padding: '0 9px' }, { placeholder: 'inherit', class: 'mc-stepper-input', 'data-focus-key': `f${f.key}`, dir: 'ltr' });
|
package/src/render/rte.js
CHANGED
|
@@ -110,6 +110,12 @@ export function renderRte(core, b) {
|
|
|
110
110
|
btn('inlineCode', 'Inline code', () => core.exec('insertHTML', '<code style="font-family:ui-monospace,monospace;font-size:0.92em;background:var(--ed-soft);padding:1px 4px">' + (core.getSelection().toString() || 'code') + '</code>')),
|
|
111
111
|
btn('superscript', 'Superscript', () => core.exec('superscript'), st('superscript')),
|
|
112
112
|
btn('subscript', 'Subscript', () => core.exec('subscript'), st('subscript')),
|
|
113
|
+
);
|
|
114
|
+
// Only where a size is actually rendered: `box` hardcodes 15px and `html`
|
|
115
|
+
// sets no font-size at all, so on those two the ± pair moved nothing while
|
|
116
|
+
// still persisting a junk `size` prop into the saved document, with the
|
|
117
|
+
// readout counting up beside it.
|
|
118
|
+
if (b.type === 'text' || b.type === 'heading') r1.append(
|
|
113
119
|
sep(),
|
|
114
120
|
btn('minus', 'Smaller text', () => core.size(b, -1)),
|
|
115
121
|
el('span', { fontFamily: 'var(--ed-font)', fontSize: '9.5px', color: 'var(--rte-muted)', minWidth: '32px', textAlign: 'center' }, { text: (b.props.size || 16) + 'px' }),
|
package/src/render/style.js
CHANGED
|
@@ -98,6 +98,13 @@ export const STYLE = `
|
|
|
98
98
|
#mc ::selection { background: var(--ed-select); }
|
|
99
99
|
#mc a { color: var(--ed-accent); }
|
|
100
100
|
#mc a:hover { color: var(--ed-accent); opacity: 0.72; }
|
|
101
|
+
/* Content links are the document's, not the chrome's: inside the sheet the
|
|
102
|
+
accent rule above painted every anchor in the HOST's brand color, which is
|
|
103
|
+
why the theme's own Link color setting never did anything visible. The
|
|
104
|
+
variable is set per render on the sheet root (render/canvas.js); anchors
|
|
105
|
+
with their own inline color keep it, exactly as in the sent mail. */
|
|
106
|
+
#mc [data-mc-sheet] a { color: var(--mc-link, var(--ed-accent)); }
|
|
107
|
+
#mc [data-mc-sheet] a:hover { color: var(--mc-link, var(--ed-accent)); opacity: 1; }
|
|
101
108
|
@keyframes mcIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
|
102
109
|
@keyframes mcFade { from { opacity: 0; } to { opacity: 1; } }
|
|
103
110
|
@keyframes mcPulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } }
|
package/types/index.d.ts
CHANGED
|
@@ -272,8 +272,15 @@ export class MailCraftEditor extends HTMLElement {
|
|
|
272
272
|
/** Merged over `storageProvider.limits` per key, this side winning. */
|
|
273
273
|
storageLimits: StorageLimits | null;
|
|
274
274
|
|
|
275
|
-
/**
|
|
276
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Send-ready email HTML — valid input to the importer, so saving the export
|
|
277
|
+
* is saving the work. `{ markers: false }` omits the fidelity-marker
|
|
278
|
+
* attributes (`data-mc*`, the inert `mc-keep` class) for hosts that want
|
|
279
|
+
* pristine HTML, at the price of a lossy reload for the few blocks whose
|
|
280
|
+
* rendered shape cannot be read back (countdown, video, section box, code,
|
|
281
|
+
* raw CSS, flex/grid rows).
|
|
282
|
+
*/
|
|
283
|
+
exportHtml(options?: { markers?: boolean }): string;
|
|
277
284
|
|
|
278
285
|
/** Parses email HTML back onto the canvas. Returns the number of rows produced. */
|
|
279
286
|
importHtml(html: string): number;
|
|
@@ -365,7 +372,7 @@ export interface EditorHandle {
|
|
|
365
372
|
element: MailCraftEditor;
|
|
366
373
|
/** Removes the editor and detaches the listeners this call attached. */
|
|
367
374
|
destroy(): void;
|
|
368
|
-
exportHtml(): string;
|
|
375
|
+
exportHtml(options?: { markers?: boolean }): string;
|
|
369
376
|
importHtml(html: string): number;
|
|
370
377
|
loadTemplate(tpl: Template): void;
|
|
371
378
|
undo(): void;
|