@react-chess-tools/react-chess-game 1.0.0 → 1.0.2

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.
@@ -2,8 +2,197 @@ import type { Meta } from "@storybook/react";
2
2
 
3
3
  import React from "react";
4
4
  import { ChessGame } from "./index";
5
+ import {
6
+ useSimulatedServer,
7
+ ServerMoveDetector,
8
+ ServerTimeSync,
9
+ } from "./ChessGame.stories.helpers";
5
10
 
6
- // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
11
+ // ============================================================================
12
+ // Design Tokens
13
+ // ============================================================================
14
+ const color = {
15
+ bg: "#f5f5f0",
16
+ surface: "#ffffff",
17
+ border: "#e2e0db",
18
+ text: "#2d2d2d",
19
+ textSecondary: "#7a7a72",
20
+ textMuted: "#a5a59c",
21
+ accent: "#5b8a3c",
22
+ accentLight: "#eef4e8",
23
+ dark: "#1c1c1a",
24
+ danger: "#c44",
25
+ dangerLight: "#fef2f2",
26
+ dangerBorder: "#e8b4b4",
27
+ warn: "#b58a1b",
28
+ warnLight: "#fdf8ec",
29
+ warnBorder: "#e0d3a8",
30
+ };
31
+
32
+ const font = {
33
+ sans: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
34
+ mono: "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace",
35
+ };
36
+
37
+ // ============================================================================
38
+ // Shared Styles
39
+ // ============================================================================
40
+ const s = {
41
+ container: {
42
+ display: "flex",
43
+ flexDirection: "column" as const,
44
+ alignItems: "center",
45
+ gap: "16px",
46
+ padding: "24px",
47
+ fontFamily: font.sans,
48
+ maxWidth: "560px",
49
+ margin: "0 auto",
50
+ },
51
+ header: {
52
+ textAlign: "center" as const,
53
+ },
54
+ title: {
55
+ fontSize: "15px",
56
+ fontWeight: 600,
57
+ color: color.text,
58
+ margin: "0 0 4px",
59
+ letterSpacing: "-0.01em",
60
+ },
61
+ subtitle: {
62
+ fontSize: "13px",
63
+ color: color.textSecondary,
64
+ margin: 0,
65
+ lineHeight: 1.4,
66
+ },
67
+ board: {
68
+ borderRadius: "6px",
69
+ overflow: "hidden",
70
+ boxShadow: "0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.04)",
71
+ },
72
+ hint: {
73
+ fontSize: "12px",
74
+ color: color.textMuted,
75
+ textAlign: "center" as const,
76
+ margin: 0,
77
+ lineHeight: 1.5,
78
+ },
79
+ btn: {
80
+ padding: "7px 14px",
81
+ fontSize: "13px",
82
+ fontWeight: 500,
83
+ fontFamily: font.sans,
84
+ cursor: "pointer",
85
+ border: `1px solid ${color.border}`,
86
+ borderRadius: "4px",
87
+ backgroundColor: color.surface,
88
+ color: color.text,
89
+ } as React.CSSProperties,
90
+ btnPrimary: {
91
+ backgroundColor: color.accent,
92
+ borderColor: color.accent,
93
+ color: "#fff",
94
+ } as React.CSSProperties,
95
+ controls: {
96
+ display: "flex",
97
+ gap: "8px",
98
+ justifyContent: "center",
99
+ flexWrap: "wrap" as const,
100
+ },
101
+ divider: {
102
+ width: "100%",
103
+ height: "1px",
104
+ backgroundColor: color.border,
105
+ margin: "4px 0",
106
+ },
107
+ };
108
+
109
+ // ============================================================================
110
+ // Clock Styles
111
+ // ============================================================================
112
+ const clock = {
113
+ row: {
114
+ display: "flex",
115
+ gap: "12px",
116
+ justifyContent: "center",
117
+ alignItems: "center",
118
+ },
119
+ cell: {
120
+ display: "flex",
121
+ flexDirection: "column" as const,
122
+ alignItems: "center",
123
+ gap: "4px",
124
+ },
125
+ label: {
126
+ fontSize: "11px",
127
+ fontWeight: 600,
128
+ color: color.textMuted,
129
+ textTransform: "uppercase" as const,
130
+ letterSpacing: "0.06em",
131
+ },
132
+ white: {
133
+ padding: "10px 20px",
134
+ fontSize: "24px",
135
+ fontWeight: 600,
136
+ fontFamily: font.mono,
137
+ borderRadius: "5px",
138
+ textAlign: "center" as const,
139
+ minWidth: "100px",
140
+ backgroundColor: color.surface,
141
+ border: `2px solid ${color.text}`,
142
+ color: color.text,
143
+ },
144
+ black: {
145
+ padding: "10px 20px",
146
+ fontSize: "24px",
147
+ fontWeight: 600,
148
+ fontFamily: font.mono,
149
+ borderRadius: "5px",
150
+ textAlign: "center" as const,
151
+ minWidth: "100px",
152
+ backgroundColor: color.dark,
153
+ border: `2px solid ${color.dark}`,
154
+ color: "#f0f0ec",
155
+ },
156
+ };
157
+
158
+ // ============================================================================
159
+ // Keyboard Hint Styles
160
+ // ============================================================================
161
+ const kbd = {
162
+ grid: {
163
+ display: "flex",
164
+ gap: "6px",
165
+ justifyContent: "center",
166
+ flexWrap: "wrap" as const,
167
+ },
168
+ item: {
169
+ display: "inline-flex",
170
+ alignItems: "center",
171
+ gap: "4px",
172
+ fontSize: "11px",
173
+ color: color.textSecondary,
174
+ },
175
+ key: {
176
+ display: "inline-flex",
177
+ alignItems: "center",
178
+ justifyContent: "center",
179
+ minWidth: "22px",
180
+ height: "22px",
181
+ padding: "0 5px",
182
+ backgroundColor: color.bg,
183
+ border: `1px solid ${color.border}`,
184
+ borderRadius: "3px",
185
+ fontFamily: font.mono,
186
+ fontSize: "11px",
187
+ fontWeight: 600,
188
+ color: color.text,
189
+ lineHeight: 1,
190
+ },
191
+ };
192
+
193
+ // ============================================================================
194
+ // Meta
195
+ // ============================================================================
7
196
  const meta = {
8
197
  title: "react-chess-game/Components/ChessGame",
9
198
  component: ChessGame.Root,
@@ -11,52 +200,402 @@ const meta = {
11
200
  argTypes: {},
12
201
  parameters: {
13
202
  actions: { argTypesRegex: "^_on.*" },
203
+ layout: "centered",
14
204
  },
15
- decorators: [
16
- (Story) => (
17
- <div style={{ width: "600px" }}>
18
- {/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */}
19
- <Story />
20
- </div>
21
- ),
22
- ],
23
205
  } satisfies Meta<typeof ChessGame.Root>;
24
206
 
25
207
  export default meta;
26
208
 
27
- // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
209
+ // ============================================================================
210
+ // Stories
211
+ // ============================================================================
28
212
 
29
- export const Default = () => {
30
- return (
31
- <ChessGame.Root>
32
- <ChessGame.KeyboardControls />
33
- <ChessGame.Board />
34
- </ChessGame.Root>
35
- );
36
- };
213
+ export const Default = () => (
214
+ <div style={s.container}>
215
+ <div style={s.header}>
216
+ <h3 style={s.title}>Standard Game</h3>
217
+ <p style={s.subtitle}>Interactive chess board with default settings</p>
218
+ </div>
219
+ <div style={s.board}>
220
+ <ChessGame.Root>
221
+ <ChessGame.KeyboardControls />
222
+ <ChessGame.Board />
223
+ </ChessGame.Root>
224
+ </div>
225
+ <p style={s.hint}>Arrow keys to navigate moves &middot; Press F to flip</p>
226
+ </div>
227
+ );
37
228
 
38
- export const WithSounds = () => {
39
- return (
40
- <ChessGame.Root>
41
- <ChessGame.Sounds />
42
- <ChessGame.Board />
43
- </ChessGame.Root>
44
- );
229
+ export const WithSounds = () => (
230
+ <div style={s.container}>
231
+ <div style={s.header}>
232
+ <h3 style={s.title}>Sound Effects</h3>
233
+ <p style={s.subtitle}>Audio feedback on every move</p>
234
+ </div>
235
+ <div style={s.board}>
236
+ <ChessGame.Root>
237
+ <ChessGame.Sounds />
238
+ <ChessGame.Board />
239
+ </ChessGame.Root>
240
+ </div>
241
+ <p style={s.hint}>Move pieces to hear sounds for each piece type</p>
242
+ </div>
243
+ );
244
+
245
+ export const WithKeyboardControls = () => (
246
+ <div style={s.container}>
247
+ <div style={s.header}>
248
+ <h3 style={s.title}>Keyboard Navigation</h3>
249
+ <p style={s.subtitle}>Custom keyboard shortcuts for game control</p>
250
+ </div>
251
+ <div style={s.board}>
252
+ <ChessGame.Root>
253
+ <ChessGame.KeyboardControls
254
+ controls={{
255
+ f: (ctx) => ctx.methods.flipBoard(),
256
+ w: (ctx) => ctx.methods.goToStart(),
257
+ s: (ctx) => ctx.methods.goToEnd(),
258
+ a: (ctx) => ctx.methods.goToPreviousMove(),
259
+ d: (ctx) => ctx.methods.goToNextMove(),
260
+ }}
261
+ />
262
+ <ChessGame.Board />
263
+ </ChessGame.Root>
264
+ </div>
265
+ <div style={kbd.grid}>
266
+ <span style={kbd.item}>
267
+ <kbd style={kbd.key}>W</kbd> Start
268
+ </span>
269
+ <span style={kbd.item}>
270
+ <kbd style={kbd.key}>A</kbd> Prev
271
+ </span>
272
+ <span style={kbd.item}>
273
+ <kbd style={kbd.key}>D</kbd> Next
274
+ </span>
275
+ <span style={kbd.item}>
276
+ <kbd style={kbd.key}>S</kbd> End
277
+ </span>
278
+ <span style={kbd.item}>
279
+ <kbd style={kbd.key}>F</kbd> Flip
280
+ </span>
281
+ </div>
282
+ </div>
283
+ );
284
+
285
+ // ============================================================================
286
+ // Clock Stories
287
+ // ============================================================================
288
+
289
+ const ClockDisplay = ({
290
+ label,
291
+ color: side,
292
+ ...props
293
+ }: { label: string; color: "white" | "black" } & Record<string, unknown>) => (
294
+ <div style={clock.cell}>
295
+ <span style={clock.label}>{label}</span>
296
+ <ChessGame.Clock.Display
297
+ color={side}
298
+ style={side === "white" ? clock.white : clock.black}
299
+ {...props}
300
+ />
301
+ </div>
302
+ );
303
+
304
+ export const WithClockBlitz = () => (
305
+ <ChessGame.Root timeControl={{ time: "5+3", clockStart: "immediate" }}>
306
+ <div style={s.container}>
307
+ <div style={s.header}>
308
+ <h3 style={s.title}>Blitz &middot; 5+3</h3>
309
+ <p style={s.subtitle}>5 minutes with 3-second increment</p>
310
+ </div>
311
+ <div style={clock.row}>
312
+ <ClockDisplay label="White" color="white" />
313
+ <ClockDisplay label="Black" color="black" />
314
+ </div>
315
+ <div style={s.board}>
316
+ <ChessGame.Board />
317
+ </div>
318
+ </div>
319
+ </ChessGame.Root>
320
+ );
321
+
322
+ export const WithClockBullet = () => (
323
+ <ChessGame.Root timeControl={{ time: "1+0", clockStart: "immediate" }}>
324
+ <div style={s.container}>
325
+ <div style={s.header}>
326
+ <h3 style={s.title}>Bullet &middot; 1+0</h3>
327
+ <p style={s.subtitle}>1 minute, no increment</p>
328
+ </div>
329
+ <div style={clock.row}>
330
+ <ClockDisplay label="White" color="white" format="ss.d" />
331
+ <ClockDisplay label="Black" color="black" format="ss.d" />
332
+ </div>
333
+ <div style={s.board}>
334
+ <ChessGame.Board />
335
+ </div>
336
+ </div>
337
+ </ChessGame.Root>
338
+ );
339
+
340
+ export const WithClockControls = () => (
341
+ <ChessGame.Root
342
+ timeControl={{ time: "3+2", clockStart: "immediate" }}
343
+ autoSwitchOnMove={false}
344
+ >
345
+ <div style={s.container}>
346
+ <div style={s.header}>
347
+ <h3 style={s.title}>Rapid &middot; 3+2</h3>
348
+ <p style={s.subtitle}>Manual clock controls enabled</p>
349
+ </div>
350
+ <div style={clock.row}>
351
+ <ClockDisplay label="White" color="white" />
352
+ <ClockDisplay label="Black" color="black" />
353
+ </div>
354
+ <div style={s.controls}>
355
+ <ChessGame.Clock.PlayPause style={{ ...s.btn, ...s.btnPrimary }}>
356
+ Play / Pause
357
+ </ChessGame.Clock.PlayPause>
358
+ <ChessGame.Clock.Switch style={s.btn}>Switch</ChessGame.Clock.Switch>
359
+ <ChessGame.Clock.Reset style={s.btn}>Reset</ChessGame.Clock.Reset>
360
+ </div>
361
+ <div style={s.board}>
362
+ <ChessGame.Board />
363
+ </div>
364
+ </div>
365
+ </ChessGame.Root>
366
+ );
367
+
368
+ // ============================================================================
369
+ // Server-Controlled Clock
370
+ // ============================================================================
371
+
372
+ const srv = {
373
+ panel: {
374
+ width: "100%",
375
+ padding: "14px 16px",
376
+ backgroundColor: color.warnLight,
377
+ border: `1px solid ${color.warnBorder}`,
378
+ borderRadius: "6px",
379
+ display: "flex",
380
+ flexDirection: "column" as const,
381
+ gap: "10px",
382
+ fontFamily: font.sans,
383
+ },
384
+ panelTitle: {
385
+ fontSize: "11px",
386
+ fontWeight: 700,
387
+ color: color.warn,
388
+ textTransform: "uppercase" as const,
389
+ letterSpacing: "0.06em",
390
+ margin: 0,
391
+ },
392
+ row: {
393
+ display: "flex",
394
+ gap: "8px",
395
+ alignItems: "center",
396
+ fontSize: "12px",
397
+ color: "#5a4e1a",
398
+ fontFamily: font.mono,
399
+ flexWrap: "wrap" as const,
400
+ },
401
+ badge: {
402
+ padding: "2px 8px",
403
+ borderRadius: "3px",
404
+ backgroundColor: "#f5edcf",
405
+ fontWeight: 600,
406
+ fontSize: "11px",
407
+ fontFamily: font.sans,
408
+ },
409
+ smallBtn: {
410
+ padding: "3px 8px",
411
+ fontSize: "11px",
412
+ fontWeight: 600,
413
+ cursor: "pointer",
414
+ border: `1px solid ${color.warnBorder}`,
415
+ borderRadius: "3px",
416
+ backgroundColor: "#f5edcf",
417
+ color: "#5a4e1a",
418
+ fontFamily: font.sans,
419
+ } as React.CSSProperties,
420
+ dangerBtn: {
421
+ padding: "3px 8px",
422
+ fontSize: "11px",
423
+ fontWeight: 600,
424
+ cursor: "pointer",
425
+ border: `1px solid ${color.dangerBorder}`,
426
+ borderRadius: "3px",
427
+ backgroundColor: color.dangerLight,
428
+ color: color.danger,
429
+ fontFamily: font.sans,
430
+ } as React.CSSProperties,
431
+ slider: {
432
+ display: "flex",
433
+ alignItems: "center",
434
+ gap: "8px",
435
+ fontSize: "12px",
436
+ color: "#5a4e1a",
437
+ fontFamily: font.sans,
438
+ },
45
439
  };
46
440
 
47
- export const WithKeyboardControls = () => {
441
+ export const WithServerControlledClock = () => {
442
+ const INITIAL_TIME = 30 * 1000;
443
+ const INCREMENT = 2 * 1000;
444
+ const { clientView, lagMs, setLagMs, serverMove, serverReset, addTime } =
445
+ useSimulatedServer(INITIAL_TIME, INCREMENT);
446
+
48
447
  return (
49
- <ChessGame.Root>
50
- <ChessGame.KeyboardControls
51
- controls={{
52
- f: (context) => context.methods.flipBoard(),
53
- w: (context) => context.methods.goToStart(),
54
- s: (context) => context.methods.goToEnd(),
55
- a: (context) => context.methods.goToPreviousMove(),
56
- d: (context) => context.methods.goToNextMove(),
448
+ <ChessGame.Root
449
+ timeControl={{
450
+ time: { baseTime: 30, increment: 2 },
451
+ clockStart: "immediate",
452
+ onTimeout: (loser) => console.log(`Server: ${loser} flagged`),
453
+ }}
454
+ autoSwitchOnMove={false}
455
+ >
456
+ <ServerTimeSync
457
+ serverTimes={{
458
+ white: clientView.whiteTime,
459
+ black: clientView.blackTime,
57
460
  }}
58
461
  />
59
- <ChessGame.Board />
462
+ <div style={s.container}>
463
+ <div style={s.header}>
464
+ <h3 style={s.title}>Server-Synced Clock</h3>
465
+ <p style={s.subtitle}>
466
+ Server-authoritative times synced via setTime()
467
+ </p>
468
+ </div>
469
+
470
+ {/* Server control panel */}
471
+ <div style={srv.panel}>
472
+ <p style={srv.panelTitle}>Server Controls</p>
473
+
474
+ <div style={srv.row}>
475
+ <span>
476
+ W: <b>{(clientView.whiteTime / 1000).toFixed(1)}s</b>
477
+ </span>
478
+ <span>
479
+ B: <b>{(clientView.blackTime / 1000).toFixed(1)}s</b>
480
+ </span>
481
+ <span style={srv.badge}>
482
+ {clientView.finished
483
+ ? "finished"
484
+ : clientView.running
485
+ ? `${clientView.activePlayer} to move`
486
+ : "waiting"}
487
+ </span>
488
+ </div>
489
+
490
+ <div style={srv.row}>
491
+ <span style={{ fontWeight: 600, fontFamily: font.sans }}>W:</span>
492
+ <button
493
+ style={srv.smallBtn}
494
+ onClick={() => addTime("white", 15000)}
495
+ >
496
+ +15s
497
+ </button>
498
+ <button
499
+ style={srv.dangerBtn}
500
+ onClick={() => addTime("white", -15000)}
501
+ >
502
+ -15s
503
+ </button>
504
+ <span
505
+ style={{
506
+ fontWeight: 600,
507
+ fontFamily: font.sans,
508
+ marginLeft: "4px",
509
+ }}
510
+ >
511
+ B:
512
+ </span>
513
+ <button
514
+ style={srv.smallBtn}
515
+ onClick={() => addTime("black", 15000)}
516
+ >
517
+ +15s
518
+ </button>
519
+ <button
520
+ style={srv.dangerBtn}
521
+ onClick={() => addTime("black", -15000)}
522
+ >
523
+ -15s
524
+ </button>
525
+ </div>
526
+
527
+ <div style={srv.slider}>
528
+ <span style={{ fontWeight: 600 }}>Lag</span>
529
+ <input
530
+ type="range"
531
+ min={0}
532
+ max={3000}
533
+ step={100}
534
+ value={lagMs}
535
+ onChange={(e) => setLagMs(Number(e.target.value))}
536
+ style={{ flex: 1 }}
537
+ />
538
+ <span
539
+ style={{
540
+ fontFamily: font.mono,
541
+ fontSize: "11px",
542
+ fontWeight: 600,
543
+ minWidth: "40px",
544
+ textAlign: "right" as const,
545
+ }}
546
+ >
547
+ {lagMs}ms
548
+ </span>
549
+ </div>
550
+ </div>
551
+
552
+ <div style={clock.row}>
553
+ <ClockDisplay label="White" color="white" />
554
+ <ClockDisplay label="Black" color="black" />
555
+ </div>
556
+
557
+ <ServerMoveDetector onMove={serverMove} />
558
+ <div style={s.board}>
559
+ <ChessGame.Board />
560
+ </div>
561
+
562
+ <div style={s.controls}>
563
+ <button style={s.btn} onClick={serverReset}>
564
+ Reset Server
565
+ </button>
566
+ </div>
567
+
568
+ <p style={s.hint}>
569
+ Use +/- to change server time. Set lag &gt; 0 and make moves to see
570
+ client interpolation with delayed server corrections.
571
+ </p>
572
+ </div>
60
573
  </ChessGame.Root>
61
574
  );
62
575
  };
576
+
577
+ export const WithClockMultiPeriod = () => (
578
+ <ChessGame.Root
579
+ timeControl={{
580
+ time: [
581
+ { baseTime: 120, increment: 0, moves: 5 },
582
+ { baseTime: 60, increment: 0 },
583
+ ],
584
+ clockStart: "immediate",
585
+ }}
586
+ >
587
+ <div style={s.container}>
588
+ <div style={s.header}>
589
+ <h3 style={s.title}>Multi-Period Tournament</h3>
590
+ <p style={s.subtitle}>2 min (5 moves) then 1 min sudden death</p>
591
+ </div>
592
+ <div style={clock.row}>
593
+ <ClockDisplay label="White" color="white" format="mm:ss" />
594
+ <ClockDisplay label="Black" color="black" format="mm:ss" />
595
+ </div>
596
+ <div style={s.board}>
597
+ <ChessGame.Board />
598
+ </div>
599
+ </div>
600
+ </ChessGame.Root>
601
+ );