@jjlmoya/utils-creative 1.2.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 (71) hide show
  1. package/package.json +64 -0
  2. package/src/category/i18n/en.ts +9 -0
  3. package/src/category/i18n/es.ts +9 -0
  4. package/src/category/i18n/fr.ts +9 -0
  5. package/src/category/index.ts +34 -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 +6 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +27 -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/seo_length.test.ts +22 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/bead-pattern-generator/bibliography.astro +18 -0
  23. package/src/tool/bead-pattern-generator/component.astro +372 -0
  24. package/src/tool/bead-pattern-generator/i18n/en.ts +61 -0
  25. package/src/tool/bead-pattern-generator/i18n/es.ts +68 -0
  26. package/src/tool/bead-pattern-generator/i18n/fr.ts +61 -0
  27. package/src/tool/bead-pattern-generator/index.ts +37 -0
  28. package/src/tool/bead-pattern-generator/seo.astro +14 -0
  29. package/src/tool/bead-pattern-generator/style.css +511 -0
  30. package/src/tool/dice-roller/bibliography.astro +17 -0
  31. package/src/tool/dice-roller/component.astro +230 -0
  32. package/src/tool/dice-roller/i18n/en.ts +87 -0
  33. package/src/tool/dice-roller/i18n/es.ts +89 -0
  34. package/src/tool/dice-roller/i18n/fr.ts +87 -0
  35. package/src/tool/dice-roller/index.ts +37 -0
  36. package/src/tool/dice-roller/seo.astro +14 -0
  37. package/src/tool/dice-roller/style.css +482 -0
  38. package/src/tool/excuse-generator/bibliography.astro +18 -0
  39. package/src/tool/excuse-generator/component.astro +140 -0
  40. package/src/tool/excuse-generator/i18n/en.ts +80 -0
  41. package/src/tool/excuse-generator/i18n/es.ts +84 -0
  42. package/src/tool/excuse-generator/i18n/fr.ts +80 -0
  43. package/src/tool/excuse-generator/index.ts +42 -0
  44. package/src/tool/excuse-generator/seo.astro +14 -0
  45. package/src/tool/excuse-generator/style.css +316 -0
  46. package/src/tool/fortune-cookie/bibliography.astro +18 -0
  47. package/src/tool/fortune-cookie/component.astro +299 -0
  48. package/src/tool/fortune-cookie/i18n/en.ts +85 -0
  49. package/src/tool/fortune-cookie/i18n/es.ts +90 -0
  50. package/src/tool/fortune-cookie/i18n/fr.ts +85 -0
  51. package/src/tool/fortune-cookie/index.ts +40 -0
  52. package/src/tool/fortune-cookie/seo.astro +14 -0
  53. package/src/tool/fortune-cookie/style.css +332 -0
  54. package/src/tool/synesthesia-painter/bibliography.astro +17 -0
  55. package/src/tool/synesthesia-painter/component.astro +110 -0
  56. package/src/tool/synesthesia-painter/i18n/en.ts +80 -0
  57. package/src/tool/synesthesia-painter/i18n/es.ts +82 -0
  58. package/src/tool/synesthesia-painter/i18n/fr.ts +80 -0
  59. package/src/tool/synesthesia-painter/index.ts +39 -0
  60. package/src/tool/synesthesia-painter/seo.astro +14 -0
  61. package/src/tool/synesthesia-painter/style.css +234 -0
  62. package/src/tool/zalgo-generator/bibliography.astro +18 -0
  63. package/src/tool/zalgo-generator/component.astro +195 -0
  64. package/src/tool/zalgo-generator/i18n/en.ts +60 -0
  65. package/src/tool/zalgo-generator/i18n/es.ts +67 -0
  66. package/src/tool/zalgo-generator/i18n/fr.ts +60 -0
  67. package/src/tool/zalgo-generator/index.ts +38 -0
  68. package/src/tool/zalgo-generator/seo.astro +14 -0
  69. package/src/tool/zalgo-generator/style.css +558 -0
  70. package/src/tools.ts +4 -0
  71. package/src/types.ts +72 -0
@@ -0,0 +1,482 @@
1
+ .dr-root {
2
+ --dr-bg: #fff;
3
+ --dr-bg-muted: #f8fafc;
4
+ --dr-bg-card: #fff;
5
+ --dr-text: #0f172a;
6
+ --dr-text-muted: #64748b;
7
+ --dr-border: #e2e8f0;
8
+ --dr-primary: #7c3aed;
9
+ --dr-primary-light: #ede9fe;
10
+ --dr-primary-dark: #5b21b6;
11
+ --dr-accent: #f59e0b;
12
+ --dr-danger: #ef4444;
13
+ --dr-success: #10b981;
14
+ --dr-shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
15
+ --dr-shadow-md: 0 4px 16px rgba(0,0,0,0.1);
16
+ --dr-shadow-lg: 0 8px 32px rgba(0,0,0,0.12);
17
+ --dr-radius: 1rem;
18
+ --dr-radius-sm: 0.5rem;
19
+
20
+ display: grid;
21
+ grid-template-columns: 1fr 1fr;
22
+ gap: 1.5rem;
23
+ max-width: 900px;
24
+ margin: 0 auto;
25
+ padding: 1rem;
26
+ }
27
+
28
+ .theme-dark .dr-root {
29
+ --dr-bg: #0f172a;
30
+ --dr-bg-muted: #1e293b;
31
+ --dr-bg-card: #1e293b;
32
+ --dr-text: #f1f5f9;
33
+ --dr-text-muted: #94a3b8;
34
+ --dr-border: #334155;
35
+ --dr-primary: #a78bfa;
36
+ --dr-primary-light: #2d1b69;
37
+ --dr-primary-dark: #7c3aed;
38
+ --dr-accent: #fbbf24;
39
+ --dr-shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
40
+ --dr-shadow-md: 0 4px 16px rgba(0,0,0,0.4);
41
+ --dr-shadow-lg: 0 8px 32px rgba(0,0,0,0.5);
42
+ }
43
+
44
+ @media (max-width: 680px) {
45
+ .dr-root {
46
+ grid-template-columns: 1fr;
47
+ }
48
+ }
49
+
50
+ /* ── panel izquierdo ── */
51
+ .dr-panel {
52
+ background: var(--dr-bg-card);
53
+ border: 1px solid var(--dr-border);
54
+ border-radius: var(--dr-radius);
55
+ box-shadow: var(--dr-shadow-md);
56
+ padding: 1.5rem;
57
+ display: flex;
58
+ flex-direction: column;
59
+ gap: 1.25rem;
60
+ }
61
+
62
+ .dr-label {
63
+ font-size: 0.75rem;
64
+ font-weight: 600;
65
+ text-transform: uppercase;
66
+ letter-spacing: 0.08em;
67
+ color: var(--dr-text-muted);
68
+ margin: 0 0 0.625rem;
69
+ }
70
+
71
+ /* ── cuadrícula de dados ── */
72
+ .dr-dice-grid {
73
+ display: grid;
74
+ grid-template-columns: repeat(4, 1fr);
75
+ gap: 0.5rem;
76
+ }
77
+
78
+ .dr-die-btn {
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ padding: 0.75rem 0.5rem;
83
+ background: var(--dr-bg-muted);
84
+ border: 1px solid var(--dr-border);
85
+ border-radius: var(--dr-radius-sm);
86
+ cursor: pointer;
87
+ transition: background 0.15s, border-color 0.15s, transform 0.1s;
88
+ color: var(--dr-primary);
89
+ font-size: 0.85rem;
90
+ font-weight: 700;
91
+ }
92
+
93
+ .dr-die-btn:hover {
94
+ background: var(--dr-primary-light);
95
+ border-color: var(--dr-primary);
96
+ transform: translateY(-2px);
97
+ }
98
+
99
+ .dr-die-btn:active {
100
+ transform: translateY(0);
101
+ }
102
+
103
+ /* ── bolsa ── */
104
+ .dr-pool-section {
105
+ border: 1px solid var(--dr-border);
106
+ border-radius: var(--dr-radius-sm);
107
+ padding: 0.875rem;
108
+ background: var(--dr-bg-muted);
109
+ }
110
+
111
+ .dr-pool-header {
112
+ display: flex;
113
+ justify-content: space-between;
114
+ align-items: center;
115
+ margin-bottom: 0.5rem;
116
+ }
117
+
118
+ .dr-pool-header .dr-label {
119
+ margin-bottom: 0;
120
+ }
121
+
122
+ .dr-clear-btn {
123
+ font-size: 0.7rem;
124
+ font-weight: 600;
125
+ color: var(--dr-text-muted);
126
+ background: none;
127
+ border: 1px solid var(--dr-border);
128
+ border-radius: 0.375rem;
129
+ padding: 0.2rem 0.5rem;
130
+ cursor: pointer;
131
+ transition: color 0.15s, border-color 0.15s;
132
+ }
133
+
134
+ .dr-clear-btn:hover {
135
+ color: var(--dr-danger);
136
+ border-color: var(--dr-danger);
137
+ }
138
+
139
+ .dr-pool-display {
140
+ display: flex;
141
+ flex-wrap: wrap;
142
+ gap: 0.5rem;
143
+ min-height: 2.5rem;
144
+ align-items: center;
145
+ }
146
+
147
+ .dr-pool-empty {
148
+ font-size: 0.8rem;
149
+ color: var(--dr-text-muted);
150
+ font-style: italic;
151
+ }
152
+
153
+ .dr-pool-chip {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 0.25rem;
157
+ background: var(--dr-primary-light);
158
+ border: 1px solid var(--dr-primary);
159
+ border-radius: 2rem;
160
+ padding: 0.2rem 0.5rem 0.2rem 0.75rem;
161
+ }
162
+
163
+ .dr-chip-label {
164
+ font-size: 0.8rem;
165
+ font-weight: 700;
166
+ color: var(--dr-primary);
167
+ }
168
+
169
+ .dr-chip-remove {
170
+ width: 1.125rem;
171
+ height: 1.125rem;
172
+ border-radius: 50%;
173
+ border: none;
174
+ background: var(--dr-primary);
175
+ color: #fff;
176
+ font-size: 0.8rem;
177
+ line-height: 1;
178
+ cursor: pointer;
179
+ display: flex;
180
+ align-items: center;
181
+ justify-content: center;
182
+ transition: background 0.15s;
183
+ padding: 0;
184
+ }
185
+
186
+ .dr-chip-remove:hover {
187
+ background: var(--dr-primary-dark);
188
+ }
189
+
190
+ /* ── modificador ── */
191
+ .dr-modifier-section {
192
+ display: flex;
193
+ align-items: center;
194
+ justify-content: space-between;
195
+ }
196
+
197
+ .dr-modifier-controls {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: 0.75rem;
201
+ background: var(--dr-bg-muted);
202
+ border: 1px solid var(--dr-border);
203
+ border-radius: 2rem;
204
+ padding: 0.25rem 0.5rem;
205
+ }
206
+
207
+ .dr-mod-btn {
208
+ width: 1.75rem;
209
+ height: 1.75rem;
210
+ border-radius: 50%;
211
+ border: 1px solid var(--dr-border);
212
+ background: var(--dr-bg-card);
213
+ color: var(--dr-text);
214
+ font-size: 1rem;
215
+ font-weight: 700;
216
+ cursor: pointer;
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ transition: background 0.15s, border-color 0.15s;
221
+ line-height: 1;
222
+ padding: 0;
223
+ }
224
+
225
+ .dr-mod-btn:hover {
226
+ background: var(--dr-primary-light);
227
+ border-color: var(--dr-primary);
228
+ }
229
+
230
+ .dr-modifier-display {
231
+ min-width: 2rem;
232
+ text-align: center;
233
+ font-weight: 700;
234
+ font-size: 1rem;
235
+ color: var(--dr-text);
236
+ }
237
+
238
+ /* ── botón lanzar ── */
239
+ .dr-roll-btn {
240
+ display: flex;
241
+ align-items: center;
242
+ justify-content: center;
243
+ gap: 0.5rem;
244
+ width: 100%;
245
+ padding: 0.875rem;
246
+ background: linear-gradient(135deg, var(--dr-primary), var(--dr-primary-dark));
247
+ color: #fff;
248
+ border: none;
249
+ border-radius: var(--dr-radius-sm);
250
+ font-size: 1rem;
251
+ font-weight: 700;
252
+ cursor: pointer;
253
+ transition: opacity 0.2s, transform 0.1s;
254
+ box-shadow: 0 4px 12px rgba(124, 58, 237, 0.35);
255
+ margin-top: auto;
256
+ }
257
+
258
+ .dr-roll-btn:disabled {
259
+ opacity: 0.5;
260
+ cursor: not-allowed;
261
+ box-shadow: none;
262
+ }
263
+
264
+ .dr-roll-btn:not(:disabled):hover {
265
+ transform: translateY(-1px);
266
+ box-shadow: 0 6px 18px rgba(124, 58, 237, 0.45);
267
+ }
268
+
269
+ .dr-roll-btn:not(:disabled):active {
270
+ transform: translateY(0);
271
+ }
272
+
273
+ @keyframes dr-shake {
274
+ 0%, 100% { transform: rotate(0deg); }
275
+ 20% { transform: rotate(-15deg); }
276
+ 40% { transform: rotate(15deg); }
277
+ 60% { transform: rotate(-10deg); }
278
+ 80% { transform: rotate(10deg); }
279
+ }
280
+
281
+ .dr-rolling {
282
+ animation: dr-shake 0.4s ease-in-out;
283
+ }
284
+
285
+ /* ── panel de resultados ── */
286
+ .dr-results-panel {
287
+ display: flex;
288
+ flex-direction: column;
289
+ gap: 1.25rem;
290
+ }
291
+
292
+ .dr-result-card {
293
+ background: var(--dr-bg-card);
294
+ border: 1px solid var(--dr-border);
295
+ border-radius: var(--dr-radius);
296
+ box-shadow: var(--dr-shadow-md);
297
+ padding: 1.5rem;
298
+ min-height: 12rem;
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: center;
302
+ }
303
+
304
+ .dr-result-empty {
305
+ display: flex;
306
+ flex-direction: column;
307
+ align-items: center;
308
+ gap: 0.5rem;
309
+ color: var(--dr-text-muted);
310
+ }
311
+
312
+ .dr-result-empty p {
313
+ font-size: 0.9rem;
314
+ margin: 0;
315
+ }
316
+
317
+ .dr-result-content {
318
+ width: 100%;
319
+ flex-direction: column;
320
+ align-items: center;
321
+ gap: 1rem;
322
+ }
323
+
324
+ .dr-total-area {
325
+ display: flex;
326
+ flex-direction: column;
327
+ align-items: center;
328
+ gap: 0.25rem;
329
+ }
330
+
331
+ .dr-total-label {
332
+ font-size: 0.75rem;
333
+ font-weight: 600;
334
+ text-transform: uppercase;
335
+ letter-spacing: 0.08em;
336
+ color: var(--dr-text-muted);
337
+ }
338
+
339
+ .dr-total-val {
340
+ font-size: 4rem;
341
+ font-weight: 900;
342
+ color: var(--dr-primary);
343
+ line-height: 1;
344
+ }
345
+
346
+ .dr-roll-expr {
347
+ font-size: 0.8rem;
348
+ color: var(--dr-text-muted);
349
+ font-style: italic;
350
+ }
351
+
352
+ .dr-dice-results {
353
+ display: flex;
354
+ flex-wrap: wrap;
355
+ gap: 0.5rem;
356
+ justify-content: center;
357
+ }
358
+
359
+ .dr-result-pip {
360
+ display: flex;
361
+ flex-direction: column;
362
+ align-items: center;
363
+ gap: 0.15rem;
364
+ padding: 0.5rem 0.75rem;
365
+ background: var(--dr-bg-muted);
366
+ border: 2px solid var(--dr-border);
367
+ border-radius: var(--dr-radius-sm);
368
+ min-width: 3rem;
369
+ transition: border-color 0.2s;
370
+ }
371
+
372
+ .dr-pip-max {
373
+ border-color: var(--dr-success);
374
+ background: rgba(16, 185, 129, 0.1);
375
+ }
376
+
377
+ .dr-pip-min {
378
+ border-color: var(--dr-danger);
379
+ background: rgba(239, 68, 68, 0.1);
380
+ }
381
+
382
+ .dr-pip-val {
383
+ font-size: 1.25rem;
384
+ font-weight: 800;
385
+ color: var(--dr-text);
386
+ line-height: 1;
387
+ }
388
+
389
+ .dr-pip-die {
390
+ font-size: 0.65rem;
391
+ color: var(--dr-text-muted);
392
+ font-weight: 600;
393
+ }
394
+
395
+ /* ── historial ── */
396
+ .dr-history-section {
397
+ background: var(--dr-bg-card);
398
+ border: 1px solid var(--dr-border);
399
+ border-radius: var(--dr-radius);
400
+ box-shadow: var(--dr-shadow-sm);
401
+ padding: 1.25rem;
402
+ flex: 1;
403
+ }
404
+
405
+ .dr-history-header {
406
+ display: flex;
407
+ justify-content: space-between;
408
+ align-items: center;
409
+ margin-bottom: 0.75rem;
410
+ }
411
+
412
+ .dr-history-header .dr-label {
413
+ margin-bottom: 0;
414
+ }
415
+
416
+ .dr-history-list {
417
+ display: flex;
418
+ flex-direction: column;
419
+ gap: 0.375rem;
420
+ max-height: 220px;
421
+ overflow-y: auto;
422
+ }
423
+
424
+ .dr-history-list::-webkit-scrollbar {
425
+ width: 4px;
426
+ }
427
+
428
+ .dr-history-list::-webkit-scrollbar-thumb {
429
+ background: var(--dr-border);
430
+ border-radius: 2px;
431
+ }
432
+
433
+ .dr-history-empty {
434
+ font-size: 0.8rem;
435
+ color: var(--dr-text-muted);
436
+ font-style: italic;
437
+ text-align: center;
438
+ margin: 1rem 0;
439
+ }
440
+
441
+ .dr-history-item {
442
+ display: grid;
443
+ grid-template-columns: 1fr auto auto;
444
+ align-items: center;
445
+ gap: 0.75rem;
446
+ padding: 0.4rem 0.6rem;
447
+ background: var(--dr-bg-muted);
448
+ border-radius: var(--dr-radius-sm);
449
+ font-size: 0.8rem;
450
+ animation: dr-fadein 0.2s ease-out;
451
+ }
452
+
453
+ @keyframes dr-fadein {
454
+ from {
455
+ opacity: 0;
456
+ transform: translate-y(-4px);
457
+ }
458
+
459
+ to {
460
+ opacity: 1;
461
+ transform: translateY(0);
462
+ }
463
+ }
464
+
465
+ .dr-hist-expr {
466
+ color: var(--dr-text-muted);
467
+ font-size: 0.75rem;
468
+ overflow: hidden;
469
+ text-overflow: ellipsis;
470
+ white-space: nowrap;
471
+ }
472
+
473
+ .dr-hist-result {
474
+ font-weight: 800;
475
+ font-size: 1rem;
476
+ color: var(--dr-primary);
477
+ }
478
+
479
+ .dr-hist-time {
480
+ font-size: 0.7rem;
481
+ color: var(--dr-text-muted);
482
+ }
@@ -0,0 +1,18 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { excuseGenerator, type ExcuseGeneratorLocaleContent } 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 localeContentLoader = (excuseGenerator.i18n as Record<string, () => Promise<ExcuseGeneratorLocaleContent>>)[locale];
12
+ const content = localeContentLoader ? await localeContentLoader() : null;
13
+ if (!content) return null;
14
+
15
+ const { bibliography } = content;
16
+ ---
17
+
18
+ <SharedBibliography links={bibliography} />
@@ -0,0 +1,140 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import './style.css';
4
+ import type { ExcuseGeneratorUI } from './index';
5
+
6
+ interface Props {
7
+ ui: ExcuseGeneratorUI;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ ---
12
+
13
+ <div id="excuse-generator-root" class="excuse-generator" data-ui={JSON.stringify(ui)}>
14
+ <div class="excuse-generator-root">
15
+ <div class="excuse-generator-card">
16
+ <div class="excuse-generator-blob excuse-generator-blob-1"></div>
17
+ <div class="excuse-generator-blob excuse-generator-blob-2"></div>
18
+
19
+ <div class="excuse-generator-slots">
20
+ <div id="excuse-part-1" class="excuse-generator-slot">
21
+ <span class="excuse-generator-slot-label">{ui.subjectLabel}</span>
22
+ <p class="excuse-generator-slot-text">?</p>
23
+ </div>
24
+
25
+ <div class="excuse-generator-separator">
26
+ <Icon name="mdi:plus" />
27
+ </div>
28
+ <div class="excuse-generator-separator-mobile">
29
+ <Icon name="mdi:arrow-down" />
30
+ </div>
31
+
32
+ <div id="excuse-part-2" class="excuse-generator-slot">
33
+ <span class="excuse-generator-slot-label">{ui.actionLabel}</span>
34
+ <p class="excuse-generator-slot-text">?</p>
35
+ </div>
36
+
37
+ <div class="excuse-generator-separator">
38
+ <Icon name="mdi:arrow-right" />
39
+ </div>
40
+ <div class="excuse-generator-separator-mobile">
41
+ <Icon name="mdi:arrow-down" />
42
+ </div>
43
+
44
+ <div id="excuse-part-3" class="excuse-generator-slot">
45
+ <span class="excuse-generator-slot-label">{ui.victimLabel}</span>
46
+ <p class="excuse-generator-slot-text">?</p>
47
+ </div>
48
+ </div>
49
+
50
+ <div class="excuse-generator-actions">
51
+ <button id="excuse-generate-btn" class="excuse-generator-btn">
52
+ <span class="excuse-generator-btn-content">
53
+ <Icon name="mdi:creation" />
54
+ {ui.generateBtn}
55
+ </span>
56
+ <div class="excuse-generator-btn-gradient"></div>
57
+ </button>
58
+
59
+ <button id="excuse-copy-btn" class="excuse-generator-copy">
60
+ <Icon name="mdi:content-copy" />
61
+ {ui.copyBtn}
62
+ </button>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <script>
69
+ const root = document.getElementById('excuse-generator-root') as HTMLElement;
70
+ const ui = JSON.parse(root.dataset.ui as string) as Record<string, string>;
71
+
72
+ const starts = JSON.parse(ui.starts) as string[];
73
+ const middles = JSON.parse(ui.middles) as string[];
74
+ const ends = JSON.parse(ui.ends) as string[];
75
+
76
+ const els = {
77
+ p1: document.querySelector('#excuse-part-1 .excuse-generator-slot-text') as HTMLElement,
78
+ p2: document.querySelector('#excuse-part-2 .excuse-generator-slot-text') as HTMLElement,
79
+ p3: document.querySelector('#excuse-part-3 .excuse-generator-slot-text') as HTMLElement,
80
+ genBtn: document.getElementById('excuse-generate-btn') as HTMLButtonElement,
81
+ copyBtn: document.getElementById('excuse-copy-btn') as HTMLElement,
82
+ };
83
+
84
+ let generatedText = '';
85
+
86
+ function runSlot(element: HTMLElement, options: string[], duration: number): Promise<string> {
87
+ return new Promise((resolve) => {
88
+ const intervalTime = 50;
89
+ const steps = duration / intervalTime;
90
+ let currentStep = 0;
91
+
92
+ element.classList.add('blur');
93
+
94
+ const timer = setInterval(() => {
95
+ const randomText = options[Math.floor(Math.random() * options.length)];
96
+ element.innerText = randomText || '';
97
+ currentStep++;
98
+
99
+ if (currentStep >= steps) {
100
+ clearInterval(timer);
101
+ element.classList.remove('blur');
102
+ const final = options[Math.floor(Math.random() * options.length)] || '';
103
+ element.innerText = final;
104
+ resolve(final);
105
+ }
106
+ }, intervalTime);
107
+ });
108
+ }
109
+
110
+ async function generateExcuse() {
111
+ if (!els.p1 || !els.p2 || !els.p3 || !els.genBtn) return;
112
+
113
+ els.genBtn.disabled = true;
114
+ if (els.copyBtn) {
115
+ els.copyBtn.classList.remove('visible');
116
+ }
117
+
118
+ const res1 = await runSlot(els.p1, starts, 800);
119
+ const res2 = await runSlot(els.p2, middles, 1400);
120
+ const res3 = await runSlot(els.p3, ends, 2000);
121
+
122
+ generatedText = `${res1} ${res2} ${res3}`;
123
+
124
+ els.genBtn.disabled = false;
125
+ if (els.copyBtn) {
126
+ els.copyBtn.classList.add('visible');
127
+
128
+ els.copyBtn.onclick = () => {
129
+ navigator.clipboard.writeText(generatedText);
130
+ const original = els.copyBtn.innerHTML;
131
+ els.copyBtn.innerHTML = `<span class="excuse-generator-copy-success">${ui.copied}</span>`;
132
+ setTimeout(() => {
133
+ els.copyBtn.innerHTML = original;
134
+ }, 2000);
135
+ };
136
+ }
137
+ }
138
+
139
+ els.genBtn?.addEventListener('click', generateExcuse);
140
+ </script>
@@ -0,0 +1,80 @@
1
+ import type { ExcuseGeneratorLocaleContent } from '../index';
2
+
3
+ export const content: ExcuseGeneratorLocaleContent = {
4
+ slug: 'excuse-generator',
5
+ title: 'Excuse Generator',
6
+ description: 'Semantic gambling machine to get rid of commitments with style. Generate surreal and irrefutable excuses instantly.',
7
+ faqTitle: 'Frequently Asked Questions',
8
+ bibliographyTitle: 'Leisure Bibliography',
9
+ ui: {
10
+ title: 'Surreal Excuse Generator',
11
+ description: 'Next-generation alibi calculator.',
12
+ subjectLabel: 'Subject',
13
+ actionLabel: 'Action',
14
+ victimLabel: 'Victim',
15
+ generateBtn: 'GENERATE EXCUSE',
16
+ copyBtn: 'Copy this craziness',
17
+ copied: 'Copied!',
18
+ starts: JSON.stringify([
19
+ "My cat", "My grandmother", "The president", "An alien", "My neighbor from the 5th floor",
20
+ "The wifi", "My horoscope", "A time traveler", "The washing machine", "My fridge",
21
+ "The spirit of Christmas", "My shadow", "A ninja", "The vegan police", "Siri",
22
+ "My carnivorous plant", "The Amazon delivery guy", "My evil clone", "A wizard", "Godzilla"
23
+ ]),
24
+ middles: JSON.stringify([
25
+ "has declared war on", "has eaten", "has kidnapped", "has set fire to",
26
+ "is organizing a party in", "has vomited on", "refuses to leave",
27
+ "has hacked", "has stolen", "has fallen in love with", "is meditating on",
28
+ "has cast a spell on", "is protesting against", "has founded a cult in",
29
+ "is dancing samba in", "has summoned a demon in", "has blocked access to",
30
+ "has turned into gold", "is trying to sell", "has written a book about"
31
+ ]),
32
+ ends: JSON.stringify([
33
+ "my house keys.", "my will to live.", "the garage door.",
34
+ "my favorite pants.", "the living room router.", "my only pair of shoes.",
35
+ "the laws of physics.", "my dignity.", "the building's elevator.",
36
+ "my Netflix account.", "my toothbrush.", "my house deeds.",
37
+ "my pogs collection.", "the car's handbrake.", "my desire to socialize.",
38
+ "my coffee supply.", "the toilet lid.", "the TV remote.",
39
+ "my lucky socks.", "Western civilization."
40
+ ]),
41
+ faqTitle: 'FAQ',
42
+ bibliographyTitle: 'References'
43
+ },
44
+ seo: [
45
+ { type: 'title', text: 'The Scientific Art of the Perfect Excuse', level: 2 },
46
+ { type: 'paragraph', html: 'We live in the era of hyperconnectivity. Your phone vibrates, your watch notifies you, and your social agenda looks like a game of Tetris about to lose. The pressure to say "yes" to everything has created an epidemic of social exhaustion.' },
47
+ { type: 'title', text: 'The Renaissance of JOMO (Joy of Missing Out)', level: 3 },
48
+ { type: 'paragraph', html: 'While FOMO (Fear of Missing Out) dominated the last decade, digital wellness experts now advocate for <strong>JOMO: The Joy of Missing Out</strong>. It\'s not about isolation, but about intentionality.' },
49
+ { type: 'tip', title: 'The Formula for the Irrefutable Alibi', html: '<strong>The Dissociated Subject:</strong> You are never the culprit. It is "the wifi", "my cat", "the universe". Shift the blame to an external entity.<br><strong>The Hyperbolic Action:</strong> The situation must be absurd or technical enough that nobody asks for details.<br><strong>The Physical Block:</strong> The final outcome must be binary: either I go or I stay home.' },
50
+ { type: 'title', text: 'A Brief History of the Excuse', level: 3 },
51
+ { type: 'list', items: [
52
+ '<strong>Middle Ages:</strong> "My horse has lost a horseshoe" (A timeless classic).',
53
+ '<strong>Industrial Revolution:</strong> "The steam engine overheated".',
54
+ '<strong>Digital Era:</strong> "My internet dropped right in the middle of an update".',
55
+ ]},
56
+ { type: 'proscons', items: [
57
+ { pro: 'Instantly relieves social pressure', con: 'Overuse erodes trust' },
58
+ { pro: 'Protects your energy and boundaries', con: 'May generate guilt if used carelessly' },
59
+ { pro: 'Creative and humorous tone defuses tension', con: 'Not suitable for formal or professional contexts' },
60
+ ]},
61
+ ],
62
+ faq: [
63
+ {
64
+ question: 'Can this generator save my marriage?',
65
+ answer: 'While we are not therapists, avoiding that dinner with the in-laws using a plausible technical emergency can significantly reduce tension. Use responsibly.'
66
+ },
67
+ {
68
+ question: 'Why such surreal phrases?',
69
+ answer: 'The strategy is based on Cognitive Dissonance. If you say something boring it is verifiable. If you say something absurd, the surprise blocks the capacity for anger.'
70
+ }
71
+ ],
72
+ bibliography: [
73
+ { name: 'Procrastinator\'s Manifesto', url: 'https://example.com/manifesto' }
74
+ ],
75
+ howTo: [
76
+ { name: 'Generate', text: 'Click the generate button to create a new excuse.' },
77
+ { name: 'Copy', text: 'Click the copy button to bring the excuse to your clipboard.' }
78
+ ],
79
+ schemas: []
80
+ };