@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.
Files changed (228) hide show
  1. package/package.json +64 -0
  2. package/src/category/i18n/en.ts +34 -0
  3. package/src/category/i18n/es.ts +34 -0
  4. package/src/category/i18n/fr.ts +34 -0
  5. package/src/category/index.ts +31 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +78 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/css_specificity_logic.test.ts +39 -0
  17. package/src/tests/faq_count.test.ts +19 -0
  18. package/src/tests/locale_completeness.test.ts +42 -0
  19. package/src/tests/mocks/astro_mock.js +2 -0
  20. package/src/tests/no_h1_in_components.test.ts +48 -0
  21. package/src/tests/schemas_fulfillment.test.ts +23 -0
  22. package/src/tests/seo_length.test.ts +22 -0
  23. package/src/tests/title_quality.test.ts +55 -0
  24. package/src/tests/tool_validation.test.ts +17 -0
  25. package/src/tool/aspectRatio/bibliography.astro +14 -0
  26. package/src/tool/aspectRatio/component.astro +468 -0
  27. package/src/tool/aspectRatio/i18n/en.ts +177 -0
  28. package/src/tool/aspectRatio/i18n/es.ts +177 -0
  29. package/src/tool/aspectRatio/i18n/fr.ts +177 -0
  30. package/src/tool/aspectRatio/index.ts +34 -0
  31. package/src/tool/aspectRatio/seo.astro +14 -0
  32. package/src/tool/aspectRatio/ui.ts +12 -0
  33. package/src/tool/calculadoraTiempoDatos/bibliography.astro +14 -0
  34. package/src/tool/calculadoraTiempoDatos/component.astro +829 -0
  35. package/src/tool/calculadoraTiempoDatos/i18n/en.ts +150 -0
  36. package/src/tool/calculadoraTiempoDatos/i18n/es.ts +163 -0
  37. package/src/tool/calculadoraTiempoDatos/i18n/fr.ts +150 -0
  38. package/src/tool/calculadoraTiempoDatos/index.ts +33 -0
  39. package/src/tool/calculadoraTiempoDatos/seo.astro +14 -0
  40. package/src/tool/calculadoraTiempoDatos/ui.ts +24 -0
  41. package/src/tool/colorConverter/bibliography.astro +14 -0
  42. package/src/tool/colorConverter/component.astro +504 -0
  43. package/src/tool/colorConverter/i18n/en.ts +126 -0
  44. package/src/tool/colorConverter/i18n/es.ts +126 -0
  45. package/src/tool/colorConverter/i18n/fr.ts +126 -0
  46. package/src/tool/colorConverter/index.ts +33 -0
  47. package/src/tool/colorConverter/seo.astro +14 -0
  48. package/src/tool/colorConverter/ui.ts +16 -0
  49. package/src/tool/conversorExcelCsvHtml/bibliography.astro +14 -0
  50. package/src/tool/conversorExcelCsvHtml/component.astro +595 -0
  51. package/src/tool/conversorExcelCsvHtml/i18n/en.ts +129 -0
  52. package/src/tool/conversorExcelCsvHtml/i18n/es.ts +129 -0
  53. package/src/tool/conversorExcelCsvHtml/i18n/fr.ts +129 -0
  54. package/src/tool/conversorExcelCsvHtml/index.ts +33 -0
  55. package/src/tool/conversorExcelCsvHtml/seo.astro +14 -0
  56. package/src/tool/conversorExcelCsvHtml/ui.ts +14 -0
  57. package/src/tool/cronGenerator/bibliography.astro +14 -0
  58. package/src/tool/cronGenerator/component.astro +667 -0
  59. package/src/tool/cronGenerator/i18n/en.ts +191 -0
  60. package/src/tool/cronGenerator/i18n/es.ts +191 -0
  61. package/src/tool/cronGenerator/i18n/fr.ts +191 -0
  62. package/src/tool/cronGenerator/index.ts +34 -0
  63. package/src/tool/cronGenerator/seo.astro +14 -0
  64. package/src/tool/cronGenerator/ui.ts +45 -0
  65. package/src/tool/cssSpecificityCalculator/bibliography.astro +14 -0
  66. package/src/tool/cssSpecificityCalculator/component.astro +292 -0
  67. package/src/tool/cssSpecificityCalculator/i18n/en.ts +168 -0
  68. package/src/tool/cssSpecificityCalculator/i18n/es.ts +168 -0
  69. package/src/tool/cssSpecificityCalculator/i18n/fr.ts +168 -0
  70. package/src/tool/cssSpecificityCalculator/index.ts +34 -0
  71. package/src/tool/cssSpecificityCalculator/logic.ts +100 -0
  72. package/src/tool/cssSpecificityCalculator/seo.astro +14 -0
  73. package/src/tool/cssSpecificityCalculator/ui-controller.ts +57 -0
  74. package/src/tool/cssSpecificityCalculator/ui.ts +11 -0
  75. package/src/tool/cssToInlineConverter/bibliography.astro +14 -0
  76. package/src/tool/cssToInlineConverter/component.astro +308 -0
  77. package/src/tool/cssToInlineConverter/i18n/en.ts +160 -0
  78. package/src/tool/cssToInlineConverter/i18n/es.ts +160 -0
  79. package/src/tool/cssToInlineConverter/i18n/fr.ts +160 -0
  80. package/src/tool/cssToInlineConverter/index.ts +34 -0
  81. package/src/tool/cssToInlineConverter/seo.astro +14 -0
  82. package/src/tool/cssToInlineConverter/ui.ts +12 -0
  83. package/src/tool/duplicateCssRemover/bibliography.astro +14 -0
  84. package/src/tool/duplicateCssRemover/component.astro +353 -0
  85. package/src/tool/duplicateCssRemover/i18n/en.ts +167 -0
  86. package/src/tool/duplicateCssRemover/i18n/es.ts +167 -0
  87. package/src/tool/duplicateCssRemover/i18n/fr.ts +167 -0
  88. package/src/tool/duplicateCssRemover/index.ts +34 -0
  89. package/src/tool/duplicateCssRemover/seo.astro +14 -0
  90. package/src/tool/duplicateCssRemover/ui.ts +12 -0
  91. package/src/tool/generadorSecurityTxt/bibliography.astro +14 -0
  92. package/src/tool/generadorSecurityTxt/component.astro +473 -0
  93. package/src/tool/generadorSecurityTxt/i18n/en.ts +173 -0
  94. package/src/tool/generadorSecurityTxt/i18n/es.ts +173 -0
  95. package/src/tool/generadorSecurityTxt/i18n/fr.ts +173 -0
  96. package/src/tool/generadorSecurityTxt/index.ts +33 -0
  97. package/src/tool/generadorSecurityTxt/seo.astro +14 -0
  98. package/src/tool/generadorSecurityTxt/ui.ts +27 -0
  99. package/src/tool/hashGenerator/bibliography.astro +14 -0
  100. package/src/tool/hashGenerator/component.astro +367 -0
  101. package/src/tool/hashGenerator/i18n/en.ts +118 -0
  102. package/src/tool/hashGenerator/i18n/es.ts +118 -0
  103. package/src/tool/hashGenerator/i18n/fr.ts +118 -0
  104. package/src/tool/hashGenerator/index.ts +33 -0
  105. package/src/tool/hashGenerator/md5.ts +138 -0
  106. package/src/tool/hashGenerator/seo.astro +14 -0
  107. package/src/tool/hashGenerator/ui.ts +11 -0
  108. package/src/tool/inspectorCertificadosSsl/bibliography.astro +14 -0
  109. package/src/tool/inspectorCertificadosSsl/component.astro +580 -0
  110. package/src/tool/inspectorCertificadosSsl/i18n/en.ts +214 -0
  111. package/src/tool/inspectorCertificadosSsl/i18n/es.ts +214 -0
  112. package/src/tool/inspectorCertificadosSsl/i18n/fr.ts +214 -0
  113. package/src/tool/inspectorCertificadosSsl/index.ts +33 -0
  114. package/src/tool/inspectorCertificadosSsl/seo.astro +14 -0
  115. package/src/tool/inspectorCertificadosSsl/ui.ts +29 -0
  116. package/src/tool/jsonFormatter/bibliography.astro +14 -0
  117. package/src/tool/jsonFormatter/component.astro +590 -0
  118. package/src/tool/jsonFormatter/i18n/en.ts +156 -0
  119. package/src/tool/jsonFormatter/i18n/es.ts +156 -0
  120. package/src/tool/jsonFormatter/i18n/fr.ts +156 -0
  121. package/src/tool/jsonFormatter/index.ts +34 -0
  122. package/src/tool/jsonFormatter/seo.astro +14 -0
  123. package/src/tool/jsonFormatter/ui.ts +15 -0
  124. package/src/tool/keycode/bibliography.astro +14 -0
  125. package/src/tool/keycode/component.astro +540 -0
  126. package/src/tool/keycode/i18n/en.ts +160 -0
  127. package/src/tool/keycode/i18n/es.ts +160 -0
  128. package/src/tool/keycode/i18n/fr.ts +160 -0
  129. package/src/tool/keycode/index.ts +34 -0
  130. package/src/tool/keycode/seo.astro +14 -0
  131. package/src/tool/keycode/ui.ts +10 -0
  132. package/src/tool/llmCostCalculator/bibliography.astro +14 -0
  133. package/src/tool/llmCostCalculator/component.astro +488 -0
  134. package/src/tool/llmCostCalculator/data.ts +99 -0
  135. package/src/tool/llmCostCalculator/i18n/en.ts +165 -0
  136. package/src/tool/llmCostCalculator/i18n/es.ts +165 -0
  137. package/src/tool/llmCostCalculator/i18n/fr.ts +165 -0
  138. package/src/tool/llmCostCalculator/index.ts +34 -0
  139. package/src/tool/llmCostCalculator/seo.astro +14 -0
  140. package/src/tool/llmCostCalculator/ui.ts +11 -0
  141. package/src/tool/mobileMockupGenerator/app.ts +614 -0
  142. package/src/tool/mobileMockupGenerator/bibliography.astro +14 -0
  143. package/src/tool/mobileMockupGenerator/component.astro +1044 -0
  144. package/src/tool/mobileMockupGenerator/constants.ts +48 -0
  145. package/src/tool/mobileMockupGenerator/i18n/en.ts +121 -0
  146. package/src/tool/mobileMockupGenerator/i18n/es.ts +121 -0
  147. package/src/tool/mobileMockupGenerator/i18n/fr.ts +121 -0
  148. package/src/tool/mobileMockupGenerator/index.ts +33 -0
  149. package/src/tool/mobileMockupGenerator/rendering.ts +189 -0
  150. package/src/tool/mobileMockupGenerator/seo.astro +14 -0
  151. package/src/tool/mobileMockupGenerator/store.ts +145 -0
  152. package/src/tool/mobileMockupGenerator/types.ts +41 -0
  153. package/src/tool/mobileMockupGenerator/ui.ts +48 -0
  154. package/src/tool/musicalTypography/bibliography.astro +14 -0
  155. package/src/tool/musicalTypography/component.astro +480 -0
  156. package/src/tool/musicalTypography/i18n/en.ts +179 -0
  157. package/src/tool/musicalTypography/i18n/es.ts +179 -0
  158. package/src/tool/musicalTypography/i18n/fr.ts +179 -0
  159. package/src/tool/musicalTypography/index.ts +33 -0
  160. package/src/tool/musicalTypography/seo.astro +14 -0
  161. package/src/tool/musicalTypography/ui.ts +25 -0
  162. package/src/tool/placeholderGenerator/bibliography.astro +14 -0
  163. package/src/tool/placeholderGenerator/component.astro +499 -0
  164. package/src/tool/placeholderGenerator/i18n/en.ts +176 -0
  165. package/src/tool/placeholderGenerator/i18n/es.ts +176 -0
  166. package/src/tool/placeholderGenerator/i18n/fr.ts +176 -0
  167. package/src/tool/placeholderGenerator/index.ts +34 -0
  168. package/src/tool/placeholderGenerator/seo.astro +14 -0
  169. package/src/tool/placeholderGenerator/ui.ts +14 -0
  170. package/src/tool/promptLibrary/bibliography.astro +14 -0
  171. package/src/tool/promptLibrary/component.astro +1131 -0
  172. package/src/tool/promptLibrary/controller.ts +49 -0
  173. package/src/tool/promptLibrary/i18n/en.ts +131 -0
  174. package/src/tool/promptLibrary/i18n/es.ts +131 -0
  175. package/src/tool/promptLibrary/i18n/fr.ts +126 -0
  176. package/src/tool/promptLibrary/index.ts +33 -0
  177. package/src/tool/promptLibrary/seo.astro +14 -0
  178. package/src/tool/promptLibrary/ui.ts +29 -0
  179. package/src/tool/readabilityCalculator/bibliography.astro +14 -0
  180. package/src/tool/readabilityCalculator/component.astro +534 -0
  181. package/src/tool/readabilityCalculator/i18n/en.ts +133 -0
  182. package/src/tool/readabilityCalculator/i18n/es.ts +133 -0
  183. package/src/tool/readabilityCalculator/i18n/fr.ts +133 -0
  184. package/src/tool/readabilityCalculator/index.ts +33 -0
  185. package/src/tool/readabilityCalculator/seo.astro +14 -0
  186. package/src/tool/readabilityCalculator/ui.ts +27 -0
  187. package/src/tool/svgSanitizer/bibliography.astro +14 -0
  188. package/src/tool/svgSanitizer/component.astro +554 -0
  189. package/src/tool/svgSanitizer/i18n/en.ts +125 -0
  190. package/src/tool/svgSanitizer/i18n/es.ts +125 -0
  191. package/src/tool/svgSanitizer/i18n/fr.ts +125 -0
  192. package/src/tool/svgSanitizer/index.ts +33 -0
  193. package/src/tool/svgSanitizer/seo.astro +14 -0
  194. package/src/tool/svgSanitizer/ui.ts +18 -0
  195. package/src/tool/svgToCss/bibliography.astro +14 -0
  196. package/src/tool/svgToCss/component.astro +381 -0
  197. package/src/tool/svgToCss/i18n/en.ts +174 -0
  198. package/src/tool/svgToCss/i18n/es.ts +174 -0
  199. package/src/tool/svgToCss/i18n/fr.ts +174 -0
  200. package/src/tool/svgToCss/index.ts +34 -0
  201. package/src/tool/svgToCss/seo.astro +14 -0
  202. package/src/tool/svgToCss/ui.ts +13 -0
  203. package/src/tool/urlCleaner/bibliography.astro +14 -0
  204. package/src/tool/urlCleaner/component.astro +458 -0
  205. package/src/tool/urlCleaner/i18n/en.ts +116 -0
  206. package/src/tool/urlCleaner/i18n/es.ts +116 -0
  207. package/src/tool/urlCleaner/i18n/fr.ts +116 -0
  208. package/src/tool/urlCleaner/index.ts +33 -0
  209. package/src/tool/urlCleaner/seo.astro +14 -0
  210. package/src/tool/urlCleaner/ui.ts +12 -0
  211. package/src/tool/urlEncoderDecoder/bibliography.astro +14 -0
  212. package/src/tool/urlEncoderDecoder/component.astro +291 -0
  213. package/src/tool/urlEncoderDecoder/i18n/en.ts +162 -0
  214. package/src/tool/urlEncoderDecoder/i18n/es.ts +162 -0
  215. package/src/tool/urlEncoderDecoder/i18n/fr.ts +162 -0
  216. package/src/tool/urlEncoderDecoder/index.ts +34 -0
  217. package/src/tool/urlEncoderDecoder/seo.astro +14 -0
  218. package/src/tool/urlEncoderDecoder/ui.ts +11 -0
  219. package/src/tool/utmGenerator/bibliography.astro +14 -0
  220. package/src/tool/utmGenerator/component.astro +270 -0
  221. package/src/tool/utmGenerator/i18n/en.ts +117 -0
  222. package/src/tool/utmGenerator/i18n/es.ts +117 -0
  223. package/src/tool/utmGenerator/i18n/fr.ts +117 -0
  224. package/src/tool/utmGenerator/index.ts +33 -0
  225. package/src/tool/utmGenerator/seo.astro +14 -0
  226. package/src/tool/utmGenerator/ui.ts +13 -0
  227. package/src/tools.ts +27 -0
  228. package/src/types.ts +72 -0
@@ -0,0 +1,554 @@
1
+ ---
2
+ import type { SvgSanitizerUI } from './ui';
3
+
4
+ interface Props {
5
+ ui?: Record<string, unknown>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ const t = (ui ?? {}) as SvgSanitizerUI;
10
+
11
+ const placeholder = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xml:space="preserve" data-name="Figma Export">
12
+ <metadata>...</metadata>
13
+ <g id="Layer_1" data-name="Capa 1"></g>
14
+ <path d="M10 10 L90 90"/>
15
+ </svg>`;
16
+ ---
17
+
18
+ <div id="ss-root" class="ss-root" data-i18n={JSON.stringify(t)}>
19
+ <div id="ss-layout" class="ss-layout">
20
+ <div class="ss-panel ss-input-panel">
21
+ <label for="ss-input-area" class="ss-panel-label">{t.labelInput}</label>
22
+ <textarea
23
+ id="ss-input-area"
24
+ class="ss-textarea"
25
+ placeholder={placeholder}
26
+ spellcheck="false"
27
+ autocomplete="off"
28
+ ></textarea>
29
+
30
+ <div class="ss-options-row">
31
+ <label class="ss-toggle-label">
32
+ <input type="checkbox" id="ss-opt-ids" class="ss-toggle-input" />
33
+ <span class="ss-toggle-track"></span>
34
+ {t.optRemoveIds}
35
+ </label>
36
+ <label class="ss-toggle-label">
37
+ <input type="checkbox" id="ss-opt-wh" class="ss-toggle-input" checked />
38
+ <span class="ss-toggle-track"></span>
39
+ {t.optRemoveWH}
40
+ </label>
41
+ <label class="ss-toggle-label">
42
+ <input type="checkbox" id="ss-opt-minify" class="ss-toggle-input" />
43
+ <span class="ss-toggle-track"></span>
44
+ {t.optMinify}
45
+ </label>
46
+ </div>
47
+
48
+ <button id="ss-btn-sanitize" class="ss-btn-sanitize">
49
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19.36 2.72L20.78 4.14L15.06 9.85C16.13 11.39 16.28 13.24 15.38 14.44L9.06 8.12C10.26 7.22 12.11 7.38 13.65 8.44L19.36 2.72M5.93 17.57C3.92 15.56 2.69 13.16 2.35 10.92L7.23 8.83L14.67 16.27L12.58 21.15C10.34 20.81 7.94 19.58 5.93 17.57Z"/></svg>
50
+ {t.btnSanitize}
51
+ </button>
52
+
53
+ <p id="ss-error" class="ss-error" style="display:none"></p>
54
+ </div>
55
+
56
+ <div id="ss-output-panel" class="ss-panel ss-output-panel" style="display:none">
57
+ <div class="ss-output-header">
58
+ <span class="ss-output-label">{t.labelOutput}</span>
59
+ <div class="ss-output-actions">
60
+ <button id="ss-btn-copy" class="ss-btn-action">
61
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
62
+ {t.btnCopy}
63
+ </button>
64
+ <button id="ss-btn-download" class="ss-btn-action ss-btn-download">
65
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/></svg>
66
+ {t.btnDownload}
67
+ </button>
68
+ </div>
69
+ </div>
70
+
71
+ <textarea id="ss-output-area" class="ss-textarea ss-output-textarea" readonly spellcheck="false"></textarea>
72
+
73
+ <div class="ss-stats-row">
74
+ <div class="ss-stat-chip">
75
+ <span class="ss-chip-value" id="ss-stat-original">--</span>
76
+ <span class="ss-chip-label">{t.statOriginal}</span>
77
+ </div>
78
+ <div class="ss-stat-chip ss-chip-green">
79
+ <span class="ss-chip-value" id="ss-stat-cleaned">--</span>
80
+ <span class="ss-chip-label">{t.statCleaned}</span>
81
+ </div>
82
+ <div class="ss-stat-chip ss-chip-purple">
83
+ <span class="ss-chip-value" id="ss-stat-reduction">--</span>
84
+ <span class="ss-chip-label">{t.statReduction}</span>
85
+ </div>
86
+ <div class="ss-stat-chip ss-chip-amber">
87
+ <span class="ss-chip-value" id="ss-stat-elems">--</span>
88
+ <span class="ss-chip-label">{t.statElems}</span>
89
+ </div>
90
+ <div class="ss-stat-chip ss-chip-rose">
91
+ <span class="ss-chip-value" id="ss-stat-attrs">--</span>
92
+ <span class="ss-chip-label">{t.statAttrs}</span>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <style>
100
+ .ss-root {
101
+ width: 100%;
102
+ }
103
+
104
+ .ss-layout {
105
+ display: grid;
106
+ grid-template-columns: 1fr;
107
+ gap: 1.5rem;
108
+ max-width: 1100px;
109
+ margin: 0 auto;
110
+ align-items: start;
111
+ }
112
+
113
+ @media (min-width: 768px) {
114
+ .ss-layout.ss-has-output {
115
+ grid-template-columns: 1fr 1fr;
116
+ }
117
+ }
118
+
119
+ .ss-panel {
120
+ background: rgba(255, 255, 255, 0.75);
121
+ backdrop-filter: blur(14px);
122
+ -webkit-backdrop-filter: blur(14px);
123
+ border: 1px solid rgba(255, 255, 255, 0.35);
124
+ border-radius: 1.5rem;
125
+ padding: 1.5rem;
126
+ box-shadow: 0 8px 32px rgba(31, 38, 135, 0.06);
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 1rem;
130
+ }
131
+
132
+ :global(.theme-dark) .ss-panel {
133
+ background: rgba(15, 23, 42, 0.75);
134
+ border-color: rgba(255, 255, 255, 0.06);
135
+ }
136
+
137
+ .ss-panel-label {
138
+ font-size: 0.8rem;
139
+ font-weight: 700;
140
+ color: rgb(100, 116, 139);
141
+ text-transform: uppercase;
142
+ letter-spacing: 0.06em;
143
+ }
144
+
145
+ :global(.theme-dark) .ss-panel-label {
146
+ color: rgb(148, 163, 184);
147
+ }
148
+
149
+ .ss-textarea {
150
+ width: 100%;
151
+ min-height: 260px;
152
+ resize: vertical;
153
+ background: rgba(0, 0, 0, 0.03);
154
+ border: 1.5px solid rgb(226, 232, 240);
155
+ border-radius: 0.875rem;
156
+ padding: 1rem;
157
+ font-size: 0.8rem;
158
+ line-height: 1.6;
159
+ color: rgb(51, 65, 85);
160
+ outline: none;
161
+ transition: border-color 0.2s ease;
162
+ box-sizing: border-box;
163
+ }
164
+
165
+ :global(.theme-dark) .ss-textarea {
166
+ background: rgba(255, 255, 255, 0.04);
167
+ border-color: rgb(51, 65, 85);
168
+ color: rgb(203, 213, 225);
169
+ }
170
+
171
+ .ss-textarea:focus {
172
+ border-color: rgb(139, 92, 246);
173
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
174
+ }
175
+
176
+ .ss-output-textarea {
177
+ min-height: 220px;
178
+ cursor: text;
179
+ }
180
+
181
+ .ss-options-row {
182
+ display: flex;
183
+ flex-wrap: wrap;
184
+ gap: 1rem;
185
+ }
186
+
187
+ .ss-toggle-label {
188
+ display: flex;
189
+ align-items: center;
190
+ gap: 0.5rem;
191
+ font-size: 0.875rem;
192
+ color: rgb(71, 85, 105);
193
+ cursor: pointer;
194
+ user-select: none;
195
+ }
196
+
197
+ :global(.theme-dark) .ss-toggle-label {
198
+ color: rgb(148, 163, 184);
199
+ }
200
+
201
+ .ss-toggle-input {
202
+ display: none;
203
+ }
204
+
205
+ .ss-toggle-track {
206
+ width: 36px;
207
+ height: 20px;
208
+ background: rgb(226, 232, 240);
209
+ border-radius: 999px;
210
+ position: relative;
211
+ transition: background 0.2s ease;
212
+ flex-shrink: 0;
213
+ }
214
+
215
+ .ss-toggle-track::after {
216
+ content: "";
217
+ position: absolute;
218
+ top: 2px;
219
+ left: 2px;
220
+ width: 16px;
221
+ height: 16px;
222
+ background: white;
223
+ border-radius: 50%;
224
+ transition: transform 0.2s ease;
225
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
226
+ }
227
+
228
+ .ss-toggle-input:checked + .ss-toggle-track {
229
+ background: rgb(139, 92, 246);
230
+ }
231
+
232
+ .ss-toggle-input:checked + .ss-toggle-track::after {
233
+ transform: translateX(16px);
234
+ }
235
+
236
+ .ss-btn-sanitize {
237
+ background: linear-gradient(135deg, rgb(139, 92, 246), rgb(124, 58, 237));
238
+ color: white;
239
+ border: none;
240
+ border-radius: 0.875rem;
241
+ padding: 0.875rem 1.5rem;
242
+ font-weight: 700;
243
+ font-size: 1rem;
244
+ cursor: pointer;
245
+ display: flex;
246
+ align-items: center;
247
+ justify-content: center;
248
+ gap: 0.5rem;
249
+ transition: all 0.2s ease;
250
+ box-shadow: 0 4px 14px rgba(139, 92, 246, 0.35);
251
+ }
252
+
253
+ .ss-btn-sanitize:hover {
254
+ transform: translateY(-2px);
255
+ box-shadow: 0 6px 20px rgba(139, 92, 246, 0.5);
256
+ }
257
+
258
+ .ss-error {
259
+ font-size: 0.875rem;
260
+ color: rgb(239, 68, 68);
261
+ font-weight: 500;
262
+ margin: 0;
263
+ }
264
+
265
+ .ss-output-header {
266
+ display: flex;
267
+ justify-content: space-between;
268
+ align-items: center;
269
+ flex-wrap: wrap;
270
+ gap: 0.5rem;
271
+ }
272
+
273
+ .ss-output-label {
274
+ font-size: 0.8rem;
275
+ font-weight: 700;
276
+ color: rgb(139, 92, 246);
277
+ text-transform: uppercase;
278
+ letter-spacing: 0.06em;
279
+ }
280
+
281
+ .ss-output-actions {
282
+ display: flex;
283
+ gap: 0.5rem;
284
+ }
285
+
286
+ .ss-btn-action {
287
+ display: flex;
288
+ align-items: center;
289
+ gap: 0.35rem;
290
+ font-size: 0.8rem;
291
+ font-weight: 600;
292
+ padding: 0.4rem 0.875rem;
293
+ border-radius: 2rem;
294
+ border: 1.5px solid rgb(226, 232, 240);
295
+ background: white;
296
+ color: rgb(71, 85, 105);
297
+ cursor: pointer;
298
+ transition: all 0.2s ease;
299
+ }
300
+
301
+ :global(.theme-dark) .ss-btn-action {
302
+ background: rgb(30, 41, 59);
303
+ border-color: rgb(51, 65, 85);
304
+ color: rgb(148, 163, 184);
305
+ }
306
+
307
+ .ss-btn-action:hover {
308
+ border-color: rgb(139, 92, 246);
309
+ color: rgb(139, 92, 246);
310
+ }
311
+
312
+ .ss-btn-action.ss-copied {
313
+ background: rgb(16, 185, 129);
314
+ border-color: rgb(16, 185, 129);
315
+ color: white;
316
+ }
317
+
318
+ .ss-btn-download:hover {
319
+ border-color: rgb(99, 102, 241);
320
+ color: rgb(99, 102, 241);
321
+ }
322
+
323
+ .ss-stats-row {
324
+ display: grid;
325
+ grid-template-columns: repeat(5, 1fr);
326
+ gap: 0.5rem;
327
+ }
328
+
329
+ @media (max-width: 600px) {
330
+ .ss-stats-row {
331
+ grid-template-columns: repeat(3, 1fr);
332
+ }
333
+ }
334
+
335
+ .ss-stat-chip {
336
+ display: flex;
337
+ flex-direction: column;
338
+ align-items: center;
339
+ gap: 0.2rem;
340
+ padding: 0.625rem 0.375rem;
341
+ background: rgba(0, 0, 0, 0.025);
342
+ border-radius: 0.75rem;
343
+ text-align: center;
344
+ border-top: 2px solid rgb(226, 232, 240);
345
+ }
346
+
347
+ :global(.theme-dark) .ss-stat-chip {
348
+ background: rgba(255, 255, 255, 0.04);
349
+ border-color: rgb(51, 65, 85);
350
+ }
351
+
352
+ .ss-chip-green {
353
+ border-color: rgb(16, 185, 129);
354
+ }
355
+
356
+ .ss-chip-purple {
357
+ border-color: rgb(139, 92, 246);
358
+ }
359
+
360
+ .ss-chip-amber {
361
+ border-color: rgb(245, 158, 11);
362
+ }
363
+
364
+ .ss-chip-rose {
365
+ border-color: rgb(244, 63, 94);
366
+ }
367
+
368
+ .ss-chip-value {
369
+ font-size: 0.9rem;
370
+ font-weight: 800;
371
+ color: rgb(15, 23, 42);
372
+ line-height: 1.2;
373
+ }
374
+
375
+ :global(.theme-dark) .ss-chip-value {
376
+ color: white;
377
+ }
378
+
379
+ .ss-chip-label {
380
+ font-size: 0.6rem;
381
+ color: rgb(148, 163, 184);
382
+ font-weight: 600;
383
+ text-transform: uppercase;
384
+ letter-spacing: 0.04em;
385
+ line-height: 1.3;
386
+ }
387
+ </style>
388
+
389
+ <script>
390
+ import type { SvgSanitizerUI } from './ui';
391
+
392
+ interface SanitizeOptions { removeIds: boolean; removeWidthHeight: boolean; minify: boolean; }
393
+ interface SanitizeResult { svg: string; originalBytes: number; cleanedBytes: number; removedElements: number; removedAttrs: number; }
394
+
395
+ const BAD_ATTRS = new Set(['xml:space', 'version', 'xmlns:dc', 'xmlns:cc', 'xmlns:rdf', 'xmlns:svg', 'xmlns:inkscape', 'xmlns:sodipodi', 'xmlns:xlink', 'enable-background']);
396
+ const BAD_ATTR_PREFIXES = ['data-', 'inkscape:', 'sodipodi:'];
397
+ const METADATA_TAGS = ['metadata', 'title', 'desc'];
398
+ const NS_PATTERNS = [/<sodipodi:[^>]*(?:\/>|>[\s\S]*?<\/sodipodi:[^>]*>)/gi, /<inkscape:[^>]*(?:\/>|>[\s\S]*?<\/inkscape:[^>]*>)/gi];
399
+
400
+ function stripNamespacedElements(text: string): { text: string; count: number } {
401
+ let count = 0;
402
+ let result = text;
403
+ for (const pattern of NS_PATTERNS) {
404
+ result = result.replace(pattern, () => { count++; return ''; });
405
+ }
406
+ return { text: result, count };
407
+ }
408
+
409
+ function prettify(svg: string): string {
410
+ let indent = 0;
411
+ return svg.replace(/></g, '>\n<').split('\n').map((line) => {
412
+ line = line.trim();
413
+ if (!line) return '';
414
+ if (line.startsWith('</')) indent = Math.max(0, indent - 1);
415
+ const out = ' '.repeat(indent) + line;
416
+ if (!line.startsWith('</') && !line.endsWith('/>')) indent++;
417
+ return out;
418
+ }).filter(Boolean).join('\n');
419
+ }
420
+
421
+ function parseDoc(text: string): Document {
422
+ const parser = new DOMParser();
423
+ const doc = parser.parseFromString(text, 'image/svg+xml');
424
+ if (!doc.querySelector('parsererror')) return doc;
425
+ const htmlDoc = new DOMParser().parseFromString(`<body>${text}</body>`, 'text/html');
426
+ const found = htmlDoc.querySelector('svg');
427
+ if (!found) throw new Error('No valid SVG found.');
428
+ return parser.parseFromString(found.outerHTML, 'image/svg+xml');
429
+ }
430
+
431
+ function removeElements(svgEl: Element): number {
432
+ let removed = 0;
433
+ for (const tag of METADATA_TAGS) {
434
+ svgEl.querySelectorAll(tag).forEach((el) => { el.remove(); removed++; });
435
+ }
436
+ svgEl.querySelectorAll('defs').forEach((defs) => {
437
+ if (defs.children.length === 0) { defs.remove(); removed++; }
438
+ });
439
+ let changed = true;
440
+ while (changed) {
441
+ changed = false;
442
+ svgEl.querySelectorAll('g').forEach((g) => {
443
+ if (g.children.length === 0 && !g.textContent?.trim()) { g.remove(); removed++; changed = true; }
444
+ });
445
+ }
446
+ return removed;
447
+ }
448
+
449
+ function isBadAttr(n: string, el: Element, options: SanitizeOptions): boolean {
450
+ if (BAD_ATTRS.has(n)) return true;
451
+ if (BAD_ATTR_PREFIXES.some((p) => n.startsWith(p))) return true;
452
+ if (options.removeIds && n === 'id') return true;
453
+ if (!options.removeWidthHeight || el.tagName !== 'svg') return false;
454
+ return n === 'width' || n === 'height';
455
+ }
456
+
457
+ function removeAttrs(svgEl: Element, options: SanitizeOptions): number {
458
+ let removed = 0;
459
+ const allEls = [svgEl, ...Array.from(svgEl.querySelectorAll('*'))];
460
+ for (const el of allEls) {
461
+ const toRemove: string[] = [];
462
+ for (const attr of el.attributes) {
463
+ if (isBadAttr(attr.name, el, options)) toRemove.push(attr.name);
464
+ }
465
+ toRemove.forEach((a) => { el.removeAttribute(a); removed++; });
466
+ }
467
+ return removed;
468
+ }
469
+
470
+ function sanitizeSVG(input: string, options: SanitizeOptions): SanitizeResult {
471
+ const originalBytes = new TextEncoder().encode(input).length;
472
+ let text = input.trim()
473
+ .replace(/<\?xml[^?]*\?>\s*/gi, '')
474
+ .replace(/<!DOCTYPE[^>]*>\s*/gi, '')
475
+ .replace(/<!-{2}[\s\S]*?-{2}>/g, '');
476
+ const stripped = stripNamespacedElements(text);
477
+ text = stripped.text;
478
+ const doc = parseDoc(text);
479
+ const svgEl = doc.querySelector('svg') ?? doc.documentElement;
480
+ const removedElements = stripped.count + removeElements(svgEl);
481
+ const removedAttrs = removeAttrs(svgEl, options);
482
+ let svg = new XMLSerializer().serializeToString(svgEl);
483
+ if (!svg.includes('xlink:href')) svg = svg.replace(/\s*xmlns:xlink="[^"]*"/g, '');
484
+ svg = options.minify ? svg.replace(/\s+/g, ' ').replace(/> </g, '><').trim() : prettify(svg);
485
+ const cleanedBytes = new TextEncoder().encode(svg).length;
486
+ return { svg, originalBytes, cleanedBytes, removedElements, removedAttrs };
487
+ }
488
+
489
+ const root = document.getElementById('ss-root') as HTMLElement;
490
+ const t = JSON.parse(root.dataset.i18n ?? '{}') as SvgSanitizerUI;
491
+ const layout = document.getElementById('ss-layout') as HTMLElement;
492
+ const textarea = document.getElementById('ss-input-area') as HTMLTextAreaElement;
493
+ const optIds = document.getElementById('ss-opt-ids') as HTMLInputElement;
494
+ const optWH = document.getElementById('ss-opt-wh') as HTMLInputElement;
495
+ const optMinify = document.getElementById('ss-opt-minify') as HTMLInputElement;
496
+ const errorEl = document.getElementById('ss-error') as HTMLElement;
497
+ const outputPanel = document.getElementById('ss-output-panel') as HTMLElement;
498
+ const outputArea = document.getElementById('ss-output-area') as HTMLTextAreaElement;
499
+ const copyBtn = document.getElementById('ss-btn-copy') as HTMLButtonElement;
500
+ const statOriginal = document.getElementById('ss-stat-original') as HTMLElement;
501
+ const statCleaned = document.getElementById('ss-stat-cleaned') as HTMLElement;
502
+ const statReduction = document.getElementById('ss-stat-reduction') as HTMLElement;
503
+ const statElems = document.getElementById('ss-stat-elems') as HTMLElement;
504
+ const statAttrs = document.getElementById('ss-stat-attrs') as HTMLElement;
505
+
506
+ const svgCopied = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/></svg>';
507
+
508
+ function fmt(bytes: number): string {
509
+ if (bytes < 1024) return `${bytes} B`;
510
+ return `${(bytes / 1024).toFixed(1)} KB`;
511
+ }
512
+
513
+ function showResult(r: SanitizeResult): void {
514
+ outputArea.value = r.svg;
515
+ const pct = Math.round(((r.originalBytes - r.cleanedBytes) / r.originalBytes) * 100);
516
+ statOriginal.textContent = fmt(r.originalBytes);
517
+ statCleaned.textContent = fmt(r.cleanedBytes);
518
+ statReduction.textContent = `${pct}%`;
519
+ statElems.textContent = r.removedElements.toString();
520
+ statAttrs.textContent = r.removedAttrs.toString();
521
+ outputPanel.style.display = '';
522
+ layout.classList.add('ss-has-output');
523
+ }
524
+
525
+ document.getElementById('ss-btn-sanitize')!.addEventListener('click', () => {
526
+ const input = textarea.value.trim();
527
+ errorEl.style.display = 'none';
528
+ if (!input) { errorEl.textContent = t.errorPaste; errorEl.style.display = ''; return; }
529
+ const options: SanitizeOptions = { removeIds: optIds.checked, removeWidthHeight: optWH.checked, minify: optMinify.checked };
530
+ try {
531
+ showResult(sanitizeSVG(input, options));
532
+ } catch (e) {
533
+ errorEl.textContent = e instanceof Error ? e.message : t.errorProcess;
534
+ errorEl.style.display = '';
535
+ }
536
+ });
537
+
538
+ copyBtn.addEventListener('click', () => {
539
+ navigator.clipboard.writeText(outputArea.value);
540
+ const origInner = copyBtn.innerHTML;
541
+ copyBtn.innerHTML = `${svgCopied} ${t.btnCopied}`;
542
+ copyBtn.classList.add('ss-copied');
543
+ setTimeout(() => { copyBtn.innerHTML = origInner; copyBtn.classList.remove('ss-copied'); }, 2000);
544
+ });
545
+
546
+ document.getElementById('ss-btn-download')!.addEventListener('click', () => {
547
+ const blob = new Blob([outputArea.value], { type: 'image/svg+xml' });
548
+ const a = document.createElement('a');
549
+ a.href = URL.createObjectURL(blob);
550
+ a.download = 'optimized.svg';
551
+ a.click();
552
+ URL.revokeObjectURL(a.href);
553
+ });
554
+ </script>
@@ -0,0 +1,125 @@
1
+ import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { SvgSanitizerUI } from '../ui';
4
+
5
+ const slug = 'svg-sanitizer';
6
+ const title = 'Online SVG Sanitizer';
7
+ const description = 'Optimize and clean SVGs exported from Figma, Adobe Illustrator or Inkscape. Remove metadata, unnecessary attributes and empty groups to get a production-ready SVG.';
8
+
9
+ const faqData = [
10
+ {
11
+ question: 'Can I paste the full HTML of a page with an embedded SVG?',
12
+ answer: 'Yes. The sanitizer detects the SVG element inside the HTML and extracts only that block for processing. It also works if you paste the SVG fragment directly.',
13
+ },
14
+ {
15
+ question: 'Does it work with Illustrator SVGs?',
16
+ answer: 'Yes. Illustrator exports SVGs with XML declarations, metadata and proprietary namespaces that the sanitizer removes. The result is an SVG compatible with all modern browsers.',
17
+ },
18
+ {
19
+ question: 'What is the difference between cleaning and minifying?',
20
+ answer: 'Cleaning removes unnecessary attributes and tags but keeps the indented format so you can read and edit the code. Minifying also collapses everything into a single line to reduce file size to the maximum.',
21
+ },
22
+ {
23
+ question: 'Can removing IDs break the SVG?',
24
+ answer: 'Only if an element in the SVG references another by ID, for example through fill="url(#gradient)". In that case, disable the Remove IDs option. It is disabled by default precisely to avoid this problem.',
25
+ },
26
+ {
27
+ question: 'Is my SVG code sent to any server?',
28
+ answer: 'No. All processing happens in your browser using the native DOMParser and XMLSerializer APIs. The code never leaves your device.',
29
+ },
30
+ ];
31
+
32
+ const howToData = [
33
+ { name: 'Paste the SVG', text: 'Paste the SVG code exported from Figma, Illustrator or Inkscape into the text area.' },
34
+ { name: 'Configure options', text: 'Enable or disable options: remove IDs, strip width/height and minify according to your needs.' },
35
+ { name: 'Clean', text: 'Click Clean SVG to process the code and get the optimized result.' },
36
+ { name: 'Copy or download', text: 'Use the Copy or Download buttons to get the clean SVG ready for production.' },
37
+ ];
38
+
39
+ const faqSchema: WithContext<FAQPage> = {
40
+ '@context': 'https://schema.org',
41
+ '@type': 'FAQPage',
42
+ mainEntity: faqData.map((item) => ({
43
+ '@type': 'Question',
44
+ name: item.question,
45
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
46
+ })),
47
+ };
48
+
49
+ const howToSchema: WithContext<HowToThing> = {
50
+ '@context': 'https://schema.org',
51
+ '@type': 'HowTo',
52
+ name: title,
53
+ description,
54
+ step: howToData.map((s, i) => ({ '@type': 'HowToStep', position: i + 1, name: s.name, text: s.text })),
55
+ };
56
+
57
+ const appSchema: WithContext<SoftwareApplication> = {
58
+ '@context': 'https://schema.org',
59
+ '@type': 'SoftwareApplication',
60
+ name: title,
61
+ description,
62
+ applicationCategory: 'UtilityApplication',
63
+ operatingSystem: 'All',
64
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
65
+ inLanguage: 'en',
66
+ };
67
+
68
+ const ui: SvgSanitizerUI = {
69
+ labelInput: 'Paste your dirty SVG here',
70
+ labelOutput: 'Clean SVG',
71
+ optRemoveIds: 'Remove IDs',
72
+ optRemoveWH: 'Strip width/height',
73
+ optMinify: 'Minify',
74
+ btnSanitize: 'Clean SVG',
75
+ btnCopy: 'Copy',
76
+ btnCopied: 'Copied',
77
+ btnDownload: 'Download',
78
+ statOriginal: 'Original',
79
+ statCleaned: 'Clean',
80
+ statReduction: 'Reduction',
81
+ statElems: 'Elem. removed',
82
+ statAttrs: 'Attrs. removed',
83
+ errorPaste: 'Paste an SVG before cleaning.',
84
+ errorProcess: 'Error processing the SVG.',
85
+ };
86
+
87
+ export const content: ToolLocaleContent<SvgSanitizerUI> = {
88
+ slug,
89
+ title,
90
+ description,
91
+ ui,
92
+ faqTitle: 'Frequently Asked Questions',
93
+ faq: faqData,
94
+ bibliographyTitle: 'References',
95
+ bibliography: [
96
+ { name: 'SVG Specification - W3C', url: 'https://www.w3.org/TR/SVG2/' },
97
+ { name: 'Figma SVG Export - Official Documentation', url: 'https://help.figma.com/hc/en-us/articles/360040028114-Export-from-Figma' },
98
+ { name: 'SVGO - SVG Optimizer (open source reference)', url: 'https://github.com/svg/svgo' },
99
+ ],
100
+ howTo: howToData,
101
+ schemas: [faqSchema, howToSchema, appSchema],
102
+ seo: [
103
+ { type: 'title', text: 'SVG Sanitizer: Clean Code from Figma and Illustrator', level: 2 },
104
+ {
105
+ type: 'paragraph',
106
+ html: 'Paste any SVG exported from <strong>Figma</strong>, Adobe Illustrator or the browser inspector and get a clean, optimized vector file ready for production.',
107
+ },
108
+ { type: 'title', text: 'Why are exported SVGs so dirty?', level: 3 },
109
+ {
110
+ type: 'paragraph',
111
+ html: 'When you export an SVG from Figma, you get a file loaded with attributes that only make sense inside the app: <code>data-name</code>, <code>xml:space</code>, references to internal layers and design metadata. The result is an SVG that can weigh 40-70% more than necessary.',
112
+ },
113
+ { type: 'title', text: 'What the Sanitizer removes', level: 3 },
114
+ {
115
+ type: 'list',
116
+ items: [
117
+ '<strong>Editor metadata:</strong> <code>metadata</code>, <code>title</code> and <code>desc</code> tags that Figma and Illustrator add automatically.',
118
+ '<strong>Inkscape namespaces:</strong> all elements with <code>inkscape:</code> and <code>sodipodi:</code> prefix.',
119
+ '<strong>Unnecessary attributes:</strong> <code>xml:space</code>, <code>version</code>, superfluous <code>xmlns:*</code> and Figma <code>data-*</code> attributes.',
120
+ '<strong>Empty groups:</strong> <code>&lt;g&gt;</code> elements with no content left as artifacts of deleted layers.',
121
+ '<strong>XML declarations:</strong> the <code>&lt;?xml version="1.0"?&gt;</code> declaration and DOCTYPE unnecessary when embedding SVG in HTML.',
122
+ ],
123
+ },
124
+ ],
125
+ };