@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,667 @@
1
+ ---
2
+ import type { KnownLocale } from '../../types';
3
+ import type { CronGeneratorUI } 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 CronGeneratorUI;
12
+
13
+ const presets = [
14
+ { label: t.presetEveryMinute, cron: '* * * * *' },
15
+ { label: t.presetEveryHour, cron: '0 * * * *' },
16
+ { label: t.presetDaily, cron: '0 0 * * *' },
17
+ { label: t.presetWeekly, cron: '0 0 * * 0' },
18
+ { label: t.presetMonthly, cron: '0 0 1 * *' },
19
+ { label: t.presetYearly, cron: '0 0 1 1 *' },
20
+ ];
21
+
22
+ const dispFields = [
23
+ { id: 'minute', label: t.labelMinute },
24
+ { id: 'hour', label: t.labelHour },
25
+ { id: 'dom', label: t.labelDom },
26
+ { id: 'month', label: t.labelMonth },
27
+ { id: 'dow', label: t.labelDow },
28
+ ];
29
+
30
+ const advFields = [
31
+ { id: 'minute', label: t.fieldMinute, hint: t.hintMinute },
32
+ { id: 'hour', label: t.fieldHour, hint: '' },
33
+ { id: 'dom', label: t.fieldDom, hint: '' },
34
+ { id: 'month', label: t.fieldMonth, hint: '' },
35
+ { id: 'dow', label: t.fieldDow, hint: t.hintDow },
36
+ ];
37
+ ---
38
+
39
+ <div class="cron-root" id="cron-root" data-i18n={JSON.stringify(t)}>
40
+ <div class="cron-outer-card">
41
+ <div class="cron-hero">
42
+ <div class="cron-expr-row">
43
+ {dispFields.map(({ id, label }) => (
44
+ <div class="cron-part-wrapper">
45
+ <span id={`cron-disp-${id}`} class="cron-disp-part">*</span>
46
+ <span class="cron-part-label">{label}</span>
47
+ </div>
48
+ ))}
49
+ </div>
50
+ <div class="cron-human-wrap">
51
+ <span id="cron-human-readable" class="cron-human-text">{t.descEveryMinute}</span>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="cron-body">
56
+ <div class="cron-grid">
57
+ <div class="cron-controls">
58
+ <div class="cron-tabs">
59
+ <button class="cron-tab-btn" data-tab="quick" data-active>{t.tabQuick}</button>
60
+ <button class="cron-tab-btn" data-tab="advanced">{t.tabAdvanced}</button>
61
+ </div>
62
+
63
+ <div id="cron-tab-quick">
64
+ <div class="cron-presets">
65
+ {presets.map(({ label, cron }) => (
66
+ <button class="cron-preset-btn" data-cron={cron}>
67
+ <span class="cron-preset-label">{label}</span>
68
+ <span class="cron-preset-expr">{cron}</span>
69
+ </button>
70
+ ))}
71
+ </div>
72
+ </div>
73
+
74
+ <div id="cron-tab-advanced" style="display:none">
75
+ {advFields.map(({ id, label, hint }) => (
76
+ <div class="cron-field">
77
+ <label for={`cron-input-${id}`} class="cron-field-label">{label}</label>
78
+ <input type="text" id={`cron-input-${id}`} class="cron-input" value="*" autocomplete="off" spellcheck="false" />
79
+ {hint && <span class="cron-field-hint">{hint}</span>}
80
+ </div>
81
+ ))}
82
+ </div>
83
+ </div>
84
+
85
+ <div class="cron-runs-section">
86
+ <h3 class="cron-runs-title">{t.labelNextRuns}</h3>
87
+ <ul id="cron-next-runs" class="cron-runs-list">
88
+ {[...Array(5)].map(() => <li class="cron-run-skeleton"></li>)}
89
+ </ul>
90
+ </div>
91
+ </div>
92
+
93
+ <button id="cron-btn-copy" class="cron-copy-btn">{t.btnCopy}</button>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <style>
99
+ .cron-root {
100
+ --cron-primary: #8b5cf6;
101
+ --cron-success: #10b981;
102
+ --cron-card-bg: #fff;
103
+ --cron-bg: #f8fafc;
104
+ --cron-text: #0f172a;
105
+ --cron-text-muted: #64748b;
106
+ --cron-border: #e2e8f0;
107
+
108
+ max-width: 900px;
109
+ margin: 2rem auto;
110
+ color: var(--cron-text);
111
+ }
112
+
113
+ :global(.theme-dark) .cron-root {
114
+ --cron-card-bg: #1e293b;
115
+ --cron-bg: #0f172a;
116
+ --cron-text: #f1f5f9;
117
+ --cron-text-muted: #94a3b8;
118
+ --cron-border: #334155;
119
+ }
120
+
121
+ .cron-outer-card {
122
+ background: var(--cron-card-bg);
123
+ border: 1px solid var(--cron-border);
124
+ border-radius: 1.5rem;
125
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.07);
126
+ overflow: hidden;
127
+ }
128
+
129
+ .cron-hero {
130
+ background: #0f172a;
131
+ padding: 2.25rem 2rem 1.75rem;
132
+ display: flex;
133
+ flex-direction: column;
134
+ align-items: center;
135
+ gap: 1.25rem;
136
+ }
137
+
138
+ .cron-expr-row {
139
+ display: flex;
140
+ align-items: flex-start;
141
+ gap: 1.25rem;
142
+ flex-wrap: wrap;
143
+ justify-content: center;
144
+ }
145
+
146
+ .cron-part-wrapper {
147
+ display: flex;
148
+ flex-direction: column;
149
+ align-items: center;
150
+ gap: 0.375rem;
151
+ }
152
+
153
+ .cron-disp-part {
154
+ font-size: 3rem;
155
+ font-weight: 800;
156
+ color: #a78bfa;
157
+ line-height: 1;
158
+ font-variant-numeric: tabular-nums;
159
+ transition: color 0.2s;
160
+ }
161
+
162
+ .cron-part-label {
163
+ font-size: 0.65rem;
164
+ font-weight: 600;
165
+ color: #475569;
166
+ text-transform: uppercase;
167
+ letter-spacing: 0.08em;
168
+ white-space: nowrap;
169
+ }
170
+
171
+ .cron-human-wrap {
172
+ background: rgba(255, 255, 255, 0.06);
173
+ border: 1px solid rgba(255, 255, 255, 0.1);
174
+ border-radius: 0.75rem;
175
+ padding: 0.5rem 1.25rem;
176
+ }
177
+
178
+ .cron-human-text {
179
+ color: #cbd5e1;
180
+ font-size: 0.925rem;
181
+ font-weight: 500;
182
+ }
183
+
184
+ .cron-human-text[data-error] {
185
+ color: #f87171;
186
+ }
187
+
188
+ .cron-body {
189
+ padding: 1.5rem;
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: 1.25rem;
193
+ }
194
+
195
+ .cron-grid {
196
+ display: grid;
197
+ grid-template-columns: 1fr 1fr;
198
+ gap: 1.25rem;
199
+ }
200
+
201
+ @media (max-width: 700px) {
202
+ .cron-grid {
203
+ grid-template-columns: 1fr;
204
+ }
205
+ }
206
+
207
+ .cron-controls {
208
+ background: var(--cron-bg);
209
+ border: 1px solid var(--cron-border);
210
+ border-radius: 1rem;
211
+ padding: 1.125rem;
212
+ }
213
+
214
+ .cron-tabs {
215
+ display: flex;
216
+ gap: 0.5rem;
217
+ margin-bottom: 1.125rem;
218
+ border-bottom: 1px solid var(--cron-border);
219
+ padding-bottom: 0.875rem;
220
+ }
221
+
222
+ .cron-tab-btn {
223
+ padding: 0.375rem 1rem;
224
+ border-radius: 0.5rem;
225
+ border: none;
226
+ background: transparent;
227
+ color: var(--cron-text-muted);
228
+ font-size: 0.875rem;
229
+ font-weight: 600;
230
+ cursor: pointer;
231
+ transition: background 0.15s, color 0.15s;
232
+ }
233
+
234
+ .cron-tab-btn[data-active] {
235
+ background: rgba(139, 92, 246, 0.12);
236
+ color: var(--cron-primary);
237
+ }
238
+
239
+ .cron-presets {
240
+ display: grid;
241
+ grid-template-columns: 1fr 1fr;
242
+ gap: 0.5rem;
243
+ }
244
+
245
+ .cron-preset-btn {
246
+ display: flex;
247
+ flex-direction: column;
248
+ align-items: flex-start;
249
+ gap: 0.2rem;
250
+ padding: 0.75rem;
251
+ background: var(--cron-card-bg);
252
+ border: 1px solid var(--cron-border);
253
+ border-radius: 0.75rem;
254
+ cursor: pointer;
255
+ text-align: left;
256
+ transition: border-color 0.15s, background 0.15s;
257
+ }
258
+
259
+ .cron-preset-btn:hover {
260
+ border-color: var(--cron-primary);
261
+ background: rgba(139, 92, 246, 0.06);
262
+ }
263
+
264
+ .cron-preset-label {
265
+ font-size: 0.825rem;
266
+ font-weight: 700;
267
+ color: var(--cron-text);
268
+ }
269
+
270
+ .cron-preset-expr {
271
+ font-size: 0.72rem;
272
+ color: var(--cron-text-muted);
273
+ }
274
+
275
+ .cron-field {
276
+ display: flex;
277
+ flex-direction: column;
278
+ gap: 0.3rem;
279
+ margin-bottom: 0.75rem;
280
+ }
281
+
282
+ .cron-field:last-child {
283
+ margin-bottom: 0;
284
+ }
285
+
286
+ .cron-field-label {
287
+ font-size: 0.78rem;
288
+ font-weight: 600;
289
+ color: var(--cron-text-muted);
290
+ }
291
+
292
+ .cron-input {
293
+ width: 100%;
294
+ padding: 0.45rem 0.75rem;
295
+ font-size: 0.875rem;
296
+ background: var(--cron-card-bg);
297
+ border: 1px solid var(--cron-border);
298
+ border-radius: 0.5rem;
299
+ color: var(--cron-text);
300
+ outline: none;
301
+ box-sizing: border-box;
302
+ transition: border-color 0.2s, box-shadow 0.2s;
303
+ }
304
+
305
+ .cron-input:focus {
306
+ border-color: var(--cron-primary);
307
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.14);
308
+ }
309
+
310
+ .cron-field-hint {
311
+ font-size: 0.7rem;
312
+ color: var(--cron-text-muted);
313
+ }
314
+
315
+ .cron-runs-section {
316
+ background: var(--cron-bg);
317
+ border: 1px solid var(--cron-border);
318
+ border-radius: 1rem;
319
+ padding: 1.125rem;
320
+ display: flex;
321
+ flex-direction: column;
322
+ }
323
+
324
+ .cron-runs-title {
325
+ font-size: 0.8rem;
326
+ font-weight: 700;
327
+ color: var(--cron-text-muted);
328
+ text-transform: uppercase;
329
+ letter-spacing: 0.06em;
330
+ margin: 0 0 0.75rem;
331
+ }
332
+
333
+ .cron-runs-list {
334
+ list-style: none;
335
+ margin: 0;
336
+ padding: 0;
337
+ display: flex;
338
+ flex-direction: column;
339
+ gap: 0.4rem;
340
+ flex: 1;
341
+ }
342
+
343
+ .cron-run-skeleton {
344
+ height: 2.25rem;
345
+ background: var(--cron-card-bg);
346
+ border-radius: 0.5rem;
347
+ border: 1px solid var(--cron-border);
348
+ }
349
+
350
+ :global(.cron-run-item) {
351
+ display: flex;
352
+ align-items: center;
353
+ justify-content: space-between;
354
+ padding: 0.5rem 0.75rem;
355
+ background: var(--cron-card-bg);
356
+ border: 1px solid var(--cron-border);
357
+ border-radius: 0.5rem;
358
+ font-size: 0.825rem;
359
+ }
360
+
361
+ :global(.cron-run-date) {
362
+ font-weight: 600;
363
+ color: var(--cron-text);
364
+ text-transform: capitalize;
365
+ }
366
+
367
+ :global(.cron-run-rel) {
368
+ font-size: 0.72rem;
369
+ color: var(--cron-text-muted);
370
+ }
371
+
372
+ :global(.cron-no-runs) {
373
+ padding: 1rem;
374
+ text-align: center;
375
+ color: var(--cron-text-muted);
376
+ font-size: 0.825rem;
377
+ font-style: italic;
378
+ }
379
+
380
+ .cron-copy-btn {
381
+ width: 100%;
382
+ padding: 0.8rem;
383
+ background: var(--cron-primary);
384
+ color: #fff;
385
+ border: none;
386
+ border-radius: 0.875rem;
387
+ font-size: 0.925rem;
388
+ font-weight: 700;
389
+ cursor: pointer;
390
+ transition: background 0.2s, transform 0.1s;
391
+ }
392
+
393
+ .cron-copy-btn:hover {
394
+ background: #7c3aed;
395
+ }
396
+
397
+ .cron-copy-btn:active {
398
+ transform: scale(0.98);
399
+ }
400
+
401
+ .cron-copy-btn[data-copied] {
402
+ background: var(--cron-success);
403
+ }
404
+
405
+ @media (max-width: 480px) {
406
+ .cron-disp-part {
407
+ font-size: 2rem;
408
+ }
409
+
410
+ .cron-presets {
411
+ grid-template-columns: 1fr;
412
+ }
413
+ }
414
+ </style>
415
+
416
+ <script>
417
+ import type { CronGeneratorUI } from './ui';
418
+
419
+ type Displays = { minute: HTMLElement; hour: HTMLElement; dom: HTMLElement; month: HTMLElement; dow: HTMLElement };
420
+ type Inputs = { minute: HTMLInputElement; hour: HTMLInputElement; dom: HTMLInputElement; month: HTMLInputElement; dow: HTMLInputElement };
421
+ type DOMRefs = { desc: HTMLElement; list: HTMLElement; disps: Displays };
422
+
423
+ function inRange(v: number, min: number, max: number): boolean {
424
+ return v >= min && v <= max;
425
+ }
426
+
427
+ function parseStepPart(part: string, min: number, max: number): number[] {
428
+ const m = part.match(/^(\*|\d+(?:-\d+)?)\/(\d+)$/);
429
+ if (!m) return [];
430
+ const step = parseInt(m[2], 10);
431
+ let start = min, end = max;
432
+ if (m[1] !== '*') { const r = m[1].split('-').map(Number); start = r[0]; end = r[1] ?? max; }
433
+ const vals: number[] = [];
434
+ for (let i = start; i <= end; i += step) { if (inRange(i, min, max)) vals.push(i); }
435
+ return vals;
436
+ }
437
+
438
+ function parseRangePart(part: string, min: number, max: number): number[] {
439
+ if (!part.includes('-')) {
440
+ const v = parseInt(part, 10);
441
+ return inRange(v, min, max) ? [v] : [];
442
+ }
443
+ const [s, e] = part.split('-').map(Number);
444
+ const vals: number[] = [];
445
+ for (let i = s; i <= e; i++) { if (inRange(i, min, max)) vals.push(i); }
446
+ return vals;
447
+ }
448
+
449
+ function parsePart(partStr: string, min: number, max: number): number[] {
450
+ if (partStr === '*') return Array.from({ length: max - min + 1 }, (_, i) => i + min);
451
+ const values = new Set<number>();
452
+ for (const part of partStr.split(',')) {
453
+ const step = parseStepPart(part, min, max);
454
+ if (step.length) { step.forEach(v => values.add(v)); continue; }
455
+ parseRangePart(part, min, max).forEach(v => values.add(v));
456
+ }
457
+ return Array.from(values).sort((a, b) => a - b);
458
+ }
459
+
460
+ function describeTimePart(m: string, h: string, t: CronGeneratorUI): string {
461
+ if (m === '*' && h === '*') return t.descEveryMin;
462
+ if (m === '0' && h === '*') return t.descEveryHourOnDot;
463
+ if (h === '*') return t.descAtMinute.replace('{m}', m);
464
+ return t.descAtTime.replace('{h}', h).replace('{m}', m.padStart(2, '0'));
465
+ }
466
+
467
+ function describeDayPart(dom: string, mon: string, dow: string, t: CronGeneratorUI): string {
468
+ let s = '';
469
+ if (dom !== '*') s += t.descOnDay.replace('{d}', dom);
470
+ if (mon !== '*') s += t.descInMonth.replace('{mon}', mon);
471
+ if (dow !== '*') s += t.descIfDow.replace('{d}', dow);
472
+ return s;
473
+ }
474
+
475
+ function describeCron(parts: string[], t: CronGeneratorUI): string {
476
+ const expr = parts.join(' ');
477
+ if (expr === '* * * * *') return t.descEveryMinute;
478
+ if (expr === '0 * * * *') return t.descEveryHour;
479
+ if (expr === '0 0 * * *') return t.descEveryDay;
480
+ return t.descPrefix + ' ' + describeTimePart(parts[0], parts[1], t) + describeDayPart(parts[2], parts[3], parts[4], t);
481
+ }
482
+
483
+ function isDayValid(parts: string[], doms: number[], dows: number[], cursor: Date): boolean {
484
+ const domR = parts[2] !== '*';
485
+ const dowR = parts[4] !== '*';
486
+ if (!domR && !dowR) return true;
487
+ if (domR && dowR) return doms.includes(cursor.getDate()) || dows.includes(cursor.getDay());
488
+ return domR ? doms.includes(cursor.getDate()) : dows.includes(cursor.getDay());
489
+ }
490
+
491
+ function calculateNextRuns(parts: string[]): Date[] {
492
+ const mins = parsePart(parts[0], 0, 59);
493
+ const hrs = parsePart(parts[1], 0, 23);
494
+ const doms = parsePart(parts[2], 1, 31);
495
+ const mons = parsePart(parts[3], 1, 12);
496
+ const dows = parsePart(parts[4], 0, 6);
497
+ const runs: Date[] = [];
498
+ const cursor = new Date();
499
+ cursor.setSeconds(0); cursor.setMilliseconds(0);
500
+ cursor.setMinutes(cursor.getMinutes() + 1);
501
+ let iter = 0;
502
+ while (runs.length < 5 && iter++ < 20000) {
503
+ if (!mons.includes(cursor.getMonth() + 1)) { cursor.setMonth(cursor.getMonth() + 1); cursor.setDate(1); cursor.setHours(0); cursor.setMinutes(0); continue; }
504
+ if (!isDayValid(parts, doms, dows, cursor)) { cursor.setDate(cursor.getDate() + 1); cursor.setHours(0); cursor.setMinutes(0); continue; }
505
+ if (!hrs.includes(cursor.getHours())) { cursor.setHours(cursor.getHours() + 1); cursor.setMinutes(0); continue; }
506
+ if (mins.includes(cursor.getMinutes())) runs.push(new Date(cursor));
507
+ cursor.setMinutes(cursor.getMinutes() + 1);
508
+ }
509
+ return runs;
510
+ }
511
+
512
+ function getTimeRemaining(target: Date, t: CronGeneratorUI): string {
513
+ const diff = target.getTime() - Date.now();
514
+ if (diff < 0) return t.timeNow;
515
+ const mins = Math.floor(diff / 60000);
516
+ if (mins < 60) return `${mins} ${t.timeMin}`;
517
+ const hours = Math.floor(mins / 60);
518
+ if (hours < 24) return `${hours} ${t.timeHour}`;
519
+ return `${Math.floor(hours / 24)} ${t.timeDays}`;
520
+ }
521
+
522
+ function buildRunItem(date: Date, t: CronGeneratorUI): HTMLLIElement {
523
+ const li = document.createElement('li');
524
+ li.className = 'cron-run-item';
525
+ const dateStr = new Intl.DateTimeFormat(t.locale, { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }).format(date);
526
+ const left = document.createElement('span');
527
+ left.className = 'cron-run-date';
528
+ left.textContent = dateStr;
529
+ const right = document.createElement('span');
530
+ right.className = 'cron-run-rel';
531
+ right.textContent = getTimeRemaining(date, t);
532
+ li.append(left, right);
533
+ return li;
534
+ }
535
+
536
+ function setDisplaysError(disps: Displays, isError: boolean): void {
537
+ const color = isError ? '#f87171' : 'var(--cron-primary)';
538
+ const keys = ['minute', 'hour', 'dom', 'month', 'dow'] as const;
539
+ keys.forEach(k => { disps[k].style.color = color; });
540
+ }
541
+
542
+ function updateDescription(parts: string[], desc: HTMLElement, disps: Displays, t: CronGeneratorUI): void {
543
+ try {
544
+ desc.textContent = describeCron(parts, t);
545
+ desc.removeAttribute('data-error');
546
+ setDisplaysError(disps, false);
547
+ } catch {
548
+ desc.textContent = t.descInvalid;
549
+ desc.setAttribute('data-error', '');
550
+ setDisplaysError(disps, true);
551
+ }
552
+ }
553
+
554
+ function updateNextRuns(parts: string[], list: HTMLElement, t: CronGeneratorUI): void {
555
+ try {
556
+ const dates = calculateNextRuns(parts);
557
+ list.innerHTML = '';
558
+ if (!dates.length) { list.innerHTML = `<li class="cron-no-runs">${t.msgNoRuns}</li>`; return; }
559
+ dates.forEach(d => list.appendChild(buildRunItem(d, t)));
560
+ } catch {
561
+ list.innerHTML = `<li class="cron-no-runs">${t.msgError}</li>`;
562
+ }
563
+ }
564
+
565
+ function updateDisplays(parts: string[], disps: Displays): void {
566
+ const keys = ['minute', 'hour', 'dom', 'month', 'dow'] as const;
567
+ keys.forEach((k, i) => { disps[k].textContent = parts[i]; });
568
+ }
569
+
570
+ function syncInputs(parts: string[], inps: Inputs): void {
571
+ const keys = ['minute', 'hour', 'dom', 'month', 'dow'] as const;
572
+ keys.forEach((k, i) => { inps[k].value = parts[i]; });
573
+ }
574
+
575
+ function updateAll(parts: string[], refs: DOMRefs, t: CronGeneratorUI): void {
576
+ updateDisplays(parts, refs.disps);
577
+ updateDescription(parts, refs.desc, refs.disps, t);
578
+ updateNextRuns(parts, refs.list, t);
579
+ }
580
+
581
+ function getDisps(root: HTMLElement): Displays {
582
+ return {
583
+ minute: root.querySelector<HTMLElement>('#cron-disp-minute')!,
584
+ hour: root.querySelector<HTMLElement>('#cron-disp-hour')!,
585
+ dom: root.querySelector<HTMLElement>('#cron-disp-dom')!,
586
+ month: root.querySelector<HTMLElement>('#cron-disp-month')!,
587
+ dow: root.querySelector<HTMLElement>('#cron-disp-dow')!,
588
+ };
589
+ }
590
+
591
+ function getInps(root: HTMLElement): Inputs {
592
+ return {
593
+ minute: root.querySelector<HTMLInputElement>('#cron-input-minute')!,
594
+ hour: root.querySelector<HTMLInputElement>('#cron-input-hour')!,
595
+ dom: root.querySelector<HTMLInputElement>('#cron-input-dom')!,
596
+ month: root.querySelector<HTMLInputElement>('#cron-input-month')!,
597
+ dow: root.querySelector<HTMLInputElement>('#cron-input-dow')!,
598
+ };
599
+ }
600
+
601
+ function setupTabs(root: HTMLElement): void {
602
+ const tabs = root.querySelectorAll<HTMLElement>('.cron-tab-btn');
603
+ const quick = root.querySelector<HTMLElement>('#cron-tab-quick');
604
+ const adv = root.querySelector<HTMLElement>('#cron-tab-advanced');
605
+ tabs.forEach(btn => {
606
+ btn.addEventListener('click', () => {
607
+ tabs.forEach(t => t.removeAttribute('data-active'));
608
+ btn.setAttribute('data-active', '');
609
+ const isQuick = btn.dataset.tab === 'quick';
610
+ if (quick) quick.style.display = isQuick ? '' : 'none';
611
+ if (adv) adv.style.display = isQuick ? 'none' : '';
612
+ });
613
+ });
614
+ }
615
+
616
+ function setupPresets(root: HTMLElement, onChange: (parts: string[]) => void): void {
617
+ root.querySelectorAll<HTMLElement>('.cron-preset-btn').forEach(btn => {
618
+ btn.addEventListener('click', () => {
619
+ const cron = btn.dataset.cron;
620
+ if (cron) onChange(cron.split(' '));
621
+ });
622
+ });
623
+ }
624
+
625
+ function setupInputs(root: HTMLElement, onChange: (parts: string[]) => void, getParts: () => string[]): void {
626
+ const keys = ['minute', 'hour', 'dom', 'month', 'dow'];
627
+ keys.forEach((key, idx) => {
628
+ const inp = root.querySelector<HTMLInputElement>(`#cron-input-${key}`);
629
+ if (!inp) return;
630
+ inp.addEventListener('input', () => {
631
+ const parts = [...getParts()];
632
+ parts[idx] = inp.value.trim() || '*';
633
+ onChange(parts);
634
+ });
635
+ });
636
+ }
637
+
638
+ function setupCopyBtn(btn: HTMLElement, getExpr: () => string, t: CronGeneratorUI): void {
639
+ btn.addEventListener('click', () => {
640
+ navigator.clipboard.writeText(getExpr());
641
+ const orig = btn.textContent ?? '';
642
+ btn.textContent = t.btnCopied;
643
+ btn.setAttribute('data-copied', '');
644
+ setTimeout(() => { btn.textContent = orig; btn.removeAttribute('data-copied'); }, 2000);
645
+ });
646
+ }
647
+
648
+ function setup(root: HTMLElement): void {
649
+ const t = JSON.parse(root.dataset.i18n ?? '{}') as CronGeneratorUI;
650
+ let parts = ['*', '*', '*', '*', '*'];
651
+ const desc = root.querySelector<HTMLElement>('#cron-human-readable')!;
652
+ const list = root.querySelector<HTMLElement>('#cron-next-runs')!;
653
+ const disps = getDisps(root);
654
+ const inps = getInps(root);
655
+ const btn = root.querySelector<HTMLElement>('#cron-btn-copy')!;
656
+ const refs: DOMRefs = { desc, list, disps };
657
+ const upd = (p: string[]) => { parts = p; syncInputs(p, inps); updateAll(p, refs, t); };
658
+ setupTabs(root);
659
+ setupPresets(root, upd);
660
+ setupInputs(root, upd, () => parts);
661
+ setupCopyBtn(btn, () => parts.join(' '), t);
662
+ upd(parts);
663
+ }
664
+
665
+ const root = document.getElementById('cron-root');
666
+ if (root) setup(root);
667
+ </script>