@jjlmoya/utils-sports 1.44.0 → 1.45.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 (66) hide show
  1. package/package.json +1 -1
  2. package/src/category/i18n/de.ts +4 -9
  3. package/src/category/i18n/en.ts +4 -9
  4. package/src/category/i18n/es.ts +4 -9
  5. package/src/category/i18n/fr.ts +4 -9
  6. package/src/category/i18n/id.ts +4 -9
  7. package/src/category/i18n/it.ts +4 -9
  8. package/src/category/i18n/ja.ts +4 -9
  9. package/src/category/i18n/ko.ts +4 -9
  10. package/src/category/i18n/nl.ts +4 -9
  11. package/src/category/i18n/pl.ts +4 -9
  12. package/src/category/i18n/pt.ts +4 -9
  13. package/src/category/i18n/ru.ts +5 -10
  14. package/src/category/i18n/sv.ts +4 -9
  15. package/src/category/i18n/tr.ts +4 -9
  16. package/src/category/i18n/zh.ts +4 -9
  17. package/src/entries.ts +4 -0
  18. package/src/index.ts +3 -0
  19. package/src/pages/[locale]/[slug].astro +3 -11
  20. package/src/tests/pagespeed_best_practices.test.ts +2 -2
  21. package/src/tests/seo_length.test.ts +2 -1
  22. package/src/tool/baseballScoreKeeper/component.astro +1 -1
  23. package/src/tool/baseballScoreKeeper/ui-init.ts +1 -1
  24. package/src/tool/baseballScoreKeeper/ui.ts +1 -1
  25. package/src/tool/swimCssCalculator/entry.ts +4 -1
  26. package/src/tool/swimCssCalculator/ui.ts +1 -1
  27. package/src/tool/tennisScoreKeeper/events.ts +1 -1
  28. package/src/tool/tennisScoreKeeper/render.ts +26 -21
  29. package/src/tool/tennisScoreKeeper/ui.ts +1 -1
  30. package/src/tool/tournamentBracket/index.ts +1 -0
  31. package/src/tool/volleyballScoreKeeper/actions.ts +104 -0
  32. package/src/tool/volleyballScoreKeeper/audio.ts +24 -0
  33. package/src/tool/volleyballScoreKeeper/bibliography.astro +6 -0
  34. package/src/tool/volleyballScoreKeeper/bibliography.ts +8 -0
  35. package/src/tool/volleyballScoreKeeper/component.astro +204 -0
  36. package/src/tool/volleyballScoreKeeper/controller.ts +183 -0
  37. package/src/tool/volleyballScoreKeeper/court-renderer.ts +51 -0
  38. package/src/tool/volleyballScoreKeeper/entry.ts +30 -0
  39. package/src/tool/volleyballScoreKeeper/i18n/de.ts +157 -0
  40. package/src/tool/volleyballScoreKeeper/i18n/en.ts +157 -0
  41. package/src/tool/volleyballScoreKeeper/i18n/es.ts +157 -0
  42. package/src/tool/volleyballScoreKeeper/i18n/fr.ts +157 -0
  43. package/src/tool/volleyballScoreKeeper/i18n/id.ts +157 -0
  44. package/src/tool/volleyballScoreKeeper/i18n/it.ts +157 -0
  45. package/src/tool/volleyballScoreKeeper/i18n/ja.ts +157 -0
  46. package/src/tool/volleyballScoreKeeper/i18n/ko.ts +157 -0
  47. package/src/tool/volleyballScoreKeeper/i18n/nl.ts +157 -0
  48. package/src/tool/volleyballScoreKeeper/i18n/pl.ts +157 -0
  49. package/src/tool/volleyballScoreKeeper/i18n/pt.ts +157 -0
  50. package/src/tool/volleyballScoreKeeper/i18n/ru.ts +157 -0
  51. package/src/tool/volleyballScoreKeeper/i18n/sv.ts +157 -0
  52. package/src/tool/volleyballScoreKeeper/i18n/tr.ts +157 -0
  53. package/src/tool/volleyballScoreKeeper/i18n/zh.ts +157 -0
  54. package/src/tool/volleyballScoreKeeper/index.ts +9 -0
  55. package/src/tool/volleyballScoreKeeper/logic.test.ts +124 -0
  56. package/src/tool/volleyballScoreKeeper/logic.ts +13 -0
  57. package/src/tool/volleyballScoreKeeper/scoring.ts +91 -0
  58. package/src/tool/volleyballScoreKeeper/seo.astro +14 -0
  59. package/src/tool/volleyballScoreKeeper/sets.ts +34 -0
  60. package/src/tool/volleyballScoreKeeper/state.ts +31 -0
  61. package/src/tool/volleyballScoreKeeper/storage.ts +29 -0
  62. package/src/tool/volleyballScoreKeeper/ui.ts +83 -0
  63. package/src/tool/volleyballScoreKeeper/view.ts +127 -0
  64. package/src/tool/volleyballScoreKeeper/volleyball-scorekeeper.css +637 -0
  65. package/src/tools.ts +2 -0
  66. package/src/types.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-sports",
3
- "version": "1.44.0",
3
+ "version": "1.45.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: 'Der Trend für 2026 ist <strong>datenbasiertes Training (VBT)</strong>. Wearables und Tracking-Apps demokratisieren den Zugang zu Analysen, die früher dem Spitzensport vorbehalten waren. Diese Tools bringen Sie an die Spitze und ermöglichen es Ihnen, intelligenter zu trainieren, nicht unbedingt härter.',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: 'Reflexe', html: '<p>Präzision in Millisekunden</p>' },
102
- { type: 'card', title: 'Fortschritt', html: '<p>Historische Visualisierung</p>' },
103
- { type: 'card', title: 'Wettkampf', html: '<p>Strukturierte Verwaltung</p>' },
104
- { type: 'card', title: 'Präzision', html: '<p>Absolute Daten</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: 'Reflexe', html: '<p>Millisekundengenaue Präzision</p>' },
99
+ { type: 'card', title: 'Fortschritt', html: '<p>Historische Visualisierung</p>' },
100
+ { type: 'card', title: 'Wettbewerb', html: '<p>Strukturierte Verwaltung</p>' },
101
+ { type: 'card', title: 'Präzision', html: '<p>Absolute Daten</p>' },
107
102
  ],
108
103
  };
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: 'The 2026 trend is <strong>Velocity-Based Training (VBT)</strong>. Wearable devices and tracking apps are democratizing access to analytics that were once exclusive to elite sports. These tools put you at the cutting edge, allowing you to train smarter, not necessarily harder.',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: 'Reflexes', html: '<p>Millisecond precision</p>' },
102
- { type: 'card', title: 'Progress', html: '<p>Historical visualization</p>' },
103
- { type: 'card', title: 'Competition', html: '<p>Structured management</p>' },
104
- { type: 'card', title: 'Precision', html: '<p>Absolute data</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: 'Reflexes', html: '<p>Millisecond precision</p>' },
99
+ { type: 'card', title: 'Progress', html: '<p>Historical visualization</p>' },
100
+ { type: 'card', title: 'Competition', html: '<p>Structured management</p>' },
101
+ { type: 'card', title: 'Precision', html: '<p>Absolute data</p>' },
107
102
  ],
108
103
  };
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: 'La tendencia de 2026 es el <strong>Entrenamiento Basado en Datos (VBT)</strong>. Los dispositivos vestibles y las aplicaciones de seguimiento están democratizando el acceso a la analítica que antes era exclusiva del deporte de élite. Estas herramientas te sitúan en la vanguardia, permitiéndote entrenar de forma más inteligente, no necesariamente más dura.',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: 'Reflejos', html: '<p>Milisegundos de precisión</p>' },
102
- { type: 'card', title: 'Progreso', html: '<p>Visualización histórica</p>' },
103
- { type: 'card', title: 'Competencia', html: '<p>Gestión estructurada</p>' },
104
- { type: 'card', title: 'Precisión', html: '<p>Datos absolutos</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: 'Reflejos', html: '<p>Milisegundos de precisión</p>' },
99
+ { type: 'card', title: 'Progreso', html: '<p>Visualización histórica</p>' },
100
+ { type: 'card', title: 'Competencia', html: '<p>Gestión estructurada</p>' },
101
+ { type: 'card', title: 'Precisión', html: '<p>Datos absolutos</p>' },
107
102
  ],
108
103
  };
@@ -82,14 +82,9 @@ export const content: CategoryLocaleContent = {
82
82
  type: 'paragraph',
83
83
  html: 'La tendance de 2026 est l\'<strong>Entraînement Basé sur la Vélocité (EBV)</strong>. Les appareils connectés et les applications de suivi démocratisent l\'accès à l\'analytique autrefois réservée au sport d\'élite. Ces outils vous placent à l\'avant-garde, vous permettant de vous entraîner plus intelligemment, pas nécessairement plus durement.',
84
84
  },
85
- {
86
- type: 'grid',
87
- columns: [
88
- { type: 'card', title: 'Réflexes', html: '<p>Précision en millisecondes</p>' },
89
- { type: 'card', title: 'Progression', html: '<p>Visualisation historique</p>' },
90
- { type: 'card', title: 'Compétition', html: '<p>Gestion structurée</p>' },
91
- { type: 'card', title: 'Précision', html: '<p>Données absolues</p>' },
92
- ],
93
- },
85
+ { type: 'card', title: 'Réflexes', html: '<p>Précision en millisecondes</p>' },
86
+ { type: 'card', title: 'Progression', html: '<p>Visualisation historique</p>' },
87
+ { type: 'card', title: 'Compétition', html: '<p>Gestion structurée</p>' },
88
+ { type: 'card', title: 'Précision', html: '<p>Données absolues</p>' },
94
89
  ],
95
90
  };
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: 'Tren tahun 2026 adalah <strong>Data-Driven Training (VBT)</strong>. Perangkat yang dapat dikenakan dan aplikasi pelacakan mendemokrasikan akses ke analitik yang dulunya eksklusif untuk olahraga elit. Alat-alat ini menempatkan Anda di garis depan, memungkinkan Anda berlatih dengan lebih cerdas, bukan harus lebih keras.',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: 'Refleks', html: '<p>Presisi milidetik</p>' },
102
- { type: 'card', title: 'Kemajuan', html: '<p>Visualisasi historis</p>' },
103
- { type: 'card', title: 'Kompetisi', html: '<p>Manajemen terstruktur</p>' },
104
- { type: 'card', title: 'Presisi', html: '<p>Data absolut</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: 'Refleks', html: '<p>Presisi milidetik</p>' },
99
+ { type: 'card', title: 'Kemajuan', html: '<p>Visualisasi historis</p>' },
100
+ { type: 'card', title: 'Kompetisi', html: '<p>Manajemen terstruktur</p>' },
101
+ { type: 'card', title: 'Presisi', html: '<p>Data absolut</p>' },
107
102
  ],
108
103
  };
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: 'La tendenza del 2026 è l\'<strong>Allenamento Basato sui Dati (VBT)</strong>. I dispositivi indossabili e le app di monitoraggio stanno democratizzando l\'accesso all\'analitica che prima era esclusiva dello sport d\'élite. Questi strumenti ti pongono all\'avanguardia, permettendoti di allenarti in modo più intelligente, non necessariamente più duramente.',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: 'Riflessi', html: '<p>Millisecondi di precisione</p>' },
102
- { type: 'card', title: 'Progresso', html: '<p>Visualizzazione storica</p>' },
103
- { type: 'card', title: 'Competizione', html: '<p>Gestione strutturata</p>' },
104
- { type: 'card', title: 'Precisione', html: '<p>Dati assoluti</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: 'Riflessi', html: '<p>Precisione al millisecondo</p>' },
99
+ { type: 'card', title: 'Progresso', html: '<p>Visualizzazione storica</p>' },
100
+ { type: 'card', title: 'Competizione', html: '<p>Gestione strutturata</p>' },
101
+ { type: 'card', title: 'Precisione', html: '<p>Dati assoluti</p>' },
107
102
  ],
108
103
  };
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: '2026年のトレンドは、<strong>ベロシティ・ベースド・トレーニング(VBT)</strong>です。ウェアラブルデバイスやトラッキングアプリにより、かつてはエリートスポーツに限定されていた分析へのアクセスが民主化されています。これらのツールは、あなたを最先端に位置づけ、単にハードにではなく、よりスマートにトレーニングすることを可能にします。',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: '反射神経', html: '<p>ミリ秒単位の精度</p>' },
102
- { type: 'card', title: '進捗', html: '<p>履歴の視覚化</p>' },
103
- { type: 'card', title: '競技', html: '<p>構造化された管理</p>' },
104
- { type: 'card', title: '精度', html: '<p>絶対的なデータ</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: '反射神経', html: '<p>ミリ秒単位の精度</p>' },
99
+ { type: 'card', title: '進捗', html: '<p>履歴の視覚化</p>' },
100
+ { type: 'card', title: '競技', html: '<p>構造化された管理</p>' },
101
+ { type: 'card', title: '精度', html: '<p>絶対的なデータ</p>' },
107
102
  ],
108
103
  };
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: '2026년 트렌드는 <strong>속도 기반 트레이닝(VBT)</strong>입니다. 웨어러블 장치와 추적 앱은 한때 엘리트 스포츠에만 국한되었던 분석에 대한 접근을 대중화하고 있습니다. 이러한 도구는 귀하를 최첨단에 두어 단순히 더 열심히 하는 것이 아니라 더 똑똑하게 트레이닝할 수 있게 해줍니다.',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: '반사 신경', html: '<p>밀리초 단위 정밀도</p>' },
102
- { type: 'card', title: '진행 상황', html: '<p>과거 시각화</p>' },
103
- { type: 'card', title: '경쟁', html: '<p>구조화된 관리</p>' },
104
- { type: 'card', title: '정밀도', html: '<p>절대적인 데이터</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: '반사 신경', html: '<p>밀리초 단위 정밀도</p>' },
99
+ { type: 'card', title: '진행 상황', html: '<p>과거 시각화</p>' },
100
+ { type: 'card', title: '경쟁', html: '<p>구조화된 관리</p>' },
101
+ { type: 'card', title: '정밀도', html: '<p>절대적인 데이터</p>' },
107
102
  ],
108
103
  };
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: 'De trend van 2026 is <strong>Data-Driven Training (VBT)</strong>. Wearables en tracking-apps democratiseren de toegang tot analytics die voorheen exclusief waren voor de topsport. Deze tools plaatsen je in de voorhoede, waardoor je slimmer kunt trainen, niet per se harder.',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: 'Reflexen', html: '<p>Milliseconden precisie</p>' },
102
- { type: 'card', title: 'Voortgang', html: '<p>Historische visualisatie</p>' },
103
- { type: 'card', title: 'Competitie', html: '<p>Gestructureerd beheer</p>' },
104
- { type: 'card', title: 'Precisie', html: '<p>Absolute gegevens</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: 'Reflexen', html: '<p>Milliseconden precisie</p>' },
99
+ { type: 'card', title: 'Voortgang', html: '<p>Historische visualisatie</p>' },
100
+ { type: 'card', title: 'Competitie', html: '<p>Gestructureerd beheer</p>' },
101
+ { type: 'card', title: 'Precisie', html: '<p>Absolute gegevens</p>' },
107
102
  ],
108
103
  };
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: 'Trendem roku 2026 jest <strong>Trening Oparty na Danych (VBT)</strong>. Urządzenia ubieralne i aplikacje do śledzenia postępów demokratyzują dostęp do analityki, która wcześniej była zarezerwowana dla sportu wyczynowego. Te narzędzia stawiają Cię w awangardzie, pozwalając trenować mądrzej, a niekoniecznie ciężej.',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: 'Refleks', html: '<p>Milisecundy precyzji</p>' },
102
- { type: 'card', title: 'Postęp', html: '<p>Wizualizacja historyczna</p>' },
103
- { type: 'card', title: 'Zawody', html: '<p>Zarządzanie strukturalne</p>' },
104
- { type: 'card', title: 'Precyzja', html: '<p>Dane absolutne</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: 'Refleks', html: '<p>Milisecundy precyzji</p>' },
99
+ { type: 'card', title: 'Postęp', html: '<p>Wizualizacja historyczna</p>' },
100
+ { type: 'card', title: 'Zawody', html: '<p>Zarządzanie strukturalne</p>' },
101
+ { type: 'card', title: 'Precyzja', html: '<p>Dane absolutne</p>' },
107
102
  ],
108
103
  };
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: 'A tendência de 2026 é o <strong>Treino Baseado em Dados (VBT)</strong>. Os dispositivos vestíveis e as aplicações de acompanhamento estão a democratizar o acesso à analítica que antes era exclusiva do desporto de elite. Estas ferramentas colocam-no na vanguarda, permitindo-lhe treinar de forma mais inteligente, não necessariamente mais dura.',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: 'Reflexos', html: '<p>Milissegundos de precisão</p>' },
102
- { type: 'card', title: 'Progresso', html: '<p>Visualização histórica</p>' },
103
- { type: 'card', title: 'Competição', html: '<p>Gestão estruturada</p>' },
104
- { type: 'card', title: 'Precisão', html: '<p>Dados absolutos</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: 'Reflexos', html: '<p>Milissegundos de precisão</p>' },
99
+ { type: 'card', title: 'Progresso', html: '<p>Visualização histórica</p>' },
100
+ { type: 'card', title: 'Competição', html: '<p>Gestão estruturada</p>' },
101
+ { type: 'card', title: 'Precisão', html: '<p>Dados absolutos</p>' },
107
102
  ],
108
103
  };
@@ -1,4 +1,4 @@
1
- import type { CategoryLocaleContent } from '../../types';
1
+ import type { CategoryLocaleContent } from '../../types';
2
2
 
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'sports',
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: 'Трендом 2026 года является <strong>тренировка на основе скорости (Velocity-Based Training, VBT)</strong>. Носимые устройства и приложения для отслеживания демократизируют доступ к аналитике, которая когда-то была эксклюзивной для элитного спорта. Эти инструменты ставят вас на передовую, позволяя тренироваться умнее, а не просто интенсивнее.',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: 'Рефлексы', html: '<p>Прецизионность в миллисекундах</p>' },
102
- { type: 'card', title: 'Прогресс', html: '<p>Историческая визуализация</p>' },
103
- { type: 'card', title: 'Соревнования', html: '<p>Структурированное управление</p>' },
104
- { type: 'card', title: 'Точность', html: '<p>Абсолютные данные</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: 'Рефлексы', html: '<p>Прецизионность в миллисекундах</p>' },
99
+ { type: 'card', title: 'Прогресс', html: '<p>Историческая визуализация</p>' },
100
+ { type: 'card', title: 'Соревнования', html: '<p>Структурированное управление</p>' },
101
+ { type: 'card', title: 'Точность', html: '<p>Абсолютные данные</p>' },
107
102
  ],
108
103
  };
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: 'Trenden för 2026 är <strong>Databaserad Träning (VBT)</strong>. Wearables och spårningsappar demokratiserar tillgången till analys som tidigare var exklusiv för elitidrott. Dessa verktyg placerar dig i framkant och låter dig träna smartare, inte nödvändigtvis hårdare.',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: 'Reflexer', html: '<p>Millisekunds precision</p>' },
102
- { type: 'card', title: 'Framsteg', html: '<p>Historisk visualisering</p>' },
103
- { type: 'card', title: 'Tävling', html: '<p>Strukturerad hantering</p>' },
104
- { type: 'card', title: 'Precision', html: '<p>Absolut data</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: 'Reflexer', html: '<p>Millisekunds precision</p>' },
99
+ { type: 'card', title: 'Framsteg', html: '<p>Historisk visualisering</p>' },
100
+ { type: 'card', title: 'Tävling', html: '<p>Strukturerad hantering</p>' },
101
+ { type: 'card', title: 'Precision', html: '<p>Absolut data</p>' },
107
102
  ],
108
103
  };
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: '2026 eğilimi, <strong>Veriye Dayalı Antrenman (VBT)</strong>\'dır. Giyilebilir cihazlar ve takip uygulamaları, eskiden elit sporlara özel olan analitiklere erişimi demokratikleştiriyor. Bu araçlar sizi ön plana çıkararak daha sert değil, daha akıllıca antrenman yapmanızı sağlar.',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: 'Refleksler', html: '<p>Milisaniye hassasiyeti</p>' },
102
- { type: 'card', title: 'İlerleme', html: '<p>Geçmiş görselleştirme</p>' },
103
- { type: 'card', title: 'Müsabaka', html: '<p>Yapılandırılmış yönetim</p>' },
104
- { type: 'card', title: 'Hassasiyet', html: '<p>Mutlak veriler</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: 'Refleksler', html: '<p>Milisaniye hassasiyeti</p>' },
99
+ { type: 'card', title: 'İlerleme', html: '<p>Geçmiş görselleştirme</p>' },
100
+ { type: 'card', title: 'Müsabaka', html: '<p>Yapılandırılmış yönetim</p>' },
101
+ { type: 'card', title: 'Hassasiyet', html: '<p>Mutlak veriler</p>' },
107
102
  ],
108
103
  };
@@ -95,14 +95,9 @@ export const content: CategoryLocaleContent = {
95
95
  type: 'paragraph',
96
96
  html: '2026 年的趋势是<strong>基于速度的训练 (Velocity-Based Training, VBT)</strong>。穿戴式设备和追踪应用正使曾经专属精英运动的数据分析变得平民化。这些工具让您处于前沿,让您更聪明地训练,而不仅仅是更刻苦。',
97
97
  },
98
- {
99
- type: 'grid',
100
- columns: [
101
- { type: 'card', title: '反应', html: '<p>毫秒级精度</p>' },
102
- { type: 'card', title: '进度', html: '<p>历史可视化</p>' },
103
- { type: 'card', title: '竞赛', html: '<p>规范化管理</p>' },
104
- { type: 'card', title: '精准度', html: '<p>绝对数据</p>' },
105
- ],
106
- },
98
+ { type: 'card', title: '反应', html: '<p>毫秒级精度</p>' },
99
+ { type: 'card', title: '进度', html: '<p>历史可视化</p>' },
100
+ { type: 'card', title: '竞赛', html: '<p>规范化管理</p>' },
101
+ { type: 'card', title: '精准度', html: '<p>绝对数据</p>' },
107
102
  ],
108
103
  };
package/src/entries.ts CHANGED
@@ -36,6 +36,8 @@ export { runningPacePredictor } from './tool/runningPacePredictor/entry';
36
36
  export type { RunningPacePredictorLocaleContent } from './tool/runningPacePredictor/entry';
37
37
  export { swimCssCalculator } from './tool/swimCssCalculator/entry';
38
38
  export type { SwimCssCalculatorLocaleContent } from './tool/swimCssCalculator/entry';
39
+ export { volleyballScoreKeeper } from './tool/volleyballScoreKeeper/entry';
40
+ export type { VolleyballScoreKeeperLocaleContent } from './tool/volleyballScoreKeeper/entry';
39
41
 
40
42
  export { sportsCategory } from './category';
41
43
 
@@ -58,6 +60,7 @@ import { baseballScoreKeeper } from './tool/baseballScoreKeeper/entry';
58
60
  import { penaltyShootout } from './tool/penaltyShootout/entry';
59
61
  import { runningPacePredictor } from './tool/runningPacePredictor/entry';
60
62
  import { swimCssCalculator } from './tool/swimCssCalculator/entry';
63
+ import { volleyballScoreKeeper } from './tool/volleyballScoreKeeper/entry';
61
64
 
62
65
  export const ALL_ENTRIES = [
63
66
  basketScoreKeeper,
@@ -79,4 +82,5 @@ export const ALL_ENTRIES = [
79
82
  penaltyShootout,
80
83
  runningPacePredictor,
81
84
  swimCssCalculator,
85
+ volleyballScoreKeeper,
82
86
  ];
package/src/index.ts CHANGED
@@ -34,3 +34,6 @@ export type { EloRatingCalculatorUI, EloRatingCalculatorLocaleContent } from './
34
34
 
35
35
  export { PENALTY_SHOOTOUT_TOOL, penaltyShootout } from './tool/penaltyShootout/index';
36
36
  export type { PenaltyShootoutUI, PenaltyShootoutLocaleContent } from './tool/penaltyShootout/index';
37
+
38
+ export { VOLLEYBALL_SCOREKEEPER_TOOL, volleyballScoreKeeper } from './tool/volleyballScoreKeeper/index';
39
+ export type { VolleyballScoreKeeperUI, VolleyballScoreKeeperLocaleContent } from './tool/volleyballScoreKeeper/index';
@@ -70,20 +70,12 @@ interface NavItem {
70
70
  isActive?: boolean;
71
71
  }
72
72
 
73
- interface Props {
74
- Component: unknown;
75
- locale: KnownLocale;
76
- content: ToolLocaleContent;
77
- localeUrls: Partial<Record<KnownLocale, string>>;
78
- allToolsNav: NavItem[];
79
- englishSlug: string;
80
- }
81
-
82
73
  const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
83
74
 
84
75
  const cssFiles = import.meta.glob("../../tool/**" + "/" + "*.css", { query: "?raw", import: "default" });
85
76
  const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
86
- const toolCss = cssKey ? await cssFiles[cssKey]() as string : "";
77
+ const loader = cssKey ? cssFiles[cssKey] : undefined;
78
+ const toolCss = loader ? ((await loader()) as string) : "";
87
79
 
88
80
  const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
89
81
 
@@ -132,7 +124,7 @@ const titleBase = words.slice(1).join(" ") || "";
132
124
  </section>
133
125
 
134
126
  <section class="section-faq">
135
- <FAQSection items={content.faq} inLanguage={locale} />
127
+ <FAQSection items={content.faq} />
136
128
  </section>
137
129
 
138
130
  <section class="section-bibliography">
@@ -171,8 +171,8 @@ describe('PageSpeed best-practice guards', () => {
171
171
  ALL_TOOLS.forEach((tool) => {
172
172
  Object.entries(tool.entry.i18n).forEach(([locale, loader]) => {
173
173
  it(`${tool.entry.id}/${locale} keeps SEO headings sequential`, async () => {
174
- if (!loader) return;
175
- const content = (await loader()) as ToolLocaleContent;
174
+ if (typeof loader !== 'function') return;
175
+ const content = (await (loader as () => Promise<ToolLocaleContent>)());
176
176
  const levels = headingLevels(content.seo);
177
177
  const failures = findHeadingLevelJumps(levels);
178
178
 
@@ -10,7 +10,8 @@ describe('SEO Content Length Validation', () => {
10
10
  describe(`Tool: ${entry.id}`, () => {
11
11
  Object.keys(entry.i18n).forEach((locale) => {
12
12
  it(`${locale}: SEO section should exist`, async () => {
13
- const loader = (entry.i18n as Record<string, () => Promise<{ seo?: unknown[] }>>)[locale];
13
+ const loader = (entry.i18n as Record<string, (() => Promise<{ seo?: unknown[] }>) | undefined>)[locale];
14
+ if (!loader) return;
14
15
  const content = await loader();
15
16
  if (!content.seo) return;
16
17
  expect(Array.isArray(content.seo)).toBe(true);
@@ -8,7 +8,7 @@ interface Props {
8
8
  }
9
9
 
10
10
  const { ui } = Astro.props;
11
- const t = (ui ?? {}) as BaseballScoreKeeperUI;
11
+ const t = (ui ?? {}) as unknown as BaseballScoreKeeperUI;
12
12
  ---
13
13
 
14
14
  <div class="bb-app" id="bb-app" data-bb-ui={JSON.stringify(t)}>
@@ -38,7 +38,7 @@ function renderAll(state: GameState) {
38
38
  renderInnings(state);
39
39
  }
40
40
 
41
- function renderDots(id: string, filled: number, total: number, color: string) {
41
+ function renderDots(id: string, filled: number, _total: number, color: string) {
42
42
  const el = q(id);
43
43
  if (!el) return;
44
44
  const dots = el.querySelectorAll('.bb-dot');
@@ -1,4 +1,4 @@
1
- export interface BaseballScoreKeeperUI {
1
+ export interface BaseballScoreKeeperUI extends Record<string, string> {
2
2
  title: string;
3
3
  description: string;
4
4
  away: string;
@@ -1,6 +1,9 @@
1
- import type { SportsToolEntry } from '../../types';
1
+ import type { SportsToolEntry, ToolLocaleContent } from '../../types';
2
2
  import type { SwimCssCalculatorUI } from './ui';
3
3
 
4
+ export type { SwimCssCalculatorUI };
5
+ export type SwimCssCalculatorLocaleContent = ToolLocaleContent<SwimCssCalculatorUI>;
6
+
4
7
  export const swimCssCalculatorEntry: SportsToolEntry<SwimCssCalculatorUI> = {
5
8
  id: 'css-swim-calculator',
6
9
  icons: {
@@ -1,4 +1,4 @@
1
- export interface SwimCssCalculatorUI {
1
+ export interface SwimCssCalculatorUI extends Record<string, string> {
2
2
  title: string;
3
3
  subtitle: string;
4
4
  unitLabel: string;
@@ -102,7 +102,7 @@ export function setupFullscreen(card: HTMLElement, api: TennisAPI): void {
102
102
  });
103
103
  }
104
104
 
105
- export function setupNameInputs(score: Record<string, unknown>, save: () => void): void {
105
+ export function setupNameInputs(_score: unknown, save: () => void): void {
106
106
  const na = document.getElementById('tn-name-a') as HTMLInputElement;
107
107
  const nb = document.getElementById('tn-name-b') as HTMLInputElement;
108
108
  [na, nb].forEach((inp) => {
@@ -35,33 +35,38 @@ function renderStatus(score: MatchScore, t: TennisScoreKeeperUI): void {
35
35
  st.textContent = '';
36
36
  }
37
37
 
38
+ function fillHistorySetBox(boxA: HTMLDivElement, boxB: HTMLDivElement, set: { gamesA: number; gamesB: number; tiebreakPointsA?: number; tiebreakPointsB?: number }): void {
39
+ boxA.textContent = String(set.gamesA);
40
+ boxB.textContent = String(set.gamesB);
41
+ if (set.tiebreakPointsA !== undefined) {
42
+ appendTiebreakScore(boxA, String(set.tiebreakPointsA));
43
+ appendTiebreakScore(boxB, String(set.tiebreakPointsB));
44
+ }
45
+ }
46
+
47
+ function fillActiveSetBox(boxA: HTMLDivElement, boxB: HTMLDivElement, score: MatchScore): void {
48
+ boxA.classList.add('tn-set-box-active');
49
+ boxB.classList.add('tn-set-box-active');
50
+ boxA.textContent = String(score.gamesWonInCurrentSetA);
51
+ boxB.textContent = String(score.gamesWonInCurrentSetB);
52
+ if (score.inTiebreak) {
53
+ appendTiebreakScore(boxA, String(score.tiebreakPointsA));
54
+ appendTiebreakScore(boxB, String(score.tiebreakPointsB));
55
+ }
56
+ }
57
+
38
58
  function buildSetBox(score: MatchScore, i: number): { boxA: HTMLDivElement; boxB: HTMLDivElement } {
39
59
  const boxA = document.createElement('div');
40
60
  boxA.className = 'tn-set-box';
41
61
  const boxB = document.createElement('div');
42
62
  boxB.className = 'tn-set-box';
43
63
  const activeIndex = score.setHistory.length;
64
+ const set = score.setHistory[i];
44
65
 
45
- if (i < score.setHistory.length) {
46
- const set = score.setHistory[i];
47
- boxA.textContent = String(set.gamesA);
48
- boxB.textContent = String(set.gamesB);
49
- if (set.tiebreakPointsA !== undefined) {
50
- appendTiebreakScore(boxA, String(set.tiebreakPointsA));
51
- appendTiebreakScore(boxB, String(set.tiebreakPointsB));
52
- }
66
+ if (set) {
67
+ fillHistorySetBox(boxA, boxB, set);
53
68
  } else if (i === activeIndex && !checkSetOver(score)) {
54
- boxA.classList.add('tn-set-box-active');
55
- boxB.classList.add('tn-set-box-active');
56
- boxA.textContent = String(score.gamesWonInCurrentSetA);
57
- boxB.textContent = String(score.gamesWonInCurrentSetB);
58
- if (score.inTiebreak) {
59
- appendTiebreakScore(boxA, String(score.tiebreakPointsA));
60
- appendTiebreakScore(boxB, String(score.tiebreakPointsB));
61
- }
62
- } else {
63
- boxA.textContent = '';
64
- boxB.textContent = '';
69
+ fillActiveSetBox(boxA, boxB, score);
65
70
  }
66
71
  return { boxA, boxB };
67
72
  }
@@ -136,7 +141,7 @@ function getInputName(side: PlayerSide, fallback: string): string {
136
141
  return inp?.value?.trim() || fallback;
137
142
  }
138
143
 
139
- function renderPlayerNames(score: MatchScore, t: TennisScoreKeeperUI): void {
144
+ function renderPlayerNames(_score: MatchScore, t: TennisScoreKeeperUI): void {
140
145
  const nameA = getInputName('a', t.playerA);
141
146
  const nameB = getInputName('b', t.playerB);
142
147
  const hA = el('tn-hist-name-a');
@@ -191,7 +196,7 @@ export function showWinner(name: string, score: MatchScore): void {
191
196
  const d = document.createElement('div');
192
197
  d.className = 'tn-confetti-particle';
193
198
  d.style.left = `${Math.random() * 100}%`;
194
- d.style.background = colors[Math.floor(Math.random() * colors.length)];
199
+ d.style.background = colors[Math.floor(Math.random() * colors.length)] ?? '#ccff00';
195
200
  d.style.animationDuration = `${2 + Math.random() * 3}s`;
196
201
  d.style.animationDelay = `${Math.random() * 0.6}s`;
197
202
  c.appendChild(d);
@@ -1,4 +1,4 @@
1
- export interface TennisScoreKeeperUI {
1
+ export interface TennisScoreKeeperUI extends Record<string, string> {
2
2
  playerA: string;
3
3
  playerB: string;
4
4
  winnerLabel: string;
@@ -1,3 +1,4 @@
1
+ import type { ToolDefinition } from '../../types';
1
2
  import { tournamentBracket } from './entry';
2
3
  export * from './entry';
3
4
  export const TOURNAMENT_BRACKET_TOOL: ToolDefinition = {