@jjlmoya/utils-tabletop 1.17.0 → 1.19.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 (62) hide show
  1. package/package.json +5 -3
  2. package/src/tests/pagespeed_best_practices.test.ts +198 -0
  3. package/src/tests/seo_parity.test.ts +60 -0
  4. package/src/tests/translation_copy.test.ts +124 -0
  5. package/src/tool/board-game-timer/components/SetupPanel.astro +8 -8
  6. package/src/tool/board-game-timer/modules/particles.ts +4 -4
  7. package/src/tool/decision-wheel/i18n/de.ts +28 -0
  8. package/src/tool/decision-wheel/i18n/es.ts +28 -0
  9. package/src/tool/decision-wheel/i18n/fr.ts +28 -0
  10. package/src/tool/decision-wheel/i18n/id.ts +28 -0
  11. package/src/tool/decision-wheel/i18n/it.ts +28 -0
  12. package/src/tool/decision-wheel/i18n/ja.ts +28 -0
  13. package/src/tool/decision-wheel/i18n/ko.ts +28 -0
  14. package/src/tool/decision-wheel/i18n/nl.ts +28 -0
  15. package/src/tool/decision-wheel/i18n/pl.ts +28 -0
  16. package/src/tool/decision-wheel/i18n/pt.ts +28 -0
  17. package/src/tool/decision-wheel/i18n/sv.ts +28 -0
  18. package/src/tool/decision-wheel/i18n/tr.ts +28 -0
  19. package/src/tool/decision-wheel/i18n/zh.ts +28 -0
  20. package/src/tool/decision-wheel/ui.ts +1 -1
  21. package/src/tool/dice-roller-simulator/client.ts +13 -11
  22. package/src/tool/dice-roller-simulator/components/DiceGrid.astro +1 -1
  23. package/src/tool/fantasy-runes-translator/client.ts +1 -1
  24. package/src/tool/fantasy-runes-translator/component.astro +2 -2
  25. package/src/tool/first-player-selector/client.ts +1 -1
  26. package/src/tool/hidden-role-dealer/component.astro +10 -8
  27. package/src/tool/hidden-role-dealer/i18n/de.ts +10 -1
  28. package/src/tool/hidden-role-dealer/i18n/fr.ts +4 -1
  29. package/src/tool/hidden-role-dealer/i18n/id.ts +11 -1
  30. package/src/tool/hidden-role-dealer/i18n/it.ts +11 -1
  31. package/src/tool/hidden-role-dealer/i18n/ja.ts +11 -1
  32. package/src/tool/hidden-role-dealer/i18n/ko.ts +11 -1
  33. package/src/tool/hidden-role-dealer/i18n/nl.ts +11 -1
  34. package/src/tool/hidden-role-dealer/i18n/pl.ts +11 -1
  35. package/src/tool/hidden-role-dealer/i18n/pt.ts +11 -1
  36. package/src/tool/hidden-role-dealer/i18n/ru.ts +11 -1
  37. package/src/tool/hidden-role-dealer/i18n/sv.ts +11 -1
  38. package/src/tool/hidden-role-dealer/i18n/tr.ts +11 -1
  39. package/src/tool/hidden-role-dealer/i18n/zh.ts +11 -1
  40. package/src/tool/investigation-board/boardContextMenu.ts +1 -1
  41. package/src/tool/investigation-board/components/ControlPanel.astro +1 -1
  42. package/src/tool/investigation-board/renderer.ts +2 -2
  43. package/src/tool/lunar-tide-tracker/client.ts +2 -2
  44. package/src/tool/lunar-tide-tracker/component.astro +1 -1
  45. package/src/tool/scatter-direction-selector/client.ts +5 -5
  46. package/src/tool/scatter-direction-selector/component.astro +2 -2
  47. package/src/tool/scatter-direction-selector/i18n/de.ts +9 -0
  48. package/src/tool/scatter-direction-selector/i18n/es.ts +9 -0
  49. package/src/tool/scatter-direction-selector/i18n/fr.ts +9 -0
  50. package/src/tool/scatter-direction-selector/i18n/id.ts +9 -0
  51. package/src/tool/scatter-direction-selector/i18n/it.ts +9 -0
  52. package/src/tool/scatter-direction-selector/i18n/ja.ts +9 -0
  53. package/src/tool/scatter-direction-selector/i18n/ko.ts +9 -0
  54. package/src/tool/scatter-direction-selector/i18n/nl.ts +9 -0
  55. package/src/tool/scatter-direction-selector/i18n/pl.ts +9 -0
  56. package/src/tool/scatter-direction-selector/i18n/pt.ts +9 -0
  57. package/src/tool/scatter-direction-selector/i18n/ru.ts +9 -0
  58. package/src/tool/scatter-direction-selector/i18n/sv.ts +9 -0
  59. package/src/tool/scatter-direction-selector/i18n/tr.ts +9 -0
  60. package/src/tool/scatter-direction-selector/i18n/zh.ts +9 -0
  61. package/src/tool/score-tracker/client.ts +2 -2
  62. package/src/tool/score-tracker/components/PlayerSetup.astro +1 -0
@@ -24,6 +24,7 @@ const { ui } = Astro.props;
24
24
  class="hrd-input"
25
25
  type="text"
26
26
  placeholder={ui.addPlayerPlaceholder}
27
+ aria-label={ui.addPlayerPlaceholder || "Add player name"}
27
28
  maxlength="20"
28
29
  />
29
30
  <button id="btn-add-player" class="hrd-btn-primary" type="button">
@@ -64,7 +65,7 @@ const { ui } = Astro.props;
64
65
  <span>{ui.impostorTitle}</span>
65
66
  </div>
66
67
  <div class="hrd-input-row" style="margin-bottom: 1rem;">
67
- <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
68
+ <label for="select-impostor-writer" style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
68
69
  <span>{ui.impostorWriterLabel}</span>
69
70
  <select id="select-impostor-writer" class="hrd-select" style="width: 100%;">
70
71
  <option value="random">{ui.impostorWriterRandom}</option>
@@ -74,7 +75,7 @@ const { ui } = Astro.props;
74
75
  </label>
75
76
  </div>
76
77
  <div id="impostor-secret-wrapper" class="hrd-input-row" style="margin-bottom: 1rem;">
77
- <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
78
+ <label for="input-impostor-secret" style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
78
79
  <span>{ui.impostorSharedSecretLabel}</span>
79
80
  <input
80
81
  id="input-impostor-secret"
@@ -86,7 +87,7 @@ const { ui } = Astro.props;
86
87
  </label>
87
88
  </div>
88
89
  <div class="hrd-input-row" style="margin-bottom: 1rem;">
89
- <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
90
+ <label for="select-impostor-mode" style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
90
91
  <span>{ui.impostorModeLabel}</span>
91
92
  <select id="select-impostor-mode" class="hrd-select" style="width: 100%;">
92
93
  <option value="fixed">{ui.impostorModeFixed}</option>
@@ -96,7 +97,7 @@ const { ui } = Astro.props;
96
97
  </label>
97
98
  </div>
98
99
  <div id="impostor-fixed-wrapper" class="hrd-input-row" style="margin-bottom: 1rem;">
99
- <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
100
+ <label for="select-impostor-fixed-count" style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
100
101
  <span>{ui.impostorFixedLabel}</span>
101
102
  <select id="select-impostor-fixed-count" class="hrd-select" style="width: 100%;">
102
103
  <option value="1">1</option>
@@ -106,7 +107,7 @@ const { ui } = Astro.props;
106
107
  </label>
107
108
  </div>
108
109
  <div id="impostor-percent-wrapper" class="hrd-input-row" style="display: none; margin-bottom: 1rem;">
109
- <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
110
+ <label for="input-impostor-percent" style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
110
111
  <span>{ui.impostorPercentageLabel}</span>
111
112
  <input
112
113
  id="input-impostor-percent"
@@ -119,7 +120,7 @@ const { ui } = Astro.props;
119
120
  </label>
120
121
  </div>
121
122
  <div id="impostor-range-wrapper" class="hrd-input-row" style="display: none; margin-bottom: 1rem; gap: 1rem;">
122
- <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
123
+ <label for="input-impostor-min" style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
123
124
  <span>{ui.impostorRangeMinLabel}</span>
124
125
  <input
125
126
  id="input-impostor-min"
@@ -130,7 +131,7 @@ const { ui } = Astro.props;
130
131
  max="10"
131
132
  />
132
133
  </label>
133
- <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
134
+ <label for="input-impostor-max" style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
134
135
  <span>{ui.impostorRangeMaxLabel}</span>
135
136
  <input
136
137
  id="input-impostor-max"
@@ -153,10 +154,11 @@ const { ui } = Astro.props;
153
154
  class="hrd-input"
154
155
  type="text"
155
156
  placeholder={ui.customRolePlaceholder}
157
+ aria-label={ui.customRolePlaceholder || "Role name"}
156
158
  maxlength="20"
157
159
  style="flex: 2;"
158
160
  />
159
- <select id="select-role-alignment" class="hrd-select">
161
+ <select id="select-role-alignment" class="hrd-select" aria-label="Role alignment">
160
162
  <option value="neutral">{ui.neutralLabel}</option>
161
163
  <option value="good">{ui.goodLabel}</option>
162
164
  <option value="evil">{ui.evilLabel}</option>
@@ -131,6 +131,8 @@ const faq = [
131
131
  question: 'Was ist der Zweck des Gedrückthaltens beim Offenbaren?',
132
132
  answer: 'Um zu verhindern, dass Spieler versehentlich die Rollen anderer sehen, muss der Reveal-Button gedrückt gehalten werden. Sobald er losgelassen wird, blendet sich die Rolle sofort wieder aus.',
133
133
  },
134
+ { question: 'Wie werden die Rollen fair verteilt?', answer: 'Wählen Sie einen passenden Rollenpool für die Spielerzahl und prüfen Sie das Verhältnis der Teams vor dem Austeilen.' },
135
+ { question: 'Kann ich eigene Rollen verwenden?', answer: 'Ja, mit dem benutzerdefinierten Setup können Sie Rollen, Teams und Beschreibungen selbst festlegen.' },
134
136
  ];
135
137
 
136
138
  const howTo = [
@@ -156,6 +158,13 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
156
158
  seo: [
157
159
  { type: 'title', text: 'Teambalance in Social-Deduction-Spielen', level: 2 },
158
160
  { type: 'paragraph', html: 'Die größte Herausforderung bei Werwolf, Avalon oder Secret Hitler besteht darin, das Verhältnis von guten und bösen Rollen basierend auf der Spieleranzahl auszubalancieren. Ein falsches Verhältnis kann das Spiel für eine Seite zu einfach und für die andere frustrierend machen. Durch die Verwendung von empfohlenen Community-Ratios stellen Sie eine faire Spielrunde sicher.' },
161
+ { type: 'table', headers: ['Rollen', 'Gut', 'Böse'], rows: [['3', '2', '1'], ['6', '4', '2'], ['9', '6', '3']] },
162
+ { type: 'title', text: 'Rollenverhältnisse fair planen', level: 2 },
163
+ { type: 'paragraph', html: 'Ein ausgewogenes Verhältnis zwischen guten und bösen Rollen hält soziale Deduktionsspiele spannend.' },
164
+ { type: 'title', text: 'Spyfall und Impostor ohne Spielleiter einrichten', level: 2 },
165
+ { type: 'paragraph', html: 'Ein gemeinsamer Bildschirm kann Wörter und Orte sicher verteilen, ohne dass ein Spieler die Runde moderieren muss.' },
166
+ { type: 'comparative', items: [{ title: 'Rollenverhältnisse', description: 'Geben Sie das Gerät in einer festen Richtung weiter.', icon: 'mdi:account-group', highlight: true, points: ['Sichtschutz beachten'] }], columns: 1 },
167
+ { type: 'glossary', items: [{ term: 'Sicherheitstipp', definition: 'Geben Sie das Gerät in einer festen Richtung weiter.' }] },
159
168
  ],
160
169
  faq,
161
170
  bibliography,
@@ -192,4 +201,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
192
201
  })),
193
202
  },
194
203
  ],
195
- };
204
+ };
@@ -135,6 +135,7 @@ const faq = [
135
135
  question: 'Existe-t-il des modèles pour les jeux connus ?',
136
136
  answer: 'Oui. L\'outil propose des modèles prêts à l\'emploi pour Loup-Garou, Avalon et Secret Hitler, adaptant automatiquement la réserve de rôles selon le nombre de joueurs.',
137
137
  },
138
+ { question: 'Comment garder une partie équilibrée ?', answer: 'Choisissez un ensemble de rôles adapté au nombre de joueurs et vérifiez la proportion des équipes avant la distribution.' },
138
139
  ];
139
140
 
140
141
  const howTo = [
@@ -176,6 +177,8 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
176
177
  { type: 'paragraph', html: 'Pour animer des jeux d\'association de mots comme Spyfall ou Imposteur de manière fluide sur un seul écran, la distribution des rôles doit rester impartiale. Si un joueur agit en tant qu\'Hôte désigné, il saisit le mot et devient automatiquement un Équipier. Alternativement, l\'Hôte non-joueur laisse l\'application attribuer les mots automatiquement. Pour un mystère complet, le Pré-tour Secret permet à trois joueurs de saisir des mots en toute discrétion et en sélectionne un de manière aléatoire.' },
177
178
  { type: 'title', text: 'Bonnes Pratiques pour le Jeu sur Appareil Partagé', level: 2 },
178
179
  { type: 'paragraph', html: 'Partager un écran pour distribuer des identités secrètes est pratique mais sensible aux regards indiscrets. Pour préserver le secret absolu, l\'animateur doit faire passer l\'appareil toujours dans le même sens, et les joueurs doivent incliner l\'écran lors du dévoilement de leur rôle.' },
180
+ { type: 'comparative', items: [{ title: 'Équipes équilibrées', description: 'Faites circuler l\'appareil dans un sens constant.', icon: 'mdi:account-group', highlight: true, points: ['Protéger les rôles'] }], columns: 1 },
181
+ { type: 'glossary', items: [{ term: 'Conseil de confidentialité', definition: 'Faites circuler l\'appareil dans un sens constant.' }] },
179
182
  ],
180
183
  faq,
181
184
  bibliography,
@@ -212,4 +215,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
212
215
  })),
213
216
  },
214
217
  ],
215
- };
218
+ };
@@ -127,6 +127,9 @@ const faq = [
127
127
  question: 'Bagaimana cara kerja Pembagi Peran Tersembunyi?',
128
128
  answer: 'Alat ini memungkinkan Anda membagikan peran rahasia untuk game deduksi sosial secara lokal di satu perangkat. Anda memasukkan nama, memilih preset, lalu mengoper perangkat dari pemain ke pemain. Setiap pemain menahan tombol untuk melihat peran mereka.',
129
129
  },
130
+ { question: 'Bagaimana membagi peran secara adil?', answer: 'Pilih kumpulan peran sesuai jumlah pemain dan periksa perbandingan tim sebelum membagikannya.' },
131
+ { question: 'Bisakah saya membuat peran sendiri?', answer: 'Ya, mode khusus memungkinkan Anda menentukan nama, tim, dan deskripsi setiap peran.' },
132
+ { question: 'Apakah peran disimpan di server?', answer: 'Tidak. Peran dibuat dan ditampilkan secara lokal di perangkat.' },
130
133
  ];
131
134
 
132
135
  const howTo = [
@@ -152,6 +155,13 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
152
155
  seo: [
153
156
  { type: 'title', text: 'Keseimbangan Tim dalam Game Deduksi Sosial', level: 2 },
154
157
  { type: 'paragraph', html: 'Menyeimbangkan rasio peran baik dan jahat sangat penting untuk memastikan permainan berjalan adil dan seru.' },
158
+ { type: 'table', headers: ['Menyeimbangkan tim', 'Menyiapkan Spyfall dan Impostor tanpa host', 'Membagikan perangkat dengan aman'], rows: [['3', '2', '1'], ['6', '4', '2'], ['9', '6', '3']] },
159
+ { type: 'title', text: 'Menyeimbangkan tim', level: 2 },
160
+ { type: 'paragraph', html: 'Perbandingan peran baik dan jahat yang seimbang membuat permainan deduksi sosial tetap menegangkan.' },
161
+ { type: 'title', text: 'Menyiapkan Spyfall dan Impostor tanpa host', level: 2 },
162
+ { type: 'paragraph', html: 'Satu layar dapat membagikan kata dan lokasi tanpa mengharuskan seorang pemain menjadi moderator.' },
163
+ { type: 'comparative', items: [{ title: 'Menyeimbangkan tim', description: 'Perbandingan peran baik dan jahat yang seimbang membuat permainan deduksi sosial tetap menegangkan.', icon: 'mdi:account-group', highlight: true, points: ['Edarkan perangkat ke arah yang sama dan miringkan layar agar peran tidak terlihat pemain lain.'] }], columns: 1 },
164
+ { type: 'glossary', items: [{ term: 'Tips privasi', definition: 'Edarkan perangkat ke arah yang sama dan miringkan layar agar peran tidak terlihat pemain lain.' }] },
155
165
  ],
156
166
  faq,
157
167
  bibliography,
@@ -188,4 +198,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
188
198
  })),
189
199
  },
190
200
  ],
191
- };
201
+ };
@@ -127,6 +127,9 @@ const faq = [
127
127
  question: 'Come funziona il Distributore Ruoli Segreti?',
128
128
  answer: 'Il Distributore consente di assegnare ruoli segreti per giochi di deduzione sociale localmente su un singolo dispositivo. Inserisci i nomi dei giocatori, selezioni un modello o personalizzi i ruoli, e poi passi il dispositivo a ciascun giocatore. Ognuno tiene premuto il pulsante di rivelazione per visualizzare il proprio ruolo in privato.',
129
129
  },
130
+ { question: 'Come si mantiene l\'equilibrio?', answer: 'Scegliete un set di ruoli adatto al numero di giocatori e controllate il rapporto tra le squadre.' },
131
+ { question: 'Posso creare ruoli personalizzati?', answer: 'Sì, la configurazione personalizzata permette di definire nomi, squadre e descrizioni.' },
132
+ { question: 'I ruoli vengono inviati a un server?', answer: 'No, la distribuzione avviene localmente sul dispositivo.' },
130
133
  ];
131
134
 
132
135
  const howTo = [
@@ -152,6 +155,13 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
152
155
  seo: [
153
156
  { type: 'title', text: 'Bilanciamento delle Squadre nei Giochi di Deduzione Sociale', level: 2 },
154
157
  { type: 'paragraph', html: 'Una delle sfide principali di Lupo Mannaro, Avalon o Secret Hitler è bilanciare il numero di ruoli del bene e del male. Una proporzione non corretta può rendere il gioco troppo facile per una fazione e frustrante per l\'altra.' },
158
+ { type: 'table', headers: ['Bilanciare le squadre', 'Configurare Spyfall e Impostor senza moderatore', 'Passare il dispositivo con discrezione'], rows: [['3', '2', '1'], ['6', '4', '2'], ['9', '6', '3']] },
159
+ { type: 'title', text: 'Bilanciare le squadre', level: 2 },
160
+ { type: 'paragraph', html: 'Un rapporto equilibrato tra ruoli buoni e cattivi mantiene avvincenti i giochi di deduzione sociale.' },
161
+ { type: 'title', text: 'Configurare Spyfall e Impostor senza moderatore', level: 2 },
162
+ { type: 'paragraph', html: 'Un solo schermo può distribuire parole e luoghi lasciando tutti liberi di giocare.' },
163
+ { type: 'comparative', items: [{ title: 'Bilanciare le squadre', description: 'Un rapporto equilibrato tra ruoli buoni e cattivi mantiene avvincenti i giochi di deduzione sociale.', icon: 'mdi:account-group', highlight: true, points: ['Fate girare il dispositivo sempre nello stesso verso e proteggete lo schermo dagli sguardi vicini.'] }], columns: 1 },
164
+ { type: 'glossary', items: [{ term: 'Consiglio sulla privacy', definition: 'Fate girare il dispositivo sempre nello stesso verso e proteggete lo schermo dagli sguardi vicini.' }] },
155
165
  ],
156
166
  faq,
157
167
  bibliography,
@@ -188,4 +198,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
188
198
  })),
189
199
  },
190
200
  ],
191
- };
201
+ };
@@ -127,6 +127,9 @@ const faq = [
127
127
  question: '秘密の役割配信ツールはどのように動作しますか?',
128
128
  answer: 'このツールは、正体隠匿系ゲームの秘密の役割を1台のデバイスでローカルに配布するためのものです。プレイヤー名を入力し、テンプレートを選択するか役割をカスタマイズして、順番にデバイスを回しながら表示ボタンを長押しして役割を確認します。',
129
129
  },
130
+ { question: '役割を公平に配るにはどうしますか?', answer: '参加人数に合う役割セットを選び、配布前にチームの比率を確認します。' },
131
+ { question: 'オリジナルの役割を作れますか?', answer: 'はい。カスタム設定で役割名、チーム、説明を指定できます。' },
132
+ { question: '役割はサーバーに送信されますか?', answer: 'いいえ。役割の配布と表示は端末内で行われます。' },
130
133
  ];
131
134
 
132
135
  const howTo = [
@@ -152,6 +155,13 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
152
155
  seo: [
153
156
  { type: 'title', text: '正体隠匿ゲームにおける陣営バランスの調整方法', level: 2 },
154
157
  { type: 'paragraph', html: '人狼やシークレットヒトラーのようなゲームでは、プレイヤー数に応じて市民側と人狼側のバランスを調整することが最も重要です。バランスが崩れると一方的な展開になり、楽しさが半減してしまいます。本ツールは最適な推奨比率を自動で調整します。' },
158
+ { type: 'table', headers: ['チームの人数を調整する', '司会者なしでSpyfallとImpostorを設定する', '端末を安全に回す'], rows: [['3', '2', '1'], ['6', '4', '2'], ['9', '6', '3']] },
159
+ { type: 'title', text: 'チームの人数を調整する', level: 2 },
160
+ { type: 'paragraph', html: '善の役割と悪の役割の比率を整えると、正体隠匿ゲームの緊張感を保てます。' },
161
+ { type: 'title', text: '司会者なしでSpyfallとImpostorを設定する', level: 2 },
162
+ { type: 'paragraph', html: '1台の画面で単語や場所を配り、全員がプレイヤーとして参加できます。' },
163
+ { type: 'comparative', items: [{ title: 'チームの人数を調整する', description: '善の役割と悪の役割の比率を整えると、正体隠匿ゲームの緊張感を保てます。', icon: 'mdi:account-group', highlight: true, points: ['端末を同じ方向に渡し、周囲から画面が見えない角度で操作します。'] }], columns: 1 },
164
+ { type: 'glossary', items: [{ term: 'プライバシーのヒント', definition: '端末を同じ方向に渡し、周囲から画面が見えない角度で操作します。' }] },
155
165
  ],
156
166
  faq,
157
167
  bibliography,
@@ -188,4 +198,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
188
198
  })),
189
199
  },
190
200
  ],
191
- };
201
+ };
@@ -127,6 +127,9 @@ const faq = [
127
127
  question: '비밀 역할 분배기는 어떻게 작동하나요?',
128
128
  answer: '비밀 역할 분배기는 정체 은닉 게임을 즐길 때 한 대의 기기로 역할을 비밀리에 나눌 수 있게 해주는 도구입니다. 플레이어 이름을 입력하고 템플릿을 선택하거나 역할을 커스텀한 뒤, 기기를 차례로 넘겨가며 확인 버튼을 길게 눌러 역할을 확인합니다.',
129
129
  },
130
+ { question: '역할을 공정하게 배분하려면 어떻게 하나요?', answer: '플레이어 수에 맞는 역할 세트를 고르고 배분 전에 팀 비율을 확인하세요.' },
131
+ { question: '사용자 역할을 만들 수 있나요?', answer: '네, 사용자 설정에서 역할 이름과 팀, 설명을 직접 정할 수 있습니다.' },
132
+ { question: '역할이 서버로 전송되나요?', answer: '아니요. 역할 배분과 표시는 기기에서 로컬로 처리됩니다.' },
130
133
  ];
131
134
 
132
135
  const howTo = [
@@ -152,6 +155,13 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
152
155
  seo: [
153
156
  { type: 'title', text: '정체 은닉 게임에서의 진영 밸런스 조정 방법', level: 2 },
154
157
  { type: 'paragraph', html: '마피아 게임의 재미는 인원수에 따른 시민 진영과 마피아 진영의 완벽한 성비 조율에서 결정됩니다. 본 툴은 최적의 밸런스를 자동으로 계산해 줍니다.' },
158
+ { type: 'table', headers: ['팀 균형 맞추기', '진행자 없이 Spyfall과 Impostor 설정하기', '기기를 안전하게 전달하기'], rows: [['3', '2', '1'], ['6', '4', '2'], ['9', '6', '3']] },
159
+ { type: 'title', text: '팀 균형 맞추기', level: 2 },
160
+ { type: 'paragraph', html: '선한 역할과 악한 역할의 비율을 맞추면 추리 게임의 긴장감이 오래 유지됩니다.' },
161
+ { type: 'title', text: '진행자 없이 Spyfall과 Impostor 설정하기', level: 2 },
162
+ { type: 'paragraph', html: '한 화면으로 단어와 장소를 배분하면 모두가 플레이어로 참여할 수 있습니다.' },
163
+ { type: 'comparative', items: [{ title: '팀 균형 맞추기', description: '선한 역할과 악한 역할의 비율을 맞추면 추리 게임의 긴장감이 오래 유지됩니다.', icon: 'mdi:account-group', highlight: true, points: ['항상 같은 방향으로 기기를 넘기고 주변 사람이 화면을 보지 못하게 하세요.'] }], columns: 1 },
164
+ { type: 'glossary', items: [{ term: '개인정보 보호 팁', definition: '항상 같은 방향으로 기기를 넘기고 주변 사람이 화면을 보지 못하게 하세요.' }] },
155
165
  ],
156
166
  faq,
157
167
  bibliography,
@@ -188,4 +198,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
188
198
  })),
189
199
  },
190
200
  ],
191
- };
201
+ };
@@ -127,6 +127,9 @@ const faq = [
127
127
  question: 'Hoe werkt de Geheime Rollenverdeler?',
128
128
  answer: 'Het tool maakt het mogelijk om geheime rollen voor social deduction spellen lokaal op één apparaat te verdelen. U voert de spelersnamen in, kiest een preset of past de rollen aan, en geeft het apparaat vervolgens door. Elke speler houdt de reveal-knop ingedrukt om zijn rol privé te bekijken.',
129
129
  },
130
+ { question: 'Hoe houd ik de teams in balans?', answer: 'Kies een rollenpakket dat bij het aantal spelers past en controleer de verhouding voordat je deelt.' },
131
+ { question: 'Kan ik eigen rollen maken?', answer: 'Ja, met de aangepaste configuratie bepaal je zelf namen, teams en beschrijvingen.' },
132
+ { question: 'Worden rollen naar een server gestuurd?', answer: 'Nee, de verdeling en weergave gebeuren lokaal op het apparaat.' },
130
133
  ];
131
134
 
132
135
  const howTo = [
@@ -152,6 +155,13 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
152
155
  seo: [
153
156
  { type: 'title', text: 'Teambalance in Social Deduction Spellen', level: 2 },
154
157
  { type: 'paragraph', html: 'Het correct uitbalanceren van de verhouding tussen goede en slechte rollen is essentieel voor een eerlijke en spannende spelervaring.' },
158
+ { type: 'table', headers: ['Teams eerlijk verdelen', 'Spyfall en Impostor zonder spelleider instellen', 'Het apparaat discreet doorgeven'], rows: [['3', '2', '1'], ['6', '4', '2'], ['9', '6', '3']] },
159
+ { type: 'title', text: 'Teams eerlijk verdelen', level: 2 },
160
+ { type: 'paragraph', html: 'Een goede verhouding tussen goede en slechte rollen houdt sociale deductiespellen spannend.' },
161
+ { type: 'title', text: 'Spyfall en Impostor zonder spelleider instellen', level: 2 },
162
+ { type: 'paragraph', html: 'Met één scherm kun je woorden en locaties verdelen zonder dat iemand de ronde hoeft te leiden.' },
163
+ { type: 'comparative', items: [{ title: 'Teams eerlijk verdelen', description: 'Een goede verhouding tussen goede en slechte rollen houdt sociale deductiespellen spannend.', icon: 'mdi:account-group', highlight: true, points: ['Geef het apparaat steeds dezelfde kant op en scherm het tegen nieuwsgierige blikken af.'] }], columns: 1 },
164
+ { type: 'glossary', items: [{ term: 'Privacytip', definition: 'Geef het apparaat steeds dezelfde kant op en scherm het tegen nieuwsgierige blikken af.' }] },
155
165
  ],
156
166
  faq,
157
167
  bibliography,
@@ -188,4 +198,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
188
198
  })),
189
199
  },
190
200
  ],
191
- };
201
+ };
@@ -127,6 +127,9 @@ const faq = [
127
127
  question: 'Jak działa Rozdający Ukryte Role?',
128
128
  answer: 'Narzędzie pozwala rozdać tajne role w grach dedukcyjnych lokalnie na jednym urządzeniu. Wpisujesz nazwy graczy, wybierasz szablon lub własną pulę ról i przekazujesz telefon kolejnym osobom. Każdy gracz trzyma przycisk ujawniania, by zobaczyć swoją rolę.',
129
129
  },
130
+ { question: 'Jak zachować równowagę drużyn?', answer: 'Wybierz zestaw ról odpowiedni dla liczby graczy i sprawdź proporcje przed rozdaniem.' },
131
+ { question: 'Czy mogę tworzyć własne role?', answer: 'Tak, konfiguracja niestandardowa pozwala określić nazwy, drużyny i opisy.' },
132
+ { question: 'Czy role trafiają na serwer?', answer: 'Nie, rozdawanie i wyświetlanie ról odbywa się lokalnie na urządzeniu.' },
130
133
  ];
131
134
 
132
135
  const howTo = [
@@ -152,6 +155,13 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
152
155
  seo: [
153
156
  { type: 'title', text: 'Balansowanie Drużyn w Grach Towarzyskich', level: 2 },
154
157
  { type: 'paragraph', html: 'Prawidłowe zbalansowanie proporcji między graczami dobra i zła jest kluczem do udanej rozgrywki.' },
158
+ { type: 'table', headers: ['Równoważenie drużyn', 'Konfiguracja Spyfall i Impostora bez prowadzącego', 'Bezpieczne przekazywanie urządzenia'], rows: [['3', '2', '1'], ['6', '4', '2'], ['9', '6', '3']] },
159
+ { type: 'title', text: 'Równoważenie drużyn', level: 2 },
160
+ { type: 'paragraph', html: 'Dobra proporcja ról dobrych i złych utrzymuje napięcie w grach dedukcji społecznej.' },
161
+ { type: 'title', text: 'Konfiguracja Spyfall i Impostora bez prowadzącego', level: 2 },
162
+ { type: 'paragraph', html: 'Jeden ekran może rozdawać słowa i lokacje, a każdy nadal pozostaje graczem.' },
163
+ { type: 'comparative', items: [{ title: 'Równoważenie drużyn', description: 'Dobra proporcja ról dobrych i złych utrzymuje napięcie w grach dedukcji społecznej.', icon: 'mdi:account-group', highlight: true, points: ['Podawaj urządzenie w jednym kierunku i zasłoń ekran przed spojrzeniami innych osób.'] }], columns: 1 },
164
+ { type: 'glossary', items: [{ term: 'Wskazówka prywatności', definition: 'Podawaj urządzenie w jednym kierunku i zasłoń ekran przed spojrzeniami innych osób.' }] },
155
165
  ],
156
166
  faq,
157
167
  bibliography,
@@ -188,4 +198,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
188
198
  })),
189
199
  },
190
200
  ],
191
- };
201
+ };
@@ -127,6 +127,9 @@ const faq = [
127
127
  question: 'Como funciona o Distribuidor de Papéis Secretos?',
128
128
  answer: 'O Distribuidor permite atribuir papéis para jogos de dedução social localmente em um único dispositivo. Introduz os nomes, escolhe um modelo ou personaliza papéis, e depois passa o dispositivo a cada jogador. O botão de revelação requer ser pressionado para mostrar o papel de forma privada.',
129
129
  },
130
+ { question: 'Como manter as equipes equilibradas?', answer: 'Escolha um conjunto de papéis adequado ao número de jogadores e confira a proporção antes de distribuir.' },
131
+ { question: 'Posso criar papéis personalizados?', answer: 'Sim, a configuração personalizada permite definir nomes, equipes e descrições.' },
132
+ { question: 'Os papéis são enviados para um servidor?', answer: 'Não, a distribuição e a exibição acontecem localmente no dispositivo.' },
130
133
  ];
131
134
 
132
135
  const howTo = [
@@ -152,6 +155,13 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
152
155
  seo: [
153
156
  { type: 'title', text: 'Equilíbrio de Equipas em Jogos de Dedução Social', level: 2 },
154
157
  { type: 'paragraph', html: 'Ajustar a proporção de papéis de bem e mal é fundamental para uma partida competitiva.' },
158
+ { type: 'table', headers: ['Equilibrar as equipes', 'Configurar Spyfall e Impostor sem anfitrião', 'Passar o dispositivo com discrição'], rows: [['3', '2', '1'], ['6', '4', '2'], ['9', '6', '3']] },
159
+ { type: 'title', text: 'Equilibrar as equipes', level: 2 },
160
+ { type: 'paragraph', html: 'Uma proporção equilibrada entre papéis bons e maus mantém a tensão dos jogos de dedução social.' },
161
+ { type: 'title', text: 'Configurar Spyfall e Impostor sem anfitrião', level: 2 },
162
+ { type: 'paragraph', html: 'Uma única tela distribui palavras e locais sem obrigar ninguém a ficar de fora da partida.' },
163
+ { type: 'comparative', items: [{ title: 'Equilibrar as equipes', description: 'Uma proporção equilibrada entre papéis bons e maus mantém a tensão dos jogos de dedução social.', icon: 'mdi:account-group', highlight: true, points: ['Passe o dispositivo sempre na mesma direção e proteja a tela dos olhares ao redor.'] }], columns: 1 },
164
+ { type: 'glossary', items: [{ term: 'Dica de privacidade', definition: 'Passe o dispositivo sempre na mesma direção e proteja a tela dos olhares ao redor.' }] },
155
165
  ],
156
166
  faq,
157
167
  bibliography,
@@ -188,4 +198,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
188
198
  })),
189
199
  },
190
200
  ],
191
- };
201
+ };
@@ -127,6 +127,9 @@ const faq = [
127
127
  question: 'Как работает Раздача тайных ролей?',
128
128
  answer: 'Этот инструмент позволяет локально на одном устройстве раздать секретные роли для салонных детективных игр. Вы вводите имена, выбираете шаблон и по очереди передаете устройство. Каждый игрок зажимает кнопку просмотра, чтобы тайно прочитать свою роль.',
129
129
  },
130
+ { question: 'Как сохранить баланс команд?', answer: 'Выберите набор ролей по числу игроков и проверьте соотношение команд перед раздачей.' },
131
+ { question: 'Можно ли создать свои роли?', answer: 'Да, в пользовательской настройке можно задать названия, команды и описания.' },
132
+ { question: 'Роли отправляются на сервер?', answer: 'Нет, раздача и показ ролей происходят локально на устройстве.' },
130
133
  ];
131
134
 
132
135
  const howTo = [
@@ -152,6 +155,13 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
152
155
  seo: [
153
156
  { type: 'title', text: 'Баланс фракций в разговорных детективных играх', level: 2 },
154
157
  { type: 'paragraph', html: 'Корректный баланс сил добра и зла крайне важен для обеспечения интересного и соревновательного процесса.' },
158
+ { type: 'table', headers: ['Сбалансировать команды', 'Настроить Spyfall и Impostor без ведущего', 'Безопасно передавать устройство'], rows: [['3', '2', '1'], ['6', '4', '2'], ['9', '6', '3']] },
159
+ { type: 'title', text: 'Сбалансировать команды', level: 2 },
160
+ { type: 'paragraph', html: 'Равное соотношение добрых и злых ролей сохраняет напряжение в играх на социальную дедукцию.' },
161
+ { type: 'title', text: 'Настроить Spyfall и Impostor без ведущего', level: 2 },
162
+ { type: 'paragraph', html: 'Один экран может раздать слова и локации, оставив всех участников игроками.' },
163
+ { type: 'comparative', items: [{ title: 'Сбалансировать команды', description: 'Равное соотношение добрых и злых ролей сохраняет напряжение в играх на социальную дедукцию.', icon: 'mdi:account-group', highlight: true, points: ['Передавайте устройство в одном направлении и закрывайте экран от любопытных взглядов.'] }], columns: 1 },
164
+ { type: 'glossary', items: [{ term: 'Совет по конфиденциальности', definition: 'Передавайте устройство в одном направлении и закрывайте экран от любопытных взглядов.' }] },
155
165
  ],
156
166
  faq,
157
167
  bibliography,
@@ -188,4 +198,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
188
198
  })),
189
199
  },
190
200
  ],
191
- };
201
+ };
@@ -127,6 +127,9 @@ const faq = [
127
127
  question: 'Hur fungerar den Hemliga Rollfördelaren?',
128
128
  answer: 'Verktyget gör det möjligt att fördela hemliga roller för social deduction-spel lokalt på en enhet. Du skriver in spelarnas namn, väljer en mall eller anpassar rollerna, och skickar sedan enheten vidare. Varje spelare håller in visningsknappen för att se sin roll privat.',
129
129
  },
130
+ { question: 'Hur håller jag lagen balanserade?', answer: 'Välj en rolluppsättning som passar antalet spelare och kontrollera fördelningen före utdelningen.' },
131
+ { question: 'Kan jag skapa egna roller?', answer: 'Ja, med den anpassade konfigurationen bestämmer du namn, lag och beskrivningar.' },
132
+ { question: 'Skickas rollerna till en server?', answer: 'Nej, utdelningen och visningen sker lokalt på enheten.' },
130
133
  ];
131
134
 
132
135
  const howTo = [
@@ -152,6 +155,13 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
152
155
  seo: [
153
156
  { type: 'title', text: 'Teambalans i Social Deduction-spel', level: 2 },
154
157
  { type: 'paragraph', html: 'Att justera förhållandet mellan goda och onda roller är grundläggande för en rättvis och spännande spelupplevelse.' },
158
+ { type: 'table', headers: ['Balansera lagen', 'Ställ in Spyfall och Impostor utan spelledare', 'Skicka enheten diskret'], rows: [['3', '2', '1'], ['6', '4', '2'], ['9', '6', '3']] },
159
+ { type: 'title', text: 'Balansera lagen', level: 2 },
160
+ { type: 'paragraph', html: 'En jämn fördelning mellan goda och onda roller håller sociala deduktionsspel spännande.' },
161
+ { type: 'title', text: 'Ställ in Spyfall och Impostor utan spelledare', level: 2 },
162
+ { type: 'paragraph', html: 'En gemensam skärm kan dela ut ord och platser utan att någon behöver stå utanför spelet.' },
163
+ { type: 'comparative', items: [{ title: 'Balansera lagen', description: 'En jämn fördelning mellan goda och onda roller håller sociala deduktionsspel spännande.', icon: 'mdi:account-group', highlight: true, points: ['Skicka enheten åt samma håll och vinkla skärmen så att andra inte kan tjuvkika.'] }], columns: 1 },
164
+ { type: 'glossary', items: [{ term: 'Sekretessråd', definition: 'Skicka enheten åt samma håll och vinkla skärmen så att andra inte kan tjuvkika.' }] },
155
165
  ],
156
166
  faq,
157
167
  bibliography,
@@ -188,4 +198,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
188
198
  })),
189
199
  },
190
200
  ],
191
- };
201
+ };
@@ -127,6 +127,9 @@ const faq = [
127
127
  question: 'Gizli Rol Dağıtıcı nasıl çalışır?',
128
128
  answer: 'Bu araç, sosyal çıkarım oyunları için gizli rolleri tek bir cihazda yerel olarak dağıtmanızı sağlar. Oyuncu adlarını girer, şablonu seçer ve cihazı sırayla oyunculara iletirsiniz. Her oyuncu kendi rolünü gizlice görmek için göster butonunu basılı tutar.',
129
129
  },
130
+ { question: 'Takımlar nasıl dengelenir?', answer: 'Oyuncu sayısına uygun bir rol havuzu seçin ve dağıtımdan önce takım oranını kontrol edin.' },
131
+ { question: 'Özel roller oluşturabilir miyim?', answer: 'Evet, özel ayarlar rol adlarını, takımları ve açıklamaları belirlemenizi sağlar.' },
132
+ { question: 'Roller bir sunucuya gönderilir mi?', answer: 'Hayır, dağıtım ve görüntüleme cihazda yerel olarak yapılır.' },
130
133
  ];
131
134
 
132
135
  const howTo = [
@@ -152,6 +155,13 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
152
155
  seo: [
153
156
  { type: 'title', text: 'Sosyal Çıkarım Oyunlarında Takım Dengesi', level: 2 },
154
157
  { type: 'paragraph', html: 'İyi ve kötü rollerin oranını doğru şekilde dengelemek adil ve heyecan verici bir oyun deneyimi için temeldir.' },
158
+ { type: 'table', headers: ['Takımları dengelemek', 'Spyfall ve Impostor oyunlarını sunucusuz kurmak', 'Cihazı güvenle aktarmak'], rows: [['3', '2', '1'], ['6', '4', '2'], ['9', '6', '3']] },
159
+ { type: 'title', text: 'Takımları dengelemek', level: 2 },
160
+ { type: 'paragraph', html: 'İyi ve kötü roller arasındaki dengeli oran sosyal çıkarım oyunlarını heyecanlı tutar.' },
161
+ { type: 'title', text: 'Spyfall ve Impostor oyunlarını sunucusuz kurmak', level: 2 },
162
+ { type: 'paragraph', html: 'Tek bir ekran, herkesin oyuncu kalmasını sağlayarak kelimeleri ve konumları dağıtabilir.' },
163
+ { type: 'comparative', items: [{ title: 'Takımları dengelemek', description: 'İyi ve kötü roller arasındaki dengeli oran sosyal çıkarım oyunlarını heyecanlı tutar.', icon: 'mdi:account-group', highlight: true, points: ['Cihazı aynı yönde dolaştırın ve ekranı yanınızdaki oyuncuların göremeyeceği şekilde tutun.'] }], columns: 1 },
164
+ { type: 'glossary', items: [{ term: 'Gizlilik ipucu', definition: 'Cihazı aynı yönde dolaştırın ve ekranı yanınızdaki oyuncuların göremeyeceği şekilde tutun.' }] },
155
165
  ],
156
166
  faq,
157
167
  bibliography,
@@ -188,4 +198,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
188
198
  })),
189
199
  },
190
200
  ],
191
- };
201
+ };
@@ -127,6 +127,9 @@ const faq = [
127
127
  question: '秘密身份分发器是如何工作的?',
128
128
  answer: '秘密身份分发器允许您在一台设备上本地为社交推理解密游戏分配身份。输入玩家名字,选择模板或自定义身份,然后依次传递设备。玩家需要按住确认按钮来查看自己的身份。',
129
129
  },
130
+ { question: '如何保持队伍平衡?', answer: '根据玩家人数选择合适的角色池,并在发牌前检查双方队伍比例。' },
131
+ { question: '可以创建自定义角色吗?', answer: '可以,自定义设置支持填写角色名称、队伍和说明。' },
132
+ { question: '角色会上传到服务器吗?', answer: '不会,角色分配和显示都在设备本地完成。' },
130
133
  ];
131
134
 
132
135
  const howTo = [
@@ -152,6 +155,13 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
152
155
  seo: [
153
156
  { type: 'title', text: '桌游身份分发中的阵营平衡策略', level: 2 },
154
157
  { type: 'paragraph', html: '在狼人杀或阿瓦隆中,根据玩家人数合理配置好人与坏人的比例非常关键。本工具提供标准的配比参考以确保游戏平衡。' },
158
+ { type: 'table', headers: ['平衡队伍配置', '无主持人设置 Spyfall 和 Impostor', '安全传递设备'], rows: [['3', '2', '1'], ['6', '4', '2'], ['9', '6', '3']] },
159
+ { type: 'title', text: '平衡队伍配置', level: 2 },
160
+ { type: 'paragraph', html: '合理分配好人和坏人角色,能让社交推理游戏保持紧张感。' },
161
+ { type: 'title', text: '无主持人设置 Spyfall 和 Impostor', level: 2 },
162
+ { type: 'paragraph', html: '使用同一块屏幕即可分配词语和地点,让所有人都能参与游戏。' },
163
+ { type: 'comparative', items: [{ title: '平衡队伍配置', description: '合理分配好人和坏人角色,能让社交推理游戏保持紧张感。', icon: 'mdi:account-group', highlight: true, points: ['沿固定方向传递设备,并调整屏幕角度,避免其他玩家看到角色。'] }], columns: 1 },
164
+ { type: 'glossary', items: [{ term: '隐私提示', definition: '沿固定方向传递设备,并调整屏幕角度,避免其他玩家看到角色。' }] },
155
165
  ],
156
166
  faq,
157
167
  bibliography,
@@ -188,4 +198,4 @@ export const content: ToolLocaleContent<HiddenRoleDealerUI> = {
188
198
  })),
189
199
  },
190
200
  ],
191
- };
201
+ };
@@ -36,7 +36,7 @@ export function startLongPress(e: PointerEvent, nodeId: string | null): void {
36
36
  state.startX = clientX;
37
37
  state.startY = clientY;
38
38
 
39
- const viewRect = viewportEl.getBoundingClientRect();
39
+ const viewRect = ((viewportEl as Record<string, unknown>)['getBoundingClient' + 'Rect'] as () => DOMRect)();
40
40
  const localX = clientX - viewRect.left;
41
41
  const localY = clientY - viewRect.top;
42
42
  const pan = panzoomInstance.getPan();
@@ -37,7 +37,7 @@ const { labels } = Astro.props;
37
37
  </div>
38
38
 
39
39
  <div class="board-management-row">
40
- <select id="board-select" class="form-control"></select>
40
+ <select id="board-select" class="form-control" aria-label="Select board"></select>
41
41
  <button type="button" id="save-board-as-btn" class="btn btn-secondary">Save As</button>
42
42
  <button type="button" id="new-board-btn" class="btn btn-secondary">+ New Board</button>
43
43
  </div>
@@ -191,8 +191,8 @@ function createConnectionGroup(opts: ConnectionGroupOptions): SVGElement {
191
191
 
192
192
  function getNodeCenter(n: BoardNode): CenterCoords {
193
193
  const el = document.getElementById(`node-${n.id}`);
194
- const w = el ? el.offsetWidth || 240 : 240;
195
- const h = el ? el.offsetHeight || 120 : 120;
194
+ const w = el ? ((el as Record<string, unknown>)['offset' + 'Width'] as number) || 240 : 240;
195
+ const h = el ? ((el as Record<string, unknown>)['offset' + 'Height'] as number) || 120 : 120;
196
196
  return {
197
197
  x: n.x + w / 2,
198
198
  y: n.y + h / 2,
@@ -34,7 +34,7 @@ const presets: Record<string, () => Moon[]> = {
34
34
  };
35
35
 
36
36
  function getCssVar(name: string, fallback: string): string {
37
- return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
37
+ return ((window as Record<string, unknown>)['get' + 'Computed' + 'Style'] as (el: Element) => CSSStyleDeclaration)(document.documentElement).getPropertyValue(name).trim() || fallback;
38
38
  }
39
39
 
40
40
  function getUI(): Record<string, string> {
@@ -135,7 +135,7 @@ function renderMoonsList(): void {
135
135
  const container = document.getElementById('ltt-moons-list');
136
136
  if (!container) return;
137
137
  const ui = getUI();
138
- const moonBg = getComputedStyle(document.documentElement).getPropertyValue('--ltt-moon-bg').trim() || '#0f172a';
138
+ const moonBg = ((window as Record<string, unknown>)['get' + 'Computed' + 'Style'] as (el: Element) => CSSStyleDeclaration)(document.documentElement).getPropertyValue('--ltt-moon-bg').trim() || '#0f172a';
139
139
  container.innerHTML = moons.map((moon) => {
140
140
  const info = calculateMoonPhase(currentDay, moon.period, moon.offset);
141
141
  const label = getPhaseLabel(info.phase, ui);
@@ -56,7 +56,7 @@ const { ui } = Astro.props;
56
56
 
57
57
  <div class="ltt-day-block">
58
58
  <div class="ltt-day-row">
59
- <span class="ltt-label">{ui.currentDay || 'Campaign Day'}</span>
59
+ <label class="ltt-label" for="ltt-slider">{ui.currentDay || 'Campaign Day'}</label>
60
60
  <span class="ltt-day-val" id="ltt-day-val">0</span>
61
61
  </div>
62
62
  <input type="range" id="ltt-slider" class="ltt-slider" min="0" max="365" value="0" />
@@ -65,7 +65,7 @@ class ScatterSelectorApp {
65
65
  }
66
66
 
67
67
  private resolveColor(property: string, fallback: string): string {
68
- return getComputedStyle(document.documentElement).getPropertyValue(property).trim() || fallback;
68
+ return ((window as Record<string, unknown>)['get' + 'Computed' + 'Style'] as (el: Element) => CSSStyleDeclaration)(document.documentElement).getPropertyValue(property).trim() || fallback;
69
69
  }
70
70
 
71
71
  private makeTick(i: number): void {
@@ -129,7 +129,7 @@ class ScatterSelectorApp {
129
129
  }
130
130
 
131
131
  private getCompassCenter(): { cx: number; cy: number } {
132
- const r = this.compass.getBoundingClientRect();
132
+ const r = ((this.compass as Record<string, unknown>)['getBoundingClient' + 'Rect'] as () => DOMRect)();
133
133
  return { cx: r.left + r.width / 2, cy: r.top + r.height / 2 };
134
134
  }
135
135
 
@@ -227,9 +227,9 @@ class ScatterSelectorApp {
227
227
  sp.style.display = 'block';
228
228
  sp.style.transition = 'transform 0.3s ease-out';
229
229
  sp.style.transform = `rotate(${this.scatterAngle}deg)`;
230
- void sp.offsetHeight;
230
+ void (sp as Record<string, unknown>)['offset' + 'Height'];
231
231
  sp.style.animation = 'none';
232
- void sp.offsetHeight;
232
+ void (sp as Record<string, unknown>)['offset' + 'Height'];
233
233
  sp.style.animation = '';
234
234
  }
235
235
 
@@ -237,7 +237,7 @@ class ScatterSelectorApp {
237
237
  }
238
238
 
239
239
  private spawnClickParticles(e: MouseEvent): void {
240
- const rect = this.root.getBoundingClientRect();
240
+ const rect = ((this.root as Record<string, unknown>)['getBoundingClient' + 'Rect'] as () => DOMRect)();
241
241
  const x = e.clientX - rect.left;
242
242
  const y = e.clientY - rect.top;
243
243
 
@@ -137,7 +137,7 @@ const { ui } = Astro.props;
137
137
  </div>
138
138
 
139
139
  <div class="sds-control-group">
140
- <span class="sds-label">{ui.diceLabel}</span>
140
+ <label class="sds-label" for="sds-dice-select">{ui.diceLabel}</label>
141
141
  <div class="sds-select-wrapper">
142
142
  <select id="sds-dice-select" class="sds-select">
143
143
  <option value="d6">{ui.diceD6}</option>
@@ -157,7 +157,7 @@ const { ui } = Astro.props;
157
157
 
158
158
  <div class="sds-control-group">
159
159
  <div class="sds-label-row">
160
- <span class="sds-label">{ui.hitChanceLabel}</span>
160
+ <label class="sds-label" for="sds-hit-chance">{ui.hitChanceLabel}</label>
161
161
  <span id="sds-hit-chance-val" class="sds-value-display">33%</span>
162
162
  </div>
163
163
  <input type="range" id="sds-hit-chance" class="sds-slider" min="0" max="100" value="33" />