@jjlmoya/utils-health 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 (155) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +60 -0
  3. package/src/category/i18n/es.ts +60 -0
  4. package/src/category/i18n/fr.ts +60 -0
  5. package/src/category/index.ts +22 -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 +28 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +36 -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/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +22 -0
  22. package/src/tests/title_quality.test.ts +55 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/binauralTuner/bibliography.astro +14 -0
  25. package/src/tool/binauralTuner/component.astro +687 -0
  26. package/src/tool/binauralTuner/i18n/en.ts +187 -0
  27. package/src/tool/binauralTuner/i18n/es.ts +187 -0
  28. package/src/tool/binauralTuner/i18n/fr.ts +187 -0
  29. package/src/tool/binauralTuner/index.ts +27 -0
  30. package/src/tool/binauralTuner/seo.astro +14 -0
  31. package/src/tool/binauralTuner/ui.ts +18 -0
  32. package/src/tool/bloodUnitConverter/bibliography.astro +14 -0
  33. package/src/tool/bloodUnitConverter/component.astro +915 -0
  34. package/src/tool/bloodUnitConverter/i18n/en.ts +227 -0
  35. package/src/tool/bloodUnitConverter/i18n/es.ts +250 -0
  36. package/src/tool/bloodUnitConverter/i18n/fr.ts +218 -0
  37. package/src/tool/bloodUnitConverter/index.ts +27 -0
  38. package/src/tool/bloodUnitConverter/seo.astro +14 -0
  39. package/src/tool/bloodUnitConverter/ui.ts +38 -0
  40. package/src/tool/bmiCalculator/bibliography.astro +14 -0
  41. package/src/tool/bmiCalculator/component.astro +415 -0
  42. package/src/tool/bmiCalculator/i18n/en.ts +217 -0
  43. package/src/tool/bmiCalculator/i18n/es.ts +221 -0
  44. package/src/tool/bmiCalculator/i18n/fr.ts +217 -0
  45. package/src/tool/bmiCalculator/index.ts +27 -0
  46. package/src/tool/bmiCalculator/seo.astro +14 -0
  47. package/src/tool/bmiCalculator/ui.ts +21 -0
  48. package/src/tool/breathingVisualizer/bibliography.astro +14 -0
  49. package/src/tool/breathingVisualizer/component.astro +636 -0
  50. package/src/tool/breathingVisualizer/i18n/en.ts +206 -0
  51. package/src/tool/breathingVisualizer/i18n/es.ts +206 -0
  52. package/src/tool/breathingVisualizer/i18n/fr.ts +206 -0
  53. package/src/tool/breathingVisualizer/index.ts +27 -0
  54. package/src/tool/breathingVisualizer/seo.astro +14 -0
  55. package/src/tool/breathingVisualizer/ui.ts +31 -0
  56. package/src/tool/caffeineTracker/bibliography.astro +14 -0
  57. package/src/tool/caffeineTracker/component.astro +1210 -0
  58. package/src/tool/caffeineTracker/i18n/en.ts +198 -0
  59. package/src/tool/caffeineTracker/i18n/es.ts +198 -0
  60. package/src/tool/caffeineTracker/i18n/fr.ts +198 -0
  61. package/src/tool/caffeineTracker/index.ts +27 -0
  62. package/src/tool/caffeineTracker/logic.ts +31 -0
  63. package/src/tool/caffeineTracker/seo.astro +14 -0
  64. package/src/tool/caffeineTracker/ui.ts +36 -0
  65. package/src/tool/daltonismSimulator/bibliography.astro +14 -0
  66. package/src/tool/daltonismSimulator/component.astro +383 -0
  67. package/src/tool/daltonismSimulator/i18n/en.ts +188 -0
  68. package/src/tool/daltonismSimulator/i18n/es.ts +218 -0
  69. package/src/tool/daltonismSimulator/i18n/fr.ts +168 -0
  70. package/src/tool/daltonismSimulator/index.ts +27 -0
  71. package/src/tool/daltonismSimulator/seo.astro +14 -0
  72. package/src/tool/daltonismSimulator/ui.ts +20 -0
  73. package/src/tool/digestionStopwatch/bibliography.astro +14 -0
  74. package/src/tool/digestionStopwatch/component.astro +627 -0
  75. package/src/tool/digestionStopwatch/i18n/en.ts +173 -0
  76. package/src/tool/digestionStopwatch/i18n/es.ts +173 -0
  77. package/src/tool/digestionStopwatch/i18n/fr.ts +173 -0
  78. package/src/tool/digestionStopwatch/index.ts +27 -0
  79. package/src/tool/digestionStopwatch/logic.ts +63 -0
  80. package/src/tool/digestionStopwatch/seo.astro +14 -0
  81. package/src/tool/digestionStopwatch/ui.ts +20 -0
  82. package/src/tool/epworthSleepinessScale/bibliography.astro +14 -0
  83. package/src/tool/epworthSleepinessScale/component.astro +528 -0
  84. package/src/tool/epworthSleepinessScale/i18n/en.ts +217 -0
  85. package/src/tool/epworthSleepinessScale/i18n/es.ts +217 -0
  86. package/src/tool/epworthSleepinessScale/i18n/fr.ts +217 -0
  87. package/src/tool/epworthSleepinessScale/index.ts +27 -0
  88. package/src/tool/epworthSleepinessScale/seo.astro +14 -0
  89. package/src/tool/epworthSleepinessScale/ui.ts +27 -0
  90. package/src/tool/hydrationCalculator/bibliography.astro +14 -0
  91. package/src/tool/hydrationCalculator/component.astro +694 -0
  92. package/src/tool/hydrationCalculator/i18n/en.ts +217 -0
  93. package/src/tool/hydrationCalculator/i18n/es.ts +222 -0
  94. package/src/tool/hydrationCalculator/i18n/fr.ts +199 -0
  95. package/src/tool/hydrationCalculator/index.ts +27 -0
  96. package/src/tool/hydrationCalculator/seo.astro +14 -0
  97. package/src/tool/hydrationCalculator/ui.ts +28 -0
  98. package/src/tool/pelliRobsonTest/bibliography.astro +14 -0
  99. package/src/tool/pelliRobsonTest/component.astro +653 -0
  100. package/src/tool/pelliRobsonTest/i18n/en.ts +205 -0
  101. package/src/tool/pelliRobsonTest/i18n/es.ts +205 -0
  102. package/src/tool/pelliRobsonTest/i18n/fr.ts +205 -0
  103. package/src/tool/pelliRobsonTest/index.ts +27 -0
  104. package/src/tool/pelliRobsonTest/seo.astro +14 -0
  105. package/src/tool/pelliRobsonTest/ui.ts +21 -0
  106. package/src/tool/peripheralVisionTrainer/bibliography.astro +14 -0
  107. package/src/tool/peripheralVisionTrainer/component.astro +678 -0
  108. package/src/tool/peripheralVisionTrainer/i18n/en.ts +224 -0
  109. package/src/tool/peripheralVisionTrainer/i18n/es.ts +224 -0
  110. package/src/tool/peripheralVisionTrainer/i18n/fr.ts +211 -0
  111. package/src/tool/peripheralVisionTrainer/index.ts +27 -0
  112. package/src/tool/peripheralVisionTrainer/seo.astro +14 -0
  113. package/src/tool/peripheralVisionTrainer/ui.ts +26 -0
  114. package/src/tool/readingDistanceCalculator/bibliography.astro +14 -0
  115. package/src/tool/readingDistanceCalculator/component.astro +588 -0
  116. package/src/tool/readingDistanceCalculator/i18n/en.ts +202 -0
  117. package/src/tool/readingDistanceCalculator/i18n/es.ts +215 -0
  118. package/src/tool/readingDistanceCalculator/i18n/fr.ts +193 -0
  119. package/src/tool/readingDistanceCalculator/index.ts +31 -0
  120. package/src/tool/readingDistanceCalculator/seo.astro +14 -0
  121. package/src/tool/readingDistanceCalculator/ui.ts +18 -0
  122. package/src/tool/screenDecompressionTime/bibliography.astro +14 -0
  123. package/src/tool/screenDecompressionTime/component.astro +671 -0
  124. package/src/tool/screenDecompressionTime/i18n/en.ts +225 -0
  125. package/src/tool/screenDecompressionTime/i18n/es.ts +247 -0
  126. package/src/tool/screenDecompressionTime/i18n/fr.ts +225 -0
  127. package/src/tool/screenDecompressionTime/index.ts +27 -0
  128. package/src/tool/screenDecompressionTime/seo.astro +14 -0
  129. package/src/tool/screenDecompressionTime/ui.ts +32 -0
  130. package/src/tool/tinnitusReliever/bibliography.astro +14 -0
  131. package/src/tool/tinnitusReliever/component.astro +581 -0
  132. package/src/tool/tinnitusReliever/i18n/en.ts +161 -0
  133. package/src/tool/tinnitusReliever/i18n/es.ts +161 -0
  134. package/src/tool/tinnitusReliever/i18n/fr.ts +161 -0
  135. package/src/tool/tinnitusReliever/index.ts +27 -0
  136. package/src/tool/tinnitusReliever/seo.astro +14 -0
  137. package/src/tool/tinnitusReliever/ui.ts +9 -0
  138. package/src/tool/ubeCalculator/bibliography.astro +14 -0
  139. package/src/tool/ubeCalculator/component.astro +683 -0
  140. package/src/tool/ubeCalculator/i18n/en.ts +200 -0
  141. package/src/tool/ubeCalculator/i18n/es.ts +200 -0
  142. package/src/tool/ubeCalculator/i18n/fr.ts +196 -0
  143. package/src/tool/ubeCalculator/index.ts +27 -0
  144. package/src/tool/ubeCalculator/seo.astro +14 -0
  145. package/src/tool/ubeCalculator/ui.ts +26 -0
  146. package/src/tool/waterPurifier/bibliography.astro +14 -0
  147. package/src/tool/waterPurifier/component.astro +628 -0
  148. package/src/tool/waterPurifier/i18n/en.ts +167 -0
  149. package/src/tool/waterPurifier/i18n/es.ts +167 -0
  150. package/src/tool/waterPurifier/i18n/fr.ts +167 -0
  151. package/src/tool/waterPurifier/index.ts +27 -0
  152. package/src/tool/waterPurifier/seo.astro +14 -0
  153. package/src/tool/waterPurifier/ui.ts +18 -0
  154. package/src/tools.ts +19 -0
  155. package/src/types.ts +72 -0
@@ -0,0 +1,915 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { BloodUnitConverterUI } from './ui';
4
+
5
+ interface Props {
6
+ ui?: Partial<BloodUnitConverterUI>;
7
+ }
8
+
9
+ const ui = (Astro.props.ui ?? {}) as BloodUnitConverterUI;
10
+
11
+ interface AnalyteRange {
12
+ max: number | null;
13
+ labelKey: string;
14
+ status: string;
15
+ }
16
+
17
+ interface Analyte {
18
+ id: string;
19
+ nameKey: string;
20
+ icon: string;
21
+ factor: number;
22
+ ranges: AnalyteRange[];
23
+ }
24
+
25
+ const analytes: Analyte[] = [
26
+ {
27
+ id: 'glucosa',
28
+ nameKey: 'analyteGlucosa',
29
+ icon: 'mdi:water-percent',
30
+ factor: 18.016,
31
+ ranges: [
32
+ { max: 70, labelKey: 'rangeBajo', status: 'low' },
33
+ { max: 100, labelKey: 'rangeNormal', status: 'normal' },
34
+ { max: 126, labelKey: 'rangePrediabetes', status: 'border' },
35
+ { max: null, labelKey: 'rangeDiabetes', status: 'high' },
36
+ ],
37
+ },
38
+ {
39
+ id: 'colesterol-total',
40
+ nameKey: 'analyteColesterolTotal',
41
+ icon: 'mdi:heart-pulse',
42
+ factor: 38.67,
43
+ ranges: [
44
+ { max: 200, labelKey: 'rangeDeseable', status: 'normal' },
45
+ { max: 240, labelKey: 'rangeLimite', status: 'border' },
46
+ { max: null, labelKey: 'rangeAlto', status: 'high' },
47
+ ],
48
+ },
49
+ {
50
+ id: 'hdl',
51
+ nameKey: 'analyteHdl',
52
+ icon: 'mdi:shield-check',
53
+ factor: 38.67,
54
+ ranges: [
55
+ { max: 40, labelKey: 'rangeRiesgo', status: 'high' },
56
+ { max: 60, labelKey: 'rangeAceptable', status: 'border' },
57
+ { max: null, labelKey: 'rangeExcelente', status: 'normal' },
58
+ ],
59
+ },
60
+ {
61
+ id: 'ldl',
62
+ nameKey: 'analyteLdl',
63
+ icon: 'mdi:alert-octagon',
64
+ factor: 38.67,
65
+ ranges: [
66
+ { max: 100, labelKey: 'rangeOptimo', status: 'normal' },
67
+ { max: 130, labelKey: 'rangeCasiOptimo', status: 'normal' },
68
+ { max: 160, labelKey: 'rangeLimite', status: 'border' },
69
+ { max: 190, labelKey: 'rangeAlto', status: 'high' },
70
+ { max: null, labelKey: 'rangeMuyAlto', status: 'high' },
71
+ ],
72
+ },
73
+ {
74
+ id: 'trigliceridos',
75
+ nameKey: 'analyteTriglic',
76
+ icon: 'mdi:chart-arc',
77
+ factor: 88.57,
78
+ ranges: [
79
+ { max: 150, labelKey: 'rangeNormal', status: 'normal' },
80
+ { max: 200, labelKey: 'rangeLimite', status: 'border' },
81
+ { max: 500, labelKey: 'rangeAlto', status: 'high' },
82
+ { max: null, labelKey: 'rangeMuyAlto', status: 'high' },
83
+ ],
84
+ },
85
+ {
86
+ id: 'creatinina',
87
+ nameKey: 'analyteCreatinina',
88
+ icon: 'mdi:test-tube',
89
+ factor: 11.312,
90
+ ranges: [
91
+ { max: 0.7, labelKey: 'rangeBajo', status: 'low' },
92
+ { max: 1.3, labelKey: 'rangeNormal', status: 'normal' },
93
+ { max: null, labelKey: 'rangeElevado', status: 'high' },
94
+ ],
95
+ },
96
+ {
97
+ id: 'urea',
98
+ nameKey: 'analyteUrea',
99
+ icon: 'mdi:flask-outline',
100
+ factor: 6.006,
101
+ ranges: [
102
+ { max: 10, labelKey: 'rangeBajo', status: 'low' },
103
+ { max: 50, labelKey: 'rangeNormal', status: 'normal' },
104
+ { max: null, labelKey: 'rangeAlto', status: 'high' },
105
+ ],
106
+ },
107
+ ];
108
+ ---
109
+
110
+ <div class="buc" data-ui={JSON.stringify(ui)}>
111
+ <div class="buc__card">
112
+
113
+
114
+ <div class="buc__header">
115
+ <span class="buc__header-icon"><Icon name="mdi:stethoscope" /></span>
116
+ <div>
117
+ <h2 class="buc__header-title" data-key="appTitle">Analítica Suite</h2>
118
+ <p class="buc__header-sub" data-key="appSubtitle">Conversor de unidades clínicas profesional</p>
119
+ </div>
120
+ </div>
121
+
122
+
123
+ <div class="buc__section">
124
+ <span class="buc__section-title" data-key="sectionAnalyte">Seleccionar Analito</span>
125
+ <div class="buc__analyte-grid">
126
+ {analytes.map((a) => (
127
+ <button
128
+ class={`buc__analyte${a.id === 'glucosa' ? ' buc__analyte--active' : ''}`}
129
+ data-id={a.id}
130
+ data-factor={a.factor}
131
+ data-ranges={JSON.stringify(a.ranges)}
132
+ >
133
+ <span class="buc__analyte-icon"><Icon name={a.icon} /></span>
134
+ <span class="buc__analyte-label" data-key={a.nameKey}></span>
135
+ </button>
136
+ ))}
137
+ </div>
138
+ </div>
139
+
140
+
141
+ <div class="buc__section buc__section--bordered">
142
+ <div class="buc__converter">
143
+ <div class="buc__input-group" id="buc-group-mg">
144
+ <label class="buc__input-label" for="buc-mg" data-key="labelMg">Masa (mg/dL)</label>
145
+ <div class="buc__input-wrap">
146
+ <input class="buc__input" id="buc-mg" type="number" placeholder="0.00" step="any" inputmode="decimal" />
147
+ <span class="buc__unit-tag">mg/dL</span>
148
+ </div>
149
+ </div>
150
+
151
+ <button class="buc__swap" id="buc-swap" aria-label="Cambiar dirección de conversión">
152
+ <span class="buc__swap-icon buc__swap-icon--h"><Icon name="mdi:swap-horizontal" /></span>
153
+ <span class="buc__swap-icon buc__swap-icon--v"><Icon name="mdi:swap-vertical" /></span>
154
+ </button>
155
+
156
+ <div class="buc__input-group" id="buc-group-mmol">
157
+ <label class="buc__input-label" for="buc-mmol" data-key="labelMmol">Molar (mmol/L)</label>
158
+ <div class="buc__input-wrap">
159
+ <input class="buc__input" id="buc-mmol" type="number" placeholder="0.00" step="any" inputmode="decimal" />
160
+ <span class="buc__unit-tag">mmol/L</span>
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+
166
+ <div class="buc__gauge">
167
+ <div class="buc__gauge-track">
168
+ <div class="buc__gauge-seg buc__gauge-seg--low"></div>
169
+ <div class="buc__gauge-seg buc__gauge-seg--normal"></div>
170
+ <div class="buc__gauge-seg buc__gauge-seg--border"></div>
171
+ <div class="buc__gauge-seg buc__gauge-seg--high"></div>
172
+ <div class="buc__gauge-marker" id="buc-gauge-marker"></div>
173
+ </div>
174
+ <div class="buc__gauge-status">
175
+ <span class="buc__status-text" id="buc-status" data-key="statusIdle">Analice un valor</span>
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+
181
+ <div class="buc__section buc__section--bordered">
182
+ <div class="buc__history-header">
183
+ <span class="buc__section-title" data-key="sectionHistory">Mi Historial</span>
184
+ <button class="buc__btn-add" id="buc-add">
185
+ <span class="buc__btn-icon"><Icon name="mdi:plus" /></span>
186
+ <span data-key="historyAdd">Añadir a Tabla</span>
187
+ </button>
188
+ </div>
189
+ <div class="buc__table-wrap">
190
+ <table class="buc__table" id="buc-table">
191
+ <thead>
192
+ <tr>
193
+ <th data-key="historyAnalyte">Analito</th>
194
+ <th data-key="historyMg">mg/dL</th>
195
+ <th data-key="historyMmol">mmol/L</th>
196
+ <th data-key="historyStatus">Estado</th>
197
+ <th></th>
198
+ </tr>
199
+ </thead>
200
+ <tbody id="buc-tbody">
201
+ <tr class="buc__empty-row">
202
+ <td colspan="5" data-key="historyEmpty">No hay registros guardados</td>
203
+ </tr>
204
+ </tbody>
205
+ </table>
206
+ </div>
207
+ </div>
208
+
209
+
210
+ <div class="buc__footer">
211
+ <span class="buc__footer-icon"><Icon name="mdi:information-outline" /></span>
212
+ <p class="buc__footer-text" data-key="disclaimerText">
213
+ Aviso Médico: herramienta informativa. No sustituye un diagnóstico profesional.
214
+ </p>
215
+ </div>
216
+
217
+ </div>
218
+
219
+
220
+ <div class="buc__icon-cache" aria-hidden="true">
221
+ <span id="buc-icon-delete"><Icon name="mdi:close-circle-outline" /></span>
222
+ </div>
223
+ </div>
224
+
225
+ <style>
226
+ .buc {
227
+ --buc-accent: #3b82f6;
228
+ --buc-accent-dark: #2563eb;
229
+ --buc-bg: #fff;
230
+ --buc-border: #e2e8f0;
231
+ --buc-divider: rgba(226, 232, 240, 0.8);
232
+ --buc-text: #0f172a;
233
+ --buc-muted: #64748b;
234
+ --buc-input-bg: #f1f5f9;
235
+ --buc-header-bg: linear-gradient(135deg, rgba(59,130,246,0.12) 0%, rgba(37,99,235,0.04) 100%);
236
+ --buc-low: #64748b;
237
+ --buc-normal: #10b981;
238
+ --buc-border-c: #f59e0b;
239
+ --buc-high: #ef4444;
240
+
241
+ max-width: 900px;
242
+ margin: 0 auto;
243
+ padding: 1rem;
244
+ }
245
+
246
+
247
+ .buc__card {
248
+ background: var(--buc-bg);
249
+ border: 1px solid var(--buc-border);
250
+ border-radius: 2.5rem;
251
+ box-shadow: 0 25px 50px rgba(0,0,0,0.08);
252
+ overflow: hidden;
253
+ }
254
+
255
+ .buc__icon-cache { display: none; }
256
+
257
+
258
+ .buc__section {
259
+ padding: 2.5rem;
260
+ }
261
+
262
+ .buc__section--bordered {
263
+ border-top: 1px solid var(--buc-divider);
264
+ }
265
+
266
+ .buc__section-title {
267
+ display: block;
268
+ font-size: 0.75rem;
269
+ font-weight: 700;
270
+ text-transform: uppercase;
271
+ letter-spacing: 0.1em;
272
+ color: var(--buc-muted);
273
+ margin-bottom: 1.5rem;
274
+ }
275
+
276
+
277
+ .buc__header {
278
+ padding: 2.5rem;
279
+ background: var(--buc-header-bg);
280
+ border-bottom: 1px solid var(--buc-divider);
281
+ display: flex;
282
+ align-items: center;
283
+ gap: 1.5rem;
284
+ }
285
+
286
+ .buc__header-icon {
287
+ color: var(--buc-accent);
288
+ width: 3rem;
289
+ height: 3rem;
290
+ flex-shrink: 0;
291
+ }
292
+
293
+ .buc__header-icon svg { width: 100%; height: 100%; display: block; }
294
+
295
+ .buc__header-title {
296
+ font-size: 1.75rem;
297
+ font-weight: 800;
298
+ color: var(--buc-text);
299
+ margin: 0;
300
+ }
301
+
302
+ .buc__header-sub {
303
+ font-size: 0.9rem;
304
+ color: var(--buc-muted);
305
+ margin: 0.2rem 0 0;
306
+ }
307
+
308
+
309
+ .buc__analyte-grid {
310
+ display: grid;
311
+ grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
312
+ gap: 0.875rem;
313
+ }
314
+
315
+ .buc__analyte {
316
+ background: var(--buc-bg);
317
+ border: 1px solid var(--buc-border);
318
+ border-radius: 1.25rem;
319
+ padding: 1rem 0.75rem;
320
+ display: flex;
321
+ flex-direction: column;
322
+ align-items: center;
323
+ gap: 0.45rem;
324
+ cursor: pointer;
325
+ transition: all 0.2s ease;
326
+ color: var(--buc-text);
327
+ }
328
+
329
+ .buc__analyte:hover {
330
+ transform: translateY(-3px);
331
+ box-shadow: 0 8px 20px rgba(0,0,0,0.08);
332
+ border-color: var(--buc-accent);
333
+ }
334
+
335
+ .buc__analyte--active {
336
+ background: var(--buc-accent);
337
+ border-color: var(--buc-accent);
338
+ color: #fff;
339
+ box-shadow: 0 8px 20px rgba(59,130,246,0.3);
340
+ }
341
+
342
+ .buc__analyte-icon {
343
+ width: 1.75rem;
344
+ height: 1.75rem;
345
+ }
346
+
347
+ .buc__analyte-icon svg { width: 100%; height: 100%; display: block; }
348
+
349
+ .buc__analyte-label {
350
+ font-size: 0.75rem;
351
+ font-weight: 700;
352
+ text-align: center;
353
+ line-height: 1.2;
354
+ }
355
+
356
+
357
+ .buc__converter {
358
+ display: grid;
359
+ grid-template-columns: 1fr auto 1fr;
360
+ align-items: center;
361
+ gap: 1.25rem;
362
+ margin-bottom: 2rem;
363
+ }
364
+
365
+ .buc__input-group {
366
+ display: flex;
367
+ flex-direction: column;
368
+ gap: 0.5rem;
369
+ }
370
+
371
+ .buc__input-label {
372
+ font-size: 0.7rem;
373
+ font-weight: 700;
374
+ text-transform: uppercase;
375
+ letter-spacing: 0.05em;
376
+ color: var(--buc-muted);
377
+ }
378
+
379
+ .buc__input-wrap {
380
+ position: relative;
381
+ display: flex;
382
+ align-items: center;
383
+ }
384
+
385
+ .buc__input {
386
+ width: 100%;
387
+ background: var(--buc-input-bg);
388
+ border: 2px solid transparent;
389
+ border-radius: 1rem;
390
+ padding: 1rem 4.5rem 1rem 1.1rem;
391
+ font-size: 1.5rem;
392
+ font-weight: 800;
393
+ color: var(--buc-text);
394
+ outline: none;
395
+ transition: all 0.2s ease;
396
+ box-sizing: border-box;
397
+ }
398
+
399
+ .buc__input:focus {
400
+ background: var(--buc-bg);
401
+ border-color: var(--buc-accent);
402
+ box-shadow: 0 0 0 3px rgba(59,130,246,0.12);
403
+ }
404
+
405
+ .buc__input::-webkit-inner-spin-button,
406
+ .buc__input::-webkit-outer-spin-button { -webkit-appearance: none; }
407
+ .buc__input[type="number"] { -moz-appearance: textfield; }
408
+
409
+ .buc__unit-tag {
410
+ position: absolute;
411
+ right: 0.875rem;
412
+ font-size: 0.68rem;
413
+ font-weight: 800;
414
+ color: var(--buc-muted);
415
+ background: var(--buc-bg);
416
+ padding: 0.2rem 0.45rem;
417
+ border-radius: 0.4rem;
418
+ border: 1px solid var(--buc-border);
419
+ pointer-events: none;
420
+ }
421
+
422
+ .buc__swap {
423
+ width: 3.25rem;
424
+ height: 3.25rem;
425
+ border-radius: 50%;
426
+ background: var(--buc-input-bg);
427
+ border: 1px solid var(--buc-border);
428
+ display: flex;
429
+ align-items: center;
430
+ justify-content: center;
431
+ cursor: pointer;
432
+ color: var(--buc-text);
433
+ transition: all 0.3s ease;
434
+ flex-shrink: 0;
435
+ }
436
+
437
+ .buc__swap:hover, .buc__swap--active {
438
+ background: var(--buc-accent);
439
+ border-color: var(--buc-accent);
440
+ color: #fff;
441
+ }
442
+
443
+ .buc__swap:hover { transform: rotate(180deg); }
444
+
445
+ .buc__swap-icon {
446
+ width: 1.4rem;
447
+ height: 1.4rem;
448
+ }
449
+
450
+ .buc__swap-icon svg { width: 100%; height: 100%; display: block; }
451
+
452
+ .buc__swap-icon--v { display: none; }
453
+
454
+
455
+ .buc__gauge-track {
456
+ height: 14px;
457
+ background: var(--buc-input-bg);
458
+ border-radius: 7px;
459
+ display: flex;
460
+ position: relative;
461
+ overflow: visible;
462
+ margin-bottom: 1.5rem;
463
+ }
464
+
465
+ .buc__gauge-seg {
466
+ height: 100%;
467
+ flex: 1;
468
+ }
469
+
470
+ .buc__gauge-seg--low { background: var(--buc-low); border-radius: 7px 0 0 7px; }
471
+ .buc__gauge-seg--normal { background: var(--buc-normal); }
472
+ .buc__gauge-seg--border { background: var(--buc-border-c); }
473
+ .buc__gauge-seg--high { background: var(--buc-high); border-radius: 0 7px 7px 0; }
474
+
475
+ .buc__gauge-marker {
476
+ position: absolute;
477
+ top: -8px;
478
+ left: 0%;
479
+ width: 30px;
480
+ height: 30px;
481
+ background: var(--buc-bg);
482
+ border: 5px solid var(--buc-accent);
483
+ border-radius: 50%;
484
+ transform: translateX(-50%);
485
+ transition: left 0.55s cubic-bezier(0.34, 1.56, 0.64, 1);
486
+ z-index: 2;
487
+ box-shadow: 0 4px 10px rgba(0,0,0,0.1);
488
+ }
489
+
490
+ .buc__gauge-status {
491
+ text-align: center;
492
+ min-height: 2rem;
493
+ }
494
+
495
+ .buc__status-text {
496
+ font-size: 1.2rem;
497
+ font-weight: 800;
498
+ color: var(--buc-muted);
499
+ transition: color 0.3s ease;
500
+ }
501
+
502
+ .buc :global(.buc__status--low) { color: var(--buc-low); }
503
+ .buc :global(.buc__status--normal) { color: var(--buc-normal); }
504
+ .buc :global(.buc__status--border) { color: var(--buc-border-c); }
505
+ .buc :global(.buc__status--high) { color: var(--buc-high); }
506
+
507
+
508
+ .buc__history-header {
509
+ display: flex;
510
+ justify-content: space-between;
511
+ align-items: center;
512
+ margin-bottom: 1.5rem;
513
+ }
514
+
515
+ .buc__history-header .buc__section-title { margin-bottom: 0; }
516
+
517
+ .buc__btn-add {
518
+ display: inline-flex;
519
+ align-items: center;
520
+ gap: 0.5rem;
521
+ background: rgba(59,130,246,0.1);
522
+ color: var(--buc-accent);
523
+ border: none;
524
+ padding: 0.625rem 1.25rem;
525
+ border-radius: 0.875rem;
526
+ font-weight: 800;
527
+ font-size: 0.875rem;
528
+ cursor: pointer;
529
+ transition: all 0.2s ease;
530
+ }
531
+
532
+ .buc__btn-add:hover {
533
+ background: rgba(59,130,246,0.18);
534
+ transform: translateY(-1px);
535
+ }
536
+
537
+ .buc__btn-icon {
538
+ width: 1.1rem;
539
+ height: 1.1rem;
540
+ }
541
+
542
+ .buc__btn-icon svg { width: 100%; height: 100%; display: block; }
543
+
544
+ .buc__table-wrap {
545
+ overflow-x: auto;
546
+ border-radius: 1rem;
547
+ border: 1px solid var(--buc-border);
548
+ }
549
+
550
+ .buc__table {
551
+ width: 100%;
552
+ border-collapse: collapse;
553
+ text-align: left;
554
+ min-width: 520px;
555
+ }
556
+
557
+ .buc__table thead th {
558
+ padding: 1rem 1.25rem;
559
+ font-size: 0.72rem;
560
+ text-transform: uppercase;
561
+ font-weight: 800;
562
+ color: var(--buc-muted);
563
+ background: rgba(0,0,0,0.02);
564
+ border-bottom: 2px solid var(--buc-border);
565
+ }
566
+
567
+ .buc__table tbody td {
568
+ padding: 1rem 1.25rem;
569
+ font-size: 0.95rem;
570
+ font-weight: 600;
571
+ border-bottom: 1px solid var(--buc-border);
572
+ color: var(--buc-text);
573
+ }
574
+
575
+ .buc__table tbody tr:last-child td { border-bottom: none; }
576
+
577
+ .buc__empty-row td {
578
+ text-align: center;
579
+ padding: 3rem;
580
+ color: var(--buc-muted);
581
+ font-style: italic;
582
+ font-weight: 400;
583
+ }
584
+
585
+
586
+ :global(.buc__row-status) {
587
+ font-size: 0.8rem;
588
+ font-weight: 700;
589
+ padding: 0.25rem 0.75rem;
590
+ border-radius: 2rem;
591
+ display: inline-block;
592
+ }
593
+
594
+ :global(.buc__row-status--low) { background: rgba(100,116,139,0.12); color: var(--buc-low); }
595
+ :global(.buc__row-status--normal) { background: rgba(16,185,129,0.12); color: var(--buc-normal); }
596
+ :global(.buc__row-status--border) { background: rgba(245,158,11,0.12); color: var(--buc-border-c); }
597
+ :global(.buc__row-status--high) { background: rgba(239,68,68,0.12); color: var(--buc-high); }
598
+
599
+ :global(.buc__row-delete) {
600
+ background: rgba(239,68,68,0.08);
601
+ border: none;
602
+ width: 2rem;
603
+ height: 2rem;
604
+ border-radius: 0.5rem;
605
+ cursor: pointer;
606
+ color: var(--buc-high);
607
+ display: flex;
608
+ align-items: center;
609
+ justify-content: center;
610
+ transition: all 0.2s ease;
611
+ padding: 0;
612
+ }
613
+
614
+ :global(.buc__row-delete) svg { width: 1.1rem; height: 1.1rem; display: block; }
615
+
616
+ :global(.buc__row-delete):hover {
617
+ background: var(--buc-high);
618
+ color: #fff;
619
+ }
620
+
621
+
622
+ .buc__footer {
623
+ padding: 1.5rem 2.5rem;
624
+ background: rgba(0,0,0,0.02);
625
+ border-top: 1px solid var(--buc-divider);
626
+ display: flex;
627
+ gap: 1rem;
628
+ align-items: flex-start;
629
+ }
630
+
631
+ .buc__footer-icon {
632
+ color: var(--buc-accent);
633
+ width: 1.4rem;
634
+ height: 1.4rem;
635
+ flex-shrink: 0;
636
+ margin-top: 0.1rem;
637
+ }
638
+
639
+ .buc__footer-icon svg { width: 100%; height: 100%; display: block; }
640
+
641
+ .buc__footer-text {
642
+ font-size: 0.82rem;
643
+ line-height: 1.6;
644
+ color: var(--buc-muted);
645
+ margin: 0;
646
+ }
647
+
648
+
649
+ :global(.theme-dark) .buc {
650
+ --buc-bg: #0f172a;
651
+ --buc-border: #1e293b;
652
+ --buc-divider: rgba(30,41,59,0.9);
653
+ --buc-text: #f8fafc;
654
+ --buc-muted: #94a3b8;
655
+ --buc-input-bg: #1e293b;
656
+ --buc-header-bg: linear-gradient(135deg, rgba(59,130,246,0.12) 0%, rgba(15,23,42,0.5) 100%);
657
+ }
658
+
659
+ :global(.theme-dark) .buc__analyte {
660
+ background: #1e293b;
661
+ border-color: #334155;
662
+ }
663
+
664
+ :global(.theme-dark) .buc__unit-tag {
665
+ background: #0f172a;
666
+ border-color: #334155;
667
+ }
668
+
669
+ :global(.theme-dark) .buc__input:focus {
670
+ background: #1e293b;
671
+ }
672
+
673
+ :global(.theme-dark) .buc__table thead th {
674
+ background: rgba(255,255,255,0.02);
675
+ }
676
+
677
+ :global(.theme-dark) .buc__footer {
678
+ background: rgba(255,255,255,0.02);
679
+ }
680
+
681
+
682
+ @media (max-width: 768px) {
683
+ .buc__section { padding: 1.75rem 1.25rem; }
684
+ .buc__header { padding: 1.75rem 1.25rem; }
685
+ .buc__footer { padding: 1.25rem; }
686
+
687
+ .buc__converter {
688
+ grid-template-columns: 1fr;
689
+ gap: 1.25rem;
690
+ }
691
+
692
+ .buc__swap {
693
+ justify-self: center;
694
+ }
695
+
696
+ .buc__swap-icon--h { display: none; }
697
+ .buc__swap-icon--v { display: block; }
698
+
699
+ .buc__input { font-size: 1.25rem; }
700
+ }
701
+ </style>
702
+
703
+ <script>
704
+ interface BucUI {
705
+ appTitle?: string; appSubtitle?: string;
706
+ sectionAnalyte?: string; sectionHistory?: string;
707
+ labelMg?: string; labelMmol?: string;
708
+ swapAriaLabel?: string; statusIdle?: string;
709
+ analyteGlucosa?: string; analyteColesterolTotal?: string;
710
+ analyteHdl?: string; analyteLdl?: string;
711
+ analyteTriglic?: string; analyteCreatinina?: string; analyteUrea?: string;
712
+ rangeBajo?: string; rangeNormal?: string; rangeAlto?: string;
713
+ rangeMuyAlto?: string; rangeLimite?: string; rangeElevado?: string;
714
+ rangePrediabetes?: string; rangeDiabetes?: string; rangeDeseable?: string;
715
+ rangeRiesgo?: string; rangeAceptable?: string; rangeExcelente?: string;
716
+ rangeOptimo?: string; rangeCasiOptimo?: string;
717
+ historyAdd?: string; historyAnalyte?: string;
718
+ historyMg?: string; historyMmol?: string;
719
+ historyStatus?: string; historyEmpty?: string;
720
+ disclaimerText?: string;
721
+ }
722
+
723
+ interface AnalyteRange {
724
+ max: number | null;
725
+ labelKey: string;
726
+ status: string;
727
+ }
728
+
729
+ function applyUI(container: Element, ui: BucUI): void {
730
+ container.querySelectorAll<HTMLElement>('[data-key]').forEach((el) => {
731
+ const key = el.dataset.key as keyof BucUI;
732
+ const val = ui[key];
733
+ if (val) el.textContent = val;
734
+ });
735
+ }
736
+
737
+ function calcGaugePosition(mg: number, ranges: AnalyteRange[]): { pct: number; labelKey: string; status: string } {
738
+ const segSize = 100 / ranges.length;
739
+ for (let i = 0; i < ranges.length; i++) {
740
+ const range = ranges[i];
741
+ const prevMax = i > 0 ? (ranges[i - 1].max ?? 0) : 0;
742
+ if (range.max === null || mg < range.max) {
743
+ let pct: number;
744
+ if (range.max === null) {
745
+ pct = Math.min(i * segSize + segSize * 0.8, 97);
746
+ } else {
747
+ const offset = (mg - prevMax) / (range.max - prevMax);
748
+ pct = i * segSize + offset * segSize;
749
+ }
750
+ return { pct: Math.min(Math.max(pct, 0), 97), labelKey: range.labelKey, status: range.status };
751
+ }
752
+ }
753
+ const last = ranges[ranges.length - 1];
754
+ return { pct: 95, labelKey: last.labelKey, status: last.status };
755
+ }
756
+
757
+ // eslint-disable-next-line complexity, max-lines-per-function
758
+ function initBuc(container: Element): void {
759
+ const ui: BucUI = JSON.parse((container as HTMLElement).dataset.ui ?? '{}');
760
+ applyUI(container, ui);
761
+
762
+ const inputMg = container.querySelector<HTMLInputElement>('#buc-mg');
763
+ const inputMmol = container.querySelector<HTMLInputElement>('#buc-mmol');
764
+ const swapBtn = container.querySelector<HTMLButtonElement>('#buc-swap');
765
+ const marker = container.querySelector<HTMLElement>('#buc-gauge-marker');
766
+ const statusEl = container.querySelector<HTMLElement>('#buc-status');
767
+ const addBtn = container.querySelector<HTMLButtonElement>('#buc-add');
768
+ const tbody = container.querySelector<HTMLElement>('#buc-tbody');
769
+ const deleteHtml = container.querySelector('#buc-icon-delete')?.innerHTML ?? '';
770
+
771
+ let factor = 18.016;
772
+ let ranges: AnalyteRange[] = JSON.parse(
773
+ container.querySelector<HTMLElement>('.buc__analyte--active')?.dataset.ranges ?? '[]'
774
+ );
775
+ let isMgToMmol = true;
776
+
777
+ // eslint-disable-next-line complexity
778
+ function updateStatus(mg: number): void {
779
+ if (!mg || isNaN(mg)) {
780
+ if (marker) marker.style.left = '0%';
781
+ if (statusEl) {
782
+ statusEl.textContent = ui.statusIdle ?? 'Analice un valor';
783
+ statusEl.className = 'buc__status-text';
784
+ }
785
+ return;
786
+ }
787
+ const result = calcGaugePosition(mg, ranges);
788
+ if (marker) marker.style.left = `${result.pct}%`;
789
+ if (statusEl) {
790
+ statusEl.textContent = ui[result.labelKey as keyof BucUI] ?? result.labelKey;
791
+ statusEl.className = `buc__status-text buc__status--${result.status}`;
792
+ }
793
+ }
794
+
795
+ function convertMgToMmol(valMg: number): void {
796
+ if (!isNaN(valMg)) {
797
+ if (inputMmol) inputMmol.value = (valMg / factor).toFixed(3);
798
+ updateStatus(valMg);
799
+ } else {
800
+ if (inputMmol) inputMmol.value = '';
801
+ updateStatus(NaN);
802
+ }
803
+ }
804
+
805
+ function convertMmolToMg(valMmol: number): void {
806
+ if (!isNaN(valMmol)) {
807
+ const mg = valMmol * factor;
808
+ if (inputMg) inputMg.value = mg.toFixed(3);
809
+ updateStatus(mg);
810
+ } else {
811
+ if (inputMg) inputMg.value = '';
812
+ updateStatus(NaN);
813
+ }
814
+ }
815
+
816
+ function convert(): void {
817
+ const valMg = parseFloat(inputMg?.value ?? '');
818
+ const valMmol = parseFloat(inputMmol?.value ?? '');
819
+ if (isMgToMmol) {
820
+ convertMgToMmol(valMg);
821
+ } else {
822
+ convertMmolToMg(valMmol);
823
+ }
824
+ }
825
+
826
+
827
+ container.querySelectorAll<HTMLElement>('.buc__analyte').forEach((btn) => {
828
+ btn.addEventListener('click', () => {
829
+ container.querySelectorAll('.buc__analyte').forEach((b) => b.classList.remove('buc__analyte--active'));
830
+ btn.classList.add('buc__analyte--active');
831
+ factor = parseFloat(btn.dataset.factor ?? '1');
832
+ ranges = JSON.parse(btn.dataset.ranges ?? '[]');
833
+ convert();
834
+ });
835
+ });
836
+
837
+ inputMg?.addEventListener('input', () => { if (isMgToMmol) convert(); });
838
+ inputMmol?.addEventListener('input', () => { if (!isMgToMmol) convert(); });
839
+
840
+ swapBtn?.addEventListener('click', () => {
841
+ isMgToMmol = !isMgToMmol;
842
+ swapBtn.classList.toggle('buc__swap--active', !isMgToMmol);
843
+ convert();
844
+ });
845
+
846
+ function createDeleteCell(row: HTMLTableRowElement): HTMLTableCellElement {
847
+ const tdDel = document.createElement('td');
848
+ const delBtn = document.createElement('button');
849
+ delBtn.className = 'buc__row-delete';
850
+ delBtn.innerHTML = deleteHtml;
851
+ delBtn.setAttribute('aria-label', 'Delete');
852
+ delBtn.addEventListener('click', () => {
853
+ row.remove();
854
+ if (tbody && tbody.children.length === 0) {
855
+ const empty = document.createElement('tr');
856
+ empty.className = 'buc__empty-row';
857
+ const td = document.createElement('td');
858
+ td.setAttribute('colspan', '5');
859
+ td.textContent = ui.historyEmpty ?? 'No hay registros guardados';
860
+ empty.appendChild(td);
861
+ tbody.appendChild(empty);
862
+ }
863
+ });
864
+ tdDel.appendChild(delBtn);
865
+ return tdDel;
866
+ }
867
+
868
+ function createStatusCell(statusText: string, statusCls: string): HTMLTableCellElement {
869
+ const tdStatus = document.createElement('td');
870
+ const badge = document.createElement('span');
871
+ badge.className = `buc__row-status buc__row-status--${statusCls.replace('buc__status--', '')}`;
872
+ badge.textContent = statusText;
873
+ tdStatus.appendChild(badge);
874
+ return tdStatus;
875
+ }
876
+
877
+ function buildRowCells(data: { analyteName: string; valMg: string; valMmol: string; statusText: string; statusCls: string }, row: HTMLTableRowElement): void {
878
+ const tdAnalyte = document.createElement('td');
879
+ tdAnalyte.textContent = data.analyteName;
880
+ const tdMg = document.createElement('td');
881
+ tdMg.textContent = data.valMg;
882
+ const tdMmol = document.createElement('td');
883
+ tdMmol.textContent = data.valMmol;
884
+
885
+ row.appendChild(tdAnalyte);
886
+ row.appendChild(tdMg);
887
+ row.appendChild(tdMmol);
888
+ row.appendChild(createStatusCell(data.statusText, data.statusCls));
889
+ row.appendChild(createDeleteCell(row));
890
+ }
891
+
892
+ // eslint-disable-next-line complexity
893
+ function addHistoryRow(): void {
894
+ const valMg = inputMg?.value;
895
+ const valMmol = inputMmol?.value;
896
+ const statusText = statusEl?.textContent ?? '';
897
+ const statusCls = (statusEl?.className ?? '').replace('buc__status-text', '').trim();
898
+ const analyteName = container.querySelector<HTMLElement>('.buc__analyte--active .buc__analyte-label')?.textContent ?? '';
899
+
900
+ if (!valMg || !valMmol || !analyteName || statusText === (ui.statusIdle ?? '')) return;
901
+
902
+ tbody?.querySelector('.buc__empty-row')?.remove();
903
+
904
+ const row = document.createElement('tr');
905
+ buildRowCells({ analyteName, valMg, valMmol, statusText, statusCls }, row);
906
+ tbody?.appendChild(row);
907
+ }
908
+
909
+ addBtn?.addEventListener('click', addHistoryRow);
910
+
911
+ updateStatus(NaN);
912
+ }
913
+
914
+ document.querySelectorAll<HTMLElement>('.buc').forEach(initBuc);
915
+ </script>