@seliseblocks/mailcraft 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/DOCS.md +4 -3
  2. package/README.md +4 -1
  3. package/README.md.txt +4 -1
  4. package/dist/mailcraft-editor.bundle.js +57 -54
  5. package/dist/mailcraft-editor.bundle.js.map +3 -3
  6. package/examples/vanilla.html +19 -18
  7. package/package.json +1 -1
  8. package/src/core/brand.js +81 -0
  9. package/src/core/editor-core.js +41 -8
  10. package/src/core/export.js +8 -1
  11. package/src/core/i18n/ar.js +2 -2
  12. package/src/core/i18n/bg.js +2 -2
  13. package/src/core/i18n/bn.js +2 -2
  14. package/src/core/i18n/ca.js +2 -2
  15. package/src/core/i18n/cs.js +2 -2
  16. package/src/core/i18n/da.js +2 -2
  17. package/src/core/i18n/de-CH.js +2 -2
  18. package/src/core/i18n/de.js +2 -2
  19. package/src/core/i18n/dz.js +2 -2
  20. package/src/core/i18n/el.js +2 -2
  21. package/src/core/i18n/en.js +2 -2
  22. package/src/core/i18n/es.js +2 -2
  23. package/src/core/i18n/et.js +2 -2
  24. package/src/core/i18n/fi.js +2 -2
  25. package/src/core/i18n/fr.js +2 -2
  26. package/src/core/i18n/hr.js +2 -2
  27. package/src/core/i18n/hu.js +2 -2
  28. package/src/core/i18n/it.js +2 -2
  29. package/src/core/i18n/lt.js +2 -2
  30. package/src/core/i18n/lv.js +2 -2
  31. package/src/core/i18n/nb.js +2 -2
  32. package/src/core/i18n/nl.js +2 -2
  33. package/src/core/i18n/pl.js +2 -2
  34. package/src/core/i18n/pt.js +2 -2
  35. package/src/core/i18n/ro.js +2 -2
  36. package/src/core/i18n/ru.js +2 -2
  37. package/src/core/i18n/sk.js +2 -2
  38. package/src/core/i18n/sl.js +2 -2
  39. package/src/core/i18n/sv.js +2 -2
  40. package/src/core/i18n/tr.js +2 -2
  41. package/src/core/i18n/uk.js +2 -2
  42. package/src/core/icons.js +1 -0
  43. package/src/core/import-html.js +63 -9
  44. package/src/core/sanitize.js +34 -0
  45. package/src/core/theme.js +1 -1
  46. package/src/mailcraft-editor.js +20 -10
  47. package/src/render/block-body.js +12 -6
  48. package/src/render/canvas.js +7 -1
  49. package/src/render/fields.js +9 -1
  50. package/src/render/screenshot.js +13 -2
  51. package/src/render/style.js +14 -11
@@ -66,7 +66,7 @@ export const HU = {
66
66
  'Szakaszalapú: először helyezz el egy elrendezést, majd blokkokat az oszlopaiba — minden szakasznak saját háttere, kerete, flex vagy rács beállításai vannak.',
67
67
  'rows.hintStack': 'Szabad halmozás: elrendezések nélkül — minden elhelyezett blokk saját teljes szélességű szakasszá válik, sorrendben egymásra halmozva.',
68
68
  'files.libraryCount': 'Könyvtár — {count} fájl',
69
- 'files.openManager': 'Fájlkezelő megnyitása',
69
+ 'files.openManager': 'Médiatár megnyitása',
70
70
  'files.dragToCanvas': 'Húzz bármely fájlt közvetlenül a rajzvászonra, hogy képblokként helyezd el.',
71
71
  'layers.structure': 'Struktúra',
72
72
  'layers.sectionSingle': 'Szakasz',
@@ -82,7 +82,7 @@ export const HU = {
82
82
  'vars.noMatch': 'Egyetlen változó sem felel meg ennek a szűrőnek.',
83
83
  'vars.noneDeclared': 'Az alkalmazásod még nem deklarált változókat.',
84
84
  'vars.rteHint': 'Szöveg szerkesztése közben egy változót közvetlenül a beágyazott eszköztár legördülő menüjéből is kiválaszthatsz.',
85
- 'modal.fileManager': 'Fájlkezelő',
85
+ 'modal.fileManager': 'Médiatár',
86
86
  'modal.assetsTitle': 'Erőforrások',
87
87
  'modal.replaceImage': 'Kép cseréje',
88
88
  'library.searchPlaceholder': 'Fájlok keresése…',
@@ -66,7 +66,7 @@ export const IT = {
66
66
  'Basato su sezioni: rilascia prima un layout, poi i blocchi nelle sue colonne — ogni sezione ha proprio sfondo, bordo, impostazioni flex o griglia.',
67
67
  'rows.hintStack': 'Pila libera: senza layout — ogni blocco rilasciato diventa una propria sezione a piena larghezza, impilata in ordine.',
68
68
  'files.libraryCount': 'Libreria — {count} file',
69
- 'files.openManager': 'Apri gestione file',
69
+ 'files.openManager': 'Apri libreria multimediale',
70
70
  'files.dragToCanvas': 'Trascina un qualsiasi file direttamente sul canvas per inserirlo come blocco immagine.',
71
71
  'layers.structure': 'Struttura',
72
72
  'layers.sectionSingle': 'Sezione',
@@ -82,7 +82,7 @@ export const IT = {
82
82
  'vars.noMatch': 'Nessuna variabile corrisponde a questo filtro.',
83
83
  'vars.noneDeclared': 'La tua applicazione non ha ancora dichiarato variabili.',
84
84
  'vars.rteHint': 'Durante la modifica del testo puoi anche scegliere una variabile direttamente dal menu della barra degli strumenti in linea.',
85
- 'modal.fileManager': 'Gestione file',
85
+ 'modal.fileManager': 'Libreria multimediale',
86
86
  'modal.assetsTitle': 'Risorse',
87
87
  'modal.replaceImage': 'Sostituisci immagine',
88
88
  'library.searchPlaceholder': 'Cerca file…',
@@ -66,7 +66,7 @@ export const LT = {
66
66
  'Pagrįsta sekcijomis: pirmiausia numeskite išdėstymą, tada blokus į jo stulpelius — kiekviena sekcija turi savo foną, kraštinę, flex arba tinklelio nustatymus.',
67
67
  'rows.hintStack': 'Laisvas krūvinys: be išdėstymų — kiekvienas numestas blokas tampa savo pilno pločio sekcija, sudėta iš eilės.',
68
68
  'files.libraryCount': 'Biblioteka — {count} failų',
69
- 'files.openManager': 'Atidaryti failų tvarkyklę',
69
+ 'files.openManager': 'Atidaryti medijos biblioteką',
70
70
  'files.dragToCanvas': 'Vilkite bet kurį failą tiesiai į drobę, kad jį padėtumėte kaip vaizdo bloką.',
71
71
  'layers.structure': 'Struktūra',
72
72
  'layers.sectionSingle': 'Sekcija',
@@ -82,7 +82,7 @@ export const LT = {
82
82
  'vars.noMatch': 'Nė vienas kintamasis neatitinka šio filtro.',
83
83
  'vars.noneDeclared': 'Jūsų programa dar nedeklaravo jokių kintamųjų.',
84
84
  'vars.rteHint': 'Redaguodami tekstą taip pat galite pasirinkti kintamąjį tiesiai iš įterptos įrankių juostos išskleidžiamojo meniu.',
85
- 'modal.fileManager': 'Failų tvarkyklė',
85
+ 'modal.fileManager': 'Medijos biblioteka',
86
86
  'modal.assetsTitle': 'Ištekliai',
87
87
  'modal.replaceImage': 'Pakeisti vaizdą',
88
88
  'library.searchPlaceholder': 'Ieškoti failų…',
@@ -66,7 +66,7 @@ export const LV = {
66
66
  'Balstīts uz sadaļām: vispirms nometiet izkārtojumu, tad blokus tā kolonnās — katrai sadaļai ir sava fona, apmales, flex vai režģa iestatījumi.',
67
67
  'rows.hintStack': 'Brīva kaudze: bez izkārtojumiem — katrs nomestais bloks kļūst par savu pilna platuma sadaļu, sakrautu secībā.',
68
68
  'files.libraryCount': 'Bibliotēka — {count} faili',
69
- 'files.openManager': 'Atvērt failu pārvaldnieku',
69
+ 'files.openManager': 'Atvērt multivides bibliotēku',
70
70
  'files.dragToCanvas': 'Velciet jebkuru failu tieši uz audekla, lai novietotu to kā attēla bloku.',
71
71
  'layers.structure': 'Struktūra',
72
72
  'layers.sectionSingle': 'Sadaļa',
@@ -82,7 +82,7 @@ export const LV = {
82
82
  'vars.noMatch': 'Neviens mainīgais neatbilst šim filtram.',
83
83
  'vars.noneDeclared': 'Jūsu lietotne vēl nav deklarējusi nevienu mainīgo.',
84
84
  'vars.rteHint': 'Rediģējot tekstu, jūs varat arī izvēlēties mainīgo tieši no iegultās rīkjoslas nolaižamās izvēlnes.',
85
- 'modal.fileManager': 'Failu pārvaldnieks',
85
+ 'modal.fileManager': 'Multivides bibliotēka',
86
86
  'modal.assetsTitle': 'Resursi',
87
87
  'modal.replaceImage': 'Aizstāt attēlu',
88
88
  'library.searchPlaceholder': 'Meklēt failus…',
@@ -66,7 +66,7 @@ export const NB = {
66
66
  'Seksjonsbasert: slipp en layout først, deretter blokker i kolonnene — hver seksjon har egen bakgrunn, kant, flex- eller rutenettinnstillinger.',
67
67
  'rows.hintStack': 'Fri stabel: uten layouter — hver blokk du slipper blir sin egen fullbredde-seksjon, stablet i rekkefølge.',
68
68
  'files.libraryCount': 'Bibliotek — {count} filer',
69
- 'files.openManager': 'Åpne filbehandling',
69
+ 'files.openManager': 'Åpne mediebibliotek',
70
70
  'files.dragToCanvas': 'Dra en hvilken som helst fil direkte til lerretet for å plassere den som en bildeblokk.',
71
71
  'layers.structure': 'Struktur',
72
72
  'layers.sectionSingle': 'Seksjon',
@@ -82,7 +82,7 @@ export const NB = {
82
82
  'vars.noMatch': 'Ingen variabel samsvarer med dette filteret.',
83
83
  'vars.noneDeclared': 'Applikasjonen din har ennå ikke erklært noen variabler.',
84
84
  'vars.rteHint': 'Når du redigerer tekst kan du også velge en variabel direkte fra rullegardinmenyen i den innebygde verktøylinjen.',
85
- 'modal.fileManager': 'Filbehandling',
85
+ 'modal.fileManager': 'Mediebibliotek',
86
86
  'modal.assetsTitle': 'Ressurser',
87
87
  'modal.replaceImage': 'Erstatt bilde',
88
88
  'library.searchPlaceholder': 'Søk i filer…',
@@ -66,7 +66,7 @@ export const NL = {
66
66
  'Op secties gebaseerd: plaats eerst een lay-out, sleep dan blokken in de kolommen — elke sectie heeft zijn eigen achtergrond, rand, flex- of gridinstellingen.',
67
67
  'rows.hintStack': 'Vrije stapel: zonder lay-outs — elk blok dat je plaatst wordt een eigen sectie over de volle breedte, op volgorde gestapeld.',
68
68
  'files.libraryCount': 'Bibliotheek — {count} bestanden',
69
- 'files.openManager': 'Bestandsbeheer openen',
69
+ 'files.openManager': 'Mediabibliotheek openen',
70
70
  'files.dragToCanvas': 'Sleep elk bestand direct naar het canvas om het als afbeeldingsblok te plaatsen.',
71
71
  'layers.structure': 'Structuur',
72
72
  'layers.sectionSingle': 'Sectie',
@@ -82,7 +82,7 @@ export const NL = {
82
82
  'vars.noMatch': 'Geen variabele komt overeen met dit filter.',
83
83
  'vars.noneDeclared': 'Jouw applicatie heeft nog geen variabelen gedeclareerd.',
84
84
  'vars.rteHint': 'Tijdens het bewerken van tekst kun je ook een variabele kiezen via het dropdownmenu van de inline werkbalk.',
85
- 'modal.fileManager': 'Bestandsbeheer',
85
+ 'modal.fileManager': 'Mediabibliotheek',
86
86
  'modal.assetsTitle': 'Bestanden',
87
87
  'modal.replaceImage': 'Afbeelding vervangen',
88
88
  'library.searchPlaceholder': 'Bestanden zoeken…',
@@ -66,7 +66,7 @@ export const PL = {
66
66
  'Oparty na sekcjach: najpierw upuść układ, potem bloki w jego kolumnach — każda sekcja ma własne tło, obramowanie, ustawienia flex lub siatki.',
67
67
  'rows.hintStack': 'Wolny stos: bez układów — każdy upuszczony blok staje się osobną, pełnowymiarową sekcją, ułożoną w kolejności.',
68
68
  'files.libraryCount': 'Biblioteka — {count} plików',
69
- 'files.openManager': 'Otwórz menedżera plików',
69
+ 'files.openManager': 'Otwórz bibliotekę mediów',
70
70
  'files.dragToCanvas': 'Przeciągnij dowolny plik bezpośrednio na płótno, aby umieścić go jako blok obrazu.',
71
71
  'layers.structure': 'Struktura',
72
72
  'layers.sectionSingle': 'Sekcja',
@@ -82,7 +82,7 @@ export const PL = {
82
82
  'vars.noMatch': 'Żadna zmienna nie pasuje do tego filtra.',
83
83
  'vars.noneDeclared': 'Twoja aplikacja nie zadeklarowała jeszcze żadnych zmiennych.',
84
84
  'vars.rteHint': 'Podczas edycji tekstu możesz też wybrać zmienną bezpośrednio z rozwijanego menu paska narzędzi.',
85
- 'modal.fileManager': 'Menedżer plików',
85
+ 'modal.fileManager': 'Biblioteka mediów',
86
86
  'modal.assetsTitle': 'Zasoby',
87
87
  'modal.replaceImage': 'Zastąp obraz',
88
88
  'library.searchPlaceholder': 'Szukaj plików…',
@@ -66,7 +66,7 @@ export const PT = {
66
66
  'Baseado em seções: solte um layout primeiro, depois blocos em suas colunas — cada seção tem seu próprio fundo, borda, configurações flex ou grade.',
67
67
  'rows.hintStack': 'Pilha livre: sem layouts — cada bloco que você solta se torna sua própria seção de largura total, empilhada em ordem.',
68
68
  'files.libraryCount': 'Biblioteca — {count} arquivos',
69
- 'files.openManager': 'Abrir gerenciador de arquivos',
69
+ 'files.openManager': 'Abrir biblioteca de mídia',
70
70
  'files.dragToCanvas': 'Arraste qualquer arquivo diretamente para a tela para colocá-lo como bloco de imagem.',
71
71
  'layers.structure': 'Estrutura',
72
72
  'layers.sectionSingle': 'Seção',
@@ -82,7 +82,7 @@ export const PT = {
82
82
  'vars.noMatch': 'Nenhuma variável corresponde a esse filtro.',
83
83
  'vars.noneDeclared': 'Sua aplicação ainda não declarou nenhuma variável.',
84
84
  'vars.rteHint': 'Ao editar texto, você também pode escolher uma variável diretamente no menu da barra de ferramentas em linha.',
85
- 'modal.fileManager': 'Gerenciador de arquivos',
85
+ 'modal.fileManager': 'Biblioteca de mídia',
86
86
  'modal.assetsTitle': 'Recursos',
87
87
  'modal.replaceImage': 'Substituir imagem',
88
88
  'library.searchPlaceholder': 'Pesquisar arquivos…',
@@ -66,7 +66,7 @@ export const RO = {
66
66
  'Bazat pe secțiuni: plasați mai întâi un aspect, apoi blocuri în coloanele sale — fiecare secțiune are propriul fundal, bordură, setări flex sau grilă.',
67
67
  'rows.hintStack': 'Stivă liberă: fără aspecte — fiecare bloc plasat devine propria secțiune pe toată lățimea, stivuită în ordine.',
68
68
  'files.libraryCount': 'Bibliotecă — {count} fișiere',
69
- 'files.openManager': 'Deschide managerul de fișiere',
69
+ 'files.openManager': 'Deschide biblioteca media',
70
70
  'files.dragToCanvas': 'Trageți orice fișier direct pe canvas pentru a-l plasa ca bloc de imagine.',
71
71
  'layers.structure': 'Structură',
72
72
  'layers.sectionSingle': 'Secțiune',
@@ -82,7 +82,7 @@ export const RO = {
82
82
  'vars.noMatch': 'Nicio variabilă nu corespunde acestui filtru.',
83
83
  'vars.noneDeclared': 'Aplicația dvs. nu a declarat încă nicio variabilă.',
84
84
  'vars.rteHint': 'În timp ce editați text, puteți alege o variabilă și direct din meniul derulant al barei de instrumente în linie.',
85
- 'modal.fileManager': 'Manager de fișiere',
85
+ 'modal.fileManager': 'Biblioteca media',
86
86
  'modal.assetsTitle': 'Resurse',
87
87
  'modal.replaceImage': 'Înlocuiește imaginea',
88
88
  'library.searchPlaceholder': 'Căutare fișiere…',
@@ -66,7 +66,7 @@ export const RU = {
66
66
  'На основе секций: сначала поместите макет, затем блоки в его колонки — у каждой секции свой фон, граница, настройки flex или сетки.',
67
67
  'rows.hintStack': 'Свободный стек: без макетов — каждый добавленный блок становится собственной секцией на всю ширину, уложенной по порядку.',
68
68
  'files.libraryCount': 'Библиотека — {count} файлов',
69
- 'files.openManager': 'Открыть файловый менеджер',
69
+ 'files.openManager': 'Открыть медиатеку',
70
70
  'files.dragToCanvas': 'Перетащите любой файл прямо на холст, чтобы разместить его как блок изображения.',
71
71
  'layers.structure': 'Структура',
72
72
  'layers.sectionSingle': 'Секция',
@@ -82,7 +82,7 @@ export const RU = {
82
82
  'vars.noMatch': 'Нет переменных, соответствующих этому фильтру.',
83
83
  'vars.noneDeclared': 'Ваше приложение ещё не объявило переменные.',
84
84
  'vars.rteHint': 'При редактировании текста вы также можете выбрать переменную прямо из выпадающего списка встроенной панели инструментов.',
85
- 'modal.fileManager': 'Файловый менеджер',
85
+ 'modal.fileManager': 'Медиатека',
86
86
  'modal.assetsTitle': 'Файлы',
87
87
  'modal.replaceImage': 'Заменить изображение',
88
88
  'library.searchPlaceholder': 'Поиск файлов…',
@@ -66,7 +66,7 @@ export const SK = {
66
66
  'Založené na sekciách: najprv umiestnite rozloženie, potom bloky do jeho stĺpcov — každá sekcia má vlastné pozadie, ohraničenie, nastavenia flex alebo mriežky.',
67
67
  'rows.hintStack': 'Voľný zásobník: bez rozložení — každý umiestnený blok sa stane vlastnou sekciou na celú šírku, zoradenou postupne.',
68
68
  'files.libraryCount': 'Knižnica — {count} súborov',
69
- 'files.openManager': 'Otvoriť správcu súborov',
69
+ 'files.openManager': 'Otvoriť knižnicu médií',
70
70
  'files.dragToCanvas': 'Presuňte ľubovoľný súbor priamo na plátno a umiestnite ho ako obrazový blok.',
71
71
  'layers.structure': 'Štruktúra',
72
72
  'layers.sectionSingle': 'Sekcia',
@@ -82,7 +82,7 @@ export const SK = {
82
82
  'vars.noMatch': 'Žiadna premenná nezodpovedá tomuto filtru.',
83
83
  'vars.noneDeclared': 'Vaša aplikácia zatiaľ nedeklarovala žiadne premenné.',
84
84
  'vars.rteHint': 'Pri úprave textu môžete premennú vybrať aj priamo z rozbaľovacej ponuky vloženého panela nástrojov.',
85
- 'modal.fileManager': 'Správca súborov',
85
+ 'modal.fileManager': 'Knižnica médií',
86
86
  'modal.assetsTitle': 'Zdroje',
87
87
  'modal.replaceImage': 'Nahradiť obrázok',
88
88
  'library.searchPlaceholder': 'Hľadať súbory…',
@@ -66,7 +66,7 @@ export const SL = {
66
66
  'Temelji na odsekih: najprej spustite postavitev, nato bloke v njene stolpce — vsak odsek ima lastno ozadje, rob, nastavitve flex ali mreže.',
67
67
  'rows.hintStack': 'Prosti sklad: brez postavitev — vsak spuščen blok postane lasten odsek čez celo širino, zložen po vrsti.',
68
68
  'files.libraryCount': 'Knjižnica — {count} datotek',
69
- 'files.openManager': 'Odpri upravitelja datotek',
69
+ 'files.openManager': 'Odpri predstavnostno knjižnico',
70
70
  'files.dragToCanvas': 'Povlecite katero koli datoteko neposredno na platno, da jo postavite kot slikovni blok.',
71
71
  'layers.structure': 'Struktura',
72
72
  'layers.sectionSingle': 'Odsek',
@@ -82,7 +82,7 @@ export const SL = {
82
82
  'vars.noMatch': 'Nobena spremenljivka ne ustreza temu filtru.',
83
83
  'vars.noneDeclared': 'Vaša aplikacija še ni deklarirala nobenih spremenljivk.',
84
84
  'vars.rteHint': 'Med urejanjem besedila lahko spremenljivko izberete tudi neposredno iz spustnega menija vgrajene orodne vrstice.',
85
- 'modal.fileManager': 'Upravitelj datotek',
85
+ 'modal.fileManager': 'Predstavnostna knjižnica',
86
86
  'modal.assetsTitle': 'Viri',
87
87
  'modal.replaceImage': 'Zamenjaj sliko',
88
88
  'library.searchPlaceholder': 'Iskanje datotek…',
@@ -66,7 +66,7 @@ export const SV = {
66
66
  'Sektionsbaserad: släpp en layout först, sedan block i dess kolumner — varje sektion har egen bakgrund, kant, flex- eller rutnätsinställningar.',
67
67
  'rows.hintStack': 'Fri stapel: utan layouter — varje block du släpper blir sin egen sektion i full bredd, staplad i ordning.',
68
68
  'files.libraryCount': 'Bibliotek — {count} filer',
69
- 'files.openManager': 'Öppna filhanteraren',
69
+ 'files.openManager': 'Öppna mediebiblioteket',
70
70
  'files.dragToCanvas': 'Dra valfri fil direkt till canvasen för att placera den som ett bildblock.',
71
71
  'layers.structure': 'Struktur',
72
72
  'layers.sectionSingle': 'Sektion',
@@ -82,7 +82,7 @@ export const SV = {
82
82
  'vars.noMatch': 'Ingen variabel matchar det filtret.',
83
83
  'vars.noneDeclared': 'Din applikation har ännu inte deklarerat några variabler.',
84
84
  'vars.rteHint': 'När du redigerar text kan du också välja en variabel direkt från rullgardinsmenyn i den inbäddade verktygsfältet.',
85
- 'modal.fileManager': 'Filhanterare',
85
+ 'modal.fileManager': 'Mediebibliotek',
86
86
  'modal.assetsTitle': 'Resurser',
87
87
  'modal.replaceImage': 'Ersätt bild',
88
88
  'library.searchPlaceholder': 'Sök filer…',
@@ -66,7 +66,7 @@ export const TR = {
66
66
  'Bölüm tabanlı: önce bir düzen bırakın, ardından sütunlarına bloklar bırakın — her bölümün kendi arka planı, kenarlığı, flex veya ızgara ayarları vardır.',
67
67
  'rows.hintStack': 'Serbest yığın: düzen olmadan — bıraktığınız her blok, sırayla istiflenen kendi tam genişlikteki bölümü haline gelir.',
68
68
  'files.libraryCount': 'Kitaplık — {count} dosya',
69
- 'files.openManager': 'Dosya yöneticisini aç',
69
+ 'files.openManager': 'Medya kitaplığını aç',
70
70
  'files.dragToCanvas': 'Görsel blok olarak yerleştirmek için herhangi bir dosyayı doğrudan tuvale sürükleyin.',
71
71
  'layers.structure': 'Yapı',
72
72
  'layers.sectionSingle': 'Bölüm',
@@ -82,7 +82,7 @@ export const TR = {
82
82
  'vars.noMatch': 'Bu filtreyle eşleşen değişken yok.',
83
83
  'vars.noneDeclared': 'Uygulamanız henüz herhangi bir değişken tanımlamadı.',
84
84
  'vars.rteHint': 'Metni düzenlerken, satır içi araç çubuğunun açılır menüsünden doğrudan bir değişken de seçebilirsiniz.',
85
- 'modal.fileManager': 'Dosya yöneticisi',
85
+ 'modal.fileManager': 'Medya kitaplığı',
86
86
  'modal.assetsTitle': 'Varlıklar',
87
87
  'modal.replaceImage': 'Görseli değiştir',
88
88
  'library.searchPlaceholder': 'Dosyaları ara…',
@@ -66,7 +66,7 @@ export const UK = {
66
66
  'На основі секцій: спочатку розмістіть макет, потім блоки в його колонках — кожна секція має власний фон, межу, налаштування flex або сітки.',
67
67
  'rows.hintStack': 'Вільний стек: без макетів — кожен доданий блок стає власною секцією на всю ширину, укладеною по порядку.',
68
68
  'files.libraryCount': 'Бібліотека — {count} файлів',
69
- 'files.openManager': 'Відкрити файловий менеджер',
69
+ 'files.openManager': 'Відкрити медіатеку',
70
70
  'files.dragToCanvas': 'Перетягніть будь-який файл прямо на полотно, щоб розмістити його як блок зображення.',
71
71
  'layers.structure': 'Структура',
72
72
  'layers.sectionSingle': 'Секція',
@@ -82,7 +82,7 @@ export const UK = {
82
82
  'vars.noMatch': 'Немає змінних, що відповідають цьому фільтру.',
83
83
  'vars.noneDeclared': 'Ваш застосунок ще не оголосив жодних змінних.',
84
84
  'vars.rteHint': 'Під час редагування тексту ви також можете вибрати змінну прямо з випадного списку вбудованої панелі інструментів.',
85
- 'modal.fileManager': 'Файловий менеджер',
85
+ 'modal.fileManager': 'Медіатека',
86
86
  'modal.assetsTitle': 'Файли',
87
87
  'modal.replaceImage': 'Замінити зображення',
88
88
  'library.searchPlaceholder': 'Пошук файлів…',
package/src/core/icons.js CHANGED
@@ -235,3 +235,4 @@ export function socialKey(label) {
235
235
  if (l.includes('mail') || l.includes('email')) return 'mail';
236
236
  return 'globe';
237
237
  }
238
+
@@ -442,6 +442,21 @@ function logicMarkersOf(text) {
442
442
  return out;
443
443
  }
444
444
 
445
+ /** Whether an element (or the transparent single-child chain under it) carries its own padding -- the shape the exporter writes for a block's py/px, and a builder section's own spacing. Such a wrapper is one block, never part of a text run. */
446
+ function hasOwnRunPad(el) {
447
+ let e = el;
448
+ if (e.style && paddingOf(e.style)) return true;
449
+ while (
450
+ e.children && e.children.length === 1
451
+ && !INLINE_TAGS.test(e.firstElementChild.tagName)
452
+ && (e.textContent || '') === (e.firstElementChild.textContent || '')
453
+ ) {
454
+ e = e.firstElementChild;
455
+ if (e.style && paddingOf(e.style)) return true;
456
+ }
457
+ return false;
458
+ }
459
+
445
460
  function blocksFromNodes(nodes) {
446
461
  const out = [];
447
462
  let buf = [];
@@ -458,6 +473,11 @@ function blocksFromNodes(nodes) {
458
473
  // first-element-only read never saw. Per-element differences inside
459
474
  // the run survive as the inline styles `cleanImportHtml` now keeps.
460
475
  let baseEl = INLINE_TAGS.test(bufFirstEl.tagName) ? (bufFirstEl.parentElement || bufFirstEl) : bufFirstEl;
476
+ // The run's own spacing: the exporter writes a text block's py/px as
477
+ // padding on the wrapper div the descent below walks straight past
478
+ // (it lands on the <p>), so a padded block came back at the default
479
+ // 10px/0 on every save. First padding on the wrapper chain wins.
480
+ let runPad = paddingOf(baseEl.style);
461
481
  // Then descend through transparent single-child wrappers: builders
462
482
  // nest a `font-family:sans-serif` shim div around the div that
463
483
  // carries the real typography, and `inheritedStyle` below walks *up*
@@ -467,7 +487,8 @@ function blocksFromNodes(nodes) {
467
487
  baseEl.children.length === 1
468
488
  && !INLINE_TAGS.test(baseEl.firstElementChild.tagName)
469
489
  && (baseEl.textContent || '') === (baseEl.firstElementChild.textContent || '')
470
- ) baseEl = baseEl.firstElementChild;
490
+ ) { baseEl = baseEl.firstElementChild; if (!runPad) runPad = paddingOf(baseEl.style); }
491
+ if (runPad) { over.py = runPad.py; over.px = runPad.px; }
471
492
  const size = fontPx(inheritedStyle(baseEl, 'fontSize')); if (size) over.size = size;
472
493
  const color = inheritedStyle(baseEl, 'color'); if (color) over.color = color;
473
494
  over.align = textAlignOf(baseEl);
@@ -513,8 +534,14 @@ function blocksFromNodes(nodes) {
513
534
  return;
514
535
  }
515
536
  if (isStructural(target)) { flush(); out.push(blk('html', { code: n.outerHTML })); return; }
537
+ // A wrapper carrying its own padding is a block boundary, not part of a
538
+ // run: the exporter writes every text block as exactly such a padded div,
539
+ // and buffering two of them together merged neighbouring blocks into one
540
+ // -- the second lost its padding, size, everything -- on every save.
541
+ if (n.nodeType === 1 && !INLINE_TAGS.test(target.tagName) && hasOwnRunPad(target) && buf.length) flush();
516
542
  if (!bufFirstEl) bufFirstEl = target;
517
543
  buf.push(n.outerHTML);
544
+ if (n.nodeType === 1 && !INLINE_TAGS.test(target.tagName) && hasOwnRunPad(target)) flush();
518
545
  });
519
546
  flush();
520
547
  return out;
@@ -570,16 +597,26 @@ function isPassthroughTable(tb) {
570
597
  return cells.length === 1;
571
598
  }
572
599
 
600
+ /** CSSOM hands colors back as `rgb(r, g, b)` even when the source (and the
601
+ * user's own value) was hex -- so every save visibly rewrote colour fields.
602
+ * Solid rgb() flattens back to hex; anything else (rgba, keywords,
603
+ * `transparent`) passes through untouched. */
604
+ function hexOf(value) {
605
+ const m = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/.exec(String(value == null ? '' : value).trim());
606
+ if (!m) return value;
607
+ return '#' + [m[1], m[2], m[3]].map((n) => Number(n).toString(16).padStart(2, '0')).join('');
608
+ }
609
+
573
610
  function bgOf(el) {
574
611
  const st = el.style;
575
- if (st && st.backgroundColor) return st.backgroundColor;
612
+ if (st && st.backgroundColor) return hexOf(st.backgroundColor);
576
613
  // The `background` shorthand is only a usable *color* when it holds no
577
614
  // image/gradient -- browsers populate `backgroundColor` from a shorthand
578
615
  // that includes a color, so reaching here with a url() means there is no
579
616
  // color to take (and taking the raw string set broken values).
580
617
  const short = (st && st.background) || '';
581
- if (short && short.indexOf('url(') < 0 && short.indexOf('gradient(') < 0) return short;
582
- return (el.getAttribute && el.getAttribute('bgcolor')) || '';
618
+ if (short && short.indexOf('url(') < 0 && short.indexOf('gradient(') < 0) return hexOf(short);
619
+ return hexOf((el.getAttribute && el.getAttribute('bgcolor')) || '');
583
620
  }
584
621
 
585
622
  function padOf(el) {
@@ -604,7 +641,7 @@ function applyPad(rows, pad) {
604
641
 
605
642
  /** Longhand side colors are always a single value; the `borderColor` shorthand reads back as a per-side list when the sides differ (`rgb(...) rgb(...) currentcolor`), which is not a usable color. */
606
643
  function borderColorOf(st) {
607
- const ok = (v) => (v && v !== 'currentcolor' ? v : '');
644
+ const ok = (v) => (v && v !== 'currentcolor' ? hexOf(v) : '');
608
645
  return ok(st.borderTopColor) || ok(st.borderLeftColor) || ok(st.borderRightColor) || ok(st.borderBottomColor) || '';
609
646
  }
610
647
 
@@ -940,7 +977,7 @@ function collectRows(nodes) {
940
977
  function themeFromParsedDoc(doc) {
941
978
  const theme = {};
942
979
  const body = doc.body;
943
- let bg = (body.style && (body.style.backgroundColor || body.style.background)) || body.getAttribute('bgcolor') || '';
980
+ let bg = hexOf((body.style && (body.style.backgroundColor || body.style.background)) || body.getAttribute('bgcolor') || '');
944
981
  if (!bg) {
945
982
  const outer = body.querySelector('table');
946
983
  if (outer) bg = bgOf(outer) || (outer.querySelector('td') ? bgOf(outer.querySelector('td')) : '');
@@ -986,12 +1023,26 @@ function themeFromParsedDoc(doc) {
986
1023
  theme.borderStyle = borderStyleOf(content.style);
987
1024
  theme.borderColor = borderColorOf(content.style) || '';
988
1025
  }
1026
+ const sh = content.style && content.style.boxShadow;
1027
+ if (sh && sh !== 'none') theme.shadow = sh;
1028
+ // CLAIMED MEANS CONSUMED. These styles now live on the theme; leaving
1029
+ // them on the node let the row walker's card-folding (applyFrame /
1030
+ // applyPad) absorb the very same border, radius, shadow and page
1031
+ // padding into the rows -- so one save doubled every frame the user
1032
+ // had set at canvas level. htmlToDoc runs this pass before the row
1033
+ // pass for exactly this reason.
1034
+ if (content.style) {
1035
+ content.style.border = '';
1036
+ content.style.borderRadius = '';
1037
+ content.style.boxShadow = '';
1038
+ }
989
1039
  const cell = content.closest ? content.closest('td') : null;
990
1040
  if (cell && cell.style) {
991
1041
  const padY = PX(cell.style.paddingTop);
992
1042
  const padX = PX(cell.style.paddingLeft);
993
1043
  if (padY > 0) theme.padY = padY;
994
1044
  if (padX > 0) theme.padX = padX;
1045
+ cell.style.padding = '';
995
1046
  }
996
1047
  }
997
1048
  }
@@ -1018,8 +1069,8 @@ function themeFromParsedDoc(doc) {
1018
1069
  linkCounts[c] = (linkCounts[c] || 0) + 1;
1019
1070
  });
1020
1071
  const link = Object.keys(linkCounts).sort((a, b) => linkCounts[b] - linkCounts[a])[0];
1021
- if (link) theme.link = link;
1022
- if (body.style && body.style.color) theme.text = body.style.color;
1072
+ if (link) theme.link = hexOf(link);
1073
+ if (body.style && body.style.color) theme.text = hexOf(body.style.color);
1023
1074
  return theme;
1024
1075
  }
1025
1076
 
@@ -1057,7 +1108,10 @@ export function htmlToDoc(src) {
1057
1108
  // (never-inlined exports, hand-written emails) classify like inlined ones.
1058
1109
  // Best-effort: a pathological stylesheet must never block the import.
1059
1110
  try { inlineStylesheets(doc); } catch { /* proceed with inline styles only */ }
1060
- return { rows: collectRows(Array.from(doc.body.childNodes)), theme: themeFromParsedDoc(doc) };
1111
+ // Theme first: themeFromParsedDoc consumes the styles it claims off the
1112
+ // scaffold nodes, and the row walker must see the cleaned DOM.
1113
+ const theme = themeFromParsedDoc(doc);
1114
+ return { rows: collectRows(Array.from(doc.body.childNodes)), theme };
1061
1115
  }
1062
1116
 
1063
1117
  export function htmlToRows(src) {
@@ -131,6 +131,40 @@ export const cssUrl = (u) => {
131
131
  return raw.replace(/["'()\\]|\s/g, (c) => CSS_URL_ESCAPE[c] || '%20');
132
132
  };
133
133
 
134
+ /**
135
+ * A link target on its way into an anchor. Two jobs, and both of them showed
136
+ * up as "the link does not work" rather than as anything that errors:
137
+ *
138
+ * - A bare host (`selise.ch`, `www.selise.ch/pricing`) gets `https://`.
139
+ * Typed with no scheme it is a *relative* URL, so the mail client
140
+ * resolves it against its own origin and the click lands nowhere -- the
141
+ * single most common way an authored link ships broken, and invisible in
142
+ * the editor because the canvas never follows it.
143
+ * - Schemes are allowlisted the way `cssUrl` allowlists image sources, so
144
+ * `javascript:` resolves to nothing instead of becoming a click target.
145
+ *
146
+ * Left exactly as typed: anything already carrying an allowed scheme, an
147
+ * in-page `#anchor`, a rooted `/path`, and anything opening with a merge tag
148
+ * -- `{{ResetUrl}}` is a whole URL the host substitutes later, so prefixing
149
+ * it would corrupt what reaches the template engine. A bare `name@host.tld`
150
+ * becomes `mailto:`, which is the only thing it could have meant.
151
+ */
152
+ export const linkHref = (u) => {
153
+ const raw = String(u == null ? '' : u).trim();
154
+ if (!raw) return '';
155
+ const scheme = raw.match(/^([a-z][a-z0-9+.-]*):/i);
156
+ if (scheme) {
157
+ if (/^(https?|mailto|tel)$/i.test(scheme[1])) return raw;
158
+ // `selise.ch:8080/x` matches the scheme shape but is a host and a port;
159
+ // it falls through to be prefixed rather than dropped.
160
+ if (!/^[a-z][a-z0-9+.-]*:\d/i.test(raw)) return '';
161
+ }
162
+ if (raw.startsWith('//')) return 'https:' + raw;
163
+ if (/^[#/]/.test(raw) || raw.startsWith('{{')) return raw;
164
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw)) return 'mailto:' + raw;
165
+ return 'https://' + raw;
166
+ };
167
+
134
168
  export const escHtml = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
135
169
 
136
170
  /** Minimal HTML syntax highlighter for the code view. */
package/src/core/theme.js CHANGED
@@ -12,4 +12,4 @@
12
12
  * accept the literal `transparent` (and any rgba()/#rrggbbaa value) as well
13
13
  * as a hex colour.
14
14
  */
15
- export const THEME = () => ({ bg: '#eef2f7', contentBg: '#ffffff', width: 620, padY: 0, padX: 0, radius: 0, borderW: 0, borderStyle: 'solid', borderColor: '#e2e2e5', font: '"Helvetica Neue", Helvetica, Arial, sans-serif', text: '#172033', link: '#0065b3' });
15
+ export const THEME = () => ({ bg: '#eef2f7', contentBg: '#ffffff', width: 620, padY: 0, padX: 0, radius: 0, borderW: 0, borderStyle: 'solid', borderColor: '#e2e2e5', shadow: '', font: '"Helvetica Neue", Helvetica, Arial, sans-serif', text: '#172033', link: '#0065b3' });
@@ -6,6 +6,7 @@ import { renderDoc } from './render/canvas.js';
6
6
  import { renderRte } from './render/rte.js';
7
7
  import { renderField, renderFieldCards, typeCommit } from './render/fields.js';
8
8
  import { icon } from './core/icons.js';
9
+ import { brandLockup } from './core/brand.js';
9
10
  import { TOKEN } from './core/variables.js';
10
11
  import { hl, cssUrl } from './core/sanitize.js';
11
12
  import { decorateLogicTags } from './core/export.js';
@@ -494,7 +495,15 @@ export class MailCraftEditor extends ElementBase {
494
495
  async copyScreenshot(blob) {
495
496
  const t = this.core.t;
496
497
  try {
497
- const png = blob || await this.screenshotPng();
498
+ // Safari only honours `clipboard.write()` while the user gesture is
499
+ // still live, and awaiting the capture first (per-image fetch +
500
+ // inline, decode, rasterize, PNG encode) always outlives it --
501
+ // NotAllowedError on every attempt, where Chromium is lenient and
502
+ // lets it through. Handing ClipboardItem the *promise* is the
503
+ // supported shape for exactly this: the write is issued
504
+ // synchronously with the gesture and the capture resolves inside
505
+ // it. Chromium accepts the same form, so there is no branch here.
506
+ const png = blob ? Promise.resolve(blob) : this.screenshotPng();
498
507
  await navigator.clipboard.write([new ClipboardItem({ 'image/png': png })]);
499
508
  this.core.flash(t('toast.pngCopied'));
500
509
  } catch {
@@ -569,15 +578,11 @@ export class MailCraftEditor extends ElementBase {
569
578
  const t = this.core.t;
570
579
  const header = elS('header', "display: flex; align-items: center; gap: 14px; padding: 0 16px; border-bottom: 1px solid var(--ed-line); background: linear-gradient(to bottom, var(--ed-panel), var(--ed-panel-2)); position: relative; z-index: 30;", { class: 'mc-header' });
571
580
 
572
- const brandMark = elS('span', '', { class: 'mc-brand-mark' });
573
- const markSvg = icon('mailSpark', 20);
574
- markSvg.setAttribute('stroke-width', '1.8');
575
- brandMark.appendChild(markSvg);
576
- const brand = elS('div', 'display: flex; align-items: center; gap: 9px;');
577
- brand.append(
578
- brandMark,
579
- elS('span', 'font-family: var(--ed-font); font-weight: 600; font-size: 15px; letter-spacing: -0.01em; line-height: 1;', { text: 'MailCraft', class: 'mc-brand-name' }),
580
- );
581
+ // The logo itself, not a mark beside a typeset word: the wordmark is part
582
+ // of the artwork, so it keeps the brand's own typeface whatever font the
583
+ // host pushes into --ed-font.
584
+ const brand = elS('div', 'display: flex; align-items: center;', { class: 'mc-brand' });
585
+ brand.appendChild(brandLockup(30));
581
586
  if (on.logo) header.append(brand, elS('div', 'width: 1px; height: 22px; background: var(--ed-line);'));
582
587
 
583
588
  this.savedLabel = elS('span', 'display: flex; align-items: center; gap: 6px; font-family: ui-monospace, monospace; font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ed-faint); white-space: nowrap;');
@@ -1224,6 +1229,11 @@ export class MailCraftEditor extends ElementBase {
1224
1229
  tile.addEventListener('mouseleave', () => { tile.style.borderColor = 'var(--ed-line)'; });
1225
1230
  tile.addEventListener('dragstart', this.core.startDrag({ kind: 'asset', assetId: a.id }));
1226
1231
  tile.addEventListener('dragend', () => { this.core.drag = null; this.core.setState({ drop: null, rowDrop: null }); });
1232
+ // Touch Safari never fires HTML5 drag events, so drag alone left
1233
+ // these tiles unusable on iOS. Click is the same fallback the
1234
+ // library modal's tiles already carry; a completed drag ends in
1235
+ // `dragend`, not `click`, so the two do not both fire.
1236
+ tile.addEventListener('click', () => this.core.useAsset(a));
1227
1237
  tile.appendChild(elS('div', `width: 100%; height: 64px; background-image: url("${a.url}"); background-size: cover; background-position: center; background-color: var(--ed-work);`, { role: 'img', 'aria-label': a.name }));
1228
1238
  tile.appendChild(elS('div', 'font-family: ui-monospace, monospace; font-size: 9px; color: var(--ed-muted); padding: 4px 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;', { text: a.name }));
1229
1239
  grid.appendChild(tile);
@@ -1,6 +1,7 @@
1
1
  import { icon, brandIcon, socialKey, SOCIAL_BRAND, contrastInk } from '../core/icons.js';
2
2
  import { pad } from '../core/layout-style.js';
3
3
  import { parseItems, cellsOf } from '../core/parse.js';
4
+ import { linkHref } from '../core/sanitize.js';
4
5
 
5
6
  function el(tag, style, attrs) {
6
7
  const node = document.createElement(tag);
@@ -77,12 +78,17 @@ export function blockBody(b, theme, live, ctx) {
77
78
  }
78
79
  case 'image': {
79
80
  const wrap = el('div', { padding: pad(p), textAlign: p.align, fontSize: '0' }, attr);
80
- if (p.href) {
81
+ const imgHref = linkHref(p.href);
82
+ if (imgHref) {
81
83
  // The % width must live on the anchor, not the img: a percentage on a
82
84
  // child of a shrink-to-fit inline-block resolves against the image's
83
85
  // own intrinsic size (i.e. not at all), which rendered every linked
84
86
  // logo/icon at full intrinsic width no matter what `width` said.
85
- const a = el('a', { display: 'inline-block', width: p.width + '%' }, { href: p.href });
87
+ const a = el('a', { display: 'inline-block', width: p.width + '%' }, { href: imgHref });
88
+ // Same guard as every other anchor the canvas draws: without it a
89
+ // click on a linked logo navigates the host application away from the
90
+ // editor, taking the uncommitted document with it.
91
+ a.addEventListener('click', (e) => e.preventDefault());
86
92
  a.appendChild(el('img', { width: '100%', borderRadius: p.radius + 'px', display: 'block', border: '0' }, { src: p.src, alt: p.alt }));
87
93
  wrap.appendChild(a);
88
94
  } else {
@@ -100,7 +106,7 @@ export function blockBody(b, theme, live, ctx) {
100
106
  // email pattern; the color falls back to the label color so a bare
101
107
  // "outline thickness" bump looks right without a second step.
102
108
  border: p.borderW ? p.borderW + 'px ' + (p.borderStyle || 'solid') + ' ' + (p.borderColor || p.color) : '0',
103
- }, { href: p.href, ...editableAttrs(live), text: p.label });
109
+ }, { href: linkHref(p.href), ...editableAttrs(live), text: p.label });
104
110
  a.addEventListener('click', (e) => e.preventDefault());
105
111
  // Button editing is deliberately plain: no focus-tracked `editing` state, no
106
112
  // paste handling, no floating RTE toolbar -- only its label commits on blur.
@@ -139,7 +145,7 @@ export function blockBody(b, theme, live, ctx) {
139
145
  borderRadius: shape === 'circle' ? '50%' : shape === 'square' ? Math.round(box * 0.28) + 'px' : '0',
140
146
  color: iconColor, background: badge ? source : 'transparent',
141
147
  border: shape === 'outline' ? '1px solid ' + source : '0',
142
- }, { href: it.href, title: it.label });
148
+ }, { href: linkHref(it.href), title: it.label });
143
149
  a.addEventListener('click', (e) => e.preventDefault());
144
150
  let provided = null;
145
151
  if (ctx && ctx.iconProvider) { try { provided = ctx.iconProvider(key, { label: it.label, size: p.size, color: iconColor }); } catch { provided = null; } }
@@ -154,7 +160,7 @@ export function blockBody(b, theme, live, ctx) {
154
160
  }
155
161
  case 'video': {
156
162
  const wrap = el('div', { padding: '4px 0', textAlign: 'center' }, attr);
157
- const a = el('a', { display: 'block', position: 'relative' }, { href: p.href });
163
+ const a = el('a', { display: 'block', position: 'relative' }, { href: linkHref(p.href) });
158
164
  a.addEventListener('click', (e) => e.preventDefault());
159
165
  a.appendChild(el('img', { width: '100%', display: 'block', border: '0' }, { src: p.src, alt: p.caption }));
160
166
  const badge = el('span', { position: 'absolute', inset: '0', display: 'flex', alignItems: 'center', justifyContent: 'center' });
@@ -197,7 +203,7 @@ export function blockBody(b, theme, live, ctx) {
197
203
  case 'menu': {
198
204
  const wrap = el('div', { textAlign: p.align, padding: '10px 0' }, attr);
199
205
  parseItems(p.items).forEach((it) => {
200
- const a = el('a', { color: p.color, fontSize: p.size + 'px', fontFamily: p.fontFamily || t.font, textDecoration: 'none', margin: '0 ' + p.gap / 2 + 'px', letterSpacing: '0.12em', textTransform: 'uppercase' }, { href: it.href, text: it.label });
206
+ const a = el('a', { color: p.color, fontSize: p.size + 'px', fontFamily: p.fontFamily || t.font, textDecoration: 'none', margin: '0 ' + p.gap / 2 + 'px', letterSpacing: '0.12em', textTransform: 'uppercase' }, { href: linkHref(it.href), text: it.label });
201
207
  a.addEventListener('click', (e) => e.preventDefault());
202
208
  wrap.appendChild(a);
203
209
  });