@jjlmoya/utils-tools 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 (134) hide show
  1. package/package.json +63 -0
  2. package/src/category/i18n/en.ts +172 -0
  3. package/src/category/i18n/es.ts +172 -0
  4. package/src/category/i18n/fr.ts +172 -0
  5. package/src/category/index.ts +23 -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 +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +90 -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 +23 -0
  22. package/src/tests/title_quality.test.ts +56 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/date-diff-calculator/bibliography.astro +14 -0
  25. package/src/tool/date-diff-calculator/component.astro +370 -0
  26. package/src/tool/date-diff-calculator/i18n/en.ts +132 -0
  27. package/src/tool/date-diff-calculator/i18n/es.ts +132 -0
  28. package/src/tool/date-diff-calculator/i18n/fr.ts +132 -0
  29. package/src/tool/date-diff-calculator/index.ts +22 -0
  30. package/src/tool/date-diff-calculator/seo.astro +14 -0
  31. package/src/tool/date-diff-calculator/ui.ts +17 -0
  32. package/src/tool/drive-direct-link/bibliography.astro +14 -0
  33. package/src/tool/drive-direct-link/component.astro +280 -0
  34. package/src/tool/drive-direct-link/i18n/en.ts +118 -0
  35. package/src/tool/drive-direct-link/i18n/es.ts +118 -0
  36. package/src/tool/drive-direct-link/i18n/fr.ts +118 -0
  37. package/src/tool/drive-direct-link/index.ts +22 -0
  38. package/src/tool/drive-direct-link/seo.astro +14 -0
  39. package/src/tool/drive-direct-link/ui.ts +10 -0
  40. package/src/tool/email-list-cleaner/bibliography.astro +14 -0
  41. package/src/tool/email-list-cleaner/component.astro +375 -0
  42. package/src/tool/email-list-cleaner/i18n/en.ts +140 -0
  43. package/src/tool/email-list-cleaner/i18n/es.ts +140 -0
  44. package/src/tool/email-list-cleaner/i18n/fr.ts +140 -0
  45. package/src/tool/email-list-cleaner/index.ts +22 -0
  46. package/src/tool/email-list-cleaner/seo.astro +14 -0
  47. package/src/tool/email-list-cleaner/ui.ts +15 -0
  48. package/src/tool/env-badge-spain/bibliography.astro +14 -0
  49. package/src/tool/env-badge-spain/component.astro +303 -0
  50. package/src/tool/env-badge-spain/components/BadgeForm.astro +243 -0
  51. package/src/tool/env-badge-spain/components/BadgeResult.astro +151 -0
  52. package/src/tool/env-badge-spain/i18n/en.ts +153 -0
  53. package/src/tool/env-badge-spain/i18n/es.ts +153 -0
  54. package/src/tool/env-badge-spain/i18n/fr.ts +153 -0
  55. package/src/tool/env-badge-spain/index.ts +22 -0
  56. package/src/tool/env-badge-spain/seo.astro +14 -0
  57. package/src/tool/env-badge-spain/ui.ts +53 -0
  58. package/src/tool/morse-beacon/bibliography.astro +14 -0
  59. package/src/tool/morse-beacon/component.astro +534 -0
  60. package/src/tool/morse-beacon/i18n/en.ts +157 -0
  61. package/src/tool/morse-beacon/i18n/es.ts +157 -0
  62. package/src/tool/morse-beacon/i18n/fr.ts +157 -0
  63. package/src/tool/morse-beacon/index.ts +22 -0
  64. package/src/tool/morse-beacon/logic/MorseEngine.ts +124 -0
  65. package/src/tool/morse-beacon/seo.astro +14 -0
  66. package/src/tool/morse-beacon/ui.ts +18 -0
  67. package/src/tool/password-generator/bibliography.astro +14 -0
  68. package/src/tool/password-generator/component.astro +259 -0
  69. package/src/tool/password-generator/components/Config.astro +227 -0
  70. package/src/tool/password-generator/components/Display.astro +147 -0
  71. package/src/tool/password-generator/components/Strength.astro +70 -0
  72. package/src/tool/password-generator/i18n/en.ts +166 -0
  73. package/src/tool/password-generator/i18n/es.ts +166 -0
  74. package/src/tool/password-generator/i18n/fr.ts +166 -0
  75. package/src/tool/password-generator/index.ts +22 -0
  76. package/src/tool/password-generator/seo.astro +14 -0
  77. package/src/tool/password-generator/ui.ts +16 -0
  78. package/src/tool/routes/bibliography.astro +14 -0
  79. package/src/tool/routes/component.astro +543 -0
  80. package/src/tool/routes/i18n/en.ts +157 -0
  81. package/src/tool/routes/i18n/es.ts +157 -0
  82. package/src/tool/routes/i18n/fr.ts +157 -0
  83. package/src/tool/routes/index.ts +22 -0
  84. package/src/tool/routes/logic/GeocodingService.ts +60 -0
  85. package/src/tool/routes/logic/RouteManager.ts +192 -0
  86. package/src/tool/routes/logic/RouteService.ts +66 -0
  87. package/src/tool/routes/seo.astro +14 -0
  88. package/src/tool/routes/ui.ts +16 -0
  89. package/src/tool/rule-of-three/bibliography.astro +14 -0
  90. package/src/tool/rule-of-three/component.astro +369 -0
  91. package/src/tool/rule-of-three/i18n/en.ts +171 -0
  92. package/src/tool/rule-of-three/i18n/es.ts +171 -0
  93. package/src/tool/rule-of-three/i18n/fr.ts +171 -0
  94. package/src/tool/rule-of-three/index.ts +22 -0
  95. package/src/tool/rule-of-three/seo.astro +14 -0
  96. package/src/tool/rule-of-three/ui.ts +13 -0
  97. package/src/tool/seo-content-optimizer/bibliography.astro +14 -0
  98. package/src/tool/seo-content-optimizer/component.astro +552 -0
  99. package/src/tool/seo-content-optimizer/i18n/en.ts +136 -0
  100. package/src/tool/seo-content-optimizer/i18n/es.ts +136 -0
  101. package/src/tool/seo-content-optimizer/i18n/fr.ts +136 -0
  102. package/src/tool/seo-content-optimizer/index.ts +22 -0
  103. package/src/tool/seo-content-optimizer/seo.astro +14 -0
  104. package/src/tool/seo-content-optimizer/ui.ts +29 -0
  105. package/src/tool/speed-reader/bibliography.astro +14 -0
  106. package/src/tool/speed-reader/component.astro +586 -0
  107. package/src/tool/speed-reader/i18n/en.ts +152 -0
  108. package/src/tool/speed-reader/i18n/es.ts +152 -0
  109. package/src/tool/speed-reader/i18n/fr.ts +152 -0
  110. package/src/tool/speed-reader/index.ts +22 -0
  111. package/src/tool/speed-reader/logic/RSVPEngine.ts +106 -0
  112. package/src/tool/speed-reader/seo.astro +14 -0
  113. package/src/tool/speed-reader/ui.ts +14 -0
  114. package/src/tool/text-pixel-calculator/bibliography.astro +14 -0
  115. package/src/tool/text-pixel-calculator/component.astro +315 -0
  116. package/src/tool/text-pixel-calculator/components/Editor.astro +240 -0
  117. package/src/tool/text-pixel-calculator/components/Preview.astro +155 -0
  118. package/src/tool/text-pixel-calculator/components/Stats.astro +87 -0
  119. package/src/tool/text-pixel-calculator/i18n/en.ts +133 -0
  120. package/src/tool/text-pixel-calculator/i18n/es.ts +133 -0
  121. package/src/tool/text-pixel-calculator/i18n/fr.ts +133 -0
  122. package/src/tool/text-pixel-calculator/index.ts +22 -0
  123. package/src/tool/text-pixel-calculator/seo.astro +14 -0
  124. package/src/tool/text-pixel-calculator/ui.ts +15 -0
  125. package/src/tool/whatsapp-link/bibliography.astro +14 -0
  126. package/src/tool/whatsapp-link/component.astro +455 -0
  127. package/src/tool/whatsapp-link/i18n/en.ts +128 -0
  128. package/src/tool/whatsapp-link/i18n/es.ts +128 -0
  129. package/src/tool/whatsapp-link/i18n/fr.ts +128 -0
  130. package/src/tool/whatsapp-link/index.ts +22 -0
  131. package/src/tool/whatsapp-link/seo.astro +14 -0
  132. package/src/tool/whatsapp-link/ui.ts +15 -0
  133. package/src/tools.ts +15 -0
  134. package/src/types.ts +72 -0
@@ -0,0 +1,586 @@
1
+ ---
2
+ import type { SpeedReaderUI } from './ui';
3
+
4
+ interface Props {
5
+ ui?: Record<string, unknown>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ const t = (ui ?? {}) as SpeedReaderUI;
10
+ ---
11
+
12
+ <div class="sr-root" data-ui={JSON.stringify(t)}>
13
+ <div class="sr-stage" id="sr-stage">
14
+ <div class="sr-badge">
15
+ <span id="sr-wpm-display">300</span>
16
+ <span class="sr-badge-label">PPM</span>
17
+ <span class="sr-led" id="sr-led"></span>
18
+ </div>
19
+
20
+ <div class="sr-guide-v"></div>
21
+ <div class="sr-guide-h"></div>
22
+
23
+ <div class="sr-word" id="sr-word">
24
+ <div class="sr-word-left"><span id="sr-word-left"></span></div>
25
+ <div class="sr-word-pivot">
26
+ <span id="sr-word-pivot">-</span>
27
+ <div class="sr-pivot-beam"></div>
28
+ </div>
29
+ <div class="sr-word-right"><span id="sr-word-right"></span></div>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="sr-panel">
34
+ <div class="sr-playrow">
35
+ <button id="sr-play-btn" class="sr-play-btn" aria-label={t.play}>
36
+ <svg class="sr-play-icon" id="sr-play-icon" viewBox="0 0 24 24" fill="currentColor">
37
+ <path d="M8 5L19 12L8 19V5Z"/>
38
+ </svg>
39
+ </button>
40
+
41
+ <div class="sr-progress-wrap">
42
+ <div class="sr-progress-header">
43
+ <span class="sr-progress-label">{t.progressLabel}</span>
44
+ <span class="sr-progress-pct" id="sr-progress-text">0%</span>
45
+ </div>
46
+ <input type="range" id="sr-progress" class="sr-slider" min="0" max="100" value="0" />
47
+ </div>
48
+ </div>
49
+
50
+ <div class="sr-config">
51
+ <div class="sr-speed-section">
52
+ <div class="sr-speed-header">
53
+ <label class="sr-config-label" for="sr-wpm">{t.speedLabel}</label>
54
+ <span class="sr-wpm-badge" id="sr-wpm-badge">300 PPM</span>
55
+ </div>
56
+ <input type="range" id="sr-wpm" class="sr-slider" min="200" max="1000" step="50" value="300" />
57
+ <div class="sr-speed-labels">
58
+ <span>{t.slowLabel}</span>
59
+ <span>{t.fastLabel}</span>
60
+ </div>
61
+ <p class="sr-speed-hint">{t.speedHint}</p>
62
+ </div>
63
+
64
+ <div class="sr-text-section">
65
+ <label class="sr-config-label" for="sr-text">{t.textLabel}</label>
66
+ <div class="sr-textarea-wrap">
67
+ <textarea id="sr-text" class="sr-textarea" placeholder={t.textPlaceholder}></textarea>
68
+ <div class="sr-textarea-footer">
69
+ <span class="sr-word-count" id="sr-word-count">0 {t.words}</span>
70
+ <button id="sr-clear" class="sr-clear-btn">{t.clearLabel}</button>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ <style>
79
+ .sr-root {
80
+ --sr-accent: #4f46e5;
81
+ --sr-accent-hover: #4338ca;
82
+ --sr-violet: #7c3aed;
83
+ --sr-pivot-color: #ef4444;
84
+ --sr-panel-bg: #fff;
85
+ --sr-panel-border: #f1f5f9;
86
+ --sr-field-bg: #f8fafc;
87
+ --sr-field-border: #e2e8f0;
88
+ --sr-field-focus: #4f46e5;
89
+ --sr-text-main: #1e293b;
90
+ --sr-text-label: #64748b;
91
+ --sr-text-hint: #94a3b8;
92
+ --sr-text-muted: #475569;
93
+ --sr-btn-shadow: rgba(79, 70, 229, 0.35);
94
+ --sr-badge-bg: rgba(15, 23, 42, 0.8);
95
+ --sr-badge-border: #334155;
96
+ --sr-led-idle: #22c55e;
97
+ --sr-led-active: #f59e0b;
98
+ --sr-guide-color: rgba(71, 85, 105, 0.3);
99
+ --sr-word-color: #e2e8f0;
100
+ --sr-word-muted: #475569;
101
+ --sr-clear-color: #f87171;
102
+ --sr-clear-hover: #fee2e2;
103
+
104
+ width: 100%;
105
+ max-width: 56rem;
106
+ margin: 0 auto;
107
+ display: flex;
108
+ flex-direction: column;
109
+ gap: 1rem;
110
+ }
111
+
112
+ :global(.theme-dark) .sr-root {
113
+ --sr-panel-bg: #0f172a;
114
+ --sr-panel-border: #1e293b;
115
+ --sr-field-bg: rgba(30, 41, 59, 0.5);
116
+ --sr-field-border: #334155;
117
+ --sr-text-main: #e2e8f0;
118
+ --sr-text-label: #94a3b8;
119
+ --sr-text-hint: #64748b;
120
+ --sr-text-muted: #64748b;
121
+ --sr-word-muted: #64748b;
122
+ }
123
+
124
+ .sr-stage {
125
+ position: relative;
126
+ background: #0f172a;
127
+ border-radius: 1.5rem;
128
+ height: 22rem;
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ overflow: hidden;
133
+ border: 1px solid #1e293b;
134
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
135
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
136
+ }
137
+
138
+ :global(.sr-stage-active) {
139
+ transform: scale(1.02);
140
+ border-color: rgba(79, 70, 229, 0.4);
141
+ }
142
+
143
+ .sr-badge {
144
+ position: absolute;
145
+ top: 1.25rem;
146
+ right: 1.25rem;
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 0.375rem;
150
+ background: var(--sr-badge-bg);
151
+ border: 1px solid var(--sr-badge-border);
152
+ border-radius: 9999px;
153
+ padding: 0.25rem 0.75rem;
154
+ font-size: 0.75rem;
155
+ color: #cbd5e1;
156
+ z-index: 20;
157
+ backdrop-filter: blur(4px);
158
+ }
159
+
160
+ .sr-badge > span:first-child {
161
+ font-weight: 700;
162
+ color: #f1f5f9;
163
+ }
164
+
165
+ .sr-badge-label {
166
+ color: #64748b;
167
+ text-transform: uppercase;
168
+ letter-spacing: 0.05em;
169
+ }
170
+
171
+ .sr-led {
172
+ width: 0.5rem;
173
+ height: 0.5rem;
174
+ border-radius: 50%;
175
+ background: var(--sr-led-idle);
176
+ transition: background 0.2s;
177
+ }
178
+
179
+ :global(.sr-led-active) {
180
+ background: var(--sr-led-active);
181
+ animation: sr-pulse 1s ease-in-out infinite;
182
+ }
183
+
184
+ @keyframes sr-pulse {
185
+ 0%, 100% { opacity: 1; }
186
+ 50% { opacity: 0.5; }
187
+ }
188
+
189
+ .sr-guide-v {
190
+ position: absolute;
191
+ top: 0;
192
+ bottom: 0;
193
+ left: 50%;
194
+ width: 1px;
195
+ background: rgba(71, 85, 105, 0.25);
196
+ transform: translateX(-50%);
197
+ pointer-events: none;
198
+ }
199
+
200
+ .sr-guide-h {
201
+ position: absolute;
202
+ top: 50%;
203
+ left: 50%;
204
+ width: 18rem;
205
+ height: 4.5rem;
206
+ transform: translate(-50%, -60%);
207
+ border-top: 1px solid rgba(71, 85, 105, 0.2);
208
+ border-bottom: 1px solid rgba(71, 85, 105, 0.2);
209
+ border-radius: 0.5rem;
210
+ pointer-events: none;
211
+ }
212
+
213
+ .sr-word {
214
+ position: relative;
215
+ z-index: 10;
216
+ display: grid;
217
+ grid-template-columns: 1fr auto 1fr;
218
+ align-items: center;
219
+ width: 100%;
220
+ max-width: 36rem;
221
+ padding: 0 2rem;
222
+ font-size: clamp(2rem, 6vw, 3.5rem);
223
+ font-weight: 700;
224
+ user-select: none;
225
+ }
226
+
227
+ .sr-word-left {
228
+ text-align: right;
229
+ color: var(--sr-word-muted);
230
+ white-space: nowrap;
231
+ }
232
+
233
+ .sr-word-pivot {
234
+ position: relative;
235
+ display: flex;
236
+ justify-content: center;
237
+ flex-shrink: 0;
238
+ min-width: 1ch;
239
+ margin: 0 1px;
240
+ }
241
+
242
+ .sr-word-pivot > span {
243
+ color: var(--sr-pivot-color);
244
+ font-weight: 900;
245
+ position: relative;
246
+ z-index: 2;
247
+ }
248
+
249
+ .sr-pivot-beam {
250
+ position: absolute;
251
+ top: 50%;
252
+ left: 50%;
253
+ transform: translate(-50%, -50%);
254
+ width: 2px;
255
+ height: 4rem;
256
+ background: rgba(239, 68, 68, 0.15);
257
+ border-radius: 9999px;
258
+ }
259
+
260
+ .sr-word-right {
261
+ text-align: left;
262
+ color: var(--sr-word-muted);
263
+ white-space: nowrap;
264
+ }
265
+
266
+ .sr-panel {
267
+ background: var(--sr-panel-bg);
268
+ border: 1px solid var(--sr-panel-border);
269
+ border-radius: 1.5rem;
270
+ padding: 1.5rem;
271
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.04);
272
+ display: flex;
273
+ flex-direction: column;
274
+ gap: 1.5rem;
275
+ }
276
+
277
+ .sr-playrow {
278
+ display: flex;
279
+ align-items: center;
280
+ gap: 1.25rem;
281
+ }
282
+
283
+ .sr-play-btn {
284
+ flex-shrink: 0;
285
+ width: 3.75rem;
286
+ height: 3.75rem;
287
+ background: var(--sr-accent);
288
+ color: #fff;
289
+ border: none;
290
+ border-radius: 1rem;
291
+ display: flex;
292
+ align-items: center;
293
+ justify-content: center;
294
+ cursor: pointer;
295
+ box-shadow: 0 4px 14px var(--sr-btn-shadow);
296
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
297
+ }
298
+
299
+ .sr-play-btn:hover {
300
+ background: var(--sr-accent-hover);
301
+ transform: scale(1.05);
302
+ }
303
+
304
+ .sr-play-btn:active {
305
+ transform: scale(0.95);
306
+ }
307
+
308
+ :global(.sr-play-btn-active) {
309
+ background: #1e293b;
310
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
311
+ }
312
+
313
+ :global(.sr-play-btn-active):hover {
314
+ background: #0f172a;
315
+ }
316
+
317
+ .sr-play-icon {
318
+ width: 1.75rem;
319
+ height: 1.75rem;
320
+ }
321
+
322
+ .sr-progress-wrap {
323
+ flex: 1;
324
+ display: flex;
325
+ flex-direction: column;
326
+ gap: 0.5rem;
327
+ }
328
+
329
+ .sr-progress-header {
330
+ display: flex;
331
+ justify-content: space-between;
332
+ align-items: center;
333
+ font-size: 0.7rem;
334
+ font-weight: 700;
335
+ text-transform: uppercase;
336
+ letter-spacing: 0.08em;
337
+ color: var(--sr-text-hint);
338
+ padding: 0 0.25rem;
339
+ }
340
+
341
+ .sr-progress-pct {
342
+ color: var(--sr-accent);
343
+ }
344
+
345
+ .sr-slider {
346
+ width: 100%;
347
+ cursor: pointer;
348
+ accent-color: var(--sr-accent);
349
+ }
350
+
351
+ .sr-config {
352
+ display: grid;
353
+ grid-template-columns: 1fr;
354
+ gap: 1.5rem;
355
+ border-top: 1px solid var(--sr-panel-border);
356
+ padding-top: 1.5rem;
357
+ }
358
+
359
+ @media (min-width: 640px) {
360
+ .sr-config { grid-template-columns: 1fr 1fr; }
361
+ }
362
+
363
+ .sr-config-label {
364
+ display: block;
365
+ font-size: 1rem;
366
+ font-weight: 900;
367
+ color: var(--sr-text-main);
368
+ }
369
+
370
+ .sr-speed-section {
371
+ display: flex;
372
+ flex-direction: column;
373
+ gap: 0.75rem;
374
+ }
375
+
376
+ .sr-speed-header {
377
+ display: flex;
378
+ justify-content: space-between;
379
+ align-items: center;
380
+ }
381
+
382
+ .sr-wpm-badge {
383
+ background: rgba(79, 70, 229, 0.08);
384
+ color: var(--sr-accent);
385
+ border: 1px solid rgba(79, 70, 229, 0.2);
386
+ border-radius: 9999px;
387
+ padding: 0.2rem 0.75rem;
388
+ font-size: 0.8rem;
389
+ font-weight: 700;
390
+ }
391
+
392
+ .sr-speed-labels {
393
+ display: flex;
394
+ justify-content: space-between;
395
+ font-size: 0.7rem;
396
+ font-weight: 700;
397
+ color: var(--sr-text-hint);
398
+ letter-spacing: 0.08em;
399
+ }
400
+
401
+ .sr-speed-hint {
402
+ font-size: 0.8rem;
403
+ color: var(--sr-text-hint);
404
+ line-height: 1.5;
405
+ margin: 0;
406
+ }
407
+
408
+ .sr-text-section {
409
+ display: flex;
410
+ flex-direction: column;
411
+ gap: 0.5rem;
412
+ }
413
+
414
+ .sr-textarea-wrap {
415
+ position: relative;
416
+ }
417
+
418
+ .sr-textarea {
419
+ width: 100%;
420
+ height: 9rem;
421
+ background: var(--sr-field-bg);
422
+ border: 1px solid var(--sr-field-border);
423
+ border-radius: 0.75rem;
424
+ padding: 0.875rem 1rem 2.5rem;
425
+ font-size: 0.85rem;
426
+ color: var(--sr-text-main);
427
+ line-height: 1.6;
428
+ resize: none;
429
+ outline: none;
430
+ transition: border-color 0.2s;
431
+ box-sizing: border-box;
432
+ }
433
+
434
+ .sr-textarea::placeholder {
435
+ color: var(--sr-text-hint);
436
+ }
437
+
438
+ .sr-textarea:focus {
439
+ border-color: var(--sr-field-focus);
440
+ }
441
+
442
+ .sr-textarea-footer {
443
+ position: absolute;
444
+ bottom: 0.625rem;
445
+ right: 0.625rem;
446
+ display: flex;
447
+ align-items: center;
448
+ gap: 0.5rem;
449
+ }
450
+
451
+ .sr-word-count {
452
+ font-size: 0.7rem;
453
+ font-weight: 700;
454
+ color: var(--sr-text-hint);
455
+ background: var(--sr-panel-bg);
456
+ border: 1px solid var(--sr-panel-border);
457
+ padding: 0.2rem 0.5rem;
458
+ border-radius: 0.375rem;
459
+ }
460
+
461
+ .sr-clear-btn {
462
+ font-size: 0.7rem;
463
+ font-weight: 700;
464
+ color: var(--sr-clear-color);
465
+ background: transparent;
466
+ border: none;
467
+ cursor: pointer;
468
+ padding: 0.2rem 0.375rem;
469
+ border-radius: 0.375rem;
470
+ transition: background 0.15s;
471
+ letter-spacing: 0.05em;
472
+ }
473
+
474
+ .sr-clear-btn:hover {
475
+ background: var(--sr-clear-hover);
476
+ }
477
+ </style>
478
+
479
+ <script>
480
+ import { RSVPEngine } from './logic/RSVPEngine';
481
+ import type { RSVPWord } from './logic/RSVPEngine';
482
+ import type { SpeedReaderUI } from './ui';
483
+
484
+ const root = document.querySelector('.sr-root') as HTMLElement;
485
+ const t = JSON.parse(root?.dataset.ui ?? '{}') as SpeedReaderUI;
486
+
487
+ const stage = document.getElementById('sr-stage')!;
488
+ const wpmDisplay = document.getElementById('sr-wpm-display')!;
489
+ const led = document.getElementById('sr-led')!;
490
+ const wordLeft = document.getElementById('sr-word-left')!;
491
+ const wordPivot = document.getElementById('sr-word-pivot')!;
492
+ const wordRight = document.getElementById('sr-word-right')!;
493
+ const playBtn = document.getElementById('sr-play-btn') as HTMLButtonElement;
494
+ const playIcon = document.getElementById('sr-play-icon')!;
495
+ const progressSlider = document.getElementById('sr-progress') as HTMLInputElement;
496
+ const progressText = document.getElementById('sr-progress-text')!;
497
+ const wpmSlider = document.getElementById('sr-wpm') as HTMLInputElement;
498
+ const wpmBadge = document.getElementById('sr-wpm-badge')!;
499
+ const textInput = document.getElementById('sr-text') as HTMLTextAreaElement;
500
+ const wordCountEl = document.getElementById('sr-word-count')!;
501
+ const clearBtn = document.getElementById('sr-clear')!;
502
+
503
+ const ICON_PLAY = '<path d="M8 5L19 12L8 19V5Z"/>';
504
+ const ICON_PAUSE = '<path d="M6 5H10V19H6ZM14 5H18V19H14Z"/>';
505
+
506
+ let playing = false;
507
+ let engine: RSVPEngine;
508
+
509
+ function onWordUpdate(word: RSVPWord) {
510
+ wordLeft.textContent = word.left;
511
+ wordPivot.textContent = word.pivot;
512
+ wordRight.textContent = word.right;
513
+ const p = engine.getProgress();
514
+ progressSlider.value = String(p);
515
+ progressText.textContent = Math.round(p) + '%';
516
+ }
517
+
518
+ function onComplete() {
519
+ setPlay(false, false);
520
+ }
521
+
522
+ function setPlay(on: boolean, rewind = true) {
523
+ playing = on;
524
+ if (on) {
525
+ engine.play();
526
+ playIcon.innerHTML = ICON_PAUSE;
527
+ playBtn.setAttribute('aria-label', t.pause);
528
+ playBtn.classList.add('sr-play-btn-active');
529
+ stage.classList.add('sr-stage-active');
530
+ led.classList.add('sr-led-active');
531
+ } else {
532
+ engine.pause(rewind);
533
+ playIcon.innerHTML = ICON_PLAY;
534
+ playBtn.setAttribute('aria-label', t.play);
535
+ playBtn.classList.remove('sr-play-btn-active');
536
+ stage.classList.remove('sr-stage-active');
537
+ led.classList.remove('sr-led-active');
538
+ }
539
+ }
540
+
541
+ function updateWordCount() {
542
+ const count = textInput.value.trim().split(/\s+/).filter((w) => w.length > 0).length;
543
+ wordCountEl.textContent = `${count} ${t.words}`;
544
+ }
545
+
546
+ function init() {
547
+ textInput.value = t.defaultText ?? '';
548
+ engine = new RSVPEngine(onWordUpdate, onComplete);
549
+ engine.setText(t.defaultText ?? '');
550
+ engine.seek(0);
551
+ updateWordCount();
552
+ }
553
+
554
+ playBtn.addEventListener('click', () => { setPlay(!playing); });
555
+
556
+ wpmSlider.addEventListener('input', () => {
557
+ const val = parseInt(wpmSlider.value);
558
+ wpmBadge.textContent = `${val} PPM`;
559
+ wpmDisplay.textContent = String(val);
560
+ engine.setWPM(val);
561
+ });
562
+
563
+ textInput.addEventListener('input', () => {
564
+ engine.setText(textInput.value);
565
+ updateWordCount();
566
+ if (playing) setPlay(false);
567
+ });
568
+
569
+ progressSlider.addEventListener('input', () => {
570
+ const val = parseFloat(progressSlider.value);
571
+ engine.seek(val);
572
+ progressText.textContent = Math.round(val) + '%';
573
+ });
574
+
575
+ clearBtn.addEventListener('click', () => {
576
+ textInput.value = '';
577
+ engine.setText('');
578
+ setPlay(false, false);
579
+ wordLeft.textContent = '';
580
+ wordPivot.textContent = '-';
581
+ wordRight.textContent = '';
582
+ updateWordCount();
583
+ });
584
+
585
+ init();
586
+ </script>