@jjlmoya/utils-chrono 1.20.0 → 1.21.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 (33) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -0
  3. package/src/entries.ts +4 -1
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +1 -1
  6. package/src/tests/tool_validation.test.ts +1 -1
  7. package/src/tool/watch-crystal-material-comparison/bibliography.astro +16 -0
  8. package/src/tool/watch-crystal-material-comparison/bibliography.ts +16 -0
  9. package/src/tool/watch-crystal-material-comparison/client.ts +150 -0
  10. package/src/tool/watch-crystal-material-comparison/component.astro +15 -0
  11. package/src/tool/watch-crystal-material-comparison/components/CrystalPanel.astro +20 -0
  12. package/src/tool/watch-crystal-material-comparison/entry.ts +43 -0
  13. package/src/tool/watch-crystal-material-comparison/helpers.ts +49 -0
  14. package/src/tool/watch-crystal-material-comparison/i18n/de.ts +35 -0
  15. package/src/tool/watch-crystal-material-comparison/i18n/en.ts +90 -0
  16. package/src/tool/watch-crystal-material-comparison/i18n/es.ts +35 -0
  17. package/src/tool/watch-crystal-material-comparison/i18n/fr.ts +35 -0
  18. package/src/tool/watch-crystal-material-comparison/i18n/id.ts +35 -0
  19. package/src/tool/watch-crystal-material-comparison/i18n/it.ts +35 -0
  20. package/src/tool/watch-crystal-material-comparison/i18n/ja.ts +35 -0
  21. package/src/tool/watch-crystal-material-comparison/i18n/ko.ts +35 -0
  22. package/src/tool/watch-crystal-material-comparison/i18n/nl.ts +35 -0
  23. package/src/tool/watch-crystal-material-comparison/i18n/pl.ts +35 -0
  24. package/src/tool/watch-crystal-material-comparison/i18n/pt.ts +35 -0
  25. package/src/tool/watch-crystal-material-comparison/i18n/ru.ts +35 -0
  26. package/src/tool/watch-crystal-material-comparison/i18n/sv.ts +35 -0
  27. package/src/tool/watch-crystal-material-comparison/i18n/tr.ts +35 -0
  28. package/src/tool/watch-crystal-material-comparison/i18n/zh.ts +35 -0
  29. package/src/tool/watch-crystal-material-comparison/index.ts +11 -0
  30. package/src/tool/watch-crystal-material-comparison/logic.ts +55 -0
  31. package/src/tool/watch-crystal-material-comparison/seo.astro +16 -0
  32. package/src/tool/watch-crystal-material-comparison/watch-crystal-material-comparison.css +625 -0
  33. package/src/tools.ts +2 -0
@@ -0,0 +1,625 @@
1
+ .tool-main-card {
2
+ --gold: #d4af37;
3
+ --gold-light: #f0d68a;
4
+ --gold-dark: #a8871e;
5
+ --color-white: #fff;
6
+
7
+ max-width: 780px;
8
+ margin: 0 auto;
9
+ }
10
+
11
+ .crystal-layout {
12
+ display: grid;
13
+ grid-template-columns: 180px 1fr;
14
+ gap: 1rem;
15
+ align-items: start;
16
+ }
17
+
18
+ /* ===== LEFT MENU ===== */
19
+
20
+ .crystal-list {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: 0.75rem;
24
+ }
25
+
26
+ .crystal-list-item {
27
+ position: relative;
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 0.65rem;
31
+ padding: 0.9rem;
32
+ border-radius: 1rem;
33
+ border: 1px solid rgba(255,255,255,0.06);
34
+ background: rgba(255,255,255,0.03);
35
+ cursor: pointer;
36
+ transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
37
+ user-select: none;
38
+ overflow: hidden;
39
+ }
40
+
41
+ .crystal-list-item::before {
42
+ content: '';
43
+ position: absolute;
44
+ left: 0;
45
+ top: 0;
46
+ bottom: 0;
47
+ width: 3px;
48
+ border-radius: 0 2px 2px 0;
49
+ background: transparent;
50
+ transition: all 0.35s ease;
51
+ }
52
+
53
+ .crystal-list-item:hover {
54
+ border-color: rgba(212, 175, 55, 0.3);
55
+ transform: translateX(3px);
56
+ }
57
+
58
+ .crystal-list-item.active {
59
+ border-color: var(--gold);
60
+ background: linear-gradient(135deg, rgba(212,175,55,0.12), rgba(212,175,55,0.04));
61
+ box-shadow: 0 0 30px rgba(212,175,55,0.06);
62
+ }
63
+
64
+ .crystal-list-item.active::before {
65
+ background: var(--gold);
66
+ box-shadow: 0 0 12px rgba(212,175,55,0.5);
67
+ }
68
+
69
+ .crystal-list-item.dragging {
70
+ opacity: 0.3;
71
+ transform: scale(0.95);
72
+ }
73
+
74
+ .crystal-list-item.drag-over {
75
+ border-color: var(--gold);
76
+ background: rgba(212,175,55,0.15);
77
+ box-shadow: 0 0 40px rgba(212,175,55,0.12);
78
+ transform: scale(1.02);
79
+ }
80
+
81
+ .list-grip {
82
+ display: flex;
83
+ flex-direction: column;
84
+ gap: 2px;
85
+ opacity: 0.15;
86
+ transition: opacity 0.2s ease;
87
+ flex-shrink: 0;
88
+ }
89
+
90
+ .crystal-list-item:hover .list-grip {
91
+ opacity: 0.4;
92
+ }
93
+
94
+ .list-grip span {
95
+ display: block;
96
+ width: 3px;
97
+ height: 3px;
98
+ border-radius: 50%;
99
+ background: var(--text-base);
100
+ }
101
+
102
+ .list-ovr {
103
+ position: relative;
104
+ width: 40px;
105
+ height: 40px;
106
+ border-radius: 50%;
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ font-size: 1rem;
111
+ font-weight: 800;
112
+ color: var(--color-white);
113
+ flex-shrink: 0;
114
+ letter-spacing: -0.02em;
115
+ box-shadow: 0 0 20px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.2);
116
+ }
117
+
118
+ .list-ovr::after {
119
+ content: '';
120
+ position: absolute;
121
+ inset: -2px;
122
+ border-radius: 50%;
123
+ border: 2px solid rgba(255,255,255,0.15);
124
+ }
125
+
126
+ .list-info {
127
+ display: flex;
128
+ flex-direction: column;
129
+ flex: 1;
130
+ min-width: 0;
131
+ }
132
+
133
+ .list-name {
134
+ font-size: 0.75rem;
135
+ font-weight: 700;
136
+ color: var(--text-base);
137
+ line-height: 1.2;
138
+ white-space: nowrap;
139
+ overflow: hidden;
140
+ text-overflow: ellipsis;
141
+ }
142
+
143
+ .list-hint {
144
+ font-size: 0.5rem;
145
+ font-weight: 600;
146
+ color: var(--gold);
147
+ text-transform: uppercase;
148
+ letter-spacing: 0.1em;
149
+ opacity: 0;
150
+ transition: opacity 0.5s ease;
151
+ }
152
+
153
+ .crystal-list-item.active .list-hint {
154
+ opacity: 0.6;
155
+ animation: hint-pulse 2s ease-in-out infinite;
156
+ }
157
+
158
+ @keyframes hint-pulse {
159
+ 0%, 100% { opacity: 0.3; }
160
+ 50% { opacity: 0.8; }
161
+ }
162
+
163
+ /* ===== BIG CARD ===== */
164
+
165
+ .crystal-card-wrap {
166
+ position: relative;
167
+ min-height: 320px;
168
+ }
169
+
170
+ .crystal-card-big {
171
+ position: relative;
172
+ border-radius: 1.25rem;
173
+ overflow: hidden;
174
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
175
+ box-shadow: 0 8px 40px rgba(0,0,0,0.3);
176
+ }
177
+
178
+ .crystal-card-big.card-leaving {
179
+ opacity: 0;
180
+ transform: scale(0.96) translateY(6px);
181
+ }
182
+
183
+ .crystal-card-big.card-entering {
184
+ animation: card-enter 0.5s cubic-bezier(0.4, 0, 0.2, 1);
185
+ }
186
+
187
+ @keyframes card-enter {
188
+ from {
189
+ opacity: 0;
190
+ transform: scale(0.96) translateY(6px);
191
+ }
192
+ to {
193
+ opacity: 1;
194
+ transform: scale(1) translateY(0);
195
+ }
196
+ }
197
+
198
+ .crystal-card-big::before {
199
+ content: '';
200
+ position: absolute;
201
+ inset: 0;
202
+ background: linear-gradient(180deg, rgba(255,255,255,0.08) 0%, transparent 40%, rgba(0,0,0,0.2) 100%);
203
+ z-index: 1;
204
+ pointer-events: none;
205
+ }
206
+
207
+ .crystal-card-big::after {
208
+ content: '';
209
+ position: absolute;
210
+ top: -50%;
211
+ left: -50%;
212
+ width: 200%;
213
+ height: 200%;
214
+ background: conic-gradient(from 0deg at 50% 50%, transparent 0%, rgba(255,255,255,0.03) 25%, transparent 50%);
215
+ animation: card-shine 8s linear infinite;
216
+ z-index: 2;
217
+ pointer-events: none;
218
+ }
219
+
220
+ @keyframes card-shine {
221
+ from { transform: rotate(0deg); }
222
+ to { transform: rotate(360deg); }
223
+ }
224
+
225
+ .card-orb {
226
+ position: absolute;
227
+ top: 50%;
228
+ left: 50%;
229
+ width: 300px;
230
+ height: 300px;
231
+ border-radius: 50%;
232
+ transform: translate(-50%, -50%);
233
+ filter: blur(60px);
234
+ opacity: 0.3;
235
+ z-index: 0;
236
+ transition: all 0.6s ease;
237
+ }
238
+
239
+ .card-sparkles {
240
+ position: absolute;
241
+ inset: 0;
242
+ z-index: 2;
243
+ pointer-events: none;
244
+ overflow: hidden;
245
+ }
246
+
247
+ .card-sparkle {
248
+ position: absolute;
249
+ width: 3px;
250
+ height: 3px;
251
+ background: #fff;
252
+ border-radius: 50%;
253
+ opacity: 0;
254
+ animation: sparkle 3s ease-in-out infinite;
255
+ }
256
+
257
+ @keyframes sparkle {
258
+ 0%, 100% {
259
+ opacity: 0;
260
+ transform: scale(0);
261
+ }
262
+ 50% {
263
+ opacity: 0.5;
264
+ transform: scale(1);
265
+ }
266
+ }
267
+
268
+ .card-gold-accent {
269
+ position: absolute;
270
+ top: 0;
271
+ left: 0;
272
+ right: 0;
273
+ height: 3px;
274
+ background: linear-gradient(90deg, transparent, var(--gold-light), var(--gold), var(--gold-light), transparent);
275
+ z-index: 3;
276
+ animation: gold-shimmer 3s ease-in-out infinite;
277
+ }
278
+
279
+ @keyframes gold-shimmer {
280
+ 0%, 100% { opacity: 0.7; }
281
+ 50% { opacity: 1; }
282
+ }
283
+
284
+ .card-pattern {
285
+ position: absolute;
286
+ inset: 0;
287
+ z-index: 1;
288
+ pointer-events: none;
289
+ opacity: 0.04;
290
+ background-image:
291
+ radial-gradient(circle at 25% 25%, #fff 1px, transparent 1px),
292
+ radial-gradient(circle at 75% 75%, #fff 1px, transparent 1px);
293
+ background-size: 24px 24px;
294
+ }
295
+
296
+ .card-header {
297
+ position: relative;
298
+ z-index: 3;
299
+ padding: 1.5rem 1.5rem 1rem;
300
+ display: flex;
301
+ align-items: center;
302
+ gap: 1rem;
303
+ }
304
+
305
+ .card-ovr-wrap {
306
+ position: relative;
307
+ flex-shrink: 0;
308
+ }
309
+
310
+ .card-ovr-ring {
311
+ position: absolute;
312
+ inset: -4px;
313
+ border-radius: 50%;
314
+ border: 2px solid rgba(255,255,255,0.2);
315
+ animation: ovr-pulse 2.5s ease-in-out infinite;
316
+ }
317
+
318
+ @keyframes ovr-pulse {
319
+ 0%, 100% {
320
+ transform: scale(1);
321
+ opacity: 0.6;
322
+ }
323
+ 50% {
324
+ transform: scale(1.06);
325
+ opacity: 1;
326
+ }
327
+ }
328
+
329
+ .card-ovr {
330
+ width: 62px;
331
+ height: 62px;
332
+ border-radius: 50%;
333
+ display: flex;
334
+ flex-direction: column;
335
+ align-items: center;
336
+ justify-content: center;
337
+ color: var(--color-white);
338
+ position: relative;
339
+ box-shadow: 0 0 30px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.25);
340
+ }
341
+
342
+ .card-ovr-num {
343
+ font-size: 1.4rem;
344
+ font-weight: 900;
345
+ line-height: 1;
346
+ letter-spacing: -0.03em;
347
+ text-shadow: 0 2px 8px rgba(0,0,0,0.3);
348
+ }
349
+
350
+ .card-ovr-label {
351
+ font-size: 0.4rem;
352
+ font-weight: 700;
353
+ text-transform: uppercase;
354
+ letter-spacing: 0.12em;
355
+ opacity: 0.7;
356
+ }
357
+
358
+ .card-title {
359
+ display: flex;
360
+ flex-direction: column;
361
+ }
362
+
363
+ .card-name {
364
+ font-size: 1.35rem;
365
+ font-weight: 800;
366
+ color: var(--color-white);
367
+ text-shadow: 0 2px 8px rgba(0,0,0,0.3);
368
+ line-height: 1.1;
369
+ letter-spacing: -0.01em;
370
+ }
371
+
372
+ .card-name-sub {
373
+ font-size: 0.55rem;
374
+ font-weight: 600;
375
+ color: rgba(255,255,255,0.5);
376
+ text-transform: uppercase;
377
+ letter-spacing: 0.08em;
378
+ margin-top: 0.1rem;
379
+ }
380
+
381
+ .card-price-gems {
382
+ margin-left: auto;
383
+ display: flex;
384
+ gap: 0.25rem;
385
+ padding: 0.35rem 0.65rem;
386
+ border-radius: 2rem;
387
+ background: rgba(0,0,0,0.25);
388
+ border: 1px solid rgba(255,255,255,0.1);
389
+ backdrop-filter: blur(4px);
390
+ font-size: 0;
391
+ }
392
+
393
+ .price-gem {
394
+ display: inline-block;
395
+ width: 8px;
396
+ height: 8px;
397
+ border-radius: 1px;
398
+ transform: rotate(45deg);
399
+ border: 1px solid rgba(255,255,255,0.3);
400
+ transition: all 0.3s ease;
401
+ }
402
+
403
+ .price-gem.filled {
404
+ background: var(--gold-light);
405
+ border-color: var(--gold);
406
+ box-shadow: 0 0 8px rgba(212,175,55,0.5);
407
+ animation: gem-glow 2s ease-in-out infinite;
408
+ }
409
+
410
+ @keyframes gem-glow {
411
+ 0%, 100% { box-shadow: 0 0 6px rgba(212,175,55,0.3); }
412
+ 50% { box-shadow: 0 0 12px rgba(212,175,55,0.6); }
413
+ }
414
+
415
+ .price-gem.empty {
416
+ background: transparent;
417
+ border-color: rgba(255,255,255,0.08);
418
+ }
419
+
420
+ .card-stats {
421
+ position: relative;
422
+ z-index: 3;
423
+ padding: 0.75rem 1.5rem 1.25rem;
424
+ display: flex;
425
+ flex-direction: column;
426
+ gap: 0.5rem;
427
+ }
428
+
429
+ .card-stat {
430
+ display: flex;
431
+ align-items: center;
432
+ gap: 0.65rem;
433
+ opacity: 0;
434
+ animation: stat-in 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
435
+ }
436
+
437
+ @keyframes stat-in {
438
+ from {
439
+ opacity: 0;
440
+ transform: translateX(-12px);
441
+ }
442
+ to {
443
+ opacity: 1;
444
+ transform: translateX(0);
445
+ }
446
+ }
447
+
448
+ .card-stat-label {
449
+ font-size: 0.55rem;
450
+ font-weight: 700;
451
+ color: rgba(255,255,255,0.5);
452
+ min-width: 36px;
453
+ text-transform: uppercase;
454
+ letter-spacing: 0.08em;
455
+ }
456
+
457
+ .card-stat-bar {
458
+ flex: 1;
459
+ height: 8px;
460
+ border-radius: 4px;
461
+ background: rgba(0,0,0,0.25);
462
+ overflow: hidden;
463
+ position: relative;
464
+ }
465
+
466
+ .card-stat-bar-glow {
467
+ position: absolute;
468
+ top: 0;
469
+ left: 0;
470
+ right: 0;
471
+ bottom: 0;
472
+ border-radius: 4px;
473
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.12);
474
+ }
475
+
476
+ .card-stat-fill {
477
+ height: 100%;
478
+ border-radius: 4px;
479
+ transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
480
+ position: relative;
481
+ }
482
+
483
+ .card-stat-fill::after {
484
+ content: '';
485
+ position: absolute;
486
+ top: 0;
487
+ left: 0;
488
+ right: 0;
489
+ height: 50%;
490
+ border-radius: 4px 4px 0 0;
491
+ background: linear-gradient(180deg, rgba(255,255,255,0.3), transparent);
492
+ }
493
+
494
+ .card-stat-val {
495
+ font-size: 0.7rem;
496
+ font-weight: 800;
497
+ color: rgba(255,255,255,0.7);
498
+ min-width: 24px;
499
+ text-align: right;
500
+ font-variant-numeric: tabular-nums;
501
+ }
502
+
503
+ /* ===== FIGHT / COMPARISON ===== */
504
+
505
+ .crystal-fight {
506
+ margin-top: 1rem;
507
+ border-radius: 1.25rem;
508
+ overflow: hidden;
509
+ border: 1px solid rgba(212,175,55,0.15);
510
+ background: rgba(0,0,0,0.15);
511
+ animation: fight-in 0.5s cubic-bezier(0.4, 0, 0.2, 1);
512
+ }
513
+
514
+ @keyframes fight-in {
515
+ from {
516
+ opacity: 0;
517
+ transform: translateY(12px);
518
+ }
519
+ to {
520
+ opacity: 1;
521
+ transform: translateY(0);
522
+ }
523
+ }
524
+
525
+ .crystal-fight-grid {
526
+ display: grid;
527
+ grid-template-columns: 1fr auto 1fr;
528
+ gap: 0;
529
+ }
530
+
531
+ .crystal-fight-vs {
532
+ display: flex;
533
+ flex-direction: column;
534
+ align-items: center;
535
+ justify-content: center;
536
+ padding: 1rem;
537
+ font-size: 0.9rem;
538
+ font-weight: 900;
539
+ color: var(--gold);
540
+ opacity: 0.35;
541
+ letter-spacing: 0.1em;
542
+ text-transform: uppercase;
543
+ writing-mode: vertical-lr;
544
+ animation: vs-pulse 2s ease-in-out infinite;
545
+ }
546
+
547
+ @keyframes vs-pulse {
548
+ 0%, 100% { opacity: 0.25; }
549
+ 50% { opacity: 0.5; }
550
+ }
551
+
552
+ .crystal-fight-close {
553
+ display: block;
554
+ width: 100%;
555
+ padding: 0.6rem;
556
+ border: none;
557
+ background: rgba(0,0,0,0.2);
558
+ color: rgba(255,255,255,0.4);
559
+ font-size: 0.65rem;
560
+ font-weight: 600;
561
+ text-transform: uppercase;
562
+ letter-spacing: 0.08em;
563
+ cursor: pointer;
564
+ transition: all 0.25s ease;
565
+ border-top: 1px solid rgba(255,255,255,0.05);
566
+ }
567
+
568
+ .crystal-fight-close:hover {
569
+ color: var(--gold-light);
570
+ background: rgba(0,0,0,0.35);
571
+ }
572
+
573
+ /* ===== DRAG TUTORIAL ===== */
574
+
575
+ .crystal-drag-hint {
576
+ margin-top: 0.6rem;
577
+ display: flex;
578
+ align-items: center;
579
+ justify-content: center;
580
+ gap: 0.35rem;
581
+ font-size: 0.55rem;
582
+ font-weight: 600;
583
+ color: var(--text-dimmed);
584
+ text-transform: uppercase;
585
+ letter-spacing: 0.08em;
586
+ opacity: 0.4;
587
+ transition: opacity 0.5s ease;
588
+ }
589
+
590
+ .crystal-drag-hint.dimmed {
591
+ opacity: 0.15;
592
+ }
593
+
594
+ .crystal-drag-hint-hidden {
595
+ display: none;
596
+ }
597
+
598
+ .drag-arrow {
599
+ display: inline-block;
600
+ animation: drag-arrow-move 1.5s ease-in-out infinite;
601
+ }
602
+
603
+ @keyframes drag-arrow-move {
604
+ 0%, 100% { transform: translateX(0); }
605
+ 50% { transform: translateX(4px); }
606
+ }
607
+
608
+ @media (max-width: 600px) {
609
+ .crystal-layout {
610
+ grid-template-columns: 1fr;
611
+ }
612
+ .crystal-list {
613
+ flex-direction: row;
614
+ overflow-x: auto;
615
+ padding-bottom: 0.25rem;
616
+ gap: 0.5rem;
617
+ }
618
+ .crystal-list-item {
619
+ flex-shrink: 0;
620
+ min-width: 120px;
621
+ }
622
+ .list-hint {
623
+ display: none;
624
+ }
625
+ }
package/src/tools.ts CHANGED
@@ -24,6 +24,7 @@ import { GMT_WORLD_TIMER_TOOL } from './tool/gmt-world-timer';
24
24
  import { QUARTZ_BATTERY_HEALTH_TOOL } from './tool/quartz-battery-health';
25
25
  import { MAINSPRING_FINDER_TOOL } from './tool/mainspring-finder';
26
26
  import { ALTITUDE_WATCH_ACCURACY_ESTIMATOR_TOOL } from './tool/altitude-watch-accuracy-estimator';
27
+ import { WATCH_CRYSTAL_MATERIAL_COMPARISON_TOOL } from './tool/watch-crystal-material-comparison';
27
28
 
28
29
  export const ALL_TOOLS: ToolDefinition[] = [
29
30
  WATCH_ACCURACY_TRACKER_TOOL,
@@ -50,6 +51,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
50
51
  QUARTZ_BATTERY_HEALTH_TOOL,
51
52
  MAINSPRING_FINDER_TOOL,
52
53
  ALTITUDE_WATCH_ACCURACY_ESTIMATOR_TOOL,
54
+ WATCH_CRYSTAL_MATERIAL_COMPARISON_TOOL,
53
55
  ];
54
56
 
55
57