@seliseblocks/mailcraft 0.2.0 → 0.2.2
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 +10 -8
- package/README.md +4 -3
- package/README.md.txt +4 -3
- package/dist/mailcraft-editor.bundle.js +90 -51
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/examples/templates/activate-your-account.html +1 -1
- package/examples/templates/back-in-stock.html +1 -1
- package/examples/templates/cart-left-behind.html +1 -1
- package/examples/templates/community-giveaway.html +1 -1
- package/examples/templates/frontend-futures-invite.html +1 -1
- package/examples/templates/give-25-get-25.html +1 -1
- package/examples/templates/invoice-paid.html +1 -1
- package/examples/templates/meet-nova-launch.html +1 -1
- package/examples/templates/mega-weekend-sale.html +1 -1
- package/examples/templates/order-confirmed.html +1 -1
- package/examples/templates/rate-your-headphones.html +1 -1
- package/examples/templates/reset-your-password.html +1 -1
- package/examples/templates/thankyou-promo-code.html +1 -1
- package/examples/templates/the-sunday-brief.html +1 -1
- package/examples/templates/welcome-to-your-workspace.html +1 -1
- package/examples/templates/your-order-shipped.html +1 -1
- package/examples/templates/your-password-was-changed.html +1 -1
- package/examples/templates/your-signin-code.html +1 -1
- package/examples/vanilla.html +27 -25
- package/package.json +1 -1
- package/src/core/binder.js +21 -1
- package/src/core/editor-core.js +12 -2
- package/src/core/export.js +19 -3
- package/src/core/i18n/ar.js +0 -1
- package/src/core/i18n/bg.js +0 -1
- package/src/core/i18n/bn.js +0 -1
- package/src/core/i18n/ca.js +0 -1
- package/src/core/i18n/cs.js +0 -1
- package/src/core/i18n/da.js +0 -1
- package/src/core/i18n/de-CH.js +0 -1
- package/src/core/i18n/de.js +0 -1
- package/src/core/i18n/dz.js +0 -1
- package/src/core/i18n/el.js +0 -1
- package/src/core/i18n/en.js +0 -1
- package/src/core/i18n/es.js +0 -1
- package/src/core/i18n/et.js +0 -1
- package/src/core/i18n/fi.js +0 -1
- package/src/core/i18n/fr.js +0 -1
- package/src/core/i18n/hr.js +0 -1
- package/src/core/i18n/hu.js +0 -1
- package/src/core/i18n/it.js +0 -1
- package/src/core/i18n/loaders.js +78 -0
- package/src/core/i18n/lt.js +0 -1
- package/src/core/i18n/lv.js +0 -1
- package/src/core/i18n/nb.js +0 -1
- package/src/core/i18n/nl.js +0 -1
- package/src/core/i18n/pl.js +0 -1
- package/src/core/i18n/pt.js +0 -1
- package/src/core/i18n/ro.js +0 -1
- package/src/core/i18n/ru.js +0 -1
- package/src/core/i18n/sk.js +0 -1
- package/src/core/i18n/sl.js +0 -1
- package/src/core/i18n/sv.js +0 -1
- package/src/core/i18n/tables.js +5 -4
- package/src/core/i18n/tr.js +0 -1
- package/src/core/i18n/uk.js +0 -1
- package/src/core/import-html.js +31 -0
- package/src/core/storage-limits.js +16 -11
- package/src/core/storage.js +1 -1
- package/src/core/theme.js +15 -1
- package/src/index.js +1 -0
- package/src/mailcraft-editor.js +79 -23
- package/src/render/canvas.js +43 -6
- package/src/render/fields.js +17 -0
- package/src/render/screenshot.js +7 -1
- package/src/render/style.js +503 -460
- package/types/index.d.ts +14 -2
package/src/mailcraft-editor.js
CHANGED
|
@@ -17,7 +17,7 @@ import { createStoryViewer } from './render/story.js';
|
|
|
17
17
|
import { STYLE } from './render/style.js';
|
|
18
18
|
import { createTranslator, isRtl } from './core/i18n/index.js';
|
|
19
19
|
import { ACCENT_VARS, accentTokens, parseColor } from './core/accent.js';
|
|
20
|
-
import {
|
|
20
|
+
import { localeTable, loadLocale } from './core/i18n/loaders.js';
|
|
21
21
|
|
|
22
22
|
/** `elS` mirrors the template's literal `style="..."` attribute strings verbatim -- copied, not re-derived, to keep the port pixel-exact. */
|
|
23
23
|
function elS(tag, styleStr, attrs = {}, ...children) {
|
|
@@ -298,13 +298,26 @@ export class MailCraftEditor extends ElementBase {
|
|
|
298
298
|
|
|
299
299
|
/**
|
|
300
300
|
* Resolves the `locale` attribute to its shipped message table
|
|
301
|
-
* (core/i18n/
|
|
301
|
+
* (core/i18n/loaders.js) and overlays any host-set `.messages` on top --
|
|
302
302
|
* attribute picks the language, `.messages` stays the documented way to
|
|
303
303
|
* override individual strings. With no locale attribute this degrades to
|
|
304
304
|
* the old behavior: `.messages` alone over built-in English.
|
|
305
|
+
*
|
|
306
|
+
* Tables load lazily (a literal dynamic import per tag, so app bundlers
|
|
307
|
+
* code-split them instead of shipping all 31). A not-yet-loaded locale
|
|
308
|
+
* renders English for the tick the chunk takes to arrive, then this method
|
|
309
|
+
* re-runs; the load is a no-op re-render if the attribute changed again
|
|
310
|
+
* meanwhile -- the guard re-reads the live attribute, never the old tag.
|
|
305
311
|
*/
|
|
306
312
|
refreshTranslator() {
|
|
307
|
-
const
|
|
313
|
+
const tag = this.getAttribute('locale') || '';
|
|
314
|
+
let table = tag ? localeTable(tag) : null;
|
|
315
|
+
if (table === undefined) {
|
|
316
|
+
table = null;
|
|
317
|
+
loadLocale(tag).then(() => {
|
|
318
|
+
if ((this.getAttribute('locale') || '') === tag) this.refreshTranslator();
|
|
319
|
+
});
|
|
320
|
+
}
|
|
308
321
|
const overrides = this.core.messages;
|
|
309
322
|
this.core.t = createTranslator(table ? Object.assign({}, table, overrides || {}) : overrides);
|
|
310
323
|
this.story?.retranslate();
|
|
@@ -429,10 +442,10 @@ export class MailCraftEditor extends ElementBase {
|
|
|
429
442
|
|
|
430
443
|
/**
|
|
431
444
|
* What may be uploaded: `{ accept, maxBytes, maxWidth, maxHeight, maxFilesPerDrop, allowSvg }`.
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
* See `core/storage-limits.js`.
|
|
445
|
+
* Only `maxBytes` is required alongside a provider -- sizes depend on the
|
|
446
|
+
* sending platform and the host's own rules, none of which this package can
|
|
447
|
+
* know. `accept` is optional and defaults to every image type the validator
|
|
448
|
+
* can recognize; list MIME types to narrow it. See `core/storage-limits.js`.
|
|
436
449
|
*/
|
|
437
450
|
get storageLimits() { return this.core.storageLimits; }
|
|
438
451
|
set storageLimits(limits) { this.core.setStorageLimits(limits); }
|
|
@@ -523,7 +536,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
523
536
|
|
|
524
537
|
this.footerBar = this.footerText = this.footerLink = null;
|
|
525
538
|
|
|
526
|
-
this.body = elS('div', 'display: grid; grid-template-columns: minmax(0, 1fr) 340px; grid-template-rows: minmax(0, 1fr) auto; min-height: 0;', { class: 'mc-layout' });
|
|
539
|
+
this.body = elS('div', 'display: grid; grid-template-columns: minmax(0, 1fr) 340px; grid-template-rows: minmax(0, 1fr) auto; min-height: 0; background: var(--ed-work);', { class: 'mc-layout' });
|
|
527
540
|
this.frame.appendChild(this.body);
|
|
528
541
|
this.body.appendChild(this.buildMain());
|
|
529
542
|
this.body.appendChild(this.buildAside());
|
|
@@ -688,17 +701,19 @@ export class MailCraftEditor extends ElementBase {
|
|
|
688
701
|
this.toastEl.textContent = s.toast || '';
|
|
689
702
|
}
|
|
690
703
|
|
|
691
|
-
/** Repaint just the countdown digits on the canvas --
|
|
692
|
-
*
|
|
704
|
+
/** Repaint just the countdown digits on the canvas -- and in the preview,
|
|
705
|
+
* whose sheet is now memoized rather than rebuilt every render -- driven by
|
|
706
|
+
* the core's 1s tick (core.onTick), which used to re-render the whole editor. */
|
|
693
707
|
refreshCountdowns() {
|
|
694
708
|
const now = this.core.state.now;
|
|
695
|
-
this.
|
|
709
|
+
const slots = this.core.state.previewOpen && this.previewSlot ? [this.canvasSlot, this.previewSlot] : [this.canvasSlot];
|
|
710
|
+
slots.forEach((slot) => slot.querySelectorAll('[data-mc-countdown]').forEach((wrap) => {
|
|
696
711
|
const ms = Math.max(0, new Date(wrap.getAttribute('data-mc-countdown')).getTime() - now);
|
|
697
712
|
const vals = { days: Math.floor(ms / 86400000), hrs: Math.floor(ms / 3600000) % 24, min: Math.floor(ms / 60000) % 60, sec: Math.floor(ms / 1000) % 60 };
|
|
698
713
|
wrap.querySelectorAll('[data-mc-count]').forEach((n) => {
|
|
699
714
|
n.textContent = String(vals[n.getAttribute('data-mc-count')] ?? 0).padStart(2, '0');
|
|
700
715
|
});
|
|
701
|
-
});
|
|
716
|
+
}));
|
|
702
717
|
}
|
|
703
718
|
|
|
704
719
|
/** The header's "Saved HH:MM" label -- autosave updates it through
|
|
@@ -744,7 +759,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
744
759
|
* would strand whichever node the previous render had left in place.
|
|
745
760
|
*/
|
|
746
761
|
buildFooter() {
|
|
747
|
-
this.footerBar = elS('div', 'display: flex; grid-column: 1; grid-row: 2; justify-self: center; align-items: center; justify-content: center; gap: 4px; max-width: calc(100% - 32px); padding: 6px 14px;
|
|
762
|
+
this.footerBar = elS('div', 'display: flex; grid-column: 1; grid-row: 2; justify-self: center; align-items: center; justify-content: center; gap: 4px; max-width: calc(100% - 32px); padding: 6px 14px; background: transparent; color: var(--ed-panel-meta); font-family: var(--ed-font); font-size: var(--ed-panel-meta-size); letter-spacing: 0.06em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;', { class: 'mc-footer' });
|
|
748
763
|
this.footerText = elS('span', '', { class: 'mc-footer-text' });
|
|
749
764
|
this.footerLink = elS('a', 'color: inherit; text-decoration: none;', { class: 'mc-footer-link' });
|
|
750
765
|
this.footerBar.appendChild(this.footerText);
|
|
@@ -921,8 +936,11 @@ export class MailCraftEditor extends ElementBase {
|
|
|
921
936
|
if (this.redoBtn) this.redoBtn.disabled = !s.future.length;
|
|
922
937
|
// A host-supplied `theme` attribute owns light/dark (see applyTheme) --
|
|
923
938
|
// hide the built-in toggle rather than let it fight the host's control.
|
|
939
|
+
// setProperty(..., 'important'): a plain assignment loses to the
|
|
940
|
+
// `!important` on `.mc-icon-label` (render/style.js) that keeps every
|
|
941
|
+
// other label-icon button visible, leaving the toggle rendered anyway.
|
|
924
942
|
if (this.chromeBtn) {
|
|
925
|
-
this.chromeBtn.style.display
|
|
943
|
+
this.chromeBtn.style.setProperty('display', this.hasAttribute('theme') ? 'none' : 'flex', 'important');
|
|
926
944
|
this.chromeBtn.labelNode.textContent = s.chrome === 'light' ? t('action.chromeToDark') : t('action.chromeToLight');
|
|
927
945
|
this.chromeBtn.i18nTipKey = s.chrome === 'light' ? 'action.chromeHintToDark' : 'action.chromeHintToLight';
|
|
928
946
|
this.chromeBtn.tipNode.textContent = t(this.chromeBtn.i18nTipKey);
|
|
@@ -961,7 +979,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
961
979
|
this.renderExportModal();
|
|
962
980
|
this.renderCodeModal();
|
|
963
981
|
this.renderAiModal();
|
|
964
|
-
this.renderPreviewModal(
|
|
982
|
+
this.renderPreviewModal();
|
|
965
983
|
|
|
966
984
|
this.refreshToast();
|
|
967
985
|
}
|
|
@@ -1589,8 +1607,16 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1589
1607
|
|
|
1590
1608
|
const right = elS('div', 'display: grid; grid-template-rows: auto 1fr; min-height: 0; border: 1px solid var(--ed-line); border-radius: 12px; overflow: hidden; background: var(--ed-panel);', { class: 'mc-code-preview' });
|
|
1591
1609
|
right.appendChild(elS('div', 'padding: 7px 16px; border-bottom: 1px solid var(--ed-line); background: var(--ed-panel); font-family: var(--ed-font); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ed-muted);', { text: t('code.livePreview'), 'data-i18n': 'code.livePreview', class: 'mc-pane-label' }));
|
|
1592
|
-
|
|
1593
|
-
|
|
1610
|
+
// Flush, not a card on a mat: the document inside the frame paints its
|
|
1611
|
+
// own page background edge to edge, so padding the wrap and framing the
|
|
1612
|
+
// iframe stacked a second and third rectangle around a template that
|
|
1613
|
+
// already has one -- the pane, a mat, a bordered card, then the email's
|
|
1614
|
+
// own page, then its content column. `is-device` (renderCodeModal) puts
|
|
1615
|
+
// the mat and the frame back for the 390px phone width, where the card
|
|
1616
|
+
// *is* the point.
|
|
1617
|
+
const previewWrap = elS('div', 'overflow: auto; display: flex; justify-content: center; background: var(--ed-work);', { class: 'mc-code-preview-body' });
|
|
1618
|
+
this.codePreviewBody = previewWrap;
|
|
1619
|
+
this.codeFrame = elS('iframe', "width: 100%; height: 100%; min-height: 420px; border: 0; background: #ffffff; transition: width 0.24s cubic-bezier(0.22,0.61,0.36,1);", { title: t('code.liveHtmlPreviewTitle'), 'data-i18n-title': 'code.liveHtmlPreviewTitle', class: 'mc-code-frame' });
|
|
1594
1620
|
this.codeFrame.addEventListener('load', () => this.syncCodePreviewScrollbar());
|
|
1595
1621
|
previewWrap.appendChild(this.codeFrame);
|
|
1596
1622
|
right.appendChild(previewWrap);
|
|
@@ -1670,7 +1696,9 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1670
1696
|
tip(btn, w.label, 'down');
|
|
1671
1697
|
this.codeWidthSeg.appendChild(btn);
|
|
1672
1698
|
});
|
|
1673
|
-
|
|
1699
|
+
const phone = s.codeDevice === 'mobile';
|
|
1700
|
+
this.codeFrame.style.width = phone ? '390px' : '100%';
|
|
1701
|
+
this.codePreviewBody.classList.toggle('is-device', phone);
|
|
1674
1702
|
}
|
|
1675
1703
|
|
|
1676
1704
|
/** Repaint the syntax layer, gutter and metadata without touching the
|
|
@@ -1799,7 +1827,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1799
1827
|
buildPreviewModal() {
|
|
1800
1828
|
const t = this.core.t;
|
|
1801
1829
|
const overlay = elS('div', '', { class: 'mc-fullscreen-panel mc-preview-panel' });
|
|
1802
|
-
overlay.style.cssText = 'position: absolute; inset: 0; z-index: 60; display: grid; grid-template-rows: 58px minmax(0, 1fr); animation: mcFade 0.16s ease; display: none;';
|
|
1830
|
+
overlay.style.cssText = 'position: absolute; inset: 0; z-index: 60; background: var(--ed-work); display: grid; grid-template-rows: 58px minmax(0, 1fr); animation: mcFade 0.16s ease; display: none;';
|
|
1803
1831
|
const head = elS('div', 'display: flex; align-items: center; gap: 12px; padding: 0 20px; border-bottom: 1px solid var(--ed-line); background: var(--ed-panel);', { class: 'mc-preview-toolbar' });
|
|
1804
1832
|
const headText = elS('div', 'flex: 1; min-width: 0;');
|
|
1805
1833
|
this.previewKickerEl = 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-preview-kicker' });
|
|
@@ -1813,7 +1841,16 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1813
1841
|
closeBtn.addEventListener('click', () => this.core.setState({ previewOpen: false }));
|
|
1814
1842
|
head.append(headText, this.previewDeviceSeg, closeBtn);
|
|
1815
1843
|
overlay.appendChild(head);
|
|
1816
|
-
|
|
1844
|
+
// This body *is* the sent email's page: the theme's page background is
|
|
1845
|
+
// applied on each render (renderPreviewModal), and the stylesheet gives
|
|
1846
|
+
// it container padding painted in that background -- breathing room the
|
|
1847
|
+
// way a mail client's viewport provides it, with the sheet itself, and
|
|
1848
|
+
// everything exportHtml() renders, untouched.
|
|
1849
|
+
const body = elS('div', 'overflow-y: auto;', { class: 'mc-preview-body' });
|
|
1850
|
+
// Kept on the instance: renderPreviewModal repaints it from the document's
|
|
1851
|
+
// page background on every render, and it runs long after this builder's
|
|
1852
|
+
// local has gone out of scope.
|
|
1853
|
+
this.previewBody = body;
|
|
1817
1854
|
this.previewSlot = elS('div', '', { class: 'mc-sheet-wrap' });
|
|
1818
1855
|
this.previewSlot.style.cssText = 'display: flex; justify-content: center;';
|
|
1819
1856
|
body.appendChild(this.previewSlot);
|
|
@@ -1825,7 +1862,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1825
1862
|
const s = this.core.state;
|
|
1826
1863
|
const t = this.core.t;
|
|
1827
1864
|
this.previewModal.style.display = s.previewOpen ? 'grid' : 'none';
|
|
1828
|
-
if (!s.previewOpen) return;
|
|
1865
|
+
if (!s.previewOpen) { this._previewDoc = null; return; }
|
|
1829
1866
|
this.previewKickerEl.textContent = s.device === 'mobile' ? t('preview.kickerMobile') : t('preview.kickerDesktop', { width: s.doc.theme.width });
|
|
1830
1867
|
this.previewDeviceSeg.innerHTML = '';
|
|
1831
1868
|
[
|
|
@@ -1838,8 +1875,27 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1838
1875
|
tip(btn, d.label, 'down');
|
|
1839
1876
|
this.previewDeviceSeg.appendChild(btn);
|
|
1840
1877
|
});
|
|
1841
|
-
|
|
1842
|
-
|
|
1878
|
+
// The preview body doubles as the sent page: the theme's page background
|
|
1879
|
+
// paints it, exactly as the exported <body style="background:..."> does.
|
|
1880
|
+
// Falls back to the empty string -- and so to the stylesheet's dotted
|
|
1881
|
+
// grid -- for a transparent page, which is exactly what a client with no
|
|
1882
|
+
// page colour of its own shows behind the email.
|
|
1883
|
+
const pageBg = s.doc.theme.bg || '';
|
|
1884
|
+
this.previewBody.style.background = /^(transparent|none)$/i.test(pageBg.trim()) ? '' : pageBg;
|
|
1885
|
+
// Rebuild the sheet only when what it renders from actually changed.
|
|
1886
|
+
// renderPreviewModal runs on *every* render pass, and rebuilding the
|
|
1887
|
+
// whole email document each time made the open preview visibly rough:
|
|
1888
|
+
// every unrelated setState re-created every node (images re-decoded and
|
|
1889
|
+
// flashed, the scroll anchor jumped). The three inputs are cheap
|
|
1890
|
+
// identity checks -- commit/undo/redo always replace `doc` with a fresh
|
|
1891
|
+
// parse, refreshTranslator always replaces `t` -- so a stale hit is
|
|
1892
|
+
// impossible.
|
|
1893
|
+
if (s.doc !== this._previewDoc || s.device !== this._previewDevice || t !== this._previewT) {
|
|
1894
|
+
this._previewDoc = s.doc;
|
|
1895
|
+
this._previewDevice = s.device;
|
|
1896
|
+
this._previewT = t;
|
|
1897
|
+
this.previewSlot.replaceChildren(renderDoc(this.core, false));
|
|
1898
|
+
}
|
|
1843
1899
|
}
|
|
1844
1900
|
}
|
|
1845
1901
|
|
package/src/render/canvas.js
CHANGED
|
@@ -175,21 +175,57 @@ export function renderDoc(core, live) {
|
|
|
175
175
|
// clients render it LTR -- letting the chrome's direction cascade in here
|
|
176
176
|
// flipped bidi punctuation and default alignment on the canvas and made it
|
|
177
177
|
// disagree with the exported result.
|
|
178
|
+
const radius = Number(theme.radius) || 0;
|
|
178
179
|
const root = el('div', {
|
|
179
|
-
width: width + 'px', maxWidth: '100%', background: theme.contentBg, color: theme.text, fontFamily: theme.font,
|
|
180
|
-
|
|
180
|
+
width: width + 'px', maxWidth: '100%', background: theme.contentBg || 'transparent', color: theme.text, fontFamily: theme.font,
|
|
181
|
+
// Only when the document actually asks for a shape: an unconditional
|
|
182
|
+
// `0px` would override the editor chrome's own soft corner on the sheet
|
|
183
|
+
// (style.js) and square off every template that never touched the field.
|
|
184
|
+
borderRadius: radius ? radius + 'px' : '',
|
|
185
|
+
// Clipping the rows to that corner is right for the sent email (export
|
|
186
|
+
// emits `overflow:hidden` alongside the radius) and for the static
|
|
187
|
+
// preview, but not for the editable canvas: the sheet is also what the
|
|
188
|
+
// row grip straddles and what the floating RTE toolbar overhangs
|
|
189
|
+
// (top:-82px on the first block), and hiding the overflow cuts both off.
|
|
190
|
+
overflow: !live && radius ? 'hidden' : '',
|
|
191
|
+
transition: 'width 0.28s cubic-bezier(0.22,0.61,0.36,1), background 0.2s, border-radius 0.2s',
|
|
181
192
|
}, { 'data-mc-sheet': '1', dir: 'ltr' });
|
|
182
193
|
|
|
194
|
+
/*
|
|
195
|
+
* The page: the full-width section the email sits on, painted from the
|
|
196
|
+
* document's own `bg`/`padY`/`padX` rather than left to the workspace
|
|
197
|
+
* chrome. It is the band a mail client shows around the content column --
|
|
198
|
+
* the one part of the template that used to exist only in the exported
|
|
199
|
+
* HTML, so changing its colour did nothing visible in the editor and its
|
|
200
|
+
* size could not be changed at all. With the padding at its 0 default the
|
|
201
|
+
* page hugs the sheet exactly and nothing about the canvas changes.
|
|
202
|
+
*/
|
|
203
|
+
const padY = Number(theme.padY) || 0;
|
|
204
|
+
const padX = Number(theme.padX) || 0;
|
|
205
|
+
const page = el('div', {
|
|
206
|
+
background: theme.bg || 'transparent',
|
|
207
|
+
padding: padY + 'px ' + padX + 'px',
|
|
208
|
+
boxSizing: 'border-box', maxWidth: '100%', display: 'flex', justifyContent: 'center',
|
|
209
|
+
transition: 'background 0.2s, padding 0.22s cubic-bezier(0.22,0.61,0.36,1)',
|
|
210
|
+
// `is-padded` is what moves the editor's frame outward (style.js): with
|
|
211
|
+
// no padding the page hugs the sheet exactly, so the sheet keeps the
|
|
212
|
+
// frame and an untouched canvas looks exactly as it always did.
|
|
213
|
+
}, { 'data-mc-page': '1', class: 'mc-page' + (padY || padX ? ' is-padded' : ''), dir: 'ltr' });
|
|
214
|
+
|
|
183
215
|
const rowLines = [];
|
|
184
216
|
if (live) {
|
|
185
|
-
|
|
217
|
+
// On the page, not the sheet: a drag held over the page padding is still
|
|
218
|
+
// aimed at the template, and these fire for the sheet's own events too
|
|
219
|
+
// (they bubble). Column drops stop propagation before they get here, as
|
|
220
|
+
// they did when these listeners sat on the sheet.
|
|
221
|
+
page.addEventListener('dragover', (e) => {
|
|
186
222
|
const dr = core.drag; if (!dr) return;
|
|
187
223
|
e.preventDefault();
|
|
188
224
|
const index = core.indexFromPoint(root, e.clientY);
|
|
189
225
|
showLine(rowDropTracker, rowLines[index]);
|
|
190
226
|
});
|
|
191
|
-
|
|
192
|
-
|
|
227
|
+
page.addEventListener('dragleave', (e) => { if (e.target === root || e.target === page) hideActive(rowDropTracker); });
|
|
228
|
+
page.addEventListener('drop', (e) => { hideActive(rowDropTracker); core.canvasDrop(e); });
|
|
193
229
|
}
|
|
194
230
|
|
|
195
231
|
d.rows.forEach((r, ri) => {
|
|
@@ -309,5 +345,6 @@ export function renderDoc(core, live) {
|
|
|
309
345
|
root.appendChild(el('div', { border: '1px dashed var(--ed-accent-sheet-line)', padding: '90px 20px', textAlign: 'center', fontFamily: 'ui-monospace,monospace', fontSize: '10.5px', letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--ed-faint)' }, { text: 'drag a row or block here' }));
|
|
310
346
|
}
|
|
311
347
|
|
|
312
|
-
|
|
348
|
+
page.appendChild(root);
|
|
349
|
+
return page;
|
|
313
350
|
}
|
package/src/render/fields.js
CHANGED
|
@@ -545,6 +545,17 @@ export function renderField(f) {
|
|
|
545
545
|
row.appendChild(fieldLabel(f.label, true));
|
|
546
546
|
const pill = el('div', { display: 'flex', alignItems: 'center', flex: 'none', height: '32px', border: '1px solid var(--ed-line)', borderRadius: '8px', background: 'var(--ed-panel)', overflow: 'hidden', transition: 'border-color 0.16s, box-shadow 0.16s' }, { class: 'mc-field-control mc-color-control' });
|
|
547
547
|
const swatch = el('label', { position: 'relative', width: '29px', alignSelf: 'stretch', flex: 'none', borderRight: '1px solid var(--ed-line)', background: f.swatch, cursor: 'pointer' }, { title: f.label });
|
|
548
|
+
// A transparent value has no colour to show, so the swatch shows the
|
|
549
|
+
// conventional checkerboard instead -- otherwise "no fill" and "white"
|
|
550
|
+
// are the same white square.
|
|
551
|
+
if (f.transparent) {
|
|
552
|
+
Object.assign(swatch.style, {
|
|
553
|
+
backgroundColor: '#ffffff',
|
|
554
|
+
backgroundImage: 'linear-gradient(45deg,#c3c9d2 25%,transparent 25%,transparent 75%,#c3c9d2 75%),linear-gradient(45deg,#c3c9d2 25%,transparent 25%,transparent 75%,#c3c9d2 75%)',
|
|
555
|
+
backgroundSize: '8px 8px',
|
|
556
|
+
backgroundPosition: '0 0, 4px 4px',
|
|
557
|
+
});
|
|
558
|
+
}
|
|
548
559
|
const picker = el('input', { position: 'absolute', inset: '0', width: '100%', height: '100%', opacity: '0', cursor: 'pointer', padding: '0', border: '0' }, { type: 'color' });
|
|
549
560
|
picker.value = f.swatch;
|
|
550
561
|
// The native picker fires `input` continuously while dragging the hue
|
|
@@ -560,6 +571,12 @@ export function renderField(f) {
|
|
|
560
571
|
hex.addEventListener('focus', () => { pill.style.borderColor = 'var(--ed-accent)'; pill.style.boxShadow = '0 0 0 3px var(--ed-soft)'; });
|
|
561
572
|
hex.addEventListener('blur', () => { pill.style.borderColor = 'var(--ed-line)'; pill.style.boxShadow = 'none'; hexCommit.flush(); });
|
|
562
573
|
pill.append(swatch, hex);
|
|
574
|
+
if (f.clearable) {
|
|
575
|
+
const none = el('button', { flex: 'none', alignSelf: 'stretch', width: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '0', borderLeft: '1px solid var(--ed-line)', background: f.transparent ? 'var(--ed-accent)' : 'transparent', color: f.transparent ? 'var(--ed-accent-ink)' : 'var(--ed-muted)', cursor: 'pointer', padding: '0' }, { type: 'button', title: f.transparent ? 'Transparent — click for a solid colour' : 'Make transparent (no fill)', 'aria-pressed': String(!!f.transparent) });
|
|
576
|
+
none.appendChild(icon(f.transparent ? 'check' : 'clear', 12));
|
|
577
|
+
none.addEventListener('click', () => f.onToggleTransparent());
|
|
578
|
+
pill.appendChild(none);
|
|
579
|
+
}
|
|
563
580
|
row.appendChild(pill);
|
|
564
581
|
wrap.appendChild(row);
|
|
565
582
|
return wrap;
|
package/src/render/screenshot.js
CHANGED
|
@@ -80,7 +80,13 @@ export const captureTemplatePng = async function (core, mountInto, scale = 2) {
|
|
|
80
80
|
const wrapper = document.createElement('div');
|
|
81
81
|
// Off-screen but attached (fixed keeps it out of any scroll container's
|
|
82
82
|
// scrollHeight, so capturing never moves the user's canvas position).
|
|
83
|
-
|
|
83
|
+
// The document's own page padding sits inside renderDoc's page element, so
|
|
84
|
+
// the wrapper has to be wide enough for it -- sized off the content width
|
|
85
|
+
// alone, `max-width:100%` would shrink the sheet below `theme.width` by
|
|
86
|
+
// exactly the padding and the capture would come out narrower than the
|
|
87
|
+
// template really is.
|
|
88
|
+
const pageW = theme.width + (Number(theme.padX) || 0) * 2;
|
|
89
|
+
wrapper.style.cssText = `position: fixed; left: -100000px; top: 0; width: ${pageW + pad * 2}px; background: ${theme.bg}; padding: ${pad}px; box-sizing: border-box; display: flex; justify-content: center;`;
|
|
84
90
|
wrapper.setAttribute('aria-hidden', 'true');
|
|
85
91
|
// renderDoc sizes the sheet from `state.device`, so with the mobile toggle
|
|
86
92
|
// on it would return a 375px sheet floating inside this desktop-width
|