@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,636 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { KnownLocale } from '../../types';
4
+ import type { BreathingVisualizerUI } from './ui';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ ui?: Record<string, unknown>;
9
+ }
10
+
11
+ const t = (Astro.props.ui ?? {}) as BreathingVisualizerUI;
12
+ ---
13
+
14
+ <section class="bv">
15
+ <div class="bv__container" id="bv-container" data-ui={JSON.stringify(t)}>
16
+ <div class="bv__gradient"></div>
17
+ <div id="bv-glow" class="bv__glow"></div>
18
+
19
+ <div class="bv__visual">
20
+ <div id="bv-round-indicator" class="bv__round-indicator bv__hidden">
21
+ {t.roundPrefix} <span id="bv-round">1</span>
22
+ </div>
23
+ <div class="bv__orb-area">
24
+ <div id="bv-ring-1" class="bv__ring"></div>
25
+ <div id="bv-ring-2" class="bv__ring bv__ring--delay"></div>
26
+ <div id="bv-orb" class="bv__orb">
27
+ <div class="bv__orb-inner"></div>
28
+ <div id="bv-orb-text" class="bv__orb-text">{t.orbIdle}</div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="bv__info">
34
+ <div id="bv-phase-label" class="bv__phase-label">{t.idlePhase}</div>
35
+ <div id="bv-timer" class="bv__timer">00</div>
36
+ <div id="bv-breath-count" class="bv__breath-count bv__hidden">
37
+ {t.breathPrefix} <span id="bv-breath">1</span> / 30
38
+ </div>
39
+ </div>
40
+
41
+ <div class="bv__controls">
42
+ <div class="bv__tabs">
43
+ <button id="bv-btn-box" class="bv__tab bv__tab--active">{t.btnBox}</button>
44
+ <button id="bv-btn-relax" class="bv__tab">{t.btnRelax}</button>
45
+ <button id="bv-btn-wimhof" class="bv__tab">{t.btnWimhof}</button>
46
+ </div>
47
+ <div class="bv__actions">
48
+ <button id="bv-start" class="bv__start">
49
+ <span class="bv__start-content">
50
+ <span id="bv-icon-play"><Icon name="mdi:play" class="bv__icon" /></span>
51
+ <span id="bv-icon-stop" class="bv__hidden"><Icon name="mdi:stop" class="bv__icon" /></span>
52
+ <span id="bv-start-text">{t.startLabel}</span>
53
+ </span>
54
+ </button>
55
+ <div class="bv__toggles">
56
+ <button id="bv-loop" class="bv__toggle" title={t.loopTitle}>
57
+ <Icon name="mdi:repeat" class="bv__icon" />
58
+ </button>
59
+ <button id="bv-vib" class="bv__toggle bv__toggle--vib-active" title={t.vibTitle}>
60
+ <Icon name="mdi:vibrate" class="bv__icon" />
61
+ </button>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </section>
67
+
68
+ <style>
69
+ @keyframes bv-pulse-ring {
70
+ 0% { transform: scale(0.8); opacity: 0.5; }
71
+ 100% { transform: scale(2); opacity: 0; }
72
+ }
73
+
74
+ @keyframes bv-pulse {
75
+ 0%, 100% { opacity: 1; }
76
+ 50% { opacity: 0.5; }
77
+ }
78
+
79
+ .bv {
80
+ --bv-bg: #fff;
81
+ --bv-border: #e2e8f0;
82
+ --bv-phase: #94a3b8;
83
+ --bv-timer: #0f172a;
84
+ --bv-round-text: #059669;
85
+ --bv-round-bg: rgba(5, 150, 105, 0.1);
86
+ --bv-round-border: rgba(5, 150, 105, 0.2);
87
+ --bv-tab-bg: #f1f5f9;
88
+ --bv-tab-border: #e2e8f0;
89
+ --bv-btn-active-bg: #fff;
90
+ --bv-btn-active-text: #0f172a;
91
+ --bv-btn-text: #64748b;
92
+ --bv-start-bg: #0f172a;
93
+ --bv-start-text: #fff;
94
+ --bv-toggle-bg: #f1f5f9;
95
+ --bv-toggle-border: #e2e8f0;
96
+ --bv-toggle-text: #94a3b8;
97
+
98
+ max-width: 56rem;
99
+ margin: 0 auto;
100
+ padding: 3rem 1rem;
101
+ }
102
+
103
+ :global(.theme-dark) .bv {
104
+ --bv-bg: #09090b;
105
+ --bv-border: #27272a;
106
+ --bv-phase: #71717a;
107
+ --bv-timer: #fff;
108
+ --bv-round-text: #34d399;
109
+ --bv-round-bg: rgba(52, 211, 153, 0.1);
110
+ --bv-round-border: rgba(52, 211, 153, 0.2);
111
+ --bv-tab-bg: #18181b;
112
+ --bv-tab-border: #27272a;
113
+ --bv-btn-active-bg: #27272a;
114
+ --bv-btn-active-text: #fff;
115
+ --bv-btn-text: #71717a;
116
+ --bv-start-bg: #fff;
117
+ --bv-start-text: #0f172a;
118
+ --bv-toggle-bg: #18181b;
119
+ --bv-toggle-border: #27272a;
120
+ --bv-toggle-text: #94a3b8;
121
+ }
122
+
123
+ .bv__container {
124
+ background: var(--bv-bg);
125
+ border: 1px solid var(--bv-border);
126
+ border-radius: 3rem;
127
+ padding: 4rem 2rem;
128
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
129
+ display: flex;
130
+ flex-direction: column;
131
+ align-items: center;
132
+ min-height: 650px;
133
+ position: relative;
134
+ overflow: hidden;
135
+ gap: 3rem;
136
+ transition: all 0.5s;
137
+ }
138
+
139
+ .bv__gradient {
140
+ position: absolute;
141
+ inset: 0;
142
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, transparent 50%, rgba(16, 185, 129, 0.05) 100%);
143
+ pointer-events: none;
144
+ }
145
+
146
+ .bv__glow {
147
+ position: absolute;
148
+ inset: 0;
149
+ background-color: transparent;
150
+ transition: background-color 1s;
151
+ filter: blur(120px);
152
+ pointer-events: none;
153
+ }
154
+
155
+ .bv__visual {
156
+ position: relative;
157
+ z-index: 10;
158
+ display: flex;
159
+ flex-direction: column;
160
+ align-items: center;
161
+ gap: 2rem;
162
+ }
163
+
164
+ .bv__round-indicator {
165
+ padding: 0.25rem 1rem;
166
+ border-radius: 9999px;
167
+ background: var(--bv-round-bg);
168
+ border: 1px solid var(--bv-round-border);
169
+ color: var(--bv-round-text);
170
+ font-size: 0.75rem;
171
+ font-weight: 700;
172
+ text-transform: uppercase;
173
+ letter-spacing: 0.1em;
174
+ }
175
+
176
+ .bv__orb-area {
177
+ position: relative;
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ width: 16rem;
182
+ height: 16rem;
183
+ }
184
+
185
+ .bv__ring {
186
+ position: absolute;
187
+ width: 16rem;
188
+ height: 16rem;
189
+ border: 2px solid rgba(59, 130, 246, 0.2);
190
+ border-radius: 50%;
191
+ }
192
+
193
+ .bv__ring--delay { border-color: rgba(16, 185, 129, 0.1); animation-delay: 75ms; }
194
+ .bv__ring--active { animation: bv-pulse-ring 4s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
195
+
196
+ .bv__orb {
197
+ width: 12rem;
198
+ height: 12rem;
199
+ border-radius: 50%;
200
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.8) 0%, rgba(16, 185, 129, 0.8) 100%);
201
+ box-shadow: 0 0 50px rgba(59, 130, 246, 0.3);
202
+ backdrop-filter: blur(4px);
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: center;
206
+ position: relative;
207
+ transition-property: transform;
208
+ transition-timing-function: linear;
209
+ transition-duration: 4s;
210
+ }
211
+
212
+ .bv__orb-inner {
213
+ position: absolute;
214
+ inset: 0.5rem;
215
+ border: 2px solid rgba(255, 255, 255, 0.2);
216
+ border-radius: 50%;
217
+ }
218
+
219
+ .bv__orb-text {
220
+ color: #fff;
221
+ font-weight: 700;
222
+ font-size: 1.25rem;
223
+ text-transform: uppercase;
224
+ letter-spacing: 0.1em;
225
+ text-align: center;
226
+ padding: 0 1rem;
227
+ user-select: none;
228
+ animation: bv-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
229
+ }
230
+
231
+ .bv__info {
232
+ text-align: center;
233
+ z-index: 10;
234
+ display: flex;
235
+ flex-direction: column;
236
+ align-items: center;
237
+ gap: 0.5rem;
238
+ }
239
+
240
+ .bv__phase-label {
241
+ color: var(--bv-phase);
242
+ font-size: 0.75rem;
243
+ text-transform: uppercase;
244
+ letter-spacing: 0.3em;
245
+ }
246
+
247
+ .bv__timer {
248
+ font-size: 3rem;
249
+ font-weight: 900;
250
+ color: var(--bv-timer);
251
+ font-variant-numeric: tabular-nums;
252
+ line-height: 1;
253
+ }
254
+
255
+ .bv__breath-count {
256
+ font-size: 0.75rem;
257
+ font-weight: 700;
258
+ color: #3b82f6;
259
+ text-transform: uppercase;
260
+ letter-spacing: 0.1em;
261
+ }
262
+
263
+ .bv__controls {
264
+ z-index: 10;
265
+ width: 100%;
266
+ display: flex;
267
+ flex-direction: column;
268
+ align-items: center;
269
+ gap: 1.5rem;
270
+ }
271
+
272
+ .bv__tabs {
273
+ display: flex;
274
+ flex-wrap: wrap;
275
+ justify-content: center;
276
+ gap: 0.25rem;
277
+ padding: 0.5rem;
278
+ background: var(--bv-tab-bg);
279
+ border: 1px solid var(--bv-tab-border);
280
+ border-radius: 1.5rem;
281
+ width: 100%;
282
+ max-width: 36rem;
283
+ }
284
+
285
+ .bv__tab {
286
+ flex: 1;
287
+ padding: 0.75rem 1rem;
288
+ border-radius: 1rem;
289
+ font-size: 0.75rem;
290
+ font-weight: 700;
291
+ transition: all 0.2s;
292
+ cursor: pointer;
293
+ color: var(--bv-btn-text);
294
+ background: transparent;
295
+ border: none;
296
+ }
297
+
298
+ .bv__tab--active {
299
+ background: var(--bv-btn-active-bg);
300
+ color: var(--bv-btn-active-text);
301
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
302
+ }
303
+
304
+ .bv__actions {
305
+ display: flex;
306
+ flex-direction: column;
307
+ align-items: center;
308
+ gap: 1rem;
309
+ }
310
+
311
+ @media (min-width: 640px) {
312
+ .bv__actions { flex-direction: row; }
313
+ }
314
+
315
+ .bv__start {
316
+ padding: 1.25rem 3rem;
317
+ background: var(--bv-start-bg);
318
+ color: var(--bv-start-text);
319
+ font-weight: 900;
320
+ font-size: 1.125rem;
321
+ border-radius: 1rem;
322
+ border: none;
323
+ cursor: pointer;
324
+ box-shadow: 0 20px 25px -5px rgba(59, 130, 246, 0.2);
325
+ transition: transform 0.2s;
326
+ }
327
+
328
+ .bv__start:hover { transform: scale(1.05); }
329
+ .bv__start:active { transform: scale(0.95); }
330
+
331
+ .bv__start-content {
332
+ display: flex;
333
+ align-items: center;
334
+ gap: 0.75rem;
335
+ }
336
+
337
+ .bv__toggles {
338
+ display: flex;
339
+ align-items: center;
340
+ gap: 1rem;
341
+ }
342
+
343
+ .bv__toggle {
344
+ padding: 1rem;
345
+ background: var(--bv-toggle-bg);
346
+ border: 1px solid var(--bv-toggle-border);
347
+ border-radius: 1rem;
348
+ color: var(--bv-toggle-text);
349
+ cursor: pointer;
350
+ transition: all 0.2s;
351
+ display: flex;
352
+ align-items: center;
353
+ justify-content: center;
354
+ }
355
+
356
+ .bv__toggle:hover { color: #3b82f6; }
357
+
358
+ .bv__toggle--loop-active {
359
+ color: #3b82f6;
360
+ background-color: rgba(59, 130, 246, 0.1);
361
+ border-color: rgba(59, 130, 246, 0.2);
362
+ }
363
+
364
+ .bv__toggle--vib-active {
365
+ color: #10b981;
366
+ background-color: rgba(16, 185, 129, 0.1);
367
+ border-color: rgba(16, 185, 129, 0.2);
368
+ }
369
+
370
+ .bv__icon { width: 1.25rem; height: 1.25rem; }
371
+
372
+ .bv__hidden { display: none; }
373
+
374
+ @media (max-width: 640px) {
375
+ .bv__container { padding: 2rem 1rem; border-radius: 2rem; }
376
+ .bv__timer { font-size: 2.5rem; }
377
+ }
378
+ </style>
379
+
380
+ <script>
381
+ interface PhaseConfig {
382
+ name: string;
383
+ duration: number;
384
+ label: string;
385
+ color: string;
386
+ scale: number;
387
+ }
388
+
389
+ interface WimhofConfig {
390
+ breaths: number;
391
+ inhaleTime: number;
392
+ exhaleTime: number;
393
+ retentionTime: number;
394
+ recoveryTime: number;
395
+ }
396
+
397
+ interface BVui {
398
+ startLabel: string; stopLabel: string; sessionEnded: string; orbIdle: string;
399
+ phaseInhale: string; phaseHold: string; phaseExhale: string; phaseEmpty: string;
400
+ phaseRelease: string; phaseRecover: string;
401
+ labelBoxInhale: string; labelBoxHold: string; labelBoxExhale: string; labelBoxEmpty: string;
402
+ labelRelaxInhale: string; labelRelaxHold: string; labelRelaxExhale: string;
403
+ labelWimInhale: string; labelWimRelease: string; labelWimHold: string; labelWimRecover: string;
404
+ }
405
+
406
+ const container = document.getElementById('bv-container') as HTMLElement;
407
+ const ui = JSON.parse(container.dataset.ui ?? '{}') as BVui;
408
+
409
+ const box: PhaseConfig[] = [
410
+ { name: ui.phaseInhale, duration: 4, label: ui.labelBoxInhale, color: 'blue', scale: 1.5 },
411
+ { name: ui.phaseHold, duration: 4, label: ui.labelBoxHold, color: 'indigo', scale: 1.5 },
412
+ { name: ui.phaseExhale, duration: 4, label: ui.labelBoxExhale, color: 'emerald', scale: 0.8 },
413
+ { name: ui.phaseEmpty, duration: 4, label: ui.labelBoxEmpty, color: 'slate', scale: 0.8 },
414
+ ];
415
+
416
+ const relax: PhaseConfig[] = [
417
+ { name: ui.phaseInhale, duration: 4, label: ui.labelRelaxInhale, color: 'blue', scale: 1.5 },
418
+ { name: ui.phaseHold, duration: 7, label: ui.labelRelaxHold, color: 'indigo', scale: 1.5 },
419
+ { name: ui.phaseExhale, duration: 8, label: ui.labelRelaxExhale, color: 'emerald', scale: 0.8 },
420
+ ];
421
+
422
+ const wh: WimhofConfig = { breaths: 30, inhaleTime: 1.5, exhaleTime: 1.5, retentionTime: 60, recoveryTime: 15 };
423
+
424
+ const orb = document.getElementById('bv-orb');
425
+ const orbText = document.getElementById('bv-orb-text');
426
+ const phaseLabel = document.getElementById('bv-phase-label');
427
+ const timerDisplay = document.getElementById('bv-timer');
428
+ const glow = document.getElementById('bv-glow');
429
+ const ring1 = document.getElementById('bv-ring-1');
430
+ const ring2 = document.getElementById('bv-ring-2');
431
+ const roundIndicator = document.getElementById('bv-round-indicator');
432
+ const currentRoundEl = document.getElementById('bv-round');
433
+ const breathCount = document.getElementById('bv-breath-count');
434
+ const currentBreathEl = document.getElementById('bv-breath');
435
+ const startBtn = document.getElementById('bv-start');
436
+ const startText = document.getElementById('bv-start-text');
437
+ const playIcon = document.getElementById('bv-icon-play');
438
+ const stopIcon = document.getElementById('bv-icon-stop');
439
+ const btnBox = document.getElementById('bv-btn-box');
440
+ const btnRelax = document.getElementById('bv-btn-relax');
441
+ const btnWimhof = document.getElementById('bv-btn-wimhof');
442
+ const loopBtn = document.getElementById('bv-loop');
443
+ const vibBtn = document.getElementById('bv-vib');
444
+
445
+ let isRunning = false;
446
+ let currentPhase = 0;
447
+ let timer = 0;
448
+ let currentRound = 1;
449
+ let currentBreath = 1;
450
+ let isLooping = false;
451
+ let vibrationEnabled = true;
452
+ let technique: 'box' | 'relax' | 'wimhof' = 'box';
453
+ let interval: ReturnType<typeof setInterval> | undefined;
454
+
455
+ function getWimhofConfig(phase: number, t: number, w: WimhofConfig): PhaseConfig {
456
+ if (phase === 0) {
457
+ if (t >= w.inhaleTime / 2) {
458
+ return { name: ui.phaseInhale, duration: w.inhaleTime, label: ui.labelWimInhale, color: 'blue', scale: 1.6 };
459
+ }
460
+ return { name: ui.phaseRelease, duration: w.exhaleTime, label: ui.labelWimRelease, color: 'emerald', scale: 1.1 };
461
+ }
462
+ if (phase === 1) {
463
+ return { name: ui.phaseHold, duration: w.retentionTime, label: ui.labelWimHold, color: 'indigo', scale: 0.7 };
464
+ }
465
+ return { name: ui.phaseRecover, duration: w.recoveryTime, label: ui.labelWimRecover, color: 'blue', scale: 1.3 };
466
+ }
467
+
468
+ function getConfig(): PhaseConfig {
469
+ if (technique === 'wimhof') return getWimhofConfig(currentPhase, timer, wh);
470
+ const phases = technique === 'box' ? box : relax;
471
+ return phases[currentPhase];
472
+ }
473
+
474
+ function applyGlow(color: string): void {
475
+ if (!glow) return;
476
+ const map: Record<string, string> = {
477
+ blue: 'rgba(59,130,246,0.15)', indigo: 'rgba(99,102,241,0.15)',
478
+ emerald: 'rgba(16,185,129,0.15)', slate: 'rgba(107,114,128,0.1)',
479
+ };
480
+ glow.style.backgroundColor = map[color] ?? 'transparent';
481
+ }
482
+
483
+ function applyOrbScale(cfg: PhaseConfig): void {
484
+ if (!orb) return;
485
+ const isWimhofBreath = technique === 'wimhof' && currentPhase === 0;
486
+ const dur = isWimhofBreath ? 0.7 : cfg.duration;
487
+ orb.style.transitionDuration = `${dur}s`;
488
+ orb.style.transform = `scale(${cfg.scale})`;
489
+ }
490
+
491
+ function triggerVibration(duration: number): void {
492
+ if (!vibrationEnabled) return;
493
+ const atBoundary = timer === duration || timer === 1;
494
+ if (atBoundary) window.navigator.vibrate?.(100);
495
+ }
496
+
497
+ function updateUI(): void {
498
+ const cfg = getConfig();
499
+ if (orbText) orbText.textContent = cfg.name;
500
+ if (phaseLabel) phaseLabel.textContent = cfg.label;
501
+ if (timerDisplay) timerDisplay.textContent = Math.ceil(timer).toString().padStart(2, '0');
502
+ applyOrbScale(cfg);
503
+ applyGlow(cfg.color);
504
+ triggerVibration(cfg.duration);
505
+ }
506
+
507
+ function setRunning(running: boolean): void {
508
+ if (startText) startText.textContent = running ? ui.stopLabel : ui.startLabel;
509
+ playIcon?.classList.toggle('bv__hidden', running);
510
+ stopIcon?.classList.toggle('bv__hidden', !running);
511
+ }
512
+
513
+ function resetRings(): void {
514
+ ring1?.classList.remove('bv__ring--active');
515
+ ring2?.classList.remove('bv__ring--active');
516
+ }
517
+
518
+ function resetIndicators(): void {
519
+ roundIndicator?.classList.add('bv__hidden');
520
+ breathCount?.classList.add('bv__hidden');
521
+ }
522
+
523
+ function stopSession(): void {
524
+ isRunning = false;
525
+ clearInterval(interval);
526
+ setRunning(false);
527
+ if (phaseLabel) phaseLabel.textContent = ui.sessionEnded;
528
+ if (orbText) orbText.textContent = ui.orbIdle;
529
+ resetRings();
530
+ resetIndicators();
531
+ if (orb) { orb.style.transitionDuration = '1s'; orb.style.transform = 'scale(1)'; }
532
+ if (glow) glow.style.backgroundColor = 'transparent';
533
+ }
534
+
535
+ function handleWimhofBreathing(): void {
536
+ if (orbText?.textContent === ui.phaseInhale) { timer = wh.exhaleTime; return; }
537
+ currentBreath++;
538
+ if (currentBreath > wh.breaths) {
539
+ currentPhase = 1;
540
+ timer = wh.retentionTime;
541
+ breathCount?.classList.add('bv__hidden');
542
+ return;
543
+ }
544
+ timer = wh.inhaleTime;
545
+ if (currentBreathEl) currentBreathEl.textContent = currentBreath.toString();
546
+ }
547
+
548
+ function handleWimhofTick(): void {
549
+ timer -= 0.1;
550
+ if (timer > 0) return;
551
+ if (currentPhase === 0) { handleWimhofBreathing(); return; }
552
+ if (currentPhase === 1) { currentPhase = 2; timer = wh.recoveryTime; return; }
553
+ if (!isLooping) { stopSession(); return; }
554
+ currentRound++;
555
+ if (currentRoundEl) currentRoundEl.textContent = currentRound.toString();
556
+ currentPhase = 0;
557
+ currentBreath = 1;
558
+ timer = wh.inhaleTime;
559
+ breathCount?.classList.remove('bv__hidden');
560
+ if (currentBreathEl) currentBreathEl.textContent = '1';
561
+ }
562
+
563
+ function handleRegularTick(): void {
564
+ timer--;
565
+ if (timer >= 1) return;
566
+ currentPhase++;
567
+ const phases = technique === 'box' ? box : relax;
568
+ if (currentPhase < phases.length) { timer = phases[currentPhase].duration; return; }
569
+ if (!isLooping) { stopSession(); return; }
570
+ currentRound++;
571
+ if (currentRoundEl) currentRoundEl.textContent = currentRound.toString();
572
+ currentPhase = 0;
573
+ roundIndicator?.classList.remove('bv__hidden');
574
+ timer = phases[0].duration;
575
+ }
576
+
577
+ function tick(): void {
578
+ if (technique === 'wimhof') handleWimhofTick(); else handleRegularTick();
579
+ updateUI();
580
+ }
581
+
582
+ function initWimhofSession(): void {
583
+ timer = wh.inhaleTime;
584
+ breathCount?.classList.remove('bv__hidden');
585
+ roundIndicator?.classList.remove('bv__hidden');
586
+ }
587
+
588
+ function initRegularSession(): void {
589
+ const phases = technique === 'box' ? box : relax;
590
+ timer = phases[0].duration;
591
+ breathCount?.classList.add('bv__hidden');
592
+ roundIndicator?.classList.toggle('bv__hidden', !isLooping);
593
+ }
594
+
595
+ function activateRings(): void {
596
+ ring1?.classList.add('bv__ring--active');
597
+ ring2?.classList.add('bv__ring--active');
598
+ }
599
+
600
+ function startSession(): void {
601
+ isRunning = true;
602
+ currentPhase = 0;
603
+ currentRound = 1;
604
+ currentBreath = 1;
605
+ if (technique === 'wimhof') initWimhofSession(); else initRegularSession();
606
+ setRunning(true);
607
+ activateRings();
608
+ updateUI();
609
+ const ms = technique === 'wimhof' ? 100 : 1000;
610
+ interval = setInterval(tick, ms);
611
+ }
612
+
613
+ function updateTabs(): void {
614
+ const active = 'bv__tab--active';
615
+ btnBox?.classList.toggle(active, technique === 'box');
616
+ btnRelax?.classList.toggle(active, technique === 'relax');
617
+ btnWimhof?.classList.toggle(active, technique === 'wimhof');
618
+ }
619
+
620
+ startBtn?.addEventListener('click', () => { if (isRunning) stopSession(); else startSession(); });
621
+ btnBox?.addEventListener('click', () => { if (!isRunning) { technique = 'box'; updateTabs(); } });
622
+ btnRelax?.addEventListener('click', () => { if (!isRunning) { technique = 'relax'; updateTabs(); } });
623
+ btnWimhof?.addEventListener('click', () => { if (!isRunning) { technique = 'wimhof'; updateTabs(); } });
624
+
625
+ if (loopBtn) loopBtn.addEventListener('click', () => {
626
+ isLooping = !isLooping;
627
+ loopBtn.classList.toggle('bv__toggle--loop-active', isLooping);
628
+ });
629
+
630
+ if (vibBtn) vibBtn.addEventListener('click', () => {
631
+ vibrationEnabled = !vibrationEnabled;
632
+ vibBtn.classList.toggle('bv__toggle--vib-active', vibrationEnabled);
633
+ });
634
+
635
+ document.addEventListener('astro:before-preparation', () => clearInterval(interval));
636
+ </script>