@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,694 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { HydrationCalculatorUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: string;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const ui = (Astro.props.ui ?? {}) as HydrationCalculatorUI;
11
+ ---
12
+
13
+ <div class="hyd" data-ui={JSON.stringify(ui)}>
14
+ <div class="hyd__card">
15
+
16
+
17
+ <div class="hyd__section hyd__section--main">
18
+ <div class="hyd__main-grid">
19
+
20
+
21
+ <div class="hyd__controls">
22
+
23
+
24
+ <div class="hyd__group">
25
+ <h4 class="hyd__group-title">
26
+ <span class="hyd__group-icon"><Icon name="mdi:weight-kilogram" /></span>
27
+ <span data-key="sectionWeight">{ui.sectionWeight}</span>
28
+ </h4>
29
+ <div class="hyd__field">
30
+ <label class="hyd__label" data-key="labelWeight">{ui.labelWeight}</label>
31
+ <input type="number" id="hyd-weight" class="hyd__input" value="70" min="20" max="250" />
32
+ </div>
33
+ </div>
34
+
35
+
36
+ <div class="hyd__group">
37
+ <div class="hyd__group-header">
38
+ <h4 class="hyd__group-title">
39
+ <span class="hyd__group-icon"><Icon name="mdi:thermometer" /></span>
40
+ <span data-key="sectionClimate">{ui.sectionClimate}</span>
41
+ </h4>
42
+ <button id="hyd-weather" class="hyd__weather-btn" title={ui.weatherBtnTitle ?? ''}>
43
+ <div class="hyd__spinner"></div>
44
+ <span class="hyd__weather-icon"><Icon name="mdi:map-marker" /></span>
45
+ </button>
46
+ </div>
47
+ <div class="hyd__field-row">
48
+ <div class="hyd__field">
49
+ <label class="hyd__label" data-key="labelTemp">{ui.labelTemp}</label>
50
+ <input type="number" id="hyd-temp" class="hyd__input" value="22" min="-10" max="60" />
51
+ </div>
52
+ <div class="hyd__field">
53
+ <label class="hyd__label" data-key="labelHum">{ui.labelHum}</label>
54
+ <input type="number" id="hyd-hum" class="hyd__input" value="50" min="0" max="100" />
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+
60
+ <div class="hyd__group">
61
+ <h4 class="hyd__group-title">
62
+ <span class="hyd__group-icon"><Icon name="mdi:run-fast" /></span>
63
+ <span data-key="sectionActivity">{ui.sectionActivity}</span>
64
+ </h4>
65
+ <div class="hyd__intensity-grid">
66
+ <button class="hyd__intensity-btn hyd__intensity-btn--active" data-loss="0">
67
+ <span data-key="intensitySedentary">{ui.intensitySedentary}</span>
68
+ </button>
69
+ <button class="hyd__intensity-btn" data-loss="500">
70
+ <span data-key="intensityActive">{ui.intensityActive}</span>
71
+ </button>
72
+ <button class="hyd__intensity-btn" data-loss="1000">
73
+ <span data-key="intensityAthlete">{ui.intensityAthlete}</span>
74
+ </button>
75
+ </div>
76
+ <div class="hyd__field">
77
+ <label class="hyd__label" data-key="labelDuration">{ui.labelDuration}</label>
78
+ <input type="number" id="hyd-duration" class="hyd__input" value="0" min="0" max="1440" />
79
+ </div>
80
+ </div>
81
+
82
+ </div>
83
+
84
+
85
+ <div class="hyd__visual">
86
+ <div class="hyd__bottle">
87
+ <div class="hyd__bottle-fill" id="hyd-fill">
88
+ <div class="hyd__wave"></div>
89
+ </div>
90
+ </div>
91
+ <div class="hyd__total">
92
+ <span class="hyd__total-value" id="hyd-liters">2.50</span>
93
+ <span class="hyd__total-unit" data-key="resultUnit">{ui.resultUnit}</span>
94
+ </div>
95
+ </div>
96
+
97
+ </div>
98
+ </div>
99
+
100
+
101
+ <div class="hyd__section hyd__section--bordered hyd__section--stats">
102
+ <div class="hyd__stats-grid">
103
+
104
+ <div class="hyd__stat">
105
+ <span class="hyd__stat-label" data-key="statRefill">{ui.statRefill}</span>
106
+ <span class="hyd__stat-value" id="hyd-refill">—</span>
107
+ <p class="hyd__stat-help" data-key="statRefillHelp">{ui.statRefillHelp}</p>
108
+ </div>
109
+
110
+ <div class="hyd__stat">
111
+ <span class="hyd__stat-label" data-key="statMix">{ui.statMix}</span>
112
+ <span class="hyd__stat-value" id="hyd-mix">—</span>
113
+ <p class="hyd__stat-help" id="hyd-mix-detail"></p>
114
+ </div>
115
+
116
+ <div class="hyd__stat">
117
+ <span class="hyd__stat-label" data-key="statUrine">{ui.statUrine}</span>
118
+ <div class="hyd__urine-track">
119
+ <div class="hyd__urine-seg hyd__urine-seg--1"></div>
120
+ <div class="hyd__urine-seg hyd__urine-seg--2"></div>
121
+ <div class="hyd__urine-seg hyd__urine-seg--3"></div>
122
+ <div class="hyd__urine-seg hyd__urine-seg--4"></div>
123
+ <div class="hyd__urine-seg hyd__urine-seg--5"></div>
124
+ <div class="hyd__urine-seg hyd__urine-seg--6"></div>
125
+ <div class="hyd__urine-seg hyd__urine-seg--7"></div>
126
+ </div>
127
+ <div class="hyd__urine-icons">
128
+ <span class="hyd__urine-good"><Icon name="mdi:check-circle" /></span>
129
+ <span class="hyd__urine-bad"><Icon name="mdi:close-circle" /></span>
130
+ </div>
131
+ </div>
132
+
133
+ </div>
134
+ </div>
135
+
136
+
137
+ <div class="hyd__section hyd__section--bordered hyd__section--alert" id="hyd-alert" hidden>
138
+ <div class="hyd__alert">
139
+ <span class="hyd__alert-icon"><Icon name="mdi:alert-circle" /></span>
140
+ <span class="hyd__alert-text" data-key="alertElectrolytes">{ui.alertElectrolytes}</span>
141
+ </div>
142
+ </div>
143
+
144
+ </div>
145
+ </div>
146
+
147
+ <style>
148
+ .hyd {
149
+ --hyd-primary: #0ea5e9;
150
+ --hyd-primary-dark: #0284c7;
151
+ --hyd-primary-light: #e0f2fe;
152
+ --hyd-bg: #fff;
153
+ --hyd-border: rgba(14, 165, 233, 0.15);
154
+ --hyd-divider: rgba(14, 165, 233, 0.1);
155
+ --hyd-surface: #f8fafc;
156
+ --hyd-text: #1e293b;
157
+ --hyd-text-muted: #64748b;
158
+ --hyd-input-bg: #f8fafc;
159
+ --hyd-input-border: #e2e8f0;
160
+ --hyd-visual-bg: #f0f9ff;
161
+ --hyd-visual-border: #e0f2fe;
162
+ --hyd-alert-bg: #fff1f2;
163
+ --hyd-alert-color: #e11d48;
164
+ --hyd-alert-border: #ffe4e6;
165
+
166
+ width: 100%;
167
+ max-width: 720px;
168
+ margin: 0 auto;
169
+ }
170
+
171
+ :global(.theme-dark) .hyd {
172
+ --hyd-bg: #0f172a;
173
+ --hyd-border: rgba(14, 165, 233, 0.2);
174
+ --hyd-divider: rgba(14, 165, 233, 0.12);
175
+ --hyd-surface: #1e293b;
176
+ --hyd-text: #f1f5f9;
177
+ --hyd-text-muted: #94a3b8;
178
+ --hyd-input-bg: #1e293b;
179
+ --hyd-input-border: #334155;
180
+ --hyd-visual-bg: rgba(14, 165, 233, 0.06);
181
+ --hyd-visual-border: rgba(14, 165, 233, 0.15);
182
+ --hyd-alert-bg: rgba(225, 29, 72, 0.12);
183
+ --hyd-alert-color: #fb7185;
184
+ --hyd-alert-border: rgba(225, 29, 72, 0.2);
185
+ }
186
+
187
+ .hyd__card {
188
+ background: var(--hyd-bg);
189
+ border: 2px solid var(--hyd-border);
190
+ border-radius: 2rem;
191
+ box-shadow: 0 20px 60px rgba(14, 165, 233, 0.08);
192
+ overflow: hidden;
193
+ }
194
+
195
+ .hyd__section {
196
+ padding: 2rem;
197
+ }
198
+
199
+ .hyd__section--bordered {
200
+ border-top: 1px solid var(--hyd-divider);
201
+ }
202
+
203
+
204
+ .hyd__main-grid {
205
+ display: grid;
206
+ grid-template-columns: 1fr;
207
+ gap: 1.5rem;
208
+ }
209
+
210
+ @media (min-width: 560px) {
211
+ .hyd__main-grid {
212
+ grid-template-columns: 1fr auto;
213
+ gap: 2rem;
214
+ align-items: start;
215
+ }
216
+ }
217
+
218
+
219
+ .hyd__controls {
220
+ display: flex;
221
+ flex-direction: column;
222
+ gap: 1.5rem;
223
+ }
224
+
225
+ .hyd__group {
226
+ display: flex;
227
+ flex-direction: column;
228
+ gap: 0.75rem;
229
+ }
230
+
231
+ .hyd__group-header {
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: space-between;
235
+ }
236
+
237
+ .hyd__group-title {
238
+ margin: 0;
239
+ font-size: 0.7rem;
240
+ font-weight: 800;
241
+ text-transform: uppercase;
242
+ letter-spacing: 0.08em;
243
+ color: var(--hyd-text-muted);
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 0.4rem;
247
+ }
248
+
249
+ .hyd__group-icon {
250
+ width: 1rem;
251
+ height: 1rem;
252
+ display: flex;
253
+ align-items: center;
254
+ }
255
+
256
+ .hyd__group-icon svg {
257
+ width: 100%;
258
+ height: 100%;
259
+ }
260
+
261
+ .hyd__weather-btn {
262
+ background: var(--hyd-primary-light);
263
+ color: var(--hyd-primary);
264
+ border: none;
265
+ width: 2rem;
266
+ height: 2rem;
267
+ border-radius: 0.5rem;
268
+ cursor: pointer;
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ transition: background 0.2s, color 0.2s;
273
+ flex-shrink: 0;
274
+ }
275
+
276
+ :global(.theme-dark) .hyd__weather-btn {
277
+ background: rgba(14, 165, 233, 0.15);
278
+ }
279
+
280
+ .hyd__weather-btn:hover {
281
+ background: var(--hyd-primary);
282
+ color: #fff;
283
+ }
284
+
285
+ .hyd__weather-icon svg,
286
+ .hyd__weather-btn svg {
287
+ width: 1rem;
288
+ height: 1rem;
289
+ }
290
+
291
+ .hyd__spinner {
292
+ width: 1rem;
293
+ height: 1rem;
294
+ border: 2px solid rgba(14, 165, 233, 0.25);
295
+ border-top-color: var(--hyd-primary);
296
+ border-radius: 50%;
297
+ animation: hyd-spin 0.8s linear infinite;
298
+ display: none;
299
+ }
300
+
301
+ :global(.hyd__weather-btn[data-loading="true"]) .hyd__spinner { display: block; }
302
+ :global(.hyd__weather-btn[data-loading="true"]) .hyd__weather-icon { display: none; }
303
+
304
+ @keyframes hyd-spin {
305
+ to { transform: rotate(360deg); }
306
+ }
307
+
308
+ .hyd__field-row {
309
+ display: grid;
310
+ grid-template-columns: 1fr 1fr;
311
+ gap: 0.75rem;
312
+ }
313
+
314
+ .hyd__field {
315
+ display: flex;
316
+ flex-direction: column;
317
+ gap: 0.35rem;
318
+ }
319
+
320
+ .hyd__label {
321
+ font-size: 0.7rem;
322
+ font-weight: 600;
323
+ color: var(--hyd-text-muted);
324
+ }
325
+
326
+ .hyd__input {
327
+ background: var(--hyd-input-bg);
328
+ border: 1.5px solid var(--hyd-input-border);
329
+ border-radius: 0.75rem;
330
+ padding: 0.65rem 0.75rem;
331
+ font-size: 1rem;
332
+ font-weight: 700;
333
+ color: var(--hyd-text);
334
+ width: 100%;
335
+ box-sizing: border-box;
336
+ outline: none;
337
+ transition: border-color 0.2s, box-shadow 0.2s;
338
+ }
339
+
340
+ .hyd__input:focus {
341
+ border-color: var(--hyd-primary);
342
+ box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.12);
343
+ }
344
+
345
+
346
+ .hyd__intensity-grid {
347
+ display: grid;
348
+ grid-template-columns: repeat(3, 1fr);
349
+ gap: 0.5rem;
350
+ }
351
+
352
+ .hyd__intensity-btn {
353
+ background: var(--hyd-input-bg);
354
+ border: 1.5px solid var(--hyd-input-border);
355
+ border-radius: 0.75rem;
356
+ padding: 0.6rem 0.25rem;
357
+ font-size: 0.75rem;
358
+ font-weight: 700;
359
+ color: var(--hyd-text-muted);
360
+ cursor: pointer;
361
+ transition: all 0.2s;
362
+ }
363
+
364
+ .hyd__intensity-btn:hover {
365
+ border-color: var(--hyd-primary);
366
+ color: var(--hyd-primary);
367
+ }
368
+
369
+ .hyd__intensity-btn--active {
370
+ background: var(--hyd-primary);
371
+ color: #fff;
372
+ border-color: var(--hyd-primary);
373
+ box-shadow: 0 4px 12px rgba(14, 165, 233, 0.25);
374
+ }
375
+
376
+
377
+ .hyd__visual {
378
+ background: var(--hyd-visual-bg);
379
+ border: 1.5px solid var(--hyd-visual-border);
380
+ border-radius: 1.5rem;
381
+ padding: 2rem 1.5rem;
382
+ display: flex;
383
+ flex-direction: column;
384
+ align-items: center;
385
+ gap: 1.5rem;
386
+ }
387
+
388
+ @media (min-width: 560px) {
389
+ .hyd__visual {
390
+ min-width: 160px;
391
+ }
392
+ }
393
+
394
+ .hyd__bottle {
395
+ width: 80px;
396
+ height: 200px;
397
+ background: #fff;
398
+ border: 6px solid #fff;
399
+ border-radius: 20px 20px 40px 40px;
400
+ overflow: hidden;
401
+ position: relative;
402
+ box-shadow: 0 8px 24px rgba(14, 165, 233, 0.1), inset 0 0 12px rgba(0,0,0,0.02);
403
+ }
404
+
405
+ :global(.theme-dark) .hyd__bottle {
406
+ background: #1e293b;
407
+ border-color: #334155;
408
+ }
409
+
410
+ .hyd__bottle-fill {
411
+ position: absolute;
412
+ bottom: 0;
413
+ width: 100%;
414
+ background: linear-gradient(to top, var(--hyd-primary), #38bdf8);
415
+ transition: height 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
416
+ height: 42%;
417
+ }
418
+
419
+ .hyd__wave {
420
+ position: absolute;
421
+ width: 300%;
422
+ height: 300%;
423
+ background: rgba(255,255,255,0.3);
424
+ top: -285%;
425
+ left: -100%;
426
+ border-radius: 42%;
427
+ animation: hyd-wave 10s linear infinite;
428
+ pointer-events: none;
429
+ }
430
+
431
+ .hyd__wave::before {
432
+ content: '';
433
+ position: absolute;
434
+ width: 100%;
435
+ height: 100%;
436
+ background: rgba(255,255,255,0.2);
437
+ border-radius: 40%;
438
+ animation: hyd-wave 14s linear infinite reverse;
439
+ }
440
+
441
+ @keyframes hyd-wave {
442
+ from { transform: rotate(0deg); }
443
+ to { transform: rotate(360deg); }
444
+ }
445
+
446
+ .hyd__total {
447
+ text-align: center;
448
+ }
449
+
450
+ .hyd__total-value {
451
+ display: block;
452
+ font-size: 3.5rem;
453
+ font-weight: 900;
454
+ line-height: 1;
455
+ color: var(--hyd-primary);
456
+ }
457
+
458
+ .hyd__total-unit {
459
+ display: block;
460
+ font-size: 0.65rem;
461
+ font-weight: 800;
462
+ text-transform: uppercase;
463
+ letter-spacing: 0.1em;
464
+ color: var(--hyd-text-muted);
465
+ margin-top: 0.25rem;
466
+ }
467
+
468
+
469
+ .hyd__stats-grid {
470
+ display: grid;
471
+ grid-template-columns: 1fr;
472
+ gap: 1rem;
473
+ }
474
+
475
+ @media (min-width: 560px) {
476
+ .hyd__stats-grid {
477
+ grid-template-columns: repeat(3, 1fr);
478
+ }
479
+ }
480
+
481
+ .hyd__stat {
482
+ background: var(--hyd-surface);
483
+ border: 1.5px solid var(--hyd-divider);
484
+ border-radius: 1rem;
485
+ padding: 1rem;
486
+ display: flex;
487
+ flex-direction: column;
488
+ }
489
+
490
+ .hyd__stat-label {
491
+ font-size: 0.65rem;
492
+ font-weight: 800;
493
+ text-transform: uppercase;
494
+ letter-spacing: 0.06em;
495
+ color: var(--hyd-text-muted);
496
+ margin-bottom: 0.4rem;
497
+ }
498
+
499
+ .hyd__stat-value {
500
+ font-size: 0.95rem;
501
+ font-weight: 800;
502
+ color: var(--hyd-text);
503
+ }
504
+
505
+ .hyd__stat-help {
506
+ font-size: 0.7rem;
507
+ color: var(--hyd-text-muted);
508
+ margin: 0.4rem 0 0;
509
+ line-height: 1.4;
510
+ }
511
+
512
+
513
+ .hyd__urine-track {
514
+ display: flex;
515
+ height: 10px;
516
+ border-radius: 5px;
517
+ overflow: hidden;
518
+ margin-top: 0.5rem;
519
+ border: 2px solid var(--hyd-bg);
520
+ }
521
+
522
+ .hyd__urine-seg { flex: 1; }
523
+ .hyd__urine-seg--1 { background: #fef9c3; }
524
+ .hyd__urine-seg--2 { background: #fef08a; }
525
+ .hyd__urine-seg--3 { background: #fde047; }
526
+ .hyd__urine-seg--4 { background: #facc15; }
527
+ .hyd__urine-seg--5 { background: #eab308; }
528
+ .hyd__urine-seg--6 { background: #ca8a04; }
529
+ .hyd__urine-seg--7 { background: #a16207; }
530
+
531
+ .hyd__urine-icons {
532
+ display: flex;
533
+ justify-content: space-between;
534
+ margin-top: 0.35rem;
535
+ }
536
+
537
+ .hyd__urine-good svg { color: #22c55e; width: 1rem; height: 1rem; }
538
+ .hyd__urine-bad svg { color: #ef4444; width: 1rem; height: 1rem; }
539
+
540
+
541
+ .hyd__section--alert {
542
+ padding-top: 1.25rem;
543
+ padding-bottom: 1.25rem;
544
+ }
545
+
546
+ .hyd__alert {
547
+ background: var(--hyd-alert-bg);
548
+ border: 1.5px solid var(--hyd-alert-border);
549
+ border-radius: 0.875rem;
550
+ padding: 0.875rem 1rem;
551
+ display: flex;
552
+ align-items: center;
553
+ gap: 0.75rem;
554
+ color: var(--hyd-alert-color);
555
+ }
556
+
557
+ .hyd__alert-icon {
558
+ flex-shrink: 0;
559
+ width: 1.25rem;
560
+ height: 1.25rem;
561
+ }
562
+
563
+ .hyd__alert-icon svg {
564
+ width: 100%;
565
+ height: 100%;
566
+ }
567
+
568
+ .hyd__alert-text {
569
+ font-size: 0.85rem;
570
+ font-weight: 600;
571
+ line-height: 1.4;
572
+ }
573
+ </style>
574
+
575
+ <script>
576
+ // eslint-disable-next-line max-lines-per-function
577
+ function initHyd(container: Element) {
578
+ const ui = JSON.parse((container as HTMLElement).dataset.ui ?? '{}') as Record<string, string>;
579
+
580
+ const weightEl = container.querySelector<HTMLInputElement>('#hyd-weight');
581
+ const tempEl = container.querySelector<HTMLInputElement>('#hyd-temp');
582
+ const humEl = container.querySelector<HTMLInputElement>('#hyd-hum');
583
+ const durationEl = container.querySelector<HTMLInputElement>('#hyd-duration');
584
+ const intensityBtns = container.querySelectorAll<HTMLButtonElement>('.hyd__intensity-btn');
585
+ const litersEl = container.querySelector<HTMLElement>('#hyd-liters');
586
+ const fillEl = container.querySelector<HTMLElement>('#hyd-fill');
587
+ const refillEl = container.querySelector<HTMLElement>('#hyd-refill');
588
+ const mixEl = container.querySelector<HTMLElement>('#hyd-mix');
589
+ const mixDetailEl = container.querySelector<HTMLElement>('#hyd-mix-detail');
590
+ const alertEl = container.querySelector<HTMLElement>('#hyd-alert');
591
+ const weatherBtn = container.querySelector<HTMLButtonElement>('#hyd-weather');
592
+
593
+ let intensityLoss = 0;
594
+
595
+ function calcBaseHydration(weight: number, temp: number, humidity: number): number {
596
+ let base = weight * 35;
597
+ if (temp > 30) base *= 1 + (temp - 30) * 0.1;
598
+ if (humidity > 60 && temp > 25) base *= 1.05;
599
+ return base;
600
+ }
601
+
602
+ function getRefillFreq(totalL: number, duration: number): string {
603
+ if (totalL > 3.5) return ui.freqHigh ?? '—';
604
+ if (duration > 0) return ui.freqMed ?? '—';
605
+ return ui.freqLow ?? '—';
606
+ }
607
+
608
+ // eslint-disable-next-line complexity
609
+ function getDrinkMix(totalL: number, temp: number): { mix: string; detail: string } {
610
+ const mixes: Record<string, { mix: string; detail: string }> = {
611
+ high: { mix: ui.mixWaterElectrolytes ?? '—', detail: ui.mixDetailHigh ?? '' },
612
+ med: { mix: ui.mixWaterSalt ?? '—', detail: ui.mixDetailMed ?? '' },
613
+ low: { mix: ui.mixWater ?? '—', detail: ui.mixDetailLow ?? '' },
614
+ };
615
+ let level = 'low';
616
+ if (totalL > 4 || temp > 38) level = 'high';
617
+ else if (totalL > 3 || temp > 30) level = 'med';
618
+ return mixes[level];
619
+ }
620
+
621
+ // eslint-disable-next-line complexity
622
+ function calculate() {
623
+ const weight = parseFloat(weightEl?.value ?? '70') || 70;
624
+ const temp = parseFloat(tempEl?.value ?? '22') || 22;
625
+ const humidity = parseFloat(humEl?.value ?? '50') || 50;
626
+ const duration = parseFloat(durationEl?.value ?? '0') || 0;
627
+
628
+ const base = calcBaseHydration(weight, temp, humidity);
629
+ const activityLoss = intensityLoss * (duration / 60);
630
+ const totalMl = base + activityLoss;
631
+ const totalL = totalMl / 1000;
632
+
633
+ if (litersEl) litersEl.textContent = totalL.toFixed(2);
634
+ const fillPct = Math.min(100, (totalL / 6) * 100);
635
+ if (fillEl) fillEl.style.height = `${fillPct}%`;
636
+
637
+ const freq = getRefillFreq(totalL, duration);
638
+ const { mix, detail } = getDrinkMix(totalL, temp);
639
+
640
+ if (refillEl) refillEl.textContent = freq;
641
+ if (mixEl) mixEl.textContent = mix;
642
+ if (mixDetailEl) mixDetailEl.textContent = detail;
643
+ if (alertEl) alertEl.hidden = !(totalL > 3.5 || temp > 38);
644
+ }
645
+
646
+ weightEl?.addEventListener('input', calculate);
647
+ tempEl?.addEventListener('input', calculate);
648
+ humEl?.addEventListener('input', calculate);
649
+ durationEl?.addEventListener('input', calculate);
650
+
651
+ intensityBtns.forEach((btn) => {
652
+ btn.addEventListener('click', () => {
653
+ intensityBtns.forEach((b) => b.classList.remove('hyd__intensity-btn--active'));
654
+ btn.classList.add('hyd__intensity-btn--active');
655
+ intensityLoss = parseInt(btn.dataset.loss ?? '0');
656
+ calculate();
657
+ });
658
+ });
659
+
660
+ weatherBtn?.addEventListener('click', async () => {
661
+ weatherBtn.setAttribute('data-loading', 'true');
662
+ weatherBtn.disabled = true;
663
+
664
+ try {
665
+ const pos = await new Promise<GeolocationPosition>((resolve, reject) => {
666
+ navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: 8000 });
667
+ });
668
+
669
+ const { latitude, longitude } = pos.coords;
670
+ const res = await fetch(
671
+ `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,relative_humidity_2m`,
672
+ );
673
+ const data = await res.json() as { current?: { temperature_2m: number; relative_humidity_2m: number } };
674
+
675
+ if (data.current) {
676
+ const t = Math.round(data.current.temperature_2m);
677
+ const h = Math.round(data.current.relative_humidity_2m);
678
+ if (tempEl) tempEl.value = String(t);
679
+ if (humEl) humEl.value = String(h);
680
+ calculate();
681
+ }
682
+ } catch {
683
+
684
+ } finally {
685
+ weatherBtn.removeAttribute('data-loading');
686
+ weatherBtn.disabled = false;
687
+ }
688
+ });
689
+
690
+ calculate();
691
+ }
692
+
693
+ document.querySelectorAll('.hyd').forEach(initHyd);
694
+ </script>