@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
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import type { Meta } from "@storybook/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { ChessClock } from "./index";
|
|
4
|
+
import { useChessClockContext } from "../../hooks/useChessClockContext";
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Design Tokens
|
|
8
|
+
// ============================================================================
|
|
9
|
+
const color = {
|
|
10
|
+
bg: "#f5f5f0",
|
|
11
|
+
surface: "#ffffff",
|
|
12
|
+
border: "#e2e0db",
|
|
13
|
+
text: "#2d2d2d",
|
|
14
|
+
textSecondary: "#7a7a72",
|
|
15
|
+
textMuted: "#a5a59c",
|
|
16
|
+
accent: "#5b8a3c",
|
|
17
|
+
dark: "#1c1c1a",
|
|
18
|
+
info: "#e7f5ff",
|
|
19
|
+
infoBorder: "#b4d5f0",
|
|
20
|
+
infoText: "#1864ab",
|
|
21
|
+
warn: "#b58a1b",
|
|
22
|
+
warnBg: "#fff9db",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const font = {
|
|
26
|
+
sans: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
|
27
|
+
mono: "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Shared Styles
|
|
32
|
+
// ============================================================================
|
|
33
|
+
const s = {
|
|
34
|
+
container: {
|
|
35
|
+
display: "flex",
|
|
36
|
+
flexDirection: "column" as const,
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
gap: "16px",
|
|
39
|
+
padding: "24px",
|
|
40
|
+
fontFamily: font.sans,
|
|
41
|
+
maxWidth: "420px",
|
|
42
|
+
margin: "0 auto",
|
|
43
|
+
},
|
|
44
|
+
header: {
|
|
45
|
+
textAlign: "center" as const,
|
|
46
|
+
},
|
|
47
|
+
title: {
|
|
48
|
+
fontSize: "15px",
|
|
49
|
+
fontWeight: 600,
|
|
50
|
+
color: color.text,
|
|
51
|
+
margin: "0 0 4px",
|
|
52
|
+
letterSpacing: "-0.01em",
|
|
53
|
+
},
|
|
54
|
+
subtitle: {
|
|
55
|
+
fontSize: "13px",
|
|
56
|
+
color: color.textSecondary,
|
|
57
|
+
margin: 0,
|
|
58
|
+
lineHeight: 1.4,
|
|
59
|
+
},
|
|
60
|
+
controls: {
|
|
61
|
+
display: "flex",
|
|
62
|
+
gap: "8px",
|
|
63
|
+
justifyContent: "center",
|
|
64
|
+
flexWrap: "wrap" as const,
|
|
65
|
+
},
|
|
66
|
+
btn: {
|
|
67
|
+
padding: "7px 14px",
|
|
68
|
+
fontSize: "13px",
|
|
69
|
+
fontWeight: 500,
|
|
70
|
+
fontFamily: font.sans,
|
|
71
|
+
cursor: "pointer",
|
|
72
|
+
border: `1px solid ${color.border}`,
|
|
73
|
+
borderRadius: "4px",
|
|
74
|
+
backgroundColor: color.surface,
|
|
75
|
+
color: color.text,
|
|
76
|
+
} as React.CSSProperties,
|
|
77
|
+
btnPrimary: {
|
|
78
|
+
backgroundColor: color.accent,
|
|
79
|
+
borderColor: color.accent,
|
|
80
|
+
color: "#fff",
|
|
81
|
+
} as React.CSSProperties,
|
|
82
|
+
btnDisabled: {
|
|
83
|
+
opacity: 0.5,
|
|
84
|
+
cursor: "not-allowed",
|
|
85
|
+
} as React.CSSProperties,
|
|
86
|
+
info: {
|
|
87
|
+
padding: "10px 14px",
|
|
88
|
+
backgroundColor: color.info,
|
|
89
|
+
border: `1px solid ${color.infoBorder}`,
|
|
90
|
+
borderRadius: "5px",
|
|
91
|
+
fontSize: "12px",
|
|
92
|
+
color: color.infoText,
|
|
93
|
+
textAlign: "center" as const,
|
|
94
|
+
lineHeight: 1.5,
|
|
95
|
+
},
|
|
96
|
+
logBox: {
|
|
97
|
+
width: "100%",
|
|
98
|
+
padding: "10px 12px",
|
|
99
|
+
backgroundColor: color.bg,
|
|
100
|
+
border: `1px solid ${color.border}`,
|
|
101
|
+
borderRadius: "5px",
|
|
102
|
+
fontSize: "11px",
|
|
103
|
+
fontFamily: font.mono,
|
|
104
|
+
maxHeight: "90px",
|
|
105
|
+
overflow: "auto",
|
|
106
|
+
color: color.textSecondary,
|
|
107
|
+
},
|
|
108
|
+
radioGroup: {
|
|
109
|
+
display: "flex",
|
|
110
|
+
gap: "8px",
|
|
111
|
+
flexWrap: "wrap" as const,
|
|
112
|
+
justifyContent: "center",
|
|
113
|
+
},
|
|
114
|
+
radioLabel: {
|
|
115
|
+
display: "flex",
|
|
116
|
+
alignItems: "center",
|
|
117
|
+
gap: "4px",
|
|
118
|
+
fontSize: "13px",
|
|
119
|
+
fontWeight: 500,
|
|
120
|
+
color: color.text,
|
|
121
|
+
cursor: "pointer",
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const clock = {
|
|
126
|
+
row: {
|
|
127
|
+
display: "flex",
|
|
128
|
+
gap: "12px",
|
|
129
|
+
justifyContent: "center",
|
|
130
|
+
alignItems: "center",
|
|
131
|
+
},
|
|
132
|
+
cell: {
|
|
133
|
+
display: "flex",
|
|
134
|
+
flexDirection: "column" as const,
|
|
135
|
+
alignItems: "center",
|
|
136
|
+
gap: "4px",
|
|
137
|
+
},
|
|
138
|
+
label: {
|
|
139
|
+
fontSize: "11px",
|
|
140
|
+
fontWeight: 600,
|
|
141
|
+
color: color.textMuted,
|
|
142
|
+
textTransform: "uppercase" as const,
|
|
143
|
+
letterSpacing: "0.06em",
|
|
144
|
+
},
|
|
145
|
+
white: {
|
|
146
|
+
padding: "10px 20px",
|
|
147
|
+
fontSize: "24px",
|
|
148
|
+
fontWeight: 600,
|
|
149
|
+
fontFamily: font.mono,
|
|
150
|
+
borderRadius: "5px",
|
|
151
|
+
textAlign: "center" as const,
|
|
152
|
+
minWidth: "100px",
|
|
153
|
+
backgroundColor: color.surface,
|
|
154
|
+
border: `2px solid ${color.text}`,
|
|
155
|
+
color: color.text,
|
|
156
|
+
},
|
|
157
|
+
black: {
|
|
158
|
+
padding: "10px 20px",
|
|
159
|
+
fontSize: "24px",
|
|
160
|
+
fontWeight: 600,
|
|
161
|
+
fontFamily: font.mono,
|
|
162
|
+
borderRadius: "5px",
|
|
163
|
+
textAlign: "center" as const,
|
|
164
|
+
minWidth: "100px",
|
|
165
|
+
backgroundColor: color.dark,
|
|
166
|
+
border: `2px solid ${color.dark}`,
|
|
167
|
+
color: "#f0f0ec",
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// Reusable Helpers
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
const ClockPair = ({
|
|
176
|
+
format,
|
|
177
|
+
}: {
|
|
178
|
+
format?: "auto" | "mm:ss" | "ss.d" | "hh:mm:ss";
|
|
179
|
+
}) => (
|
|
180
|
+
<div style={clock.row}>
|
|
181
|
+
<div style={clock.cell}>
|
|
182
|
+
<span style={clock.label}>White</span>
|
|
183
|
+
<ChessClock.Display color="white" format={format} style={clock.white} />
|
|
184
|
+
</div>
|
|
185
|
+
<div style={clock.cell}>
|
|
186
|
+
<span style={clock.label}>Black</span>
|
|
187
|
+
<ChessClock.Display color="black" format={format} style={clock.black} />
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const PlayPauseBtn = () => {
|
|
193
|
+
const { status } = useChessClockContext();
|
|
194
|
+
const isDisabled = status === "finished" || status === "delayed";
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<ChessClock.PlayPause
|
|
198
|
+
style={{
|
|
199
|
+
...s.btn,
|
|
200
|
+
...s.btnPrimary,
|
|
201
|
+
...(isDisabled ? s.btnDisabled : {}),
|
|
202
|
+
}}
|
|
203
|
+
startContent="Start"
|
|
204
|
+
pauseContent="Pause"
|
|
205
|
+
resumeContent="Resume"
|
|
206
|
+
finishedContent="Game Over"
|
|
207
|
+
/>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const SwitchBtn = () => (
|
|
212
|
+
<ChessClock.Switch style={s.btn}>Switch</ChessClock.Switch>
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const ResetBtn = () => <ChessClock.Reset style={s.btn}>Reset</ChessClock.Reset>;
|
|
216
|
+
|
|
217
|
+
const Controls = ({ children }: { children?: React.ReactNode }) => (
|
|
218
|
+
<div style={s.controls}>
|
|
219
|
+
<PlayPauseBtn />
|
|
220
|
+
<SwitchBtn />
|
|
221
|
+
{children}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// Meta
|
|
227
|
+
// ============================================================================
|
|
228
|
+
const meta = {
|
|
229
|
+
title: "react-chess-clock/Components/ChessClock",
|
|
230
|
+
component: ChessClock.Root,
|
|
231
|
+
tags: ["components", "clock", "timer"],
|
|
232
|
+
argTypes: {},
|
|
233
|
+
parameters: {
|
|
234
|
+
actions: { argTypesRegex: "^_on.*" },
|
|
235
|
+
layout: "centered",
|
|
236
|
+
},
|
|
237
|
+
} satisfies Meta<typeof ChessClock.Root>;
|
|
238
|
+
|
|
239
|
+
export default meta;
|
|
240
|
+
|
|
241
|
+
// ============================================================================
|
|
242
|
+
// 1. Default
|
|
243
|
+
// ============================================================================
|
|
244
|
+
|
|
245
|
+
export const Default = () => (
|
|
246
|
+
<ChessClock.Root timeControl={{ time: "5+3" }}>
|
|
247
|
+
<div style={s.container}>
|
|
248
|
+
<div style={s.header}>
|
|
249
|
+
<h3 style={s.title}>Blitz · 5+3</h3>
|
|
250
|
+
<p style={s.subtitle}>
|
|
251
|
+
5 minutes with 3-second Fischer increment (delayed start)
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
<ClockPair />
|
|
255
|
+
<div style={s.info}>Clock starts after Black's first move</div>
|
|
256
|
+
<Controls>
|
|
257
|
+
<ResetBtn />
|
|
258
|
+
</Controls>
|
|
259
|
+
</div>
|
|
260
|
+
</ChessClock.Root>
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// 2. TimingMethods
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
export const TimingMethods = () => {
|
|
268
|
+
const [method, setMethod] = React.useState<"fischer" | "delay" | "bronstein">(
|
|
269
|
+
"fischer",
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const descriptions: Record<string, string> = {
|
|
273
|
+
fischer: "Adds increment to your clock after each move.",
|
|
274
|
+
delay: "Countdown waits for the delay period before decrementing.",
|
|
275
|
+
bronstein:
|
|
276
|
+
"Adds back actual time used (up to delay amount) after each move.",
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<ChessClock.Root
|
|
281
|
+
key={method}
|
|
282
|
+
timeControl={{
|
|
283
|
+
time: {
|
|
284
|
+
baseTime: 300,
|
|
285
|
+
increment: method === "fischer" ? 3 : 0,
|
|
286
|
+
delay: method !== "fischer" ? 3 : 0,
|
|
287
|
+
},
|
|
288
|
+
timingMethod: method,
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
<div style={s.container}>
|
|
292
|
+
<div style={s.header}>
|
|
293
|
+
<h3 style={s.title}>Timing Methods</h3>
|
|
294
|
+
<p style={s.subtitle}>Compare Fischer, Delay, and Bronstein</p>
|
|
295
|
+
</div>
|
|
296
|
+
<div style={s.radioGroup}>
|
|
297
|
+
{(["fischer", "delay", "bronstein"] as const).map((m) => (
|
|
298
|
+
<label key={m} style={s.radioLabel}>
|
|
299
|
+
<input
|
|
300
|
+
type="radio"
|
|
301
|
+
name="timing"
|
|
302
|
+
checked={method === m}
|
|
303
|
+
onChange={() => setMethod(m)}
|
|
304
|
+
/>
|
|
305
|
+
{m.charAt(0).toUpperCase() + m.slice(1)}
|
|
306
|
+
</label>
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
<ClockPair />
|
|
310
|
+
<div style={s.info}>{descriptions[method]}</div>
|
|
311
|
+
<Controls>
|
|
312
|
+
<ResetBtn />
|
|
313
|
+
</Controls>
|
|
314
|
+
</div>
|
|
315
|
+
</ChessClock.Root>
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// 3. ClockStartModes
|
|
321
|
+
// ============================================================================
|
|
322
|
+
|
|
323
|
+
export const ClockStartModes = () => {
|
|
324
|
+
const [mode, setMode] = React.useState<"delayed" | "immediate" | "manual">(
|
|
325
|
+
"delayed",
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const descriptions: Record<string, string> = {
|
|
329
|
+
delayed:
|
|
330
|
+
"Clock starts after Black's first move (Lichess-style). White moves → Black moves → Clock starts.",
|
|
331
|
+
immediate:
|
|
332
|
+
"White's clock starts counting down immediately on first switch (Chess.com-style).",
|
|
333
|
+
manual: "Clock stays idle until you press Start.",
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<ChessClock.Root key={mode} timeControl={{ time: "5+3", clockStart: mode }}>
|
|
338
|
+
<div style={s.container}>
|
|
339
|
+
<div style={s.header}>
|
|
340
|
+
<h3 style={s.title}>Clock Start Modes</h3>
|
|
341
|
+
<p style={s.subtitle}>Controls when the clock begins counting down</p>
|
|
342
|
+
</div>
|
|
343
|
+
<div style={s.radioGroup}>
|
|
344
|
+
{(["delayed", "immediate", "manual"] as const).map((m) => (
|
|
345
|
+
<label key={m} style={s.radioLabel}>
|
|
346
|
+
<input
|
|
347
|
+
type="radio"
|
|
348
|
+
name="clockStart"
|
|
349
|
+
checked={mode === m}
|
|
350
|
+
onChange={() => setMode(m)}
|
|
351
|
+
/>
|
|
352
|
+
{m.charAt(0).toUpperCase() + m.slice(1)}
|
|
353
|
+
</label>
|
|
354
|
+
))}
|
|
355
|
+
</div>
|
|
356
|
+
<ClockPair />
|
|
357
|
+
<div style={s.info}>{descriptions[mode]}</div>
|
|
358
|
+
<Controls>
|
|
359
|
+
<ResetBtn />
|
|
360
|
+
</Controls>
|
|
361
|
+
</div>
|
|
362
|
+
</ChessClock.Root>
|
|
363
|
+
);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// ============================================================================
|
|
367
|
+
// 4. TimeOdds
|
|
368
|
+
// ============================================================================
|
|
369
|
+
|
|
370
|
+
export const TimeOdds = () => (
|
|
371
|
+
<ChessClock.Root timeControl={{ time: "5", whiteTime: 300, blackTime: 180 }}>
|
|
372
|
+
<div style={s.container}>
|
|
373
|
+
<div style={s.header}>
|
|
374
|
+
<h3 style={s.title}>Time Odds</h3>
|
|
375
|
+
<p style={s.subtitle}>White 5 min vs Black 3 min</p>
|
|
376
|
+
</div>
|
|
377
|
+
<ClockPair />
|
|
378
|
+
<Controls>
|
|
379
|
+
<ResetBtn />
|
|
380
|
+
</Controls>
|
|
381
|
+
</div>
|
|
382
|
+
</ChessClock.Root>
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// ============================================================================
|
|
386
|
+
// 5. DisplayFormats
|
|
387
|
+
// ============================================================================
|
|
388
|
+
|
|
389
|
+
const fmt = {
|
|
390
|
+
row: {
|
|
391
|
+
display: "flex",
|
|
392
|
+
justifyContent: "space-between",
|
|
393
|
+
alignItems: "center",
|
|
394
|
+
padding: "6px 10px",
|
|
395
|
+
backgroundColor: color.bg,
|
|
396
|
+
borderRadius: "4px",
|
|
397
|
+
},
|
|
398
|
+
label: {
|
|
399
|
+
fontSize: "12px",
|
|
400
|
+
fontWeight: 500,
|
|
401
|
+
color: color.textSecondary,
|
|
402
|
+
fontFamily: font.mono,
|
|
403
|
+
},
|
|
404
|
+
display: {
|
|
405
|
+
padding: "6px 14px",
|
|
406
|
+
fontSize: "16px",
|
|
407
|
+
fontWeight: 600,
|
|
408
|
+
fontFamily: font.mono,
|
|
409
|
+
borderRadius: "4px",
|
|
410
|
+
backgroundColor: color.surface,
|
|
411
|
+
border: `2px solid ${color.text}`,
|
|
412
|
+
color: color.text,
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
export const DisplayFormats = () => (
|
|
417
|
+
<ChessClock.Root
|
|
418
|
+
timeControl={{
|
|
419
|
+
time: { baseTime: 20, increment: 0 },
|
|
420
|
+
clockStart: "immediate",
|
|
421
|
+
}}
|
|
422
|
+
>
|
|
423
|
+
<div style={s.container}>
|
|
424
|
+
<div style={s.header}>
|
|
425
|
+
<h3 style={s.title}>Display Formats</h3>
|
|
426
|
+
<p style={s.subtitle}>
|
|
427
|
+
All built-in formats + custom formatTime (20s base to show auto
|
|
428
|
+
decimals)
|
|
429
|
+
</p>
|
|
430
|
+
</div>
|
|
431
|
+
<div
|
|
432
|
+
style={{
|
|
433
|
+
display: "flex",
|
|
434
|
+
flexDirection: "column",
|
|
435
|
+
gap: "6px",
|
|
436
|
+
width: "100%",
|
|
437
|
+
}}
|
|
438
|
+
>
|
|
439
|
+
<div style={fmt.row}>
|
|
440
|
+
<span style={fmt.label}>format="auto"</span>
|
|
441
|
+
<ChessClock.Display color="white" format="auto" style={fmt.display} />
|
|
442
|
+
</div>
|
|
443
|
+
<div style={fmt.row}>
|
|
444
|
+
<span style={fmt.label}>format="mm:ss"</span>
|
|
445
|
+
<ChessClock.Display
|
|
446
|
+
color="white"
|
|
447
|
+
format="mm:ss"
|
|
448
|
+
style={fmt.display}
|
|
449
|
+
/>
|
|
450
|
+
</div>
|
|
451
|
+
<div style={fmt.row}>
|
|
452
|
+
<span style={fmt.label}>format="hh:mm:ss"</span>
|
|
453
|
+
<ChessClock.Display
|
|
454
|
+
color="white"
|
|
455
|
+
format="hh:mm:ss"
|
|
456
|
+
style={fmt.display}
|
|
457
|
+
/>
|
|
458
|
+
</div>
|
|
459
|
+
<div style={fmt.row}>
|
|
460
|
+
<span style={fmt.label}>format="ss.d"</span>
|
|
461
|
+
<ChessClock.Display color="white" format="ss.d" style={fmt.display} />
|
|
462
|
+
</div>
|
|
463
|
+
<div style={fmt.row}>
|
|
464
|
+
<span style={fmt.label}>Custom fn</span>
|
|
465
|
+
<ChessClock.Display
|
|
466
|
+
color="white"
|
|
467
|
+
formatTime={(ms) => `${Math.ceil(ms / 1000)}s`}
|
|
468
|
+
style={fmt.display}
|
|
469
|
+
/>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
<Controls>
|
|
473
|
+
<ResetBtn />
|
|
474
|
+
</Controls>
|
|
475
|
+
</div>
|
|
476
|
+
</ChessClock.Root>
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// ============================================================================
|
|
480
|
+
// 6. Callbacks
|
|
481
|
+
// ============================================================================
|
|
482
|
+
|
|
483
|
+
export const Callbacks = () => {
|
|
484
|
+
const [logs, setLogs] = React.useState<string[]>([]);
|
|
485
|
+
const addLog = (msg: string) => setLogs((prev) => [...prev.slice(-4), msg]);
|
|
486
|
+
|
|
487
|
+
return (
|
|
488
|
+
<ChessClock.Root
|
|
489
|
+
timeControl={{
|
|
490
|
+
time: "1+5",
|
|
491
|
+
clockStart: "immediate",
|
|
492
|
+
onTimeout: (loser) => addLog(`Timeout! ${loser} loses on time`),
|
|
493
|
+
onSwitch: (active) => addLog(`Switch: ${active} is now active`),
|
|
494
|
+
onTimeUpdate: (times) =>
|
|
495
|
+
addLog(`Time: W=${times.white}ms B=${times.black}ms`),
|
|
496
|
+
}}
|
|
497
|
+
>
|
|
498
|
+
<div style={s.container}>
|
|
499
|
+
<div style={s.header}>
|
|
500
|
+
<h3 style={s.title}>Callbacks · 1+5</h3>
|
|
501
|
+
<p style={s.subtitle}>
|
|
502
|
+
onTimeout, onSwitch, onTimeUpdate event logging
|
|
503
|
+
</p>
|
|
504
|
+
</div>
|
|
505
|
+
<ClockPair format="ss.d" />
|
|
506
|
+
<Controls>
|
|
507
|
+
<ResetBtn />
|
|
508
|
+
</Controls>
|
|
509
|
+
<div style={s.logBox}>
|
|
510
|
+
{logs.length === 0 ? (
|
|
511
|
+
<span style={{ color: color.textMuted, fontStyle: "italic" }}>
|
|
512
|
+
Events will appear here...
|
|
513
|
+
</span>
|
|
514
|
+
) : (
|
|
515
|
+
logs.map((log, i) => <div key={i}>{log}</div>)
|
|
516
|
+
)}
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
</ChessClock.Root>
|
|
520
|
+
);
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// ============================================================================
|
|
524
|
+
// 7. DynamicReset
|
|
525
|
+
// ============================================================================
|
|
526
|
+
|
|
527
|
+
export const DynamicReset = () => (
|
|
528
|
+
<ChessClock.Root timeControl={{ time: "5+3" }}>
|
|
529
|
+
<div style={s.container}>
|
|
530
|
+
<div style={s.header}>
|
|
531
|
+
<h3 style={s.title}>Dynamic Reset</h3>
|
|
532
|
+
<p style={s.subtitle}>
|
|
533
|
+
Reset with a different time control using the timeControl prop
|
|
534
|
+
</p>
|
|
535
|
+
</div>
|
|
536
|
+
<ClockPair />
|
|
537
|
+
<Controls />
|
|
538
|
+
<div style={s.controls}>
|
|
539
|
+
<ChessClock.Reset style={s.btn}>Reset</ChessClock.Reset>
|
|
540
|
+
<ChessClock.Reset style={s.btn} timeControl="1">
|
|
541
|
+
1+0
|
|
542
|
+
</ChessClock.Reset>
|
|
543
|
+
<ChessClock.Reset style={s.btn} timeControl="3+2">
|
|
544
|
+
3+2
|
|
545
|
+
</ChessClock.Reset>
|
|
546
|
+
<ChessClock.Reset style={s.btn} timeControl="5+3">
|
|
547
|
+
5+3
|
|
548
|
+
</ChessClock.Reset>
|
|
549
|
+
<ChessClock.Reset style={s.btn} timeControl="10">
|
|
550
|
+
10+0
|
|
551
|
+
</ChessClock.Reset>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
</ChessClock.Root>
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
// ============================================================================
|
|
558
|
+
// 8. MultiPeriod
|
|
559
|
+
// ============================================================================
|
|
560
|
+
|
|
561
|
+
function PeriodInfo() {
|
|
562
|
+
const { currentPeriodIndex, totalPeriods, periodMoves, currentPeriod } =
|
|
563
|
+
useChessClockContext();
|
|
564
|
+
|
|
565
|
+
const periodLabel = (playerColor: "white" | "black") => {
|
|
566
|
+
const idx = currentPeriodIndex[playerColor];
|
|
567
|
+
const moves = periodMoves[playerColor];
|
|
568
|
+
const period = currentPeriod[playerColor];
|
|
569
|
+
const required = period.moves;
|
|
570
|
+
return required
|
|
571
|
+
? `Period ${idx + 1}/${totalPeriods} — ${moves}/${required} moves`
|
|
572
|
+
: `Period ${idx + 1}/${totalPeriods} — Sudden death`;
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
return (
|
|
576
|
+
<div
|
|
577
|
+
style={{
|
|
578
|
+
width: "100%",
|
|
579
|
+
display: "flex",
|
|
580
|
+
flexDirection: "column",
|
|
581
|
+
gap: "4px",
|
|
582
|
+
}}
|
|
583
|
+
>
|
|
584
|
+
{(["white", "black"] as const).map((c) => (
|
|
585
|
+
<div
|
|
586
|
+
key={c}
|
|
587
|
+
style={{
|
|
588
|
+
display: "flex",
|
|
589
|
+
justifyContent: "space-between",
|
|
590
|
+
padding: "6px 10px",
|
|
591
|
+
backgroundColor: c === "white" ? color.bg : color.dark,
|
|
592
|
+
borderRadius: "4px",
|
|
593
|
+
fontSize: "12px",
|
|
594
|
+
fontFamily: font.mono,
|
|
595
|
+
color: c === "white" ? color.text : "#f0f0ec",
|
|
596
|
+
}}
|
|
597
|
+
>
|
|
598
|
+
<span style={{ textTransform: "capitalize" }}>{c}</span>
|
|
599
|
+
<span>{periodLabel(c)}</span>
|
|
600
|
+
</div>
|
|
601
|
+
))}
|
|
602
|
+
</div>
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export const MultiPeriod = () => (
|
|
607
|
+
<ChessClock.Root
|
|
608
|
+
timeControl={{
|
|
609
|
+
time: [
|
|
610
|
+
{ baseTime: 5400, increment: 30, moves: 40 },
|
|
611
|
+
{ baseTime: 1800, increment: 30, moves: 20 },
|
|
612
|
+
{ baseTime: 900, increment: 30 },
|
|
613
|
+
],
|
|
614
|
+
clockStart: "manual",
|
|
615
|
+
}}
|
|
616
|
+
>
|
|
617
|
+
<div style={s.container}>
|
|
618
|
+
<div style={s.header}>
|
|
619
|
+
<h3 style={s.title}>FIDE Classical</h3>
|
|
620
|
+
<p style={s.subtitle}>90min/40 + 30min/20 + 15min SD</p>
|
|
621
|
+
</div>
|
|
622
|
+
<ClockPair />
|
|
623
|
+
<PeriodInfo />
|
|
624
|
+
<div style={s.info}>
|
|
625
|
+
Each player advances independently through 3 periods
|
|
626
|
+
</div>
|
|
627
|
+
<Controls>
|
|
628
|
+
<ResetBtn />
|
|
629
|
+
</Controls>
|
|
630
|
+
</div>
|
|
631
|
+
</ChessClock.Root>
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
// ============================================================================
|
|
635
|
+
// 9. ServerSync
|
|
636
|
+
// ============================================================================
|
|
637
|
+
|
|
638
|
+
function ServerTimeSyncHelper({
|
|
639
|
+
serverTimes,
|
|
640
|
+
}: {
|
|
641
|
+
serverTimes: { white: number; black: number };
|
|
642
|
+
}) {
|
|
643
|
+
const { methods } = useChessClockContext();
|
|
644
|
+
const prevTimes = React.useRef(serverTimes);
|
|
645
|
+
|
|
646
|
+
React.useEffect(() => {
|
|
647
|
+
if (serverTimes.white !== prevTimes.current.white) {
|
|
648
|
+
methods.setTime("white", serverTimes.white);
|
|
649
|
+
}
|
|
650
|
+
if (serverTimes.black !== prevTimes.current.black) {
|
|
651
|
+
methods.setTime("black", serverTimes.black);
|
|
652
|
+
}
|
|
653
|
+
prevTimes.current = serverTimes;
|
|
654
|
+
}, [serverTimes, methods]);
|
|
655
|
+
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export const ServerSync = () => {
|
|
660
|
+
const [serverTimes, setServerTimes] = React.useState({
|
|
661
|
+
white: 300000,
|
|
662
|
+
black: 300000,
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
React.useEffect(() => {
|
|
666
|
+
const interval = setInterval(() => {
|
|
667
|
+
setServerTimes((prev) => ({
|
|
668
|
+
white: Math.max(0, prev.white - 1000),
|
|
669
|
+
black: Math.max(0, prev.black - 2000),
|
|
670
|
+
}));
|
|
671
|
+
}, 500);
|
|
672
|
+
return () => clearInterval(interval);
|
|
673
|
+
}, []);
|
|
674
|
+
|
|
675
|
+
return (
|
|
676
|
+
<ChessClock.Root
|
|
677
|
+
timeControl={{
|
|
678
|
+
time: "5+0",
|
|
679
|
+
onTimeout: (loser) => console.log(`${loser} timed out`),
|
|
680
|
+
}}
|
|
681
|
+
>
|
|
682
|
+
<ServerTimeSyncHelper serverTimes={serverTimes} />
|
|
683
|
+
<div style={s.container}>
|
|
684
|
+
<div style={s.header}>
|
|
685
|
+
<h3 style={s.title}>Server Sync</h3>
|
|
686
|
+
<p style={s.subtitle}>Times synced from server via setTime()</p>
|
|
687
|
+
</div>
|
|
688
|
+
<ClockPair format="mm:ss" />
|
|
689
|
+
<Controls>
|
|
690
|
+
<ResetBtn />
|
|
691
|
+
</Controls>
|
|
692
|
+
<div
|
|
693
|
+
style={{
|
|
694
|
+
...s.info,
|
|
695
|
+
backgroundColor: color.warnBg,
|
|
696
|
+
borderColor: "#e0d3a8",
|
|
697
|
+
color: color.warn,
|
|
698
|
+
}}
|
|
699
|
+
>
|
|
700
|
+
<span style={{ fontFamily: font.mono, fontSize: "11px" }}>
|
|
701
|
+
Server: W={Math.ceil(serverTimes.white / 1000)}s B=
|
|
702
|
+
{Math.ceil(serverTimes.black / 1000)}s
|
|
703
|
+
</span>
|
|
704
|
+
</div>
|
|
705
|
+
</div>
|
|
706
|
+
</ChessClock.Root>
|
|
707
|
+
);
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// ============================================================================
|
|
711
|
+
// 10. AsChild
|
|
712
|
+
// ============================================================================
|
|
713
|
+
|
|
714
|
+
function AsChildStatus() {
|
|
715
|
+
const { status } = useChessClockContext();
|
|
716
|
+
return (
|
|
717
|
+
<div
|
|
718
|
+
style={{
|
|
719
|
+
fontSize: "12px",
|
|
720
|
+
fontFamily: font.mono,
|
|
721
|
+
color: color.textSecondary,
|
|
722
|
+
}}
|
|
723
|
+
>
|
|
724
|
+
Status: {status}
|
|
725
|
+
</div>
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export const AsChild = () => {
|
|
730
|
+
const customBtn: React.CSSProperties = {
|
|
731
|
+
...s.btn,
|
|
732
|
+
display: "inline-flex",
|
|
733
|
+
alignItems: "center",
|
|
734
|
+
gap: "6px",
|
|
735
|
+
userSelect: "none",
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
return (
|
|
739
|
+
<ChessClock.Root timeControl={{ time: "5+3", clockStart: "manual" }}>
|
|
740
|
+
<div style={s.container}>
|
|
741
|
+
<div style={s.header}>
|
|
742
|
+
<h3 style={s.title}>asChild Pattern</h3>
|
|
743
|
+
<p style={s.subtitle}>
|
|
744
|
+
All controls rendered as custom elements via asChild
|
|
745
|
+
</p>
|
|
746
|
+
</div>
|
|
747
|
+
<ClockPair />
|
|
748
|
+
<div style={s.controls}>
|
|
749
|
+
<ChessClock.PlayPause
|
|
750
|
+
asChild
|
|
751
|
+
startContent="Start"
|
|
752
|
+
pauseContent="Pause"
|
|
753
|
+
resumeContent="Resume"
|
|
754
|
+
finishedContent="Game Over"
|
|
755
|
+
>
|
|
756
|
+
<div style={{ ...customBtn, ...s.btnPrimary }}>
|
|
757
|
+
<span>placeholder</span>
|
|
758
|
+
</div>
|
|
759
|
+
</ChessClock.PlayPause>
|
|
760
|
+
|
|
761
|
+
<ChessClock.Switch asChild>
|
|
762
|
+
<div style={customBtn}>
|
|
763
|
+
<span>Switch</span>
|
|
764
|
+
</div>
|
|
765
|
+
</ChessClock.Switch>
|
|
766
|
+
|
|
767
|
+
<ChessClock.Reset asChild>
|
|
768
|
+
<div style={customBtn}>
|
|
769
|
+
<span>Reset</span>
|
|
770
|
+
</div>
|
|
771
|
+
</ChessClock.Reset>
|
|
772
|
+
</div>
|
|
773
|
+
<AsChildStatus />
|
|
774
|
+
<div style={s.info}>
|
|
775
|
+
All 3 buttons use asChild with custom <div> elements.
|
|
776
|
+
<br />
|
|
777
|
+
Disabled state propagates automatically (try when idle or finished).
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
</ChessClock.Root>
|
|
781
|
+
);
|
|
782
|
+
};
|