react-native-chess-kit 0.5.2 → 0.5.4
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/README.md +19 -0
- package/lib/commonjs/board-arrows.js +7 -7
- package/lib/commonjs/board-coordinates.js +8 -8
- package/lib/commonjs/board.js +48 -39
- package/lib/commonjs/board.js.map +1 -1
- package/lib/commonjs/promotion-picker.js +9 -9
- package/lib/commonjs/promotion-picker.js.map +1 -1
- package/lib/commonjs/static-board.js +7 -7
- package/lib/commonjs/static-board.js.map +1 -1
- package/lib/commonjs/use-board-gesture.js +26 -26
- package/lib/commonjs/use-board-gesture.js.map +1 -1
- package/lib/commonjs/use-board-state.js +13 -4
- package/lib/commonjs/use-board-state.js.map +1 -1
- package/lib/module/board-arrows.js +7 -7
- package/lib/module/board-coordinates.js +8 -8
- package/lib/module/board.js +48 -39
- package/lib/module/board.js.map +1 -1
- package/lib/module/promotion-picker.js +9 -9
- package/lib/module/promotion-picker.js.map +1 -1
- package/lib/module/static-board.js +7 -7
- package/lib/module/static-board.js.map +1 -1
- package/lib/module/use-board-gesture.js +26 -26
- package/lib/module/use-board-gesture.js.map +1 -1
- package/lib/module/use-board-state.js +13 -4
- package/lib/module/use-board-state.js.map +1 -1
- package/lib/typescript/board-coordinates.d.ts.map +1 -1
- package/lib/typescript/board.d.ts.map +1 -1
- package/lib/typescript/promotion-picker.d.ts.map +1 -1
- package/lib/typescript/static-board.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +14 -3
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/use-board-gesture.d.ts.map +1 -1
- package/lib/typescript/use-board-state.d.ts.map +1 -1
- package/package.json +95 -75
- package/src/board-arrows.tsx +197 -197
- package/src/board-coordinates.tsx +192 -192
- package/src/board.tsx +59 -56
- package/src/promotion-picker.tsx +147 -147
- package/src/static-board.tsx +186 -187
- package/src/types.ts +27 -6
- package/src/use-board-gesture.ts +459 -429
- package/src/use-board-state.ts +23 -14
|
@@ -1,192 +1,192 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { View, Text } from 'react-native';
|
|
3
|
-
|
|
4
|
-
import type { ChessColor
|
|
5
|
-
|
|
6
|
-
type BoardCoordinatesProps = {
|
|
7
|
-
boardSize: number;
|
|
8
|
-
orientation: ChessColor;
|
|
9
|
-
lightColor: string;
|
|
10
|
-
darkColor: string;
|
|
11
|
-
withLetters: boolean;
|
|
12
|
-
withNumbers: boolean;
|
|
13
|
-
/** 'inside' overlays on edge squares, 'outside' renders in a gutter area. */
|
|
14
|
-
position?: 'inside' | 'outside';
|
|
15
|
-
/** Gutter width in pixels (only used when position='outside'). */
|
|
16
|
-
gutterWidth?: number;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const FILES_WHITE = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
|
|
20
|
-
const FILES_BLACK = ['h', 'g', 'f', 'e', 'd', 'c', 'b', 'a'];
|
|
21
|
-
const RANKS_WHITE = ['8', '7', '6', '5', '4', '3', '2', '1'];
|
|
22
|
-
const RANKS_BLACK = ['1', '2', '3', '4', '5', '6', '7', '8'];
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* File letters (a-h) and rank numbers (1-8) drawn on or around the board.
|
|
26
|
-
*
|
|
27
|
-
* Two modes:
|
|
28
|
-
* - **inside** (default): absolute-positioned inside each edge square, colors
|
|
29
|
-
* alternate to contrast with the square behind them.
|
|
30
|
-
* - **outside**: rendered in a gutter area around the board. Rank numbers to
|
|
31
|
-
* the left, file letters along the bottom. Uses the dark square color.
|
|
32
|
-
*/
|
|
33
|
-
export const BoardCoordinates = React.memo(function BoardCoordinates({
|
|
34
|
-
boardSize,
|
|
35
|
-
orientation,
|
|
36
|
-
lightColor,
|
|
37
|
-
darkColor,
|
|
38
|
-
withLetters,
|
|
39
|
-
withNumbers,
|
|
40
|
-
position = 'inside',
|
|
41
|
-
gutterWidth = 0,
|
|
42
|
-
}: BoardCoordinatesProps) {
|
|
43
|
-
if (!withLetters && !withNumbers) return null;
|
|
44
|
-
|
|
45
|
-
const squareSize = boardSize / 8;
|
|
46
|
-
const files = orientation === 'white' ? FILES_WHITE : FILES_BLACK;
|
|
47
|
-
const ranks = orientation === 'white' ? RANKS_WHITE : RANKS_BLACK;
|
|
48
|
-
|
|
49
|
-
// ── Outside mode: labels in gutter area around the board ──
|
|
50
|
-
if (position === 'outside') {
|
|
51
|
-
const fontSize = gutterWidth * 0.65;
|
|
52
|
-
const textColor = darkColor;
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<>
|
|
56
|
-
{/* Rank numbers — left gutter, vertically centered on each row */}
|
|
57
|
-
{withNumbers && (
|
|
58
|
-
<View
|
|
59
|
-
style={{
|
|
60
|
-
position: 'absolute',
|
|
61
|
-
left: 0,
|
|
62
|
-
top: 0,
|
|
63
|
-
width: gutterWidth,
|
|
64
|
-
height: boardSize,
|
|
65
|
-
}}
|
|
66
|
-
pointerEvents="none"
|
|
67
|
-
>
|
|
68
|
-
{ranks.map((rank, row) => (
|
|
69
|
-
<View
|
|
70
|
-
key={`r-${rank}`}
|
|
71
|
-
style={{
|
|
72
|
-
position: 'absolute',
|
|
73
|
-
top: row * squareSize,
|
|
74
|
-
width: gutterWidth,
|
|
75
|
-
height: squareSize,
|
|
76
|
-
alignItems: 'center',
|
|
77
|
-
justifyContent: 'center',
|
|
78
|
-
}}
|
|
79
|
-
>
|
|
80
|
-
<Text
|
|
81
|
-
style={{
|
|
82
|
-
fontSize,
|
|
83
|
-
fontWeight: '600',
|
|
84
|
-
color: textColor,
|
|
85
|
-
}}
|
|
86
|
-
>
|
|
87
|
-
{rank}
|
|
88
|
-
</Text>
|
|
89
|
-
</View>
|
|
90
|
-
))}
|
|
91
|
-
</View>
|
|
92
|
-
)}
|
|
93
|
-
|
|
94
|
-
{/* File letters — bottom gutter, horizontally centered on each column */}
|
|
95
|
-
{withLetters && (
|
|
96
|
-
<View
|
|
97
|
-
style={{
|
|
98
|
-
position: 'absolute',
|
|
99
|
-
left: withNumbers ? gutterWidth : 0,
|
|
100
|
-
bottom: 0,
|
|
101
|
-
width: boardSize,
|
|
102
|
-
height: gutterWidth,
|
|
103
|
-
}}
|
|
104
|
-
pointerEvents="none"
|
|
105
|
-
>
|
|
106
|
-
{files.map((file, col) => (
|
|
107
|
-
<View
|
|
108
|
-
key={`f-${file}`}
|
|
109
|
-
style={{
|
|
110
|
-
position: 'absolute',
|
|
111
|
-
left: col * squareSize,
|
|
112
|
-
width: squareSize,
|
|
113
|
-
height: gutterWidth,
|
|
114
|
-
alignItems: 'center',
|
|
115
|
-
justifyContent: 'center',
|
|
116
|
-
}}
|
|
117
|
-
>
|
|
118
|
-
<Text
|
|
119
|
-
style={{
|
|
120
|
-
fontSize,
|
|
121
|
-
fontWeight: '600',
|
|
122
|
-
color: textColor,
|
|
123
|
-
}}
|
|
124
|
-
>
|
|
125
|
-
{file}
|
|
126
|
-
</Text>
|
|
127
|
-
</View>
|
|
128
|
-
))}
|
|
129
|
-
</View>
|
|
130
|
-
)}
|
|
131
|
-
</>
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ── Inside mode (default): absolute-positioned inside edge squares ──
|
|
136
|
-
const fontSize = squareSize * 0.22;
|
|
137
|
-
const padding = squareSize * 0.06;
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
|
-
<View
|
|
141
|
-
style={{ position: 'absolute', width: boardSize, height: boardSize }}
|
|
142
|
-
pointerEvents="none"
|
|
143
|
-
>
|
|
144
|
-
{/* Rank numbers along left edge (inside each row's first square) */}
|
|
145
|
-
{withNumbers &&
|
|
146
|
-
ranks.map((rank, row) => {
|
|
147
|
-
const isLight = row % 2 === 0;
|
|
148
|
-
const textColor = isLight ? darkColor : lightColor;
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
<Text
|
|
152
|
-
key={`r-${rank}`}
|
|
153
|
-
style={{
|
|
154
|
-
position: 'absolute',
|
|
155
|
-
left: padding,
|
|
156
|
-
top: row * squareSize + padding,
|
|
157
|
-
fontSize,
|
|
158
|
-
fontWeight: '700',
|
|
159
|
-
color: textColor,
|
|
160
|
-
}}
|
|
161
|
-
>
|
|
162
|
-
{rank}
|
|
163
|
-
</Text>
|
|
164
|
-
);
|
|
165
|
-
})}
|
|
166
|
-
|
|
167
|
-
{/* File letters along bottom edge (inside each column's last square) */}
|
|
168
|
-
{withLetters &&
|
|
169
|
-
files.map((file, col) => {
|
|
170
|
-
const isLight = (7 + col) % 2 === 0;
|
|
171
|
-
const textColor = isLight ? darkColor : lightColor;
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<Text
|
|
175
|
-
key={`f-${file}`}
|
|
176
|
-
style={{
|
|
177
|
-
position: 'absolute',
|
|
178
|
-
right: (7 - col) * squareSize + padding,
|
|
179
|
-
bottom: padding,
|
|
180
|
-
fontSize,
|
|
181
|
-
fontWeight: '700',
|
|
182
|
-
color: textColor,
|
|
183
|
-
textAlign: 'right',
|
|
184
|
-
}}
|
|
185
|
-
>
|
|
186
|
-
{file}
|
|
187
|
-
</Text>
|
|
188
|
-
);
|
|
189
|
-
})}
|
|
190
|
-
</View>
|
|
191
|
-
);
|
|
192
|
-
});
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { ChessColor } from './types';
|
|
5
|
+
|
|
6
|
+
type BoardCoordinatesProps = {
|
|
7
|
+
boardSize: number;
|
|
8
|
+
orientation: ChessColor;
|
|
9
|
+
lightColor: string;
|
|
10
|
+
darkColor: string;
|
|
11
|
+
withLetters: boolean;
|
|
12
|
+
withNumbers: boolean;
|
|
13
|
+
/** 'inside' overlays on edge squares, 'outside' renders in a gutter area. */
|
|
14
|
+
position?: 'inside' | 'outside';
|
|
15
|
+
/** Gutter width in pixels (only used when position='outside'). */
|
|
16
|
+
gutterWidth?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const FILES_WHITE = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
|
|
20
|
+
const FILES_BLACK = ['h', 'g', 'f', 'e', 'd', 'c', 'b', 'a'];
|
|
21
|
+
const RANKS_WHITE = ['8', '7', '6', '5', '4', '3', '2', '1'];
|
|
22
|
+
const RANKS_BLACK = ['1', '2', '3', '4', '5', '6', '7', '8'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* File letters (a-h) and rank numbers (1-8) drawn on or around the board.
|
|
26
|
+
*
|
|
27
|
+
* Two modes:
|
|
28
|
+
* - **inside** (default): absolute-positioned inside each edge square, colors
|
|
29
|
+
* alternate to contrast with the square behind them.
|
|
30
|
+
* - **outside**: rendered in a gutter area around the board. Rank numbers to
|
|
31
|
+
* the left, file letters along the bottom. Uses the dark square color.
|
|
32
|
+
*/
|
|
33
|
+
export const BoardCoordinates = React.memo(function BoardCoordinates({
|
|
34
|
+
boardSize,
|
|
35
|
+
orientation,
|
|
36
|
+
lightColor,
|
|
37
|
+
darkColor,
|
|
38
|
+
withLetters,
|
|
39
|
+
withNumbers,
|
|
40
|
+
position = 'inside',
|
|
41
|
+
gutterWidth = 0,
|
|
42
|
+
}: BoardCoordinatesProps) {
|
|
43
|
+
if (!withLetters && !withNumbers) return null;
|
|
44
|
+
|
|
45
|
+
const squareSize = boardSize / 8;
|
|
46
|
+
const files = orientation === 'white' ? FILES_WHITE : FILES_BLACK;
|
|
47
|
+
const ranks = orientation === 'white' ? RANKS_WHITE : RANKS_BLACK;
|
|
48
|
+
|
|
49
|
+
// ── Outside mode: labels in gutter area around the board ──
|
|
50
|
+
if (position === 'outside') {
|
|
51
|
+
const fontSize = gutterWidth * 0.65;
|
|
52
|
+
const textColor = darkColor;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
{/* Rank numbers — left gutter, vertically centered on each row */}
|
|
57
|
+
{withNumbers && (
|
|
58
|
+
<View
|
|
59
|
+
style={{
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
left: 0,
|
|
62
|
+
top: 0,
|
|
63
|
+
width: gutterWidth,
|
|
64
|
+
height: boardSize,
|
|
65
|
+
}}
|
|
66
|
+
pointerEvents="none"
|
|
67
|
+
>
|
|
68
|
+
{ranks.map((rank, row) => (
|
|
69
|
+
<View
|
|
70
|
+
key={`r-${rank}`}
|
|
71
|
+
style={{
|
|
72
|
+
position: 'absolute',
|
|
73
|
+
top: row * squareSize,
|
|
74
|
+
width: gutterWidth,
|
|
75
|
+
height: squareSize,
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
justifyContent: 'center',
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<Text
|
|
81
|
+
style={{
|
|
82
|
+
fontSize,
|
|
83
|
+
fontWeight: '600',
|
|
84
|
+
color: textColor,
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{rank}
|
|
88
|
+
</Text>
|
|
89
|
+
</View>
|
|
90
|
+
))}
|
|
91
|
+
</View>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{/* File letters — bottom gutter, horizontally centered on each column */}
|
|
95
|
+
{withLetters && (
|
|
96
|
+
<View
|
|
97
|
+
style={{
|
|
98
|
+
position: 'absolute',
|
|
99
|
+
left: withNumbers ? gutterWidth : 0,
|
|
100
|
+
bottom: 0,
|
|
101
|
+
width: boardSize,
|
|
102
|
+
height: gutterWidth,
|
|
103
|
+
}}
|
|
104
|
+
pointerEvents="none"
|
|
105
|
+
>
|
|
106
|
+
{files.map((file, col) => (
|
|
107
|
+
<View
|
|
108
|
+
key={`f-${file}`}
|
|
109
|
+
style={{
|
|
110
|
+
position: 'absolute',
|
|
111
|
+
left: col * squareSize,
|
|
112
|
+
width: squareSize,
|
|
113
|
+
height: gutterWidth,
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
justifyContent: 'center',
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<Text
|
|
119
|
+
style={{
|
|
120
|
+
fontSize,
|
|
121
|
+
fontWeight: '600',
|
|
122
|
+
color: textColor,
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{file}
|
|
126
|
+
</Text>
|
|
127
|
+
</View>
|
|
128
|
+
))}
|
|
129
|
+
</View>
|
|
130
|
+
)}
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Inside mode (default): absolute-positioned inside edge squares ──
|
|
136
|
+
const fontSize = squareSize * 0.22;
|
|
137
|
+
const padding = squareSize * 0.06;
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<View
|
|
141
|
+
style={{ position: 'absolute', width: boardSize, height: boardSize }}
|
|
142
|
+
pointerEvents="none"
|
|
143
|
+
>
|
|
144
|
+
{/* Rank numbers along left edge (inside each row's first square) */}
|
|
145
|
+
{withNumbers &&
|
|
146
|
+
ranks.map((rank, row) => {
|
|
147
|
+
const isLight = row % 2 === 0;
|
|
148
|
+
const textColor = isLight ? darkColor : lightColor;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Text
|
|
152
|
+
key={`r-${rank}`}
|
|
153
|
+
style={{
|
|
154
|
+
position: 'absolute',
|
|
155
|
+
left: padding,
|
|
156
|
+
top: row * squareSize + padding,
|
|
157
|
+
fontSize,
|
|
158
|
+
fontWeight: '700',
|
|
159
|
+
color: textColor,
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{rank}
|
|
163
|
+
</Text>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
|
|
167
|
+
{/* File letters along bottom edge (inside each column's last square) */}
|
|
168
|
+
{withLetters &&
|
|
169
|
+
files.map((file, col) => {
|
|
170
|
+
const isLight = (7 + col) % 2 === 0;
|
|
171
|
+
const textColor = isLight ? darkColor : lightColor;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Text
|
|
175
|
+
key={`f-${file}`}
|
|
176
|
+
style={{
|
|
177
|
+
position: 'absolute',
|
|
178
|
+
right: (7 - col) * squareSize + padding,
|
|
179
|
+
bottom: padding,
|
|
180
|
+
fontSize,
|
|
181
|
+
fontWeight: '700',
|
|
182
|
+
color: textColor,
|
|
183
|
+
textAlign: 'right',
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{file}
|
|
187
|
+
</Text>
|
|
188
|
+
);
|
|
189
|
+
})}
|
|
190
|
+
</View>
|
|
191
|
+
);
|
|
192
|
+
});
|
package/src/board.tsx
CHANGED
|
@@ -9,12 +9,7 @@ import React, {
|
|
|
9
9
|
} from 'react';
|
|
10
10
|
import { View, type LayoutChangeEvent } from 'react-native';
|
|
11
11
|
import { GestureDetector } from 'react-native-gesture-handler';
|
|
12
|
-
import
|
|
13
|
-
useSharedValue,
|
|
14
|
-
useAnimatedStyle,
|
|
15
|
-
withTiming,
|
|
16
|
-
FadeOut,
|
|
17
|
-
} from 'react-native-reanimated';
|
|
12
|
+
import { useSharedValue, withTiming, FadeOut } from 'react-native-reanimated';
|
|
18
13
|
|
|
19
14
|
import type {
|
|
20
15
|
BoardRef,
|
|
@@ -160,6 +155,7 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
160
155
|
pieceExitAnimation,
|
|
161
156
|
|
|
162
157
|
// Promotion
|
|
158
|
+
autoPromoteTo,
|
|
163
159
|
onPromotion,
|
|
164
160
|
|
|
165
161
|
// Callbacks
|
|
@@ -185,8 +181,9 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
185
181
|
const boardColors = colors ?? DEFAULT_BOARD_COLORS;
|
|
186
182
|
|
|
187
183
|
// Resolve coordinate position: new prop takes precedence over legacy booleans
|
|
188
|
-
const coordinatePosition =
|
|
189
|
-
??
|
|
184
|
+
const coordinatePosition =
|
|
185
|
+
coordinatePositionProp ??
|
|
186
|
+
(withLettersProp === false && withNumbersProp === false ? 'none' : 'inside');
|
|
190
187
|
const isOutside = coordinatePosition === 'outside';
|
|
191
188
|
const isCoordVisible = coordinatePosition !== 'none';
|
|
192
189
|
|
|
@@ -209,10 +206,7 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
209
206
|
if (prevOrientationRef.current !== orientation) {
|
|
210
207
|
prevOrientationRef.current = orientation;
|
|
211
208
|
if (animateFlip) {
|
|
212
|
-
flipRotation.value = withTiming(
|
|
213
|
-
orientation === 'black' ? 180 : 0,
|
|
214
|
-
{ duration: 300 },
|
|
215
|
-
);
|
|
209
|
+
flipRotation.value = withTiming(orientation === 'black' ? 180 : 0, { duration: 300 });
|
|
216
210
|
} else {
|
|
217
211
|
flipRotation.value = orientation === 'black' ? 180 : 0;
|
|
218
212
|
}
|
|
@@ -253,11 +247,7 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
253
247
|
// attacked, highlight it.
|
|
254
248
|
const checkSquareState = useMemo(() => {
|
|
255
249
|
try {
|
|
256
|
-
return detectCheckSquare(
|
|
257
|
-
internalFen,
|
|
258
|
-
() => boardState.isInCheck(),
|
|
259
|
-
boardState.getTurn,
|
|
260
|
-
);
|
|
250
|
+
return detectCheckSquare(internalFen, () => boardState.isInCheck(), boardState.getTurn);
|
|
261
251
|
} catch {
|
|
262
252
|
return null;
|
|
263
253
|
}
|
|
@@ -331,18 +321,35 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
331
321
|
|
|
332
322
|
// Check for promotion
|
|
333
323
|
if (isPromotionMove(from, to)) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
324
|
+
// 1. Auto-promote to a specific piece (no picker, no callback)
|
|
325
|
+
if (autoPromoteTo) {
|
|
326
|
+
const result = boardState.applyMove(from, to, autoPromoteTo);
|
|
327
|
+
if (result.applied && result.fen) {
|
|
328
|
+
setInternalFen(result.fen);
|
|
329
|
+
}
|
|
330
|
+
onMove?.({ from, to, promotion: autoPromoteTo });
|
|
338
331
|
return;
|
|
339
332
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (
|
|
343
|
-
|
|
333
|
+
|
|
334
|
+
// 2. Consumer handles promotion UI externally via callback
|
|
335
|
+
if (onPromotion) {
|
|
336
|
+
try {
|
|
337
|
+
const choice = await onPromotion(from, to);
|
|
338
|
+
const result = boardState.applyMove(from, to, choice);
|
|
339
|
+
if (result.applied && result.fen) {
|
|
340
|
+
setInternalFen(result.fen);
|
|
341
|
+
}
|
|
342
|
+
onMove?.({ from, to, promotion: choice });
|
|
343
|
+
} catch {
|
|
344
|
+
// Promotion cancelled by consumer — piece stays at origin
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
344
347
|
}
|
|
345
|
-
|
|
348
|
+
|
|
349
|
+
// 3. Default: show built-in promotion picker
|
|
350
|
+
const piece = pieces.find((p) => p.square === from);
|
|
351
|
+
const color = piece?.color ?? 'w';
|
|
352
|
+
setPromotionState({ from, to, color });
|
|
346
353
|
return;
|
|
347
354
|
}
|
|
348
355
|
|
|
@@ -357,38 +364,24 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
357
364
|
// If chess.js rejected the move (truly illegal), do nothing —
|
|
358
365
|
// piece stays at its original square.
|
|
359
366
|
},
|
|
360
|
-
[onMove, onPromotion, isPromotionMove, pieces, boardState],
|
|
367
|
+
[onMove, onPromotion, autoPromoteTo, isPromotionMove, pieces, boardState],
|
|
361
368
|
);
|
|
362
369
|
|
|
363
370
|
// --- Promotion picker handlers ---
|
|
371
|
+
// Only reached when neither autoPromoteTo nor onPromotion is set.
|
|
364
372
|
const handlePromotionSelect = useCallback(
|
|
365
|
-
|
|
373
|
+
(piece: PromotionPiece) => {
|
|
366
374
|
if (!promotionState) return;
|
|
367
375
|
const { from, to } = promotionState;
|
|
368
376
|
setPromotionState(null);
|
|
369
377
|
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
try {
|
|
374
|
-
const choice = await onPromotion(from, to);
|
|
375
|
-
const result = boardState.applyMove(from, to, choice);
|
|
376
|
-
if (result.applied && result.fen) {
|
|
377
|
-
setInternalFen(result.fen);
|
|
378
|
-
}
|
|
379
|
-
onMove?.({ from, to });
|
|
380
|
-
} catch {
|
|
381
|
-
// Promotion cancelled — piece stays at origin
|
|
382
|
-
}
|
|
383
|
-
} else {
|
|
384
|
-
const result = boardState.applyMove(from, to, promo);
|
|
385
|
-
if (result.applied && result.fen) {
|
|
386
|
-
setInternalFen(result.fen);
|
|
387
|
-
}
|
|
388
|
-
onMove?.({ from, to });
|
|
378
|
+
const result = boardState.applyMove(from, to, piece);
|
|
379
|
+
if (result.applied && result.fen) {
|
|
380
|
+
setInternalFen(result.fen);
|
|
389
381
|
}
|
|
382
|
+
onMove?.({ from, to, promotion: piece });
|
|
390
383
|
},
|
|
391
|
-
[promotionState,
|
|
384
|
+
[promotionState, onMove, boardState],
|
|
392
385
|
);
|
|
393
386
|
|
|
394
387
|
const handlePromotionCancel = useCallback(() => {
|
|
@@ -424,7 +417,7 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
424
417
|
const result = boardState.applyMove(consumed.from, consumed.to, consumed.promotion);
|
|
425
418
|
if (result.applied && result.fen) {
|
|
426
419
|
setInternalFen(result.fen);
|
|
427
|
-
onMove?.({ from: consumed.from, to: consumed.to });
|
|
420
|
+
onMove?.({ from: consumed.from, to: consumed.to, promotion: consumed.promotion });
|
|
428
421
|
onHaptic?.('move');
|
|
429
422
|
} else {
|
|
430
423
|
// Premove was illegal — discard silently
|
|
@@ -432,7 +425,17 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
432
425
|
}
|
|
433
426
|
}
|
|
434
427
|
}
|
|
435
|
-
}, [
|
|
428
|
+
}, [
|
|
429
|
+
fen,
|
|
430
|
+
premovesEnabled,
|
|
431
|
+
premove,
|
|
432
|
+
pieces,
|
|
433
|
+
boardState,
|
|
434
|
+
consumePremove,
|
|
435
|
+
clearPremove,
|
|
436
|
+
onMove,
|
|
437
|
+
onHaptic,
|
|
438
|
+
]);
|
|
436
439
|
|
|
437
440
|
// --- Rich callbacks ref (stable, for gesture hook) ---
|
|
438
441
|
const richCallbacks = useMemo(
|
|
@@ -525,18 +528,18 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
525
528
|
|
|
526
529
|
// If no size yet (auto-sizing), render invisible container for measurement
|
|
527
530
|
if (outerSize === 0) {
|
|
528
|
-
return
|
|
529
|
-
<View style={{ flex: 1, aspectRatio: 1 }} onLayout={handleLayout} />
|
|
530
|
-
);
|
|
531
|
+
return <View style={{ flex: 1, aspectRatio: 1 }} onLayout={handleLayout} />;
|
|
531
532
|
}
|
|
532
533
|
|
|
533
534
|
// Inner board with all interactive layers
|
|
534
535
|
const boardContent = (
|
|
535
536
|
<GestureDetector gesture={gesture}>
|
|
536
537
|
<View
|
|
537
|
-
style={
|
|
538
|
-
|
|
539
|
-
|
|
538
|
+
style={
|
|
539
|
+
isOutside
|
|
540
|
+
? { width: boardSize, height: boardSize, position: 'absolute', top: 0, right: 0 }
|
|
541
|
+
: { width: boardSize, height: boardSize }
|
|
542
|
+
}
|
|
540
543
|
onLayout={!isOutside && !boardSizeProp ? handleLayout : undefined}
|
|
541
544
|
accessibilityLabel="Chess board"
|
|
542
545
|
accessibilityRole="adjustable"
|