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.
Files changed (42) hide show
  1. package/README.md +19 -0
  2. package/lib/commonjs/board-arrows.js +7 -7
  3. package/lib/commonjs/board-coordinates.js +8 -8
  4. package/lib/commonjs/board.js +48 -39
  5. package/lib/commonjs/board.js.map +1 -1
  6. package/lib/commonjs/promotion-picker.js +9 -9
  7. package/lib/commonjs/promotion-picker.js.map +1 -1
  8. package/lib/commonjs/static-board.js +7 -7
  9. package/lib/commonjs/static-board.js.map +1 -1
  10. package/lib/commonjs/use-board-gesture.js +26 -26
  11. package/lib/commonjs/use-board-gesture.js.map +1 -1
  12. package/lib/commonjs/use-board-state.js +13 -4
  13. package/lib/commonjs/use-board-state.js.map +1 -1
  14. package/lib/module/board-arrows.js +7 -7
  15. package/lib/module/board-coordinates.js +8 -8
  16. package/lib/module/board.js +48 -39
  17. package/lib/module/board.js.map +1 -1
  18. package/lib/module/promotion-picker.js +9 -9
  19. package/lib/module/promotion-picker.js.map +1 -1
  20. package/lib/module/static-board.js +7 -7
  21. package/lib/module/static-board.js.map +1 -1
  22. package/lib/module/use-board-gesture.js +26 -26
  23. package/lib/module/use-board-gesture.js.map +1 -1
  24. package/lib/module/use-board-state.js +13 -4
  25. package/lib/module/use-board-state.js.map +1 -1
  26. package/lib/typescript/board-coordinates.d.ts.map +1 -1
  27. package/lib/typescript/board.d.ts.map +1 -1
  28. package/lib/typescript/promotion-picker.d.ts.map +1 -1
  29. package/lib/typescript/static-board.d.ts.map +1 -1
  30. package/lib/typescript/types.d.ts +14 -3
  31. package/lib/typescript/types.d.ts.map +1 -1
  32. package/lib/typescript/use-board-gesture.d.ts.map +1 -1
  33. package/lib/typescript/use-board-state.d.ts.map +1 -1
  34. package/package.json +95 -75
  35. package/src/board-arrows.tsx +197 -197
  36. package/src/board-coordinates.tsx +192 -192
  37. package/src/board.tsx +59 -56
  38. package/src/promotion-picker.tsx +147 -147
  39. package/src/static-board.tsx +186 -187
  40. package/src/types.ts +27 -6
  41. package/src/use-board-gesture.ts +459 -429
  42. 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, CoordinatePosition } 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
- });
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 Animated, {
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 = coordinatePositionProp
189
- ?? (withLettersProp === false && withNumbersProp === false ? 'none' : 'inside');
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
- if (onPromotion) {
335
- const piece = pieces.find((p) => p.square === from);
336
- const color = piece?.color ?? 'w';
337
- setPromotionState({ from, to, color });
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
- // Auto-promote to queen
341
- const result = boardState.applyMove(from, to, 'q');
342
- if (result.applied && result.fen) {
343
- setInternalFen(result.fen);
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
- onMove?.({ from, to });
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
- async (piece: PromotionPiece) => {
373
+ (piece: PromotionPiece) => {
366
374
  if (!promotionState) return;
367
375
  const { from, to } = promotionState;
368
376
  setPromotionState(null);
369
377
 
370
- const promo = piece.toLowerCase();
371
-
372
- if (onPromotion) {
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, onPromotion, onMove, boardState],
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
- }, [fen, premovesEnabled, premove, pieces, boardState, consumePremove, clearPremove, onMove, onHaptic]);
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={isOutside
538
- ? { width: boardSize, height: boardSize, position: 'absolute', top: 0, right: 0 }
539
- : { width: boardSize, height: boardSize }}
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"