@jjlmoya/utils-tabletop 1.3.0 → 1.5.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 (171) hide show
  1. package/package.json +1 -1
  2. package/src/category/TabletopCategorySEO.astro +2 -7
  3. package/src/category/i18n/en.ts +4 -4
  4. package/src/category/i18n/ru.ts +2 -2
  5. package/src/category/i18n/zh.ts +8 -8
  6. package/src/category/index.ts +2 -1
  7. package/src/entries.ts +14 -1
  8. package/src/index.ts +1 -0
  9. package/src/pages/[locale]/[slug].astro +7 -6
  10. package/src/tests/locale_completeness.test.ts +1 -1
  11. package/src/tests/no_em_dash.test.ts +47 -0
  12. package/src/tests/seo_length.test.ts +1 -0
  13. package/src/tests/title_quality.test.ts +1 -1
  14. package/src/tests/tool_validation.test.ts +1 -1
  15. package/src/tool/board-game-timer/modules/ColorHelper.ts +2 -2
  16. package/src/tool/board-game-timer/modules/DuelViewManager.ts +4 -4
  17. package/src/tool/board-game-timer/modules/GameRunner.ts +1 -1
  18. package/src/tool/board-game-timer/modules/MultiplayerViewManager.ts +2 -2
  19. package/src/tool/board-game-timer/modules/SetupPanelManager.ts +1 -1
  20. package/src/tool/board-game-timer/modules/TimerEngine.ts +1 -1
  21. package/src/tool/board-game-timer/types.ts +1 -1
  22. package/src/tool/decision-wheel/audio.ts +78 -0
  23. package/src/tool/decision-wheel/bibliography.astro +16 -0
  24. package/src/tool/decision-wheel/bibliography.ts +8 -0
  25. package/src/tool/decision-wheel/client.ts +226 -0
  26. package/src/tool/decision-wheel/component.astro +48 -0
  27. package/src/tool/decision-wheel/components/SegmentEditor.astro +22 -0
  28. package/src/tool/decision-wheel/components/SpinHistory.astro +17 -0
  29. package/src/tool/decision-wheel/components/SpinResult.astro +13 -0
  30. package/src/tool/decision-wheel/components/WheelCanvas.astro +23 -0
  31. package/src/tool/decision-wheel/decision-wheel.css +632 -0
  32. package/src/tool/decision-wheel/drawing.ts +144 -0
  33. package/src/tool/decision-wheel/entry.ts +48 -0
  34. package/src/tool/decision-wheel/i18n/de.ts +97 -0
  35. package/src/tool/decision-wheel/i18n/en.ts +251 -0
  36. package/src/tool/decision-wheel/i18n/es.ts +93 -0
  37. package/src/tool/decision-wheel/i18n/fr.ts +93 -0
  38. package/src/tool/decision-wheel/i18n/id.ts +93 -0
  39. package/src/tool/decision-wheel/i18n/it.ts +93 -0
  40. package/src/tool/decision-wheel/i18n/ja.ts +93 -0
  41. package/src/tool/decision-wheel/i18n/ko.ts +93 -0
  42. package/src/tool/decision-wheel/i18n/nl.ts +93 -0
  43. package/src/tool/decision-wheel/i18n/pl.ts +93 -0
  44. package/src/tool/decision-wheel/i18n/pt.ts +93 -0
  45. package/src/tool/decision-wheel/i18n/ru.ts +75 -0
  46. package/src/tool/decision-wheel/i18n/sv.ts +93 -0
  47. package/src/tool/decision-wheel/i18n/tr.ts +93 -0
  48. package/src/tool/decision-wheel/i18n/zh.ts +93 -0
  49. package/src/tool/decision-wheel/index.ts +9 -0
  50. package/src/tool/decision-wheel/logic.test.ts +163 -0
  51. package/src/tool/decision-wheel/logic.ts +145 -0
  52. package/src/tool/decision-wheel/seo.astro +16 -0
  53. package/src/tool/decision-wheel/types.ts +14 -0
  54. package/src/tool/decision-wheel/ui.ts +121 -0
  55. package/src/tool/dice-roller-simulator/client.ts +2 -2
  56. package/src/tool/dice-roller-simulator/i18n/ru.ts +8 -8
  57. package/src/tool/dice-roller-simulator/modules/history.ts +1 -1
  58. package/src/tool/fantasy-runes-translator/bibliography.astro +16 -0
  59. package/src/tool/fantasy-runes-translator/bibliography.ts +16 -0
  60. package/src/tool/fantasy-runes-translator/client.ts +205 -0
  61. package/src/tool/fantasy-runes-translator/component.astro +95 -0
  62. package/src/tool/fantasy-runes-translator/entry.ts +56 -0
  63. package/src/tool/fantasy-runes-translator/fantasy-runes-translator.css +712 -0
  64. package/src/tool/fantasy-runes-translator/i18n/de.ts +139 -0
  65. package/src/tool/fantasy-runes-translator/i18n/en.ts +139 -0
  66. package/src/tool/fantasy-runes-translator/i18n/es.ts +139 -0
  67. package/src/tool/fantasy-runes-translator/i18n/fr.ts +139 -0
  68. package/src/tool/fantasy-runes-translator/i18n/id.ts +139 -0
  69. package/src/tool/fantasy-runes-translator/i18n/it.ts +139 -0
  70. package/src/tool/fantasy-runes-translator/i18n/ja.ts +139 -0
  71. package/src/tool/fantasy-runes-translator/i18n/ko.ts +139 -0
  72. package/src/tool/fantasy-runes-translator/i18n/nl.ts +139 -0
  73. package/src/tool/fantasy-runes-translator/i18n/pl.ts +139 -0
  74. package/src/tool/fantasy-runes-translator/i18n/pt.ts +139 -0
  75. package/src/tool/fantasy-runes-translator/i18n/ru.ts +139 -0
  76. package/src/tool/fantasy-runes-translator/i18n/sv.ts +139 -0
  77. package/src/tool/fantasy-runes-translator/i18n/tr.ts +139 -0
  78. package/src/tool/fantasy-runes-translator/i18n/zh.ts +139 -0
  79. package/src/tool/fantasy-runes-translator/index.ts +9 -0
  80. package/src/tool/fantasy-runes-translator/logic.ts +174 -0
  81. package/src/tool/fantasy-runes-translator/seo.astro +16 -0
  82. package/src/tool/fantasy-runes-translator/types.ts +26 -0
  83. package/src/tool/first-player-selector/bibliography.astro +16 -0
  84. package/src/tool/first-player-selector/bibliography.ts +12 -0
  85. package/src/tool/first-player-selector/client.ts +197 -0
  86. package/src/tool/first-player-selector/component.astro +77 -0
  87. package/src/tool/first-player-selector/entry.ts +42 -0
  88. package/src/tool/first-player-selector/first-player-selector.css +303 -0
  89. package/src/tool/first-player-selector/i18n/de.ts +226 -0
  90. package/src/tool/first-player-selector/i18n/en.ts +226 -0
  91. package/src/tool/first-player-selector/i18n/es.ts +226 -0
  92. package/src/tool/first-player-selector/i18n/fr.ts +227 -0
  93. package/src/tool/first-player-selector/i18n/id.ts +226 -0
  94. package/src/tool/first-player-selector/i18n/it.ts +227 -0
  95. package/src/tool/first-player-selector/i18n/ja.ts +227 -0
  96. package/src/tool/first-player-selector/i18n/ko.ts +227 -0
  97. package/src/tool/first-player-selector/i18n/nl.ts +226 -0
  98. package/src/tool/first-player-selector/i18n/pl.ts +227 -0
  99. package/src/tool/first-player-selector/i18n/pt.ts +226 -0
  100. package/src/tool/first-player-selector/i18n/ru.ts +227 -0
  101. package/src/tool/first-player-selector/i18n/sv.ts +226 -0
  102. package/src/tool/first-player-selector/i18n/tr.ts +226 -0
  103. package/src/tool/first-player-selector/i18n/zh.ts +227 -0
  104. package/src/tool/first-player-selector/index.ts +9 -0
  105. package/src/tool/first-player-selector/modules/AnimationEffects.ts +256 -0
  106. package/src/tool/first-player-selector/modules/SelectionEngine.ts +249 -0
  107. package/src/tool/first-player-selector/modules/SoundEffects.ts +87 -0
  108. package/src/tool/first-player-selector/modules/TouchTracker.ts +148 -0
  109. package/src/tool/first-player-selector/seo.astro +16 -0
  110. package/src/tool/first-player-selector/types.ts +21 -0
  111. package/src/tool/rpg-initiative-tracker/bibliography.astro +16 -0
  112. package/src/tool/rpg-initiative-tracker/bibliography.ts +12 -0
  113. package/src/tool/rpg-initiative-tracker/client.ts +201 -0
  114. package/src/tool/rpg-initiative-tracker/component.astro +23 -0
  115. package/src/tool/rpg-initiative-tracker/components/AddForm.astro +49 -0
  116. package/src/tool/rpg-initiative-tracker/components/CombatantList.astro +16 -0
  117. package/src/tool/rpg-initiative-tracker/components/TurnControls.astro +28 -0
  118. package/src/tool/rpg-initiative-tracker/entry.ts +50 -0
  119. package/src/tool/rpg-initiative-tracker/i18n/de.ts +266 -0
  120. package/src/tool/rpg-initiative-tracker/i18n/en.ts +263 -0
  121. package/src/tool/rpg-initiative-tracker/i18n/es.ts +264 -0
  122. package/src/tool/rpg-initiative-tracker/i18n/fr.ts +263 -0
  123. package/src/tool/rpg-initiative-tracker/i18n/id.ts +263 -0
  124. package/src/tool/rpg-initiative-tracker/i18n/it.ts +263 -0
  125. package/src/tool/rpg-initiative-tracker/i18n/ja.ts +263 -0
  126. package/src/tool/rpg-initiative-tracker/i18n/ko.ts +263 -0
  127. package/src/tool/rpg-initiative-tracker/i18n/nl.ts +263 -0
  128. package/src/tool/rpg-initiative-tracker/i18n/pl.ts +264 -0
  129. package/src/tool/rpg-initiative-tracker/i18n/pt.ts +263 -0
  130. package/src/tool/rpg-initiative-tracker/i18n/ru.ts +264 -0
  131. package/src/tool/rpg-initiative-tracker/i18n/sv.ts +263 -0
  132. package/src/tool/rpg-initiative-tracker/i18n/tr.ts +263 -0
  133. package/src/tool/rpg-initiative-tracker/i18n/zh.ts +263 -0
  134. package/src/tool/rpg-initiative-tracker/index.ts +9 -0
  135. package/src/tool/rpg-initiative-tracker/logic.test.ts +121 -0
  136. package/src/tool/rpg-initiative-tracker/logic.ts +122 -0
  137. package/src/tool/rpg-initiative-tracker/rpg-initiative-tracker.css +617 -0
  138. package/src/tool/rpg-initiative-tracker/seo.astro +16 -0
  139. package/src/tool/rpg-initiative-tracker/types.ts +15 -0
  140. package/src/tool/score-tracker/bibliography.astro +16 -0
  141. package/src/tool/score-tracker/bibliography.ts +12 -0
  142. package/src/tool/score-tracker/client.ts +248 -0
  143. package/src/tool/score-tracker/component.astro +34 -0
  144. package/src/tool/score-tracker/components/Leaderboard.astro +19 -0
  145. package/src/tool/score-tracker/components/PlayerSetup.astro +32 -0
  146. package/src/tool/score-tracker/components/RoundScoring.astro +22 -0
  147. package/src/tool/score-tracker/components/ScoreHistory.astro +25 -0
  148. package/src/tool/score-tracker/entry.ts +57 -0
  149. package/src/tool/score-tracker/i18n/de.ts +278 -0
  150. package/src/tool/score-tracker/i18n/en.ts +278 -0
  151. package/src/tool/score-tracker/i18n/es.ts +251 -0
  152. package/src/tool/score-tracker/i18n/fr.ts +278 -0
  153. package/src/tool/score-tracker/i18n/id.ts +278 -0
  154. package/src/tool/score-tracker/i18n/it.ts +278 -0
  155. package/src/tool/score-tracker/i18n/ja.ts +278 -0
  156. package/src/tool/score-tracker/i18n/ko.ts +278 -0
  157. package/src/tool/score-tracker/i18n/nl.ts +278 -0
  158. package/src/tool/score-tracker/i18n/pl.ts +278 -0
  159. package/src/tool/score-tracker/i18n/pt.ts +278 -0
  160. package/src/tool/score-tracker/i18n/ru.ts +278 -0
  161. package/src/tool/score-tracker/i18n/sv.ts +278 -0
  162. package/src/tool/score-tracker/i18n/tr.ts +278 -0
  163. package/src/tool/score-tracker/i18n/zh.ts +278 -0
  164. package/src/tool/score-tracker/index.ts +9 -0
  165. package/src/tool/score-tracker/logic.test.ts +83 -0
  166. package/src/tool/score-tracker/logic.ts +122 -0
  167. package/src/tool/score-tracker/score-tracker.css +1029 -0
  168. package/src/tool/score-tracker/seo.astro +16 -0
  169. package/src/tool/score-tracker/types.ts +17 -0
  170. package/src/tools.ts +10 -0
  171. package/src/types.ts +1 -2
@@ -0,0 +1,226 @@
1
+ import type { WheelSegment, SpinResultEntry, PresetKey } from './types';
2
+ import {
3
+ getPresetSegments,
4
+ addSegment as addSegmentLogic,
5
+ removeSegment as removeSegmentLogic,
6
+ updateSegment as updateSegmentLogic,
7
+ selectRandomSegment,
8
+ getSelectedIndex,
9
+ addHistoryItem,
10
+ } from './logic';
11
+ import { SoundEngine } from './audio';
12
+ import { drawWheel } from './drawing';
13
+ import { bindSegmentEvents, renderSegmentEditor, renderResult, triggerEmphasis, renderHistory } from './ui';
14
+
15
+ let segments = getPresetSegments('yesno');
16
+ let history: SpinResultEntry[] = [];
17
+ let isSpinning = false;
18
+ let currentAngle = 0;
19
+
20
+ const MAX_SEGMENTS = 16;
21
+ const soundEngine = new SoundEngine();
22
+
23
+ const canvas = document.getElementById('wheel-canvas') as HTMLCanvasElement;
24
+ const spinBtn = document.getElementById('spin-btn') as HTMLButtonElement;
25
+ const resultDisplay = document.getElementById('result-display') as HTMLElement;
26
+ const historyList = document.getElementById('history-list') as HTMLElement;
27
+ const historyEmpty = document.getElementById('history-empty') as HTMLElement;
28
+ const clearBtn = document.getElementById('clear-history-btn') as HTMLButtonElement;
29
+ const segmentList = document.getElementById('segment-list') as HTMLElement;
30
+ const addBtn = document.getElementById('add-segment-btn') as HTMLButtonElement;
31
+ const mainCard = document.querySelector('.tool-main-card') as HTMLElement;
32
+ const pointerEl = document.querySelector('.wheel-pointer') as HTMLElement;
33
+ const muteBtn = document.getElementById('mute-btn') as HTMLButtonElement;
34
+
35
+ if (muteBtn) {
36
+ const onSvg = muteBtn.querySelector('.sound-on-svg') as HTMLElement;
37
+ const offSvg = muteBtn.querySelector('.sound-off-svg') as HTMLElement;
38
+ muteBtn.addEventListener('click', () => {
39
+ const isMuted = !soundEngine.isMuted();
40
+ soundEngine.setMuted(isMuted);
41
+ if (onSvg && offSvg) {
42
+ onSvg.style.display = isMuted ? 'none' : 'block';
43
+ offSvg.style.display = isMuted ? 'block' : 'none';
44
+ }
45
+ muteBtn.setAttribute('aria-label', isMuted ? 'Unmute Sounds' : 'Mute Sounds');
46
+ });
47
+ }
48
+
49
+ function t(key: string): string {
50
+ if (!mainCard) return '';
51
+ try {
52
+ const ui = JSON.parse(mainCard.getAttribute('data-ui') || '{}');
53
+ return ui[key] || '';
54
+ } catch {
55
+ return '';
56
+ }
57
+ }
58
+
59
+ interface SpinState {
60
+ startTime: number;
61
+ duration: number;
62
+ startAngle: number;
63
+ totalRotation: number;
64
+ lastIndex: number;
65
+ resolve: () => void;
66
+ }
67
+
68
+ function runSpinAnimation(now: number, state: SpinState): void {
69
+ const elapsed = now - state.startTime;
70
+ const progress = Math.min(elapsed / state.duration, 1);
71
+ const eased = 1 - Math.pow(1 - progress, 4);
72
+ currentAngle = state.startAngle + state.totalRotation * eased;
73
+ drawWheel(canvas, segments, currentAngle);
74
+
75
+ const currentIndex = getSelectedIndex(segments, currentAngle);
76
+ if (currentIndex !== state.lastIndex) {
77
+ soundEngine.playTick();
78
+ if (pointerEl) {
79
+ pointerEl.classList.add('tick-active');
80
+ setTimeout(() => {
81
+ pointerEl.classList.remove('tick-active');
82
+ }, 40);
83
+ }
84
+ state.lastIndex = currentIndex;
85
+ }
86
+
87
+ if (progress < 1) {
88
+ requestAnimationFrame((t) => runSpinAnimation(t, state));
89
+ } else {
90
+ currentAngle = state.startAngle + state.totalRotation;
91
+ drawWheel(canvas, segments, currentAngle);
92
+ state.resolve();
93
+ }
94
+ }
95
+
96
+ function animateSpin(targetIndex: number): Promise<void> {
97
+ return new Promise((resolve) => {
98
+ const totalWeight = segments.reduce((sum, s) => sum + (s.weight || 1), 0);
99
+ let acc = 0;
100
+ for (let i = 0; i < targetIndex; i++) {
101
+ acc += segments[i].weight || 1;
102
+ }
103
+ const targetSliceWeight = segments[targetIndex].weight || 1;
104
+ const startAngleSeg = (acc / totalWeight) * 360;
105
+ const endAngleSeg = ((acc + targetSliceWeight) / totalWeight) * 360;
106
+ const targetAngle = startAngleSeg + (endAngleSeg - startAngleSeg) / 2;
107
+
108
+ const fullSpins = 4 + Math.floor(Math.random() * 3);
109
+ const totalRotation = fullSpins * 360 + (360 - targetAngle);
110
+ const duration = 2500 + fullSpins * 300;
111
+
112
+ const state: SpinState = {
113
+ startTime: performance.now(),
114
+ duration,
115
+ startAngle: currentAngle,
116
+ totalRotation,
117
+ lastIndex: getSelectedIndex(segments, currentAngle),
118
+ resolve,
119
+ };
120
+
121
+ requestAnimationFrame((t) => runSpinAnimation(t, state));
122
+ });
123
+ }
124
+
125
+ function setSegmentsWithReset(newSegments: WheelSegment[]): void {
126
+ segments = newSegments;
127
+ currentAngle = 0;
128
+ drawWheel(canvas, segments, currentAngle);
129
+ renderSegmentEditor(segmentList, segments, t, bindAll);
130
+ renderResult(resultDisplay, null, false, t);
131
+ }
132
+
133
+ function handleUpdateSegment(id: string, field: string, value: string): void {
134
+ segments = updateSegmentLogic(segments, id, field, value);
135
+ if (field === 'color' || field === 'weight') {
136
+ drawWheel(canvas, segments, currentAngle);
137
+ }
138
+ }
139
+
140
+ function handleRemoveSegment(id: string): void {
141
+ segments = removeSegmentLogic(segments, id);
142
+ renderSegmentEditor(segmentList, segments, t, bindAll);
143
+ drawWheel(canvas, segments, currentAngle);
144
+ }
145
+
146
+ function bindAll(): void {
147
+ bindSegmentEvents(segmentList, handleUpdateSegment, handleRemoveSegment);
148
+ }
149
+
150
+ async function onSpin(): Promise<void> {
151
+ if (isSpinning) return;
152
+ if (segments.length < 2) return;
153
+ isSpinning = true;
154
+ spinBtn.disabled = true;
155
+ renderResult(resultDisplay, null, true, t);
156
+
157
+ const canvasCard = canvas.closest('.canvas-card') as HTMLElement;
158
+ if (canvasCard) {
159
+ canvasCard.classList.add('spinning');
160
+ }
161
+
162
+ const targetIndex = selectRandomSegment(segments);
163
+ await animateSpin(targetIndex);
164
+
165
+ const resultSegment = segments[targetIndex];
166
+ history = addHistoryItem(history, resultSegment, targetIndex);
167
+
168
+ soundEngine.playChime();
169
+
170
+ if (canvasCard) {
171
+ canvasCard.classList.remove('spinning');
172
+ }
173
+
174
+ isSpinning = false;
175
+ spinBtn.disabled = false;
176
+ renderResult(resultDisplay, { segment: resultSegment, index: targetIndex, timestamp: Date.now() }, false, t);
177
+ renderHistory(historyList, historyEmpty, history);
178
+ triggerEmphasis();
179
+ }
180
+
181
+ spinBtn.addEventListener('click', onSpin);
182
+
183
+ clearBtn.addEventListener('click', () => {
184
+ history = [];
185
+ renderHistory(historyList, historyEmpty, history);
186
+ });
187
+
188
+ addBtn.addEventListener('click', () => {
189
+ if (segments.length >= MAX_SEGMENTS) return;
190
+ segments = addSegmentLogic(segments);
191
+ renderSegmentEditor(segmentList, segments, t, bindAll);
192
+ drawWheel(canvas, segments, currentAngle);
193
+ });
194
+
195
+ document.querySelectorAll('.preset-btn').forEach((btn) => {
196
+ btn.addEventListener('click', (e) => {
197
+ const el = e.currentTarget as HTMLButtonElement;
198
+ const key = el.dataset.preset as PresetKey;
199
+ loadPreset(key);
200
+ });
201
+ });
202
+
203
+ function loadPreset(key: PresetKey): void {
204
+ const newSegments = getPresetSegments(key);
205
+ setSegmentsWithReset(newSegments);
206
+ }
207
+
208
+ document.querySelectorAll('.tab-header').forEach((btn) => {
209
+ btn.addEventListener('click', (e) => {
210
+ const target = e.currentTarget as HTMLButtonElement;
211
+ const tabName = target.dataset.tab;
212
+
213
+ document.querySelectorAll('.tab-header').forEach((h) => h.classList.remove('active'));
214
+ document.querySelectorAll('.tab-pane').forEach((p) => p.classList.remove('active'));
215
+
216
+ target.classList.add('active');
217
+ const pane = document.getElementById(`pane-${tabName}`);
218
+ if (pane) {
219
+ pane.classList.add('active');
220
+ }
221
+ });
222
+ });
223
+
224
+ drawWheel(canvas, segments, currentAngle);
225
+ renderSegmentEditor(segmentList, segments, t, bindAll);
226
+ renderHistory(historyList, historyEmpty, history);
@@ -0,0 +1,48 @@
1
+ ---
2
+ import WheelCanvas from './components/WheelCanvas.astro';
3
+ import SegmentEditor from './components/SegmentEditor.astro';
4
+ import SpinResult from './components/SpinResult.astro';
5
+ import SpinHistory from './components/SpinHistory.astro';
6
+
7
+ interface Props {
8
+ ui: Record<string, string>;
9
+ }
10
+
11
+ const { ui } = Astro.props;
12
+ ---
13
+
14
+ <div class="tool-main-card" data-ui={JSON.stringify(ui)}>
15
+ <div class="wheel-dashboard">
16
+ <div class="wheel-layout">
17
+
18
+ <div class="wheel-canvas-section">
19
+ <div class="canvas-card">
20
+ <WheelCanvas labels={ui} />
21
+ <div class="wheel-pointer" aria-hidden="true"></div>
22
+ </div>
23
+ <button type="button" id="spin-btn" class="spin-button">{ui.spinButton || 'Spin the Wheel'}</button>
24
+ <SpinResult labels={ui} />
25
+ </div>
26
+
27
+ <div class="wheel-controls-section">
28
+ <div class="tab-container">
29
+ <div class="tab-headers">
30
+ <button type="button" class="tab-header active" data-tab="editor">{ui.presetLabel || 'Customize'}</button>
31
+ <button type="button" class="tab-header" data-tab="history">{ui.historyTitle || 'History'}</button>
32
+ </div>
33
+ <div class="tab-contents">
34
+ <div class="tab-pane active" id="pane-editor">
35
+ <SegmentEditor labels={ui} />
36
+ </div>
37
+ <div class="tab-pane" id="pane-history">
38
+ <SpinHistory labels={ui} />
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ </div>
45
+ </div>
46
+ </div>
47
+
48
+ <script src="./client.ts"></script>
@@ -0,0 +1,22 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ ---
8
+
9
+ <div class="editor-card">
10
+ <span class="editor-label">{labels.presetLabel || 'Presets'}</span>
11
+ <div class="preset-group">
12
+ <button type="button" class="preset-btn" data-preset="yesno">{labels.presetYesNo || 'Yes / No'}</button>
13
+ <button type="button" class="preset-btn" data-preset="numbers">{labels.presetNumbers || 'Numbers 1-6'}</button>
14
+ <button type="button" class="preset-btn" data-preset="actions">{labels.presetActions || 'Actions'}</button>
15
+ <button type="button" class="preset-btn" data-preset="d20">{labels.presetD20 || 'D20'}</button>
16
+ <button type="button" class="preset-btn" data-preset="alignment">{labels.presetAlignment || 'Alignment'}</button>
17
+ <button type="button" class="preset-btn" data-preset="loot">{labels.presetLoot || 'Loot Rarity'}</button>
18
+ <button type="button" class="preset-btn" data-preset="custom">{labels.presetCustom || 'Custom'}</button>
19
+ </div>
20
+ <div class="segment-list" id="segment-list"></div>
21
+ <button type="button" id="add-segment-btn" class="btn-add-segment">{labels.addSegmentLabel || 'Add Segment'}</button>
22
+ </div>
@@ -0,0 +1,17 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ ---
8
+
9
+ <div class="history-section">
10
+ <div class="history-header">
11
+ <h3 class="history-title">{labels.historyTitle || 'Spin History'}</h3>
12
+ <button type="button" id="clear-history-btn" class="btn-clear-history">{labels.clearHistory || 'Clear'}</button>
13
+ </div>
14
+ <div class="history-list" id="history-list">
15
+ <div class="history-empty" id="history-empty">{labels.noHistory || 'No spins yet.'}</div>
16
+ </div>
17
+ </div>
@@ -0,0 +1,13 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ ---
8
+
9
+ <div class="result-section">
10
+ <div id="result-display" class="result-display">
11
+ <div class="result-placeholder">{labels.noHistory || 'Spin to see result'}</div>
12
+ </div>
13
+ </div>
@@ -0,0 +1,23 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ const label = labels.title || 'Decision Wheel Spinner';
8
+ ---
9
+
10
+ <div class="wheel-canvas-container">
11
+ <canvas id="wheel-canvas" class="wheel-canvas" width="300" height="300" role="img" aria-label={label}></canvas>
12
+ </div>
13
+ <button type="button" id="mute-btn" class="mute-btn" aria-label="Mute Sounds">
14
+ <svg class="sound-on-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
15
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
16
+ <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
17
+ </svg>
18
+ <svg class="sound-off-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;">
19
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
20
+ <line x1="23" y1="9" x2="17" y2="15"></line>
21
+ <line x1="17" y1="9" x2="23" y2="15"></line>
22
+ </svg>
23
+ </button>