@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
package/src/types.ts ADDED
@@ -0,0 +1,217 @@
1
+ // ============================================================================
2
+ // Time Control Types
3
+ // ============================================================================
4
+
5
+ /**
6
+ * String notation for time controls
7
+ * Examples: "5+3" (5 minutes, 3 second increment), "10" (10 minutes, no increment)
8
+ */
9
+ export type TimeControlString = `${number}+${number}` | `${number}`;
10
+
11
+ /**
12
+ * Single period time control configuration
13
+ * Used for standalone games or the final period of tournament controls
14
+ */
15
+ export interface TimeControl {
16
+ /** Base time in seconds */
17
+ baseTime: number;
18
+ /** Increment in seconds (default: 0) */
19
+ increment?: number;
20
+ /** Delay in seconds (for delay timing methods) */
21
+ delay?: number;
22
+ }
23
+
24
+ /**
25
+ * A single time control period for tournament play
26
+ */
27
+ export interface TimeControlPhase extends TimeControl {
28
+ /** Moves required in this period (undefined = sudden death period) */
29
+ moves?: number;
30
+ }
31
+
32
+ /**
33
+ * Time control input - accepts string notation, object configuration, or multi-period array
34
+ */
35
+ export type TimeControlInput =
36
+ | TimeControlString
37
+ | TimeControl
38
+ | TimeControlPhase[];
39
+
40
+ /**
41
+ * Clock timing method
42
+ * - "fischer": Standard increment - adds time after each move
43
+ * - "delay": Simple delay - countdown waits before decrementing
44
+ * - "bronstein": Bronstein delay - adds back actual time used up to delay amount
45
+ */
46
+ export type TimingMethod = "fischer" | "delay" | "bronstein";
47
+
48
+ /**
49
+ * Clock start behavior
50
+ * - "delayed": Clock starts after Black's first move (Lichess-style)
51
+ * - "immediate": White's clock starts immediately (Chess.com-style)
52
+ * - "manual": Clock starts only when user explicitly calls start()
53
+ */
54
+ export type ClockStartMode = "delayed" | "immediate" | "manual";
55
+
56
+ /**
57
+ * Clock status
58
+ */
59
+ export type ClockStatus =
60
+ | "idle"
61
+ | "delayed"
62
+ | "running"
63
+ | "paused"
64
+ | "finished";
65
+
66
+ /**
67
+ * Player color
68
+ */
69
+ export type ClockColor = "white" | "black";
70
+
71
+ /**
72
+ * Time control configuration
73
+ */
74
+ export interface TimeControlConfig {
75
+ /** Time specification (required) */
76
+ time: TimeControlInput;
77
+ /** Override starting time for white (seconds) - for time odds */
78
+ whiteTime?: number;
79
+ /** Override starting time for black (seconds) - for time odds */
80
+ blackTime?: number;
81
+ /** Timing method (default: 'fischer') */
82
+ timingMethod?: TimingMethod;
83
+ /** Clock start behavior (default: 'delayed') */
84
+ clockStart?: ClockStartMode;
85
+
86
+ /** Callback when a player's time runs out */
87
+ onTimeout?: (loser: ClockColor) => void;
88
+ /** Callback when active player switches */
89
+ onSwitch?: (activePlayer: ClockColor) => void;
90
+ /** Callback on each time update */
91
+ onTimeUpdate?: (times: { white: number; black: number }) => void;
92
+ }
93
+
94
+ /**
95
+ * Period tracking state (only used for multi-period controls)
96
+ */
97
+ export interface PeriodState {
98
+ /** Current period index for each player (0-based) */
99
+ periodIndex: {
100
+ white: number;
101
+ black: number;
102
+ };
103
+ /** Moves made in current period by each player */
104
+ periodMoves: {
105
+ white: number;
106
+ black: number;
107
+ };
108
+ /** All periods for this time control */
109
+ periods: TimeControlPhase[];
110
+ }
111
+
112
+ /**
113
+ * Normalized time control with all times converted to milliseconds
114
+ */
115
+ export interface NormalizedTimeControl {
116
+ baseTime: number; // milliseconds
117
+ increment: number; // milliseconds
118
+ delay: number; // milliseconds
119
+ timingMethod: TimingMethod;
120
+ clockStart: ClockStartMode;
121
+ whiteTimeOverride?: number; // milliseconds
122
+ blackTimeOverride?: number; // milliseconds
123
+ /** Multi-period configuration (present only for multi-period time controls) */
124
+ periods?: TimeControlPhase[];
125
+ }
126
+
127
+ // ============================================================================
128
+ // Clock State Types
129
+ // ============================================================================
130
+
131
+ /**
132
+ * Clock times state (used for both current and initial times)
133
+ */
134
+ export interface ClockTimes {
135
+ white: number; // milliseconds
136
+ black: number; // milliseconds
137
+ }
138
+
139
+ /**
140
+ * Computed clock info
141
+ */
142
+ export interface ClockInfo {
143
+ isRunning: boolean;
144
+ isPaused: boolean;
145
+ isFinished: boolean;
146
+ isWhiteActive: boolean;
147
+ isBlackActive: boolean;
148
+ hasTimeout: boolean;
149
+ hasTimeOdds: boolean;
150
+ }
151
+
152
+ /**
153
+ * Clock methods
154
+ */
155
+ export interface ClockMethods {
156
+ start: () => void;
157
+ pause: () => void;
158
+ resume: () => void;
159
+ switch: () => void;
160
+ reset: (timeControl?: TimeControlInput) => void;
161
+ addTime: (player: ClockColor, milliseconds: number) => void;
162
+ setTime: (player: ClockColor, milliseconds: number) => void;
163
+ }
164
+
165
+ /**
166
+ * Complete clock state return type
167
+ */
168
+ export interface UseChessClockReturn {
169
+ // Time values (milliseconds)
170
+ times: ClockTimes;
171
+ initialTimes: ClockTimes;
172
+
173
+ // Status
174
+ status: ClockStatus;
175
+ activePlayer: ClockColor | null;
176
+ timeout: ClockColor | null;
177
+
178
+ // Configuration
179
+ timingMethod: TimingMethod;
180
+
181
+ // Computed
182
+ info: ClockInfo;
183
+
184
+ currentPeriodIndex: {
185
+ white: number;
186
+ black: number;
187
+ };
188
+ totalPeriods: number;
189
+ currentPeriod: {
190
+ white: TimeControlPhase;
191
+ black: TimeControlPhase;
192
+ };
193
+ periodMoves: {
194
+ white: number;
195
+ black: number;
196
+ };
197
+
198
+ // Methods
199
+ methods: ClockMethods;
200
+ }
201
+
202
+ // ============================================================================
203
+ // Utility Types
204
+ // ============================================================================
205
+
206
+ /**
207
+ * Time format options
208
+ */
209
+ export type TimeFormat = "auto" | "mm:ss" | "ss.d" | "hh:mm:ss";
210
+
211
+ /**
212
+ * Timing method result
213
+ */
214
+ export interface TimingMethodResult {
215
+ newTime: number; // milliseconds
216
+ delayRemaining: number; // milliseconds (for delay methods)
217
+ }
@@ -0,0 +1,150 @@
1
+ import { calculateSwitchTime } from "../calculateSwitchTime";
2
+
3
+ describe("calculateSwitchTime", () => {
4
+ const baseConfig = {
5
+ baseTime: 300_000,
6
+ increment: 3_000,
7
+ delay: 5_000,
8
+ timingMethod: "fischer" as const,
9
+ clockStart: "delayed" as const,
10
+ };
11
+
12
+ describe("Fischer timing method", () => {
13
+ it("should decrement time spent and add increment", () => {
14
+ const result = calculateSwitchTime(
15
+ 300_000, // currentTime
16
+ 5_000, // timeSpent
17
+ { ...baseConfig, timingMethod: "fischer", increment: 3_000 },
18
+ );
19
+ expect(result).toBe(298_000); // 300000 - 5000 + 3000
20
+ });
21
+
22
+ it("should handle time spent less than increment", () => {
23
+ const result = calculateSwitchTime(300_000, 2_000, {
24
+ ...baseConfig,
25
+ timingMethod: "fischer",
26
+ increment: 5_000,
27
+ });
28
+ expect(result).toBe(303_000); // 300000 - 2000 + 5000
29
+ });
30
+
31
+ it("should handle zero increment", () => {
32
+ const result = calculateSwitchTime(300_000, 5_000, {
33
+ ...baseConfig,
34
+ timingMethod: "fischer",
35
+ increment: 0,
36
+ });
37
+ expect(result).toBe(295_000); // 300000 - 5000
38
+ });
39
+
40
+ it("should not go below zero before increment", () => {
41
+ const result = calculateSwitchTime(1_000, 5_000, {
42
+ ...baseConfig,
43
+ timingMethod: "fischer",
44
+ increment: 3_000,
45
+ });
46
+ expect(result).toBe(3_000); // max(0, 1000 - 5000) + 3000
47
+ });
48
+
49
+ it("should handle increment on very low time", () => {
50
+ const result = calculateSwitchTime(100, 5_000, {
51
+ ...baseConfig,
52
+ timingMethod: "fischer",
53
+ increment: 3_000,
54
+ });
55
+ expect(result).toBe(3_000); // max(0, 100 - 5000) + 3000 = 0 + 3000
56
+ });
57
+ });
58
+
59
+ describe("Delay timing method", () => {
60
+ it("should not decrement when within delay period", () => {
61
+ const result = calculateSwitchTime(
62
+ 300_000,
63
+ 3_000, // within 5 second delay
64
+ { ...baseConfig, timingMethod: "delay", delay: 5_000 },
65
+ );
66
+ expect(result).toBe(300_000); // No decrement (3s - 5s delay = 0 effective)
67
+ });
68
+
69
+ it("should decrement only time spent after delay", () => {
70
+ const result = calculateSwitchTime(
71
+ 300_000,
72
+ 10_000, // 10 seconds spent, 5 second delay
73
+ { ...baseConfig, timingMethod: "delay", delay: 5_000 },
74
+ );
75
+ expect(result).toBe(295_000); // 300000 - (10000 - 5000) = 295000
76
+ });
77
+
78
+ it("should handle exactly at delay boundary", () => {
79
+ const result = calculateSwitchTime(
80
+ 300_000,
81
+ 5_000, // exactly delay amount
82
+ { ...baseConfig, timingMethod: "delay", delay: 5_000 },
83
+ );
84
+ expect(result).toBe(300_000); // max(0, 5000 - 5000) = 0 effective
85
+ });
86
+
87
+ it("should handle zero delay", () => {
88
+ const result = calculateSwitchTime(300_000, 5_000, {
89
+ ...baseConfig,
90
+ timingMethod: "delay",
91
+ delay: 0,
92
+ });
93
+ expect(result).toBe(295_000); // 300000 - 5000
94
+ });
95
+ });
96
+
97
+ describe("Bronstein timing method", () => {
98
+ it("should decrement and add back time spent within delay", () => {
99
+ const result = calculateSwitchTime(
100
+ 300_000,
101
+ 3_000, // 3 seconds spent, 5 second delay
102
+ { ...baseConfig, timingMethod: "bronstein", delay: 5_000 },
103
+ );
104
+ expect(result).toBe(300_000); // 300000 - 3000 + 3000 = 300000
105
+ });
106
+
107
+ it("should add back only delay amount when over delay", () => {
108
+ const result = calculateSwitchTime(
109
+ 300_000,
110
+ 10_000, // 10 seconds spent, 5 second delay
111
+ { ...baseConfig, timingMethod: "bronstein", delay: 5_000 },
112
+ );
113
+ expect(result).toBe(295_000); // 300000 - 10000 + 5000 = 295000
114
+ });
115
+
116
+ it("should handle zero delay", () => {
117
+ const result = calculateSwitchTime(300_000, 5_000, {
118
+ ...baseConfig,
119
+ timingMethod: "bronstein",
120
+ delay: 0,
121
+ });
122
+ expect(result).toBe(295_000); // 300000 - 5000 + 0 = 295000
123
+ });
124
+ });
125
+
126
+ describe("Edge cases", () => {
127
+ it("should handle zero time spent", () => {
128
+ const result = calculateSwitchTime(300_000, 0, baseConfig);
129
+ expect(result).toBe(303_000); // 300000 - 0 + 3000 (increment)
130
+ });
131
+
132
+ it("should handle very low current time", () => {
133
+ const result = calculateSwitchTime(500, 3_000, {
134
+ ...baseConfig,
135
+ timingMethod: "fischer",
136
+ increment: 2_000,
137
+ });
138
+ expect(result).toBe(2_000); // max(0, 500 - 3000) + 2000 = 0 + 2000
139
+ });
140
+
141
+ it("should handle zero current time", () => {
142
+ const result = calculateSwitchTime(0, 3_000, {
143
+ ...baseConfig,
144
+ timingMethod: "fischer",
145
+ increment: 2_000,
146
+ });
147
+ expect(result).toBe(2_000); // max(0, 0 - 3000) + 2000 = 0 + 2000
148
+ });
149
+ });
150
+ });
@@ -0,0 +1,83 @@
1
+ import { formatClockTime } from "../formatTime";
2
+
3
+ describe("formatClockTime", () => {
4
+ describe("auto format", () => {
5
+ it("should show mm:ss for times >= 20 seconds", () => {
6
+ expect(formatClockTime(20_000, "auto")).toBe("0:20");
7
+ expect(formatClockTime(60_000, "auto")).toBe("1:00");
8
+ expect(formatClockTime(300_000, "auto")).toBe("5:00");
9
+ expect(formatClockTime(600_000, "auto")).toBe("10:00");
10
+ });
11
+
12
+ it("should show ss.d (seconds with decimal) for times < 20 seconds", () => {
13
+ expect(formatClockTime(19_999, "auto")).toBe("20.0");
14
+ expect(formatClockTime(10_500, "auto")).toBe("10.5");
15
+ expect(formatClockTime(5_000, "auto")).toBe("5.0");
16
+ expect(formatClockTime(500, "auto")).toBe("0.5");
17
+ expect(formatClockTime(100, "auto")).toBe("0.1");
18
+ });
19
+ });
20
+
21
+ describe("mm:ss format", () => {
22
+ it("should format time as minutes:seconds", () => {
23
+ expect(formatClockTime(0, "mm:ss")).toBe("0:00");
24
+ expect(formatClockTime(5_000, "mm:ss")).toBe("0:05");
25
+ expect(formatClockTime(60_000, "mm:ss")).toBe("1:00");
26
+ expect(formatClockTime(65_000, "mm:ss")).toBe("1:05");
27
+ expect(formatClockTime(300_000, "mm:ss")).toBe("5:00");
28
+ expect(formatClockTime(365_000, "mm:ss")).toBe("6:05");
29
+ });
30
+
31
+ it("should show hours when time exceeds 60 minutes", () => {
32
+ expect(formatClockTime(3_600_000, "mm:ss")).toBe("1:00:00");
33
+ expect(formatClockTime(3_665_000, "mm:ss")).toBe("1:01:05");
34
+ expect(formatClockTime(5_400_000, "mm:ss")).toBe("1:30:00");
35
+ expect(formatClockTime(7_200_000, "mm:ss")).toBe("2:00:00");
36
+ });
37
+ });
38
+
39
+ describe("ss.d format", () => {
40
+ it("should format time as seconds with decimal", () => {
41
+ expect(formatClockTime(0, "ss.d")).toBe("0.0");
42
+ expect(formatClockTime(500, "ss.d")).toBe("0.5");
43
+ expect(formatClockTime(5_000, "ss.d")).toBe("5.0");
44
+ expect(formatClockTime(10_500, "ss.d")).toBe("10.5");
45
+ expect(formatClockTime(60_000, "ss.d")).toBe("60.0");
46
+ expect(formatClockTime(125_500, "ss.d")).toBe("125.5");
47
+ });
48
+ });
49
+
50
+ describe("hh:mm:ss format", () => {
51
+ it("should format time as hours:minutes:seconds", () => {
52
+ expect(formatClockTime(0, "hh:mm:ss")).toBe("0:00:00");
53
+ expect(formatClockTime(5_000, "hh:mm:ss")).toBe("0:00:05");
54
+ expect(formatClockTime(65_000, "hh:mm:ss")).toBe("0:01:05");
55
+ expect(formatClockTime(3_600_000, "hh:mm:ss")).toBe("1:00:00");
56
+ expect(formatClockTime(3_665_000, "hh:mm:ss")).toBe("1:01:05");
57
+ expect(formatClockTime(5_400_000, "hh:mm:ss")).toBe("1:30:00");
58
+ });
59
+ });
60
+
61
+ describe("edge cases", () => {
62
+ it("should clamp negative times to zero", () => {
63
+ expect(formatClockTime(-1000, "auto")).toBe("0.0");
64
+ expect(formatClockTime(-1000, "mm:ss")).toBe("0:00");
65
+ });
66
+
67
+ it("should handle zero time", () => {
68
+ expect(formatClockTime(0, "auto")).toBe("0.0");
69
+ expect(formatClockTime(0, "mm:ss")).toBe("0:00");
70
+ expect(formatClockTime(0, "ss.d")).toBe("0.0");
71
+ expect(formatClockTime(0, "hh:mm:ss")).toBe("0:00:00");
72
+ });
73
+
74
+ it("should ceil to nearest second for mm:ss and hh:mm:ss", () => {
75
+ // 5999ms should round up to 6 seconds
76
+ expect(formatClockTime(5_999, "mm:ss")).toBe("0:06");
77
+ expect(formatClockTime(5_999, "hh:mm:ss")).toBe("0:00:06");
78
+
79
+ // 5001ms should round up
80
+ expect(formatClockTime(5_001, "mm:ss")).toBe("0:06");
81
+ });
82
+ });
83
+ });