@react-chess-tools/react-chess-clock 1.0.1
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.
- package/CHANGELOG.md +7 -0
- package/README.md +697 -0
- package/dist/index.cjs +1014 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +528 -0
- package/dist/index.d.ts +528 -0
- package/dist/index.js +969 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/src/components/ChessClock/ChessClock.stories.tsx +782 -0
- package/src/components/ChessClock/index.ts +44 -0
- package/src/components/ChessClock/parts/Display.tsx +69 -0
- package/src/components/ChessClock/parts/PlayPause.tsx +190 -0
- package/src/components/ChessClock/parts/Reset.tsx +90 -0
- package/src/components/ChessClock/parts/Root.tsx +37 -0
- package/src/components/ChessClock/parts/Switch.tsx +84 -0
- package/src/components/ChessClock/parts/__tests__/Display.test.tsx +149 -0
- package/src/components/ChessClock/parts/__tests__/PlayPause.test.tsx +411 -0
- package/src/components/ChessClock/parts/__tests__/Reset.test.tsx +160 -0
- package/src/components/ChessClock/parts/__tests__/Root.test.tsx +49 -0
- package/src/components/ChessClock/parts/__tests__/Switch.test.tsx +204 -0
- package/src/hooks/__tests__/clockReducer.test.ts +985 -0
- package/src/hooks/__tests__/useChessClock.test.tsx +1080 -0
- package/src/hooks/clockReducer.ts +379 -0
- package/src/hooks/useChessClock.ts +406 -0
- package/src/hooks/useChessClockContext.ts +35 -0
- package/src/index.ts +65 -0
- package/src/types.ts +217 -0
- package/src/utils/__tests__/calculateSwitchTime.test.ts +150 -0
- package/src/utils/__tests__/formatTime.test.ts +83 -0
- package/src/utils/__tests__/timeControl.test.ts +414 -0
- package/src/utils/__tests__/timingMethods.test.ts +170 -0
- package/src/utils/calculateSwitchTime.ts +37 -0
- package/src/utils/formatTime.ts +59 -0
- package/src/utils/presets.ts +47 -0
- package/src/utils/timeControl.ts +205 -0
- package/src/utils/timingMethods.ts +103 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
// src/components/ChessClock/parts/Root.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
// src/hooks/useChessClockContext.ts
|
|
5
|
+
import { createContext, useContext } from "react";
|
|
6
|
+
var ChessClockContext = createContext(
|
|
7
|
+
null
|
|
8
|
+
);
|
|
9
|
+
function useChessClockContext() {
|
|
10
|
+
const context = useContext(ChessClockContext);
|
|
11
|
+
if (!context) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"useChessClockContext must be used within a ChessClock.Root component. Make sure your component is wrapped with <ChessClock.Root>."
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return context;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/hooks/useChessClock.ts
|
|
20
|
+
import {
|
|
21
|
+
useCallback,
|
|
22
|
+
useEffect,
|
|
23
|
+
useMemo,
|
|
24
|
+
useReducer,
|
|
25
|
+
useRef,
|
|
26
|
+
useState
|
|
27
|
+
} from "react";
|
|
28
|
+
|
|
29
|
+
// src/utils/timeControl.ts
|
|
30
|
+
var TIME_CONTROL_REGEX = /^(\d+(?:\.\d+)?)(?:\+(\d+(?:\.\d+)?))?$/;
|
|
31
|
+
var PERIOD_REGEX = /^(?:(?:(\d+)|sd|g)\/)?(\d+(?:\.\d+)?)(?:\+(\d+(?:\.\d+)?))?$/i;
|
|
32
|
+
function parseTimeControlString(input) {
|
|
33
|
+
const match = input.match(TIME_CONTROL_REGEX);
|
|
34
|
+
if (!match) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Invalid time control string: "${input}". Expected format: "5+3" (minutes + increment) or "10" (minutes only)`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const minutes = parseFloat(match[1]);
|
|
40
|
+
const increment = match[2] ? parseFloat(match[2]) : 0;
|
|
41
|
+
return {
|
|
42
|
+
baseTime: Math.round(minutes * 60),
|
|
43
|
+
increment: Math.round(increment)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function parsePeriod(periodStr) {
|
|
47
|
+
const trimmed = periodStr.trim();
|
|
48
|
+
const match = trimmed.match(PERIOD_REGEX);
|
|
49
|
+
if (!match) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Invalid period format: "${trimmed}". Expected format: "40/90+30" or "sd/30+30"`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const [, movesStr, timeStr, incrementStr] = match;
|
|
55
|
+
const moves = movesStr ? parseInt(movesStr, 10) : void 0;
|
|
56
|
+
const minutes = parseFloat(timeStr);
|
|
57
|
+
const increment = incrementStr ? parseFloat(incrementStr) : 0;
|
|
58
|
+
return {
|
|
59
|
+
baseTime: Math.round(minutes * 60),
|
|
60
|
+
increment: increment > 0 ? Math.round(increment) : void 0,
|
|
61
|
+
moves
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function parseMultiPeriodTimeControl(input) {
|
|
65
|
+
const parts = input.split(/\s*,\s*/);
|
|
66
|
+
return parts.map((part) => parsePeriod(part));
|
|
67
|
+
}
|
|
68
|
+
function normalizeTimeControl(input, timingMethod, clockStart) {
|
|
69
|
+
if (Array.isArray(input)) {
|
|
70
|
+
if (input.length === 0) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"Multi-period time control must have at least one period"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const periods = input.map((period) => ({
|
|
76
|
+
...period,
|
|
77
|
+
baseTime: period.baseTime * 1e3,
|
|
78
|
+
increment: period.increment !== void 0 ? period.increment * 1e3 : void 0,
|
|
79
|
+
delay: period.delay !== void 0 ? period.delay * 1e3 : void 0
|
|
80
|
+
}));
|
|
81
|
+
const lastPeriod = periods[periods.length - 1];
|
|
82
|
+
if (lastPeriod.moves !== void 0) {
|
|
83
|
+
lastPeriod.moves = void 0;
|
|
84
|
+
}
|
|
85
|
+
const firstPeriod = periods[0];
|
|
86
|
+
return {
|
|
87
|
+
baseTime: firstPeriod.baseTime,
|
|
88
|
+
increment: firstPeriod.increment ?? 0,
|
|
89
|
+
delay: firstPeriod.delay ?? 0,
|
|
90
|
+
timingMethod,
|
|
91
|
+
clockStart,
|
|
92
|
+
periods
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
let parsed;
|
|
96
|
+
if (typeof input === "string") {
|
|
97
|
+
parsed = parseTimeControlString(input);
|
|
98
|
+
} else {
|
|
99
|
+
parsed = input;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
baseTime: parsed.baseTime * 1e3,
|
|
103
|
+
// Convert to milliseconds
|
|
104
|
+
increment: (parsed.increment ?? 0) * 1e3,
|
|
105
|
+
// Convert to milliseconds
|
|
106
|
+
delay: (parsed.delay ?? 0) * 1e3,
|
|
107
|
+
// Convert to milliseconds
|
|
108
|
+
timingMethod,
|
|
109
|
+
clockStart
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function parseTimeControlConfig(config) {
|
|
113
|
+
const normalized = normalizeTimeControl(
|
|
114
|
+
config.time,
|
|
115
|
+
config.timingMethod ?? "fischer",
|
|
116
|
+
config.clockStart ?? "delayed"
|
|
117
|
+
);
|
|
118
|
+
return {
|
|
119
|
+
...normalized,
|
|
120
|
+
whiteTimeOverride: config.whiteTime ? config.whiteTime * 1e3 : void 0,
|
|
121
|
+
blackTimeOverride: config.blackTime ? config.blackTime * 1e3 : void 0
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function getInitialTimes(config) {
|
|
125
|
+
return {
|
|
126
|
+
white: config.whiteTimeOverride ?? config.baseTime,
|
|
127
|
+
black: config.blackTimeOverride ?? config.baseTime
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/utils/formatTime.ts
|
|
132
|
+
var AUTO_FORMAT_THRESHOLD = 20;
|
|
133
|
+
function formatClockTime(milliseconds, format = "auto") {
|
|
134
|
+
const clampedMs = Math.max(0, milliseconds);
|
|
135
|
+
if (format === "auto") {
|
|
136
|
+
format = clampedMs < AUTO_FORMAT_THRESHOLD * 1e3 ? "ss.d" : "mm:ss";
|
|
137
|
+
}
|
|
138
|
+
const totalSeconds = Math.ceil(clampedMs / 1e3);
|
|
139
|
+
switch (format) {
|
|
140
|
+
case "ss.d": {
|
|
141
|
+
const secondsWithDecimal = clampedMs / 1e3;
|
|
142
|
+
return secondsWithDecimal.toFixed(1);
|
|
143
|
+
}
|
|
144
|
+
case "mm:ss": {
|
|
145
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
146
|
+
const seconds = totalSeconds % 60;
|
|
147
|
+
if (totalMinutes >= 60) {
|
|
148
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
149
|
+
const minutes = totalMinutes % 60;
|
|
150
|
+
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
151
|
+
}
|
|
152
|
+
return `${totalMinutes}:${seconds.toString().padStart(2, "0")}`;
|
|
153
|
+
}
|
|
154
|
+
case "hh:mm:ss": {
|
|
155
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
156
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
157
|
+
const seconds = totalSeconds % 60;
|
|
158
|
+
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
159
|
+
}
|
|
160
|
+
default:
|
|
161
|
+
return String(totalSeconds);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/utils/timingMethods.ts
|
|
166
|
+
function applyFischerIncrement(currentTime, increment) {
|
|
167
|
+
return currentTime + increment;
|
|
168
|
+
}
|
|
169
|
+
function applyBronsteinDelay(currentTime, timeSpent, delay) {
|
|
170
|
+
const addBack = Math.min(timeSpent, delay);
|
|
171
|
+
return currentTime + addBack;
|
|
172
|
+
}
|
|
173
|
+
function calculateSwitchAdjustment(timingMethod, currentTime, timeSpent, config) {
|
|
174
|
+
switch (timingMethod) {
|
|
175
|
+
case "fischer":
|
|
176
|
+
return applyFischerIncrement(currentTime, config.increment);
|
|
177
|
+
case "delay":
|
|
178
|
+
return currentTime;
|
|
179
|
+
case "bronstein":
|
|
180
|
+
return applyBronsteinDelay(currentTime, timeSpent, config.delay);
|
|
181
|
+
default:
|
|
182
|
+
return currentTime;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function getInitialActivePlayer(clockStart) {
|
|
186
|
+
return clockStart === "immediate" ? "white" : null;
|
|
187
|
+
}
|
|
188
|
+
function getInitialStatus(clockStart) {
|
|
189
|
+
if (clockStart === "immediate") return "running";
|
|
190
|
+
if (clockStart === "delayed") return "delayed";
|
|
191
|
+
return "idle";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/utils/calculateSwitchTime.ts
|
|
195
|
+
function calculateSwitchTime(currentTime, timeSpent, config) {
|
|
196
|
+
let effectiveElapsed = timeSpent;
|
|
197
|
+
if (config.timingMethod === "delay") {
|
|
198
|
+
effectiveElapsed = Math.max(0, timeSpent - config.delay);
|
|
199
|
+
}
|
|
200
|
+
const newTime = Math.max(0, currentTime - effectiveElapsed);
|
|
201
|
+
return calculateSwitchAdjustment(
|
|
202
|
+
config.timingMethod,
|
|
203
|
+
newTime,
|
|
204
|
+
timeSpent,
|
|
205
|
+
config
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/hooks/clockReducer.ts
|
|
210
|
+
function maybeAdvancePeriod(times, periodState) {
|
|
211
|
+
let newTimes = times;
|
|
212
|
+
let newPeriodState = periodState;
|
|
213
|
+
["white", "black"].forEach((player) => {
|
|
214
|
+
const playerPeriodIndex = newPeriodState.periodIndex[player];
|
|
215
|
+
if (playerPeriodIndex < 0 || playerPeriodIndex >= newPeriodState.periods.length) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const currentPeriod = newPeriodState.periods[playerPeriodIndex];
|
|
219
|
+
if (!(currentPeriod == null ? void 0 : currentPeriod.moves)) return;
|
|
220
|
+
const movesRequired = currentPeriod.moves;
|
|
221
|
+
const playerMoves = newPeriodState.periodMoves[player];
|
|
222
|
+
if (playerMoves >= movesRequired) {
|
|
223
|
+
const nextPeriodIndex = playerPeriodIndex + 1;
|
|
224
|
+
if (nextPeriodIndex >= newPeriodState.periods.length) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const nextPeriod = newPeriodState.periods[nextPeriodIndex];
|
|
228
|
+
if (!nextPeriod) return;
|
|
229
|
+
const addedTime = nextPeriod.baseTime;
|
|
230
|
+
newPeriodState = {
|
|
231
|
+
...newPeriodState,
|
|
232
|
+
periodIndex: {
|
|
233
|
+
...newPeriodState.periodIndex,
|
|
234
|
+
[player]: nextPeriodIndex
|
|
235
|
+
},
|
|
236
|
+
periodMoves: {
|
|
237
|
+
...newPeriodState.periodMoves,
|
|
238
|
+
[player]: 0
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
newTimes = {
|
|
242
|
+
...newTimes,
|
|
243
|
+
[player]: newTimes[player] + addedTime
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
return { times: newTimes, periodState: newPeriodState };
|
|
248
|
+
}
|
|
249
|
+
function clockReducer(state, action) {
|
|
250
|
+
var _a, _b, _c;
|
|
251
|
+
switch (action.type) {
|
|
252
|
+
case "START": {
|
|
253
|
+
if (state.status === "finished") return state;
|
|
254
|
+
if (state.status === "delayed") {
|
|
255
|
+
return state;
|
|
256
|
+
}
|
|
257
|
+
const now = ((_a = action.payload) == null ? void 0 : _a.now) ?? Date.now();
|
|
258
|
+
return {
|
|
259
|
+
...state,
|
|
260
|
+
status: "running",
|
|
261
|
+
activePlayer: state.activePlayer ?? "white",
|
|
262
|
+
// Initialize move start time if not already set
|
|
263
|
+
moveStartTime: state.moveStartTime ?? now
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
case "PAUSE": {
|
|
267
|
+
if (state.status !== "running") return state;
|
|
268
|
+
const now = ((_b = action.payload) == null ? void 0 : _b.now) ?? Date.now();
|
|
269
|
+
const elapsedAtPause = state.moveStartTime !== null ? now - state.moveStartTime : 0;
|
|
270
|
+
return {
|
|
271
|
+
...state,
|
|
272
|
+
status: "paused",
|
|
273
|
+
elapsedAtPause,
|
|
274
|
+
moveStartTime: null
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
case "RESUME": {
|
|
278
|
+
if (state.status !== "paused") return state;
|
|
279
|
+
const now = ((_c = action.payload) == null ? void 0 : _c.now) ?? Date.now();
|
|
280
|
+
return {
|
|
281
|
+
...state,
|
|
282
|
+
status: "running",
|
|
283
|
+
moveStartTime: now - state.elapsedAtPause,
|
|
284
|
+
elapsedAtPause: 0
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
case "SWITCH": {
|
|
288
|
+
if (state.status === "finished") return state;
|
|
289
|
+
let newPeriodState = state.periodState;
|
|
290
|
+
const movedPlayer = state.activePlayer;
|
|
291
|
+
const now = action.payload.now ?? Date.now();
|
|
292
|
+
if (state.status === "delayed") {
|
|
293
|
+
const newCount = state.switchCount + 1;
|
|
294
|
+
const newPlayer2 = newCount % 2 === 1 ? "black" : "white";
|
|
295
|
+
const delayedMovePlayer = newCount % 2 === 1 ? "white" : "black";
|
|
296
|
+
if (state.periodState) {
|
|
297
|
+
newPeriodState = {
|
|
298
|
+
...state.periodState,
|
|
299
|
+
periodMoves: {
|
|
300
|
+
...state.periodState.periodMoves,
|
|
301
|
+
[delayedMovePlayer]: state.periodState.periodMoves[delayedMovePlayer] + 1
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (newPeriodState) {
|
|
306
|
+
const advanced = maybeAdvancePeriod(state.times, newPeriodState);
|
|
307
|
+
newPeriodState = advanced.periodState;
|
|
308
|
+
}
|
|
309
|
+
const newStateStatus = newCount >= 2 ? "running" : "delayed";
|
|
310
|
+
return {
|
|
311
|
+
...state,
|
|
312
|
+
activePlayer: newPlayer2,
|
|
313
|
+
switchCount: newCount,
|
|
314
|
+
status: newStateStatus,
|
|
315
|
+
periodState: newPeriodState,
|
|
316
|
+
// Start timing when transitioning to running
|
|
317
|
+
moveStartTime: newStateStatus === "running" ? now : state.moveStartTime
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
if (movedPlayer === null) return state;
|
|
321
|
+
const { newTimes } = action.payload;
|
|
322
|
+
const newPlayer = movedPlayer === "white" ? "black" : "white";
|
|
323
|
+
if (state.periodState) {
|
|
324
|
+
newPeriodState = {
|
|
325
|
+
...state.periodState,
|
|
326
|
+
periodMoves: {
|
|
327
|
+
...state.periodState.periodMoves,
|
|
328
|
+
[movedPlayer]: state.periodState.periodMoves[movedPlayer] + 1
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
let resolvedTimes = newTimes ?? state.times;
|
|
333
|
+
if (newPeriodState) {
|
|
334
|
+
const advanced = maybeAdvancePeriod(resolvedTimes, newPeriodState);
|
|
335
|
+
resolvedTimes = advanced.times;
|
|
336
|
+
newPeriodState = advanced.periodState;
|
|
337
|
+
}
|
|
338
|
+
const newStatus = state.status === "idle" ? "running" : state.status;
|
|
339
|
+
return {
|
|
340
|
+
...state,
|
|
341
|
+
activePlayer: newPlayer,
|
|
342
|
+
times: resolvedTimes,
|
|
343
|
+
switchCount: state.switchCount + 1,
|
|
344
|
+
periodState: newPeriodState,
|
|
345
|
+
status: newStatus,
|
|
346
|
+
// Reset timing for the new player
|
|
347
|
+
moveStartTime: newStatus === "running" ? now : null,
|
|
348
|
+
elapsedAtPause: 0
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
case "TIMEOUT": {
|
|
352
|
+
const { player } = action.payload;
|
|
353
|
+
return {
|
|
354
|
+
...state,
|
|
355
|
+
status: "finished",
|
|
356
|
+
timeout: player,
|
|
357
|
+
times: { ...state.times, [player]: 0 }
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
case "RESET": {
|
|
361
|
+
const config = parseTimeControlConfig(action.payload);
|
|
362
|
+
const initialTimes = getInitialTimes(config);
|
|
363
|
+
const now = action.payload.now;
|
|
364
|
+
const periodState = config.periods ? {
|
|
365
|
+
periodIndex: { white: 0, black: 0 },
|
|
366
|
+
periodMoves: { white: 0, black: 0 },
|
|
367
|
+
periods: config.periods
|
|
368
|
+
} : void 0;
|
|
369
|
+
return createInitialClockState(
|
|
370
|
+
initialTimes,
|
|
371
|
+
getInitialStatus(config.clockStart),
|
|
372
|
+
getInitialActivePlayer(config.clockStart),
|
|
373
|
+
config,
|
|
374
|
+
periodState,
|
|
375
|
+
now
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
case "ADD_TIME": {
|
|
379
|
+
const { player, milliseconds } = action.payload;
|
|
380
|
+
const now = action.payload.now ?? Date.now();
|
|
381
|
+
const newTimes = {
|
|
382
|
+
...state.times,
|
|
383
|
+
[player]: state.times[player] + milliseconds
|
|
384
|
+
};
|
|
385
|
+
const resetElapsed = state.status === "paused" && player === state.activePlayer;
|
|
386
|
+
return {
|
|
387
|
+
...state,
|
|
388
|
+
times: newTimes,
|
|
389
|
+
moveStartTime: state.status === "running" ? now : null,
|
|
390
|
+
...resetElapsed && { elapsedAtPause: 0 }
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
case "SET_TIME": {
|
|
394
|
+
const { player, milliseconds } = action.payload;
|
|
395
|
+
const now = action.payload.now ?? Date.now();
|
|
396
|
+
const newTimes = {
|
|
397
|
+
...state.times,
|
|
398
|
+
[player]: Math.max(0, milliseconds)
|
|
399
|
+
};
|
|
400
|
+
const resetElapsed = state.status === "paused" && player === state.activePlayer;
|
|
401
|
+
return {
|
|
402
|
+
...state,
|
|
403
|
+
times: newTimes,
|
|
404
|
+
moveStartTime: state.status === "running" ? now : null,
|
|
405
|
+
...resetElapsed && { elapsedAtPause: 0 }
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
default:
|
|
409
|
+
return state;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function createInitialClockState(initialTimes, status, activePlayer, config, periodState, now) {
|
|
413
|
+
return {
|
|
414
|
+
times: initialTimes,
|
|
415
|
+
initialTimes,
|
|
416
|
+
status,
|
|
417
|
+
activePlayer,
|
|
418
|
+
timeout: null,
|
|
419
|
+
switchCount: 0,
|
|
420
|
+
periodState,
|
|
421
|
+
config,
|
|
422
|
+
// If starting immediately, initialize the move start time
|
|
423
|
+
moveStartTime: status === "running" ? now ?? Date.now() : null,
|
|
424
|
+
elapsedAtPause: 0
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/hooks/useChessClock.ts
|
|
429
|
+
var DISABLED_CLOCK_CONFIG = {
|
|
430
|
+
time: { baseTime: 0 }
|
|
431
|
+
};
|
|
432
|
+
function serializeTimeRelevantConfig(config) {
|
|
433
|
+
return JSON.stringify({
|
|
434
|
+
time: config.time,
|
|
435
|
+
timingMethod: config.timingMethod,
|
|
436
|
+
clockStart: config.clockStart,
|
|
437
|
+
whiteTime: config.whiteTime,
|
|
438
|
+
blackTime: config.blackTime
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
function calculateDisplayTime(baseTime, moveStartTime, elapsedAtPause, timingMethod, delay) {
|
|
442
|
+
if (moveStartTime === null) {
|
|
443
|
+
let effectiveElapsed2 = elapsedAtPause;
|
|
444
|
+
if (timingMethod === "delay") {
|
|
445
|
+
effectiveElapsed2 = Math.max(0, elapsedAtPause - delay);
|
|
446
|
+
}
|
|
447
|
+
return Math.max(0, baseTime - effectiveElapsed2);
|
|
448
|
+
}
|
|
449
|
+
const now = Date.now();
|
|
450
|
+
const rawElapsed = now - moveStartTime;
|
|
451
|
+
let effectiveElapsed = rawElapsed;
|
|
452
|
+
if (timingMethod === "delay") {
|
|
453
|
+
effectiveElapsed = Math.max(0, rawElapsed - delay);
|
|
454
|
+
}
|
|
455
|
+
return Math.max(0, baseTime - effectiveElapsed);
|
|
456
|
+
}
|
|
457
|
+
function useChessClock(options) {
|
|
458
|
+
const [state, dispatch] = useReducer(clockReducer, null, () => {
|
|
459
|
+
const initialConfig = parseTimeControlConfig(options);
|
|
460
|
+
const initialTimesValue = getInitialTimes(initialConfig);
|
|
461
|
+
const initialPeriodState = initialConfig.periods && initialConfig.periods.length > 1 ? {
|
|
462
|
+
periodIndex: { white: 0, black: 0 },
|
|
463
|
+
periodMoves: { white: 0, black: 0 },
|
|
464
|
+
periods: initialConfig.periods
|
|
465
|
+
} : void 0;
|
|
466
|
+
return createInitialClockState(
|
|
467
|
+
initialTimesValue,
|
|
468
|
+
getInitialStatus(initialConfig.clockStart),
|
|
469
|
+
getInitialActivePlayer(initialConfig.clockStart),
|
|
470
|
+
initialConfig,
|
|
471
|
+
initialPeriodState
|
|
472
|
+
);
|
|
473
|
+
});
|
|
474
|
+
const optionsRef = useRef(options);
|
|
475
|
+
optionsRef.current = options;
|
|
476
|
+
const stateRef = useRef(state);
|
|
477
|
+
stateRef.current = state;
|
|
478
|
+
const configKey = serializeTimeRelevantConfig(options);
|
|
479
|
+
const prevConfigRef = useRef(configKey);
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
if (prevConfigRef.current !== configKey) {
|
|
482
|
+
prevConfigRef.current = configKey;
|
|
483
|
+
dispatch({ type: "RESET", payload: { ...options, now: Date.now() } });
|
|
484
|
+
}
|
|
485
|
+
}, [configKey, options]);
|
|
486
|
+
const [tick, forceUpdate] = useState(0);
|
|
487
|
+
const displayTimes = state.activePlayer === null ? state.times : {
|
|
488
|
+
...state.times,
|
|
489
|
+
[state.activePlayer]: calculateDisplayTime(
|
|
490
|
+
state.times[state.activePlayer],
|
|
491
|
+
state.moveStartTime,
|
|
492
|
+
state.elapsedAtPause,
|
|
493
|
+
state.config.timingMethod,
|
|
494
|
+
state.config.delay
|
|
495
|
+
)
|
|
496
|
+
};
|
|
497
|
+
const activePlayerTimedOut = state.status === "running" && state.activePlayer !== null && displayTimes[state.activePlayer] <= 0;
|
|
498
|
+
useEffect(() => {
|
|
499
|
+
if (state.status !== "running" || state.activePlayer === null) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const intervalId = setInterval(() => forceUpdate((c) => c + 1), 100);
|
|
503
|
+
return () => clearInterval(intervalId);
|
|
504
|
+
}, [state.status, state.activePlayer]);
|
|
505
|
+
useEffect(() => {
|
|
506
|
+
var _a, _b;
|
|
507
|
+
if (state.status === "running" && state.activePlayer !== null) {
|
|
508
|
+
(_b = (_a = optionsRef.current).onTimeUpdate) == null ? void 0 : _b.call(_a, displayTimes);
|
|
509
|
+
}
|
|
510
|
+
}, [tick]);
|
|
511
|
+
useEffect(() => {
|
|
512
|
+
var _a, _b;
|
|
513
|
+
if (activePlayerTimedOut && state.activePlayer !== null) {
|
|
514
|
+
dispatch({ type: "TIMEOUT", payload: { player: state.activePlayer } });
|
|
515
|
+
(_b = (_a = optionsRef.current).onTimeout) == null ? void 0 : _b.call(_a, state.activePlayer);
|
|
516
|
+
}
|
|
517
|
+
}, [activePlayerTimedOut, state.activePlayer]);
|
|
518
|
+
const previousActivePlayerRef = useRef(state.activePlayer);
|
|
519
|
+
useEffect(() => {
|
|
520
|
+
var _a, _b;
|
|
521
|
+
if (state.activePlayer !== previousActivePlayerRef.current && state.activePlayer !== null) {
|
|
522
|
+
(_b = (_a = optionsRef.current).onSwitch) == null ? void 0 : _b.call(_a, state.activePlayer);
|
|
523
|
+
}
|
|
524
|
+
previousActivePlayerRef.current = state.activePlayer;
|
|
525
|
+
}, [state.activePlayer]);
|
|
526
|
+
const info = useMemo(
|
|
527
|
+
() => ({
|
|
528
|
+
isRunning: state.status === "running",
|
|
529
|
+
isPaused: state.status === "paused",
|
|
530
|
+
isFinished: state.status === "finished",
|
|
531
|
+
isWhiteActive: state.activePlayer === "white",
|
|
532
|
+
isBlackActive: state.activePlayer === "black",
|
|
533
|
+
hasTimeout: state.timeout !== null,
|
|
534
|
+
// Time odds is based on initial configuration, not current remaining time
|
|
535
|
+
hasTimeOdds: state.initialTimes.white !== state.initialTimes.black
|
|
536
|
+
}),
|
|
537
|
+
[
|
|
538
|
+
state.status,
|
|
539
|
+
state.activePlayer,
|
|
540
|
+
state.timeout,
|
|
541
|
+
state.initialTimes.white,
|
|
542
|
+
state.initialTimes.black
|
|
543
|
+
]
|
|
544
|
+
);
|
|
545
|
+
const start = useCallback(() => {
|
|
546
|
+
dispatch({ type: "START", payload: { now: Date.now() } });
|
|
547
|
+
}, []);
|
|
548
|
+
const pause = useCallback(() => {
|
|
549
|
+
dispatch({ type: "PAUSE", payload: { now: Date.now() } });
|
|
550
|
+
}, []);
|
|
551
|
+
const resume = useCallback(() => {
|
|
552
|
+
dispatch({ type: "RESUME", payload: { now: Date.now() } });
|
|
553
|
+
}, []);
|
|
554
|
+
const switchPlayer = useCallback(() => {
|
|
555
|
+
const currentState = stateRef.current;
|
|
556
|
+
const now = Date.now();
|
|
557
|
+
let newTimes;
|
|
558
|
+
if (currentState.status !== "delayed" && currentState.activePlayer && currentState.moveStartTime !== null) {
|
|
559
|
+
const timeSpent = now - currentState.moveStartTime;
|
|
560
|
+
const currentTime = currentState.times[currentState.activePlayer];
|
|
561
|
+
const newTime = calculateSwitchTime(
|
|
562
|
+
currentTime,
|
|
563
|
+
timeSpent,
|
|
564
|
+
currentState.config
|
|
565
|
+
);
|
|
566
|
+
newTimes = {
|
|
567
|
+
...currentState.times,
|
|
568
|
+
[currentState.activePlayer]: newTime
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
dispatch({ type: "SWITCH", payload: { newTimes, now } });
|
|
572
|
+
}, []);
|
|
573
|
+
const reset = useCallback((newTimeControl) => {
|
|
574
|
+
const currentOptions = optionsRef.current;
|
|
575
|
+
const resetConfig = newTimeControl ? { ...currentOptions, time: newTimeControl } : currentOptions;
|
|
576
|
+
dispatch({ type: "RESET", payload: { ...resetConfig, now: Date.now() } });
|
|
577
|
+
}, []);
|
|
578
|
+
const addTime = useCallback((player, milliseconds) => {
|
|
579
|
+
dispatch({
|
|
580
|
+
type: "ADD_TIME",
|
|
581
|
+
payload: { player, milliseconds, now: Date.now() }
|
|
582
|
+
});
|
|
583
|
+
}, []);
|
|
584
|
+
const setTime = useCallback((player, milliseconds) => {
|
|
585
|
+
dispatch({
|
|
586
|
+
type: "SET_TIME",
|
|
587
|
+
payload: { player, milliseconds, now: Date.now() }
|
|
588
|
+
});
|
|
589
|
+
}, []);
|
|
590
|
+
const methods = useMemo(
|
|
591
|
+
() => ({
|
|
592
|
+
start,
|
|
593
|
+
pause,
|
|
594
|
+
resume,
|
|
595
|
+
switch: switchPlayer,
|
|
596
|
+
reset,
|
|
597
|
+
addTime,
|
|
598
|
+
setTime
|
|
599
|
+
}),
|
|
600
|
+
[start, pause, resume, switchPlayer, reset, addTime, setTime]
|
|
601
|
+
);
|
|
602
|
+
const periodInfo = useMemo(() => {
|
|
603
|
+
if (!state.periodState) {
|
|
604
|
+
return {
|
|
605
|
+
currentPeriodIndex: { white: 0, black: 0 },
|
|
606
|
+
totalPeriods: 1,
|
|
607
|
+
currentPeriod: {
|
|
608
|
+
white: {
|
|
609
|
+
baseTime: state.config.baseTime / 1e3,
|
|
610
|
+
increment: state.config.increment / 1e3,
|
|
611
|
+
delay: state.config.delay / 1e3
|
|
612
|
+
},
|
|
613
|
+
black: {
|
|
614
|
+
baseTime: state.config.baseTime / 1e3,
|
|
615
|
+
increment: state.config.increment / 1e3,
|
|
616
|
+
delay: state.config.delay / 1e3
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
periodMoves: { white: 0, black: 0 }
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const periods = state.periodState.periods;
|
|
623
|
+
const getCurrentPeriod = (periodIndex) => {
|
|
624
|
+
const period = periodIndex >= 0 && periodIndex < periods.length ? periods[periodIndex] : periods[0];
|
|
625
|
+
return {
|
|
626
|
+
...period,
|
|
627
|
+
baseTime: period.baseTime / 1e3,
|
|
628
|
+
increment: period.increment !== void 0 ? period.increment / 1e3 : void 0,
|
|
629
|
+
delay: period.delay !== void 0 ? period.delay / 1e3 : void 0
|
|
630
|
+
};
|
|
631
|
+
};
|
|
632
|
+
return {
|
|
633
|
+
currentPeriodIndex: state.periodState.periodIndex,
|
|
634
|
+
totalPeriods: periods.length,
|
|
635
|
+
currentPeriod: {
|
|
636
|
+
white: getCurrentPeriod(state.periodState.periodIndex.white),
|
|
637
|
+
black: getCurrentPeriod(state.periodState.periodIndex.black)
|
|
638
|
+
},
|
|
639
|
+
periodMoves: state.periodState.periodMoves
|
|
640
|
+
};
|
|
641
|
+
}, [state.periodState, state.config]);
|
|
642
|
+
return {
|
|
643
|
+
times: displayTimes,
|
|
644
|
+
initialTimes: state.initialTimes,
|
|
645
|
+
status: state.status,
|
|
646
|
+
activePlayer: state.activePlayer,
|
|
647
|
+
timeout: state.timeout,
|
|
648
|
+
timingMethod: state.config.timingMethod,
|
|
649
|
+
info,
|
|
650
|
+
...periodInfo,
|
|
651
|
+
methods
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function useOptionalChessClock(options) {
|
|
655
|
+
const result = useChessClock(options ?? DISABLED_CLOCK_CONFIG);
|
|
656
|
+
return options === void 0 ? null : result;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/components/ChessClock/parts/Root.tsx
|
|
660
|
+
var Root = ({
|
|
661
|
+
timeControl,
|
|
662
|
+
children
|
|
663
|
+
}) => {
|
|
664
|
+
const clockState = useChessClock(timeControl);
|
|
665
|
+
return /* @__PURE__ */ React.createElement(ChessClockContext.Provider, { value: clockState }, children);
|
|
666
|
+
};
|
|
667
|
+
Root.displayName = "ChessClock.Root";
|
|
668
|
+
|
|
669
|
+
// src/components/ChessClock/parts/Display.tsx
|
|
670
|
+
import React2 from "react";
|
|
671
|
+
var Display = React2.forwardRef(
|
|
672
|
+
({
|
|
673
|
+
color,
|
|
674
|
+
format = "auto",
|
|
675
|
+
formatTime: customFormatTime,
|
|
676
|
+
className,
|
|
677
|
+
style,
|
|
678
|
+
...rest
|
|
679
|
+
}, ref) => {
|
|
680
|
+
const { times, activePlayer, status, timeout } = useChessClockContext();
|
|
681
|
+
const time = times[color];
|
|
682
|
+
const isActive = activePlayer === color;
|
|
683
|
+
const hasTimeout = timeout === color;
|
|
684
|
+
const isPaused = status === "paused";
|
|
685
|
+
const formattedTime = customFormatTime ? customFormatTime(time) : formatClockTime(time, format);
|
|
686
|
+
return /* @__PURE__ */ React2.createElement(
|
|
687
|
+
"div",
|
|
688
|
+
{
|
|
689
|
+
ref,
|
|
690
|
+
className,
|
|
691
|
+
style,
|
|
692
|
+
"data-clock-color": color,
|
|
693
|
+
"data-clock-active": isActive ? "true" : "false",
|
|
694
|
+
"data-clock-paused": isPaused ? "true" : "false",
|
|
695
|
+
"data-clock-timeout": hasTimeout ? "true" : "false",
|
|
696
|
+
"data-clock-status": status,
|
|
697
|
+
...rest
|
|
698
|
+
},
|
|
699
|
+
formattedTime
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
);
|
|
703
|
+
Display.displayName = "ChessClock.Display";
|
|
704
|
+
|
|
705
|
+
// src/components/ChessClock/parts/Switch.tsx
|
|
706
|
+
import React3 from "react";
|
|
707
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
708
|
+
var Switch = React3.forwardRef(
|
|
709
|
+
({
|
|
710
|
+
asChild = false,
|
|
711
|
+
children,
|
|
712
|
+
onClick,
|
|
713
|
+
disabled,
|
|
714
|
+
className,
|
|
715
|
+
style,
|
|
716
|
+
type,
|
|
717
|
+
...rest
|
|
718
|
+
}, ref) => {
|
|
719
|
+
const { methods, status } = useChessClockContext();
|
|
720
|
+
const isDisabled = disabled || status === "finished" || status === "idle";
|
|
721
|
+
const handleClick = React3.useCallback(
|
|
722
|
+
(e) => {
|
|
723
|
+
methods.switch();
|
|
724
|
+
onClick == null ? void 0 : onClick(e);
|
|
725
|
+
},
|
|
726
|
+
[methods, onClick]
|
|
727
|
+
);
|
|
728
|
+
return asChild ? /* @__PURE__ */ React3.createElement(
|
|
729
|
+
Slot,
|
|
730
|
+
{
|
|
731
|
+
ref,
|
|
732
|
+
onClick: handleClick,
|
|
733
|
+
className,
|
|
734
|
+
style,
|
|
735
|
+
...{ ...rest, disabled: isDisabled }
|
|
736
|
+
},
|
|
737
|
+
children
|
|
738
|
+
) : /* @__PURE__ */ React3.createElement(
|
|
739
|
+
"button",
|
|
740
|
+
{
|
|
741
|
+
ref,
|
|
742
|
+
type: type || "button",
|
|
743
|
+
className,
|
|
744
|
+
style,
|
|
745
|
+
onClick: handleClick,
|
|
746
|
+
disabled: isDisabled,
|
|
747
|
+
...rest
|
|
748
|
+
},
|
|
749
|
+
children
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
);
|
|
753
|
+
Switch.displayName = "ChessClock.Switch";
|
|
754
|
+
|
|
755
|
+
// src/components/ChessClock/parts/PlayPause.tsx
|
|
756
|
+
import React4 from "react";
|
|
757
|
+
import { Slot as Slot2 } from "@radix-ui/react-slot";
|
|
758
|
+
var DEFAULT_CONTENT = {
|
|
759
|
+
start: "Start",
|
|
760
|
+
pause: "Pause",
|
|
761
|
+
resume: "Resume",
|
|
762
|
+
delayed: "Start",
|
|
763
|
+
finished: "Game Over"
|
|
764
|
+
};
|
|
765
|
+
var resolveContent = (isFinished, isDelayed, shouldShowStart, isPaused, isRunning, customContent) => {
|
|
766
|
+
if (isFinished) {
|
|
767
|
+
return customContent.finishedContent ?? DEFAULT_CONTENT.finished;
|
|
768
|
+
}
|
|
769
|
+
if (isDelayed) {
|
|
770
|
+
return customContent.delayedContent ?? DEFAULT_CONTENT.delayed;
|
|
771
|
+
}
|
|
772
|
+
if (shouldShowStart) {
|
|
773
|
+
return customContent.startContent ?? DEFAULT_CONTENT.start;
|
|
774
|
+
}
|
|
775
|
+
if (isPaused) {
|
|
776
|
+
return customContent.resumeContent ?? DEFAULT_CONTENT.resume;
|
|
777
|
+
}
|
|
778
|
+
if (isRunning) {
|
|
779
|
+
return customContent.pauseContent ?? DEFAULT_CONTENT.pause;
|
|
780
|
+
}
|
|
781
|
+
return DEFAULT_CONTENT.start;
|
|
782
|
+
};
|
|
783
|
+
var PlayPause = React4.forwardRef(
|
|
784
|
+
({
|
|
785
|
+
asChild = false,
|
|
786
|
+
startContent,
|
|
787
|
+
pauseContent,
|
|
788
|
+
resumeContent,
|
|
789
|
+
delayedContent,
|
|
790
|
+
finishedContent,
|
|
791
|
+
children,
|
|
792
|
+
onClick,
|
|
793
|
+
disabled,
|
|
794
|
+
className,
|
|
795
|
+
style,
|
|
796
|
+
type,
|
|
797
|
+
...rest
|
|
798
|
+
}, ref) => {
|
|
799
|
+
const { status, methods } = useChessClockContext();
|
|
800
|
+
const isIdle = status === "idle";
|
|
801
|
+
const isDelayed = status === "delayed";
|
|
802
|
+
const isPaused = status === "paused";
|
|
803
|
+
const isRunning = status === "running";
|
|
804
|
+
const isFinished = status === "finished";
|
|
805
|
+
const shouldShowStart = isIdle || isDelayed;
|
|
806
|
+
const isDisabled = disabled || isFinished || isDelayed;
|
|
807
|
+
const handleClick = React4.useCallback(
|
|
808
|
+
(e) => {
|
|
809
|
+
if (shouldShowStart) {
|
|
810
|
+
methods.start();
|
|
811
|
+
} else if (isPaused) {
|
|
812
|
+
methods.resume();
|
|
813
|
+
} else if (isRunning) {
|
|
814
|
+
methods.pause();
|
|
815
|
+
}
|
|
816
|
+
onClick == null ? void 0 : onClick(e);
|
|
817
|
+
},
|
|
818
|
+
[shouldShowStart, isPaused, isRunning, methods, onClick]
|
|
819
|
+
);
|
|
820
|
+
const content = children ?? resolveContent(
|
|
821
|
+
isFinished,
|
|
822
|
+
isDelayed,
|
|
823
|
+
shouldShowStart,
|
|
824
|
+
isPaused,
|
|
825
|
+
isRunning,
|
|
826
|
+
{
|
|
827
|
+
startContent,
|
|
828
|
+
pauseContent,
|
|
829
|
+
resumeContent,
|
|
830
|
+
delayedContent,
|
|
831
|
+
finishedContent
|
|
832
|
+
}
|
|
833
|
+
);
|
|
834
|
+
return asChild ? /* @__PURE__ */ React4.createElement(
|
|
835
|
+
Slot2,
|
|
836
|
+
{
|
|
837
|
+
ref,
|
|
838
|
+
onClick: handleClick,
|
|
839
|
+
className,
|
|
840
|
+
style,
|
|
841
|
+
...{ ...rest, disabled: isDisabled }
|
|
842
|
+
},
|
|
843
|
+
content
|
|
844
|
+
) : /* @__PURE__ */ React4.createElement(
|
|
845
|
+
"button",
|
|
846
|
+
{
|
|
847
|
+
ref,
|
|
848
|
+
type: type || "button",
|
|
849
|
+
className,
|
|
850
|
+
style,
|
|
851
|
+
onClick: handleClick,
|
|
852
|
+
disabled: isDisabled,
|
|
853
|
+
...rest
|
|
854
|
+
},
|
|
855
|
+
content
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
);
|
|
859
|
+
PlayPause.displayName = "ChessClock.PlayPause";
|
|
860
|
+
|
|
861
|
+
// src/components/ChessClock/parts/Reset.tsx
|
|
862
|
+
import React5 from "react";
|
|
863
|
+
import { Slot as Slot3 } from "@radix-ui/react-slot";
|
|
864
|
+
var Reset = React5.forwardRef(
|
|
865
|
+
({
|
|
866
|
+
asChild = false,
|
|
867
|
+
timeControl,
|
|
868
|
+
children,
|
|
869
|
+
onClick,
|
|
870
|
+
disabled,
|
|
871
|
+
className,
|
|
872
|
+
style,
|
|
873
|
+
type,
|
|
874
|
+
...rest
|
|
875
|
+
}, ref) => {
|
|
876
|
+
const { methods, status } = useChessClockContext();
|
|
877
|
+
const isDisabled = disabled || status === "idle";
|
|
878
|
+
const handleClick = React5.useCallback(
|
|
879
|
+
(e) => {
|
|
880
|
+
methods.reset(timeControl);
|
|
881
|
+
onClick == null ? void 0 : onClick(e);
|
|
882
|
+
},
|
|
883
|
+
[methods, timeControl, onClick]
|
|
884
|
+
);
|
|
885
|
+
return asChild ? /* @__PURE__ */ React5.createElement(
|
|
886
|
+
Slot3,
|
|
887
|
+
{
|
|
888
|
+
ref,
|
|
889
|
+
onClick: handleClick,
|
|
890
|
+
className,
|
|
891
|
+
style,
|
|
892
|
+
...{ ...rest, disabled: isDisabled }
|
|
893
|
+
},
|
|
894
|
+
children
|
|
895
|
+
) : /* @__PURE__ */ React5.createElement(
|
|
896
|
+
"button",
|
|
897
|
+
{
|
|
898
|
+
ref,
|
|
899
|
+
type: type || "button",
|
|
900
|
+
className,
|
|
901
|
+
style,
|
|
902
|
+
onClick: handleClick,
|
|
903
|
+
disabled: isDisabled,
|
|
904
|
+
...rest
|
|
905
|
+
},
|
|
906
|
+
children
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
);
|
|
910
|
+
Reset.displayName = "ChessClock.Reset";
|
|
911
|
+
|
|
912
|
+
// src/components/ChessClock/index.ts
|
|
913
|
+
var ChessClock = {
|
|
914
|
+
Root,
|
|
915
|
+
Display,
|
|
916
|
+
Switch,
|
|
917
|
+
PlayPause,
|
|
918
|
+
Reset
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// src/utils/presets.ts
|
|
922
|
+
var presets = {
|
|
923
|
+
// Bullet (< 3 minutes)
|
|
924
|
+
bullet1_0: { baseTime: 60, increment: 0 },
|
|
925
|
+
bullet1_1: { baseTime: 60, increment: 1 },
|
|
926
|
+
bullet2_1: { baseTime: 120, increment: 1 },
|
|
927
|
+
// Blitz (3-10 minutes)
|
|
928
|
+
blitz3_0: { baseTime: 180, increment: 0 },
|
|
929
|
+
blitz3_2: { baseTime: 180, increment: 2 },
|
|
930
|
+
blitz5_0: { baseTime: 300, increment: 0 },
|
|
931
|
+
blitz5_3: { baseTime: 300, increment: 3 },
|
|
932
|
+
// Rapid (10-60 minutes)
|
|
933
|
+
rapid10_0: { baseTime: 600, increment: 0 },
|
|
934
|
+
rapid10_5: { baseTime: 600, increment: 5 },
|
|
935
|
+
rapid15_10: { baseTime: 900, increment: 10 },
|
|
936
|
+
// Classical (≥ 60 minutes)
|
|
937
|
+
classical30_0: { baseTime: 1800, increment: 0 },
|
|
938
|
+
classical90_30: { baseTime: 5400, increment: 30 },
|
|
939
|
+
// Tournament (multi-period)
|
|
940
|
+
fideClassical: [
|
|
941
|
+
{ baseTime: 5400, increment: 30, moves: 40 },
|
|
942
|
+
{ baseTime: 1800, increment: 30, moves: 20 },
|
|
943
|
+
{ baseTime: 900, increment: 30 }
|
|
944
|
+
],
|
|
945
|
+
uscfClassical: [
|
|
946
|
+
{ baseTime: 7200, moves: 40 },
|
|
947
|
+
{ baseTime: 3600, moves: 20 },
|
|
948
|
+
{ baseTime: 1800 }
|
|
949
|
+
]
|
|
950
|
+
};
|
|
951
|
+
export {
|
|
952
|
+
ChessClock,
|
|
953
|
+
ChessClockContext,
|
|
954
|
+
Display,
|
|
955
|
+
PlayPause,
|
|
956
|
+
Reset,
|
|
957
|
+
Switch,
|
|
958
|
+
formatClockTime,
|
|
959
|
+
getInitialTimes,
|
|
960
|
+
normalizeTimeControl,
|
|
961
|
+
parseMultiPeriodTimeControl,
|
|
962
|
+
parseTimeControlConfig,
|
|
963
|
+
parseTimeControlString,
|
|
964
|
+
presets,
|
|
965
|
+
useChessClock,
|
|
966
|
+
useChessClockContext,
|
|
967
|
+
useOptionalChessClock
|
|
968
|
+
};
|
|
969
|
+
//# sourceMappingURL=index.js.map
|