@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,683 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { UbeCalculatorUI } from './ui';
4
+
5
+ interface Props {
6
+ ui?: Partial<UbeCalculatorUI>;
7
+ }
8
+
9
+ const ui = (Astro.props.ui ?? {}) as UbeCalculatorUI;
10
+
11
+ const PRESETS = [
12
+ { key: 'drinkCana', vol: 200, abv: 5, icon: 'mdi:beer', id: 'cana' },
13
+ { key: 'drinkTercio', vol: 330, abv: 5, icon: 'mdi:bottle-soda', id: 'tercio' },
14
+ { key: 'drinkVino', vol: 120, abv: 12, icon: 'mdi:glass-wine', id: 'vino' },
15
+ { key: 'drinkCopa', vol: 50, abv: 40, icon: 'mdi:glass-cocktail', id: 'copa' },
16
+ { key: 'drinkChupito', vol: 40, abv: 40, icon: 'mdi:cup-outline', id: 'chupito' },
17
+ ] as const;
18
+ ---
19
+
20
+ <div class="ube" data-ui={JSON.stringify(ui)}>
21
+ <div class="ube__card">
22
+
23
+
24
+ <div class="ube__section">
25
+ <h2 class="ube__section-title" data-key="sectionAdd">Añadir Bebida</h2>
26
+
27
+ <div class="ube__presets">
28
+ {PRESETS.map((p) => (
29
+ <button class="ube__preset" data-vol={p.vol} data-abv={p.abv} data-icon-id={p.id}>
30
+ <span class="ube__preset-icon"><Icon name={p.icon} /></span>
31
+ <span class="ube__preset-label" data-key={p.key}></span>
32
+ <span class="ube__preset-meta">{p.vol}ml · {p.abv}%</span>
33
+ </button>
34
+ ))}
35
+ </div>
36
+
37
+ <div class="ube__custom">
38
+ <div class="ube__field">
39
+ <label class="ube__label" for="ube-vol" data-key="labelVolume">Volumen (ml)</label>
40
+ <input class="ube__input" id="ube-vol" type="number" min="1" max="2000" placeholder="250" />
41
+ </div>
42
+ <div class="ube__field">
43
+ <label class="ube__label" for="ube-abv" data-key="labelAbv">Graduación (% ABV)</label>
44
+ <input class="ube__input" id="ube-abv" type="number" min="0.1" max="100" step="0.1" placeholder="5.0" />
45
+ </div>
46
+ <button class="ube__btn-add" id="ube-add-custom" data-key="btnAddCustom">
47
+ Añadir Bebida Manual
48
+ </button>
49
+ </div>
50
+ </div>
51
+
52
+
53
+ <div class="ube__section ube__section--bordered">
54
+ <h2 class="ube__section-title" data-key="sectionConsumed">Consumo Acumulado</h2>
55
+
56
+ <div class="ube__list" id="ube-list"></div>
57
+
58
+ <div class="ube__empty" id="ube-empty">
59
+ <span class="ube__empty-icon"><Icon name="mdi:cup-off-outline" /></span>
60
+ <p class="ube__empty-title" data-key="emptyTitle">Tu lista de consumo está vacía</p>
61
+ <p class="ube__empty-hint" data-key="emptyHint">Selecciona bebidas arriba o añade manualmente</p>
62
+ </div>
63
+ </div>
64
+
65
+
66
+ <div class="ube__section ube__section--bordered ube__section--results">
67
+ <div class="ube__totals">
68
+ <div class="ube__stat">
69
+ <span class="ube__stat-value" id="ube-grams">0.0</span>
70
+ <span class="ube__stat-label" data-key="resultLabel">Carga Tóxica Absoluta</span>
71
+ </div>
72
+ <div class="ube__stat ube__stat--accent">
73
+ <span class="ube__stat-value" id="ube-ubes">0.0</span>
74
+ <span class="ube__stat-label" data-key="ubesLabel">UBEs</span>
75
+ </div>
76
+ </div>
77
+
78
+ <h3 class="ube__risk-header" data-key="riskHeader">Riesgo Epidemiológico</h3>
79
+ <div class="ube__risk-bar">
80
+ <div class="ube__risk-fill" id="ube-risk-fill"></div>
81
+ </div>
82
+ <div class="ube__risk-markers">
83
+ <span data-key="riskMarkerZero">0 UBEs</span>
84
+ <span data-key="riskMarkerHigh">Riesgo Alto &gt; 4 UBEs</span>
85
+ </div>
86
+ <div class="ube__risk-badge" id="ube-risk-badge"></div>
87
+ </div>
88
+
89
+ </div>
90
+
91
+
92
+ <div class="ube__icon-cache" aria-hidden="true">
93
+ {PRESETS.map((p) => (
94
+ <span id={`ube-icon-${p.id}`}><Icon name={p.icon} /></span>
95
+ ))}
96
+ <span id="ube-icon-custom"><Icon name="mdi:beaker-outline" /></span>
97
+ <span id="ube-icon-remove"><Icon name="mdi:close-circle-outline" /></span>
98
+ </div>
99
+ </div>
100
+
101
+ <style>
102
+ .ube {
103
+ --ube-primary: #8b5cf6;
104
+ --ube-accent: #ec4899;
105
+ --ube-success: #10b981;
106
+ --ube-warning: #f59e0b;
107
+ --ube-danger: #ef4444;
108
+ --ube-bg: #fff;
109
+ --ube-border: rgba(139, 92, 246, 0.18);
110
+ --ube-divider: rgba(139, 92, 246, 0.1);
111
+ --ube-text: #1e1b4b;
112
+ --ube-muted: #6b7280;
113
+ --ube-input-bg: #f5f3ff;
114
+ --ube-input-border: rgba(139, 92, 246, 0.25);
115
+ --ube-preset-bg: #f5f3ff;
116
+ --ube-preset-border: rgba(139, 92, 246, 0.2);
117
+ --ube-results-bg: linear-gradient(135deg, rgba(139, 92, 246, 0.05) 0%, rgba(236, 72, 153, 0.05) 100%);
118
+
119
+ max-width: 860px;
120
+ margin: 0 auto;
121
+ padding: 1rem;
122
+ }
123
+
124
+
125
+ .ube__card {
126
+ background: var(--ube-bg);
127
+ border: 2px solid var(--ube-border);
128
+ border-radius: 2.5rem;
129
+ box-shadow: 0 20px 60px rgba(139, 92, 246, 0.1);
130
+ overflow: hidden;
131
+ }
132
+
133
+
134
+ .ube__icon-cache {
135
+ display: none;
136
+ }
137
+
138
+
139
+ .ube__section {
140
+ padding: 2.5rem;
141
+ }
142
+
143
+ .ube__section--bordered {
144
+ border-top: 1px solid var(--ube-divider);
145
+ }
146
+
147
+ .ube__section--results {
148
+ background: var(--ube-results-bg);
149
+ }
150
+
151
+ .ube__section-title {
152
+ font-size: 0.8rem;
153
+ font-weight: 800;
154
+ text-transform: uppercase;
155
+ letter-spacing: 3px;
156
+ color: var(--ube-primary);
157
+ margin: 0 0 1.5rem;
158
+ }
159
+
160
+
161
+ .ube__presets {
162
+ display: grid;
163
+ grid-template-columns: repeat(5, 1fr);
164
+ gap: 0.75rem;
165
+ margin-bottom: 1.5rem;
166
+ }
167
+
168
+ .ube__preset {
169
+ background: var(--ube-preset-bg);
170
+ border: 1px solid var(--ube-preset-border);
171
+ border-radius: 1.25rem;
172
+ padding: 1.1rem 0.5rem;
173
+ cursor: pointer;
174
+ transition: all 0.22s ease;
175
+ display: flex;
176
+ flex-direction: column;
177
+ align-items: center;
178
+ gap: 0.35rem;
179
+ text-align: center;
180
+ }
181
+
182
+ .ube__preset:hover {
183
+ background: var(--ube-primary);
184
+ border-color: var(--ube-primary);
185
+ transform: translateY(-3px);
186
+ box-shadow: 0 8px 20px rgba(139, 92, 246, 0.28);
187
+ }
188
+
189
+ .ube__preset:hover .ube__preset-label,
190
+ .ube__preset:hover .ube__preset-meta,
191
+ .ube__preset:hover .ube__preset-icon {
192
+ color: #fff;
193
+ }
194
+
195
+ .ube__preset-icon {
196
+ color: var(--ube-primary);
197
+ width: 1.75rem;
198
+ height: 1.75rem;
199
+ transition: color 0.22s ease;
200
+ }
201
+
202
+ .ube__preset-icon svg {
203
+ width: 100%;
204
+ height: 100%;
205
+ display: block;
206
+ }
207
+
208
+ .ube__preset-label {
209
+ font-size: 0.78rem;
210
+ font-weight: 700;
211
+ color: var(--ube-text);
212
+ transition: color 0.22s ease;
213
+ }
214
+
215
+ .ube__preset-meta {
216
+ font-size: 0.68rem;
217
+ color: var(--ube-muted);
218
+ transition: color 0.22s ease;
219
+ }
220
+
221
+
222
+ .ube__custom {
223
+ display: grid;
224
+ grid-template-columns: 1fr 1fr auto;
225
+ gap: 1rem;
226
+ align-items: end;
227
+ }
228
+
229
+ .ube__field {
230
+ display: flex;
231
+ flex-direction: column;
232
+ gap: 0.4rem;
233
+ }
234
+
235
+ .ube__label {
236
+ font-size: 0.75rem;
237
+ font-weight: 700;
238
+ text-transform: uppercase;
239
+ letter-spacing: 1px;
240
+ color: var(--ube-muted);
241
+ }
242
+
243
+ .ube__input {
244
+ background: var(--ube-input-bg);
245
+ border: 1px solid var(--ube-input-border);
246
+ border-radius: 0.875rem;
247
+ padding: 0.75rem 1rem;
248
+ font-size: 1rem;
249
+ font-weight: 600;
250
+ color: var(--ube-text);
251
+ outline: none;
252
+ transition: all 0.2s ease;
253
+ width: 100%;
254
+ box-sizing: border-box;
255
+ }
256
+
257
+ .ube__input:focus {
258
+ border-color: var(--ube-primary);
259
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
260
+ }
261
+
262
+ .ube__btn-add {
263
+ background: linear-gradient(135deg, var(--ube-primary) 0%, var(--ube-accent) 100%);
264
+ color: #fff;
265
+ border: none;
266
+ padding: 0.75rem 1.5rem;
267
+ border-radius: 0.875rem;
268
+ font-weight: 800;
269
+ font-size: 0.85rem;
270
+ cursor: pointer;
271
+ transition: all 0.25s ease;
272
+ white-space: nowrap;
273
+ box-shadow: 0 4px 14px rgba(139, 92, 246, 0.25);
274
+ }
275
+
276
+ .ube__btn-add:hover {
277
+ transform: translateY(-2px);
278
+ box-shadow: 0 8px 24px rgba(139, 92, 246, 0.38);
279
+ filter: brightness(1.08);
280
+ }
281
+
282
+
283
+ .ube__list {
284
+ display: flex;
285
+ flex-direction: column;
286
+ gap: 0.625rem;
287
+ }
288
+
289
+ :global(.ube__drink) {
290
+ background: var(--ube-preset-bg);
291
+ border: 1px solid var(--ube-preset-border);
292
+ border-radius: 1rem;
293
+ padding: 0.875rem 1.1rem;
294
+ display: flex;
295
+ align-items: center;
296
+ gap: 0.875rem;
297
+ animation: ube-slide-in 0.28s cubic-bezier(0.23, 1, 0.32, 1);
298
+ }
299
+
300
+ :global(.ube__drink-icon) {
301
+ width: 1.6rem;
302
+ height: 1.6rem;
303
+ flex-shrink: 0;
304
+ color: var(--ube-primary);
305
+ }
306
+
307
+ :global(.ube__drink-icon) svg { width: 100%; height: 100%; display: block; }
308
+
309
+ :global(.ube__drink-info) {
310
+ flex: 1;
311
+ display: flex;
312
+ flex-direction: column;
313
+ gap: 0.15rem;
314
+ }
315
+
316
+ :global(.ube__drink-name) {
317
+ font-size: 0.9rem;
318
+ font-weight: 700;
319
+ color: var(--ube-text);
320
+ }
321
+
322
+ :global(.ube__drink-detail) {
323
+ font-size: 0.75rem;
324
+ color: var(--ube-muted);
325
+ }
326
+
327
+ :global(.ube__drink-grams) {
328
+ font-size: 0.82rem;
329
+ font-weight: 700;
330
+ color: var(--ube-primary);
331
+ flex-shrink: 0;
332
+ }
333
+
334
+ :global(.ube__drink-remove) {
335
+ background: none;
336
+ border: none;
337
+ width: 1.3rem;
338
+ height: 1.3rem;
339
+ cursor: pointer;
340
+ opacity: 0.38;
341
+ transition: opacity 0.15s ease, color 0.15s ease;
342
+ padding: 0;
343
+ flex-shrink: 0;
344
+ color: var(--ube-muted);
345
+ }
346
+
347
+ :global(.ube__drink-remove) svg { width: 100%; height: 100%; display: block; }
348
+
349
+ :global(.ube__drink-remove):hover {
350
+ opacity: 1;
351
+ color: var(--ube-danger);
352
+ }
353
+
354
+ @keyframes ube-slide-in {
355
+ from { opacity: 0; transform: translateY(-6px); }
356
+ to { opacity: 1; transform: translateY(0); }
357
+ }
358
+
359
+
360
+ .ube__empty {
361
+ text-align: center;
362
+ padding: 2.5rem 1rem;
363
+ display: flex;
364
+ flex-direction: column;
365
+ align-items: center;
366
+ gap: 0.4rem;
367
+ }
368
+
369
+ .ube__empty-icon {
370
+ width: 2.75rem;
371
+ height: 2.75rem;
372
+ opacity: 0.25;
373
+ margin-bottom: 0.4rem;
374
+ color: var(--ube-muted);
375
+ }
376
+
377
+ .ube__empty-icon svg { width: 100%; height: 100%; display: block; }
378
+
379
+ .ube__empty-title {
380
+ font-size: 0.95rem;
381
+ font-weight: 700;
382
+ color: var(--ube-text);
383
+ margin: 0;
384
+ }
385
+
386
+ .ube__empty-hint {
387
+ font-size: 0.82rem;
388
+ color: var(--ube-muted);
389
+ margin: 0;
390
+ }
391
+
392
+
393
+ .ube__totals {
394
+ display: grid;
395
+ grid-template-columns: 1fr 1fr;
396
+ gap: 1.25rem;
397
+ margin-bottom: 1.75rem;
398
+ }
399
+
400
+ .ube__stat {
401
+ background: rgba(255, 255, 255, 0.7);
402
+ border: 1px solid var(--ube-border);
403
+ border-radius: 1.25rem;
404
+ padding: 1.5rem;
405
+ text-align: center;
406
+ display: flex;
407
+ flex-direction: column;
408
+ gap: 0.4rem;
409
+ }
410
+
411
+ .ube__stat--accent {
412
+ border-color: rgba(236, 72, 153, 0.25);
413
+ }
414
+
415
+ .ube__stat-value {
416
+ font-size: 2.75rem;
417
+ font-weight: 950;
418
+ line-height: 1;
419
+ background: linear-gradient(135deg, var(--ube-primary) 0%, var(--ube-accent) 100%);
420
+ -webkit-background-clip: text;
421
+ background-clip: text;
422
+ -webkit-text-fill-color: transparent;
423
+ }
424
+
425
+ .ube__stat-label {
426
+ font-size: 0.7rem;
427
+ font-weight: 700;
428
+ text-transform: uppercase;
429
+ letter-spacing: 2px;
430
+ color: var(--ube-muted);
431
+ }
432
+
433
+ .ube__risk-header {
434
+ font-size: 0.75rem;
435
+ font-weight: 700;
436
+ text-transform: uppercase;
437
+ letter-spacing: 2px;
438
+ color: var(--ube-muted);
439
+ margin: 0 0 0.875rem;
440
+ }
441
+
442
+ .ube__risk-bar {
443
+ height: 10px;
444
+ background: rgba(0, 0, 0, 0.06);
445
+ border-radius: 8px;
446
+ overflow: hidden;
447
+ margin-bottom: 0.4rem;
448
+ }
449
+
450
+ .ube__risk-fill {
451
+ height: 100%;
452
+ width: 0%;
453
+ border-radius: 8px;
454
+ transition: width 0.55s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.4s ease;
455
+ background: linear-gradient(90deg, var(--ube-success), var(--ube-warning));
456
+ }
457
+
458
+ .ube__risk-markers {
459
+ display: flex;
460
+ justify-content: space-between;
461
+ font-size: 0.7rem;
462
+ color: var(--ube-muted);
463
+ font-weight: 600;
464
+ margin-bottom: 1.1rem;
465
+ }
466
+
467
+ .ube__risk-badge {
468
+ display: inline-block;
469
+ padding: 0.45rem 1.75rem;
470
+ border-radius: 2rem;
471
+ font-weight: 800;
472
+ font-size: 0.85rem;
473
+ text-transform: uppercase;
474
+ letter-spacing: 2px;
475
+ min-height: 2rem;
476
+ transition: all 0.3s ease;
477
+ }
478
+
479
+ :global(.ube__risk-badge--none) { background: rgba(107,114,128,0.1); color: #6b7280; border: 2px solid #6b7280; }
480
+ :global(.ube__risk-badge--low) { background: rgba(16,185,129,0.1); color: #10b981; border: 2px solid #10b981; }
481
+ :global(.ube__risk-badge--mid) { background: rgba(245,158,11,0.1); color: #f59e0b; border: 2px solid #f59e0b; }
482
+ :global(.ube__risk-badge--high) { background: rgba(239,68,68,0.1); color: #ef4444; border: 2px solid #ef4444; }
483
+
484
+
485
+ :global(.theme-dark) .ube {
486
+ --ube-bg: #1a1033;
487
+ --ube-border: rgba(139, 92, 246, 0.28);
488
+ --ube-divider: rgba(139, 92, 246, 0.12);
489
+ --ube-text: #f5f3ff;
490
+ --ube-muted: #a78bfa;
491
+ --ube-input-bg: rgba(139, 92, 246, 0.1);
492
+ --ube-input-border: rgba(139, 92, 246, 0.28);
493
+ --ube-preset-bg: rgba(139, 92, 246, 0.08);
494
+ --ube-preset-border: rgba(139, 92, 246, 0.2);
495
+ --ube-results-bg: linear-gradient(135deg, rgba(139, 92, 246, 0.08) 0%, rgba(236, 72, 153, 0.08) 100%);
496
+ }
497
+
498
+ :global(.theme-dark) .ube__stat {
499
+ background: rgba(255, 255, 255, 0.04);
500
+ }
501
+
502
+ :global(.theme-dark) .ube__risk-bar {
503
+ background: rgba(255, 255, 255, 0.06);
504
+ }
505
+
506
+ :global(.theme-dark) .ube__input {
507
+ color: var(--ube-text);
508
+ }
509
+
510
+
511
+ @media (max-width: 768px) {
512
+ .ube__section { padding: 1.75rem 1.25rem; }
513
+ .ube__presets { grid-template-columns: repeat(3, 1fr); }
514
+ .ube__custom { grid-template-columns: 1fr 1fr; }
515
+ .ube__btn-add { grid-column: 1 / -1; }
516
+ .ube__totals { grid-template-columns: 1fr; }
517
+ }
518
+
519
+ @media (max-width: 480px) {
520
+ .ube__presets { grid-template-columns: repeat(2, 1fr); }
521
+ .ube__custom { grid-template-columns: 1fr; }
522
+ }
523
+ </style>
524
+
525
+ <script>
526
+ interface UbeUI {
527
+ sectionAdd?: string; drinkCana?: string; drinkTercio?: string; drinkVino?: string;
528
+ drinkCopa?: string; drinkChupito?: string; labelVolume?: string; labelAbv?: string;
529
+ btnAddCustom?: string; customDrinkName?: string; sectionConsumed?: string;
530
+ drinkSingular?: string; drinkPlural?: string; emptyTitle?: string; emptyHint?: string;
531
+ resultLabel?: string; ubesLabel?: string; riskHeader?: string;
532
+ riskNone?: string; riskLow?: string; riskMid?: string; riskHigh?: string;
533
+ riskMarkerZero?: string; riskMarkerHigh?: string;
534
+ }
535
+
536
+ interface ConsumedDrink {
537
+ id: number; name: string; iconId: string;
538
+ vol: number; abv: number; grams: number; ubes: number;
539
+ }
540
+
541
+ interface RiskTier { label: string; cls: string; gradient: string; }
542
+
543
+ function applyUI(container: Element, ui: UbeUI): void {
544
+ container.querySelectorAll<HTMLElement>('[data-key]').forEach((el) => {
545
+ const key = el.dataset.key as keyof UbeUI;
546
+ const val = ui[key];
547
+ if (val) el.textContent = val;
548
+ });
549
+ }
550
+
551
+ function getRiskTier(ubes: number, ui: UbeUI): RiskTier {
552
+ if (ubes <= 0) return { label: ui.riskNone ?? 'Sin Consumo', cls: 'ube__risk-badge--none', gradient: 'linear-gradient(90deg,#6b7280,#9ca3af)' };
553
+ if (ubes <= 2) return { label: ui.riskLow ?? 'Bajo', cls: 'ube__risk-badge--low', gradient: 'linear-gradient(90deg,#10b981,#34d399)' };
554
+ if (ubes <= 4) return { label: ui.riskMid ?? 'Moderado', cls: 'ube__risk-badge--mid', gradient: 'linear-gradient(90deg,#f59e0b,#fbbf24)' };
555
+ return { label: ui.riskHigh ?? 'Alto', cls: 'ube__risk-badge--high', gradient: 'linear-gradient(90deg,#ef4444,#f87171)' };
556
+ }
557
+
558
+ // eslint-disable-next-line max-lines-per-function
559
+ function initUbe(container: Element): void {
560
+ const ui: UbeUI = JSON.parse((container as HTMLElement).dataset.ui ?? '{}');
561
+ applyUI(container, ui);
562
+
563
+ const listEl = container.querySelector<HTMLElement>('#ube-list');
564
+ const emptyEl = container.querySelector<HTMLElement>('#ube-empty');
565
+ const gramsEl = container.querySelector<HTMLElement>('#ube-grams');
566
+ const ubesEl = container.querySelector<HTMLElement>('#ube-ubes');
567
+ const riskFillEl = container.querySelector<HTMLElement>('#ube-risk-fill');
568
+ const riskBadgeEl = container.querySelector<HTMLElement>('#ube-risk-badge');
569
+ const volInput = container.querySelector<HTMLInputElement>('#ube-vol');
570
+ const abvInput = container.querySelector<HTMLInputElement>('#ube-abv');
571
+ const addBtn = container.querySelector<HTMLButtonElement>('#ube-add-custom');
572
+ const removeIconHtml = container.querySelector('#ube-icon-remove')?.innerHTML ?? '';
573
+
574
+ let drinks: ConsumedDrink[] = [];
575
+ let nextId = 1;
576
+
577
+ function getIconHtml(iconId: string): string {
578
+ return container.querySelector(`#ube-icon-${iconId}`)?.innerHTML ?? '';
579
+ }
580
+
581
+ function addDrink(name: string, iconId: string, vol: number, abv: number): void {
582
+ const grams = vol * (abv / 100) * 0.8;
583
+ const ubes = grams / 10;
584
+ drinks.push({ id: nextId++, name, iconId, vol, abv, grams, ubes });
585
+ renderList();
586
+ }
587
+
588
+ function removeDrink(id: number): void {
589
+ drinks = drinks.filter((d) => d.id !== id);
590
+ renderList();
591
+ }
592
+
593
+ function createDrinkCard(drink: ConsumedDrink): HTMLDivElement {
594
+ const card = document.createElement('div');
595
+ card.className = 'ube__drink';
596
+
597
+ const iconSpan = document.createElement('span');
598
+ iconSpan.className = 'ube__drink-icon';
599
+ iconSpan.innerHTML = getIconHtml(drink.iconId);
600
+ card.appendChild(iconSpan);
601
+
602
+ const infoDiv = document.createElement('div');
603
+ infoDiv.className = 'ube__drink-info';
604
+ const nameSpan = document.createElement('span');
605
+ nameSpan.className = 'ube__drink-name';
606
+ nameSpan.textContent = drink.name;
607
+ const detailSpan = document.createElement('span');
608
+ detailSpan.className = 'ube__drink-detail';
609
+ detailSpan.textContent = `${drink.vol} ml · ${drink.abv}% ABV`;
610
+ infoDiv.appendChild(nameSpan);
611
+ infoDiv.appendChild(detailSpan);
612
+ card.appendChild(infoDiv);
613
+
614
+ const gramsSpan = document.createElement('span');
615
+ gramsSpan.className = 'ube__drink-grams';
616
+ gramsSpan.textContent = `${drink.grams.toFixed(1)} g`;
617
+ card.appendChild(gramsSpan);
618
+
619
+ const removeBtn = document.createElement('button');
620
+ removeBtn.className = 'ube__drink-remove';
621
+ removeBtn.innerHTML = removeIconHtml;
622
+ removeBtn.setAttribute('aria-label', 'Remove');
623
+ removeBtn.addEventListener('click', () => removeDrink(drink.id));
624
+ card.appendChild(removeBtn);
625
+
626
+ return card;
627
+ }
628
+
629
+ function renderList(): void {
630
+ if (!listEl) return;
631
+ listEl.innerHTML = '';
632
+
633
+ if (drinks.length === 0) {
634
+ if (emptyEl) emptyEl.style.display = 'flex';
635
+ updateResults(0);
636
+ return;
637
+ }
638
+ if (emptyEl) emptyEl.style.display = 'none';
639
+
640
+ drinks.forEach(drink => listEl.appendChild(createDrinkCard(drink)));
641
+ const totalGrams = drinks.reduce((sum, d) => sum + d.grams, 0);
642
+ updateResults(totalGrams);
643
+ }
644
+
645
+ function updateResults(totalGrams: number): void {
646
+ const totalUbes = totalGrams / 10;
647
+ if (gramsEl) gramsEl.textContent = totalGrams.toFixed(1);
648
+ if (ubesEl) ubesEl.textContent = totalUbes.toFixed(2);
649
+ const pct = Math.min((totalUbes / 6) * 100, 100);
650
+ const tier = getRiskTier(totalUbes, ui);
651
+ if (riskFillEl) { riskFillEl.style.width = `${pct}%`; riskFillEl.style.background = tier.gradient; }
652
+ if (riskBadgeEl) { riskBadgeEl.textContent = tier.label; riskBadgeEl.className = `ube__risk-badge ${tier.cls}`; }
653
+ }
654
+
655
+ function handlePresetClick(btn: HTMLButtonElement) {
656
+ const vol = parseFloat(btn.dataset.vol ?? '0');
657
+ const abv = parseFloat(btn.dataset.abv ?? '0');
658
+ const iconId = btn.dataset.iconId ?? 'custom';
659
+ const name = btn.querySelector<HTMLElement>('.ube__preset-label')?.textContent ?? (ui.customDrinkName ?? 'Bebida');
660
+ addDrink(name, iconId, vol, abv);
661
+ }
662
+
663
+ // eslint-disable-next-line complexity
664
+ function handleCustomClick() {
665
+ const vol = parseFloat(volInput?.value ?? '0');
666
+ const abv = parseFloat(abvInput?.value ?? '0');
667
+ if (vol <= 0 || abv <= 0) return;
668
+ addDrink(ui.customDrinkName ?? 'Personalizada', 'custom', vol, abv);
669
+ if (volInput) volInput.value = '';
670
+ if (abvInput) abvInput.value = '';
671
+ }
672
+
673
+ container.querySelectorAll<HTMLButtonElement>('.ube__preset').forEach((btn) => {
674
+ btn.addEventListener('click', () => handlePresetClick(btn));
675
+ });
676
+
677
+ addBtn?.addEventListener('click', handleCustomClick);
678
+
679
+ renderList();
680
+ }
681
+
682
+ document.querySelectorAll<HTMLElement>('.ube').forEach(initUbe);
683
+ </script>