@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,21 @@
1
+ export interface PelliRobsonTestUI extends Record<string, string> {
2
+ introSensLabel: string;
3
+ introTestLabel: string;
4
+ introTitle: string;
5
+ introDescription: string;
6
+ introBtnStart: string;
7
+ hudLevelLabel: string;
8
+ hudScoreLabel: string;
9
+ inputPlaceholder: string;
10
+ btnCantSee: string;
11
+ resultScoreLabel: string;
12
+ result4Title: string;
13
+ result4Text: string;
14
+ result3Title: string;
15
+ result3Text: string;
16
+ result2Title: string;
17
+ result2Text: string;
18
+ result1Title: string;
19
+ result1Text: string;
20
+ btnRestart: string;
21
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography } from '@jjlmoya/utils-shared';
3
+ import { peripheralVisionTrainer } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await peripheralVisionTrainer.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <Bibliography items={content.bibliography} title={content.bibliographyTitle} />}
@@ -0,0 +1,678 @@
1
+ ---
2
+ import type { PeripheralVisionTrainerUI } from './ui';
3
+ import { Icon } from 'astro-icon/components';
4
+
5
+ interface Props {
6
+ ui?: Partial<PeripheralVisionTrainerUI>;
7
+ }
8
+
9
+ const ui = (Astro.props.ui ?? {}) as PeripheralVisionTrainerUI;
10
+ ---
11
+
12
+ <div class="pvt" data-ui={JSON.stringify(ui)}>
13
+
14
+ <div class="pvt__view pvt__view--intro" id="pvt-intro">
15
+ <h3 class="pvt__intro-title" data-key="introTitle">Visión Periférica</h3>
16
+ <p class="pvt__intro-desc" data-key="introDesc">
17
+ Entrena tu capacidad para procesar información fuera de tu foco directo.
18
+ </p>
19
+
20
+ <div class="pvt__benefits">
21
+ <div class="pvt__benefit pvt__benefit--blue">
22
+ <div class="pvt__benefit-icon"><Icon name="mdi:flash" /></div>
23
+ <span class="pvt__benefit-title" data-key="benefit1Title">Velocidad</span>
24
+ <p class="pvt__benefit-desc" data-key="benefit1Desc">Reduce el tiempo de reacción ante estímulos inesperados.</p>
25
+ </div>
26
+ <div class="pvt__benefit pvt__benefit--green">
27
+ <div class="pvt__benefit-icon"><Icon name="mdi:eye" /></div>
28
+ <span class="pvt__benefit-title" data-key="benefit2Title">Lectura Rápida</span>
29
+ <p class="pvt__benefit-desc" data-key="benefit2Desc">Amplía tu campo visual para captar frases enteras.</p>
30
+ </div>
31
+ <div class="pvt__benefit pvt__benefit--amber">
32
+ <div class="pvt__benefit-icon"><Icon name="mdi:target" /></div>
33
+ <span class="pvt__benefit-title" data-key="benefit3Title">Foco</span>
34
+ <p class="pvt__benefit-desc" data-key="benefit3Desc">Mejora la concentración manteniendo el centro fijo.</p>
35
+ </div>
36
+ </div>
37
+
38
+ <button class="pvt__btn" id="pvt-start" data-key="btnStart">Comenzar Entrenamiento</button>
39
+ </div>
40
+
41
+
42
+ <div class="pvt__view pvt__view--game pvt__view--hidden" id="pvt-game">
43
+ <div class="pvt__hud">
44
+ <div class="pvt__hud-stat">
45
+ <span class="pvt__stat-val" id="pvt-score">0</span>
46
+ <span class="pvt__stat-lbl" data-key="hudPoints">Puntos</span>
47
+ </div>
48
+ <div class="pvt__hud-stat">
49
+ <span class="pvt__stat-val" id="pvt-level">1</span>
50
+ <span class="pvt__stat-lbl" data-key="hudLevel">Nivel</span>
51
+ </div>
52
+ <div class="pvt__hud-stat">
53
+ <span class="pvt__stat-val" id="pvt-time">60</span>
54
+ <span class="pvt__stat-lbl" data-key="hudTime">Tiempo</span>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="pvt__viewport" id="pvt-viewport">
59
+ <div class="pvt__focus-ring"></div>
60
+ <div class="pvt__center-anchor"></div>
61
+ <div class="pvt__target pvt__view--hidden" id="pvt-target"></div>
62
+ </div>
63
+ </div>
64
+
65
+
66
+ <div class="pvt__view pvt__view--result pvt__view--hidden" id="pvt-result">
67
+ <h3 class="pvt__rank-title" id="pvt-rank">EXPLORADOR</h3>
68
+ <div class="pvt__hud pvt__hud--result">
69
+ <div class="pvt__hud-stat">
70
+ <span class="pvt__stat-val" id="pvt-final">0</span>
71
+ <span class="pvt__stat-lbl" data-key="resultTotal">Total</span>
72
+ </div>
73
+ </div>
74
+ <p class="pvt__rank-text" id="pvt-rank-text">Sigue practicando.</p>
75
+ <button class="pvt__btn" id="pvt-restart" data-key="btnRestart">Reiniciar Protocolo</button>
76
+ </div>
77
+
78
+
79
+ <div class="pvt__method" id="pvt-method" data-key="methodNote">
80
+ MÉTODO: Mantén la mirada fija en el punto naranja central.
81
+ </div>
82
+
83
+
84
+ <div class="pvt__mobile" id="pvt-mobile">
85
+ <div class="pvt__mobile-icon"><Icon name="mdi:monitor" /></div>
86
+ <h3 class="pvt__mobile-title" data-key="mobileTitle">Pantalla Demasiado Pequeña</h3>
87
+ <p class="pvt__mobile-desc" data-key="mobileDesc">
88
+ El Entrenador de Visión Periférica requiere un teclado físico.
89
+ </p>
90
+ <div class="pvt__mobile-tip">
91
+ <span><Icon name="mdi:laptop" /></span>
92
+ <span data-key="mobileTip">Por favor, accede desde un ordenador con teclado físico.</span>
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ <style>
98
+ .pvt {
99
+ --pvt-bg: #f8fafc;
100
+ --pvt-surface: #fff;
101
+ --pvt-primary: #3b82f6;
102
+ --pvt-accent: #f59e0b;
103
+ --pvt-success: #10b981;
104
+ --pvt-danger: #ef4444;
105
+ --pvt-text: #0f172a;
106
+ --pvt-muted: #94a3b8;
107
+ --pvt-surface-2: #f1f5f9;
108
+ --pvt-border: rgba(0, 0, 0, 0.06);
109
+ --pvt-shadow: 0 40px 100px -20px rgba(0, 0, 0, 0.06);
110
+
111
+ display: flex;
112
+ flex-direction: column;
113
+ gap: 2rem;
114
+ padding: 4rem;
115
+ max-width: 1200px;
116
+ margin: 0 auto;
117
+ background: var(--pvt-surface);
118
+ border-radius: 48px;
119
+ box-shadow: var(--pvt-shadow);
120
+ border: 1px solid var(--pvt-border);
121
+ text-align: center;
122
+ position: relative;
123
+ overflow: hidden;
124
+ transition: background 0.3s ease, color 0.3s ease;
125
+ }
126
+
127
+
128
+ .pvt__view {
129
+ display: flex;
130
+ flex-direction: column;
131
+ align-items: center;
132
+ gap: 1.5rem;
133
+ }
134
+
135
+ .pvt__view--hidden {
136
+ display: none;
137
+ }
138
+
139
+ .pvt__view--game {
140
+ gap: 2rem;
141
+ }
142
+
143
+ .pvt__view--result {
144
+ animation: pvt-slide-in 0.6s cubic-bezier(0.16, 1, 0.3, 1);
145
+ }
146
+
147
+ @keyframes pvt-slide-in {
148
+ from { opacity: 0; transform: translateY(20px); }
149
+ to { opacity: 1; transform: translateY(0); }
150
+ }
151
+
152
+
153
+ .pvt__intro-title {
154
+ font-size: 3rem;
155
+ font-weight: 950;
156
+ color: var(--pvt-text);
157
+ margin: 0;
158
+ line-height: 1.1;
159
+ }
160
+
161
+ .pvt__intro-desc {
162
+ color: var(--pvt-muted);
163
+ max-width: 600px;
164
+ margin: 0 auto;
165
+ line-height: 1.8;
166
+ font-size: 1.1rem;
167
+ }
168
+
169
+
170
+ .pvt__benefits {
171
+ display: grid;
172
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
173
+ gap: 1.5rem;
174
+ width: 100%;
175
+ margin: 1rem 0;
176
+ }
177
+
178
+ .pvt__benefit {
179
+ background: var(--pvt-surface-2);
180
+ padding: 2rem;
181
+ border-radius: 24px;
182
+ display: flex;
183
+ flex-direction: column;
184
+ align-items: center;
185
+ gap: 0.75rem;
186
+ border: 1px solid var(--pvt-border);
187
+ transition: transform 0.3s ease;
188
+ }
189
+
190
+ .pvt__benefit:hover {
191
+ transform: translateY(-4px);
192
+ }
193
+
194
+ .pvt__benefit-icon {
195
+ width: 52px;
196
+ height: 52px;
197
+ border-radius: 14px;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ font-size: 1.6rem;
202
+ line-height: 1;
203
+ }
204
+
205
+ .pvt__benefit--blue .pvt__benefit-icon {
206
+ background: rgba(59, 130, 246, 0.1);
207
+ color: var(--pvt-primary);
208
+ }
209
+
210
+ .pvt__benefit--green .pvt__benefit-icon {
211
+ background: rgba(16, 185, 129, 0.1);
212
+ color: var(--pvt-success);
213
+ }
214
+
215
+ .pvt__benefit--amber .pvt__benefit-icon {
216
+ background: rgba(245, 158, 11, 0.1);
217
+ color: var(--pvt-accent);
218
+ }
219
+
220
+ .pvt__benefit-title {
221
+ font-size: 1.05rem;
222
+ font-weight: 800;
223
+ color: var(--pvt-text);
224
+ }
225
+
226
+ .pvt__benefit-desc {
227
+ font-size: 0.9rem;
228
+ color: var(--pvt-muted);
229
+ line-height: 1.5;
230
+ margin: 0;
231
+ }
232
+
233
+
234
+ .pvt__btn {
235
+ background: var(--pvt-primary);
236
+ color: #fff;
237
+ border: none;
238
+ padding: 1.5rem 3.5rem;
239
+ font-size: 1.3rem;
240
+ font-weight: 900;
241
+ border-radius: 24px;
242
+ cursor: pointer;
243
+ box-shadow: 0 20px 40px rgba(59, 130, 246, 0.2);
244
+ transition: all 0.2s ease;
245
+ }
246
+
247
+ .pvt__btn:hover {
248
+ transform: translateY(-5px);
249
+ box-shadow: 0 30px 60px rgba(59, 130, 246, 0.3);
250
+ }
251
+
252
+
253
+ .pvt__hud {
254
+ display: flex;
255
+ justify-content: space-around;
256
+ gap: 1.5rem;
257
+ width: 100%;
258
+ }
259
+
260
+ .pvt__hud--result {
261
+ justify-content: center;
262
+ }
263
+
264
+ .pvt__hud-stat {
265
+ background: var(--pvt-surface-2);
266
+ padding: 1.5rem;
267
+ border-radius: 24px;
268
+ flex: 1;
269
+ display: flex;
270
+ flex-direction: column;
271
+ align-items: center;
272
+ gap: 0.25rem;
273
+ }
274
+
275
+ .pvt__stat-val {
276
+ font-size: 2.5rem;
277
+ font-weight: 950;
278
+ color: var(--pvt-text);
279
+ line-height: 1;
280
+ }
281
+
282
+ .pvt__stat-lbl {
283
+ font-size: 0.7rem;
284
+ font-weight: 800;
285
+ color: var(--pvt-muted);
286
+ text-transform: uppercase;
287
+ letter-spacing: 2px;
288
+ }
289
+
290
+
291
+ .pvt__viewport {
292
+ position: relative;
293
+ width: 100%;
294
+ height: 580px;
295
+ background: var(--pvt-bg);
296
+ border-radius: 40px;
297
+ overflow: hidden;
298
+ cursor: none;
299
+ border: 4px solid var(--pvt-surface-2);
300
+ box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.02);
301
+ }
302
+
303
+ .pvt__center-anchor {
304
+ position: absolute;
305
+ top: 50%;
306
+ left: 50%;
307
+ transform: translate(-50%, -50%);
308
+ width: 20px;
309
+ height: 20px;
310
+ background: var(--pvt-accent);
311
+ border-radius: 50%;
312
+ box-shadow: 0 0 30px rgba(245, 158, 11, 0.4);
313
+ z-index: 10;
314
+ }
315
+
316
+ .pvt__focus-ring {
317
+ position: absolute;
318
+ top: 50%;
319
+ left: 50%;
320
+ transform: translate(-50%, -50%);
321
+ width: 80px;
322
+ height: 80px;
323
+ border: 4px solid var(--pvt-accent);
324
+ opacity: 0.15;
325
+ border-radius: 50%;
326
+ animation: pvt-pulse 2s ease-out infinite;
327
+ pointer-events: none;
328
+ }
329
+
330
+ @keyframes pvt-pulse {
331
+ 0% { transform: translate(-50%, -50%) scale(1); opacity: 0.3; }
332
+ 100% { transform: translate(-50%, -50%) scale(3); opacity: 0; }
333
+ }
334
+
335
+ .pvt__target {
336
+ position: absolute;
337
+ width: 45px;
338
+ height: 45px;
339
+ left: -9999px;
340
+ top: -9999px;
341
+ background: var(--pvt-text);
342
+ color: var(--pvt-surface);
343
+ border-radius: 12px;
344
+ display: flex;
345
+ justify-content: center;
346
+ align-items: center;
347
+ font-size: 1.5rem;
348
+ font-weight: 950;
349
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12);
350
+ transform: translate(-50%, -50%);
351
+ transition: background 0.1s ease;
352
+ }
353
+
354
+ .pvt :global(.pvt__target--wrong) {
355
+ background: var(--pvt-danger);
356
+ color: #fff;
357
+ animation: pvt-shake 0.4s ease;
358
+ }
359
+
360
+ .pvt :global(.pvt__target--correct) {
361
+ background: var(--pvt-success);
362
+ color: #fff;
363
+ animation: pvt-pop 0.3s ease;
364
+ }
365
+
366
+ @keyframes pvt-shake {
367
+ 0%, 100% { transform: translate(-50%, -50%) translateX(0); }
368
+ 25% { transform: translate(-50%, -50%) translateX(-10px); }
369
+ 75% { transform: translate(-50%, -50%) translateX(10px); }
370
+ }
371
+
372
+ @keyframes pvt-pop {
373
+ 0% { transform: translate(-50%, -50%) scale(0.8); }
374
+ 50% { transform: translate(-50%, -50%) scale(1.2); }
375
+ 100% { transform: translate(-50%, -50%) scale(1); }
376
+ }
377
+
378
+
379
+ .pvt__rank-title {
380
+ font-size: 3.5rem;
381
+ font-weight: 950;
382
+ color: var(--pvt-text);
383
+ margin: 0;
384
+ }
385
+
386
+ .pvt__rank-text {
387
+ color: var(--pvt-muted);
388
+ font-size: 1.2rem;
389
+ max-width: 500px;
390
+ margin: 0 auto;
391
+ line-height: 1.6;
392
+ }
393
+
394
+
395
+ .pvt__method {
396
+ background: var(--pvt-surface-2);
397
+ border-radius: 16px;
398
+ padding: 1rem 1.5rem;
399
+ font-size: 0.85rem;
400
+ font-weight: 700;
401
+ color: var(--pvt-muted);
402
+ letter-spacing: 0.03em;
403
+ line-height: 1.5;
404
+ }
405
+
406
+
407
+ .pvt__mobile {
408
+ display: none;
409
+ }
410
+
411
+
412
+ :global(.theme-dark) .pvt {
413
+ --pvt-bg: #0f172a;
414
+ --pvt-surface: #1e293b;
415
+ --pvt-primary: #60a5fa;
416
+ --pvt-accent: #fbbf24;
417
+ --pvt-success: #34d399;
418
+ --pvt-danger: #f87171;
419
+ --pvt-text: #f8fafc;
420
+ --pvt-muted: #94a3b8;
421
+ --pvt-surface-2: #334155;
422
+ --pvt-border: rgba(255, 255, 255, 0.06);
423
+ --pvt-shadow: 0 40px 100px -20px rgba(0, 0, 0, 0.5);
424
+ }
425
+
426
+
427
+ @media (max-width: 1024px) {
428
+ .pvt {
429
+ padding: 2rem 1.5rem;
430
+ gap: 1.5rem;
431
+ }
432
+
433
+ #pvt-intro,
434
+ #pvt-game,
435
+ #pvt-result,
436
+ .pvt__method {
437
+ display: none;
438
+ }
439
+
440
+ .pvt__mobile {
441
+ display: flex;
442
+ flex-direction: column;
443
+ align-items: center;
444
+ justify-content: center;
445
+ text-align: center;
446
+ gap: 1.5rem;
447
+ padding: 2rem 1rem;
448
+ background: var(--pvt-surface-2);
449
+ border-radius: 24px;
450
+ border: 1px dashed var(--pvt-border);
451
+ }
452
+
453
+ .pvt__mobile-icon {
454
+ font-size: 4rem;
455
+ background: rgba(245, 158, 11, 0.1);
456
+ width: 100px;
457
+ height: 100px;
458
+ border-radius: 50%;
459
+ display: flex;
460
+ align-items: center;
461
+ justify-content: center;
462
+ }
463
+
464
+ .pvt__mobile-title {
465
+ font-size: 1.75rem;
466
+ font-weight: 900;
467
+ color: var(--pvt-text);
468
+ margin: 0;
469
+ }
470
+
471
+ .pvt__mobile-desc {
472
+ font-size: 1rem;
473
+ line-height: 1.6;
474
+ color: var(--pvt-muted);
475
+ max-width: 320px;
476
+ margin: 0;
477
+ }
478
+
479
+ .pvt__mobile-tip {
480
+ display: flex;
481
+ flex-direction: column;
482
+ align-items: center;
483
+ gap: 0.75rem;
484
+ background: var(--pvt-bg);
485
+ padding: 1.25rem 1.5rem;
486
+ border-radius: 16px;
487
+ font-size: 0.95rem;
488
+ color: var(--pvt-text);
489
+ font-weight: 600;
490
+ border: 1px solid var(--pvt-border);
491
+ }
492
+
493
+ .pvt__mobile-tip span:first-child {
494
+ font-size: 2.5rem;
495
+ }
496
+ }
497
+ </style>
498
+
499
+ <script>
500
+ interface PvtUI {
501
+ introTitle?: string;
502
+ introDesc?: string;
503
+ benefit1Title?: string;
504
+ benefit1Desc?: string;
505
+ benefit2Title?: string;
506
+ benefit2Desc?: string;
507
+ benefit3Title?: string;
508
+ benefit3Desc?: string;
509
+ btnStart?: string;
510
+ hudPoints?: string;
511
+ hudLevel?: string;
512
+ hudTime?: string;
513
+ methodNote?: string;
514
+ mobileTitle?: string;
515
+ mobileDesc?: string;
516
+ mobileTip?: string;
517
+ resultTotal?: string;
518
+ rank3Title?: string;
519
+ rank3Text?: string;
520
+ rank2Title?: string;
521
+ rank2Text?: string;
522
+ rank1Title?: string;
523
+ rank1Text?: string;
524
+ btnRestart?: string;
525
+ }
526
+
527
+ function applyUI(container: Element, ui: PvtUI): void {
528
+ container.querySelectorAll<HTMLElement>('[data-key]').forEach((el) => {
529
+ const key = el.dataset.key as keyof PvtUI;
530
+ const val = ui[key];
531
+ if (val) el.textContent = val;
532
+ });
533
+ }
534
+
535
+ // eslint-disable-next-line complexity
536
+ function getRank(score: number, ui: PvtUI): { title: string; text: string } {
537
+ const ranks: Array<[number, { title: string; text: string }]> = [
538
+ [1200, { title: ui.rank3Title ?? 'VISIÓN FRACTAL', text: ui.rank3Text ?? '' }],
539
+ [600, { title: ui.rank2Title ?? 'HALCÓN', text: ui.rank2Text ?? '' }],
540
+ ];
541
+ const rank = ranks.find(([threshold]) => score > threshold);
542
+ return rank?.[1] ?? { title: ui.rank1Title ?? 'EXPLORADOR', text: ui.rank1Text ?? '' };
543
+ }
544
+
545
+ // eslint-disable-next-line max-lines-per-function
546
+ function initGame(container: Element): void {
547
+ const ui: PvtUI = JSON.parse((container as HTMLElement).dataset.ui ?? '{}');
548
+ applyUI(container, ui);
549
+
550
+ const introEl = container.querySelector<HTMLElement>('#pvt-intro');
551
+ const gameEl = container.querySelector<HTMLElement>('#pvt-game');
552
+ const resultEl = container.querySelector<HTMLElement>('#pvt-result');
553
+ const targetEl = container.querySelector<HTMLElement>('#pvt-target');
554
+ const viewportEl = container.querySelector<HTMLElement>('#pvt-viewport');
555
+ const scoreEl = container.querySelector<HTMLElement>('#pvt-score');
556
+ const levelEl = container.querySelector<HTMLElement>('#pvt-level');
557
+ const timeEl = container.querySelector<HTMLElement>('#pvt-time');
558
+ const finalEl = container.querySelector<HTMLElement>('#pvt-final');
559
+ const rankEl = container.querySelector<HTMLElement>('#pvt-rank');
560
+ const rankTextEl = container.querySelector<HTMLElement>('#pvt-rank-text');
561
+
562
+ let score = 0;
563
+ let level = 1;
564
+ let timeLeft = 60;
565
+ let gameActive = false;
566
+ let currentKey = '';
567
+ let targetVisible = false;
568
+ let timerId: number | null = null;
569
+ let spawnId: number | null = null;
570
+
571
+ function updateHUD(): void {
572
+ if (scoreEl) scoreEl.textContent = score.toString();
573
+ if (levelEl) levelEl.textContent = level.toString();
574
+ }
575
+
576
+ function spawnTarget(): void {
577
+ if (!gameActive || !targetEl || !viewportEl) return;
578
+ const keys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
579
+ currentKey = keys[Math.floor(Math.random() * keys.length)] ?? 'ArrowUp';
580
+
581
+ const rect = viewportEl.getBoundingClientRect();
582
+ const margin = 80;
583
+ const arrowMap: Record<string, { x: number; y: number; icon: string }> = {
584
+ ArrowUp: { x: Math.random() * (rect.width - 200) + 100, y: margin, icon: '↑' },
585
+ ArrowDown: { x: Math.random() * (rect.width - 200) + 100, y: rect.height - margin, icon: '↓' },
586
+ ArrowLeft: { x: margin, y: Math.random() * (rect.height - 200) + 100, icon: '←' },
587
+ ArrowRight: { x: rect.width - margin, y: Math.random() * (rect.height - 200) + 100, icon: '→' },
588
+ };
589
+
590
+ const pos = arrowMap[currentKey];
591
+ if (!pos) return;
592
+
593
+ targetEl.style.left = `${pos.x}px`;
594
+ targetEl.style.top = `${pos.y}px`;
595
+ targetEl.textContent = pos.icon;
596
+ targetEl.classList.remove('pvt__view--hidden', 'pvt__target--correct', 'pvt__target--wrong');
597
+ targetVisible = true;
598
+
599
+ const visibilityTime = Math.max(400, 1500 - level * 100);
600
+ window.setTimeout(() => {
601
+ if (targetVisible) {
602
+ targetEl.classList.add('pvt__view--hidden');
603
+ targetVisible = false;
604
+ scheduleNextSpawn();
605
+ }
606
+ }, visibilityTime);
607
+ }
608
+
609
+ function scheduleNextSpawn(): void {
610
+ if (!gameActive) return;
611
+ const delay = Math.max(500, 2000 - level * 150);
612
+ spawnId = window.setTimeout(spawnTarget, delay);
613
+ }
614
+
615
+ function handleKey(key: string): void {
616
+ if (!gameActive || !targetVisible || !targetEl) return;
617
+ if (key === currentKey) {
618
+ score += 10 * level;
619
+ if (score > 0 && score % 100 === 0) level++;
620
+ targetEl.classList.add('pvt__target--correct');
621
+ targetVisible = false;
622
+ setTimeout(() => targetEl.classList.add('pvt__view--hidden'), 250);
623
+ updateHUD();
624
+ if (spawnId) clearTimeout(spawnId);
625
+ scheduleNextSpawn();
626
+ } else {
627
+ targetEl.classList.add('pvt__target--wrong');
628
+ score = Math.max(0, score - 5);
629
+ updateHUD();
630
+ }
631
+ }
632
+
633
+ function startGame(): void {
634
+ score = 0;
635
+ level = 1;
636
+ timeLeft = 60;
637
+ gameActive = true;
638
+ introEl?.classList.add('pvt__view--hidden');
639
+ resultEl?.classList.add('pvt__view--hidden');
640
+ gameEl?.classList.remove('pvt__view--hidden');
641
+ updateHUD();
642
+ if (timeEl) timeEl.textContent = '60';
643
+ timerId = window.setInterval(() => {
644
+ timeLeft--;
645
+ if (timeEl) timeEl.textContent = timeLeft.toString();
646
+ if (timeLeft <= 0) endGame();
647
+ }, 1000);
648
+ setTimeout(scheduleNextSpawn, 600);
649
+ }
650
+
651
+ function endGame(): void {
652
+ gameActive = false;
653
+ if (timerId) clearInterval(timerId);
654
+ if (spawnId) clearTimeout(spawnId);
655
+ gameEl?.classList.add('pvt__view--hidden');
656
+ resultEl?.classList.remove('pvt__view--hidden');
657
+ if (finalEl) finalEl.textContent = score.toString();
658
+ if (rankEl && rankTextEl) {
659
+ const rank = getRank(score, ui);
660
+ rankEl.textContent = rank.title;
661
+ rankTextEl.textContent = rank.text;
662
+ }
663
+ }
664
+
665
+ container.querySelector('#pvt-start')?.addEventListener('click', startGame);
666
+ container.querySelector('#pvt-restart')?.addEventListener('click', startGame);
667
+
668
+ document.addEventListener('keydown', (e) => {
669
+ const arrows = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
670
+ if (gameActive && arrows.includes(e.key)) {
671
+ e.preventDefault();
672
+ handleKey(e.key);
673
+ }
674
+ });
675
+ }
676
+
677
+ document.querySelectorAll<HTMLElement>('.pvt').forEach(initGame);
678
+ </script>