@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.
@@ -0,0 +1,174 @@
1
+ import React from "react";
2
+ import { useChessGameContext } from "../../../hooks/useChessGameContext";
3
+ import {
4
+ Display as ClockDisplay,
5
+ Switch as ClockSwitch,
6
+ PlayPause as ClockPlayPause,
7
+ Reset as ClockReset,
8
+ ChessClockContext,
9
+ } from "@react-chess-tools/react-chess-clock";
10
+ import type {
11
+ ChessClockDisplayProps,
12
+ ChessClockControlProps,
13
+ ChessClockPlayPauseProps,
14
+ ChessClockResetProps,
15
+ ClockColor,
16
+ } from "@react-chess-tools/react-chess-clock";
17
+
18
+ export type {
19
+ ChessClockDisplayProps,
20
+ ChessClockControlProps,
21
+ ChessClockPlayPauseProps,
22
+ ChessClockResetProps,
23
+ ClockColor,
24
+ } from "@react-chess-tools/react-chess-clock";
25
+
26
+ export interface ClockDisplayProps extends Omit<
27
+ ChessClockDisplayProps,
28
+ "color"
29
+ > {
30
+ color: ClockColor;
31
+ }
32
+
33
+ /**
34
+ * ChessGame.Clock.Display - Autonomous display component
35
+ *
36
+ * Wraps react-chess-clock's Display component, providing clock state from ChessGame.Root context.
37
+ * No need to pass timeControl - it's inherited from the root.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * <ChessGame.Root timeControl={{ time: "5+3" }}>
42
+ * <ChessGame.Clock.Display color="white" />
43
+ * <ChessGame.Board />
44
+ * <ChessGame.Clock.Display color="black" />
45
+ * </ChessGame.Root>
46
+ * ```
47
+ */
48
+ export const Display = React.forwardRef<HTMLDivElement, ClockDisplayProps>(
49
+ ({ color, ...rest }, ref) => {
50
+ const { clock } = useChessGameContext();
51
+
52
+ if (!clock) {
53
+ return null;
54
+ }
55
+
56
+ return (
57
+ <ChessClockContext.Provider value={clock}>
58
+ <ClockDisplay ref={ref} color={color} {...rest} />
59
+ </ChessClockContext.Provider>
60
+ );
61
+ },
62
+ );
63
+
64
+ Display.displayName = "ChessGame.Clock.Display";
65
+
66
+ /**
67
+ * ChessGame.Clock.Switch - Manual switch control
68
+ *
69
+ * Wraps react-chess-clock's Switch component, providing clock state from ChessGame.Root context.
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * <ChessGame.Root timeControl={{ time: "5+3" }}>
74
+ * <ChessGame.Clock.Switch>Switch Clock</ChessGame.Clock.Switch>
75
+ * </ChessGame.Root>
76
+ * ```
77
+ */
78
+ export const Switch = React.forwardRef<
79
+ HTMLElement,
80
+ React.PropsWithChildren<ChessClockControlProps>
81
+ >(({ children, ...rest }, ref) => {
82
+ const { clock } = useChessGameContext();
83
+
84
+ if (!clock) {
85
+ return null;
86
+ }
87
+
88
+ return (
89
+ <ChessClockContext.Provider value={clock}>
90
+ <ClockSwitch ref={ref} {...rest}>
91
+ {children}
92
+ </ClockSwitch>
93
+ </ChessClockContext.Provider>
94
+ );
95
+ });
96
+
97
+ Switch.displayName = "ChessGame.Clock.Switch";
98
+
99
+ /**
100
+ * ChessGame.Clock.PlayPause - Play/pause control
101
+ *
102
+ * Wraps react-chess-clock's PlayPause component, providing clock state from ChessGame.Root context.
103
+ *
104
+ * @example
105
+ * ```tsx
106
+ * <ChessGame.Root timeControl={{ time: "5+3" }}>
107
+ * <ChessGame.Clock.PlayPause
108
+ * startContent="Start"
109
+ * pauseContent="Pause"
110
+ * resumeContent="Resume"
111
+ * />
112
+ * </ChessGame.Root>
113
+ * ```
114
+ */
115
+ export const PlayPause = React.forwardRef<
116
+ HTMLElement,
117
+ React.PropsWithChildren<ChessClockPlayPauseProps>
118
+ >(({ children, ...rest }, ref) => {
119
+ const { clock } = useChessGameContext();
120
+
121
+ if (!clock) {
122
+ return null;
123
+ }
124
+
125
+ return (
126
+ <ChessClockContext.Provider value={clock}>
127
+ <ClockPlayPause ref={ref} {...rest}>
128
+ {children}
129
+ </ClockPlayPause>
130
+ </ChessClockContext.Provider>
131
+ );
132
+ });
133
+
134
+ PlayPause.displayName = "ChessGame.Clock.PlayPause";
135
+
136
+ /**
137
+ * ChessGame.Clock.Reset - Reset control
138
+ *
139
+ * Wraps react-chess-clock's Reset component, providing clock state from ChessGame.Root context.
140
+ *
141
+ * @example
142
+ * ```tsx
143
+ * <ChessGame.Root timeControl={{ time: "5+3" }}>
144
+ * <ChessGame.Clock.Reset>Reset</ChessGame.Clock.Reset>
145
+ * </ChessGame.Root>
146
+ * ```
147
+ */
148
+ export const Reset = React.forwardRef<
149
+ HTMLElement,
150
+ React.PropsWithChildren<ChessClockResetProps>
151
+ >(({ children, ...rest }, ref) => {
152
+ const { clock } = useChessGameContext();
153
+
154
+ if (!clock) {
155
+ return null;
156
+ }
157
+
158
+ return (
159
+ <ChessClockContext.Provider value={clock}>
160
+ <ClockReset ref={ref} {...rest}>
161
+ {children}
162
+ </ClockReset>
163
+ </ChessClockContext.Provider>
164
+ );
165
+ });
166
+
167
+ Reset.displayName = "ChessGame.Clock.Reset";
168
+
169
+ export const Clock = {
170
+ Display,
171
+ Switch,
172
+ PlayPause,
173
+ Reset,
174
+ };
@@ -1,8 +1,8 @@
1
- import type { Meta, StoryObj } from "@storybook/react";
1
+ import type { Meta } from "@storybook/react";
2
2
  import React, { useState } from "react";
3
3
  import { ChessGame } from "./index";
4
4
  import { defaultGameTheme, themes } from "../../theme";
5
- import type { ChessGameTheme, PartialChessGameTheme } from "../../theme/types";
5
+ import type { ChessGameTheme } from "../../theme/types";
6
6
 
7
7
  const meta = {
8
8
  title: "react-chess-game/Theme/Playground",
@@ -2,10 +2,12 @@ import { Root } from "./parts/Root";
2
2
  import { Board } from "./parts/Board";
3
3
  import { Sounds } from "./parts/Sounds";
4
4
  import { KeyboardControls } from "./parts/KeyboardControls";
5
+ import { Clock } from "./Clock";
5
6
 
6
7
  export const ChessGame = {
7
8
  Root,
8
9
  Board,
9
10
  Sounds,
10
11
  KeyboardControls,
12
+ Clock,
11
13
  };
@@ -14,229 +14,238 @@ import { isLegalMove, requiresPromotion } from "../../../utils/chess";
14
14
  import { useChessGameContext } from "../../../hooks/useChessGameContext";
15
15
  import { useChessGameTheme } from "../../../theme/context";
16
16
 
17
- export interface ChessGameProps {
17
+ export interface ChessGameProps extends React.HTMLAttributes<HTMLDivElement> {
18
18
  options?: ChessboardOptions;
19
19
  }
20
20
 
21
- export const Board: React.FC<ChessGameProps> = ({ options = {} }) => {
22
- const gameContext = useChessGameContext();
23
- const theme = useChessGameTheme();
21
+ export const Board = React.forwardRef<HTMLDivElement, ChessGameProps>(
22
+ ({ options = {}, className, style: userStyle, ...rest }, ref) => {
23
+ const gameContext = useChessGameContext();
24
+ const theme = useChessGameTheme();
24
25
 
25
- if (!gameContext) {
26
- throw new Error("ChessGameContext not found");
27
- }
26
+ if (!gameContext) {
27
+ throw new Error("ChessGameContext not found");
28
+ }
28
29
 
29
- const {
30
- game,
31
- currentFen,
32
- orientation,
33
- info,
34
- isLatestMove,
35
- methods: { makeMove },
36
- } = gameContext;
30
+ const {
31
+ game,
32
+ currentFen,
33
+ orientation,
34
+ info,
35
+ isLatestMove,
36
+ methods: { makeMove },
37
+ } = gameContext;
37
38
 
38
- const { turn, isGameOver } = info;
39
+ const { turn, isGameOver } = info;
39
40
 
40
- const [activeSquare, setActiveSquare] = React.useState<Square | null>(null);
41
+ const [activeSquare, setActiveSquare] = React.useState<Square | null>(null);
41
42
 
42
- const [promotionMove, setPromotionMove] =
43
- React.useState<Partial<Move> | null>(null);
43
+ const [promotionMove, setPromotionMove] =
44
+ React.useState<Partial<Move> | null>(null);
44
45
 
45
- const onSquareClick = (square: Square) => {
46
- if (isGameOver) {
47
- return;
48
- }
46
+ const onSquareClick = (square: Square) => {
47
+ if (isGameOver) {
48
+ return;
49
+ }
49
50
 
50
- if (activeSquare === null) {
51
- const squadreInfo = game.get(square);
52
- if (squadreInfo && squadreInfo.color === turn) {
53
- return setActiveSquare(square);
51
+ if (activeSquare === null) {
52
+ const squadreInfo = game.get(square);
53
+ if (squadreInfo && squadreInfo.color === turn) {
54
+ return setActiveSquare(square);
55
+ }
56
+ return;
54
57
  }
55
- return;
56
- }
57
58
 
58
- if (
59
- !isLegalMove(game, {
60
- from: activeSquare,
61
- to: square,
62
- promotion: "q",
63
- })
64
- ) {
65
- return setActiveSquare(null);
66
- }
59
+ if (
60
+ !isLegalMove(game, {
61
+ from: activeSquare,
62
+ to: square,
63
+ promotion: "q",
64
+ })
65
+ ) {
66
+ return setActiveSquare(null);
67
+ }
67
68
 
68
- if (
69
- requiresPromotion(game, {
70
- from: activeSquare,
71
- to: square,
72
- promotion: "q",
73
- })
74
- ) {
75
- return setPromotionMove({
69
+ if (
70
+ requiresPromotion(game, {
71
+ from: activeSquare,
72
+ to: square,
73
+ promotion: "q",
74
+ })
75
+ ) {
76
+ return setPromotionMove({
77
+ from: activeSquare,
78
+ to: square,
79
+ });
80
+ }
81
+
82
+ setActiveSquare(null);
83
+ makeMove({
76
84
  from: activeSquare,
77
85
  to: square,
78
86
  });
79
- }
80
-
81
- setActiveSquare(null);
82
- makeMove({
83
- from: activeSquare,
84
- to: square,
85
- });
86
- };
87
+ };
88
+
89
+ const onPromotionPieceSelect = (piece: string): void => {
90
+ if (promotionMove?.from && promotionMove?.to) {
91
+ makeMove({
92
+ from: promotionMove.from,
93
+ to: promotionMove.to,
94
+ promotion: piece.toLowerCase(),
95
+ });
96
+ setPromotionMove(null);
97
+ }
98
+ };
87
99
 
88
- const onPromotionPieceSelect = (piece: string): void => {
89
- if (promotionMove?.from && promotionMove?.to) {
90
- makeMove({
91
- from: promotionMove.from,
92
- to: promotionMove.to,
93
- promotion: piece.toLowerCase(),
94
- });
100
+ const onSquareRightClick = () => {
101
+ setActiveSquare(null);
95
102
  setPromotionMove(null);
96
- }
97
- };
98
-
99
- const onSquareRightClick = () => {
100
- setActiveSquare(null);
101
- setPromotionMove(null);
102
- };
103
-
104
- // Calculate square width for precise positioning
105
- const squareWidth = React.useMemo(() => {
106
- if (typeof document === "undefined") return 80;
107
- const squareElement = document.querySelector(`[data-square]`);
108
- return squareElement?.getBoundingClientRect()?.width ?? 80;
109
- }, [promotionMove]);
110
-
111
- // Calculate promotion square position
112
- const promotionSquareLeft = React.useMemo(() => {
113
- if (!promotionMove?.to) return 0;
114
- const column = promotionMove.to.match(/^[a-h]/)?.[0] ?? "a";
103
+ };
104
+
105
+ // Calculate square width for precise positioning
106
+ const squareWidth = React.useMemo(() => {
107
+ if (typeof document === "undefined") return 80;
108
+ const squareElement = document.querySelector(`[data-square]`);
109
+ return squareElement?.getBoundingClientRect()?.width ?? 80;
110
+ }, [promotionMove]);
111
+
112
+ // Calculate promotion square position
113
+ const promotionSquareLeft = React.useMemo(() => {
114
+ if (!promotionMove?.to) return 0;
115
+ const column = promotionMove.to.match(/^[a-h]/)?.[0] ?? "a";
116
+ return (
117
+ squareWidth *
118
+ chessColumnToColumnIndex(
119
+ column,
120
+ 8,
121
+ orientation === "b" ? "black" : "white",
122
+ )
123
+ );
124
+ }, [promotionMove, squareWidth, orientation]);
125
+
126
+ const baseOptions: ChessboardOptions = {
127
+ squareStyles: getCustomSquareStyles(game, info, activeSquare, theme),
128
+ boardOrientation: orientation === "b" ? "black" : "white",
129
+ position: currentFen,
130
+ showNotation: true,
131
+ showAnimations: isLatestMove,
132
+ lightSquareStyle: theme.board.lightSquare,
133
+ darkSquareStyle: theme.board.darkSquare,
134
+ canDragPiece: ({ piece }) => {
135
+ if (isGameOver) return false;
136
+ return piece.pieceType[0] === turn;
137
+ },
138
+ dropSquareStyle: theme.state.dropSquare,
139
+ onPieceDrag: ({ piece, square }) => {
140
+ if (piece.pieceType[0] === turn) {
141
+ setActiveSquare(square as Square);
142
+ }
143
+ },
144
+ onPieceDrop: ({ sourceSquare, targetSquare }) => {
145
+ setActiveSquare(null);
146
+ const moveData = {
147
+ from: sourceSquare as Square,
148
+ to: targetSquare as Square,
149
+ };
150
+
151
+ // Check if promotion is needed
152
+ if (requiresPromotion(game, { ...moveData, promotion: "q" })) {
153
+ setPromotionMove(moveData);
154
+ return false; // Prevent the move until promotion is selected
155
+ }
156
+
157
+ return makeMove(moveData);
158
+ },
159
+ onSquareClick: ({ square }) => {
160
+ if (square.match(/^[a-h][1-8]$/)) {
161
+ onSquareClick(square as Square);
162
+ }
163
+ },
164
+ onSquareRightClick: onSquareRightClick,
165
+ allowDrawingArrows: true,
166
+ animationDurationInMs: game.history().length === 0 ? 0 : 300,
167
+ };
168
+
169
+ const mergedOptions = deepMergeChessboardOptions(baseOptions, options);
170
+
171
+ const mergedStyle = {
172
+ ...userStyle,
173
+ position: "relative" as const,
174
+ };
175
+
115
176
  return (
116
- squareWidth *
117
- chessColumnToColumnIndex(
118
- column,
119
- 8,
120
- orientation === "b" ? "black" : "white",
121
- )
177
+ <div ref={ref} className={className} style={mergedStyle} {...rest}>
178
+ <Chessboard options={mergedOptions} />
179
+ {promotionMove && (
180
+ <>
181
+ {/* Backdrop overlay - click to cancel */}
182
+ <div
183
+ onClick={() => setPromotionMove(null)}
184
+ onContextMenu={(e) => {
185
+ e.preventDefault();
186
+ setPromotionMove(null);
187
+ }}
188
+ style={{
189
+ position: "absolute",
190
+ top: 0,
191
+ left: 0,
192
+ right: 0,
193
+ bottom: 0,
194
+ backgroundColor: "rgba(0, 0, 0, 0.1)",
195
+ zIndex: 1000,
196
+ }}
197
+ />
198
+ {/* Promotion piece selection */}
199
+ <div
200
+ style={{
201
+ position: "absolute",
202
+ top: promotionMove.to?.[1]?.includes("8") ? 0 : "auto",
203
+ bottom: promotionMove.to?.[1].includes("1") ? 0 : "auto",
204
+ left: promotionSquareLeft,
205
+ backgroundColor: "white",
206
+ width: squareWidth,
207
+ zIndex: 1001,
208
+ display: "flex",
209
+ flexDirection: "column",
210
+ boxShadow: "0 0 10px 0 rgba(0, 0, 0, 0.5)",
211
+ }}
212
+ >
213
+ {["q", "r", "n", "b"].map((piece) => (
214
+ <button
215
+ key={piece}
216
+ onClick={() => onPromotionPieceSelect(piece)}
217
+ onContextMenu={(e) => {
218
+ e.preventDefault();
219
+ }}
220
+ style={{
221
+ width: "100%",
222
+ aspectRatio: "1",
223
+ display: "flex",
224
+ alignItems: "center",
225
+ justifyContent: "center",
226
+ padding: 0,
227
+ border: "none",
228
+ cursor: "pointer",
229
+ backgroundColor: "white",
230
+ }}
231
+ onMouseEnter={(e) => {
232
+ e.currentTarget.style.backgroundColor = "#f0f0f0";
233
+ }}
234
+ onMouseLeave={(e) => {
235
+ e.currentTarget.style.backgroundColor = "white";
236
+ }}
237
+ >
238
+ {defaultPieces[
239
+ `${turn}${piece.toUpperCase()}` as keyof typeof defaultPieces
240
+ ]()}
241
+ </button>
242
+ ))}
243
+ </div>
244
+ </>
245
+ )}
246
+ </div>
122
247
  );
123
- }, [promotionMove, squareWidth, orientation]);
124
-
125
- const baseOptions: ChessboardOptions = {
126
- squareStyles: getCustomSquareStyles(game, info, activeSquare, theme),
127
- boardOrientation: orientation === "b" ? "black" : "white",
128
- position: currentFen,
129
- showNotation: true,
130
- showAnimations: isLatestMove,
131
- lightSquareStyle: theme.board.lightSquare,
132
- darkSquareStyle: theme.board.darkSquare,
133
- canDragPiece: ({ piece }) => {
134
- if (isGameOver) return false;
135
- return piece.pieceType[0] === turn;
136
- },
137
- dropSquareStyle: theme.state.dropSquare,
138
- onPieceDrag: ({ piece, square }) => {
139
- if (piece.pieceType[0] === turn) {
140
- setActiveSquare(square as Square);
141
- }
142
- },
143
- onPieceDrop: ({ sourceSquare, targetSquare }) => {
144
- setActiveSquare(null);
145
- const moveData = {
146
- from: sourceSquare as Square,
147
- to: targetSquare as Square,
148
- };
149
-
150
- // Check if promotion is needed
151
- if (requiresPromotion(game, { ...moveData, promotion: "q" })) {
152
- setPromotionMove(moveData);
153
- return false; // Prevent the move until promotion is selected
154
- }
248
+ },
249
+ );
155
250
 
156
- return makeMove(moveData);
157
- },
158
- onSquareClick: ({ square }) => {
159
- if (square.match(/^[a-h][1-8]$/)) {
160
- onSquareClick(square as Square);
161
- }
162
- },
163
- onSquareRightClick: onSquareRightClick,
164
- allowDrawingArrows: true,
165
- animationDurationInMs: game.history().length === 0 ? 0 : 300,
166
- };
167
-
168
- const mergedOptions = deepMergeChessboardOptions(baseOptions, options);
169
-
170
- return (
171
- <div style={{ position: "relative" }}>
172
- <Chessboard options={mergedOptions} />
173
- {promotionMove && (
174
- <>
175
- {/* Backdrop overlay - click to cancel */}
176
- <div
177
- onClick={() => setPromotionMove(null)}
178
- onContextMenu={(e) => {
179
- e.preventDefault();
180
- setPromotionMove(null);
181
- }}
182
- style={{
183
- position: "absolute",
184
- top: 0,
185
- left: 0,
186
- right: 0,
187
- bottom: 0,
188
- backgroundColor: "rgba(0, 0, 0, 0.1)",
189
- zIndex: 1000,
190
- }}
191
- />
192
- {/* Promotion piece selection */}
193
- <div
194
- style={{
195
- position: "absolute",
196
- top: promotionMove.to?.[1]?.includes("8") ? 0 : "auto",
197
- bottom: promotionMove.to?.[1].includes("1") ? 0 : "auto",
198
- left: promotionSquareLeft,
199
- backgroundColor: "white",
200
- width: squareWidth,
201
- zIndex: 1001,
202
- display: "flex",
203
- flexDirection: "column",
204
- boxShadow: "0 0 10px 0 rgba(0, 0, 0, 0.5)",
205
- }}
206
- >
207
- {["q", "r", "n", "b"].map((piece) => (
208
- <button
209
- key={piece}
210
- onClick={() => onPromotionPieceSelect(piece)}
211
- onContextMenu={(e) => {
212
- e.preventDefault();
213
- }}
214
- style={{
215
- width: "100%",
216
- aspectRatio: "1",
217
- display: "flex",
218
- alignItems: "center",
219
- justifyContent: "center",
220
- padding: 0,
221
- border: "none",
222
- cursor: "pointer",
223
- backgroundColor: "white",
224
- }}
225
- onMouseEnter={(e) => {
226
- e.currentTarget.style.backgroundColor = "#f0f0f0";
227
- }}
228
- onMouseLeave={(e) => {
229
- e.currentTarget.style.backgroundColor = "white";
230
- }}
231
- >
232
- {defaultPieces[
233
- `${turn}${piece.toUpperCase()}` as keyof typeof defaultPieces
234
- ]()}
235
- </button>
236
- ))}
237
- </div>
238
- </>
239
- )}
240
- </div>
241
- );
242
- };
251
+ Board.displayName = "ChessGame.Board";
@@ -16,6 +16,13 @@ export const defaultKeyboardControls: KeyboardControls = {
16
16
  ArrowDown: (context) => context.methods.goToEnd(),
17
17
  };
18
18
 
19
+ /**
20
+ * Props for the KeyboardControls component
21
+ *
22
+ * Note: This is a logic-only component that returns null and does not render
23
+ * any DOM elements. It sets up keyboard controls via the useKeyboardControls hook.
24
+ * Therefore, it does not accept HTML attributes like className, style, etc.
25
+ */
19
26
  type KeyboardControlsProps = {
20
27
  controls?: KeyboardControls;
21
28
  };
@@ -31,3 +38,5 @@ export const KeyboardControls: React.FC<KeyboardControlsProps> = ({
31
38
  useKeyboardControls(keyboardControls);
32
39
  return null;
33
40
  };
41
+
42
+ KeyboardControls.displayName = "ChessGame.KeyboardControls";