@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.
Files changed (72) hide show
  1. package/DOCS.md +10 -8
  2. package/README.md +4 -3
  3. package/README.md.txt +4 -3
  4. package/dist/mailcraft-editor.bundle.js +90 -51
  5. package/dist/mailcraft-editor.bundle.js.map +3 -3
  6. package/examples/templates/activate-your-account.html +1 -1
  7. package/examples/templates/back-in-stock.html +1 -1
  8. package/examples/templates/cart-left-behind.html +1 -1
  9. package/examples/templates/community-giveaway.html +1 -1
  10. package/examples/templates/frontend-futures-invite.html +1 -1
  11. package/examples/templates/give-25-get-25.html +1 -1
  12. package/examples/templates/invoice-paid.html +1 -1
  13. package/examples/templates/meet-nova-launch.html +1 -1
  14. package/examples/templates/mega-weekend-sale.html +1 -1
  15. package/examples/templates/order-confirmed.html +1 -1
  16. package/examples/templates/rate-your-headphones.html +1 -1
  17. package/examples/templates/reset-your-password.html +1 -1
  18. package/examples/templates/thankyou-promo-code.html +1 -1
  19. package/examples/templates/the-sunday-brief.html +1 -1
  20. package/examples/templates/welcome-to-your-workspace.html +1 -1
  21. package/examples/templates/your-order-shipped.html +1 -1
  22. package/examples/templates/your-password-was-changed.html +1 -1
  23. package/examples/templates/your-signin-code.html +1 -1
  24. package/examples/vanilla.html +27 -25
  25. package/package.json +1 -1
  26. package/src/core/binder.js +21 -1
  27. package/src/core/editor-core.js +12 -2
  28. package/src/core/export.js +19 -3
  29. package/src/core/i18n/ar.js +0 -1
  30. package/src/core/i18n/bg.js +0 -1
  31. package/src/core/i18n/bn.js +0 -1
  32. package/src/core/i18n/ca.js +0 -1
  33. package/src/core/i18n/cs.js +0 -1
  34. package/src/core/i18n/da.js +0 -1
  35. package/src/core/i18n/de-CH.js +0 -1
  36. package/src/core/i18n/de.js +0 -1
  37. package/src/core/i18n/dz.js +0 -1
  38. package/src/core/i18n/el.js +0 -1
  39. package/src/core/i18n/en.js +0 -1
  40. package/src/core/i18n/es.js +0 -1
  41. package/src/core/i18n/et.js +0 -1
  42. package/src/core/i18n/fi.js +0 -1
  43. package/src/core/i18n/fr.js +0 -1
  44. package/src/core/i18n/hr.js +0 -1
  45. package/src/core/i18n/hu.js +0 -1
  46. package/src/core/i18n/it.js +0 -1
  47. package/src/core/i18n/loaders.js +78 -0
  48. package/src/core/i18n/lt.js +0 -1
  49. package/src/core/i18n/lv.js +0 -1
  50. package/src/core/i18n/nb.js +0 -1
  51. package/src/core/i18n/nl.js +0 -1
  52. package/src/core/i18n/pl.js +0 -1
  53. package/src/core/i18n/pt.js +0 -1
  54. package/src/core/i18n/ro.js +0 -1
  55. package/src/core/i18n/ru.js +0 -1
  56. package/src/core/i18n/sk.js +0 -1
  57. package/src/core/i18n/sl.js +0 -1
  58. package/src/core/i18n/sv.js +0 -1
  59. package/src/core/i18n/tables.js +5 -4
  60. package/src/core/i18n/tr.js +0 -1
  61. package/src/core/i18n/uk.js +0 -1
  62. package/src/core/import-html.js +31 -0
  63. package/src/core/storage-limits.js +16 -11
  64. package/src/core/storage.js +1 -1
  65. package/src/core/theme.js +15 -1
  66. package/src/index.js +1 -0
  67. package/src/mailcraft-editor.js +79 -23
  68. package/src/render/canvas.js +43 -6
  69. package/src/render/fields.js +17 -0
  70. package/src/render/screenshot.js +7 -1
  71. package/src/render/style.js +503 -460
  72. package/types/index.d.ts +14 -2
@@ -102,7 +102,6 @@ export const ET = {
102
102
  'storage.uploading': 'Laadin üles {count}…',
103
103
  'storage.errLoadFailed': 'Failikogu laadimine ebaõnnestus — {reason}',
104
104
  'storage.errNoLimits': 'Üleslaadimine on välja lülitatud: see rakendus pole määranud üleslaadimispiiranguid.',
105
- 'storage.errNoAccept': 'Üleslaadimine on välja lülitatud: ühtegi failivormingut pole lubatud.',
106
105
  'storage.errNoMaxBytes': 'Üleslaadimine on välja lülitatud: faili suurimat lubatud mahtu pole määratud.',
107
106
  'storage.errTooLarge': '{name} on {size} — piir on {max}.',
108
107
  'storage.errFormat': '{name} on {type}-fail, mis pole siin lubatud.',
@@ -102,7 +102,6 @@ export const FI = {
102
102
  'storage.uploading': 'Ladataan {count}…',
103
103
  'storage.errLoadFailed': 'Tiedostokirjastoa ei voitu ladata — {reason}',
104
104
  'storage.errNoLimits': 'Lataukset on poistettu käytöstä: tämä sovellus ei ole asettanut latausrajoja.',
105
- 'storage.errNoAccept': 'Lataukset on poistettu käytöstä: yhtään tiedostomuotoa ei ole sallittu.',
106
105
  'storage.errNoMaxBytes': 'Lataukset on poistettu käytöstä: tiedoston enimmäiskokoa ei ole asetettu.',
107
106
  'storage.errTooLarge': '{name} on {size} — raja on {max}.',
108
107
  'storage.errFormat': '{name} on {type}-tiedosto, jota ei sallita tässä.',
@@ -102,7 +102,6 @@ export const FR = {
102
102
  'storage.uploading': 'Envoi de {count}…',
103
103
  'storage.errLoadFailed': 'La bibliothèque de fichiers n’a pas pu être chargée — {reason}',
104
104
  'storage.errNoLimits': 'Les envois sont désactivés : cette application n’a défini aucune limite d’envoi.',
105
- 'storage.errNoAccept': 'Les envois sont désactivés : aucun format de fichier n’a été autorisé.',
106
105
  'storage.errNoMaxBytes': 'Les envois sont désactivés : aucune taille maximale de fichier n’a été définie.',
107
106
  'storage.errTooLarge': '{name} fait {size} — la limite est {max}.',
108
107
  'storage.errFormat': '{name} est un fichier {type}, non autorisé ici.',
@@ -102,7 +102,6 @@ export const HR = {
102
102
  'storage.uploading': 'Prijenos {count}…',
103
103
  'storage.errLoadFailed': 'Knjižnicu datoteka nije moguće učitati — {reason}',
104
104
  'storage.errNoLimits': 'Prijenos je isključen: ova aplikacija nije postavila nikakva ograničenja prijenosa.',
105
- 'storage.errNoAccept': 'Prijenos je isključen: nije dopušten nijedan format datoteke.',
106
105
  'storage.errNoMaxBytes': 'Prijenos je isključen: nije postavljena najveća veličina datoteke.',
107
106
  'storage.errTooLarge': '{name} je {size} — ograničenje je {max}.',
108
107
  'storage.errFormat': '{name} je datoteka vrste {type}, koja ovdje nije dopuštena.',
@@ -102,7 +102,6 @@ export const HU = {
102
102
  'storage.uploading': '{count} feltöltése…',
103
103
  'storage.errLoadFailed': 'A fájlkönyvtár nem tölthető be — {reason}',
104
104
  'storage.errNoLimits': 'A feltöltés ki van kapcsolva: ez az alkalmazás nem állított be feltöltési korlátokat.',
105
- 'storage.errNoAccept': 'A feltöltés ki van kapcsolva: nincs engedélyezett fájlformátum.',
106
105
  'storage.errNoMaxBytes': 'A feltöltés ki van kapcsolva: nincs beállítva maximális fájlméret.',
107
106
  'storage.errTooLarge': 'A(z) {name} mérete {size} — a korlát {max}.',
108
107
  'storage.errFormat': 'A(z) {name} egy {type} fájl, ami itt nem engedélyezett.',
@@ -102,7 +102,6 @@ export const IT = {
102
102
  'storage.uploading': 'Caricamento di {count}…',
103
103
  'storage.errLoadFailed': 'Impossibile caricare la libreria file — {reason}',
104
104
  'storage.errNoLimits': 'I caricamenti sono disattivati: questa applicazione non ha impostato limiti di caricamento.',
105
- 'storage.errNoAccept': 'I caricamenti sono disattivati: nessun formato di file è stato consentito.',
106
105
  'storage.errNoMaxBytes': 'I caricamenti sono disattivati: non è stata impostata una dimensione massima del file.',
107
106
  'storage.errTooLarge': '{name} è {size} — il limite è {max}.',
108
107
  'storage.errFormat': '{name} è un file {type}, non consentito qui.',
@@ -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
+ }
@@ -102,7 +102,6 @@ export const LT = {
102
102
  'storage.uploading': 'Įkeliama {count}…',
103
103
  'storage.errLoadFailed': 'Nepavyko įkelti failų bibliotekos — {reason}',
104
104
  'storage.errNoLimits': 'Įkėlimai išjungti: ši programa nenustatė jokių įkėlimo ribų.',
105
- 'storage.errNoAccept': 'Įkėlimai išjungti: neleidžiamas nė vienas failo formatas.',
106
105
  'storage.errNoMaxBytes': 'Įkėlimai išjungti: nenustatytas didžiausias failo dydis.',
107
106
  'storage.errTooLarge': '{name} yra {size} — riba yra {max}.',
108
107
  'storage.errFormat': '{name} yra {type} failas, kuris čia neleidžiamas.',
@@ -102,7 +102,6 @@ export const LV = {
102
102
  'storage.uploading': 'Notiek {count} augšupielāde…',
103
103
  'storage.errLoadFailed': 'Neizdevās ielādēt failu bibliotēku — {reason}',
104
104
  'storage.errNoLimits': 'Augšupielāde ir izslēgta: šī lietotne nav noteikusi augšupielādes ierobežojumus.',
105
- 'storage.errNoAccept': 'Augšupielāde ir izslēgta: nav atļauts neviens faila formāts.',
106
105
  'storage.errNoMaxBytes': 'Augšupielāde ir izslēgta: nav noteikts maksimālais faila izmērs.',
107
106
  'storage.errTooLarge': '{name} ir {size} — ierobežojums ir {max}.',
108
107
  'storage.errFormat': '{name} ir {type} fails, kas šeit nav atļauts.',
@@ -102,7 +102,6 @@ export const NB = {
102
102
  'storage.uploading': 'Laster opp {count}…',
103
103
  'storage.errLoadFailed': 'Filbiblioteket kunne ikke lastes — {reason}',
104
104
  'storage.errNoLimits': 'Opplasting er slått av: denne appen har ikke satt noen opplastingsgrenser.',
105
- 'storage.errNoAccept': 'Opplasting er slått av: ingen filformater er tillatt.',
106
105
  'storage.errNoMaxBytes': 'Opplasting er slått av: ingen maksimal filstørrelse er satt.',
107
106
  'storage.errTooLarge': '{name} er {size} — grensen er {max}.',
108
107
  'storage.errFormat': '{name} er en {type}-fil, som ikke er tillatt her.',
@@ -102,7 +102,6 @@ export const NL = {
102
102
  'storage.uploading': '{count} uploaden…',
103
103
  'storage.errLoadFailed': 'De bestandsbibliotheek kon niet worden geladen — {reason}',
104
104
  'storage.errNoLimits': 'Uploaden staat uit: deze app heeft geen uploadlimieten ingesteld.',
105
- 'storage.errNoAccept': 'Uploaden staat uit: er zijn geen bestandsformaten toegestaan.',
106
105
  'storage.errNoMaxBytes': 'Uploaden staat uit: er is geen maximale bestandsgrootte ingesteld.',
107
106
  'storage.errTooLarge': '{name} is {size} — de limiet is {max}.',
108
107
  'storage.errFormat': '{name} is een {type}-bestand, dat hier niet is toegestaan.',
@@ -102,7 +102,6 @@ export const PL = {
102
102
  'storage.uploading': 'Przesyłanie {count}…',
103
103
  'storage.errLoadFailed': 'Nie udało się wczytać biblioteki plików — {reason}',
104
104
  'storage.errNoLimits': 'Przesyłanie jest wyłączone: ta aplikacja nie ustawiła żadnych limitów przesyłania.',
105
- 'storage.errNoAccept': 'Przesyłanie jest wyłączone: nie dopuszczono żadnego formatu pliku.',
106
105
  'storage.errNoMaxBytes': 'Przesyłanie jest wyłączone: nie ustawiono maksymalnego rozmiaru pliku.',
107
106
  'storage.errTooLarge': '{name} ma {size} — limit to {max}.',
108
107
  'storage.errFormat': '{name} to plik {type}, który nie jest tu dozwolony.',
@@ -102,7 +102,6 @@ export const PT = {
102
102
  'storage.uploading': 'A carregar {count}…',
103
103
  'storage.errLoadFailed': 'Não foi possível carregar a biblioteca de ficheiros — {reason}',
104
104
  'storage.errNoLimits': 'Os carregamentos estão desativados: esta aplicação não definiu limites de carregamento.',
105
- 'storage.errNoAccept': 'Os carregamentos estão desativados: não foi permitido nenhum formato de ficheiro.',
106
105
  'storage.errNoMaxBytes': 'Os carregamentos estão desativados: não foi definido um tamanho máximo de ficheiro.',
107
106
  'storage.errTooLarge': '{name} tem {size} — o limite é {max}.',
108
107
  'storage.errFormat': '{name} é um ficheiro {type}, que não é permitido aqui.',
@@ -102,7 +102,6 @@ export const RO = {
102
102
  'storage.uploading': 'Se încarcă {count}…',
103
103
  'storage.errLoadFailed': 'Biblioteca de fișiere nu a putut fi încărcată — {reason}',
104
104
  'storage.errNoLimits': 'Încărcările sunt oprite: această aplicație nu a stabilit nicio limită de încărcare.',
105
- 'storage.errNoAccept': 'Încărcările sunt oprite: nu a fost permis niciun format de fișier.',
106
105
  'storage.errNoMaxBytes': 'Încărcările sunt oprite: nu a fost stabilită o dimensiune maximă a fișierului.',
107
106
  'storage.errTooLarge': '{name} are {size} — limita este {max}.',
108
107
  'storage.errFormat': '{name} este un fișier {type}, care nu este permis aici.',
@@ -102,7 +102,6 @@ export const RU = {
102
102
  'storage.uploading': 'Загрузка {count}…',
103
103
  'storage.errLoadFailed': 'Не удалось загрузить библиотеку файлов — {reason}',
104
104
  'storage.errNoLimits': 'Загрузка выключена: это приложение не задало ограничения на загрузку.',
105
- 'storage.errNoAccept': 'Загрузка выключена: ни один формат файлов не разрешён.',
106
105
  'storage.errNoMaxBytes': 'Загрузка выключена: не задан максимальный размер файла.',
107
106
  'storage.errTooLarge': '{name} весит {size} — предел {max}.',
108
107
  'storage.errFormat': '{name} — файл {type}, который здесь не разрешён.',
@@ -102,7 +102,6 @@ export const SK = {
102
102
  'storage.uploading': 'Nahrávanie {count}…',
103
103
  'storage.errLoadFailed': 'Knižnicu súborov sa nepodarilo načítať — {reason}',
104
104
  'storage.errNoLimits': 'Nahrávanie je vypnuté: táto aplikácia nenastavila žiadne limity nahrávania.',
105
- 'storage.errNoAccept': 'Nahrávanie je vypnuté: nebol povolený žiadny formát súboru.',
106
105
  'storage.errNoMaxBytes': 'Nahrávanie je vypnuté: nie je nastavená maximálna veľkosť súboru.',
107
106
  'storage.errTooLarge': '{name} má {size} — limit je {max}.',
108
107
  'storage.errFormat': '{name} je súbor typu {type}, ktorý tu nie je povolený.',
@@ -102,7 +102,6 @@ export const SL = {
102
102
  'storage.uploading': 'Nalaganje {count}…',
103
103
  'storage.errLoadFailed': 'Knjižnice datotek ni bilo mogoče naložiti — {reason}',
104
104
  'storage.errNoLimits': 'Nalaganje je izklopljeno: ta aplikacija ni določila omejitev nalaganja.',
105
- 'storage.errNoAccept': 'Nalaganje je izklopljeno: dovoljena ni nobena vrsta datoteke.',
106
105
  'storage.errNoMaxBytes': 'Nalaganje je izklopljeno: največja velikost datoteke ni določena.',
107
106
  'storage.errTooLarge': '{name} je {size} — omejitev je {max}.',
108
107
  'storage.errFormat': '{name} je datoteka vrste {type}, ki tu ni dovoljena.',
@@ -102,7 +102,6 @@ export const SV = {
102
102
  'storage.uploading': 'Laddar upp {count}…',
103
103
  'storage.errLoadFailed': 'Filbiblioteket kunde inte laddas — {reason}',
104
104
  'storage.errNoLimits': 'Uppladdning är avstängd: den här appen har inte satt några uppladdningsgränser.',
105
- 'storage.errNoAccept': 'Uppladdning är avstängd: inga filformat har tillåtits.',
106
105
  'storage.errNoMaxBytes': 'Uppladdning är avstängd: ingen maximal filstorlek har angetts.',
107
106
  'storage.errTooLarge': '{name} är {size} — gränsen är {max}.',
108
107
  'storage.errFormat': '{name} är en {type}-fil, som inte tillåts här.',
@@ -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 accepts that
41
- * (host-driven language is worth it, and the demo bundle carries every module
42
- * anyway); a size-sensitive host that bypasses the attribute can still
43
- * deep-import a single `core/i18n/<tag>.js` and assign `.messages` itself.
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,
@@ -102,7 +102,6 @@ export const TR = {
102
102
  'storage.uploading': '{count} yükleniyor…',
103
103
  'storage.errLoadFailed': 'Dosya kitaplığı yüklenemedi — {reason}',
104
104
  'storage.errNoLimits': 'Yüklemeler kapalı: bu uygulama hiçbir yükleme sınırı belirlemedi.',
105
- 'storage.errNoAccept': 'Yüklemeler kapalı: hiçbir dosya biçimine izin verilmedi.',
106
105
  'storage.errNoMaxBytes': 'Yüklemeler kapalı: en büyük dosya boyutu belirlenmedi.',
107
106
  'storage.errTooLarge': '{name} {size} — sınır {max}.',
108
107
  'storage.errFormat': '{name} bir {type} dosyası ve burada izin verilmiyor.',
@@ -102,7 +102,6 @@ export const UK = {
102
102
  'storage.uploading': 'Завантаження {count}…',
103
103
  'storage.errLoadFailed': 'Не вдалося завантажити бібліотеку файлів — {reason}',
104
104
  'storage.errNoLimits': 'Завантаження вимкнено: цей застосунок не задав обмежень на завантаження.',
105
- 'storage.errNoAccept': 'Завантаження вимкнено: не дозволено жодного формату файлів.',
106
105
  'storage.errNoMaxBytes': 'Завантаження вимкнено: не задано максимальний розмір файлу.',
107
106
  'storage.errTooLarge': '{name} важить {size} — обмеження {max}.',
108
107
  'storage.errFormat': '{name} — файл {type}, який тут не дозволено.',
@@ -955,6 +955,37 @@ function themeFromParsedDoc(doc) {
955
955
  });
956
956
  const bestWidth = Object.keys(widthCounts).sort((a, b) => widthCounts[b] - widthCounts[a])[0];
957
957
  if (bestWidth) theme.width = Number(bestWidth);
958
+ // Page padding and content shape, read off the same content table the
959
+ // width vote just picked: the padding on the cell that centers it is the
960
+ // band the template sits in, and the table's own radius is the content
961
+ // column's corner. Without this an import flattened both, and the next
962
+ // export silently squared the template off and closed the gap around it.
963
+ if (bestWidth) {
964
+ const content = Array.from(body.querySelectorAll('table')).find((tb) => {
965
+ const w = tb.getAttribute('width') || (tb.style && tb.style.width) || '';
966
+ return w && !String(w).endsWith('%') && PX(w) === Number(bestWidth);
967
+ });
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;
978
+ const r = PX(content.style && content.style.borderRadius);
979
+ if (r > 0) theme.radius = r;
980
+ const cell = content.closest ? content.closest('td') : null;
981
+ if (cell && cell.style) {
982
+ const padY = PX(cell.style.paddingTop);
983
+ const padX = PX(cell.style.paddingLeft);
984
+ if (padY > 0) theme.padY = padY;
985
+ if (padX > 0) theme.padX = padX;
986
+ }
987
+ }
988
+ }
958
989
  // Prefer the first *real* stack (has a comma or quotes) over a lone generic
959
990
  // keyword: builders wrap everything in a `font-family:sans-serif` shim div
960
991
  // with the actual `'DM Sans', Arial, ...` declared a level deeper.
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Upload validation -- mechanism only, no policy.
3
3
  *
4
- * Deliberately ships no default ceilings. What an email template may carry
5
- * depends on the sending platform (ESP attachment caps), the audience's
6
- * clients (Outlook's Word engine won't render WebP; nothing renders AVIF) and
7
- * the host's own product rules, so the numbers are the host's to set:
8
- * `editor.storageLimits`, or `limits` on the provider. With a provider wired
9
- * and no limits declared, uploads are refused rather than waved through.
4
+ * Sizes and counts stay undefaulted: what an email template may carry depends
5
+ * on the sending platform (ESP attachment caps) and the host's own product
6
+ * rules, so those numbers are the host's to set -- `editor.storageLimits`, or
7
+ * `limits` on the provider. Formats are the one axis with a default: every
8
+ * image type the sniffer can name is allowed unless the host narrows `accept`.
9
+ * With a provider wired and no `maxBytes` declared, uploads are refused rather
10
+ * than waved through.
10
11
  *
11
12
  * Checks run *before* the provider is called, so a rejected file never reaches
12
13
  * the backend. That matters for any store where minting an upload URL also
@@ -96,10 +97,9 @@ export function sanitizeName(name) {
96
97
  return clean.slice(0, 120 - ext.length) + ext;
97
98
  }
98
99
 
99
- /** The host has to have said what's allowed. Format and size are the two that matter; the rest are optional tightenings. */
100
+ /** The host has to have said how big a file may be; the format list is optional and defaults to all of these. */
100
101
  export function limitsProblem(limits) {
101
102
  if (!limits) return 'storage.errNoLimits';
102
- if (!Array.isArray(limits.accept) || !limits.accept.length) return 'storage.errNoAccept';
103
103
  if (!(Number(limits.maxBytes) > 0)) return 'storage.errNoMaxBytes';
104
104
  return null;
105
105
  }
@@ -121,7 +121,12 @@ export async function validateFiles(list, limits) {
121
121
  const problem = limitsProblem(limits);
122
122
  if (problem) return { accepted, rejected: files.map((f) => ({ name: f.name, key: problem, params: {} })) };
123
123
 
124
- const accept = limits.accept.map((m) => String(m).toLowerCase());
124
+ // An omitted or empty `accept` means every image type the sniffer can name,
125
+ // not "nothing" -- a host that wants a narrower list has to say so, but the
126
+ // starting point is that an image is uploadable.
127
+ const accept = Array.isArray(limits.accept) && limits.accept.length
128
+ ? limits.accept.map((m) => String(m).toLowerCase())
129
+ : null;
125
130
  const max = Number(limits.maxBytes);
126
131
  const maxW = Number(limits.maxWidth) || 0;
127
132
  const maxH = Number(limits.maxHeight) || 0;
@@ -146,7 +151,7 @@ export async function validateFiles(list, limits) {
146
151
  rejected.push({ name, key: 'storage.errUnreadable', params: { name } });
147
152
  continue;
148
153
  }
149
- if (!accept.includes(type)) {
154
+ if (accept && !accept.includes(type)) {
150
155
  rejected.push({ name, key: 'storage.errFormat', params: { name, type: type.replace(/^image\//, '').toUpperCase() } });
151
156
  continue;
152
157
  }
@@ -181,4 +186,4 @@ export async function validateFiles(list, limits) {
181
186
  export function acceptAttribute(limits) {
182
187
  if (!limits || !Array.isArray(limits.accept) || !limits.accept.length) return 'image/*';
183
188
  return limits.accept.join(',');
184
- }
189
+ }
@@ -35,7 +35,7 @@
35
35
  * Provider-declared ceilings. `editor.storageLimits` wins over these.
36
36
  *
37
37
  * @typedef {Object} StorageLimits
38
- * @property {string[]} accept Allowed MIME types, e.g. `['image/jpeg','image/png','image/gif']`. Required.
38
+ * @property {string[]} [accept] Allowed MIME types. Omitted or empty means every image type the validator recognizes.
39
39
  * @property {number} maxBytes Per-file byte ceiling. Required.
40
40
  * @property {number} [maxWidth]
41
41
  * @property {number} [maxHeight]
package/src/core/theme.js CHANGED
@@ -1 +1,15 @@
1
- export const THEME = () => ({ bg: '#eef2f7', contentBg: '#ffffff', width: 620, font: '"Helvetica Neue", Helvetica, Arial, sans-serif', text: '#172033', link: '#0065b3' });
1
+ /**
2
+ * The document's page-level model. `bg` paints the full-width area the email
3
+ * sits on (what a mail client shows around the content column), `contentBg`
4
+ * paints the content column itself, and `padY`/`padX`/`radius` shape the gap
5
+ * between the two -- the band that is otherwise an unstyleable strip around
6
+ * every template.
7
+ *
8
+ * They default to 0 rather than to the 24px/12px band the exporter used to
9
+ * hard-code: that band was in every sent template with no way to reach it,
10
+ * which is the strip around the content column that looked like a rendering
11
+ * fault. Now it is off unless a document asks for it. Both background keys
12
+ * accept the literal `transparent` (and any rgba()/#rrggbbaa value) as well
13
+ * as a hex colour.
14
+ */
15
+ export const THEME = () => ({ bg: '#eef2f7', contentBg: '#ffffff', width: 620, padY: 0, padX: 0, radius: 0, font: '"Helvetica Neue", Helvetica, Arial, sans-serif', text: '#172033', link: '#0065b3' });
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';