react-chess-core 0.1.2 → 0.1.3

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,3 @@
1
+ /** Green circle with a white check, anchored to the bottom-right of a square
2
+ * square (over the piece). */
3
+ export declare const CorrectMoveCheckBadge: () => import("react").JSX.Element;
@@ -2,6 +2,10 @@ export interface HighlightChessboardProps {
2
2
  checkSquare: string;
3
3
  hintSquare: string | null;
4
4
  incorrectMoveSquare: string | null;
5
+ /** Destination square of the last correct training move — shows a green check. */
6
+ correctMoveSquare?: string | null;
7
+ /** Enable click-to-move when `onPieceDrop` is provided. Defaults to true. */
8
+ clickToMove?: boolean;
5
9
  [key: string]: any;
6
10
  }
7
- export declare const HighlightChessboard: ({ checkSquare, hintSquare, incorrectMoveSquare, customSquareStyles: extraSquareStyles, ...props }: HighlightChessboardProps) => import("react").JSX.Element;
11
+ export declare const HighlightChessboard: ({ checkSquare, hintSquare, incorrectMoveSquare, correctMoveSquare, clickToMove, customSquareStyles: extraSquareStyles, customBoardStyle, onPieceDrop, position, arePiecesDraggable, autoPromoteToQueen, isDraggablePiece, onPromotionCheck, onSquareClick, onPromotionPieceSelect, onPieceDragBegin, showPromotionDialog: showPromotionDialogProp, promotionToSquare: promotionToSquareProp, ...props }: HighlightChessboardProps) => import("react").JSX.Element;
@@ -6,4 +6,7 @@ export declare const boardSquareHighlightColors: {
6
6
  readonly hint: "rgba(119, 177, 212, 0.75)";
7
7
  /** Muted red — softer than the in-check highlight. */
8
8
  readonly incorrect: "rgba(140, 38, 38, 0.82)";
9
+ readonly selected: "rgba(255, 255, 0, 0.45)";
10
+ readonly moveTarget: "radial-gradient(circle, rgba(0, 0, 0, 0.18) 22%, transparent 22%)";
11
+ readonly captureTarget: "radial-gradient(circle, rgba(0, 0, 0, 0.18) 72%, transparent 72%)";
9
12
  };
@@ -0,0 +1,3 @@
1
+ import type { FC } from 'react';
2
+ import type { CustomSquareProps } from 'react-chessboard/dist/chessboard/types';
3
+ export declare function createFeedbackSquareRenderer(correctMoveSquare: string): FC<CustomSquareProps>;
@@ -2,4 +2,5 @@ export * from './boardThemes';
2
2
  export * from './chessboardTheme';
3
3
  export * from './ChessboardDnDProvider';
4
4
  export * from './HighlightChessboard';
5
+ export * from './CorrectMoveCheckBadge';
5
6
  export * from './boardSquareHighlightColors';
@@ -0,0 +1,28 @@
1
+ import { type Square } from 'chess.js';
2
+ import { type CSSProperties } from 'react';
3
+ type Piece = string;
4
+ export type ClickToMoveSquareStyles = Record<string, CSSProperties>;
5
+ export type UseClickToMoveOptions = {
6
+ enabled: boolean;
7
+ position: string | undefined;
8
+ arePiecesDraggable?: boolean;
9
+ autoPromoteToQueen?: boolean;
10
+ isDraggablePiece?: (args: {
11
+ piece: Piece;
12
+ sourceSquare: Square;
13
+ }) => boolean;
14
+ onPromotionCheck?: (sourceSquare: Square, targetSquare: Square, piece: Piece) => boolean;
15
+ onPieceDrop?: (sourceSquare: Square, targetSquare: Square, piece: Piece) => boolean;
16
+ onSquareClick?: (square: Square, piece: Piece | undefined) => void;
17
+ onPromotionPieceSelect?: (piece?: Piece, promoteFromSquare?: Square, promoteToSquare?: Square) => boolean;
18
+ onPieceDragBegin?: (piece: Piece, sourceSquare: Square) => void;
19
+ };
20
+ export declare function useClickToMove({ enabled, position, arePiecesDraggable, autoPromoteToQueen, isDraggablePiece, onPromotionCheck, onPieceDrop, onSquareClick, onPromotionPieceSelect, onPieceDragBegin, }: UseClickToMoveOptions): {
21
+ clickSquareStyles: ClickToMoveSquareStyles;
22
+ handleSquareClick: (square: Square, piece: Piece | undefined) => void;
23
+ handlePromotionPieceSelect: (piece?: Piece, promoteFromSquare?: Square, promoteToSquare?: Square) => boolean;
24
+ handlePieceDragBegin: (piece: Piece, sourceSquare: Square) => void;
25
+ showPromotionDialog: boolean;
26
+ promotionToSquare: Square | null;
27
+ };
28
+ export {};
@@ -0,0 +1,2 @@
1
+ /** Pause (ms) to show the correct-move check before advancing the line. */
2
+ export declare const CORRECT_MOVE_FEEDBACK_MS = 450;
@@ -1,4 +1,6 @@
1
1
  export * from './uciFromDrop';
2
2
  export * from './expectedMoveDrop';
3
3
  export * from './useBoardRevision';
4
+ export * from './correctMoveFeedbackMs';
5
+ export * from './useCorrectMoveFeedback';
4
6
  export * from './miss';
@@ -0,0 +1,6 @@
1
+ export declare function useCorrectMoveFeedback(delayMs?: number): {
2
+ correctMoveSquare: string | null;
3
+ showCorrectMove: (targetSquare: string, onComplete?: () => void) => void;
4
+ clearCorrectMoveFeedback: () => void;
5
+ isShowingCorrectMove: boolean;
6
+ };
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
- import { createContext, useContext, useRef, useCallback, useEffect, useMemo, useState, useLayoutEffect, Component } from 'react';
2
+ import { createContext, useContext, useState, useCallback, useEffect, useMemo, useRef, useLayoutEffect, Component } from 'react';
3
3
  import { ChessboardDnDProvider as ChessboardDnDProvider$1, Chessboard } from 'react-chessboard';
4
4
  import { Chess } from 'chess.js';
5
5
  import { createPortal } from 'react-dom';
@@ -1741,8 +1741,200 @@ const boardSquareHighlightColors = {
1741
1741
  hint: 'rgba(119, 177, 212, 0.75)',
1742
1742
  /** Muted red — softer than the in-check highlight. */
1743
1743
  incorrect: 'rgba(140, 38, 38, 0.82)',
1744
+ selected: 'rgba(255, 255, 0, 0.45)',
1745
+ moveTarget: 'radial-gradient(circle, rgba(0, 0, 0, 0.18) 22%, transparent 22%)',
1746
+ captureTarget: 'radial-gradient(circle, rgba(0, 0, 0, 0.18) 72%, transparent 72%)',
1744
1747
  };
1745
1748
 
1749
+ const badgeStyle = {
1750
+ position: 'absolute',
1751
+ right: '6%',
1752
+ bottom: '6%',
1753
+ width: '26%',
1754
+ height: '26%',
1755
+ minWidth: 14,
1756
+ minHeight: 14,
1757
+ maxWidth: 26,
1758
+ maxHeight: 26,
1759
+ borderRadius: '50%',
1760
+ backgroundColor: '#2e7d32',
1761
+ display: 'flex',
1762
+ alignItems: 'center',
1763
+ justifyContent: 'center',
1764
+ boxShadow: '0 1px 3px rgba(0, 0, 0, 0.35)',
1765
+ pointerEvents: 'none',
1766
+ };
1767
+ /** Green circle with a white check, anchored to the bottom-right of a square
1768
+ * square (over the piece). */
1769
+ const CorrectMoveCheckBadge = () => (jsx("span", { "aria-hidden": true, style: badgeStyle, children: jsx("svg", { viewBox: "0 0 12 12", width: "62%", height: "62%", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsx("path", { d: "M2.5 6.2 5 8.7 9.5 3.8", stroke: "#fff", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) }));
1770
+
1771
+ const overlayStyle$1 = {
1772
+ position: 'absolute',
1773
+ inset: 0,
1774
+ pointerEvents: 'none',
1775
+ zIndex: 20,
1776
+ overflow: 'visible',
1777
+ };
1778
+ function createFeedbackSquareRenderer(correctMoveSquare) {
1779
+ return function FeedbackSquare({ children, style, square, ref }) {
1780
+ return (jsxs("div", { ref: ref, style: Object.assign(Object.assign({}, style), { position: 'relative', overflow: 'visible' }), children: [children, square === correctMoveSquare ? (jsx("div", { style: overlayStyle$1, children: jsx(CorrectMoveCheckBadge, {}) })) : null] }));
1781
+ };
1782
+ }
1783
+
1784
+ function defaultPromotionCheck(sourceSquare, targetSquare, piece) {
1785
+ return (((piece === 'wP' && sourceSquare[1] === '7' && targetSquare[1] === '8') ||
1786
+ (piece === 'bP' && sourceSquare[1] === '2' && targetSquare[1] === '1')) &&
1787
+ Math.abs(sourceSquare.charCodeAt(0) - targetSquare.charCodeAt(0)) <= 1);
1788
+ }
1789
+ function pieceAtSquare(fen, square) {
1790
+ const boardPiece = new Chess(fen).get(square);
1791
+ if (!boardPiece) {
1792
+ return null;
1793
+ }
1794
+ const color = boardPiece.color === 'w' ? 'w' : 'b';
1795
+ const type = boardPiece.type.toUpperCase();
1796
+ return `${color}${type}`;
1797
+ }
1798
+ function getMoveOptionStyles(fen, fromSquare) {
1799
+ const chess = new Chess(fen);
1800
+ const moves = chess.moves({ square: fromSquare, verbose: true });
1801
+ if (!moves.length) {
1802
+ return {
1803
+ [fromSquare]: { backgroundColor: boardSquareHighlightColors.selected },
1804
+ };
1805
+ }
1806
+ const styles = {
1807
+ [fromSquare]: { backgroundColor: boardSquareHighlightColors.selected },
1808
+ };
1809
+ for (const move of moves) {
1810
+ styles[move.to] = {
1811
+ background: chess.get(move.to)
1812
+ ? boardSquareHighlightColors.captureTarget
1813
+ : boardSquareHighlightColors.moveTarget,
1814
+ borderRadius: '50%',
1815
+ };
1816
+ }
1817
+ return styles;
1818
+ }
1819
+ function useClickToMove({ enabled, position, arePiecesDraggable = true, autoPromoteToQueen = false, isDraggablePiece, onPromotionCheck = defaultPromotionCheck, onPieceDrop, onSquareClick, onPromotionPieceSelect, onPieceDragBegin, }) {
1820
+ var _a;
1821
+ const [moveFrom, setMoveFrom] = useState(null);
1822
+ const [pendingPromotion, setPendingPromotion] = useState(null);
1823
+ const fen = typeof position === 'string' ? position : undefined;
1824
+ const clearSelection = useCallback(() => {
1825
+ setMoveFrom(null);
1826
+ setPendingPromotion(null);
1827
+ }, []);
1828
+ useEffect(() => {
1829
+ clearSelection();
1830
+ }, [fen, clearSelection]);
1831
+ const clickSquareStyles = useMemo(() => {
1832
+ if (!enabled || !moveFrom || !fen) {
1833
+ return {};
1834
+ }
1835
+ return getMoveOptionStyles(fen, moveFrom);
1836
+ }, [enabled, fen, moveFrom]);
1837
+ const canSelectPiece = useCallback((square, piece) => {
1838
+ if (!piece || !arePiecesDraggable || !onPieceDrop) {
1839
+ return false;
1840
+ }
1841
+ if (isDraggablePiece) {
1842
+ return isDraggablePiece({ piece, sourceSquare: square });
1843
+ }
1844
+ return true;
1845
+ }, [arePiecesDraggable, isDraggablePiece, onPieceDrop]);
1846
+ const tryCompleteMove = useCallback((from, to, piece) => {
1847
+ if (!onPieceDrop) {
1848
+ return false;
1849
+ }
1850
+ if (onPromotionCheck(from, to, piece)) {
1851
+ if (autoPromoteToQueen) {
1852
+ const promotedPiece = piece[0] === 'w' ? 'wQ' : 'bQ';
1853
+ const accepted = onPieceDrop(from, to, promotedPiece);
1854
+ clearSelection();
1855
+ return accepted;
1856
+ }
1857
+ setPendingPromotion({ from, to, piece });
1858
+ setMoveFrom(null);
1859
+ return true;
1860
+ }
1861
+ const accepted = onPieceDrop(from, to, piece);
1862
+ clearSelection();
1863
+ return accepted;
1864
+ }, [autoPromoteToQueen, clearSelection, onPieceDrop, onPromotionCheck]);
1865
+ const handleSquareClick = useCallback((square, piece) => {
1866
+ onSquareClick === null || onSquareClick === void 0 ? void 0 : onSquareClick(square, piece);
1867
+ if (!enabled || !onPieceDrop || !arePiecesDraggable || !fen) {
1868
+ return;
1869
+ }
1870
+ if (!moveFrom) {
1871
+ if (canSelectPiece(square, piece)) {
1872
+ setMoveFrom(square);
1873
+ }
1874
+ return;
1875
+ }
1876
+ if (square === moveFrom) {
1877
+ clearSelection();
1878
+ return;
1879
+ }
1880
+ const sourcePiece = pieceAtSquare(fen, moveFrom);
1881
+ if (!sourcePiece) {
1882
+ clearSelection();
1883
+ return;
1884
+ }
1885
+ const accepted = tryCompleteMove(moveFrom, square, sourcePiece);
1886
+ if (accepted) {
1887
+ return;
1888
+ }
1889
+ if (canSelectPiece(square, piece)) {
1890
+ setMoveFrom(square);
1891
+ return;
1892
+ }
1893
+ clearSelection();
1894
+ }, [
1895
+ arePiecesDraggable,
1896
+ canSelectPiece,
1897
+ clearSelection,
1898
+ enabled,
1899
+ fen,
1900
+ moveFrom,
1901
+ onPieceDrop,
1902
+ onSquareClick,
1903
+ tryCompleteMove,
1904
+ ]);
1905
+ const handlePromotionPieceSelect = useCallback((piece, promoteFromSquare, promoteToSquare) => {
1906
+ if (pendingPromotion && piece) {
1907
+ const { from, to } = pendingPromotion;
1908
+ onPieceDrop === null || onPieceDrop === void 0 ? void 0 : onPieceDrop(from, to, piece);
1909
+ onPromotionPieceSelect === null || onPromotionPieceSelect === void 0 ? void 0 : onPromotionPieceSelect(piece, from, to);
1910
+ clearSelection();
1911
+ return false;
1912
+ }
1913
+ if (onPromotionPieceSelect) {
1914
+ return onPromotionPieceSelect(piece, promoteFromSquare, promoteToSquare);
1915
+ }
1916
+ return true;
1917
+ }, [clearSelection, onPieceDrop, onPromotionPieceSelect, pendingPromotion]);
1918
+ const handlePieceDragBegin = useCallback((piece, sourceSquare) => {
1919
+ clearSelection();
1920
+ onPieceDragBegin === null || onPieceDragBegin === void 0 ? void 0 : onPieceDragBegin(piece, sourceSquare);
1921
+ }, [clearSelection, onPieceDragBegin]);
1922
+ return {
1923
+ clickSquareStyles,
1924
+ handleSquareClick,
1925
+ handlePromotionPieceSelect,
1926
+ handlePieceDragBegin,
1927
+ showPromotionDialog: pendingPromotion !== null,
1928
+ promotionToSquare: (_a = pendingPromotion === null || pendingPromotion === void 0 ? void 0 : pendingPromotion.to) !== null && _a !== void 0 ? _a : null,
1929
+ };
1930
+ }
1931
+
1932
+ /** Prevent mobile long-press text selection and iOS callout menus on the board. */
1933
+ const nonSelectableBoardStyle = {
1934
+ userSelect: 'none',
1935
+ WebkitUserSelect: 'none',
1936
+ WebkitTouchCallout: 'none',
1937
+ };
1746
1938
  const getCheckHighlighting = (checkSquare) => {
1747
1939
  const styles = {};
1748
1940
  styles[checkSquare] = { backgroundColor: boardSquareHighlightColors.check };
@@ -1761,12 +1953,40 @@ const getFeedbackHighlighting = (hintSquare, incorrectMoveSquare) => {
1761
1953
  return styles;
1762
1954
  };
1763
1955
  const HighlightChessboard = (_a) => {
1764
- var { checkSquare, hintSquare, incorrectMoveSquare, customSquareStyles: extraSquareStyles } = _a, props = __rest(_a, ["checkSquare", "hintSquare", "incorrectMoveSquare", "customSquareStyles"]);
1956
+ var { checkSquare, hintSquare, incorrectMoveSquare, correctMoveSquare = null, clickToMove, customSquareStyles: extraSquareStyles, customBoardStyle, onPieceDrop, position, arePiecesDraggable, autoPromoteToQueen, isDraggablePiece, onPromotionCheck, onSquareClick, onPromotionPieceSelect, onPieceDragBegin, showPromotionDialog: showPromotionDialogProp, promotionToSquare: promotionToSquareProp } = _a, props = __rest(_a, ["checkSquare", "hintSquare", "incorrectMoveSquare", "correctMoveSquare", "clickToMove", "customSquareStyles", "customBoardStyle", "onPieceDrop", "position", "arePiecesDraggable", "autoPromoteToQueen", "isDraggablePiece", "onPromotionCheck", "onSquareClick", "onPromotionPieceSelect", "onPieceDragBegin", "showPromotionDialog", "promotionToSquare"]);
1765
1957
  const { customDarkSquareStyle, customLightSquareStyle } = useChessboardTheme();
1958
+ const clickToMoveEnabled = clickToMove !== false && typeof onPieceDrop === 'function';
1959
+ const { clickSquareStyles, handleSquareClick, handlePromotionPieceSelect, handlePieceDragBegin, showPromotionDialog: clickPromotionDialog, promotionToSquare: clickPromotionToSquare, } = useClickToMove({
1960
+ enabled: clickToMoveEnabled,
1961
+ position,
1962
+ arePiecesDraggable,
1963
+ autoPromoteToQueen,
1964
+ isDraggablePiece,
1965
+ onPromotionCheck,
1966
+ onPieceDrop,
1967
+ onSquareClick,
1968
+ onPromotionPieceSelect,
1969
+ onPieceDragBegin,
1970
+ });
1766
1971
  const checkStyles = getCheckHighlighting(checkSquare);
1767
1972
  const feedbackStyles = getFeedbackHighlighting(hintSquare, incorrectMoveSquare);
1768
- const customSquareStyles = Object.assign(Object.assign(Object.assign({}, checkStyles), feedbackStyles), extraSquareStyles);
1769
- return (jsx(Chessboard, Object.assign({ customDarkSquareStyle: customDarkSquareStyle, customLightSquareStyle: customLightSquareStyle, customSquareStyles: customSquareStyles }, props)));
1973
+ const customSquareStyles = Object.assign(Object.assign(Object.assign(Object.assign({}, clickSquareStyles), checkStyles), feedbackStyles), extraSquareStyles);
1974
+ const customSquare = useMemo(() => correctMoveSquare
1975
+ ? createFeedbackSquareRenderer(correctMoveSquare)
1976
+ : undefined, [correctMoveSquare]);
1977
+ const promotionControlProps = clickPromotionDialog
1978
+ ? {
1979
+ showPromotionDialog: true,
1980
+ promotionToSquare: clickPromotionToSquare,
1981
+ }
1982
+ : showPromotionDialogProp !== undefined ||
1983
+ promotionToSquareProp !== undefined
1984
+ ? {
1985
+ showPromotionDialog: showPromotionDialogProp,
1986
+ promotionToSquare: promotionToSquareProp,
1987
+ }
1988
+ : {};
1989
+ return (jsx(Chessboard, Object.assign({ customDarkSquareStyle: customDarkSquareStyle, customLightSquareStyle: customLightSquareStyle, customSquareStyles: customSquareStyles, customSquare: customSquare, customBoardStyle: Object.assign(Object.assign({}, nonSelectableBoardStyle), customBoardStyle), position: position, arePiecesDraggable: arePiecesDraggable, autoPromoteToQueen: autoPromoteToQueen, isDraggablePiece: isDraggablePiece, onPromotionCheck: onPromotionCheck, onPieceDrop: onPieceDrop, onSquareClick: clickToMoveEnabled ? handleSquareClick : onSquareClick, onPromotionPieceSelect: clickToMoveEnabled ? handlePromotionPieceSelect : onPromotionPieceSelect, onPieceDragBegin: clickToMoveEnabled ? handlePieceDragBegin : onPieceDragBegin }, promotionControlProps, props)));
1770
1990
  };
1771
1991
 
1772
1992
  const emptyEngineEvaluation = () => ({
@@ -3502,6 +3722,37 @@ function useBoardRevision() {
3502
3722
  return { revision, bumpRevision };
3503
3723
  }
3504
3724
 
3725
+ /** Pause (ms) to show the correct-move check before advancing the line. */
3726
+ const CORRECT_MOVE_FEEDBACK_MS = 450;
3727
+
3728
+ function useCorrectMoveFeedback(delayMs = CORRECT_MOVE_FEEDBACK_MS) {
3729
+ const [correctMoveSquare, setCorrectMoveSquare] = useState(null);
3730
+ const timeoutRef = useRef(null);
3731
+ const clearCorrectMoveFeedback = useCallback(() => {
3732
+ if (timeoutRef.current !== null) {
3733
+ window.clearTimeout(timeoutRef.current);
3734
+ timeoutRef.current = null;
3735
+ }
3736
+ setCorrectMoveSquare(null);
3737
+ }, []);
3738
+ const showCorrectMove = useCallback((targetSquare, onComplete) => {
3739
+ clearCorrectMoveFeedback();
3740
+ setCorrectMoveSquare(targetSquare);
3741
+ timeoutRef.current = window.setTimeout(() => {
3742
+ timeoutRef.current = null;
3743
+ setCorrectMoveSquare(null);
3744
+ onComplete === null || onComplete === void 0 ? void 0 : onComplete();
3745
+ }, delayMs);
3746
+ }, [clearCorrectMoveFeedback, delayMs]);
3747
+ useEffect(() => clearCorrectMoveFeedback, [clearCorrectMoveFeedback]);
3748
+ return {
3749
+ correctMoveSquare,
3750
+ showCorrectMove,
3751
+ clearCorrectMoveFeedback,
3752
+ isShowingCorrectMove: correctMoveSquare !== null,
3753
+ };
3754
+ }
3755
+
3505
3756
  /** Minimum eval loss (pawns) from the wrong move before showing a refutation. */
3506
3757
  const REFUTATION_EVAL_GAP_PAWNS = 0.5;
3507
3758
  const REFUTATION_EVAL_GAP_CP = REFUTATION_EVAL_GAP_PAWNS * 100;
@@ -3809,4 +4060,4 @@ function useMissBoard({ feedback, expectedUci, positionFen, answerArrowColor, au
3809
4060
  };
3810
4061
  }
3811
4062
 
3812
- export { AnalysisBoard, AnalysisBoardCore, AnalysisBoardCoreView, AnalysisBoardLayout, AnalysisChessboardView, AnalysisEngineProvider, AnalysisErrorBoundary, AnalysisPosition, BOARD_THEMES, BOARD_THEME_IDS, ChessboardDnDProvider, ChessboardThemeContext, DEFAULT_ANALYSIS_LAYOUT, DEFAULT_BOARD_THEME, DEFAULT_STOCKFISH_SCRIPT_URL, DefaultAnalysisContainer, DefaultAnalysisSidebar, DefaultPlyNavigation, EngineEvaluationPanel, HighlightChessboard, MISS_MOVE_ANIMATION_MS, MISS_REFUTATION_MAX_WAIT_MS, MISS_REFUTATION_PAUSE_MS, MISS_WRONG_PAUSE_MS, PlyNavigation, REFUTATION_EVAL_GAP_CP, REFUTATION_EVAL_GAP_PAWNS, StockfishBrowserEngine, ThemeProvider, analysisBoardHighlightColors, analysisSidebarColors, applyUciMove, boardSquareHighlightColors, boardThemeFromLegacyUiTheme, createExpectedMoveDropHandler, createSidebarRowBandCounters, defaultRenderPlyNavigation, emptyEngineEvaluation, evaluateExpectedMoveDrop, fenAfterUci, formatEvaluation, formatPvPreview, getAnalysisModalStyles, getBoardThemeStyles, getCheckSquareFromChess, getLastMoveSquareStyles, getMissDisplay, getSidebarRowBackground, getStylesForTheme, isAnalyzableFen, isBoardThemeId, isEditableKeyboardTarget, lineEvalCpForGap, matchesExpectedUci, navButtonStyle, navRowStyle, normalizeEvalForWhite, normalizePvMoves, normalizeSubscriberOptions, parseUciInfoLine, parseUciMove, plyLabelStyle, palette as plyNavigationPalette, refutationEngineOptions, refutationEvalGapCp, refutationFromEvaluation, resolveStockfishScriptUrl, resolveStockfishWasmUrl, resolveStockfishWorkerUrl, scrubberInputStyle, splitWorkerLines, uciFromDrop, uciPvToSan, useAnalysisBoardModel, useAnalysisEngine, useAnalysisEngineContext, useBoardRevision, useChessboardTheme, useMissBoard, useMissRefutation, useMissSequence, usePositionKeyboardNav, useTheme };
4063
+ export { AnalysisBoard, AnalysisBoardCore, AnalysisBoardCoreView, AnalysisBoardLayout, AnalysisChessboardView, AnalysisEngineProvider, AnalysisErrorBoundary, AnalysisPosition, BOARD_THEMES, BOARD_THEME_IDS, CORRECT_MOVE_FEEDBACK_MS, ChessboardDnDProvider, ChessboardThemeContext, CorrectMoveCheckBadge, DEFAULT_ANALYSIS_LAYOUT, DEFAULT_BOARD_THEME, DEFAULT_STOCKFISH_SCRIPT_URL, DefaultAnalysisContainer, DefaultAnalysisSidebar, DefaultPlyNavigation, EngineEvaluationPanel, HighlightChessboard, MISS_MOVE_ANIMATION_MS, MISS_REFUTATION_MAX_WAIT_MS, MISS_REFUTATION_PAUSE_MS, MISS_WRONG_PAUSE_MS, PlyNavigation, REFUTATION_EVAL_GAP_CP, REFUTATION_EVAL_GAP_PAWNS, StockfishBrowserEngine, ThemeProvider, analysisBoardHighlightColors, analysisSidebarColors, applyUciMove, boardSquareHighlightColors, boardThemeFromLegacyUiTheme, createExpectedMoveDropHandler, createSidebarRowBandCounters, defaultRenderPlyNavigation, emptyEngineEvaluation, evaluateExpectedMoveDrop, fenAfterUci, formatEvaluation, formatPvPreview, getAnalysisModalStyles, getBoardThemeStyles, getCheckSquareFromChess, getLastMoveSquareStyles, getMissDisplay, getSidebarRowBackground, getStylesForTheme, isAnalyzableFen, isBoardThemeId, isEditableKeyboardTarget, lineEvalCpForGap, matchesExpectedUci, navButtonStyle, navRowStyle, normalizeEvalForWhite, normalizePvMoves, normalizeSubscriberOptions, parseUciInfoLine, parseUciMove, plyLabelStyle, palette as plyNavigationPalette, refutationEngineOptions, refutationEvalGapCp, refutationFromEvaluation, resolveStockfishScriptUrl, resolveStockfishWasmUrl, resolveStockfishWorkerUrl, scrubberInputStyle, splitWorkerLines, uciFromDrop, uciPvToSan, useAnalysisBoardModel, useAnalysisEngine, useAnalysisEngineContext, useBoardRevision, useChessboardTheme, useCorrectMoveFeedback, useMissBoard, useMissRefutation, useMissSequence, usePositionKeyboardNav, useTheme };
package/dist/index.js CHANGED
@@ -1743,8 +1743,200 @@ const boardSquareHighlightColors = {
1743
1743
  hint: 'rgba(119, 177, 212, 0.75)',
1744
1744
  /** Muted red — softer than the in-check highlight. */
1745
1745
  incorrect: 'rgba(140, 38, 38, 0.82)',
1746
+ selected: 'rgba(255, 255, 0, 0.45)',
1747
+ moveTarget: 'radial-gradient(circle, rgba(0, 0, 0, 0.18) 22%, transparent 22%)',
1748
+ captureTarget: 'radial-gradient(circle, rgba(0, 0, 0, 0.18) 72%, transparent 72%)',
1746
1749
  };
1747
1750
 
1751
+ const badgeStyle = {
1752
+ position: 'absolute',
1753
+ right: '6%',
1754
+ bottom: '6%',
1755
+ width: '26%',
1756
+ height: '26%',
1757
+ minWidth: 14,
1758
+ minHeight: 14,
1759
+ maxWidth: 26,
1760
+ maxHeight: 26,
1761
+ borderRadius: '50%',
1762
+ backgroundColor: '#2e7d32',
1763
+ display: 'flex',
1764
+ alignItems: 'center',
1765
+ justifyContent: 'center',
1766
+ boxShadow: '0 1px 3px rgba(0, 0, 0, 0.35)',
1767
+ pointerEvents: 'none',
1768
+ };
1769
+ /** Green circle with a white check, anchored to the bottom-right of a square
1770
+ * square (over the piece). */
1771
+ const CorrectMoveCheckBadge = () => (jsxRuntime.jsx("span", { "aria-hidden": true, style: badgeStyle, children: jsxRuntime.jsx("svg", { viewBox: "0 0 12 12", width: "62%", height: "62%", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsxRuntime.jsx("path", { d: "M2.5 6.2 5 8.7 9.5 3.8", stroke: "#fff", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) }));
1772
+
1773
+ const overlayStyle$1 = {
1774
+ position: 'absolute',
1775
+ inset: 0,
1776
+ pointerEvents: 'none',
1777
+ zIndex: 20,
1778
+ overflow: 'visible',
1779
+ };
1780
+ function createFeedbackSquareRenderer(correctMoveSquare) {
1781
+ return function FeedbackSquare({ children, style, square, ref }) {
1782
+ return (jsxRuntime.jsxs("div", { ref: ref, style: Object.assign(Object.assign({}, style), { position: 'relative', overflow: 'visible' }), children: [children, square === correctMoveSquare ? (jsxRuntime.jsx("div", { style: overlayStyle$1, children: jsxRuntime.jsx(CorrectMoveCheckBadge, {}) })) : null] }));
1783
+ };
1784
+ }
1785
+
1786
+ function defaultPromotionCheck(sourceSquare, targetSquare, piece) {
1787
+ return (((piece === 'wP' && sourceSquare[1] === '7' && targetSquare[1] === '8') ||
1788
+ (piece === 'bP' && sourceSquare[1] === '2' && targetSquare[1] === '1')) &&
1789
+ Math.abs(sourceSquare.charCodeAt(0) - targetSquare.charCodeAt(0)) <= 1);
1790
+ }
1791
+ function pieceAtSquare(fen, square) {
1792
+ const boardPiece = new chess_js.Chess(fen).get(square);
1793
+ if (!boardPiece) {
1794
+ return null;
1795
+ }
1796
+ const color = boardPiece.color === 'w' ? 'w' : 'b';
1797
+ const type = boardPiece.type.toUpperCase();
1798
+ return `${color}${type}`;
1799
+ }
1800
+ function getMoveOptionStyles(fen, fromSquare) {
1801
+ const chess = new chess_js.Chess(fen);
1802
+ const moves = chess.moves({ square: fromSquare, verbose: true });
1803
+ if (!moves.length) {
1804
+ return {
1805
+ [fromSquare]: { backgroundColor: boardSquareHighlightColors.selected },
1806
+ };
1807
+ }
1808
+ const styles = {
1809
+ [fromSquare]: { backgroundColor: boardSquareHighlightColors.selected },
1810
+ };
1811
+ for (const move of moves) {
1812
+ styles[move.to] = {
1813
+ background: chess.get(move.to)
1814
+ ? boardSquareHighlightColors.captureTarget
1815
+ : boardSquareHighlightColors.moveTarget,
1816
+ borderRadius: '50%',
1817
+ };
1818
+ }
1819
+ return styles;
1820
+ }
1821
+ function useClickToMove({ enabled, position, arePiecesDraggable = true, autoPromoteToQueen = false, isDraggablePiece, onPromotionCheck = defaultPromotionCheck, onPieceDrop, onSquareClick, onPromotionPieceSelect, onPieceDragBegin, }) {
1822
+ var _a;
1823
+ const [moveFrom, setMoveFrom] = react.useState(null);
1824
+ const [pendingPromotion, setPendingPromotion] = react.useState(null);
1825
+ const fen = typeof position === 'string' ? position : undefined;
1826
+ const clearSelection = react.useCallback(() => {
1827
+ setMoveFrom(null);
1828
+ setPendingPromotion(null);
1829
+ }, []);
1830
+ react.useEffect(() => {
1831
+ clearSelection();
1832
+ }, [fen, clearSelection]);
1833
+ const clickSquareStyles = react.useMemo(() => {
1834
+ if (!enabled || !moveFrom || !fen) {
1835
+ return {};
1836
+ }
1837
+ return getMoveOptionStyles(fen, moveFrom);
1838
+ }, [enabled, fen, moveFrom]);
1839
+ const canSelectPiece = react.useCallback((square, piece) => {
1840
+ if (!piece || !arePiecesDraggable || !onPieceDrop) {
1841
+ return false;
1842
+ }
1843
+ if (isDraggablePiece) {
1844
+ return isDraggablePiece({ piece, sourceSquare: square });
1845
+ }
1846
+ return true;
1847
+ }, [arePiecesDraggable, isDraggablePiece, onPieceDrop]);
1848
+ const tryCompleteMove = react.useCallback((from, to, piece) => {
1849
+ if (!onPieceDrop) {
1850
+ return false;
1851
+ }
1852
+ if (onPromotionCheck(from, to, piece)) {
1853
+ if (autoPromoteToQueen) {
1854
+ const promotedPiece = piece[0] === 'w' ? 'wQ' : 'bQ';
1855
+ const accepted = onPieceDrop(from, to, promotedPiece);
1856
+ clearSelection();
1857
+ return accepted;
1858
+ }
1859
+ setPendingPromotion({ from, to, piece });
1860
+ setMoveFrom(null);
1861
+ return true;
1862
+ }
1863
+ const accepted = onPieceDrop(from, to, piece);
1864
+ clearSelection();
1865
+ return accepted;
1866
+ }, [autoPromoteToQueen, clearSelection, onPieceDrop, onPromotionCheck]);
1867
+ const handleSquareClick = react.useCallback((square, piece) => {
1868
+ onSquareClick === null || onSquareClick === void 0 ? void 0 : onSquareClick(square, piece);
1869
+ if (!enabled || !onPieceDrop || !arePiecesDraggable || !fen) {
1870
+ return;
1871
+ }
1872
+ if (!moveFrom) {
1873
+ if (canSelectPiece(square, piece)) {
1874
+ setMoveFrom(square);
1875
+ }
1876
+ return;
1877
+ }
1878
+ if (square === moveFrom) {
1879
+ clearSelection();
1880
+ return;
1881
+ }
1882
+ const sourcePiece = pieceAtSquare(fen, moveFrom);
1883
+ if (!sourcePiece) {
1884
+ clearSelection();
1885
+ return;
1886
+ }
1887
+ const accepted = tryCompleteMove(moveFrom, square, sourcePiece);
1888
+ if (accepted) {
1889
+ return;
1890
+ }
1891
+ if (canSelectPiece(square, piece)) {
1892
+ setMoveFrom(square);
1893
+ return;
1894
+ }
1895
+ clearSelection();
1896
+ }, [
1897
+ arePiecesDraggable,
1898
+ canSelectPiece,
1899
+ clearSelection,
1900
+ enabled,
1901
+ fen,
1902
+ moveFrom,
1903
+ onPieceDrop,
1904
+ onSquareClick,
1905
+ tryCompleteMove,
1906
+ ]);
1907
+ const handlePromotionPieceSelect = react.useCallback((piece, promoteFromSquare, promoteToSquare) => {
1908
+ if (pendingPromotion && piece) {
1909
+ const { from, to } = pendingPromotion;
1910
+ onPieceDrop === null || onPieceDrop === void 0 ? void 0 : onPieceDrop(from, to, piece);
1911
+ onPromotionPieceSelect === null || onPromotionPieceSelect === void 0 ? void 0 : onPromotionPieceSelect(piece, from, to);
1912
+ clearSelection();
1913
+ return false;
1914
+ }
1915
+ if (onPromotionPieceSelect) {
1916
+ return onPromotionPieceSelect(piece, promoteFromSquare, promoteToSquare);
1917
+ }
1918
+ return true;
1919
+ }, [clearSelection, onPieceDrop, onPromotionPieceSelect, pendingPromotion]);
1920
+ const handlePieceDragBegin = react.useCallback((piece, sourceSquare) => {
1921
+ clearSelection();
1922
+ onPieceDragBegin === null || onPieceDragBegin === void 0 ? void 0 : onPieceDragBegin(piece, sourceSquare);
1923
+ }, [clearSelection, onPieceDragBegin]);
1924
+ return {
1925
+ clickSquareStyles,
1926
+ handleSquareClick,
1927
+ handlePromotionPieceSelect,
1928
+ handlePieceDragBegin,
1929
+ showPromotionDialog: pendingPromotion !== null,
1930
+ promotionToSquare: (_a = pendingPromotion === null || pendingPromotion === void 0 ? void 0 : pendingPromotion.to) !== null && _a !== void 0 ? _a : null,
1931
+ };
1932
+ }
1933
+
1934
+ /** Prevent mobile long-press text selection and iOS callout menus on the board. */
1935
+ const nonSelectableBoardStyle = {
1936
+ userSelect: 'none',
1937
+ WebkitUserSelect: 'none',
1938
+ WebkitTouchCallout: 'none',
1939
+ };
1748
1940
  const getCheckHighlighting = (checkSquare) => {
1749
1941
  const styles = {};
1750
1942
  styles[checkSquare] = { backgroundColor: boardSquareHighlightColors.check };
@@ -1763,12 +1955,40 @@ const getFeedbackHighlighting = (hintSquare, incorrectMoveSquare) => {
1763
1955
  return styles;
1764
1956
  };
1765
1957
  const HighlightChessboard = (_a) => {
1766
- var { checkSquare, hintSquare, incorrectMoveSquare, customSquareStyles: extraSquareStyles } = _a, props = __rest(_a, ["checkSquare", "hintSquare", "incorrectMoveSquare", "customSquareStyles"]);
1958
+ var { checkSquare, hintSquare, incorrectMoveSquare, correctMoveSquare = null, clickToMove, customSquareStyles: extraSquareStyles, customBoardStyle, onPieceDrop, position, arePiecesDraggable, autoPromoteToQueen, isDraggablePiece, onPromotionCheck, onSquareClick, onPromotionPieceSelect, onPieceDragBegin, showPromotionDialog: showPromotionDialogProp, promotionToSquare: promotionToSquareProp } = _a, props = __rest(_a, ["checkSquare", "hintSquare", "incorrectMoveSquare", "correctMoveSquare", "clickToMove", "customSquareStyles", "customBoardStyle", "onPieceDrop", "position", "arePiecesDraggable", "autoPromoteToQueen", "isDraggablePiece", "onPromotionCheck", "onSquareClick", "onPromotionPieceSelect", "onPieceDragBegin", "showPromotionDialog", "promotionToSquare"]);
1767
1959
  const { customDarkSquareStyle, customLightSquareStyle } = useChessboardTheme();
1960
+ const clickToMoveEnabled = clickToMove !== false && typeof onPieceDrop === 'function';
1961
+ const { clickSquareStyles, handleSquareClick, handlePromotionPieceSelect, handlePieceDragBegin, showPromotionDialog: clickPromotionDialog, promotionToSquare: clickPromotionToSquare, } = useClickToMove({
1962
+ enabled: clickToMoveEnabled,
1963
+ position,
1964
+ arePiecesDraggable,
1965
+ autoPromoteToQueen,
1966
+ isDraggablePiece,
1967
+ onPromotionCheck,
1968
+ onPieceDrop,
1969
+ onSquareClick,
1970
+ onPromotionPieceSelect,
1971
+ onPieceDragBegin,
1972
+ });
1768
1973
  const checkStyles = getCheckHighlighting(checkSquare);
1769
1974
  const feedbackStyles = getFeedbackHighlighting(hintSquare, incorrectMoveSquare);
1770
- const customSquareStyles = Object.assign(Object.assign(Object.assign({}, checkStyles), feedbackStyles), extraSquareStyles);
1771
- return (jsxRuntime.jsx(reactChessboard.Chessboard, Object.assign({ customDarkSquareStyle: customDarkSquareStyle, customLightSquareStyle: customLightSquareStyle, customSquareStyles: customSquareStyles }, props)));
1975
+ const customSquareStyles = Object.assign(Object.assign(Object.assign(Object.assign({}, clickSquareStyles), checkStyles), feedbackStyles), extraSquareStyles);
1976
+ const customSquare = react.useMemo(() => correctMoveSquare
1977
+ ? createFeedbackSquareRenderer(correctMoveSquare)
1978
+ : undefined, [correctMoveSquare]);
1979
+ const promotionControlProps = clickPromotionDialog
1980
+ ? {
1981
+ showPromotionDialog: true,
1982
+ promotionToSquare: clickPromotionToSquare,
1983
+ }
1984
+ : showPromotionDialogProp !== undefined ||
1985
+ promotionToSquareProp !== undefined
1986
+ ? {
1987
+ showPromotionDialog: showPromotionDialogProp,
1988
+ promotionToSquare: promotionToSquareProp,
1989
+ }
1990
+ : {};
1991
+ return (jsxRuntime.jsx(reactChessboard.Chessboard, Object.assign({ customDarkSquareStyle: customDarkSquareStyle, customLightSquareStyle: customLightSquareStyle, customSquareStyles: customSquareStyles, customSquare: customSquare, customBoardStyle: Object.assign(Object.assign({}, nonSelectableBoardStyle), customBoardStyle), position: position, arePiecesDraggable: arePiecesDraggable, autoPromoteToQueen: autoPromoteToQueen, isDraggablePiece: isDraggablePiece, onPromotionCheck: onPromotionCheck, onPieceDrop: onPieceDrop, onSquareClick: clickToMoveEnabled ? handleSquareClick : onSquareClick, onPromotionPieceSelect: clickToMoveEnabled ? handlePromotionPieceSelect : onPromotionPieceSelect, onPieceDragBegin: clickToMoveEnabled ? handlePieceDragBegin : onPieceDragBegin }, promotionControlProps, props)));
1772
1992
  };
1773
1993
 
1774
1994
  const emptyEngineEvaluation = () => ({
@@ -3504,6 +3724,37 @@ function useBoardRevision() {
3504
3724
  return { revision, bumpRevision };
3505
3725
  }
3506
3726
 
3727
+ /** Pause (ms) to show the correct-move check before advancing the line. */
3728
+ const CORRECT_MOVE_FEEDBACK_MS = 450;
3729
+
3730
+ function useCorrectMoveFeedback(delayMs = CORRECT_MOVE_FEEDBACK_MS) {
3731
+ const [correctMoveSquare, setCorrectMoveSquare] = react.useState(null);
3732
+ const timeoutRef = react.useRef(null);
3733
+ const clearCorrectMoveFeedback = react.useCallback(() => {
3734
+ if (timeoutRef.current !== null) {
3735
+ window.clearTimeout(timeoutRef.current);
3736
+ timeoutRef.current = null;
3737
+ }
3738
+ setCorrectMoveSquare(null);
3739
+ }, []);
3740
+ const showCorrectMove = react.useCallback((targetSquare, onComplete) => {
3741
+ clearCorrectMoveFeedback();
3742
+ setCorrectMoveSquare(targetSquare);
3743
+ timeoutRef.current = window.setTimeout(() => {
3744
+ timeoutRef.current = null;
3745
+ setCorrectMoveSquare(null);
3746
+ onComplete === null || onComplete === void 0 ? void 0 : onComplete();
3747
+ }, delayMs);
3748
+ }, [clearCorrectMoveFeedback, delayMs]);
3749
+ react.useEffect(() => clearCorrectMoveFeedback, [clearCorrectMoveFeedback]);
3750
+ return {
3751
+ correctMoveSquare,
3752
+ showCorrectMove,
3753
+ clearCorrectMoveFeedback,
3754
+ isShowingCorrectMove: correctMoveSquare !== null,
3755
+ };
3756
+ }
3757
+
3507
3758
  /** Minimum eval loss (pawns) from the wrong move before showing a refutation. */
3508
3759
  const REFUTATION_EVAL_GAP_PAWNS = 0.5;
3509
3760
  const REFUTATION_EVAL_GAP_CP = REFUTATION_EVAL_GAP_PAWNS * 100;
@@ -3821,8 +4072,10 @@ exports.AnalysisErrorBoundary = AnalysisErrorBoundary;
3821
4072
  exports.AnalysisPosition = AnalysisPosition;
3822
4073
  exports.BOARD_THEMES = BOARD_THEMES;
3823
4074
  exports.BOARD_THEME_IDS = BOARD_THEME_IDS;
4075
+ exports.CORRECT_MOVE_FEEDBACK_MS = CORRECT_MOVE_FEEDBACK_MS;
3824
4076
  exports.ChessboardDnDProvider = ChessboardDnDProvider;
3825
4077
  exports.ChessboardThemeContext = ChessboardThemeContext;
4078
+ exports.CorrectMoveCheckBadge = CorrectMoveCheckBadge;
3826
4079
  exports.DEFAULT_ANALYSIS_LAYOUT = DEFAULT_ANALYSIS_LAYOUT;
3827
4080
  exports.DEFAULT_BOARD_THEME = DEFAULT_BOARD_THEME;
3828
4081
  exports.DEFAULT_STOCKFISH_SCRIPT_URL = DEFAULT_STOCKFISH_SCRIPT_URL;
@@ -3889,6 +4142,7 @@ exports.useAnalysisEngine = useAnalysisEngine;
3889
4142
  exports.useAnalysisEngineContext = useAnalysisEngineContext;
3890
4143
  exports.useBoardRevision = useBoardRevision;
3891
4144
  exports.useChessboardTheme = useChessboardTheme;
4145
+ exports.useCorrectMoveFeedback = useCorrectMoveFeedback;
3892
4146
  exports.useMissBoard = useMissBoard;
3893
4147
  exports.useMissRefutation = useMissRefutation;
3894
4148
  exports.useMissSequence = useMissSequence;
@@ -5,3 +5,5 @@ export default meta;
5
5
  type Story = StoryObj<typeof HighlightChessboard>;
6
6
  export declare const StartingPosition: Story;
7
7
  export declare const WithHint: Story;
8
+ export declare const ClickToMove: Story;
9
+ export declare const WithCorrectMoveCheck: Story;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-chess-core",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Shared React chessboard theme, highlights, browser Stockfish, and analysis board",
5
5
  "license": "MIT",
6
6
  "author": "Robert Blackwell",