@jjlmoya/utils-babies 1.1.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 (82) hide show
  1. package/package.json +69 -0
  2. package/src/category/i18n/en.ts +48 -0
  3. package/src/category/i18n/es.ts +48 -0
  4. package/src/category/i18n/fr.ts +48 -0
  5. package/src/category/index.ts +24 -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 +30 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +19 -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/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/seo_length.test.ts +23 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/baby-feeding-calculator/bibliography.astro +7 -0
  23. package/src/tool/baby-feeding-calculator/component.astro +210 -0
  24. package/src/tool/baby-feeding-calculator/i18n/en.ts +162 -0
  25. package/src/tool/baby-feeding-calculator/i18n/es.ts +162 -0
  26. package/src/tool/baby-feeding-calculator/i18n/fr.ts +162 -0
  27. package/src/tool/baby-feeding-calculator/index.ts +47 -0
  28. package/src/tool/baby-feeding-calculator/logic.ts +85 -0
  29. package/src/tool/baby-feeding-calculator/seo.astro +58 -0
  30. package/src/tool/baby-feeding-calculator/style.css +329 -0
  31. package/src/tool/baby-percentile-calculator/bibliography.astro +7 -0
  32. package/src/tool/baby-percentile-calculator/component.astro +388 -0
  33. package/src/tool/baby-percentile-calculator/i18n/en.ts +244 -0
  34. package/src/tool/baby-percentile-calculator/i18n/es.ts +244 -0
  35. package/src/tool/baby-percentile-calculator/i18n/fr.ts +244 -0
  36. package/src/tool/baby-percentile-calculator/index.ts +54 -0
  37. package/src/tool/baby-percentile-calculator/lmsData.ts +80 -0
  38. package/src/tool/baby-percentile-calculator/logic.ts +85 -0
  39. package/src/tool/baby-percentile-calculator/seo.astro +36 -0
  40. package/src/tool/baby-percentile-calculator/style.css +393 -0
  41. package/src/tool/baby-size-converter/bibliography.astro +7 -0
  42. package/src/tool/baby-size-converter/component.astro +289 -0
  43. package/src/tool/baby-size-converter/data.json +11 -0
  44. package/src/tool/baby-size-converter/i18n/en.ts +203 -0
  45. package/src/tool/baby-size-converter/i18n/es.ts +203 -0
  46. package/src/tool/baby-size-converter/i18n/fr.ts +203 -0
  47. package/src/tool/baby-size-converter/index.ts +53 -0
  48. package/src/tool/baby-size-converter/logic.ts +44 -0
  49. package/src/tool/baby-size-converter/seo.astro +36 -0
  50. package/src/tool/baby-size-converter/style.css +394 -0
  51. package/src/tool/fertile-days-estimator/bibliography.astro +7 -0
  52. package/src/tool/fertile-days-estimator/component.astro +265 -0
  53. package/src/tool/fertile-days-estimator/i18n/en.ts +258 -0
  54. package/src/tool/fertile-days-estimator/i18n/es.ts +262 -0
  55. package/src/tool/fertile-days-estimator/i18n/fr.ts +258 -0
  56. package/src/tool/fertile-days-estimator/index.ts +47 -0
  57. package/src/tool/fertile-days-estimator/logic.ts +58 -0
  58. package/src/tool/fertile-days-estimator/seo.astro +36 -0
  59. package/src/tool/fertile-days-estimator/style.css +419 -0
  60. package/src/tool/pregnancy-calculator/bibliography.astro +7 -0
  61. package/src/tool/pregnancy-calculator/calculator.ts +41 -0
  62. package/src/tool/pregnancy-calculator/component.astro +432 -0
  63. package/src/tool/pregnancy-calculator/i18n/en.ts +315 -0
  64. package/src/tool/pregnancy-calculator/i18n/es.ts +319 -0
  65. package/src/tool/pregnancy-calculator/i18n/fr.ts +315 -0
  66. package/src/tool/pregnancy-calculator/index.ts +55 -0
  67. package/src/tool/pregnancy-calculator/milestones.ts +153 -0
  68. package/src/tool/pregnancy-calculator/seo.astro +36 -0
  69. package/src/tool/pregnancy-calculator/store.ts +60 -0
  70. package/src/tool/pregnancy-calculator/style.css +807 -0
  71. package/src/tool/vaccination-calendar/bibliography.astro +7 -0
  72. package/src/tool/vaccination-calendar/component.astro +286 -0
  73. package/src/tool/vaccination-calendar/i18n/en.ts +170 -0
  74. package/src/tool/vaccination-calendar/i18n/es.ts +174 -0
  75. package/src/tool/vaccination-calendar/i18n/fr.ts +170 -0
  76. package/src/tool/vaccination-calendar/index.ts +47 -0
  77. package/src/tool/vaccination-calendar/logic.ts +59 -0
  78. package/src/tool/vaccination-calendar/seo.astro +36 -0
  79. package/src/tool/vaccination-calendar/style.css +316 -0
  80. package/src/tool/vaccination-calendar/vaccinationData.ts +21 -0
  81. package/src/tools.ts +17 -0
  82. package/src/types.ts +72 -0
@@ -0,0 +1,394 @@
1
+ .baby-size-converter {
2
+ --bsc-bg: #fff;
3
+ --bsc-bg-muted: #fbfbfc;
4
+ --bsc-bg-alt: #f1f5f9;
5
+ --bsc-bg-dark: #0f172a;
6
+ --bsc-text: #0f172a;
7
+ --bsc-text-muted: #1e293b;
8
+ --bsc-text-dim: #64748b;
9
+ --bsc-text-faint: #94a3b8;
10
+ --bsc-text-lighter: #cbd5e1;
11
+ --bsc-border: #e2e8f0;
12
+ --bsc-border-inner: #f1f5f9;
13
+ --bsc-primary: #6366f1;
14
+ --bsc-primary-on: #fff;
15
+ --bsc-primary-soft: #eef2ff;
16
+ --bsc-primary-dark: #4f46e5;
17
+ --bsc-success: #10b981;
18
+ --bsc-error: #f43f5e;
19
+ --bsc-fit-large: #10b981;
20
+ --bsc-fit-small: #f43f5e;
21
+
22
+ width: 100%;
23
+ }
24
+
25
+ .theme-dark .baby-size-converter {
26
+ --bsc-bg: #0f172a;
27
+ --bsc-bg-muted: rgba(30, 41, 59, 0.15);
28
+ --bsc-bg-alt: #0f172a;
29
+ --bsc-bg-dark: #0f172a;
30
+ --bsc-text: #f1f5f9;
31
+ --bsc-text-muted: #f1f5f9;
32
+ --bsc-text-dim: #94a3b8;
33
+ --bsc-text-faint: #94a3b8;
34
+ --bsc-text-lighter: #cbd5e1;
35
+ --bsc-border: #1e293b;
36
+ --bsc-border-inner: #1e293b;
37
+ --bsc-primary: #818cf8;
38
+ --bsc-primary-on: #fff;
39
+ --bsc-primary-soft: rgba(99, 102, 241, 0.1);
40
+ --bsc-primary-dark: #818cf8;
41
+ --bsc-success: #10b981;
42
+ --bsc-error: #f43f5e;
43
+ --bsc-fit-large: #10b981;
44
+ --bsc-fit-small: #f43f5e;
45
+ }
46
+
47
+ .baby-size-converter-main {
48
+ display: grid;
49
+ grid-template-columns: 1fr 1fr;
50
+ background: var(--bsc-bg);
51
+ border: 1px solid var(--bsc-border);
52
+ border-radius: 1rem;
53
+ overflow: hidden;
54
+ }
55
+
56
+ @media (max-width: 720px) {
57
+ .baby-size-converter-main {
58
+ grid-template-columns: 1fr;
59
+ }
60
+ }
61
+
62
+ .baby-size-converter-left {
63
+ padding: 1.5rem;
64
+ border-right: 1px solid var(--bsc-border);
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: 1.25rem;
68
+ }
69
+
70
+ @media (max-width: 720px) {
71
+ .baby-size-converter-left {
72
+ border-right: none;
73
+ border-bottom: 1px solid var(--bsc-border);
74
+ }
75
+ }
76
+
77
+ .baby-size-converter-right {
78
+ padding: 1.5rem;
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: 1.25rem;
82
+ background: var(--bsc-bg-muted);
83
+ }
84
+
85
+ .baby-size-converter-section-marker {
86
+ font-size: 0.7rem;
87
+ font-weight: 700;
88
+ letter-spacing: 0.1em;
89
+ text-transform: uppercase;
90
+ color: var(--bsc-text-dim);
91
+ padding-bottom: 0.5rem;
92
+ border-bottom: 1px solid var(--bsc-border);
93
+ }
94
+
95
+ .baby-size-converter-unit-nav {
96
+ display: flex;
97
+ gap: 0.25rem;
98
+ background: var(--bsc-bg-alt);
99
+ border-radius: 0.5rem;
100
+ padding: 0.2rem;
101
+ }
102
+
103
+ .baby-size-converter-unit-tab {
104
+ flex: 1;
105
+ padding: 0.35rem 0.5rem;
106
+ border: none;
107
+ border-radius: 0.35rem;
108
+ background: transparent;
109
+ color: var(--bsc-text-dim);
110
+ font-size: 0.75rem;
111
+ font-weight: 600;
112
+ cursor: pointer;
113
+ transition: background 0.15s, color 0.15s;
114
+ }
115
+
116
+ .baby-size-converter-unit-tab.active {
117
+ background: var(--bsc-primary);
118
+ color: var(--bsc-primary-on);
119
+ }
120
+
121
+ .baby-size-converter-num-ctrl {
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 0.4rem;
125
+ }
126
+
127
+ .baby-size-converter-num-label {
128
+ font-size: 0.8rem;
129
+ font-weight: 600;
130
+ color: var(--bsc-text-muted);
131
+ }
132
+
133
+ .baby-size-converter-stepper-box {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 0.5rem;
137
+ }
138
+
139
+ .baby-size-converter-btn-step {
140
+ width: 2rem;
141
+ height: 2rem;
142
+ border: 1px solid var(--bsc-border);
143
+ border-radius: 0.4rem;
144
+ background: var(--bsc-bg);
145
+ color: var(--bsc-text);
146
+ font-size: 1.1rem;
147
+ cursor: pointer;
148
+ display: flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ transition: background 0.15s, border-color 0.15s;
152
+ }
153
+
154
+ .baby-size-converter-btn-step:hover {
155
+ background: var(--bsc-primary-soft);
156
+ border-color: var(--bsc-primary);
157
+ color: var(--bsc-primary);
158
+ }
159
+
160
+ .baby-size-converter-val-view {
161
+ flex: 1;
162
+ display: flex;
163
+ align-items: baseline;
164
+ justify-content: center;
165
+ gap: 0.3rem;
166
+ }
167
+
168
+ .baby-size-converter-val-big {
169
+ font-size: 1.5rem;
170
+ font-weight: 700;
171
+ color: var(--bsc-text);
172
+ }
173
+
174
+ .baby-size-converter-val-sub {
175
+ font-size: 0.75rem;
176
+ color: var(--bsc-text-dim);
177
+ }
178
+
179
+ .baby-size-converter-slider-line {
180
+ width: 100%;
181
+ accent-color: var(--bsc-primary);
182
+ cursor: pointer;
183
+ }
184
+
185
+ .baby-size-converter-select-box {
186
+ width: 100%;
187
+ padding: 0.5rem 0.75rem;
188
+ border: 1px solid var(--bsc-border);
189
+ border-radius: 0.5rem;
190
+ background: var(--bsc-bg);
191
+ color: var(--bsc-text);
192
+ font-size: 0.85rem;
193
+ cursor: pointer;
194
+ }
195
+
196
+ .baby-size-converter-age-rack {
197
+ display: flex;
198
+ flex-wrap: wrap;
199
+ gap: 0.35rem;
200
+ }
201
+
202
+ .baby-size-converter-age-tile {
203
+ padding: 0.3rem 0.6rem;
204
+ border: 1px solid var(--bsc-border);
205
+ border-radius: 999px;
206
+ background: var(--bsc-bg);
207
+ color: var(--bsc-text-dim);
208
+ font-size: 0.72rem;
209
+ font-weight: 600;
210
+ cursor: pointer;
211
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
212
+ }
213
+
214
+ .baby-size-converter-age-tile:hover {
215
+ background: var(--bsc-primary-soft);
216
+ border-color: var(--bsc-primary);
217
+ color: var(--bsc-primary);
218
+ }
219
+
220
+ .baby-size-converter-res-header {
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 0.75rem;
224
+ flex-wrap: wrap;
225
+ }
226
+
227
+ .baby-size-converter-res-size-main {
228
+ font-size: 2rem;
229
+ font-weight: 800;
230
+ color: var(--bsc-primary);
231
+ line-height: 1;
232
+ }
233
+
234
+ .baby-size-converter-res-brand-hint {
235
+ font-size: 0.9rem;
236
+ color: var(--bsc-text-muted);
237
+ font-weight: 600;
238
+ }
239
+
240
+ .baby-size-converter-fit-label {
241
+ font-size: 0.7rem;
242
+ font-weight: 700;
243
+ padding: 0.2rem 0.5rem;
244
+ border-radius: 999px;
245
+ background: var(--bsc-bg-alt);
246
+ color: var(--bsc-text-dim);
247
+ }
248
+
249
+ .baby-size-converter-fit-large {
250
+ background: rgba(16, 185, 129, 0.1);
251
+ color: var(--bsc-fit-large);
252
+ }
253
+
254
+ .baby-size-converter-fit-small {
255
+ background: rgba(244, 63, 94, 0.1);
256
+ color: var(--bsc-fit-small);
257
+ }
258
+
259
+ .baby-size-converter-share-btn {
260
+ margin-left: auto;
261
+ width: 2rem;
262
+ height: 2rem;
263
+ border: 1px solid var(--bsc-border);
264
+ border-radius: 0.4rem;
265
+ background: var(--bsc-bg);
266
+ color: var(--bsc-text-dim);
267
+ font-size: 1rem;
268
+ cursor: pointer;
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ transition: background 0.15s, border-color 0.15s;
273
+ }
274
+
275
+ .baby-size-converter-share-btn:hover {
276
+ background: var(--bsc-primary-soft);
277
+ border-color: var(--bsc-primary);
278
+ color: var(--bsc-primary);
279
+ }
280
+
281
+ .baby-size-converter-clothes-meta {
282
+ background: var(--bsc-bg);
283
+ border: 1px solid var(--bsc-border);
284
+ border-radius: 0.75rem;
285
+ padding: 1rem;
286
+ }
287
+
288
+ .baby-size-converter-meta-title {
289
+ font-size: 0.72rem;
290
+ font-weight: 700;
291
+ text-transform: uppercase;
292
+ letter-spacing: 0.05em;
293
+ color: var(--bsc-text-dim);
294
+ margin-bottom: 0.5rem;
295
+ }
296
+
297
+ .baby-size-converter-meta-vals {
298
+ display: flex;
299
+ gap: 1.5rem;
300
+ }
301
+
302
+ .baby-size-converter-meta-cell {
303
+ display: flex;
304
+ flex-direction: column;
305
+ gap: 0.15rem;
306
+ }
307
+
308
+ .baby-size-converter-equivalents-grid-box {
309
+ background: var(--bsc-bg);
310
+ border: 1px solid var(--bsc-border);
311
+ border-radius: 0.75rem;
312
+ overflow: hidden;
313
+ }
314
+
315
+ .baby-size-converter-eq-head-row {
316
+ display: flex;
317
+ justify-content: space-between;
318
+ padding: 0.6rem 1rem;
319
+ background: var(--bsc-bg-alt);
320
+ font-size: 0.7rem;
321
+ font-weight: 700;
322
+ text-transform: uppercase;
323
+ letter-spacing: 0.05em;
324
+ color: var(--bsc-text-dim);
325
+ }
326
+
327
+ .baby-size-converter-eq-col-name {
328
+ color: var(--bsc-text-dim);
329
+ }
330
+
331
+ .baby-size-converter-eq-data-row {
332
+ display: flex;
333
+ justify-content: space-between;
334
+ align-items: center;
335
+ padding: 0.5rem 1rem;
336
+ border-top: 1px solid var(--bsc-border-inner);
337
+ font-size: 0.82rem;
338
+ color: var(--bsc-text);
339
+ transition: background 0.1s;
340
+ }
341
+
342
+ .baby-size-converter-eq-data-row:hover {
343
+ background: var(--bsc-bg-muted);
344
+ }
345
+
346
+ .baby-size-converter-eq-active {
347
+ background: var(--bsc-primary-soft);
348
+ }
349
+
350
+ .baby-size-converter-eq-active .baby-size-converter-eq-size-v {
351
+ color: var(--bsc-primary);
352
+ font-weight: 700;
353
+ }
354
+
355
+ .baby-size-converter-eq-brand-v {
356
+ color: var(--bsc-text-muted);
357
+ }
358
+
359
+ .baby-size-converter-eq-size-v {
360
+ font-weight: 600;
361
+ }
362
+
363
+ .baby-size-converter-brand-picker {
364
+ display: flex;
365
+ flex-direction: column;
366
+ gap: 0.4rem;
367
+ }
368
+
369
+ .baby-size-converter-pro-tip-footer {
370
+ display: flex;
371
+ gap: 0.75rem;
372
+ align-items: flex-start;
373
+ background: var(--bsc-primary-soft);
374
+ border: 1px solid var(--bsc-border);
375
+ border-radius: 0.75rem;
376
+ padding: 0.75rem 1rem;
377
+ }
378
+
379
+ .baby-size-converter-footer-icon {
380
+ font-size: 1rem;
381
+ color: var(--bsc-primary);
382
+ flex-shrink: 0;
383
+ margin-top: 0.1rem;
384
+ }
385
+
386
+ .baby-size-converter-footer-text {
387
+ font-size: 0.78rem;
388
+ color: var(--bsc-text-muted);
389
+ line-height: 1.5;
390
+ }
391
+
392
+ .baby-size-converter-hidden {
393
+ display: none;
394
+ }
@@ -0,0 +1,7 @@
1
+ ---
2
+ import { Bibliography as BibliographyUI } from '@jjlmoya/utils-shared';
3
+ import type { BibliographyEntry } from '../../types';
4
+ interface Props { links?: BibliographyEntry[]; title: string; }
5
+ const { links = [], title } = Astro.props;
6
+ ---
7
+ <BibliographyUI links={links} title={title} />
@@ -0,0 +1,265 @@
1
+ ---
2
+ import './style.css';
3
+ import type { FertileDaysEstimatorUI } from './index';
4
+ interface Props { ui: FertileDaysEstimatorUI; }
5
+ const { ui } = Astro.props;
6
+ ---
7
+ <div id="fertile-days-estimator-root" class="fertile-days-estimator" data-ui={JSON.stringify(ui)}>
8
+ <div class="fertile-days-estimator-step-indicator">
9
+ <div class="fertile-days-estimator-step-item active" id="fde-step-1-indicator">
10
+ <span class="fertile-days-estimator-step-number">1</span>
11
+ <span>{ui.step1Indicator}</span>
12
+ </div>
13
+ <div class="fertile-days-estimator-step-item" id="fde-step-2-indicator">
14
+ <span class="fertile-days-estimator-step-number">2</span>
15
+ <span>{ui.step2Indicator}</span>
16
+ </div>
17
+ </div>
18
+
19
+ <div class="fertile-days-estimator-step-first" id="fde-step-first">
20
+ <h2 class="fertile-days-estimator-step-title">{ui.step1Title}</h2>
21
+ <p class="fertile-days-estimator-step-subtitle">{ui.step1Desc}</p>
22
+ <p class="fertile-days-estimator-pulse-hint">{ui.pulseHint}</p>
23
+ <div class="fertile-days-estimator-calendar" id="fde-calendar-initial">
24
+ <div class="fertile-days-estimator-calendar-nav">
25
+ <span id="fde-month-label-initial" class="fertile-days-estimator-month-label"></span>
26
+ <div class="fertile-days-estimator-nav-buttons">
27
+ <button class="fertile-days-estimator-nav-btn" id="fde-prev-initial" aria-label="Mes anterior">
28
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
29
+ <path d="M15 18l-6-6 6-6" />
30
+ </svg>
31
+ </button>
32
+ <button class="fertile-days-estimator-nav-btn" id="fde-next-initial" aria-label="Mes siguiente">
33
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
34
+ <path d="M9 18l6-6-6-6" />
35
+ </svg>
36
+ </button>
37
+ </div>
38
+ </div>
39
+ <div class="fertile-days-estimator-calendar-grid" id="fde-grid-initial"></div>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="fertile-days-estimator-layout" id="fde-layout">
44
+ <aside class="fertile-days-estimator-sidebar">
45
+ <div class="fertile-days-estimator-sidebar-header">
46
+ <strong>{ui.sidebarTitle}</strong>
47
+ <p>{ui.sidebarDesc}</p>
48
+ </div>
49
+ <div class="fertile-days-estimator-input-card">
50
+ <label class="fertile-days-estimator-range-label" for="fde-cycle-length">{ui.labelCycleLength}</label>
51
+ <div class="fertile-days-estimator-range-control">
52
+ <input
53
+ type="range"
54
+ id="fde-cycle-length"
55
+ min="21"
56
+ max="35"
57
+ value="28"
58
+ class="fertile-days-estimator-range-input"
59
+ />
60
+ <span class="fertile-days-estimator-range-value" id="fde-cycle-value">28 <span>{ui.unitDays}</span></span>
61
+ </div>
62
+ </div>
63
+ <div class="fertile-days-estimator-results" id="fde-results">
64
+ <div class="fertile-days-estimator-stat" id="fde-stat-ovulation">
65
+ <div class="fertile-days-estimator-stat-dot fertile-days-estimator-stat-dot-ovulation"></div>
66
+ <div class="fertile-days-estimator-stat-info">
67
+ <span class="fertile-days-estimator-stat-label">{ui.labelOvulation}</span>
68
+ <span class="fertile-days-estimator-stat-value" id="fde-val-ovulation">—</span>
69
+ </div>
70
+ </div>
71
+ <div class="fertile-days-estimator-stat" id="fde-stat-fertile">
72
+ <div class="fertile-days-estimator-stat-dot fertile-days-estimator-stat-dot-fertile"></div>
73
+ <div class="fertile-days-estimator-stat-info">
74
+ <span class="fertile-days-estimator-stat-label">{ui.labelFertileWindow}</span>
75
+ <span class="fertile-days-estimator-stat-value" id="fde-val-fertile">—</span>
76
+ </div>
77
+ </div>
78
+ <div class="fertile-days-estimator-stat" id="fde-stat-period">
79
+ <div class="fertile-days-estimator-stat-dot fertile-days-estimator-stat-dot-period"></div>
80
+ <div class="fertile-days-estimator-stat-info">
81
+ <span class="fertile-days-estimator-stat-label">{ui.labelNextPeriod}</span>
82
+ <span class="fertile-days-estimator-stat-value" id="fde-val-period">—</span>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </aside>
87
+
88
+ <div class="fertile-days-estimator-content">
89
+ <div class="fertile-days-estimator-calendar" id="fde-calendar-main">
90
+ <div class="fertile-days-estimator-calendar-nav">
91
+ <span id="fde-month-label-main" class="fertile-days-estimator-month-label"></span>
92
+ <div class="fertile-days-estimator-nav-buttons">
93
+ <button class="fertile-days-estimator-nav-btn" id="fde-prev-main" aria-label="Mes anterior">
94
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
95
+ <path d="M15 18l-6-6 6-6" />
96
+ </svg>
97
+ </button>
98
+ <button class="fertile-days-estimator-nav-btn" id="fde-next-main" aria-label="Mes siguiente">
99
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
100
+ <path d="M9 18l6-6-6-6" />
101
+ </svg>
102
+ </button>
103
+ </div>
104
+ </div>
105
+ <div class="fertile-days-estimator-calendar-grid" id="fde-grid-main"></div>
106
+ </div>
107
+ <div class="fertile-days-estimator-legend">
108
+ <div class="fertile-days-estimator-legend-item">
109
+ <span class="fertile-days-estimator-legend-dot fertile-days-estimator-legend-dot-selected"></span>
110
+ <span>{ui.legendSelection}</span>
111
+ </div>
112
+ <div class="fertile-days-estimator-legend-item">
113
+ <span class="fertile-days-estimator-legend-dot fertile-days-estimator-legend-dot-period"></span>
114
+ <span>{ui.legendPeriod}</span>
115
+ </div>
116
+ <div class="fertile-days-estimator-legend-item">
117
+ <span class="fertile-days-estimator-legend-dot fertile-days-estimator-legend-dot-fertile"></span>
118
+ <span>{ui.legendFertile}</span>
119
+ </div>
120
+ <div class="fertile-days-estimator-legend-item">
121
+ <span class="fertile-days-estimator-legend-dot fertile-days-estimator-legend-dot-ovulation"></span>
122
+ <span>{ui.legendOvulation}</span>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <script>
130
+ import { calculateCycle, getDaysInMonth, getMonthStartOffset, getDayType } from './logic';
131
+ import type { CycleCalcResult } from './logic';
132
+
133
+ const root = document.getElementById('fertile-days-estimator-root') as HTMLElement;
134
+ const ui = JSON.parse(root.dataset.ui as string) as Record<string, string>;
135
+
136
+ let selectedDate: Date | null = null;
137
+ let cycleLength = 28;
138
+ let result: CycleCalcResult | null = null;
139
+
140
+ const now = new Date();
141
+ let viewDate = new Date(now.getFullYear(), now.getMonth(), 1);
142
+
143
+ const stepFirstEl = root.querySelector('#fde-step-first') as HTMLElement;
144
+ const layoutEl = root.querySelector('#fde-layout') as HTMLElement;
145
+ const step1Indicator = root.querySelector('#fde-step-1-indicator') as HTMLElement;
146
+ const step2Indicator = root.querySelector('#fde-step-2-indicator') as HTMLElement;
147
+ const cycleLengthInput = root.querySelector('#fde-cycle-length') as HTMLInputElement;
148
+ const cycleValueEl = root.querySelector('#fde-cycle-value') as HTMLElement;
149
+ const gridInitial = root.querySelector('#fde-grid-initial') as HTMLElement;
150
+ const gridMain = root.querySelector('#fde-grid-main') as HTMLElement;
151
+ const monthLabelInitial = root.querySelector('#fde-month-label-initial') as HTMLElement;
152
+ const monthLabelMain = root.querySelector('#fde-month-label-main') as HTMLElement;
153
+ const valOvulation = root.querySelector('#fde-val-ovulation') as HTMLElement;
154
+ const valFertile = root.querySelector('#fde-val-fertile') as HTMLElement;
155
+ const valPeriod = root.querySelector('#fde-val-period') as HTMLElement;
156
+
157
+ const DAY_HEADERS = ['L', 'M', 'X', 'J', 'V', 'S', 'D'];
158
+
159
+ const fmt = new Intl.DateTimeFormat('es-ES', { day: 'numeric', month: 'short' });
160
+ const fmtMonth = new Intl.DateTimeFormat('es-ES', { month: 'long', year: 'numeric' });
161
+
162
+ function getToday(): Date {
163
+ const t = new Date();
164
+ t.setHours(0, 0, 0, 0);
165
+ return t;
166
+ }
167
+
168
+ function buildGrid(container: HTMLElement, isInitial: boolean): void {
169
+ container.innerHTML = '';
170
+ const year = viewDate.getFullYear();
171
+ const month = viewDate.getMonth();
172
+ const daysInMonth = getDaysInMonth(year, month);
173
+ const offset = getMonthStartOffset(year, month);
174
+ const today = getToday();
175
+ const frag = document.createDocumentFragment();
176
+ DAY_HEADERS.forEach((h) => {
177
+ const el = document.createElement('div');
178
+ el.className = 'fertile-days-estimator-day-header';
179
+ el.textContent = h;
180
+ frag.appendChild(el);
181
+ });
182
+ for (let i = 0; i < offset; i++) {
183
+ const el = document.createElement('div');
184
+ el.className = 'fertile-days-estimator-day fertile-days-estimator-day-empty';
185
+ frag.appendChild(el);
186
+ }
187
+ for (let d = 1; d <= daysInMonth; d++) {
188
+ const dayDate = new Date(year, month, d);
189
+ dayDate.setHours(0, 0, 0, 0);
190
+ const dayType = getDayType(dayDate, selectedDate, result, today);
191
+ const el = document.createElement('button');
192
+ el.className = 'fertile-days-estimator-day';
193
+ if (dayType) el.classList.add(`fertile-days-estimator-day--${dayType}`);
194
+ el.textContent = String(d);
195
+ el.addEventListener('click', () => onDayClick(dayDate, isInitial));
196
+ frag.appendChild(el);
197
+ }
198
+ container.appendChild(frag);
199
+ }
200
+
201
+ function updateMonthLabel(): void {
202
+ const label = fmtMonth.format(viewDate);
203
+ monthLabelInitial.textContent = label;
204
+ monthLabelMain.textContent = label;
205
+ }
206
+
207
+ function updateResults(): void {
208
+ if (!selectedDate || !result) return;
209
+ valOvulation.textContent = fmt.format(result.ovulationDate);
210
+ valFertile.textContent = `${fmt.format(result.fertileStart)} – ${fmt.format(result.ovulationDate)}`;
211
+ valPeriod.textContent = fmt.format(result.nextPeriod);
212
+ }
213
+
214
+ function renderAll(): void {
215
+ updateMonthLabel();
216
+ buildGrid(gridInitial, true);
217
+ buildGrid(gridMain, false);
218
+ updateResults();
219
+ }
220
+
221
+ function goToStep2(): void {
222
+ stepFirstEl.classList.add('hidden');
223
+ layoutEl.classList.add('active');
224
+ step1Indicator.classList.remove('active');
225
+ step2Indicator.classList.add('active');
226
+ }
227
+
228
+ function onDayClick(day: Date, isInitial: boolean): void {
229
+ selectedDate = day;
230
+ result = calculateCycle(day, cycleLength);
231
+ if (isInitial) goToStep2();
232
+ renderAll();
233
+ }
234
+
235
+ cycleLengthInput.addEventListener('input', () => {
236
+ cycleLength = Number(cycleLengthInput.value);
237
+ cycleValueEl.textContent = `${cycleLength} `;
238
+ const span = document.createElement('span');
239
+ span.textContent = ui['unitDays'] ?? '';
240
+ cycleValueEl.appendChild(span);
241
+ if (selectedDate) {
242
+ result = calculateCycle(selectedDate, cycleLength);
243
+ renderAll();
244
+ }
245
+ });
246
+
247
+ root.querySelector('#fde-prev-initial')?.addEventListener('click', () => {
248
+ viewDate = new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1);
249
+ renderAll();
250
+ });
251
+ root.querySelector('#fde-next-initial')?.addEventListener('click', () => {
252
+ viewDate = new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1);
253
+ renderAll();
254
+ });
255
+ root.querySelector('#fde-prev-main')?.addEventListener('click', () => {
256
+ viewDate = new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1);
257
+ renderAll();
258
+ });
259
+ root.querySelector('#fde-next-main')?.addEventListener('click', () => {
260
+ viewDate = new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1);
261
+ renderAll();
262
+ });
263
+
264
+ renderAll();
265
+ </script>