@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +697 -0
  3. package/dist/index.cjs +1014 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +528 -0
  6. package/dist/index.d.ts +528 -0
  7. package/dist/index.js +969 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +63 -0
  10. package/src/components/ChessClock/ChessClock.stories.tsx +782 -0
  11. package/src/components/ChessClock/index.ts +44 -0
  12. package/src/components/ChessClock/parts/Display.tsx +69 -0
  13. package/src/components/ChessClock/parts/PlayPause.tsx +190 -0
  14. package/src/components/ChessClock/parts/Reset.tsx +90 -0
  15. package/src/components/ChessClock/parts/Root.tsx +37 -0
  16. package/src/components/ChessClock/parts/Switch.tsx +84 -0
  17. package/src/components/ChessClock/parts/__tests__/Display.test.tsx +149 -0
  18. package/src/components/ChessClock/parts/__tests__/PlayPause.test.tsx +411 -0
  19. package/src/components/ChessClock/parts/__tests__/Reset.test.tsx +160 -0
  20. package/src/components/ChessClock/parts/__tests__/Root.test.tsx +49 -0
  21. package/src/components/ChessClock/parts/__tests__/Switch.test.tsx +204 -0
  22. package/src/hooks/__tests__/clockReducer.test.ts +985 -0
  23. package/src/hooks/__tests__/useChessClock.test.tsx +1080 -0
  24. package/src/hooks/clockReducer.ts +379 -0
  25. package/src/hooks/useChessClock.ts +406 -0
  26. package/src/hooks/useChessClockContext.ts +35 -0
  27. package/src/index.ts +65 -0
  28. package/src/types.ts +217 -0
  29. package/src/utils/__tests__/calculateSwitchTime.test.ts +150 -0
  30. package/src/utils/__tests__/formatTime.test.ts +83 -0
  31. package/src/utils/__tests__/timeControl.test.ts +414 -0
  32. package/src/utils/__tests__/timingMethods.test.ts +170 -0
  33. package/src/utils/calculateSwitchTime.ts +37 -0
  34. package/src/utils/formatTime.ts +59 -0
  35. package/src/utils/presets.ts +47 -0
  36. package/src/utils/timeControl.ts +205 -0
  37. package/src/utils/timingMethods.ts +103 -0
@@ -0,0 +1,528 @@
1
+ import * as React from 'react';
2
+ import React__default, { ReactNode } from 'react';
3
+
4
+ /**
5
+ * String notation for time controls
6
+ * Examples: "5+3" (5 minutes, 3 second increment), "10" (10 minutes, no increment)
7
+ */
8
+ type TimeControlString = `${number}+${number}` | `${number}`;
9
+ /**
10
+ * Single period time control configuration
11
+ * Used for standalone games or the final period of tournament controls
12
+ */
13
+ interface TimeControl {
14
+ /** Base time in seconds */
15
+ baseTime: number;
16
+ /** Increment in seconds (default: 0) */
17
+ increment?: number;
18
+ /** Delay in seconds (for delay timing methods) */
19
+ delay?: number;
20
+ }
21
+ /**
22
+ * A single time control period for tournament play
23
+ */
24
+ interface TimeControlPhase extends TimeControl {
25
+ /** Moves required in this period (undefined = sudden death period) */
26
+ moves?: number;
27
+ }
28
+ /**
29
+ * Time control input - accepts string notation, object configuration, or multi-period array
30
+ */
31
+ type TimeControlInput = TimeControlString | TimeControl | TimeControlPhase[];
32
+ /**
33
+ * Clock timing method
34
+ * - "fischer": Standard increment - adds time after each move
35
+ * - "delay": Simple delay - countdown waits before decrementing
36
+ * - "bronstein": Bronstein delay - adds back actual time used up to delay amount
37
+ */
38
+ type TimingMethod = "fischer" | "delay" | "bronstein";
39
+ /**
40
+ * Clock start behavior
41
+ * - "delayed": Clock starts after Black's first move (Lichess-style)
42
+ * - "immediate": White's clock starts immediately (Chess.com-style)
43
+ * - "manual": Clock starts only when user explicitly calls start()
44
+ */
45
+ type ClockStartMode = "delayed" | "immediate" | "manual";
46
+ /**
47
+ * Clock status
48
+ */
49
+ type ClockStatus = "idle" | "delayed" | "running" | "paused" | "finished";
50
+ /**
51
+ * Player color
52
+ */
53
+ type ClockColor = "white" | "black";
54
+ /**
55
+ * Time control configuration
56
+ */
57
+ interface TimeControlConfig {
58
+ /** Time specification (required) */
59
+ time: TimeControlInput;
60
+ /** Override starting time for white (seconds) - for time odds */
61
+ whiteTime?: number;
62
+ /** Override starting time for black (seconds) - for time odds */
63
+ blackTime?: number;
64
+ /** Timing method (default: 'fischer') */
65
+ timingMethod?: TimingMethod;
66
+ /** Clock start behavior (default: 'delayed') */
67
+ clockStart?: ClockStartMode;
68
+ /** Callback when a player's time runs out */
69
+ onTimeout?: (loser: ClockColor) => void;
70
+ /** Callback when active player switches */
71
+ onSwitch?: (activePlayer: ClockColor) => void;
72
+ /** Callback on each time update */
73
+ onTimeUpdate?: (times: {
74
+ white: number;
75
+ black: number;
76
+ }) => void;
77
+ }
78
+ /**
79
+ * Period tracking state (only used for multi-period controls)
80
+ */
81
+ interface PeriodState {
82
+ /** Current period index for each player (0-based) */
83
+ periodIndex: {
84
+ white: number;
85
+ black: number;
86
+ };
87
+ /** Moves made in current period by each player */
88
+ periodMoves: {
89
+ white: number;
90
+ black: number;
91
+ };
92
+ /** All periods for this time control */
93
+ periods: TimeControlPhase[];
94
+ }
95
+ /**
96
+ * Normalized time control with all times converted to milliseconds
97
+ */
98
+ interface NormalizedTimeControl {
99
+ baseTime: number;
100
+ increment: number;
101
+ delay: number;
102
+ timingMethod: TimingMethod;
103
+ clockStart: ClockStartMode;
104
+ whiteTimeOverride?: number;
105
+ blackTimeOverride?: number;
106
+ /** Multi-period configuration (present only for multi-period time controls) */
107
+ periods?: TimeControlPhase[];
108
+ }
109
+ /**
110
+ * Clock times state (used for both current and initial times)
111
+ */
112
+ interface ClockTimes {
113
+ white: number;
114
+ black: number;
115
+ }
116
+ /**
117
+ * Computed clock info
118
+ */
119
+ interface ClockInfo {
120
+ isRunning: boolean;
121
+ isPaused: boolean;
122
+ isFinished: boolean;
123
+ isWhiteActive: boolean;
124
+ isBlackActive: boolean;
125
+ hasTimeout: boolean;
126
+ hasTimeOdds: boolean;
127
+ }
128
+ /**
129
+ * Clock methods
130
+ */
131
+ interface ClockMethods {
132
+ start: () => void;
133
+ pause: () => void;
134
+ resume: () => void;
135
+ switch: () => void;
136
+ reset: (timeControl?: TimeControlInput) => void;
137
+ addTime: (player: ClockColor, milliseconds: number) => void;
138
+ setTime: (player: ClockColor, milliseconds: number) => void;
139
+ }
140
+ /**
141
+ * Complete clock state return type
142
+ */
143
+ interface UseChessClockReturn {
144
+ times: ClockTimes;
145
+ initialTimes: ClockTimes;
146
+ status: ClockStatus;
147
+ activePlayer: ClockColor | null;
148
+ timeout: ClockColor | null;
149
+ timingMethod: TimingMethod;
150
+ info: ClockInfo;
151
+ currentPeriodIndex: {
152
+ white: number;
153
+ black: number;
154
+ };
155
+ totalPeriods: number;
156
+ currentPeriod: {
157
+ white: TimeControlPhase;
158
+ black: TimeControlPhase;
159
+ };
160
+ periodMoves: {
161
+ white: number;
162
+ black: number;
163
+ };
164
+ methods: ClockMethods;
165
+ }
166
+ /**
167
+ * Time format options
168
+ */
169
+ type TimeFormat = "auto" | "mm:ss" | "ss.d" | "hh:mm:ss";
170
+ /**
171
+ * Timing method result
172
+ */
173
+ interface TimingMethodResult {
174
+ newTime: number;
175
+ delayRemaining: number;
176
+ }
177
+
178
+ interface ChessClockResetProps extends Omit<React__default.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
179
+ asChild?: boolean;
180
+ children?: ReactNode;
181
+ onClick?: React__default.MouseEventHandler<HTMLElement>;
182
+ timeControl?: TimeControlInput;
183
+ }
184
+ /**
185
+ * ChessClock.Reset - Button to reset the clock
186
+ *
187
+ * Supports the asChild pattern and optional new time control on reset.
188
+ *
189
+ * @example
190
+ * ```tsx
191
+ * <ChessClock.Reset>Reset</ChessClock.Reset>
192
+ *
193
+ * // Reset with new time control
194
+ * <ChessClock.Reset timeControl="10+5">Change to 10+5</ChessClock.Reset>
195
+ *
196
+ * // As child
197
+ * <ChessClock.Reset asChild>
198
+ * <div className="custom-reset">Reset</div>
199
+ * </ChessClock.Reset>
200
+ * ```
201
+ */
202
+ declare const Reset: React__default.ForwardRefExoticComponent<ChessClockResetProps & {
203
+ children?: ReactNode | undefined;
204
+ } & React__default.RefAttributes<HTMLElement>>;
205
+
206
+ interface ChessClockPlayPauseProps extends Omit<React__default.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
207
+ asChild?: boolean;
208
+ children?: ReactNode;
209
+ onClick?: React__default.MouseEventHandler<HTMLElement>;
210
+ /** Content shown when clock is idle (not yet started) - clicking will start the clock */
211
+ startContent?: ReactNode;
212
+ /** Content shown when clock is running - clicking will pause */
213
+ pauseContent?: ReactNode;
214
+ /** Content shown when clock is paused - clicking will resume */
215
+ resumeContent?: ReactNode;
216
+ /** Content shown when clock is in delayed mode - clicking will start the clock immediately */
217
+ delayedContent?: ReactNode;
218
+ /** Content shown when clock is finished - button is disabled but shows this content */
219
+ finishedContent?: ReactNode;
220
+ }
221
+ /**
222
+ * ChessClock.PlayPause - Button to start, pause, and resume the clock
223
+ *
224
+ * Supports the asChild pattern and conditional content based on clock state.
225
+ * When no children or custom content is provided, sensible defaults are used for each state.
226
+ *
227
+ * @example
228
+ * ```tsx
229
+ * // With children (backward compatible)
230
+ * <ChessClock.PlayPause>
231
+ * <span>Toggle</span>
232
+ * </ChessClock.PlayPause>
233
+ *
234
+ * // No props - uses defaults for all states
235
+ * <ChessClock.PlayPause />
236
+ * // Shows: "Start" → "Pause" → "Resume" → "Game Over"
237
+ *
238
+ * // Override just one state, others use defaults
239
+ * <ChessClock.PlayPause pauseContent="⏸️ Stop" />
240
+ * // Shows: "Start" → "⏸️ Stop" → "Resume" → "Game Over"
241
+ *
242
+ * // As child
243
+ * <ChessClock.PlayPause asChild>
244
+ * <div className="custom-button">Toggle</div>
245
+ * </ChessClock.PlayPause>
246
+ * ```
247
+ */
248
+ declare const PlayPause: React__default.ForwardRefExoticComponent<ChessClockPlayPauseProps & {
249
+ children?: ReactNode | undefined;
250
+ } & React__default.RefAttributes<HTMLElement>>;
251
+
252
+ interface ChessClockControlProps extends Omit<React__default.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
253
+ asChild?: boolean;
254
+ children?: ReactNode;
255
+ onClick?: React__default.MouseEventHandler<HTMLElement>;
256
+ }
257
+ /**
258
+ * ChessClock.Switch - Button to manually switch the active clock
259
+ *
260
+ * Supports the asChild pattern for custom rendering.
261
+ *
262
+ * @example
263
+ * ```tsx
264
+ * <ChessClock.Switch>Switch Clock</ChessClock.Switch>
265
+ *
266
+ * // As child
267
+ * <ChessClock.Switch asChild>
268
+ * <div className="custom-switch">Switch</div>
269
+ * </ChessClock.Switch>
270
+ * ```
271
+ */
272
+ declare const Switch: React__default.ForwardRefExoticComponent<ChessClockControlProps & {
273
+ children?: ReactNode | undefined;
274
+ } & React__default.RefAttributes<HTMLElement>>;
275
+
276
+ interface ChessClockDisplayProps extends React__default.HTMLAttributes<HTMLDivElement> {
277
+ color: ClockColor;
278
+ format?: "auto" | "mm:ss" | "ss.d" | "hh:mm:ss";
279
+ formatTime?: (milliseconds: number) => string;
280
+ }
281
+ /**
282
+ * ChessClock.Display - Displays the current time for a player
283
+ *
284
+ * Renders an unstyled div with data attributes for custom styling.
285
+ *
286
+ * @example
287
+ * ```tsx
288
+ * <ChessClock.Display color="white" format="auto" />
289
+ * <ChessClock.Display color="black" format="ss.d" />
290
+ * <ChessClock.Display
291
+ * color="white"
292
+ * formatTime={(ms) => `${Math.ceil(ms / 1000)}s`}
293
+ * />
294
+ * ```
295
+ */
296
+ declare const Display: React__default.ForwardRefExoticComponent<ChessClockDisplayProps & React__default.RefAttributes<HTMLDivElement>>;
297
+
298
+ interface ChessClockRootProps {
299
+ timeControl: TimeControlConfig;
300
+ children: ReactNode;
301
+ }
302
+
303
+ /**
304
+ * ChessClock compound components
305
+ *
306
+ * @example
307
+ * ```tsx
308
+ * import { ChessClock } from "@react-chess-tools/react-chess-clock";
309
+ *
310
+ * function ClockApp() {
311
+ * return (
312
+ * <ChessClock.Root timeControl={{ time: "5+3" }}>
313
+ * <ChessClock.Display color="black" />
314
+ * <ChessClock.Display color="white" />
315
+ * <ChessClock.Switch>Switch</ChessClock.Switch>
316
+ * <ChessClock.PlayPause
317
+ * startContent="Start"
318
+ * pauseContent="Pause"
319
+ * resumeContent="Resume"
320
+ * />
321
+ * <ChessClock.Reset>Reset</ChessClock.Reset>
322
+ * </ChessClock.Root>
323
+ * );
324
+ * }
325
+ * ```
326
+ */
327
+ declare const ChessClock: {
328
+ Root: React.FC<React.PropsWithChildren<ChessClockRootProps>>;
329
+ Display: React.ForwardRefExoticComponent<ChessClockDisplayProps & React.RefAttributes<HTMLDivElement>>;
330
+ Switch: React.ForwardRefExoticComponent<ChessClockControlProps & {
331
+ children?: React.ReactNode | undefined;
332
+ } & React.RefAttributes<HTMLElement>>;
333
+ PlayPause: React.ForwardRefExoticComponent<ChessClockPlayPauseProps & {
334
+ children?: React.ReactNode | undefined;
335
+ } & React.RefAttributes<HTMLElement>>;
336
+ Reset: React.ForwardRefExoticComponent<ChessClockResetProps & {
337
+ children?: React.ReactNode | undefined;
338
+ } & React.RefAttributes<HTMLElement>>;
339
+ };
340
+
341
+ /**
342
+ * Format milliseconds into a display string
343
+ * @param milliseconds - Time in milliseconds
344
+ * @param format - Format type
345
+ * @returns Formatted time string
346
+ */
347
+ declare function formatClockTime(milliseconds: number, format?: TimeFormat): string;
348
+
349
+ /**
350
+ * Main hook for chess clock state management.
351
+ * Provides timing functionality for chess games with various timing methods.
352
+ *
353
+ * For server-authoritative clocks, use `methods.setTime()` to sync server times.
354
+ * The clock will restart its display interpolation from the new value on each call.
355
+ *
356
+ * **Auto-reset behavior:** The clock automatically resets when time-relevant
357
+ * options change (`time`, `timingMethod`, `clockStart`, `whiteTime`, `blackTime`).
358
+ * Callbacks (`onTimeout`, `onSwitch`, `onTimeUpdate`) do not trigger a reset
359
+ * and can be changed without affecting clock state.
360
+ *
361
+ * @param options - Clock configuration options
362
+ * @returns Clock state, info, and methods
363
+ */
364
+ declare function useChessClock(options: TimeControlConfig): UseChessClockReturn;
365
+ /**
366
+ * Optional chess clock hook for cases where clock may not be needed
367
+ * Maintains hook order by always calling the implementation internally
368
+ *
369
+ * @param options - Clock configuration options, or undefined to disable
370
+ * @returns Clock state, info, and methods, or null if disabled
371
+ */
372
+ declare function useOptionalChessClock(options?: TimeControlConfig): ReturnType<typeof useChessClock> | null;
373
+
374
+ /**
375
+ * Context for chess clock state
376
+ * Provided by ChessClock.Root and consumed by child components
377
+ */
378
+ declare const ChessClockContext: React.Context<UseChessClockReturn | null>;
379
+ /**
380
+ * Hook to access chess clock context from child components
381
+ * Must be used within a ChessClock.Root provider
382
+ *
383
+ * @throws Error if used outside of ChessClock.Root
384
+ * @returns Chess clock state and methods
385
+ */
386
+ declare function useChessClockContext(): UseChessClockReturn;
387
+
388
+ /**
389
+ * Parse a time control string into a TimeControl object
390
+ * @param input - Time control string (e.g., "5+3", "10")
391
+ * @returns Parsed time control with baseTime in seconds
392
+ * @throws Error if input is invalid
393
+ */
394
+ declare function parseTimeControlString(input: TimeControlString): TimeControl;
395
+ /**
396
+ * Parse a multi-period time control string into an array of TimeControlPhase
397
+ * Format: "40/90+30,sd/30+30" or "40/120+30,20/60+30,g/15+30"
398
+ *
399
+ * Period format: [moves/]time[+increment] or [SD|G/]time[+increment]
400
+ * - moves: Number of moves required for this period (e.g., "40" in "40/90+30")
401
+ * - time: Time in minutes for this period (e.g., "90" in "40/90+30")
402
+ * - increment: Increment in seconds after each move (e.g., "30" in "40/90+30")
403
+ * - SD or G prefix: Sudden death period (no moves requirement, overrides any moves prefix)
404
+ *
405
+ * @param input - Multi-period time control string
406
+ * @returns Array of TimeControlPhase with times in seconds
407
+ * @throws Error if input is invalid
408
+ *
409
+ * @example
410
+ * ```ts
411
+ * parseMultiPeriodTimeControl("40/90+30,sd/30+30")
412
+ * // Returns: [
413
+ * // { baseTime: 5400, increment: 30, moves: 40 },
414
+ * // { baseTime: 1800, increment: 30 }
415
+ * // ]
416
+ * ```
417
+ */
418
+ declare function parseMultiPeriodTimeControl(input: string): TimeControlPhase[];
419
+ /**
420
+ * Normalize any time control input into a NormalizedTimeControl
421
+ * @param input - Time control string, object, or array of periods
422
+ * @param timingMethod - Optional timing method (defaults to "fischer")
423
+ * @param clockStart - Optional clock start mode (defaults to "delayed")
424
+ * @returns Normalized time control with times in milliseconds
425
+ */
426
+ declare function normalizeTimeControl(input: TimeControlInput, timingMethod: NormalizedTimeControl["timingMethod"], clockStart: NormalizedTimeControl["clockStart"]): NormalizedTimeControl;
427
+ /**
428
+ * Parse complete TimeControlConfig into NormalizedTimeControl
429
+ * @param config - Time control configuration
430
+ * @returns Fully normalized time control
431
+ */
432
+ declare function parseTimeControlConfig(config: TimeControlConfig): NormalizedTimeControl;
433
+ /**
434
+ * Get initial times from a normalized time control
435
+ * @param config - Normalized time control
436
+ * @returns Initial times for white and black in milliseconds
437
+ */
438
+ declare function getInitialTimes(config: NormalizedTimeControl): {
439
+ white: number;
440
+ black: number;
441
+ };
442
+
443
+ /**
444
+ * Preset time controls for common chess formats
445
+ *
446
+ * @example
447
+ * ```tsx
448
+ * import { presets } from "@react-chess-tools/react-chess-clock";
449
+ *
450
+ * <ChessClock.Root timeControl={{ time: presets.blitz5_3 }}>
451
+ * <ChessClock.Display color="white" />
452
+ * <ChessClock.Display color="black" />
453
+ * </ChessClock.Root>
454
+ * ```
455
+ */
456
+ declare const presets: {
457
+ readonly bullet1_0: {
458
+ readonly baseTime: 60;
459
+ readonly increment: 0;
460
+ };
461
+ readonly bullet1_1: {
462
+ readonly baseTime: 60;
463
+ readonly increment: 1;
464
+ };
465
+ readonly bullet2_1: {
466
+ readonly baseTime: 120;
467
+ readonly increment: 1;
468
+ };
469
+ readonly blitz3_0: {
470
+ readonly baseTime: 180;
471
+ readonly increment: 0;
472
+ };
473
+ readonly blitz3_2: {
474
+ readonly baseTime: 180;
475
+ readonly increment: 2;
476
+ };
477
+ readonly blitz5_0: {
478
+ readonly baseTime: 300;
479
+ readonly increment: 0;
480
+ };
481
+ readonly blitz5_3: {
482
+ readonly baseTime: 300;
483
+ readonly increment: 3;
484
+ };
485
+ readonly rapid10_0: {
486
+ readonly baseTime: 600;
487
+ readonly increment: 0;
488
+ };
489
+ readonly rapid10_5: {
490
+ readonly baseTime: 600;
491
+ readonly increment: 5;
492
+ };
493
+ readonly rapid15_10: {
494
+ readonly baseTime: 900;
495
+ readonly increment: 10;
496
+ };
497
+ readonly classical30_0: {
498
+ readonly baseTime: 1800;
499
+ readonly increment: 0;
500
+ };
501
+ readonly classical90_30: {
502
+ readonly baseTime: 5400;
503
+ readonly increment: 30;
504
+ };
505
+ readonly fideClassical: readonly [{
506
+ readonly baseTime: 5400;
507
+ readonly increment: 30;
508
+ readonly moves: 40;
509
+ }, {
510
+ readonly baseTime: 1800;
511
+ readonly increment: 30;
512
+ readonly moves: 20;
513
+ }, {
514
+ readonly baseTime: 900;
515
+ readonly increment: 30;
516
+ }];
517
+ readonly uscfClassical: readonly [{
518
+ readonly baseTime: 7200;
519
+ readonly moves: 40;
520
+ }, {
521
+ readonly baseTime: 3600;
522
+ readonly moves: 20;
523
+ }, {
524
+ readonly baseTime: 1800;
525
+ }];
526
+ };
527
+
528
+ export { ChessClock, ChessClockContext, type ChessClockControlProps, type ChessClockDisplayProps, type ChessClockPlayPauseProps, type ChessClockResetProps, type ChessClockRootProps, type ClockColor, type ClockInfo, type ClockMethods, type ClockStartMode, type ClockStatus, type ClockTimes, Display, type NormalizedTimeControl, type PeriodState, PlayPause, Reset, Switch, type TimeControl, type TimeControlConfig, type TimeControlInput, type TimeControlPhase, type TimeControlString, type TimeFormat, type TimingMethod, type TimingMethodResult, type UseChessClockReturn, formatClockTime, getInitialTimes, normalizeTimeControl, parseMultiPeriodTimeControl, parseTimeControlConfig, parseTimeControlString, presets, useChessClock, useChessClockContext, useOptionalChessClock };