@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.
- package/CHANGELOG.md +14 -0
- package/README.md +569 -0
- package/dist/index.cjs +265 -184
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -4
- package/dist/index.d.ts +48 -4
- package/dist/index.js +271 -185
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/components/ChessGame/ChessGame.stories.helpers.tsx +181 -0
- package/src/components/ChessGame/ChessGame.stories.tsx +574 -35
- package/src/components/ChessGame/Clock/index.tsx +174 -0
- package/src/components/ChessGame/Theme.stories.tsx +2 -2
- package/src/components/ChessGame/index.ts +2 -0
- package/src/components/ChessGame/parts/Board.tsx +214 -205
- package/src/components/ChessGame/parts/KeyboardControls.tsx +9 -0
- package/src/components/ChessGame/parts/Root.tsx +15 -1
- package/src/components/ChessGame/parts/Sounds.tsx +9 -0
- package/src/components/ChessGame/parts/__tests__/Board.test.tsx +122 -0
- package/src/components/ChessGame/parts/__tests__/KeyboardControls.test.tsx +34 -0
- package/src/components/ChessGame/parts/__tests__/Root.test.tsx +50 -0
- package/src/components/ChessGame/parts/__tests__/Sounds.test.tsx +22 -0
- package/src/hooks/useChessGame.ts +50 -12
- package/src/index.ts +10 -0
- package/src/theme/__tests__/context.test.tsx +0 -15
- package/README.MD +0 -190
|
@@ -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
|
|
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
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
if (!gameContext) {
|
|
27
|
+
throw new Error("ChessGameContext not found");
|
|
28
|
+
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
const {
|
|
31
|
+
game,
|
|
32
|
+
currentFen,
|
|
33
|
+
orientation,
|
|
34
|
+
info,
|
|
35
|
+
isLatestMove,
|
|
36
|
+
methods: { makeMove },
|
|
37
|
+
} = gameContext;
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
const { turn, isGameOver } = info;
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
const [activeSquare, setActiveSquare] = React.useState<Square | null>(null);
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
const [promotionMove, setPromotionMove] =
|
|
44
|
+
React.useState<Partial<Move> | null>(null);
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
const onSquareClick = (square: Square) => {
|
|
47
|
+
if (isGameOver) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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";
|