@jjlmoya/utils-developer 1.2.0
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/package.json +64 -0
- package/src/category/i18n/en.ts +34 -0
- package/src/category/i18n/es.ts +34 -0
- package/src/category/i18n/fr.ts +34 -0
- package/src/category/index.ts +31 -0
- package/src/category/seo.astro +15 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +11 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +78 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +146 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/css_specificity_logic.test.ts +39 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/locale_completeness.test.ts +42 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/schemas_fulfillment.test.ts +23 -0
- package/src/tests/seo_length.test.ts +22 -0
- package/src/tests/title_quality.test.ts +55 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/aspectRatio/bibliography.astro +14 -0
- package/src/tool/aspectRatio/component.astro +468 -0
- package/src/tool/aspectRatio/i18n/en.ts +177 -0
- package/src/tool/aspectRatio/i18n/es.ts +177 -0
- package/src/tool/aspectRatio/i18n/fr.ts +177 -0
- package/src/tool/aspectRatio/index.ts +34 -0
- package/src/tool/aspectRatio/seo.astro +14 -0
- package/src/tool/aspectRatio/ui.ts +12 -0
- package/src/tool/calculadoraTiempoDatos/bibliography.astro +14 -0
- package/src/tool/calculadoraTiempoDatos/component.astro +829 -0
- package/src/tool/calculadoraTiempoDatos/i18n/en.ts +150 -0
- package/src/tool/calculadoraTiempoDatos/i18n/es.ts +163 -0
- package/src/tool/calculadoraTiempoDatos/i18n/fr.ts +150 -0
- package/src/tool/calculadoraTiempoDatos/index.ts +33 -0
- package/src/tool/calculadoraTiempoDatos/seo.astro +14 -0
- package/src/tool/calculadoraTiempoDatos/ui.ts +24 -0
- package/src/tool/colorConverter/bibliography.astro +14 -0
- package/src/tool/colorConverter/component.astro +504 -0
- package/src/tool/colorConverter/i18n/en.ts +126 -0
- package/src/tool/colorConverter/i18n/es.ts +126 -0
- package/src/tool/colorConverter/i18n/fr.ts +126 -0
- package/src/tool/colorConverter/index.ts +33 -0
- package/src/tool/colorConverter/seo.astro +14 -0
- package/src/tool/colorConverter/ui.ts +16 -0
- package/src/tool/conversorExcelCsvHtml/bibliography.astro +14 -0
- package/src/tool/conversorExcelCsvHtml/component.astro +595 -0
- package/src/tool/conversorExcelCsvHtml/i18n/en.ts +129 -0
- package/src/tool/conversorExcelCsvHtml/i18n/es.ts +129 -0
- package/src/tool/conversorExcelCsvHtml/i18n/fr.ts +129 -0
- package/src/tool/conversorExcelCsvHtml/index.ts +33 -0
- package/src/tool/conversorExcelCsvHtml/seo.astro +14 -0
- package/src/tool/conversorExcelCsvHtml/ui.ts +14 -0
- package/src/tool/cronGenerator/bibliography.astro +14 -0
- package/src/tool/cronGenerator/component.astro +667 -0
- package/src/tool/cronGenerator/i18n/en.ts +191 -0
- package/src/tool/cronGenerator/i18n/es.ts +191 -0
- package/src/tool/cronGenerator/i18n/fr.ts +191 -0
- package/src/tool/cronGenerator/index.ts +34 -0
- package/src/tool/cronGenerator/seo.astro +14 -0
- package/src/tool/cronGenerator/ui.ts +45 -0
- package/src/tool/cssSpecificityCalculator/bibliography.astro +14 -0
- package/src/tool/cssSpecificityCalculator/component.astro +292 -0
- package/src/tool/cssSpecificityCalculator/i18n/en.ts +168 -0
- package/src/tool/cssSpecificityCalculator/i18n/es.ts +168 -0
- package/src/tool/cssSpecificityCalculator/i18n/fr.ts +168 -0
- package/src/tool/cssSpecificityCalculator/index.ts +34 -0
- package/src/tool/cssSpecificityCalculator/logic.ts +100 -0
- package/src/tool/cssSpecificityCalculator/seo.astro +14 -0
- package/src/tool/cssSpecificityCalculator/ui-controller.ts +57 -0
- package/src/tool/cssSpecificityCalculator/ui.ts +11 -0
- package/src/tool/cssToInlineConverter/bibliography.astro +14 -0
- package/src/tool/cssToInlineConverter/component.astro +308 -0
- package/src/tool/cssToInlineConverter/i18n/en.ts +160 -0
- package/src/tool/cssToInlineConverter/i18n/es.ts +160 -0
- package/src/tool/cssToInlineConverter/i18n/fr.ts +160 -0
- package/src/tool/cssToInlineConverter/index.ts +34 -0
- package/src/tool/cssToInlineConverter/seo.astro +14 -0
- package/src/tool/cssToInlineConverter/ui.ts +12 -0
- package/src/tool/duplicateCssRemover/bibliography.astro +14 -0
- package/src/tool/duplicateCssRemover/component.astro +353 -0
- package/src/tool/duplicateCssRemover/i18n/en.ts +167 -0
- package/src/tool/duplicateCssRemover/i18n/es.ts +167 -0
- package/src/tool/duplicateCssRemover/i18n/fr.ts +167 -0
- package/src/tool/duplicateCssRemover/index.ts +34 -0
- package/src/tool/duplicateCssRemover/seo.astro +14 -0
- package/src/tool/duplicateCssRemover/ui.ts +12 -0
- package/src/tool/generadorSecurityTxt/bibliography.astro +14 -0
- package/src/tool/generadorSecurityTxt/component.astro +473 -0
- package/src/tool/generadorSecurityTxt/i18n/en.ts +173 -0
- package/src/tool/generadorSecurityTxt/i18n/es.ts +173 -0
- package/src/tool/generadorSecurityTxt/i18n/fr.ts +173 -0
- package/src/tool/generadorSecurityTxt/index.ts +33 -0
- package/src/tool/generadorSecurityTxt/seo.astro +14 -0
- package/src/tool/generadorSecurityTxt/ui.ts +27 -0
- package/src/tool/hashGenerator/bibliography.astro +14 -0
- package/src/tool/hashGenerator/component.astro +367 -0
- package/src/tool/hashGenerator/i18n/en.ts +118 -0
- package/src/tool/hashGenerator/i18n/es.ts +118 -0
- package/src/tool/hashGenerator/i18n/fr.ts +118 -0
- package/src/tool/hashGenerator/index.ts +33 -0
- package/src/tool/hashGenerator/md5.ts +138 -0
- package/src/tool/hashGenerator/seo.astro +14 -0
- package/src/tool/hashGenerator/ui.ts +11 -0
- package/src/tool/inspectorCertificadosSsl/bibliography.astro +14 -0
- package/src/tool/inspectorCertificadosSsl/component.astro +580 -0
- package/src/tool/inspectorCertificadosSsl/i18n/en.ts +214 -0
- package/src/tool/inspectorCertificadosSsl/i18n/es.ts +214 -0
- package/src/tool/inspectorCertificadosSsl/i18n/fr.ts +214 -0
- package/src/tool/inspectorCertificadosSsl/index.ts +33 -0
- package/src/tool/inspectorCertificadosSsl/seo.astro +14 -0
- package/src/tool/inspectorCertificadosSsl/ui.ts +29 -0
- package/src/tool/jsonFormatter/bibliography.astro +14 -0
- package/src/tool/jsonFormatter/component.astro +590 -0
- package/src/tool/jsonFormatter/i18n/en.ts +156 -0
- package/src/tool/jsonFormatter/i18n/es.ts +156 -0
- package/src/tool/jsonFormatter/i18n/fr.ts +156 -0
- package/src/tool/jsonFormatter/index.ts +34 -0
- package/src/tool/jsonFormatter/seo.astro +14 -0
- package/src/tool/jsonFormatter/ui.ts +15 -0
- package/src/tool/keycode/bibliography.astro +14 -0
- package/src/tool/keycode/component.astro +540 -0
- package/src/tool/keycode/i18n/en.ts +160 -0
- package/src/tool/keycode/i18n/es.ts +160 -0
- package/src/tool/keycode/i18n/fr.ts +160 -0
- package/src/tool/keycode/index.ts +34 -0
- package/src/tool/keycode/seo.astro +14 -0
- package/src/tool/keycode/ui.ts +10 -0
- package/src/tool/llmCostCalculator/bibliography.astro +14 -0
- package/src/tool/llmCostCalculator/component.astro +488 -0
- package/src/tool/llmCostCalculator/data.ts +99 -0
- package/src/tool/llmCostCalculator/i18n/en.ts +165 -0
- package/src/tool/llmCostCalculator/i18n/es.ts +165 -0
- package/src/tool/llmCostCalculator/i18n/fr.ts +165 -0
- package/src/tool/llmCostCalculator/index.ts +34 -0
- package/src/tool/llmCostCalculator/seo.astro +14 -0
- package/src/tool/llmCostCalculator/ui.ts +11 -0
- package/src/tool/mobileMockupGenerator/app.ts +614 -0
- package/src/tool/mobileMockupGenerator/bibliography.astro +14 -0
- package/src/tool/mobileMockupGenerator/component.astro +1044 -0
- package/src/tool/mobileMockupGenerator/constants.ts +48 -0
- package/src/tool/mobileMockupGenerator/i18n/en.ts +121 -0
- package/src/tool/mobileMockupGenerator/i18n/es.ts +121 -0
- package/src/tool/mobileMockupGenerator/i18n/fr.ts +121 -0
- package/src/tool/mobileMockupGenerator/index.ts +33 -0
- package/src/tool/mobileMockupGenerator/rendering.ts +189 -0
- package/src/tool/mobileMockupGenerator/seo.astro +14 -0
- package/src/tool/mobileMockupGenerator/store.ts +145 -0
- package/src/tool/mobileMockupGenerator/types.ts +41 -0
- package/src/tool/mobileMockupGenerator/ui.ts +48 -0
- package/src/tool/musicalTypography/bibliography.astro +14 -0
- package/src/tool/musicalTypography/component.astro +480 -0
- package/src/tool/musicalTypography/i18n/en.ts +179 -0
- package/src/tool/musicalTypography/i18n/es.ts +179 -0
- package/src/tool/musicalTypography/i18n/fr.ts +179 -0
- package/src/tool/musicalTypography/index.ts +33 -0
- package/src/tool/musicalTypography/seo.astro +14 -0
- package/src/tool/musicalTypography/ui.ts +25 -0
- package/src/tool/placeholderGenerator/bibliography.astro +14 -0
- package/src/tool/placeholderGenerator/component.astro +499 -0
- package/src/tool/placeholderGenerator/i18n/en.ts +176 -0
- package/src/tool/placeholderGenerator/i18n/es.ts +176 -0
- package/src/tool/placeholderGenerator/i18n/fr.ts +176 -0
- package/src/tool/placeholderGenerator/index.ts +34 -0
- package/src/tool/placeholderGenerator/seo.astro +14 -0
- package/src/tool/placeholderGenerator/ui.ts +14 -0
- package/src/tool/promptLibrary/bibliography.astro +14 -0
- package/src/tool/promptLibrary/component.astro +1131 -0
- package/src/tool/promptLibrary/controller.ts +49 -0
- package/src/tool/promptLibrary/i18n/en.ts +131 -0
- package/src/tool/promptLibrary/i18n/es.ts +131 -0
- package/src/tool/promptLibrary/i18n/fr.ts +126 -0
- package/src/tool/promptLibrary/index.ts +33 -0
- package/src/tool/promptLibrary/seo.astro +14 -0
- package/src/tool/promptLibrary/ui.ts +29 -0
- package/src/tool/readabilityCalculator/bibliography.astro +14 -0
- package/src/tool/readabilityCalculator/component.astro +534 -0
- package/src/tool/readabilityCalculator/i18n/en.ts +133 -0
- package/src/tool/readabilityCalculator/i18n/es.ts +133 -0
- package/src/tool/readabilityCalculator/i18n/fr.ts +133 -0
- package/src/tool/readabilityCalculator/index.ts +33 -0
- package/src/tool/readabilityCalculator/seo.astro +14 -0
- package/src/tool/readabilityCalculator/ui.ts +27 -0
- package/src/tool/svgSanitizer/bibliography.astro +14 -0
- package/src/tool/svgSanitizer/component.astro +554 -0
- package/src/tool/svgSanitizer/i18n/en.ts +125 -0
- package/src/tool/svgSanitizer/i18n/es.ts +125 -0
- package/src/tool/svgSanitizer/i18n/fr.ts +125 -0
- package/src/tool/svgSanitizer/index.ts +33 -0
- package/src/tool/svgSanitizer/seo.astro +14 -0
- package/src/tool/svgSanitizer/ui.ts +18 -0
- package/src/tool/svgToCss/bibliography.astro +14 -0
- package/src/tool/svgToCss/component.astro +381 -0
- package/src/tool/svgToCss/i18n/en.ts +174 -0
- package/src/tool/svgToCss/i18n/es.ts +174 -0
- package/src/tool/svgToCss/i18n/fr.ts +174 -0
- package/src/tool/svgToCss/index.ts +34 -0
- package/src/tool/svgToCss/seo.astro +14 -0
- package/src/tool/svgToCss/ui.ts +13 -0
- package/src/tool/urlCleaner/bibliography.astro +14 -0
- package/src/tool/urlCleaner/component.astro +458 -0
- package/src/tool/urlCleaner/i18n/en.ts +116 -0
- package/src/tool/urlCleaner/i18n/es.ts +116 -0
- package/src/tool/urlCleaner/i18n/fr.ts +116 -0
- package/src/tool/urlCleaner/index.ts +33 -0
- package/src/tool/urlCleaner/seo.astro +14 -0
- package/src/tool/urlCleaner/ui.ts +12 -0
- package/src/tool/urlEncoderDecoder/bibliography.astro +14 -0
- package/src/tool/urlEncoderDecoder/component.astro +291 -0
- package/src/tool/urlEncoderDecoder/i18n/en.ts +162 -0
- package/src/tool/urlEncoderDecoder/i18n/es.ts +162 -0
- package/src/tool/urlEncoderDecoder/i18n/fr.ts +162 -0
- package/src/tool/urlEncoderDecoder/index.ts +34 -0
- package/src/tool/urlEncoderDecoder/seo.astro +14 -0
- package/src/tool/urlEncoderDecoder/ui.ts +11 -0
- package/src/tool/utmGenerator/bibliography.astro +14 -0
- package/src/tool/utmGenerator/component.astro +270 -0
- package/src/tool/utmGenerator/i18n/en.ts +117 -0
- package/src/tool/utmGenerator/i18n/es.ts +117 -0
- package/src/tool/utmGenerator/i18n/fr.ts +117 -0
- package/src/tool/utmGenerator/index.ts +33 -0
- package/src/tool/utmGenerator/seo.astro +14 -0
- package/src/tool/utmGenerator/ui.ts +13 -0
- package/src/tools.ts +27 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
import type { CssToInlineConverterUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
const slug = 'convertisseur-css-inline';
|
|
6
|
+
const title = 'Convertisseur CSS Externe en HTML Inline. Inliner pour Emails';
|
|
7
|
+
const description =
|
|
8
|
+
'Transformez vos feuilles de style et classes CSS en styles intégrés automatiquement dans votre HTML. Idéal pour les newsletters, les emails transactionnels et la mise en page web sécurisée.';
|
|
9
|
+
|
|
10
|
+
const faqData = [
|
|
11
|
+
{
|
|
12
|
+
question: 'Pourquoi les emails ont-ils besoin de CSS inline plutôt que de feuilles de style externes ?',
|
|
13
|
+
answer:
|
|
14
|
+
'Les clients de messagerie comme Outlook, Gmail ou Apple Mail filtrent ou ignorent les balises <link> et <style> pour des raisons de sécurité. La seule façon garantie d\'appliquer un style correctement dans un email est de l\'incorporer directement dans l\'attribut style de chaque élément HTML.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
question: 'Que se passe-t-il si un élément a déjà son propre attribut style ?',
|
|
18
|
+
answer:
|
|
19
|
+
'L\'outil respecte les styles inline existants et ajoute les nouvelles propriétés à la suite, simulant le comportement de la cascade CSS : les propriétés déclarées plus tard écrasent les précédentes en cas de conflit.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
question: 'Fonctionne-t-il avec des sélecteurs complexes comme :hover ou les media queries ?',
|
|
23
|
+
answer:
|
|
24
|
+
'L\'outil traite les sélecteurs de classe, d\'id, d\'attributs et de pseudo-classes structurelles que DOMParser peut résoudre. Les sélecteurs dépendants d\'état comme :hover et les media queries ne peuvent pas être inlinés (ils dépendent de l\'environnement d\'exécution) et sont ignorés.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
question: 'Mon code HTML et CSS est-il envoyé à un serveur ?',
|
|
28
|
+
answer:
|
|
29
|
+
'Non. Tout le traitement se fait à 100% dans votre navigateur via l\'API DOMParser native. Aucun octet de votre code ne quitte votre appareil, garantissant une confidentialité totale pour les modèles avec du contenu sensible.',
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const howToData = [
|
|
34
|
+
{
|
|
35
|
+
name: 'Collez votre HTML original',
|
|
36
|
+
text: 'Écrivez ou collez le HTML que vous souhaitez traiter dans le champ "HTML Original". Il peut s\'agir d\'un fragment ou d\'un document complet.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Ajoutez vos règles CSS',
|
|
40
|
+
text: 'Collez les classes et ids que vous souhaitez injecter dans le champ "Règles CSS". L\'outil les applique en respectant la spécificité des sélecteurs.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Convertissez et copiez',
|
|
44
|
+
text: 'Cliquez sur "Injecter le CSS dans le HTML". Le résultat avec tous les styles inline apparaîtra ci-dessous, prêt à copier-coller dans votre gestionnaire d\'email ou CMS.',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
49
|
+
'@context': 'https://schema.org',
|
|
50
|
+
'@type': 'FAQPage',
|
|
51
|
+
mainEntity: faqData.map((item) => ({
|
|
52
|
+
'@type': 'Question',
|
|
53
|
+
name: item.question,
|
|
54
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const howToSchema: WithContext<HowToThing> = {
|
|
59
|
+
'@context': 'https://schema.org',
|
|
60
|
+
'@type': 'HowTo',
|
|
61
|
+
name: title,
|
|
62
|
+
description,
|
|
63
|
+
step: howToData.map((step, i) => ({
|
|
64
|
+
'@type': 'HowToStep',
|
|
65
|
+
position: i + 1,
|
|
66
|
+
name: step.name,
|
|
67
|
+
text: step.text,
|
|
68
|
+
})),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
72
|
+
'@context': 'https://schema.org',
|
|
73
|
+
'@type': 'SoftwareApplication',
|
|
74
|
+
name: title,
|
|
75
|
+
description,
|
|
76
|
+
applicationCategory: 'UtilityApplication',
|
|
77
|
+
operatingSystem: 'All',
|
|
78
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
79
|
+
inLanguage: 'fr',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const ui: CssToInlineConverterUI = {
|
|
83
|
+
labelHtml: 'HTML Original',
|
|
84
|
+
labelCss: 'Règles CSS',
|
|
85
|
+
labelOutput: 'Résultat HTML En Ligne',
|
|
86
|
+
placeholderHtml: '<div class="ma-classe">Bonjour Monde</div>',
|
|
87
|
+
placeholderCss: '.ma-classe { color: red; padding: 10px; }',
|
|
88
|
+
placeholderOutput: 'Votre HTML avec les styles intégrés apparaîtra ici...',
|
|
89
|
+
btnConvert: 'Injecter le CSS dans le HTML',
|
|
90
|
+
btnCopy: 'Copier le Code',
|
|
91
|
+
btnCopied: 'Copié !',
|
|
92
|
+
msgError: 'Erreur de traitement. Vérifiez que votre HTML et votre CSS sont valides.',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const content: ToolLocaleContent<CssToInlineConverterUI> = {
|
|
96
|
+
slug,
|
|
97
|
+
title,
|
|
98
|
+
description,
|
|
99
|
+
ui,
|
|
100
|
+
faqTitle: 'Questions Fréquentes',
|
|
101
|
+
faq: faqData,
|
|
102
|
+
bibliographyTitle: 'Références et Documentation',
|
|
103
|
+
bibliography: [
|
|
104
|
+
{
|
|
105
|
+
name: 'Can I email: Matrice de support HTML et CSS pour les Emails',
|
|
106
|
+
url: 'https://www.caniemail.com/',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'MDN Web Docs: L\'attribut global style',
|
|
110
|
+
url: 'https://developer.mozilla.org/fr/docs/Web/HTML/Global_attributes/style',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'DOMParser API: Analyse sécurisée dans le navigateur',
|
|
114
|
+
url: 'https://developer.mozilla.org/fr/docs/Web/API/DOMParser',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
howTo: howToData,
|
|
118
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
119
|
+
seo: [
|
|
120
|
+
{
|
|
121
|
+
type: 'title',
|
|
122
|
+
text: 'Qu\'est-ce qu\'un CSS Inliner et pourquoi en avez-vous besoin ?',
|
|
123
|
+
level: 2,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: 'paragraph',
|
|
127
|
+
html: 'Lors du développement d\'applications web modernes, nous sommes habitués à séparer les responsabilités : le HTML construit la structure, et un fichier CSS externe apporte tout le style visuel. Cependant, tous les environnements ne font pas confiance aux feuilles de style externes ni aux balises globales <code><style></code>.',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: 'paragraph',
|
|
131
|
+
html: 'Le cas d\'usage le plus populaire où le CSS externe échoue est le <strong>Développement de Modèles d\'Email</strong>. Dans ces environnements, la seule façon fiable qu\'une police, couleur ou marge se rende correctement est qu\'elle soit imbriquée directement dans la balise : <code><span style="color: red;"></code>.',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: 'title',
|
|
135
|
+
text: 'Le problème du CSS dans les clients de messagerie',
|
|
136
|
+
level: 3,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: 'paragraph',
|
|
140
|
+
html: 'Les clients de messagerie comme Microsoft Outlook, Apple Mail ou Gmail ont des historiques notoires avec leurs moteurs de rendu CSS restrictifs. La plupart filtrent ou suppriment les balises <code><link></code> ou <code><style></code> par crainte d\'injections de code.',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'tip',
|
|
144
|
+
html: 'Pour les newsletters ou les emails transactionnels (reçus, confirmations de compte), utiliser des tableaux et le <em>CSS inline</em> est la norme d\'or dans l\'industrie du marketing par email.',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: 'title',
|
|
148
|
+
text: 'Comment cet outil fonctionne dans votre navigateur',
|
|
149
|
+
level: 3,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'list',
|
|
153
|
+
items: [
|
|
154
|
+
'<strong>Analyse Sécurisée :</strong> Utilise l\'<code>API DOMParser</code> pour transformer temporairement le HTML d\'entrée en un DOM virtuel sécurisé dans votre navigateur.',
|
|
155
|
+
'<strong>Simulation de Cascade :</strong> Analyse vos règles CSS, applique les poids de spécificité aux sélecteurs et mute les attributs <code>style</code> des éléments HTML sélectionnés.',
|
|
156
|
+
'<strong>100% Hors ligne :</strong> Aucun octet de votre code ne quitte votre appareil. Confidentialité totale pour les modèles avec du contenu sensible.',
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DeveloperToolEntry, ToolLocaleContent, ToolDefinition } from '../../types';
|
|
2
|
+
import CssToInlineConverterComponent from './component.astro';
|
|
3
|
+
import CssToInlineConverterSEO from './seo.astro';
|
|
4
|
+
import CssToInlineConverterBibliography from './bibliography.astro';
|
|
5
|
+
|
|
6
|
+
import type { CssToInlineConverterUI } from './ui';
|
|
7
|
+
|
|
8
|
+
export type CssToInlineConverterLocaleContent = ToolLocaleContent<CssToInlineConverterUI>;
|
|
9
|
+
|
|
10
|
+
import { content as es } from './i18n/es';
|
|
11
|
+
import { content as en } from './i18n/en';
|
|
12
|
+
import { content as fr } from './i18n/fr';
|
|
13
|
+
|
|
14
|
+
export const cssToInlineConverter: DeveloperToolEntry<CssToInlineConverterUI> = {
|
|
15
|
+
id: 'css-to-inline-converter',
|
|
16
|
+
icons: {
|
|
17
|
+
bg: 'mdi:email-outline',
|
|
18
|
+
fg: 'mdi:language-css3',
|
|
19
|
+
},
|
|
20
|
+
i18n: {
|
|
21
|
+
es: async () => es,
|
|
22
|
+
en: async () => en,
|
|
23
|
+
fr: async () => fr,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { CssToInlineConverterComponent, CssToInlineConverterSEO, CssToInlineConverterBibliography };
|
|
28
|
+
|
|
29
|
+
export const CSS_TO_INLINE_CONVERTER_TOOL: ToolDefinition = {
|
|
30
|
+
entry: cssToInlineConverter,
|
|
31
|
+
Component: CssToInlineConverterComponent,
|
|
32
|
+
SEOComponent: CssToInlineConverterSEO,
|
|
33
|
+
BibliographyComponent: CssToInlineConverterBibliography,
|
|
34
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { cssToInlineConverter } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'es' } = Astro.props;
|
|
11
|
+
const content = await cssToInlineConverter.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface CssToInlineConverterUI extends Record<string, string> {
|
|
2
|
+
labelHtml: string;
|
|
3
|
+
labelCss: string;
|
|
4
|
+
labelOutput: string;
|
|
5
|
+
placeholderHtml: string;
|
|
6
|
+
placeholderCss: string;
|
|
7
|
+
placeholderOutput: string;
|
|
8
|
+
btnConvert: string;
|
|
9
|
+
btnCopy: string;
|
|
10
|
+
btnCopied: string;
|
|
11
|
+
msgError: string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { duplicateCssRemover } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'es' } = Astro.props;
|
|
11
|
+
const content = await duplicateCssRemover.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { KnownLocale } from '../../types';
|
|
3
|
+
import type { DuplicateCssRemoverUI } from './ui';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale?: KnownLocale;
|
|
7
|
+
ui?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { ui } = Astro.props;
|
|
11
|
+
const t = (ui ?? {}) as DuplicateCssRemoverUI;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div
|
|
15
|
+
class="dupcss-root"
|
|
16
|
+
data-btn-copy={t.btnCopy}
|
|
17
|
+
data-btn-copied={t.btnCopied}
|
|
18
|
+
>
|
|
19
|
+
<div class="dupcss-card">
|
|
20
|
+
<div class="dupcss-stats" id="dupcss-stats">
|
|
21
|
+
<div class="dupcss-stat-item">
|
|
22
|
+
<span class="dupcss-stat-value" id="dupcss-stat-original">0</span>
|
|
23
|
+
<span class="dupcss-stat-label">{t.statBytesOriginal}</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="dupcss-stat-sep"></div>
|
|
26
|
+
<div class="dupcss-stat-item">
|
|
27
|
+
<span class="dupcss-stat-value dupcss-stat-value-clean" id="dupcss-stat-clean">0</span>
|
|
28
|
+
<span class="dupcss-stat-label">{t.statBytesClean}</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="dupcss-stat-sep"></div>
|
|
31
|
+
<div class="dupcss-stat-item">
|
|
32
|
+
<span class="dupcss-stat-value dupcss-stat-value-saving" id="dupcss-stat-saving">0%</span>
|
|
33
|
+
<span class="dupcss-stat-label">{t.statSaving}</span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="dupcss-grid">
|
|
38
|
+
<div class="dupcss-field">
|
|
39
|
+
<span class="dupcss-field-label">{t.labelInput}</span>
|
|
40
|
+
<textarea
|
|
41
|
+
id="dupcss-input"
|
|
42
|
+
class="dupcss-textarea"
|
|
43
|
+
placeholder={t.placeholderInput}
|
|
44
|
+
spellcheck="false"
|
|
45
|
+
></textarea>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="dupcss-field">
|
|
48
|
+
<div class="dupcss-field-header">
|
|
49
|
+
<span class="dupcss-field-label">{t.labelOutput}</span>
|
|
50
|
+
<button id="dupcss-copy-btn" class="dupcss-copy-btn">{t.btnCopy}</button>
|
|
51
|
+
</div>
|
|
52
|
+
<textarea
|
|
53
|
+
id="dupcss-output"
|
|
54
|
+
class="dupcss-textarea dupcss-textarea-output"
|
|
55
|
+
readonly
|
|
56
|
+
placeholder={t.placeholderOutput}
|
|
57
|
+
spellcheck="false"
|
|
58
|
+
></textarea>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="dupcss-actions">
|
|
63
|
+
<button id="dupcss-clean-btn" class="dupcss-btn">
|
|
64
|
+
<svg class="dupcss-btn-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
65
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"></path>
|
|
66
|
+
</svg>
|
|
67
|
+
{t.btnClean}
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<style>
|
|
74
|
+
.dupcss-root {
|
|
75
|
+
--dupcss-primary: #10b981;
|
|
76
|
+
--dupcss-primary-dark: #059669;
|
|
77
|
+
--dupcss-card-bg: #fff;
|
|
78
|
+
--dupcss-bg: #f8fafc;
|
|
79
|
+
--dupcss-text: #0f172a;
|
|
80
|
+
--dupcss-text-muted: #64748b;
|
|
81
|
+
--dupcss-border: #e2e8f0;
|
|
82
|
+
--dupcss-output-bg: rgba(16, 185, 129, 0.04);
|
|
83
|
+
--dupcss-saving-color: #f59e0b;
|
|
84
|
+
|
|
85
|
+
max-width: 1000px;
|
|
86
|
+
margin: 2rem auto;
|
|
87
|
+
color: var(--dupcss-text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
:global(.theme-dark) .dupcss-root {
|
|
91
|
+
--dupcss-card-bg: #1e293b;
|
|
92
|
+
--dupcss-bg: #0f172a;
|
|
93
|
+
--dupcss-text: #f1f5f9;
|
|
94
|
+
--dupcss-text-muted: #94a3b8;
|
|
95
|
+
--dupcss-border: #334155;
|
|
96
|
+
--dupcss-output-bg: rgba(16, 185, 129, 0.08);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.dupcss-card {
|
|
100
|
+
background: var(--dupcss-card-bg);
|
|
101
|
+
border: 1px solid var(--dupcss-border);
|
|
102
|
+
border-radius: 1.25rem;
|
|
103
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
|
|
104
|
+
padding: 1.75rem;
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
gap: 1.25rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.dupcss-stats {
|
|
111
|
+
display: none;
|
|
112
|
+
gap: 0;
|
|
113
|
+
background: var(--dupcss-bg);
|
|
114
|
+
border: 1px solid var(--dupcss-border);
|
|
115
|
+
border-radius: 0.875rem;
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
:global(.dupcss-stats-visible) {
|
|
120
|
+
display: flex;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.dupcss-stat-item {
|
|
124
|
+
flex: 1;
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-direction: column;
|
|
127
|
+
align-items: center;
|
|
128
|
+
padding: 0.875rem 1rem;
|
|
129
|
+
gap: 0.25rem;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.dupcss-stat-sep {
|
|
133
|
+
width: 1px;
|
|
134
|
+
background: var(--dupcss-border);
|
|
135
|
+
align-self: stretch;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.dupcss-stat-value {
|
|
139
|
+
font-size: 1.375rem;
|
|
140
|
+
font-weight: 800;
|
|
141
|
+
color: var(--dupcss-primary);
|
|
142
|
+
line-height: 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.dupcss-stat-value-saving {
|
|
146
|
+
color: var(--dupcss-saving-color);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.dupcss-stat-label {
|
|
150
|
+
font-size: 0.7rem;
|
|
151
|
+
text-transform: uppercase;
|
|
152
|
+
font-weight: 600;
|
|
153
|
+
color: var(--dupcss-text-muted);
|
|
154
|
+
letter-spacing: 0.06em;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.dupcss-grid {
|
|
158
|
+
display: grid;
|
|
159
|
+
grid-template-columns: 1fr 1fr;
|
|
160
|
+
gap: 1.25rem;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@media (max-width: 768px) {
|
|
164
|
+
.dupcss-grid {
|
|
165
|
+
grid-template-columns: 1fr;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.dupcss-field {
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
gap: 0.5rem;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.dupcss-field-header {
|
|
176
|
+
display: flex;
|
|
177
|
+
justify-content: space-between;
|
|
178
|
+
align-items: center;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.dupcss-field-label {
|
|
182
|
+
font-size: 0.8rem;
|
|
183
|
+
font-weight: 700;
|
|
184
|
+
color: var(--dupcss-text-muted);
|
|
185
|
+
text-transform: uppercase;
|
|
186
|
+
letter-spacing: 0.06em;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.dupcss-copy-btn {
|
|
190
|
+
background: transparent;
|
|
191
|
+
border: 1px solid var(--dupcss-border);
|
|
192
|
+
color: var(--dupcss-text-muted);
|
|
193
|
+
padding: 0.2rem 0.6rem;
|
|
194
|
+
border-radius: 0.4rem;
|
|
195
|
+
font-size: 0.75rem;
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
transition: all 0.15s;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.dupcss-copy-btn:hover {
|
|
202
|
+
background: var(--dupcss-bg);
|
|
203
|
+
color: var(--dupcss-primary);
|
|
204
|
+
border-color: var(--dupcss-primary);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.dupcss-textarea {
|
|
208
|
+
width: 100%;
|
|
209
|
+
height: 320px;
|
|
210
|
+
padding: 1rem 1.25rem;
|
|
211
|
+
font-size: 0.875rem;
|
|
212
|
+
color: var(--dupcss-text);
|
|
213
|
+
background: var(--dupcss-bg);
|
|
214
|
+
border: 1px solid var(--dupcss-border);
|
|
215
|
+
border-radius: 0.875rem;
|
|
216
|
+
resize: vertical;
|
|
217
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
218
|
+
outline: none;
|
|
219
|
+
white-space: pre;
|
|
220
|
+
overflow: auto;
|
|
221
|
+
line-height: 1.6;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.dupcss-textarea:focus {
|
|
225
|
+
border-color: var(--dupcss-primary);
|
|
226
|
+
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.12);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.dupcss-textarea-output {
|
|
230
|
+
border-color: var(--dupcss-primary);
|
|
231
|
+
background: var(--dupcss-output-bg);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.dupcss-textarea-output:focus {
|
|
235
|
+
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.12);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.dupcss-actions {
|
|
239
|
+
display: flex;
|
|
240
|
+
justify-content: center;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.dupcss-btn {
|
|
244
|
+
display: flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
gap: 0.5rem;
|
|
247
|
+
background: var(--dupcss-primary);
|
|
248
|
+
color: #fff;
|
|
249
|
+
font-weight: 700;
|
|
250
|
+
font-size: 0.95rem;
|
|
251
|
+
padding: 0.875rem 2rem;
|
|
252
|
+
border-radius: 0.875rem;
|
|
253
|
+
border: none;
|
|
254
|
+
cursor: pointer;
|
|
255
|
+
transition: transform 0.15s, box-shadow 0.15s, background 0.15s;
|
|
256
|
+
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.dupcss-btn:hover {
|
|
260
|
+
background: var(--dupcss-primary-dark);
|
|
261
|
+
transform: translateY(-2px);
|
|
262
|
+
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.dupcss-btn-icon {
|
|
266
|
+
width: 18px;
|
|
267
|
+
height: 18px;
|
|
268
|
+
}
|
|
269
|
+
</style>
|
|
270
|
+
|
|
271
|
+
<script>
|
|
272
|
+
const root = document.querySelector('.dupcss-root') as HTMLElement | null;
|
|
273
|
+
const inputEl = document.getElementById('dupcss-input') as HTMLTextAreaElement;
|
|
274
|
+
const outputEl = document.getElementById('dupcss-output') as HTMLTextAreaElement;
|
|
275
|
+
const cleanBtn = document.getElementById('dupcss-clean-btn');
|
|
276
|
+
const copyBtn = document.getElementById('dupcss-copy-btn');
|
|
277
|
+
const statsBlock = document.getElementById('dupcss-stats');
|
|
278
|
+
const statOriginal = document.getElementById('dupcss-stat-original');
|
|
279
|
+
const statClean = document.getElementById('dupcss-stat-clean');
|
|
280
|
+
const statSaving = document.getElementById('dupcss-stat-saving');
|
|
281
|
+
|
|
282
|
+
const txtCopy = root?.dataset.btnCopy ?? 'Copy';
|
|
283
|
+
const txtCopied = root?.dataset.btnCopied ?? 'Copied!';
|
|
284
|
+
|
|
285
|
+
function parseProps(propertiesRaw: string, propMap: Map<string, string>): void {
|
|
286
|
+
for (const p of propertiesRaw.split(';')) {
|
|
287
|
+
const colonIdx = p.indexOf(':');
|
|
288
|
+
if (colonIdx === -1) continue;
|
|
289
|
+
const key = p.slice(0, colonIdx).trim();
|
|
290
|
+
const val = p.slice(colonIdx + 1).trim();
|
|
291
|
+
if (key && val) propMap.set(key, val);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function parseRule(ruleBlock: string, stylesMap: Map<string, Map<string, string>>): void {
|
|
296
|
+
const braceIdx = ruleBlock.indexOf('{');
|
|
297
|
+
if (braceIdx === -1) return;
|
|
298
|
+
const selector = ruleBlock.slice(0, braceIdx).trim().replace(/\s+/g, ' ');
|
|
299
|
+
const propertiesRaw = ruleBlock.slice(braceIdx + 1).replace('}', '').trim();
|
|
300
|
+
if (!selector || !propertiesRaw) return;
|
|
301
|
+
if (!stylesMap.has(selector)) stylesMap.set(selector, new Map<string, string>());
|
|
302
|
+
parseProps(propertiesRaw, stylesMap.get(selector)!);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function serializeStyles(stylesMap: Map<string, Map<string, string>>): string {
|
|
306
|
+
let out = '';
|
|
307
|
+
stylesMap.forEach((propMap, selector) => {
|
|
308
|
+
if (propMap.size === 0) return;
|
|
309
|
+
out += `${selector} {\n`;
|
|
310
|
+
propMap.forEach((val, key) => { out += ` ${key}: ${val};\n`; });
|
|
311
|
+
out += `}\n\n`;
|
|
312
|
+
});
|
|
313
|
+
return out.trim();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function cleanCss(rawCss: string): string {
|
|
317
|
+
if (!rawCss.trim()) return '';
|
|
318
|
+
const withoutComments = rawCss.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
319
|
+
const matches = withoutComments.match(/(?:[^{}]+)\s*\{[^}]+\}/g) ?? [];
|
|
320
|
+
const stylesMap = new Map<string, Map<string, string>>();
|
|
321
|
+
for (const rule of matches) parseRule(rule, stylesMap);
|
|
322
|
+
return serializeStyles(stylesMap);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (cleanBtn && inputEl && outputEl && statsBlock && statOriginal && statClean && statSaving) {
|
|
326
|
+
cleanBtn.addEventListener('click', () => {
|
|
327
|
+
const result = cleanCss(inputEl.value);
|
|
328
|
+
outputEl.value = result;
|
|
329
|
+
|
|
330
|
+
if (result) {
|
|
331
|
+
const origBytes = new Blob([inputEl.value]).size;
|
|
332
|
+
const cleanBytes = new Blob([result]).size;
|
|
333
|
+
const saving = origBytes > 0 ? (((origBytes - cleanBytes) / origBytes) * 100).toFixed(1) : '0';
|
|
334
|
+
|
|
335
|
+
statOriginal.textContent = origBytes.toString();
|
|
336
|
+
statClean.textContent = cleanBytes.toString();
|
|
337
|
+
statSaving.textContent = `${saving}%`;
|
|
338
|
+
statsBlock.classList.add('dupcss-stats-visible');
|
|
339
|
+
} else {
|
|
340
|
+
statsBlock.classList.remove('dupcss-stats-visible');
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (copyBtn && outputEl) {
|
|
346
|
+
copyBtn.addEventListener('click', async () => {
|
|
347
|
+
if (!outputEl.value) return;
|
|
348
|
+
await navigator.clipboard.writeText(outputEl.value);
|
|
349
|
+
copyBtn.textContent = txtCopied;
|
|
350
|
+
setTimeout(() => { copyBtn.textContent = txtCopy; }, 1500);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
</script>
|