@jjlmoya/utils-babies 1.2.0 → 1.4.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 (27) hide show
  1. package/package.json +2 -2
  2. package/src/tool/baby-feeding-calculator/bibliography.astro +8 -4
  3. package/src/tool/baby-feeding-calculator/component.astro +208 -177
  4. package/src/tool/baby-feeding-calculator/style.css +317 -229
  5. package/src/tool/baby-percentile-calculator/bibliography.astro +8 -4
  6. package/src/tool/baby-percentile-calculator/component.astro +98 -240
  7. package/src/tool/baby-percentile-calculator/i18n/en.ts +1 -0
  8. package/src/tool/baby-percentile-calculator/i18n/es.ts +1 -0
  9. package/src/tool/baby-percentile-calculator/i18n/fr.ts +1 -0
  10. package/src/tool/baby-percentile-calculator/index.ts +1 -0
  11. package/src/tool/baby-percentile-calculator/style.css +342 -268
  12. package/src/tool/baby-size-converter/bibliography.astro +8 -4
  13. package/src/tool/baby-size-converter/component.astro +221 -212
  14. package/src/tool/baby-size-converter/style.css +433 -263
  15. package/src/tool/fertile-days-estimator/bibliography.astro +8 -4
  16. package/src/tool/fertile-days-estimator/component.astro +202 -200
  17. package/src/tool/fertile-days-estimator/style.css +408 -270
  18. package/src/tool/pregnancy-calculator/bibliography.astro +8 -4
  19. package/src/tool/pregnancy-calculator/component.astro +50 -8
  20. package/src/tool/pregnancy-calculator/i18n/en.ts +8 -0
  21. package/src/tool/pregnancy-calculator/i18n/es.ts +8 -0
  22. package/src/tool/pregnancy-calculator/i18n/fr.ts +8 -0
  23. package/src/tool/pregnancy-calculator/index.ts +8 -0
  24. package/src/tool/pregnancy-calculator/style.css +351 -134
  25. package/src/tool/vaccination-calendar/bibliography.astro +8 -4
  26. package/src/tool/vaccination-calendar/component.astro +120 -124
  27. package/src/tool/vaccination-calendar/style.css +296 -209
@@ -1,7 +1,11 @@
1
1
  ---
2
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;
3
+ import { vaccinationCalendar } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props { locale?: KnownLocale; }
7
+ const { locale = 'es' } = Astro.props;
8
+ const content = await vaccinationCalendar.i18n[locale]?.();
9
+ if (!content) return null;
6
10
  ---
7
- <BibliographyUI links={links} title={title} />
11
+ <BibliographyUI links={content.bibliography} title={content.bibliographyTitle ?? ''} />
@@ -4,100 +4,82 @@ import type { VaccinationCalendarUI } from './index';
4
4
  interface Props { ui: VaccinationCalendarUI; }
5
5
  const { ui } = Astro.props;
6
6
  ---
7
- <div id="vaccination-calendar-root" class="vaccination-calendar" data-ui={JSON.stringify(ui)}>
8
- <div class="vaccination-calendar-header">
9
- <div class="vaccination-calendar-field">
10
- <label class="vaccination-calendar-field-label" for="vc-dd" id="vc-birth-label">{ui.labelBirthDate}</label>
11
- <div class="vaccination-calendar-triple-input">
12
- <input
13
- class="vaccination-calendar-segment"
14
- id="vc-dd"
15
- type="text"
16
- inputmode="numeric"
17
- maxlength="2"
18
- placeholder={ui.placeholderDD}
19
- aria-label="Día"
20
- />
21
- <span class="vaccination-calendar-sep">/</span>
22
- <input
23
- class="vaccination-calendar-segment"
24
- id="vc-mm"
25
- type="text"
26
- inputmode="numeric"
27
- maxlength="2"
28
- placeholder={ui.placeholderMM}
29
- aria-label="Mes"
30
- />
31
- <span class="vaccination-calendar-sep">/</span>
32
- <input
33
- class="vaccination-calendar-segment vaccination-calendar-segment-year"
34
- id="vc-yyyy"
35
- type="text"
36
- inputmode="numeric"
37
- maxlength="4"
38
- placeholder={ui.placeholderAAAA}
39
- aria-label="Año"
40
- />
7
+ <div class="vc-card" id="vc-root" data-ui={JSON.stringify(ui)}>
8
+ <header class="vc-header">
9
+ <div class="vc-inputs">
10
+ <div class="vc-field">
11
+ <label>{ui.labelBirthDate}</label>
12
+ <div class="vc-triple-input" id="vc-triple">
13
+ <input type="text" id="vc-dd" class="vc-segment" placeholder={ui.placeholderDD} inputmode="numeric" maxlength="2" aria-label="Día" />
14
+ <span class="vc-sep">/</span>
15
+ <input type="text" id="vc-mm" class="vc-segment" placeholder={ui.placeholderMM} inputmode="numeric" maxlength="2" aria-label="Mes" />
16
+ <span class="vc-sep">/</span>
17
+ <input type="text" id="vc-yyyy" class="vc-segment vc-segment-year" placeholder={ui.placeholderAAAA} inputmode="numeric" maxlength="4" aria-label="Año" />
18
+ </div>
19
+ <div class="vc-age-badge" id="vc-age-badge"></div>
41
20
  </div>
42
- <div class="vaccination-calendar-error" id="vc-error" role="alert" aria-live="polite"></div>
43
21
  </div>
44
- <div class="vaccination-calendar-age-badge" id="vc-age-badge" aria-live="polite"></div>
45
- </div>
22
+ </header>
46
23
 
47
- <div class="vaccination-calendar-empty" id="vc-empty">
48
- <p>{ui.emptyMsg}</p>
24
+ <div id="vc-empty" class="vc-empty-state">
25
+ {ui.emptyMsg}
49
26
  </div>
50
27
 
51
- <div class="vaccination-calendar-context" id="vc-context">
52
- <div class="vaccination-calendar-next-title" id="vc-next-label">{ui.labelNextAppointment}</div>
53
- <div class="vaccination-calendar-next-date" id="vc-next-date"></div>
54
- <ul class="vaccination-calendar-vac-list" id="vc-next-vaccines"></ul>
55
- <div class="vaccination-calendar-actions">
56
- <button class="vaccination-calendar-btn-primary" id="vc-btn-reminder">{ui.btnAddReminder}</button>
57
- <button class="vaccination-calendar-btn-primary vaccination-calendar-btn-share" id="vc-btn-share">{ui.labelShare}</button>
58
- </div>
59
- <div class="vaccination-calendar-footer">
60
- <span>{ui.labelSource}</span>
61
- </div>
62
- </div>
28
+ <section class="vc-main-context" id="vc-context">
29
+ <h3 class="vc-next-title">{ui.labelNextAppointment}</h3>
30
+ <div class="vc-next-date" id="vc-next-date"></div>
31
+ <div class="vc-vac-list" id="vc-next-vaccines"></div>
32
+ <button id="vc-btn-reminder" class="vc-btn-primary">{ui.btnAddReminder}</button>
33
+ </section>
63
34
 
64
- <div class="vaccination-calendar-sections" id="vc-sections">
65
- <details class="vaccination-calendar-accordion-item" id="vc-accordion-past">
66
- <summary class="vaccination-calendar-accordion-trigger">{ui.labelPassed}</summary>
67
- <div class="vaccination-calendar-accordion-content">
68
- <div class="vaccination-calendar-timeline" id="vc-timeline-past"></div>
35
+ <div class="vc-sections" id="vc-sections">
36
+ <div class="vc-accordion-item" id="vc-accordion-past">
37
+ <button class="vc-accordion-trigger">
38
+ <span>{ui.labelPassed}</span>
39
+ <span class="vc-timeline-status vc-check">OK</span>
40
+ </button>
41
+ <div class="vc-accordion-content">
42
+ <div class="vc-timeline-compact" id="vc-timeline-past"></div>
69
43
  </div>
70
- </details>
71
- <details class="vaccination-calendar-accordion-item" id="vc-accordion-future" open>
72
- <summary class="vaccination-calendar-accordion-trigger">{ui.labelFuture}</summary>
73
- <div class="vaccination-calendar-accordion-content">
74
- <div class="vaccination-calendar-timeline" id="vc-timeline-future"></div>
44
+ </div>
45
+ <div class="vc-accordion-item" id="vc-accordion-future">
46
+ <button class="vc-accordion-trigger">
47
+ <span>{ui.labelFuture}</span>
48
+ <span class="vc-timeline-status vc-clock">...</span>
49
+ </button>
50
+ <div class="vc-accordion-content">
51
+ <div class="vc-timeline-compact" id="vc-timeline-future"></div>
75
52
  </div>
76
- </details>
53
+ </div>
77
54
  </div>
55
+
56
+ <footer class="vc-footer">
57
+ {ui.labelSource} •
58
+ <a href="javascript:void(0)" id="vc-share-link" class="vc-share-link">{ui.labelShare}</a>
59
+ </footer>
78
60
  </div>
79
61
 
80
62
  <script>
81
63
  import { buildDoseGroups, getAgeLabel, calculateAge, buildIcsContent } from './logic';
82
64
  import type { DoseGroup } from './logic';
83
65
 
84
- const root = document.getElementById('vaccination-calendar-root') as HTMLElement;
66
+ const root = document.getElementById('vc-root') as HTMLElement;
85
67
  const ui = JSON.parse(root.dataset.ui as string) as Record<string, string>;
86
68
 
87
- const ddInput = root.querySelector('#vc-dd') as HTMLInputElement;
88
- const mmInput = root.querySelector('#vc-mm') as HTMLInputElement;
89
- const yyyyInput = root.querySelector('#vc-yyyy') as HTMLInputElement;
90
- const errorEl = root.querySelector('#vc-error') as HTMLElement;
91
- const ageBadge = root.querySelector('#vc-age-badge') as HTMLElement;
92
- const emptyEl = root.querySelector('#vc-empty') as HTMLElement;
93
- const contextEl = root.querySelector('#vc-context') as HTMLElement;
94
- const sectionsEl = root.querySelector('#vc-sections') as HTMLElement;
95
- const nextDateEl = root.querySelector('#vc-next-date') as HTMLElement;
96
- const nextVaccinesEl = root.querySelector('#vc-next-vaccines') as HTMLElement;
97
- const timelinePast = root.querySelector('#vc-timeline-past') as HTMLElement;
98
- const timelineFuture = root.querySelector('#vc-timeline-future') as HTMLElement;
99
- const btnReminder = root.querySelector('#vc-btn-reminder') as HTMLButtonElement;
100
- const btnShare = root.querySelector('#vc-btn-share') as HTMLButtonElement;
69
+ const ddInput = document.getElementById('vc-dd') as HTMLInputElement;
70
+ const mmInput = document.getElementById('vc-mm') as HTMLInputElement;
71
+ const yyyyInput = document.getElementById('vc-yyyy') as HTMLInputElement;
72
+ const tripleEl = document.getElementById('vc-triple') as HTMLElement;
73
+ const ageBadge = document.getElementById('vc-age-badge') as HTMLElement;
74
+ const emptyEl = document.getElementById('vc-empty') as HTMLElement;
75
+ const contextEl = document.getElementById('vc-context') as HTMLElement;
76
+ const sectionsEl = document.getElementById('vc-sections') as HTMLElement;
77
+ const nextDateEl = document.getElementById('vc-next-date') as HTMLElement;
78
+ const nextVaccinesEl = document.getElementById('vc-next-vaccines') as HTMLElement;
79
+ const timelinePast = document.getElementById('vc-timeline-past') as HTMLElement;
80
+ const timelineFuture = document.getElementById('vc-timeline-future') as HTMLElement;
81
+ const btnReminder = document.getElementById('vc-btn-reminder') as HTMLButtonElement;
82
+ const shareLink = document.getElementById('vc-share-link') as HTMLAnchorElement;
101
83
 
102
84
  const doseGroups = buildDoseGroups();
103
85
  let currentBirthDate: Date | null = null;
@@ -121,27 +103,27 @@ const { ui } = Astro.props;
121
103
  return new Intl.DateTimeFormat('es-ES', { day: '2-digit', month: 'long', year: 'numeric' }).format(date);
122
104
  }
123
105
 
124
- function buildTimelineRow(group: DoseGroup, birthDate: Date, _today: Date, isPast: boolean): HTMLElement {
125
- const target = new Date(birthDate);
126
- target.setMonth(target.getMonth() + group.months);
106
+ function buildTimelineRow(group: DoseGroup, _birthDate: Date, isPast: boolean): HTMLElement {
127
107
  const row = document.createElement('div');
128
- row.className = 'vaccination-calendar-timeline-row';
129
- const ageEl = document.createElement('div');
130
- ageEl.className = 'vaccination-calendar-timeline-age';
108
+ row.className = 'vc-timeline-row';
109
+
110
+ const ageEl = document.createElement('span');
111
+ ageEl.className = 'vc-timeline-age';
131
112
  ageEl.textContent = getAgeLabel(group.months);
113
+
132
114
  const vacEl = document.createElement('div');
133
- vacEl.className = 'vaccination-calendar-timeline-vac';
115
+ vacEl.className = 'vc-timeline-vac';
134
116
  group.vaccines.forEach((name) => {
135
117
  const pill = document.createElement('span');
136
- pill.className = 'vaccination-calendar-vac-pill';
118
+ pill.className = 'vc-vac-pill';
137
119
  pill.textContent = name;
138
120
  vacEl.appendChild(pill);
139
121
  });
140
- const statusEl = document.createElement('div');
141
- statusEl.className = isPast
142
- ? 'vaccination-calendar-timeline-status vaccination-calendar-check'
143
- : 'vaccination-calendar-timeline-status vaccination-calendar-clock';
144
- statusEl.textContent = (isPast ? ui['labelStatusOk'] : ui['labelStatusPending']) ?? null;
122
+
123
+ const statusEl = document.createElement('span');
124
+ statusEl.className = isPast ? 'vc-timeline-status vc-check' : 'vc-timeline-status vc-clock';
125
+ statusEl.textContent = isPast ? (ui['labelStatusOk'] ?? 'OK') : (ui['labelStatusPending'] ?? 'PEND.');
126
+
145
127
  row.appendChild(ageEl);
146
128
  row.appendChild(vacEl);
147
129
  row.appendChild(statusEl);
@@ -158,12 +140,9 @@ const { ui } = Astro.props;
158
140
  target.setMonth(target.getMonth() + group.months);
159
141
  target.setHours(0, 0, 0, 0);
160
142
  const isPast = target <= today;
161
- const row = buildTimelineRow(group, birthDate, today, isPast);
162
- if (isPast) {
163
- timelinePast.appendChild(row);
164
- } else {
165
- timelineFuture.appendChild(row);
166
- }
143
+ const row = buildTimelineRow(group, birthDate, isPast);
144
+ if (isPast) timelinePast.appendChild(row);
145
+ else timelineFuture.appendChild(row);
167
146
  });
168
147
  }
169
148
 
@@ -183,39 +162,40 @@ const { ui } = Astro.props;
183
162
  const today = new Date();
184
163
  today.setHours(0, 0, 0, 0);
185
164
  const next = findNextGroup(birthDate);
165
+ nextVaccinesEl.innerHTML = '';
186
166
  if (!next) {
187
167
  nextDateEl.textContent = 'Calendario completo';
188
- nextVaccinesEl.innerHTML = '';
189
168
  return;
190
169
  }
191
170
  const target = new Date(birthDate);
192
171
  target.setMonth(target.getMonth() + next.months);
193
172
  target.setHours(0, 0, 0, 0);
194
173
  const isToday = target.getTime() === today.getTime();
195
- if (isToday) {
196
- root.classList.add('vaccination-calendar-is-today');
197
- nextDateEl.textContent = ui['btnToday'] ?? null;
198
- } else {
199
- root.classList.remove('vaccination-calendar-is-today');
200
- nextDateEl.textContent = `${getAgeLabel(next.months)} ${formatDate(target)}`;
201
- }
202
- nextVaccinesEl.innerHTML = '';
203
- next.vaccines.forEach((name) => {
204
- const li = document.createElement('li');
205
- li.className = 'vaccination-calendar-vac-item';
206
- const nameEl = document.createElement('span');
207
- nameEl.className = 'vaccination-calendar-vac-name';
208
- nameEl.textContent = name;
209
- li.appendChild(nameEl);
210
- nextVaccinesEl.appendChild(li);
174
+ root.classList.toggle('vc-is-today', isToday);
175
+ nextDateEl.textContent = isToday
176
+ ? (ui['btnToday'] ?? '¡Hoy!')
177
+ : `${getAgeLabel(next.months)} ${formatDate(target)}`;
178
+
179
+ next.vaccines.forEach((name, idx) => {
180
+ const item = document.createElement('div');
181
+ item.className = 'vc-vac-item';
182
+ item.innerHTML = `<div class="vc-vac-icon">${idx + 1}</div><div class="vc-vac-info"><span class="vc-vac-name">${name}</span></div>`;
183
+ nextVaccinesEl.appendChild(item);
211
184
  });
185
+
186
+ if (btnReminder) {
187
+ btnReminder.innerHTML = isToday
188
+ ? `<span>${ui['btnToday'] ?? '¡Hoy!'}</span>`
189
+ : `<span>${ui['btnAddReminder'] ?? ''}</span>`;
190
+ }
212
191
  }
213
192
 
214
193
  function updateDisplay(birthDate: Date): void {
215
194
  const today = new Date();
216
195
  today.setHours(0, 0, 0, 0);
217
196
  ageBadge.textContent = calculateAge(birthDate, today);
218
- root.classList.add('vaccination-calendar-active');
197
+ ageBadge.classList.add('vc-age-visible');
198
+ root.classList.add('vc-active');
219
199
  emptyEl.style.display = 'none';
220
200
  contextEl.style.display = '';
221
201
  sectionsEl.style.display = '';
@@ -224,12 +204,13 @@ const { ui } = Astro.props;
224
204
  }
225
205
 
226
206
  function clearDisplay(): void {
227
- root.classList.remove('vaccination-calendar-active', 'vaccination-calendar-is-today');
207
+ root.classList.remove('vc-active', 'vc-is-today');
228
208
  ageBadge.textContent = '';
209
+ ageBadge.classList.remove('vc-age-visible');
229
210
  emptyEl.style.display = '';
230
211
  contextEl.style.display = 'none';
231
212
  sectionsEl.style.display = 'none';
232
- errorEl.textContent = '';
213
+ tripleEl.classList.remove('has-error');
233
214
  }
234
215
 
235
216
  function handleInput(): void {
@@ -237,7 +218,7 @@ const { ui } = Astro.props;
237
218
  if (!date) {
238
219
  currentBirthDate = null;
239
220
  const hasValue = ddInput.value || mmInput.value || yyyyInput.value;
240
- errorEl.textContent = hasValue ? (ui['invalidMsg'] ?? '') : '';
221
+ tripleEl.classList.toggle('has-error', !!hasValue);
241
222
  clearDisplay();
242
223
  return;
243
224
  }
@@ -245,17 +226,18 @@ const { ui } = Astro.props;
245
226
  today.setHours(0, 0, 0, 0);
246
227
  if (date > today) {
247
228
  currentBirthDate = null;
248
- errorEl.textContent = ui['futureMsg'] ?? null;
229
+ tripleEl.classList.add('has-error');
249
230
  clearDisplay();
250
231
  return;
251
232
  }
252
- errorEl.textContent = '';
233
+ tripleEl.classList.remove('has-error');
253
234
  currentBirthDate = date;
254
235
  updateDisplay(date);
255
236
  }
256
237
 
257
238
  function autoAdvance(input: HTMLInputElement, next: HTMLInputElement | null, maxLen: number): void {
258
239
  input.addEventListener('input', () => {
240
+ input.value = input.value.replace(/\D/g, '');
259
241
  if (input.value.length >= maxLen && next) next.focus();
260
242
  handleInput();
261
243
  });
@@ -263,9 +245,9 @@ const { ui } = Astro.props;
263
245
 
264
246
  autoAdvance(ddInput, mmInput, 2);
265
247
  autoAdvance(mmInput, yyyyInput, 2);
266
- yyyyInput.addEventListener('input', handleInput);
248
+ yyyyInput.addEventListener('input', () => { yyyyInput.value = yyyyInput.value.replace(/\D/g, ''); handleInput(); });
267
249
 
268
- btnReminder.addEventListener('click', () => {
250
+ btnReminder?.addEventListener('click', () => {
269
251
  if (!currentBirthDate) return;
270
252
  const ics = buildIcsContent(currentBirthDate, doseGroups);
271
253
  const blob = new Blob([ics], { type: 'text/calendar' });
@@ -277,9 +259,23 @@ const { ui } = Astro.props;
277
259
  URL.revokeObjectURL(url);
278
260
  });
279
261
 
280
- btnShare.addEventListener('click', () => {
281
- if (!currentBirthDate || !navigator.share) return;
282
- navigator.share({ title: 'Calendario de Vacunación', ...(ui['labelSource'] !== undefined ? { text: ui['labelSource'] } : {}), url: window.location.href });
262
+ shareLink?.addEventListener('click', async () => {
263
+ try {
264
+ if (navigator.share) {
265
+ await navigator.share({ title: ui['labelNextAppointment'] ?? '', url: window.location.href });
266
+ } else {
267
+ await navigator.clipboard.writeText(window.location.href);
268
+ const orig = shareLink.textContent ?? '';
269
+ shareLink.textContent = '¡Enlace copiado!';
270
+ setTimeout(() => { shareLink.textContent = orig; }, 2000);
271
+ }
272
+ } catch {}
273
+ });
274
+
275
+ root.querySelectorAll('.vc-accordion-trigger').forEach((trigger) => {
276
+ trigger.addEventListener('click', () => {
277
+ trigger.parentElement?.classList.toggle('vc-open');
278
+ });
283
279
  });
284
280
 
285
281
  clearDisplay();