@seliseblocks/mailcraft 0.2.6 → 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.
- package/DOCS.md +4 -3
- package/README.md +4 -1
- package/README.md.txt +4 -1
- package/dist/mailcraft-editor.bundle.js +52 -53
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/examples/vanilla.html +19 -18
- package/package.json +1 -1
- package/src/core/brand.js +81 -0
- package/src/core/editor-core.js +35 -8
- package/src/core/i18n/ar.js +2 -2
- package/src/core/i18n/bg.js +2 -2
- package/src/core/i18n/bn.js +2 -2
- package/src/core/i18n/ca.js +2 -2
- package/src/core/i18n/cs.js +2 -2
- package/src/core/i18n/da.js +2 -2
- package/src/core/i18n/de-CH.js +2 -2
- package/src/core/i18n/de.js +2 -2
- package/src/core/i18n/dz.js +2 -2
- package/src/core/i18n/el.js +2 -2
- package/src/core/i18n/en.js +2 -2
- package/src/core/i18n/es.js +2 -2
- package/src/core/i18n/et.js +2 -2
- package/src/core/i18n/fi.js +2 -2
- package/src/core/i18n/fr.js +2 -2
- package/src/core/i18n/hr.js +2 -2
- package/src/core/i18n/hu.js +2 -2
- package/src/core/i18n/it.js +2 -2
- package/src/core/i18n/lt.js +2 -2
- package/src/core/i18n/lv.js +2 -2
- package/src/core/i18n/nb.js +2 -2
- package/src/core/i18n/nl.js +2 -2
- package/src/core/i18n/pl.js +2 -2
- package/src/core/i18n/pt.js +2 -2
- package/src/core/i18n/ro.js +2 -2
- package/src/core/i18n/ru.js +2 -2
- package/src/core/i18n/sk.js +2 -2
- package/src/core/i18n/sl.js +2 -2
- package/src/core/i18n/sv.js +2 -2
- package/src/core/i18n/tr.js +2 -2
- package/src/core/i18n/uk.js +2 -2
- package/src/core/icons.js +1 -0
- package/src/core/sanitize.js +34 -0
- package/src/mailcraft-editor.js +20 -10
- package/src/render/block-body.js +12 -6
- package/src/render/canvas.js +5 -1
- package/src/render/fields.js +9 -1
- package/src/render/screenshot.js +13 -2
- package/src/render/style.js +10 -11
package/src/core/i18n/lt.js
CHANGED
|
@@ -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
|
|
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': '
|
|
85
|
+
'modal.fileManager': 'Medijos biblioteka',
|
|
86
86
|
'modal.assetsTitle': 'Ištekliai',
|
|
87
87
|
'modal.replaceImage': 'Pakeisti vaizdą',
|
|
88
88
|
'library.searchPlaceholder': 'Ieškoti failų…',
|
package/src/core/i18n/lv.js
CHANGED
|
@@ -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
|
|
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': '
|
|
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…',
|
package/src/core/i18n/nb.js
CHANGED
|
@@ -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
|
|
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': '
|
|
85
|
+
'modal.fileManager': 'Mediebibliotek',
|
|
86
86
|
'modal.assetsTitle': 'Ressurser',
|
|
87
87
|
'modal.replaceImage': 'Erstatt bilde',
|
|
88
88
|
'library.searchPlaceholder': 'Søk i filer…',
|
package/src/core/i18n/nl.js
CHANGED
|
@@ -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': '
|
|
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': '
|
|
85
|
+
'modal.fileManager': 'Mediabibliotheek',
|
|
86
86
|
'modal.assetsTitle': 'Bestanden',
|
|
87
87
|
'modal.replaceImage': 'Afbeelding vervangen',
|
|
88
88
|
'library.searchPlaceholder': 'Bestanden zoeken…',
|
package/src/core/i18n/pl.js
CHANGED
|
@@ -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
|
|
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': '
|
|
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…',
|
package/src/core/i18n/pt.js
CHANGED
|
@@ -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
|
|
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': '
|
|
85
|
+
'modal.fileManager': 'Biblioteca de mídia',
|
|
86
86
|
'modal.assetsTitle': 'Recursos',
|
|
87
87
|
'modal.replaceImage': 'Substituir imagem',
|
|
88
88
|
'library.searchPlaceholder': 'Pesquisar arquivos…',
|
package/src/core/i18n/ro.js
CHANGED
|
@@ -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
|
|
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': '
|
|
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…',
|
package/src/core/i18n/ru.js
CHANGED
|
@@ -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': 'Поиск файлов…',
|
package/src/core/i18n/sk.js
CHANGED
|
@@ -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ť
|
|
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': '
|
|
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…',
|
package/src/core/i18n/sl.js
CHANGED
|
@@ -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
|
|
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': '
|
|
85
|
+
'modal.fileManager': 'Predstavnostna knjižnica',
|
|
86
86
|
'modal.assetsTitle': 'Viri',
|
|
87
87
|
'modal.replaceImage': 'Zamenjaj sliko',
|
|
88
88
|
'library.searchPlaceholder': 'Iskanje datotek…',
|
package/src/core/i18n/sv.js
CHANGED
|
@@ -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
|
|
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': '
|
|
85
|
+
'modal.fileManager': 'Mediebibliotek',
|
|
86
86
|
'modal.assetsTitle': 'Resurser',
|
|
87
87
|
'modal.replaceImage': 'Ersätt bild',
|
|
88
88
|
'library.searchPlaceholder': 'Sök filer…',
|
package/src/core/i18n/tr.js
CHANGED
|
@@ -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': '
|
|
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': '
|
|
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…',
|
package/src/core/i18n/uk.js
CHANGED
|
@@ -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
package/src/core/sanitize.js
CHANGED
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
135
169
|
|
|
136
170
|
/** Minimal HTML syntax highlighter for the code view. */
|
package/src/mailcraft-editor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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);
|
package/src/render/block-body.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
});
|
package/src/render/canvas.js
CHANGED
|
@@ -231,7 +231,11 @@ export function renderDoc(core, live) {
|
|
|
231
231
|
showLine(rowDropTracker, rowLines[index]);
|
|
232
232
|
});
|
|
233
233
|
page.addEventListener('dragleave', (e) => { if (e.target === root || e.target === page) hideActive(rowDropTracker); });
|
|
234
|
-
|
|
234
|
+
// Measured against `root`, exactly as the line above it was: the slots are
|
|
235
|
+
// the sheet's children, not the page's, and deriving the index a second
|
|
236
|
+
// time from the event's own target is what used to send every dropped
|
|
237
|
+
// section to index 0 while the line sat where the pointer actually was.
|
|
238
|
+
page.addEventListener('drop', (e) => { hideActive(rowDropTracker); core.canvasDrop(e, core.indexFromPoint(root, e.clientY)); });
|
|
235
239
|
}
|
|
236
240
|
|
|
237
241
|
d.rows.forEach((r, ri) => {
|
package/src/render/fields.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { icon, brandIcon, socialKey, SOCIAL_BRAND, contrastInk } from '../core/icons.js';
|
|
2
2
|
import { parseItems } from '../core/parse.js';
|
|
3
|
+
import { linkHref } from '../core/sanitize.js';
|
|
3
4
|
|
|
4
5
|
function el(tag, style, attrs) {
|
|
5
6
|
const node = document.createElement(tag);
|
|
@@ -226,7 +227,14 @@ function renderRichLinks(f) {
|
|
|
226
227
|
const urlCommit = typeCommit((v) => change(i, (a) => a.setAttribute('href', v)));
|
|
227
228
|
url.addEventListener('input', (e) => urlCommit.call(e.target.value));
|
|
228
229
|
url.addEventListener('focus', () => { url.style.borderColor = 'var(--ed-accent)'; url.style.outline = 'none'; });
|
|
229
|
-
url.addEventListener('blur', () => {
|
|
230
|
+
url.addEventListener('blur', () => {
|
|
231
|
+
url.style.borderColor = 'var(--ed-line)';
|
|
232
|
+
// Scheme normalization happens on the way out, never per keystroke: a
|
|
233
|
+
// half-typed "h" rewritten to "https://h" under the caret is unusable.
|
|
234
|
+
url.value = linkHref(url.value);
|
|
235
|
+
urlCommit.call(url.value);
|
|
236
|
+
urlCommit.flush();
|
|
237
|
+
});
|
|
230
238
|
|
|
231
239
|
const targetLabel = el('label', { display: 'flex', alignItems: 'center', gap: '6px', marginTop: '7px', width: 'fit-content', color: 'var(--ed-muted)', cursor: 'pointer', fontFamily: 'var(--ed-font)', fontSize: '10.5px', fontWeight: '500' });
|
|
232
240
|
const target = el('input', { accentColor: 'var(--ed-accent)', cursor: 'pointer' }, { type: 'checkbox', 'data-focus-key': `f${f.key}-link-target-${i}` });
|
package/src/render/screenshot.js
CHANGED
|
@@ -124,9 +124,20 @@ export const captureTemplatePng = async function (core, mountInto, scale = 2) {
|
|
|
124
124
|
img.onerror = () => reject(new Error('SVG rasterization failed'));
|
|
125
125
|
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
|
|
126
126
|
});
|
|
127
|
+
// WebKit fires `load` on an SVG-as-image before it is reliably
|
|
128
|
+
// drawable, and drawing on that tick yields a blank or half-laid-out
|
|
129
|
+
// frame. Same `decode()` guard the inner images get above.
|
|
130
|
+
if (img.decode) await img.decode().catch(() => {});
|
|
131
|
+
// Safari caps total canvas area, and past the cap it hands back a
|
|
132
|
+
// *blank* canvas that still encodes successfully -- a silently empty
|
|
133
|
+
// PNG with nothing thrown to catch. Long newsletters reach it easily
|
|
134
|
+
// at 2x (a 680x4000 template is 10.9M device pixels). Scale the
|
|
135
|
+
// capture down to fit instead of returning an empty image.
|
|
136
|
+
const MAX_CANVAS_AREA = 16e6;
|
|
137
|
+
const fit = Math.min(scale, Math.sqrt(MAX_CANVAS_AREA / Math.max(1, w * h)));
|
|
127
138
|
const canvas = document.createElement('canvas');
|
|
128
|
-
canvas.width = Math.max(1, Math.round(w *
|
|
129
|
-
canvas.height = Math.max(1, Math.round(h *
|
|
139
|
+
canvas.width = Math.max(1, Math.round(w * fit));
|
|
140
|
+
canvas.height = Math.max(1, Math.round(h * fit));
|
|
130
141
|
const ctx = canvas.getContext('2d');
|
|
131
142
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
132
143
|
return await new Promise((resolve, reject) => {
|
package/src/render/style.js
CHANGED
|
@@ -23,6 +23,10 @@ export const STYLE = `
|
|
|
23
23
|
/* The accent as it must read on the email sheet, which is a white page in
|
|
24
24
|
both themes -- so this family does NOT flip with the chrome (see the dark
|
|
25
25
|
palette below, where it is deliberately not overridden). */
|
|
26
|
+
/* Logo ink. Deliberately literal rather than accent-derived: the host can
|
|
27
|
+
repaint the chrome through the brand attribute, and a logo that follows
|
|
28
|
+
it stops being the logo. Values are the official light artwork. */
|
|
29
|
+
--ed-logo-mark: #2D86C7; --ed-logo-text: #3C3939;
|
|
26
30
|
--ed-accent-sheet: #0065b3; --ed-accent-sheet-strong: #004f8c; --ed-accent-sheet-ink: #ffffff;
|
|
27
31
|
--ed-accent-sheet-line: rgba(0,101,179,0.5);
|
|
28
32
|
--ed-grid: rgba(71,85,105,0.10);
|
|
@@ -43,6 +47,8 @@ export const STYLE = `
|
|
|
43
47
|
--ed-panel-label: #e2e8f0; --ed-panel-value: #c0cad8; --ed-panel-meta: #8d9aae;
|
|
44
48
|
--ed-accent: #58a8e3; --ed-accent-strong: #8fc4ec; --ed-accent-ink: #08111f; --ed-soft: rgba(88,168,227,0.13);
|
|
45
49
|
--ed-accent-tint: #8fcdf2; --ed-glow: rgba(88,168,227,0.28); --ed-select: rgba(88,168,227,0.3);
|
|
50
|
+
/* The official dark-mode logo artwork: mark and wordmark both go light. */
|
|
51
|
+
--ed-logo-mark: #F2F2F2; --ed-logo-text: #EEEEEE;
|
|
46
52
|
--ed-grid: rgba(148,163,184,0.08);
|
|
47
53
|
--ed-danger: #f2777c; --ed-danger-soft: rgba(242,119,124,0.14);
|
|
48
54
|
--ed-success: #4ade95; --ed-success-soft: rgba(74,222,149,0.14);
|
|
@@ -151,14 +157,9 @@ export const STYLE = `
|
|
|
151
157
|
.mc-footer-link { cursor: pointer; transition: color 0.14s ease; }
|
|
152
158
|
.mc-footer-link:hover { color: var(--ed-accent); text-decoration: underline; }
|
|
153
159
|
|
|
154
|
-
/*
|
|
155
|
-
.
|
|
156
|
-
|
|
157
|
-
width: 26px; height: 26px; border-radius: 8px;
|
|
158
|
-
background: linear-gradient(145deg, var(--ed-accent-tint), var(--ed-accent) 55%, var(--ed-accent-strong));
|
|
159
|
-
box-shadow: 0 5px 14px var(--ed-glow);
|
|
160
|
-
color: #ffffff;
|
|
161
|
-
}
|
|
160
|
+
/* The header logo. It is the official lockup artwork, so it gets no chip, no
|
|
161
|
+
gradient and no recolouring -- only a height. */
|
|
162
|
+
.mc-brand { display: flex; align-items: center; flex: none; }
|
|
162
163
|
|
|
163
164
|
/*
|
|
164
165
|
* Shared segmented-control shell -- one bordered/backed track that owns the
|
|
@@ -269,9 +270,7 @@ export const STYLE = `
|
|
|
269
270
|
/* Opaque, no backdrop-filter: the header sits over the scrolling canvas, so a
|
|
270
271
|
backdrop blur re-composites every scroll frame -- visible scroll jank. */
|
|
271
272
|
#mc .mc-header { padding: 0 18px !important; background: var(--ed-panel) !important; }
|
|
272
|
-
#mc .mc-brand
|
|
273
|
-
#mc .mc-brand-mark svg { width: 20px; height: 20px; display: block; color: #fff; }
|
|
274
|
-
#mc .mc-brand-name { font-family: var(--ed-font) !important; font-size: 17px !important; letter-spacing: -.03em !important; }
|
|
273
|
+
#mc .mc-brand svg { height: 34px; width: auto; display: block; }
|
|
275
274
|
#mc .mc-header button { font-family: var(--ed-font) !important; font-size: 10.5px !important; font-weight: 600; letter-spacing: .015em !important; text-transform: none !important; }
|
|
276
275
|
#mc .mc-header > div { border-radius: 10px; }
|
|
277
276
|
#mc .mc-segment { gap: 3px !important; padding: 3px !important; border: 0 !important; border-radius: 10px; background: var(--ed-panel-2) !important; }
|