@seliseblocks/mailcraft 0.2.1 → 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 +3 -0
- package/dist/mailcraft-editor.bundle.js +59 -48
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/package.json +1 -1
- package/src/core/i18n/loaders.js +78 -0
- package/src/core/i18n/tables.js +5 -4
- package/src/core/import-html.js +9 -0
- package/src/index.js +1 -0
- package/src/mailcraft-editor.js +46 -17
- package/src/render/style.js +503 -488
- package/types/index.d.ts +9 -0
package/package.json
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { EN } from './en.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lazy per-locale loading -- the code-splitting counterpart to tables.js.
|
|
5
|
+
*
|
|
6
|
+
* The 30 non-English tables are over half of everything under src/ (about
|
|
7
|
+
* 82 KB gzipped once minified), and the eager `LOCALE_TABLES` map forces all
|
|
8
|
+
* of them into any bundle that includes the element, whatever language the
|
|
9
|
+
* host actually renders. Each entry here is a *literal* dynamic import, which
|
|
10
|
+
* is the shape every bundler code-splits on: an app that never sets `locale`
|
|
11
|
+
* ships no tables at all, and one that sets `locale="fr"` fetches one small
|
|
12
|
+
* chunk on demand. tables.js stays exported for a host that wants the whole
|
|
13
|
+
* map eagerly (a language switcher with instant previews, say).
|
|
14
|
+
*
|
|
15
|
+
* The single-file demo bundle is unaffected: build.js rewrites `import(...)`
|
|
16
|
+
* onto its synchronous module map, so attribute-driven switching keeps
|
|
17
|
+
* working from `file://` with no network fetch.
|
|
18
|
+
*/
|
|
19
|
+
export const LOCALE_LOADERS = {
|
|
20
|
+
en: () => import('./en.js').then((m) => m.EN),
|
|
21
|
+
ar: () => import('./ar.js').then((m) => m.AR),
|
|
22
|
+
bg: () => import('./bg.js').then((m) => m.BG),
|
|
23
|
+
bn: () => import('./bn.js').then((m) => m.BN),
|
|
24
|
+
ca: () => import('./ca.js').then((m) => m.CA),
|
|
25
|
+
cs: () => import('./cs.js').then((m) => m.CS),
|
|
26
|
+
da: () => import('./da.js').then((m) => m.DA),
|
|
27
|
+
de: () => import('./de.js').then((m) => m.DE),
|
|
28
|
+
'de-CH': () => import('./de-CH.js').then((m) => m.DE_CH),
|
|
29
|
+
dz: () => import('./dz.js').then((m) => m.DZ),
|
|
30
|
+
el: () => import('./el.js').then((m) => m.EL),
|
|
31
|
+
es: () => import('./es.js').then((m) => m.ES),
|
|
32
|
+
et: () => import('./et.js').then((m) => m.ET),
|
|
33
|
+
fi: () => import('./fi.js').then((m) => m.FI),
|
|
34
|
+
fr: () => import('./fr.js').then((m) => m.FR),
|
|
35
|
+
hr: () => import('./hr.js').then((m) => m.HR),
|
|
36
|
+
hu: () => import('./hu.js').then((m) => m.HU),
|
|
37
|
+
it: () => import('./it.js').then((m) => m.IT),
|
|
38
|
+
lt: () => import('./lt.js').then((m) => m.LT),
|
|
39
|
+
lv: () => import('./lv.js').then((m) => m.LV),
|
|
40
|
+
nb: () => import('./nb.js').then((m) => m.NB),
|
|
41
|
+
nl: () => import('./nl.js').then((m) => m.NL),
|
|
42
|
+
pl: () => import('./pl.js').then((m) => m.PL),
|
|
43
|
+
pt: () => import('./pt.js').then((m) => m.PT),
|
|
44
|
+
ro: () => import('./ro.js').then((m) => m.RO),
|
|
45
|
+
ru: () => import('./ru.js').then((m) => m.RU),
|
|
46
|
+
sk: () => import('./sk.js').then((m) => m.SK),
|
|
47
|
+
sl: () => import('./sl.js').then((m) => m.SL),
|
|
48
|
+
sv: () => import('./sv.js').then((m) => m.SV),
|
|
49
|
+
tr: () => import('./tr.js').then((m) => m.TR),
|
|
50
|
+
uk: () => import('./uk.js').then((m) => m.UK),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Module-level (not per-element): two editors on one page asking for the same
|
|
54
|
+
// locale should trigger one fetch and share the parsed table. English is
|
|
55
|
+
// seeded -- it is the translator's built-in base, always statically present.
|
|
56
|
+
const cache = { en: EN };
|
|
57
|
+
const pending = {};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The already-loaded table for a tag. `undefined` means "shipped but not
|
|
61
|
+
* loaded yet" (call `loadLocale` and re-render when it lands); `null` means
|
|
62
|
+
* "no such locale" -- render the English base, exactly what the old eager
|
|
63
|
+
* `LOCALE_TABLES[tag] || null` lookup did for an unknown tag.
|
|
64
|
+
*/
|
|
65
|
+
export function localeTable(tag) {
|
|
66
|
+
if (Object.prototype.hasOwnProperty.call(cache, tag)) return cache[tag];
|
|
67
|
+
return LOCALE_LOADERS[tag] ? undefined : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Fetches (once) and caches a locale's table; resolves null for a tag that does not ship. */
|
|
71
|
+
export function loadLocale(tag) {
|
|
72
|
+
const load = LOCALE_LOADERS[tag];
|
|
73
|
+
if (!load) return Promise.resolve(null);
|
|
74
|
+
if (!pending[tag]) {
|
|
75
|
+
pending[tag] = load().then((table) => { cache[tag] = table; return table; });
|
|
76
|
+
}
|
|
77
|
+
return pending[tag];
|
|
78
|
+
}
|
package/src/core/i18n/tables.js
CHANGED
|
@@ -37,10 +37,11 @@ import { UK } from './uk.js';
|
|
|
37
37
|
* importing and assigning a table by hand.
|
|
38
38
|
*
|
|
39
39
|
* Deliberately a separate module from index.js's metadata-only `LOCALES`:
|
|
40
|
-
* importing this one pulls all translations in. The element
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* importing this one pulls all translations in, eagerly. The element itself
|
|
41
|
+
* no longer does -- it resolves the `locale` attribute through the lazy
|
|
42
|
+
* per-tag loaders (loaders.js), so a bundled app ships only the locales it
|
|
43
|
+
* actually renders. This eager map remains for a host that wants every table
|
|
44
|
+
* up front (an instant-preview language switcher, a server-side pass).
|
|
44
45
|
*/
|
|
45
46
|
export const LOCALE_TABLES = {
|
|
46
47
|
en: EN, ar: AR, bg: BG, bn: BN, ca: CA, cs: CS, da: DA, de: DE, 'de-CH': DE_CH,
|
package/src/core/import-html.js
CHANGED
|
@@ -966,6 +966,15 @@ function themeFromParsedDoc(doc) {
|
|
|
966
966
|
return w && !String(w).endsWith('%') && PX(w) === Number(bestWidth);
|
|
967
967
|
});
|
|
968
968
|
if (content) {
|
|
969
|
+
// The content column's own background -- including the literal
|
|
970
|
+
// `transparent` the exporter always writes for a see-through column.
|
|
971
|
+
// Without this the round trip lost it: export wrote
|
|
972
|
+
// `background:transparent`, the import never read it back, and the
|
|
973
|
+
// blank-doc default repainted the content area white on every
|
|
974
|
+
// save/reload. Only an explicit value is taken, so a foreign email
|
|
975
|
+
// whose content table declares nothing keeps the white default.
|
|
976
|
+
const cbg = bgOf(content);
|
|
977
|
+
if (cbg) theme.contentBg = cbg;
|
|
969
978
|
const r = PX(content.style && content.style.borderRadius);
|
|
970
979
|
if (r > 0) theme.radius = r;
|
|
971
980
|
const cell = content.closest ? content.closest('td') : null;
|
package/src/index.js
CHANGED
|
@@ -5,5 +5,6 @@ export { renderDoc } from './render/canvas.js';
|
|
|
5
5
|
export { BLOCKS, GROUPS, LAYOUTS, PALETTE } from './core/blocks.js';
|
|
6
6
|
export { createTranslator, defineMessages, missingKeys, LOCALES, isRtl, EN, MESSAGE_KEYS } from './core/i18n/index.js';
|
|
7
7
|
export { LOCALE_TABLES } from './core/i18n/tables.js';
|
|
8
|
+
export { LOCALE_LOADERS, loadLocale, localeTable } from './core/i18n/loaders.js';
|
|
8
9
|
export { ALL_FOLDER_ID, normalizeAsset, resolveLimits } from './core/storage.js';
|
|
9
10
|
export { validateFiles, sanitizeName, acceptAttribute, limitsProblem } from './core/storage-limits.js';
|
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();
|
|
@@ -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
|
|
@@ -964,7 +979,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
964
979
|
this.renderExportModal();
|
|
965
980
|
this.renderCodeModal();
|
|
966
981
|
this.renderAiModal();
|
|
967
|
-
this.renderPreviewModal(
|
|
982
|
+
this.renderPreviewModal();
|
|
968
983
|
|
|
969
984
|
this.refreshToast();
|
|
970
985
|
}
|
|
@@ -1599,7 +1614,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1599
1614
|
// own page, then its content column. `is-device` (renderCodeModal) puts
|
|
1600
1615
|
// the mat and the frame back for the 390px phone width, where the card
|
|
1601
1616
|
// *is* the point.
|
|
1602
|
-
const previewWrap = elS('div', 'overflow: auto;
|
|
1617
|
+
const previewWrap = elS('div', 'overflow: auto; display: flex; justify-content: center; background: var(--ed-work);', { class: 'mc-code-preview-body' });
|
|
1603
1618
|
this.codePreviewBody = previewWrap;
|
|
1604
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' });
|
|
1605
1620
|
this.codeFrame.addEventListener('load', () => this.syncCodePreviewScrollbar());
|
|
@@ -1812,7 +1827,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1812
1827
|
buildPreviewModal() {
|
|
1813
1828
|
const t = this.core.t;
|
|
1814
1829
|
const overlay = elS('div', '', { class: 'mc-fullscreen-panel mc-preview-panel' });
|
|
1815
|
-
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;';
|
|
1816
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' });
|
|
1817
1832
|
const headText = elS('div', 'flex: 1; min-width: 0;');
|
|
1818
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' });
|
|
@@ -1826,10 +1841,12 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1826
1841
|
closeBtn.addEventListener('click', () => this.core.setState({ previewOpen: false }));
|
|
1827
1842
|
head.append(headText, this.previewDeviceSeg, closeBtn);
|
|
1828
1843
|
overlay.appendChild(head);
|
|
1829
|
-
//
|
|
1830
|
-
//
|
|
1831
|
-
//
|
|
1832
|
-
|
|
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' });
|
|
1833
1850
|
// Kept on the instance: renderPreviewModal repaints it from the document's
|
|
1834
1851
|
// page background on every render, and it runs long after this builder's
|
|
1835
1852
|
// local has gone out of scope.
|
|
@@ -1845,7 +1862,7 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1845
1862
|
const s = this.core.state;
|
|
1846
1863
|
const t = this.core.t;
|
|
1847
1864
|
this.previewModal.style.display = s.previewOpen ? 'grid' : 'none';
|
|
1848
|
-
if (!s.previewOpen) return;
|
|
1865
|
+
if (!s.previewOpen) { this._previewDoc = null; return; }
|
|
1849
1866
|
this.previewKickerEl.textContent = s.device === 'mobile' ? t('preview.kickerMobile') : t('preview.kickerDesktop', { width: s.doc.theme.width });
|
|
1850
1867
|
this.previewDeviceSeg.innerHTML = '';
|
|
1851
1868
|
[
|
|
@@ -1865,8 +1882,20 @@ export class MailCraftEditor extends ElementBase {
|
|
|
1865
1882
|
// page colour of its own shows behind the email.
|
|
1866
1883
|
const pageBg = s.doc.theme.bg || '';
|
|
1867
1884
|
this.previewBody.style.background = /^(transparent|none)$/i.test(pageBg.trim()) ? '' : pageBg;
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
+
}
|
|
1870
1899
|
}
|
|
1871
1900
|
}
|
|
1872
1901
|
|