@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,18 @@
1
+ export interface ReadingDistanceCalculatorUI extends Record<string, string> {
2
+ bannerText: string;
3
+ zoomLabel: string;
4
+ previewText: string;
5
+ resultLabel: string;
6
+ resultUnit: string;
7
+ resultFooter: string;
8
+ statusWarning: string;
9
+ statusWarningDesc: string;
10
+ statusSafe: string;
11
+ statusSafeDesc: string;
12
+ statusIdeal: string;
13
+ statusIdealDesc: string;
14
+ conceptTitle1: string;
15
+ conceptText1: string;
16
+ conceptTitle2: string;
17
+ conceptText2: string;
18
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography } from '@jjlmoya/utils-shared';
3
+ import { screenDecompressionTime } 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 screenDecompressionTime.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <Bibliography items={content.bibliography} title={content.bibliographyTitle} />}
@@ -0,0 +1,671 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { ScreenDecompressionTimeUI } from './ui';
4
+
5
+ interface Props {
6
+ ui?: Partial<ScreenDecompressionTimeUI>;
7
+ }
8
+
9
+ const ui = (Astro.props.ui ?? {}) as ScreenDecompressionTimeUI;
10
+ ---
11
+
12
+ <div class="sdt" data-ui={JSON.stringify(ui)}>
13
+ <div class="sdt__card">
14
+
15
+
16
+ <div class="sdt__section">
17
+ <label class="sdt__section-title" for="sdt-slider" data-key="labelHours">Horas mirando pantalla</label>
18
+
19
+ <div class="sdt__slider-row">
20
+ <input class="sdt__slider" id="sdt-slider" type="range" min="0" max="16" step="0.5" value="8" />
21
+ <input class="sdt__number" id="sdt-number" type="number" min="0" max="24" step="0.5" value="8" />
22
+ </div>
23
+
24
+ <div class="sdt__presets">
25
+ <button class="sdt__preset" data-val="2" data-key="presetLow">Poco</button>
26
+ <button class="sdt__preset sdt__preset--active" data-val="8" data-key="presetNormal">Normal</button>
27
+ <button class="sdt__preset" data-val="12" data-key="presetHigh">Mucho</button>
28
+ </div>
29
+ </div>
30
+
31
+
32
+ <div class="sdt__section sdt__section--result">
33
+ <div class="sdt__result-inner">
34
+ <div class="sdt__ring-wrap">
35
+ <div class="sdt__ring" id="sdt-ring">
36
+ <div class="sdt__ring-hole"></div>
37
+ </div>
38
+ <span class="sdt__fatigue-label" id="sdt-fatigue">Moderado</span>
39
+ </div>
40
+ <div class="sdt__result-text">
41
+ <span class="sdt__result-num" id="sdt-minutes">16</span>
42
+ <span class="sdt__result-unit" data-key="resultUnit">minutos de descanso</span>
43
+ <span class="sdt__result-per" data-key="resultPer">por día</span>
44
+ </div>
45
+ </div>
46
+ </div>
47
+
48
+
49
+ <div class="sdt__section sdt__section--bordered sdt__section--stats">
50
+ <div class="sdt__stat">
51
+ <span class="sdt__stat-icon"><Icon name="mdi:timer-outline" /></span>
52
+ <span class="sdt__stat-value" id="sdt-interval">20</span>
53
+ <span class="sdt__stat-unit" data-key="unitMin">min</span>
54
+ <span class="sdt__stat-label" data-key="statInterval">Intervalo</span>
55
+ </div>
56
+ <div class="sdt__stat">
57
+ <span class="sdt__stat-icon"><Icon name="mdi:refresh" /></span>
58
+ <span class="sdt__stat-value" id="sdt-breaks">24</span>
59
+ <span class="sdt__stat-label" data-key="statBreaks">Descansos/día</span>
60
+ </div>
61
+ <div class="sdt__stat">
62
+ <span class="sdt__stat-icon"><Icon name="mdi:ruler-square-compass" /></span>
63
+ <span class="sdt__stat-value" id="sdt-distance">75</span>
64
+ <span class="sdt__stat-unit">cm</span>
65
+ <span class="sdt__stat-label" data-key="statDistance">Distancia ideal</span>
66
+ </div>
67
+ </div>
68
+
69
+
70
+ <div class="sdt__section sdt__section--bordered sdt__section--recs">
71
+ <div class="sdt__rec">
72
+ <span class="sdt__rec-icon"><Icon name="mdi:timer-pause-outline" /></span>
73
+ <div class="sdt__rec-body">
74
+ <strong class="sdt__rec-title" data-key="recBreaksTitle">Descansos</strong>
75
+ <p class="sdt__rec-text" id="sdt-rec-breaks"></p>
76
+ </div>
77
+ </div>
78
+ <div class="sdt__rec">
79
+ <span class="sdt__rec-icon"><Icon name="mdi:monitor" /></span>
80
+ <div class="sdt__rec-body">
81
+ <strong class="sdt__rec-title" data-key="recPostureTitle">Postura</strong>
82
+ <p class="sdt__rec-text" id="sdt-rec-posture"></p>
83
+ </div>
84
+ </div>
85
+ <div class="sdt__rec">
86
+ <span class="sdt__rec-icon"><Icon name="mdi:eye-circle-outline" /></span>
87
+ <div class="sdt__rec-body">
88
+ <strong class="sdt__rec-title" data-key="recBlinkTitle">Parpadeo</strong>
89
+ <p class="sdt__rec-text" id="sdt-rec-blink"></p>
90
+ </div>
91
+ </div>
92
+ <div class="sdt__rec">
93
+ <span class="sdt__rec-icon"><Icon name="mdi:lightbulb-variant-outline" /></span>
94
+ <div class="sdt__rec-body">
95
+ <strong class="sdt__rec-title" data-key="recLightTitle">Luz Azul</strong>
96
+ <p class="sdt__rec-text" id="sdt-rec-light"></p>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+
102
+ <div class="sdt__section sdt__section--bordered">
103
+ <h3 class="sdt__section-title" data-key="timelineTitle">Tu Día Visual Ideal</h3>
104
+ <div class="sdt__timeline-bar" id="sdt-timeline-bar">
105
+ <div class="sdt__tl-work" id="sdt-tl-work"></div>
106
+ <div class="sdt__tl-rest" id="sdt-tl-rest"></div>
107
+ <div class="sdt__tl-other" id="sdt-tl-other"></div>
108
+ </div>
109
+ <div class="sdt__timeline-legend">
110
+ <span class="sdt__legend-item sdt__legend-item--work">
111
+ <span class="sdt__legend-dot"></span>
112
+ <span data-key="timelineWork">Trabajo</span>
113
+ </span>
114
+ <span class="sdt__legend-item sdt__legend-item--rest">
115
+ <span class="sdt__legend-dot"></span>
116
+ <span data-key="timelineRest">Descanso</span>
117
+ </span>
118
+ <span class="sdt__legend-item sdt__legend-item--other">
119
+ <span class="sdt__legend-dot"></span>
120
+ <span data-key="timelineOther">Otros</span>
121
+ </span>
122
+ </div>
123
+ </div>
124
+
125
+ </div>
126
+ </div>
127
+
128
+ <style>
129
+ .sdt {
130
+ --sdt-primary: #06b6d4;
131
+ --sdt-secondary: #0891b2;
132
+ --sdt-accent: #06d6a6;
133
+ --sdt-low: #10b981;
134
+ --sdt-mod: #f59e0b;
135
+ --sdt-high: #ef9311;
136
+ --sdt-critical: #dc2626;
137
+ --sdt-bg: #fff;
138
+ --sdt-border: rgba(6, 182, 212, 0.2);
139
+ --sdt-divider: rgba(6, 182, 212, 0.1);
140
+ --sdt-text: #0f172a;
141
+ --sdt-muted: #64748b;
142
+ --sdt-preset-bg: rgba(6, 182, 212, 0.07);
143
+ --sdt-preset-border: rgba(6, 182, 212, 0.18);
144
+ --sdt-input-bg: #ecfeff;
145
+ --sdt-input-border: rgba(6, 182, 212, 0.25);
146
+
147
+ max-width: 860px;
148
+ margin: 0 auto;
149
+ padding: 1rem;
150
+ }
151
+
152
+
153
+ .sdt__card {
154
+ background: var(--sdt-bg);
155
+ border: 2px solid var(--sdt-border);
156
+ border-radius: 2.5rem;
157
+ box-shadow: 0 20px 60px rgba(6, 182, 212, 0.12);
158
+ overflow: hidden;
159
+ }
160
+
161
+
162
+ .sdt__section {
163
+ padding: 2.5rem;
164
+ }
165
+
166
+ .sdt__section--bordered {
167
+ border-top: 1px solid var(--sdt-divider);
168
+ }
169
+
170
+ .sdt__section-title {
171
+ display: block;
172
+ font-size: 0.8rem;
173
+ font-weight: 800;
174
+ text-transform: uppercase;
175
+ letter-spacing: 3px;
176
+ color: var(--sdt-secondary);
177
+ margin: 0 0 1.5rem;
178
+ }
179
+
180
+
181
+ .sdt__slider-row {
182
+ display: flex;
183
+ align-items: center;
184
+ gap: 1.5rem;
185
+ margin-bottom: 1.25rem;
186
+ }
187
+
188
+ .sdt__slider {
189
+ flex: 1;
190
+ -webkit-appearance: none;
191
+ appearance: none;
192
+ height: 8px;
193
+ border-radius: 4px;
194
+ background: linear-gradient(90deg, var(--sdt-primary) 50%, rgba(6,182,212,0.15) 50%);
195
+ outline: none;
196
+ cursor: pointer;
197
+ }
198
+
199
+ .sdt__slider::-webkit-slider-thumb {
200
+ -webkit-appearance: none;
201
+ width: 26px;
202
+ height: 26px;
203
+ border-radius: 50%;
204
+ background: linear-gradient(135deg, var(--sdt-primary) 0%, var(--sdt-secondary) 100%);
205
+ cursor: pointer;
206
+ box-shadow: 0 3px 12px rgba(6, 182, 212, 0.45);
207
+ transition: transform 0.15s ease;
208
+ }
209
+
210
+ .sdt__slider::-webkit-slider-thumb:hover { transform: scale(1.15); }
211
+
212
+ .sdt__slider::-moz-range-thumb {
213
+ width: 26px; height: 26px;
214
+ border-radius: 50%; border: none;
215
+ background: linear-gradient(135deg, var(--sdt-primary) 0%, var(--sdt-secondary) 100%);
216
+ cursor: pointer;
217
+ box-shadow: 0 3px 12px rgba(6, 182, 212, 0.45);
218
+ }
219
+
220
+ .sdt__number {
221
+ width: 5rem;
222
+ background: var(--sdt-input-bg);
223
+ border: 2px solid var(--sdt-input-border);
224
+ border-radius: 0.875rem;
225
+ padding: 0.5rem 0.75rem;
226
+ font-size: 1.75rem;
227
+ font-weight: 900;
228
+ color: var(--sdt-primary);
229
+ text-align: center;
230
+ outline: none;
231
+ transition: border-color 0.2s ease;
232
+ }
233
+
234
+ .sdt__number:focus { border-color: var(--sdt-primary); box-shadow: 0 0 0 3px rgba(6,182,212,0.12); }
235
+ .sdt__number::-webkit-inner-spin-button,
236
+ .sdt__number::-webkit-outer-spin-button { -webkit-appearance: none; }
237
+ .sdt__number[type="number"] { -moz-appearance: textfield; }
238
+
239
+ .sdt__presets {
240
+ display: flex;
241
+ gap: 0.75rem;
242
+ }
243
+
244
+ .sdt__preset {
245
+ background: var(--sdt-preset-bg);
246
+ border: 1px solid var(--sdt-preset-border);
247
+ border-radius: 2rem;
248
+ padding: 0.45rem 1.25rem;
249
+ font-size: 0.85rem;
250
+ font-weight: 700;
251
+ color: var(--sdt-secondary);
252
+ cursor: pointer;
253
+ transition: all 0.2s ease;
254
+ }
255
+
256
+ .sdt__preset:hover, .sdt__preset--active {
257
+ background: linear-gradient(135deg, var(--sdt-primary) 0%, var(--sdt-secondary) 100%);
258
+ border-color: transparent;
259
+ color: #fff;
260
+ box-shadow: 0 4px 14px rgba(6, 182, 212, 0.3);
261
+ }
262
+
263
+
264
+ .sdt__section--result {
265
+ background: linear-gradient(135deg, var(--sdt-primary) 0%, var(--sdt-secondary) 100%);
266
+ }
267
+
268
+ .sdt__result-inner {
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 3rem;
272
+ }
273
+
274
+ .sdt__ring-wrap {
275
+ flex-shrink: 0;
276
+ display: flex;
277
+ flex-direction: column;
278
+ align-items: center;
279
+ gap: 0.75rem;
280
+ }
281
+
282
+ .sdt__ring {
283
+ width: 150px;
284
+ height: 150px;
285
+ border-radius: 50%;
286
+ background: conic-gradient(#10b981 75%, rgba(255,255,255,0.15) 0);
287
+ position: relative;
288
+ display: flex;
289
+ align-items: center;
290
+ justify-content: center;
291
+ transition: background 0.5s ease;
292
+ }
293
+
294
+ .sdt__ring-hole {
295
+ width: 102px;
296
+ height: 102px;
297
+ border-radius: 50%;
298
+ background: linear-gradient(135deg, var(--sdt-primary) 0%, var(--sdt-secondary) 100%);
299
+ position: absolute;
300
+ }
301
+
302
+ .sdt__fatigue-label {
303
+ font-size: 0.9rem;
304
+ font-weight: 900;
305
+ color: rgba(255, 255, 255, 0.9);
306
+ text-transform: uppercase;
307
+ letter-spacing: 2px;
308
+ }
309
+
310
+ .sdt__result-text {
311
+ display: flex;
312
+ flex-direction: column;
313
+ gap: 0.2rem;
314
+ }
315
+
316
+ .sdt__result-num {
317
+ font-size: 5rem;
318
+ font-weight: 950;
319
+ line-height: 1;
320
+ color: #fff;
321
+ text-shadow: 0 4px 16px rgba(0,0,0,0.12);
322
+ }
323
+
324
+ .sdt__result-unit {
325
+ font-size: 1.15rem;
326
+ font-weight: 700;
327
+ color: rgba(255,255,255,0.9);
328
+ }
329
+
330
+ .sdt__result-per {
331
+ font-size: 0.85rem;
332
+ font-weight: 600;
333
+ color: rgba(255,255,255,0.6);
334
+ }
335
+
336
+
337
+ .sdt__section--stats {
338
+ display: flex;
339
+ gap: 0;
340
+ }
341
+
342
+ .sdt__stat {
343
+ flex: 1;
344
+ text-align: center;
345
+ display: flex;
346
+ flex-direction: column;
347
+ align-items: center;
348
+ gap: 0.25rem;
349
+ padding: 0 1rem;
350
+ }
351
+
352
+ .sdt__stat + .sdt__stat {
353
+ border-left: 1px solid var(--sdt-divider);
354
+ }
355
+
356
+ .sdt__stat-icon {
357
+ color: var(--sdt-primary);
358
+ width: 1.6rem;
359
+ height: 1.6rem;
360
+ margin-bottom: 0.2rem;
361
+ }
362
+
363
+ .sdt__stat-icon svg { width: 100%; height: 100%; display: block; }
364
+
365
+ .sdt__stat-value {
366
+ font-size: 2rem;
367
+ font-weight: 900;
368
+ color: var(--sdt-primary);
369
+ line-height: 1;
370
+ }
371
+
372
+ .sdt__stat-unit {
373
+ font-size: 0.75rem;
374
+ font-weight: 700;
375
+ color: var(--sdt-muted);
376
+ }
377
+
378
+ .sdt__stat-label {
379
+ font-size: 0.7rem;
380
+ font-weight: 700;
381
+ text-transform: uppercase;
382
+ letter-spacing: 1px;
383
+ color: var(--sdt-muted);
384
+ margin-top: 0.15rem;
385
+ }
386
+
387
+
388
+ .sdt__section--recs {
389
+ display: grid;
390
+ grid-template-columns: repeat(2, 1fr);
391
+ gap: 1.5rem;
392
+ }
393
+
394
+ .sdt__rec {
395
+ display: flex;
396
+ gap: 0.875rem;
397
+ align-items: flex-start;
398
+ }
399
+
400
+ .sdt__rec-icon {
401
+ color: var(--sdt-primary);
402
+ width: 1.75rem;
403
+ height: 1.75rem;
404
+ flex-shrink: 0;
405
+ margin-top: 0.1rem;
406
+ }
407
+
408
+ .sdt__rec-icon svg { width: 100%; height: 100%; display: block; }
409
+
410
+ .sdt__rec-body { display: flex; flex-direction: column; gap: 0.4rem; }
411
+
412
+ .sdt__rec-title {
413
+ font-size: 0.78rem;
414
+ font-weight: 900;
415
+ text-transform: uppercase;
416
+ letter-spacing: 2px;
417
+ color: var(--sdt-primary);
418
+ }
419
+
420
+ .sdt__rec-text {
421
+ font-size: 0.875rem;
422
+ line-height: 1.55;
423
+ color: var(--sdt-muted);
424
+ margin: 0;
425
+ }
426
+
427
+
428
+ .sdt__timeline-bar {
429
+ height: 40px;
430
+ border-radius: 0.75rem;
431
+ overflow: hidden;
432
+ display: flex;
433
+ margin-bottom: 0.875rem;
434
+ background: rgba(0,0,0,0.04);
435
+ }
436
+
437
+ .sdt__tl-work {
438
+ background: linear-gradient(90deg, var(--sdt-primary) 0%, var(--sdt-secondary) 100%);
439
+ transition: width 0.55s cubic-bezier(0.34, 1.56, 0.64, 1);
440
+ height: 100%;
441
+ }
442
+
443
+ .sdt__tl-rest {
444
+ background: linear-gradient(90deg, var(--sdt-accent) 0%, #059669 100%);
445
+ transition: width 0.55s cubic-bezier(0.34, 1.56, 0.64, 1);
446
+ height: 100%;
447
+ }
448
+
449
+ .sdt__tl-other { flex: 1; height: 100%; background: rgba(0,0,0,0.04); }
450
+
451
+ .sdt__timeline-legend {
452
+ display: flex;
453
+ gap: 2rem;
454
+ flex-wrap: wrap;
455
+ }
456
+
457
+ .sdt__legend-item {
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 0.45rem;
461
+ font-size: 0.78rem;
462
+ font-weight: 700;
463
+ color: var(--sdt-muted);
464
+ }
465
+
466
+ .sdt__legend-dot {
467
+ width: 10px;
468
+ height: 10px;
469
+ border-radius: 2px;
470
+ }
471
+
472
+ .sdt__legend-item--work .sdt__legend-dot { background: linear-gradient(135deg, var(--sdt-primary), var(--sdt-secondary)); }
473
+ .sdt__legend-item--rest .sdt__legend-dot { background: linear-gradient(135deg, var(--sdt-accent), #059669); }
474
+ .sdt__legend-item--other .sdt__legend-dot { background: rgba(0,0,0,0.1); }
475
+
476
+
477
+ :global(.theme-dark) .sdt {
478
+ --sdt-bg: #0a2030;
479
+ --sdt-border: rgba(6, 182, 212, 0.28);
480
+ --sdt-divider: rgba(6, 182, 212, 0.1);
481
+ --sdt-text: #ecfeff;
482
+ --sdt-muted: #67e8f9;
483
+ --sdt-preset-bg: rgba(6, 182, 212, 0.1);
484
+ --sdt-preset-border: rgba(6, 182, 212, 0.25);
485
+ --sdt-input-bg: rgba(6, 182, 212, 0.1);
486
+ --sdt-input-border: rgba(6, 182, 212, 0.28);
487
+ }
488
+
489
+ :global(.theme-dark) .sdt__timeline-bar,
490
+ :global(.theme-dark) .sdt__tl-other {
491
+ background: rgba(255,255,255,0.04);
492
+ }
493
+
494
+ :global(.theme-dark) .sdt__legend-item--other .sdt__legend-dot { background: rgba(255,255,255,0.08); }
495
+ :global(.theme-dark) .sdt__number { color: #67e8f9; }
496
+ :global(.theme-dark) .sdt__stat + .sdt__stat { border-color: rgba(6,182,212,0.12); }
497
+
498
+
499
+ @media (max-width: 768px) {
500
+ .sdt__section { padding: 1.75rem 1.25rem; }
501
+
502
+ .sdt__result-inner { flex-direction: column; text-align: center; gap: 1.25rem; }
503
+ .sdt__result-num { font-size: 4rem; }
504
+
505
+ .sdt__section--recs { grid-template-columns: 1fr; }
506
+
507
+ .sdt__section--stats { flex-direction: column; gap: 1.25rem; }
508
+ .sdt__stat { flex-direction: row; justify-content: flex-start; gap: 0.75rem; padding: 0; }
509
+ .sdt__stat + .sdt__stat { border-left: none; border-top: 1px solid var(--sdt-divider); padding-top: 1.25rem; }
510
+ .sdt__stat-value { font-size: 1.5rem; }
511
+ }
512
+
513
+ @media (max-width: 480px) {
514
+ .sdt__ring { width: 120px; height: 120px; }
515
+ .sdt__ring-hole { width: 82px; height: 82px; }
516
+ }
517
+ </style>
518
+
519
+ <script>
520
+ interface SdtUI {
521
+ labelHours?: string;
522
+ presetLow?: string; presetNormal?: string; presetHigh?: string;
523
+ resultUnit?: string; resultPer?: string;
524
+ statInterval?: string; statBreaks?: string; statDistance?: string;
525
+ timelineTitle?: string; timelineWork?: string; timelineRest?: string; timelineOther?: string;
526
+ fatigueLow?: string; fatigueMod?: string; fatigueHigh?: string; fatigueCritical?: string;
527
+ recBreaksTitle?: string; recBreaksText?: string;
528
+ recPostureTitle?: string; recPostureLow?: string; recPostureMod?: string; recPostureHigh?: string;
529
+ recBlinkTitle?: string; recBlinkText?: string;
530
+ recLightTitle?: string; recLightLow?: string; recLightMod?: string; recLightHigh?: string;
531
+ unitMin?: string;
532
+ }
533
+
534
+ function applyUI(container: Element, ui: SdtUI): void {
535
+ container.querySelectorAll<HTMLElement>('[data-key]').forEach((el) => {
536
+ const key = el.dataset.key as keyof SdtUI;
537
+ const val = ui[key];
538
+ if (val) el.textContent = val;
539
+ });
540
+ }
541
+
542
+ function getFatigueState(hours: number, ui: SdtUI): { pct: number; label: string; color: string } {
543
+ const states = [
544
+ { threshold: 0, pct: 0, label: ui.fatigueLow ?? 'Bajo', color: '#10b981' },
545
+ { threshold: 2, pct: 25, label: ui.fatigueLow ?? 'Bajo', color: '#10b981' },
546
+ { threshold: 4, pct: 50, label: ui.fatigueLow ?? 'Bajo', color: '#10b981' },
547
+ { threshold: 8, pct: 75, label: ui.fatigueMod ?? 'Moderado', color: '#f59e0b' },
548
+ { threshold: 12, pct: 95, label: ui.fatigueHigh ?? 'Alto', color: '#ef9311' },
549
+ ];
550
+ const state = states.reverse().find(s => hours >= s.threshold);
551
+ return state || { pct: 100, label: ui.fatigueCritical ?? 'Crítico', color: '#dc2626' };
552
+ }
553
+
554
+ function getDistance(hours: number): number {
555
+ if (hours <= 4) return 60;
556
+ if (hours <= 8) return 75;
557
+ return 90;
558
+ }
559
+
560
+ function getBlinks(hours: number): number {
561
+ if (hours <= 4) return 15;
562
+ if (hours <= 8) return 20;
563
+ return 25;
564
+ }
565
+
566
+ function getPosture(hours: number, ui: SdtUI): string {
567
+ if (hours <= 4) return ui.recPostureLow ?? '';
568
+ if (hours <= 8) return ui.recPostureMod ?? '';
569
+ return ui.recPostureHigh ?? '';
570
+ }
571
+
572
+ function getLight(hours: number, ui: SdtUI): string {
573
+ if (hours <= 4) return ui.recLightLow ?? '';
574
+ if (hours <= 8) return ui.recLightMod ?? '';
575
+ return ui.recLightHigh ?? '';
576
+ }
577
+
578
+ function calcState(hours: number, ui: SdtUI) {
579
+ const restMinutes = Math.round(hours * 2);
580
+ const breaks = Math.ceil((hours * 60) / 20);
581
+ const interval = hours === 0 ? 20 : Math.max(15, Math.round(120 / Math.max(1, hours / 4)));
582
+ const distance = getDistance(hours);
583
+ const blinks = getBlinks(hours);
584
+ const fatigue = getFatigueState(hours, ui);
585
+ const posture = getPosture(hours, ui);
586
+ const light = getLight(hours, ui);
587
+
588
+ return {
589
+ restMinutes, breaks, interval, distance, blinks,
590
+ fatiguePct: fatigue.pct,
591
+ fatigueLabel: fatigue.label,
592
+ fatigueColor: fatigue.color,
593
+ posture, light
594
+ };
595
+ }
596
+
597
+ // eslint-disable-next-line complexity, max-lines-per-function
598
+ function updateDOM(container: Element, hours: number, s: ReturnType<typeof calcState>, ui: SdtUI): void {
599
+ const get = (id: string) => container.querySelector<HTMLElement>(id);
600
+
601
+ const minutesEl = get('#sdt-minutes');
602
+ const intervalEl = get('#sdt-interval');
603
+ const breaksEl = get('#sdt-breaks');
604
+ const distanceEl = get('#sdt-distance');
605
+ const ringEl = get('#sdt-ring');
606
+ const fatigueEl = get('#sdt-fatigue');
607
+ const recBreaksEl = get('#sdt-rec-breaks');
608
+ const recPostureEl= get('#sdt-rec-posture');
609
+ const recBlinkEl = get('#sdt-rec-blink');
610
+ const recLightEl = get('#sdt-rec-light');
611
+ const workEl = get('#sdt-tl-work');
612
+ const restEl = get('#sdt-tl-rest');
613
+ const sliderEl = container.querySelector<HTMLInputElement>('#sdt-slider');
614
+
615
+ if (minutesEl) minutesEl.textContent = String(s.restMinutes);
616
+ if (intervalEl) intervalEl.textContent = String(s.interval);
617
+ if (breaksEl) breaksEl.textContent = String(s.breaks);
618
+ if (distanceEl) distanceEl.textContent = String(s.distance);
619
+ if (fatigueEl) fatigueEl.textContent = s.fatigueLabel;
620
+
621
+ if (ringEl) ringEl.style.background = `conic-gradient(${s.fatigueColor} ${s.fatiguePct}%, rgba(255,255,255,0.15) 0)`;
622
+
623
+ if (recBreaksEl) recBreaksEl.textContent = (ui.recBreaksText ?? '').replace('{0}', String(s.interval));
624
+ if (recPostureEl) recPostureEl.textContent = s.posture;
625
+ if (recBlinkEl) recBlinkEl.textContent = (ui.recBlinkText ?? '').replace('{0}', String(s.blinks));
626
+ if (recLightEl) recLightEl.textContent = s.light;
627
+
628
+ const workPct = (hours / 24) * 100;
629
+ const restPct = (s.restMinutes / 60 / 24) * 100;
630
+ if (workEl) workEl.style.width = `${Math.min(workPct, 100)}%`;
631
+ if (restEl) restEl.style.width = `${Math.min(restPct, 100 - workPct)}%`;
632
+
633
+ if (sliderEl) {
634
+ const pct = (hours / 16) * 100;
635
+ sliderEl.style.background = `linear-gradient(90deg, ${s.fatigueColor} ${pct}%, rgba(6,182,212,0.15) ${pct}%)`;
636
+ }
637
+ }
638
+
639
+ function syncPresets(container: Element, hours: number): void {
640
+ container.querySelectorAll<HTMLElement>('.sdt__preset').forEach((btn) => {
641
+ btn.classList.toggle('sdt__preset--active', parseFloat(btn.dataset.val ?? '0') === hours);
642
+ });
643
+ }
644
+
645
+ function initSdt(container: Element): void {
646
+ const ui: SdtUI = JSON.parse((container as HTMLElement).dataset.ui ?? '{}');
647
+ applyUI(container, ui);
648
+
649
+ const slider = container.querySelector<HTMLInputElement>('#sdt-slider');
650
+ const number = container.querySelector<HTMLInputElement>('#sdt-number');
651
+ let currentHours = 8;
652
+
653
+ function update(hours: number): void {
654
+ currentHours = Math.max(0, Math.min(24, hours));
655
+ updateDOM(container, currentHours, calcState(currentHours, ui), ui);
656
+ syncPresets(container, currentHours);
657
+ if (slider && parseFloat(slider.value) !== currentHours) slider.value = String(Math.min(currentHours, 16));
658
+ if (number && parseFloat(number.value) !== currentHours) number.value = String(currentHours);
659
+ }
660
+
661
+ slider?.addEventListener('input', () => update(parseFloat(slider.value)));
662
+ number?.addEventListener('input', () => { const v = parseFloat(number.value); if (!isNaN(v)) update(v); });
663
+ container.querySelectorAll<HTMLButtonElement>('.sdt__preset').forEach((btn) => {
664
+ btn.addEventListener('click', () => update(parseFloat(btn.dataset.val ?? '0')));
665
+ });
666
+
667
+ update(currentHours);
668
+ }
669
+
670
+ document.querySelectorAll<HTMLElement>('.sdt').forEach(initSdt);
671
+ </script>