react-chess-core 0.1.2 → 0.1.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.
@@ -49,6 +49,7 @@ export declare class AnalysisPosition {
49
49
  from: string;
50
50
  to: string;
51
51
  } | null;
52
+ getLastMoveUci(): string | null;
52
53
  fen(): string;
53
54
  getCheckSquare(): string;
54
55
  }
@@ -25,6 +25,7 @@ export type AnalysisBoardModel = {
25
25
  from: string;
26
26
  to: string;
27
27
  } | null;
28
+ lastMoveUci: string | null;
28
29
  checkSquare: string | null;
29
30
  onSelectPly: (ply: number) => void;
30
31
  onSelectHistoryRow: (row: AnalysisHistoryRow) => void;
@@ -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,14 @@ 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
+ /** UCI of the move that led to the current position (shows a last-move arrow). */
8
+ lastMoveUci?: string | null;
9
+ /** Override the default last-move arrow color. */
10
+ lastMoveArrowColor?: string;
11
+ /** Enable click-to-move when `onPieceDrop` is provided. Defaults to true. */
12
+ clickToMove?: boolean;
5
13
  [key: string]: any;
6
14
  }
7
- export declare const HighlightChessboard: ({ checkSquare, hintSquare, incorrectMoveSquare, customSquareStyles: extraSquareStyles, ...props }: HighlightChessboardProps) => import("react").JSX.Element;
15
+ export declare const HighlightChessboard: ({ checkSquare, hintSquare, incorrectMoveSquare, correctMoveSquare, lastMoveUci, lastMoveArrowColor, clickToMove, customSquareStyles: extraSquareStyles, customArrows, 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,6 @@ 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';
7
+ export * from './lastMoveArrow';
@@ -0,0 +1,8 @@
1
+ export type ChessboardArrow = [string, string, string];
2
+ /** Subtle green arrow visible on light and dark boards (Lichess-style). */
3
+ export declare const DEFAULT_LAST_MOVE_ARROW_COLOR = "rgba(155, 199, 0, 0.85)";
4
+ export declare const uciToArrow: (uci: string, color?: string) => ChessboardArrow;
5
+ export declare const lastMoveArrowFromUci: (uci: string | null | undefined, color?: string) => ChessboardArrow[];
6
+ /** UCI of the move that produced the position at {@link plyIndex}. */
7
+ export declare const lastMoveUciAtPly: (movesUci: readonly string[], plyIndex: number) => string | null;
8
+ export declare const mergeCustomArrowsWithLastMove: (customArrows: ChessboardArrow[] | undefined, lastMoveUci: string | null | undefined, lastMoveArrowColor?: string) => ChessboardArrow[];
@@ -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';
@@ -7,6 +7,7 @@ export type MissSequenceState = {
7
7
  export type MissDisplay = {
8
8
  fen: string | null;
9
9
  arrows: [string, string, string][];
10
+ lastMoveUci: string | null;
10
11
  animating: boolean;
11
12
  };
12
13
  export declare const MISS_WRONG_PAUSE_MS = 450;
@@ -19,6 +19,7 @@ export declare function useMissBoard({ feedback, expectedUci, positionFen, answe
19
19
  customArrows: [string, string, string][];
20
20
  boardPosition: string;
21
21
  boardAnimating: boolean;
22
+ lastMoveUci: string | null;
22
23
  wrapDropHandler: (onDrop: (source: string, target: string, piece: string) => boolean, { enabled, dropFen, expectedMoveUci, }: {
23
24
  enabled: boolean;
24
25
  dropFen?: string;
@@ -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,225 @@ 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
+ /** Subtle green arrow visible on light and dark boards (Lichess-style). */
1785
+ const DEFAULT_LAST_MOVE_ARROW_COLOR = 'rgba(155, 199, 0, 0.85)';
1786
+ const uciToArrow = (uci, color = DEFAULT_LAST_MOVE_ARROW_COLOR) => [uci.slice(0, 2), uci.slice(2, 4), color];
1787
+ const lastMoveArrowFromUci = (uci, color = DEFAULT_LAST_MOVE_ARROW_COLOR) => {
1788
+ if (!uci || uci.length < 4) {
1789
+ return [];
1790
+ }
1791
+ return [uciToArrow(uci, color)];
1792
+ };
1793
+ /** UCI of the move that produced the position at {@link plyIndex}. */
1794
+ const lastMoveUciAtPly = (movesUci, plyIndex) => {
1795
+ var _a;
1796
+ if (plyIndex <= 0) {
1797
+ return null;
1798
+ }
1799
+ return (_a = movesUci[plyIndex - 1]) !== null && _a !== void 0 ? _a : null;
1800
+ };
1801
+ const mergeCustomArrowsWithLastMove = (customArrows, lastMoveUci, lastMoveArrowColor) => {
1802
+ const lastMove = lastMoveArrowFromUci(lastMoveUci, lastMoveArrowColor);
1803
+ if (!lastMove.length) {
1804
+ return customArrows !== null && customArrows !== void 0 ? customArrows : [];
1805
+ }
1806
+ return [...lastMove, ...(customArrows !== null && customArrows !== void 0 ? customArrows : [])];
1807
+ };
1808
+
1809
+ function defaultPromotionCheck(sourceSquare, targetSquare, piece) {
1810
+ return (((piece === 'wP' && sourceSquare[1] === '7' && targetSquare[1] === '8') ||
1811
+ (piece === 'bP' && sourceSquare[1] === '2' && targetSquare[1] === '1')) &&
1812
+ Math.abs(sourceSquare.charCodeAt(0) - targetSquare.charCodeAt(0)) <= 1);
1813
+ }
1814
+ function pieceAtSquare(fen, square) {
1815
+ const boardPiece = new Chess(fen).get(square);
1816
+ if (!boardPiece) {
1817
+ return null;
1818
+ }
1819
+ const color = boardPiece.color === 'w' ? 'w' : 'b';
1820
+ const type = boardPiece.type.toUpperCase();
1821
+ return `${color}${type}`;
1822
+ }
1823
+ function getMoveOptionStyles(fen, fromSquare) {
1824
+ const chess = new Chess(fen);
1825
+ const moves = chess.moves({ square: fromSquare, verbose: true });
1826
+ if (!moves.length) {
1827
+ return {
1828
+ [fromSquare]: { backgroundColor: boardSquareHighlightColors.selected },
1829
+ };
1830
+ }
1831
+ const styles = {
1832
+ [fromSquare]: { backgroundColor: boardSquareHighlightColors.selected },
1833
+ };
1834
+ for (const move of moves) {
1835
+ styles[move.to] = {
1836
+ background: chess.get(move.to)
1837
+ ? boardSquareHighlightColors.captureTarget
1838
+ : boardSquareHighlightColors.moveTarget,
1839
+ borderRadius: '50%',
1840
+ };
1841
+ }
1842
+ return styles;
1843
+ }
1844
+ function useClickToMove({ enabled, position, arePiecesDraggable = true, autoPromoteToQueen = false, isDraggablePiece, onPromotionCheck = defaultPromotionCheck, onPieceDrop, onSquareClick, onPromotionPieceSelect, onPieceDragBegin, }) {
1845
+ var _a;
1846
+ const [moveFrom, setMoveFrom] = useState(null);
1847
+ const [pendingPromotion, setPendingPromotion] = useState(null);
1848
+ const fen = typeof position === 'string' ? position : undefined;
1849
+ const clearSelection = useCallback(() => {
1850
+ setMoveFrom(null);
1851
+ setPendingPromotion(null);
1852
+ }, []);
1853
+ useEffect(() => {
1854
+ clearSelection();
1855
+ }, [fen, clearSelection]);
1856
+ const clickSquareStyles = useMemo(() => {
1857
+ if (!enabled || !moveFrom || !fen) {
1858
+ return {};
1859
+ }
1860
+ return getMoveOptionStyles(fen, moveFrom);
1861
+ }, [enabled, fen, moveFrom]);
1862
+ const canSelectPiece = useCallback((square, piece) => {
1863
+ if (!piece || !arePiecesDraggable || !onPieceDrop) {
1864
+ return false;
1865
+ }
1866
+ if (isDraggablePiece) {
1867
+ return isDraggablePiece({ piece, sourceSquare: square });
1868
+ }
1869
+ return true;
1870
+ }, [arePiecesDraggable, isDraggablePiece, onPieceDrop]);
1871
+ const tryCompleteMove = useCallback((from, to, piece) => {
1872
+ if (!onPieceDrop) {
1873
+ return false;
1874
+ }
1875
+ if (onPromotionCheck(from, to, piece)) {
1876
+ if (autoPromoteToQueen) {
1877
+ const promotedPiece = piece[0] === 'w' ? 'wQ' : 'bQ';
1878
+ const accepted = onPieceDrop(from, to, promotedPiece);
1879
+ clearSelection();
1880
+ return accepted;
1881
+ }
1882
+ setPendingPromotion({ from, to, piece });
1883
+ setMoveFrom(null);
1884
+ return true;
1885
+ }
1886
+ const accepted = onPieceDrop(from, to, piece);
1887
+ clearSelection();
1888
+ return accepted;
1889
+ }, [autoPromoteToQueen, clearSelection, onPieceDrop, onPromotionCheck]);
1890
+ const handleSquareClick = useCallback((square, piece) => {
1891
+ onSquareClick === null || onSquareClick === void 0 ? void 0 : onSquareClick(square, piece);
1892
+ if (!enabled || !onPieceDrop || !arePiecesDraggable || !fen) {
1893
+ return;
1894
+ }
1895
+ if (!moveFrom) {
1896
+ if (canSelectPiece(square, piece)) {
1897
+ setMoveFrom(square);
1898
+ }
1899
+ return;
1900
+ }
1901
+ if (square === moveFrom) {
1902
+ clearSelection();
1903
+ return;
1904
+ }
1905
+ const sourcePiece = pieceAtSquare(fen, moveFrom);
1906
+ if (!sourcePiece) {
1907
+ clearSelection();
1908
+ return;
1909
+ }
1910
+ const accepted = tryCompleteMove(moveFrom, square, sourcePiece);
1911
+ if (accepted) {
1912
+ return;
1913
+ }
1914
+ if (canSelectPiece(square, piece)) {
1915
+ setMoveFrom(square);
1916
+ return;
1917
+ }
1918
+ clearSelection();
1919
+ }, [
1920
+ arePiecesDraggable,
1921
+ canSelectPiece,
1922
+ clearSelection,
1923
+ enabled,
1924
+ fen,
1925
+ moveFrom,
1926
+ onPieceDrop,
1927
+ onSquareClick,
1928
+ tryCompleteMove,
1929
+ ]);
1930
+ const handlePromotionPieceSelect = useCallback((piece, promoteFromSquare, promoteToSquare) => {
1931
+ if (pendingPromotion && piece) {
1932
+ const { from, to } = pendingPromotion;
1933
+ onPieceDrop === null || onPieceDrop === void 0 ? void 0 : onPieceDrop(from, to, piece);
1934
+ onPromotionPieceSelect === null || onPromotionPieceSelect === void 0 ? void 0 : onPromotionPieceSelect(piece, from, to);
1935
+ clearSelection();
1936
+ return false;
1937
+ }
1938
+ if (onPromotionPieceSelect) {
1939
+ return onPromotionPieceSelect(piece, promoteFromSquare, promoteToSquare);
1940
+ }
1941
+ return true;
1942
+ }, [clearSelection, onPieceDrop, onPromotionPieceSelect, pendingPromotion]);
1943
+ const handlePieceDragBegin = useCallback((piece, sourceSquare) => {
1944
+ clearSelection();
1945
+ onPieceDragBegin === null || onPieceDragBegin === void 0 ? void 0 : onPieceDragBegin(piece, sourceSquare);
1946
+ }, [clearSelection, onPieceDragBegin]);
1947
+ return {
1948
+ clickSquareStyles,
1949
+ handleSquareClick,
1950
+ handlePromotionPieceSelect,
1951
+ handlePieceDragBegin,
1952
+ showPromotionDialog: pendingPromotion !== null,
1953
+ promotionToSquare: (_a = pendingPromotion === null || pendingPromotion === void 0 ? void 0 : pendingPromotion.to) !== null && _a !== void 0 ? _a : null,
1954
+ };
1955
+ }
1956
+
1957
+ /** Prevent mobile long-press text selection and iOS callout menus on the board. */
1958
+ const nonSelectableBoardStyle = {
1959
+ userSelect: 'none',
1960
+ WebkitUserSelect: 'none',
1961
+ WebkitTouchCallout: 'none',
1962
+ };
1746
1963
  const getCheckHighlighting = (checkSquare) => {
1747
1964
  const styles = {};
1748
1965
  styles[checkSquare] = { backgroundColor: boardSquareHighlightColors.check };
@@ -1761,12 +1978,41 @@ const getFeedbackHighlighting = (hintSquare, incorrectMoveSquare) => {
1761
1978
  return styles;
1762
1979
  };
1763
1980
  const HighlightChessboard = (_a) => {
1764
- var { checkSquare, hintSquare, incorrectMoveSquare, customSquareStyles: extraSquareStyles } = _a, props = __rest(_a, ["checkSquare", "hintSquare", "incorrectMoveSquare", "customSquareStyles"]);
1981
+ var { checkSquare, hintSquare, incorrectMoveSquare, correctMoveSquare = null, lastMoveUci = null, lastMoveArrowColor, clickToMove, customSquareStyles: extraSquareStyles, customArrows, customBoardStyle, onPieceDrop, position, arePiecesDraggable, autoPromoteToQueen, isDraggablePiece, onPromotionCheck, onSquareClick, onPromotionPieceSelect, onPieceDragBegin, showPromotionDialog: showPromotionDialogProp, promotionToSquare: promotionToSquareProp } = _a, props = __rest(_a, ["checkSquare", "hintSquare", "incorrectMoveSquare", "correctMoveSquare", "lastMoveUci", "lastMoveArrowColor", "clickToMove", "customSquareStyles", "customArrows", "customBoardStyle", "onPieceDrop", "position", "arePiecesDraggable", "autoPromoteToQueen", "isDraggablePiece", "onPromotionCheck", "onSquareClick", "onPromotionPieceSelect", "onPieceDragBegin", "showPromotionDialog", "promotionToSquare"]);
1765
1982
  const { customDarkSquareStyle, customLightSquareStyle } = useChessboardTheme();
1983
+ const clickToMoveEnabled = clickToMove !== false && typeof onPieceDrop === 'function';
1984
+ const { clickSquareStyles, handleSquareClick, handlePromotionPieceSelect, handlePieceDragBegin, showPromotionDialog: clickPromotionDialog, promotionToSquare: clickPromotionToSquare, } = useClickToMove({
1985
+ enabled: clickToMoveEnabled,
1986
+ position,
1987
+ arePiecesDraggable,
1988
+ autoPromoteToQueen,
1989
+ isDraggablePiece,
1990
+ onPromotionCheck,
1991
+ onPieceDrop,
1992
+ onSquareClick,
1993
+ onPromotionPieceSelect,
1994
+ onPieceDragBegin,
1995
+ });
1766
1996
  const checkStyles = getCheckHighlighting(checkSquare);
1767
1997
  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)));
1998
+ const customSquareStyles = Object.assign(Object.assign(Object.assign(Object.assign({}, clickSquareStyles), checkStyles), feedbackStyles), extraSquareStyles);
1999
+ const customSquare = useMemo(() => correctMoveSquare
2000
+ ? createFeedbackSquareRenderer(correctMoveSquare)
2001
+ : undefined, [correctMoveSquare]);
2002
+ const mergedCustomArrows = useMemo(() => mergeCustomArrowsWithLastMove(customArrows, lastMoveUci, lastMoveArrowColor), [customArrows, lastMoveArrowColor, lastMoveUci]);
2003
+ const promotionControlProps = clickPromotionDialog
2004
+ ? {
2005
+ showPromotionDialog: true,
2006
+ promotionToSquare: clickPromotionToSquare,
2007
+ }
2008
+ : showPromotionDialogProp !== undefined ||
2009
+ promotionToSquareProp !== undefined
2010
+ ? {
2011
+ showPromotionDialog: showPromotionDialogProp,
2012
+ promotionToSquare: promotionToSquareProp,
2013
+ }
2014
+ : {};
2015
+ 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, customArrows: mergedCustomArrows }, promotionControlProps, props)));
1770
2016
  };
1771
2017
 
1772
2018
  const emptyEngineEvaluation = () => ({
@@ -2694,9 +2940,7 @@ const PlyNavigation = ({ plyIndex, totalPly, canPrev, canNext, onGoFirst, onGoPr
2694
2940
  /** Draggable analysis board (no surrounding layout chrome). */
2695
2941
  const AnalysisChessboardView = ({ model }) => {
2696
2942
  var _a;
2697
- return (jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { checkSquare: (_a = model.checkSquare) !== null && _a !== void 0 ? _a : '', hintSquare: null, incorrectMoveSquare: null, position: model.fen, boardOrientation: model.boardOrientation, boardWidth: model.boardWidth, arePiecesDraggable: true, onPieceDrop: model.onPieceDrop, promotionDialogVariant: "modal", customSquareStyles: model.lastMove
2698
- ? getLastMoveSquareStyles(model.lastMove.from, model.lastMove.to, model.theme)
2699
- : {} }) }));
2943
+ return (jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { checkSquare: (_a = model.checkSquare) !== null && _a !== void 0 ? _a : '', hintSquare: null, incorrectMoveSquare: null, position: model.fen, boardOrientation: model.boardOrientation, boardWidth: model.boardWidth, arePiecesDraggable: true, onPieceDrop: model.onPieceDrop, promotionDialogVariant: "modal", lastMoveUci: model.lastMoveUci }) }));
2700
2944
  };
2701
2945
 
2702
2946
  class AnalysisPosition {
@@ -2952,22 +3196,26 @@ class AnalysisPosition {
2952
3196
  return true;
2953
3197
  }
2954
3198
  getLastMoveSquares() {
2955
- const navPly = this.getNavPly();
2956
- if (navPly === 0) {
3199
+ const uci = this.getLastMoveUci();
3200
+ if (!uci) {
2957
3201
  return null;
2958
3202
  }
2959
- let uci;
2960
- if (this.variation && this.variationCursor > 0) {
2961
- uci = this.variation.moves[this.variationCursor - 1];
2962
- }
2963
- else {
2964
- uci = this.solutionMoves[this.mainPly - 1];
2965
- }
2966
3203
  return {
2967
3204
  from: uci.slice(0, 2),
2968
3205
  to: uci.slice(2, 4),
2969
3206
  };
2970
3207
  }
3208
+ getLastMoveUci() {
3209
+ var _a;
3210
+ const navPly = this.getNavPly();
3211
+ if (navPly === 0) {
3212
+ return null;
3213
+ }
3214
+ if (this.variation && this.variationCursor > 0) {
3215
+ return this.variation.moves[this.variationCursor - 1];
3216
+ }
3217
+ return (_a = this.solutionMoves[this.mainPly - 1]) !== null && _a !== void 0 ? _a : null;
3218
+ }
2971
3219
  fen() {
2972
3220
  return this.chess.fen();
2973
3221
  }
@@ -3012,6 +3260,7 @@ const useAnalysisBoardModel = ({ analysisContext, onClose, theme, boardWidth, en
3012
3260
  engineEvaluation,
3013
3261
  engineEnabled,
3014
3262
  lastMove: analysisPosition.getLastMoveSquares(),
3263
+ lastMoveUci: analysisPosition.getLastMoveUci(),
3015
3264
  checkSquare: analysisPosition.getCheckSquare(),
3016
3265
  onSelectPly: (ply) => {
3017
3266
  analysisPosition.goToNavPly(ply);
@@ -3502,6 +3751,37 @@ function useBoardRevision() {
3502
3751
  return { revision, bumpRevision };
3503
3752
  }
3504
3753
 
3754
+ /** Pause (ms) to show the correct-move check before advancing the line. */
3755
+ const CORRECT_MOVE_FEEDBACK_MS = 450;
3756
+
3757
+ function useCorrectMoveFeedback(delayMs = CORRECT_MOVE_FEEDBACK_MS) {
3758
+ const [correctMoveSquare, setCorrectMoveSquare] = useState(null);
3759
+ const timeoutRef = useRef(null);
3760
+ const clearCorrectMoveFeedback = useCallback(() => {
3761
+ if (timeoutRef.current !== null) {
3762
+ window.clearTimeout(timeoutRef.current);
3763
+ timeoutRef.current = null;
3764
+ }
3765
+ setCorrectMoveSquare(null);
3766
+ }, []);
3767
+ const showCorrectMove = useCallback((targetSquare, onComplete) => {
3768
+ clearCorrectMoveFeedback();
3769
+ setCorrectMoveSquare(targetSquare);
3770
+ timeoutRef.current = window.setTimeout(() => {
3771
+ timeoutRef.current = null;
3772
+ setCorrectMoveSquare(null);
3773
+ onComplete === null || onComplete === void 0 ? void 0 : onComplete();
3774
+ }, delayMs);
3775
+ }, [clearCorrectMoveFeedback, delayMs]);
3776
+ useEffect(() => clearCorrectMoveFeedback, [clearCorrectMoveFeedback]);
3777
+ return {
3778
+ correctMoveSquare,
3779
+ showCorrectMove,
3780
+ clearCorrectMoveFeedback,
3781
+ isShowingCorrectMove: correctMoveSquare !== null,
3782
+ };
3783
+ }
3784
+
3505
3785
  /** Minimum eval loss (pawns) from the wrong move before showing a refutation. */
3506
3786
  const REFUTATION_EVAL_GAP_PAWNS = 0.5;
3507
3787
  const REFUTATION_EVAL_GAP_CP = REFUTATION_EVAL_GAP_PAWNS * 100;
@@ -3595,6 +3875,7 @@ function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor)
3595
3875
  return {
3596
3876
  fen: null,
3597
3877
  arrows: [],
3878
+ lastMoveUci: null,
3598
3879
  animating: false,
3599
3880
  };
3600
3881
  }
@@ -3605,6 +3886,7 @@ function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor)
3605
3886
  return {
3606
3887
  fen: fenAfterWrong !== null && fenAfterWrong !== void 0 ? fenAfterWrong : setupFen,
3607
3888
  arrows: [],
3889
+ lastMoveUci: attemptedUci,
3608
3890
  animating: false,
3609
3891
  };
3610
3892
  case 'refutation': {
@@ -3614,6 +3896,7 @@ function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor)
3614
3896
  return {
3615
3897
  fen: (_a = fenAfterRefutation !== null && fenAfterRefutation !== void 0 ? fenAfterRefutation : fenAfterWrong) !== null && _a !== void 0 ? _a : setupFen,
3616
3898
  arrows: [],
3899
+ lastMoveUci: refutationUci,
3617
3900
  animating: Boolean(fenAfterRefutation),
3618
3901
  };
3619
3902
  }
@@ -3621,18 +3904,21 @@ function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor)
3621
3904
  return {
3622
3905
  fen: setupFen,
3623
3906
  arrows: [],
3907
+ lastMoveUci: null,
3624
3908
  animating: false,
3625
3909
  };
3626
3910
  case 'answer':
3627
3911
  return {
3628
3912
  fen: setupFen,
3629
3913
  arrows: expectedMoveArrow(expectedUci, answerArrowColor),
3914
+ lastMoveUci: null,
3630
3915
  animating: false,
3631
3916
  };
3632
3917
  default:
3633
3918
  return {
3634
3919
  fen: setupFen,
3635
3920
  arrows: [],
3921
+ lastMoveUci: null,
3636
3922
  animating: false,
3637
3923
  };
3638
3924
  }
@@ -3805,8 +4091,9 @@ function useMissBoard({ feedback, expectedUci, positionFen, answerArrowColor, au
3805
4091
  customArrows,
3806
4092
  boardPosition,
3807
4093
  boardAnimating: missSequence.display.animating,
4094
+ lastMoveUci: missSequence.display.lastMoveUci,
3808
4095
  wrapDropHandler,
3809
4096
  };
3810
4097
  }
3811
4098
 
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 };
4099
+ 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_LAST_MOVE_ARROW_COLOR, 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, lastMoveArrowFromUci, lastMoveUciAtPly, lineEvalCpForGap, matchesExpectedUci, mergeCustomArrowsWithLastMove, navButtonStyle, navRowStyle, normalizeEvalForWhite, normalizePvMoves, normalizeSubscriberOptions, parseUciInfoLine, parseUciMove, plyLabelStyle, palette as plyNavigationPalette, refutationEngineOptions, refutationEvalGapCp, refutationFromEvaluation, resolveStockfishScriptUrl, resolveStockfishWasmUrl, resolveStockfishWorkerUrl, scrubberInputStyle, splitWorkerLines, uciFromDrop, uciPvToSan, uciToArrow, useAnalysisBoardModel, useAnalysisEngine, useAnalysisEngineContext, useBoardRevision, useChessboardTheme, useCorrectMoveFeedback, useMissBoard, useMissRefutation, useMissSequence, usePositionKeyboardNav, useTheme };
package/dist/index.js CHANGED
@@ -1743,8 +1743,225 @@ 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
+ /** Subtle green arrow visible on light and dark boards (Lichess-style). */
1787
+ const DEFAULT_LAST_MOVE_ARROW_COLOR = 'rgba(155, 199, 0, 0.85)';
1788
+ const uciToArrow = (uci, color = DEFAULT_LAST_MOVE_ARROW_COLOR) => [uci.slice(0, 2), uci.slice(2, 4), color];
1789
+ const lastMoveArrowFromUci = (uci, color = DEFAULT_LAST_MOVE_ARROW_COLOR) => {
1790
+ if (!uci || uci.length < 4) {
1791
+ return [];
1792
+ }
1793
+ return [uciToArrow(uci, color)];
1794
+ };
1795
+ /** UCI of the move that produced the position at {@link plyIndex}. */
1796
+ const lastMoveUciAtPly = (movesUci, plyIndex) => {
1797
+ var _a;
1798
+ if (plyIndex <= 0) {
1799
+ return null;
1800
+ }
1801
+ return (_a = movesUci[plyIndex - 1]) !== null && _a !== void 0 ? _a : null;
1802
+ };
1803
+ const mergeCustomArrowsWithLastMove = (customArrows, lastMoveUci, lastMoveArrowColor) => {
1804
+ const lastMove = lastMoveArrowFromUci(lastMoveUci, lastMoveArrowColor);
1805
+ if (!lastMove.length) {
1806
+ return customArrows !== null && customArrows !== void 0 ? customArrows : [];
1807
+ }
1808
+ return [...lastMove, ...(customArrows !== null && customArrows !== void 0 ? customArrows : [])];
1809
+ };
1810
+
1811
+ function defaultPromotionCheck(sourceSquare, targetSquare, piece) {
1812
+ return (((piece === 'wP' && sourceSquare[1] === '7' && targetSquare[1] === '8') ||
1813
+ (piece === 'bP' && sourceSquare[1] === '2' && targetSquare[1] === '1')) &&
1814
+ Math.abs(sourceSquare.charCodeAt(0) - targetSquare.charCodeAt(0)) <= 1);
1815
+ }
1816
+ function pieceAtSquare(fen, square) {
1817
+ const boardPiece = new chess_js.Chess(fen).get(square);
1818
+ if (!boardPiece) {
1819
+ return null;
1820
+ }
1821
+ const color = boardPiece.color === 'w' ? 'w' : 'b';
1822
+ const type = boardPiece.type.toUpperCase();
1823
+ return `${color}${type}`;
1824
+ }
1825
+ function getMoveOptionStyles(fen, fromSquare) {
1826
+ const chess = new chess_js.Chess(fen);
1827
+ const moves = chess.moves({ square: fromSquare, verbose: true });
1828
+ if (!moves.length) {
1829
+ return {
1830
+ [fromSquare]: { backgroundColor: boardSquareHighlightColors.selected },
1831
+ };
1832
+ }
1833
+ const styles = {
1834
+ [fromSquare]: { backgroundColor: boardSquareHighlightColors.selected },
1835
+ };
1836
+ for (const move of moves) {
1837
+ styles[move.to] = {
1838
+ background: chess.get(move.to)
1839
+ ? boardSquareHighlightColors.captureTarget
1840
+ : boardSquareHighlightColors.moveTarget,
1841
+ borderRadius: '50%',
1842
+ };
1843
+ }
1844
+ return styles;
1845
+ }
1846
+ function useClickToMove({ enabled, position, arePiecesDraggable = true, autoPromoteToQueen = false, isDraggablePiece, onPromotionCheck = defaultPromotionCheck, onPieceDrop, onSquareClick, onPromotionPieceSelect, onPieceDragBegin, }) {
1847
+ var _a;
1848
+ const [moveFrom, setMoveFrom] = react.useState(null);
1849
+ const [pendingPromotion, setPendingPromotion] = react.useState(null);
1850
+ const fen = typeof position === 'string' ? position : undefined;
1851
+ const clearSelection = react.useCallback(() => {
1852
+ setMoveFrom(null);
1853
+ setPendingPromotion(null);
1854
+ }, []);
1855
+ react.useEffect(() => {
1856
+ clearSelection();
1857
+ }, [fen, clearSelection]);
1858
+ const clickSquareStyles = react.useMemo(() => {
1859
+ if (!enabled || !moveFrom || !fen) {
1860
+ return {};
1861
+ }
1862
+ return getMoveOptionStyles(fen, moveFrom);
1863
+ }, [enabled, fen, moveFrom]);
1864
+ const canSelectPiece = react.useCallback((square, piece) => {
1865
+ if (!piece || !arePiecesDraggable || !onPieceDrop) {
1866
+ return false;
1867
+ }
1868
+ if (isDraggablePiece) {
1869
+ return isDraggablePiece({ piece, sourceSquare: square });
1870
+ }
1871
+ return true;
1872
+ }, [arePiecesDraggable, isDraggablePiece, onPieceDrop]);
1873
+ const tryCompleteMove = react.useCallback((from, to, piece) => {
1874
+ if (!onPieceDrop) {
1875
+ return false;
1876
+ }
1877
+ if (onPromotionCheck(from, to, piece)) {
1878
+ if (autoPromoteToQueen) {
1879
+ const promotedPiece = piece[0] === 'w' ? 'wQ' : 'bQ';
1880
+ const accepted = onPieceDrop(from, to, promotedPiece);
1881
+ clearSelection();
1882
+ return accepted;
1883
+ }
1884
+ setPendingPromotion({ from, to, piece });
1885
+ setMoveFrom(null);
1886
+ return true;
1887
+ }
1888
+ const accepted = onPieceDrop(from, to, piece);
1889
+ clearSelection();
1890
+ return accepted;
1891
+ }, [autoPromoteToQueen, clearSelection, onPieceDrop, onPromotionCheck]);
1892
+ const handleSquareClick = react.useCallback((square, piece) => {
1893
+ onSquareClick === null || onSquareClick === void 0 ? void 0 : onSquareClick(square, piece);
1894
+ if (!enabled || !onPieceDrop || !arePiecesDraggable || !fen) {
1895
+ return;
1896
+ }
1897
+ if (!moveFrom) {
1898
+ if (canSelectPiece(square, piece)) {
1899
+ setMoveFrom(square);
1900
+ }
1901
+ return;
1902
+ }
1903
+ if (square === moveFrom) {
1904
+ clearSelection();
1905
+ return;
1906
+ }
1907
+ const sourcePiece = pieceAtSquare(fen, moveFrom);
1908
+ if (!sourcePiece) {
1909
+ clearSelection();
1910
+ return;
1911
+ }
1912
+ const accepted = tryCompleteMove(moveFrom, square, sourcePiece);
1913
+ if (accepted) {
1914
+ return;
1915
+ }
1916
+ if (canSelectPiece(square, piece)) {
1917
+ setMoveFrom(square);
1918
+ return;
1919
+ }
1920
+ clearSelection();
1921
+ }, [
1922
+ arePiecesDraggable,
1923
+ canSelectPiece,
1924
+ clearSelection,
1925
+ enabled,
1926
+ fen,
1927
+ moveFrom,
1928
+ onPieceDrop,
1929
+ onSquareClick,
1930
+ tryCompleteMove,
1931
+ ]);
1932
+ const handlePromotionPieceSelect = react.useCallback((piece, promoteFromSquare, promoteToSquare) => {
1933
+ if (pendingPromotion && piece) {
1934
+ const { from, to } = pendingPromotion;
1935
+ onPieceDrop === null || onPieceDrop === void 0 ? void 0 : onPieceDrop(from, to, piece);
1936
+ onPromotionPieceSelect === null || onPromotionPieceSelect === void 0 ? void 0 : onPromotionPieceSelect(piece, from, to);
1937
+ clearSelection();
1938
+ return false;
1939
+ }
1940
+ if (onPromotionPieceSelect) {
1941
+ return onPromotionPieceSelect(piece, promoteFromSquare, promoteToSquare);
1942
+ }
1943
+ return true;
1944
+ }, [clearSelection, onPieceDrop, onPromotionPieceSelect, pendingPromotion]);
1945
+ const handlePieceDragBegin = react.useCallback((piece, sourceSquare) => {
1946
+ clearSelection();
1947
+ onPieceDragBegin === null || onPieceDragBegin === void 0 ? void 0 : onPieceDragBegin(piece, sourceSquare);
1948
+ }, [clearSelection, onPieceDragBegin]);
1949
+ return {
1950
+ clickSquareStyles,
1951
+ handleSquareClick,
1952
+ handlePromotionPieceSelect,
1953
+ handlePieceDragBegin,
1954
+ showPromotionDialog: pendingPromotion !== null,
1955
+ promotionToSquare: (_a = pendingPromotion === null || pendingPromotion === void 0 ? void 0 : pendingPromotion.to) !== null && _a !== void 0 ? _a : null,
1956
+ };
1957
+ }
1958
+
1959
+ /** Prevent mobile long-press text selection and iOS callout menus on the board. */
1960
+ const nonSelectableBoardStyle = {
1961
+ userSelect: 'none',
1962
+ WebkitUserSelect: 'none',
1963
+ WebkitTouchCallout: 'none',
1964
+ };
1748
1965
  const getCheckHighlighting = (checkSquare) => {
1749
1966
  const styles = {};
1750
1967
  styles[checkSquare] = { backgroundColor: boardSquareHighlightColors.check };
@@ -1763,12 +1980,41 @@ const getFeedbackHighlighting = (hintSquare, incorrectMoveSquare) => {
1763
1980
  return styles;
1764
1981
  };
1765
1982
  const HighlightChessboard = (_a) => {
1766
- var { checkSquare, hintSquare, incorrectMoveSquare, customSquareStyles: extraSquareStyles } = _a, props = __rest(_a, ["checkSquare", "hintSquare", "incorrectMoveSquare", "customSquareStyles"]);
1983
+ var { checkSquare, hintSquare, incorrectMoveSquare, correctMoveSquare = null, lastMoveUci = null, lastMoveArrowColor, clickToMove, customSquareStyles: extraSquareStyles, customArrows, customBoardStyle, onPieceDrop, position, arePiecesDraggable, autoPromoteToQueen, isDraggablePiece, onPromotionCheck, onSquareClick, onPromotionPieceSelect, onPieceDragBegin, showPromotionDialog: showPromotionDialogProp, promotionToSquare: promotionToSquareProp } = _a, props = __rest(_a, ["checkSquare", "hintSquare", "incorrectMoveSquare", "correctMoveSquare", "lastMoveUci", "lastMoveArrowColor", "clickToMove", "customSquareStyles", "customArrows", "customBoardStyle", "onPieceDrop", "position", "arePiecesDraggable", "autoPromoteToQueen", "isDraggablePiece", "onPromotionCheck", "onSquareClick", "onPromotionPieceSelect", "onPieceDragBegin", "showPromotionDialog", "promotionToSquare"]);
1767
1984
  const { customDarkSquareStyle, customLightSquareStyle } = useChessboardTheme();
1985
+ const clickToMoveEnabled = clickToMove !== false && typeof onPieceDrop === 'function';
1986
+ const { clickSquareStyles, handleSquareClick, handlePromotionPieceSelect, handlePieceDragBegin, showPromotionDialog: clickPromotionDialog, promotionToSquare: clickPromotionToSquare, } = useClickToMove({
1987
+ enabled: clickToMoveEnabled,
1988
+ position,
1989
+ arePiecesDraggable,
1990
+ autoPromoteToQueen,
1991
+ isDraggablePiece,
1992
+ onPromotionCheck,
1993
+ onPieceDrop,
1994
+ onSquareClick,
1995
+ onPromotionPieceSelect,
1996
+ onPieceDragBegin,
1997
+ });
1768
1998
  const checkStyles = getCheckHighlighting(checkSquare);
1769
1999
  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)));
2000
+ const customSquareStyles = Object.assign(Object.assign(Object.assign(Object.assign({}, clickSquareStyles), checkStyles), feedbackStyles), extraSquareStyles);
2001
+ const customSquare = react.useMemo(() => correctMoveSquare
2002
+ ? createFeedbackSquareRenderer(correctMoveSquare)
2003
+ : undefined, [correctMoveSquare]);
2004
+ const mergedCustomArrows = react.useMemo(() => mergeCustomArrowsWithLastMove(customArrows, lastMoveUci, lastMoveArrowColor), [customArrows, lastMoveArrowColor, lastMoveUci]);
2005
+ const promotionControlProps = clickPromotionDialog
2006
+ ? {
2007
+ showPromotionDialog: true,
2008
+ promotionToSquare: clickPromotionToSquare,
2009
+ }
2010
+ : showPromotionDialogProp !== undefined ||
2011
+ promotionToSquareProp !== undefined
2012
+ ? {
2013
+ showPromotionDialog: showPromotionDialogProp,
2014
+ promotionToSquare: promotionToSquareProp,
2015
+ }
2016
+ : {};
2017
+ 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, customArrows: mergedCustomArrows }, promotionControlProps, props)));
1772
2018
  };
1773
2019
 
1774
2020
  const emptyEngineEvaluation = () => ({
@@ -2696,9 +2942,7 @@ const PlyNavigation = ({ plyIndex, totalPly, canPrev, canNext, onGoFirst, onGoPr
2696
2942
  /** Draggable analysis board (no surrounding layout chrome). */
2697
2943
  const AnalysisChessboardView = ({ model }) => {
2698
2944
  var _a;
2699
- return (jsxRuntime.jsx(ChessboardDnDProvider, { children: jsxRuntime.jsx(HighlightChessboard, { checkSquare: (_a = model.checkSquare) !== null && _a !== void 0 ? _a : '', hintSquare: null, incorrectMoveSquare: null, position: model.fen, boardOrientation: model.boardOrientation, boardWidth: model.boardWidth, arePiecesDraggable: true, onPieceDrop: model.onPieceDrop, promotionDialogVariant: "modal", customSquareStyles: model.lastMove
2700
- ? getLastMoveSquareStyles(model.lastMove.from, model.lastMove.to, model.theme)
2701
- : {} }) }));
2945
+ return (jsxRuntime.jsx(ChessboardDnDProvider, { children: jsxRuntime.jsx(HighlightChessboard, { checkSquare: (_a = model.checkSquare) !== null && _a !== void 0 ? _a : '', hintSquare: null, incorrectMoveSquare: null, position: model.fen, boardOrientation: model.boardOrientation, boardWidth: model.boardWidth, arePiecesDraggable: true, onPieceDrop: model.onPieceDrop, promotionDialogVariant: "modal", lastMoveUci: model.lastMoveUci }) }));
2702
2946
  };
2703
2947
 
2704
2948
  class AnalysisPosition {
@@ -2954,22 +3198,26 @@ class AnalysisPosition {
2954
3198
  return true;
2955
3199
  }
2956
3200
  getLastMoveSquares() {
2957
- const navPly = this.getNavPly();
2958
- if (navPly === 0) {
3201
+ const uci = this.getLastMoveUci();
3202
+ if (!uci) {
2959
3203
  return null;
2960
3204
  }
2961
- let uci;
2962
- if (this.variation && this.variationCursor > 0) {
2963
- uci = this.variation.moves[this.variationCursor - 1];
2964
- }
2965
- else {
2966
- uci = this.solutionMoves[this.mainPly - 1];
2967
- }
2968
3205
  return {
2969
3206
  from: uci.slice(0, 2),
2970
3207
  to: uci.slice(2, 4),
2971
3208
  };
2972
3209
  }
3210
+ getLastMoveUci() {
3211
+ var _a;
3212
+ const navPly = this.getNavPly();
3213
+ if (navPly === 0) {
3214
+ return null;
3215
+ }
3216
+ if (this.variation && this.variationCursor > 0) {
3217
+ return this.variation.moves[this.variationCursor - 1];
3218
+ }
3219
+ return (_a = this.solutionMoves[this.mainPly - 1]) !== null && _a !== void 0 ? _a : null;
3220
+ }
2973
3221
  fen() {
2974
3222
  return this.chess.fen();
2975
3223
  }
@@ -3014,6 +3262,7 @@ const useAnalysisBoardModel = ({ analysisContext, onClose, theme, boardWidth, en
3014
3262
  engineEvaluation,
3015
3263
  engineEnabled,
3016
3264
  lastMove: analysisPosition.getLastMoveSquares(),
3265
+ lastMoveUci: analysisPosition.getLastMoveUci(),
3017
3266
  checkSquare: analysisPosition.getCheckSquare(),
3018
3267
  onSelectPly: (ply) => {
3019
3268
  analysisPosition.goToNavPly(ply);
@@ -3504,6 +3753,37 @@ function useBoardRevision() {
3504
3753
  return { revision, bumpRevision };
3505
3754
  }
3506
3755
 
3756
+ /** Pause (ms) to show the correct-move check before advancing the line. */
3757
+ const CORRECT_MOVE_FEEDBACK_MS = 450;
3758
+
3759
+ function useCorrectMoveFeedback(delayMs = CORRECT_MOVE_FEEDBACK_MS) {
3760
+ const [correctMoveSquare, setCorrectMoveSquare] = react.useState(null);
3761
+ const timeoutRef = react.useRef(null);
3762
+ const clearCorrectMoveFeedback = react.useCallback(() => {
3763
+ if (timeoutRef.current !== null) {
3764
+ window.clearTimeout(timeoutRef.current);
3765
+ timeoutRef.current = null;
3766
+ }
3767
+ setCorrectMoveSquare(null);
3768
+ }, []);
3769
+ const showCorrectMove = react.useCallback((targetSquare, onComplete) => {
3770
+ clearCorrectMoveFeedback();
3771
+ setCorrectMoveSquare(targetSquare);
3772
+ timeoutRef.current = window.setTimeout(() => {
3773
+ timeoutRef.current = null;
3774
+ setCorrectMoveSquare(null);
3775
+ onComplete === null || onComplete === void 0 ? void 0 : onComplete();
3776
+ }, delayMs);
3777
+ }, [clearCorrectMoveFeedback, delayMs]);
3778
+ react.useEffect(() => clearCorrectMoveFeedback, [clearCorrectMoveFeedback]);
3779
+ return {
3780
+ correctMoveSquare,
3781
+ showCorrectMove,
3782
+ clearCorrectMoveFeedback,
3783
+ isShowingCorrectMove: correctMoveSquare !== null,
3784
+ };
3785
+ }
3786
+
3507
3787
  /** Minimum eval loss (pawns) from the wrong move before showing a refutation. */
3508
3788
  const REFUTATION_EVAL_GAP_PAWNS = 0.5;
3509
3789
  const REFUTATION_EVAL_GAP_CP = REFUTATION_EVAL_GAP_PAWNS * 100;
@@ -3597,6 +3877,7 @@ function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor)
3597
3877
  return {
3598
3878
  fen: null,
3599
3879
  arrows: [],
3880
+ lastMoveUci: null,
3600
3881
  animating: false,
3601
3882
  };
3602
3883
  }
@@ -3607,6 +3888,7 @@ function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor)
3607
3888
  return {
3608
3889
  fen: fenAfterWrong !== null && fenAfterWrong !== void 0 ? fenAfterWrong : setupFen,
3609
3890
  arrows: [],
3891
+ lastMoveUci: attemptedUci,
3610
3892
  animating: false,
3611
3893
  };
3612
3894
  case 'refutation': {
@@ -3616,6 +3898,7 @@ function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor)
3616
3898
  return {
3617
3899
  fen: (_a = fenAfterRefutation !== null && fenAfterRefutation !== void 0 ? fenAfterRefutation : fenAfterWrong) !== null && _a !== void 0 ? _a : setupFen,
3618
3900
  arrows: [],
3901
+ lastMoveUci: refutationUci,
3619
3902
  animating: Boolean(fenAfterRefutation),
3620
3903
  };
3621
3904
  }
@@ -3623,18 +3906,21 @@ function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor)
3623
3906
  return {
3624
3907
  fen: setupFen,
3625
3908
  arrows: [],
3909
+ lastMoveUci: null,
3626
3910
  animating: false,
3627
3911
  };
3628
3912
  case 'answer':
3629
3913
  return {
3630
3914
  fen: setupFen,
3631
3915
  arrows: expectedMoveArrow(expectedUci, answerArrowColor),
3916
+ lastMoveUci: null,
3632
3917
  animating: false,
3633
3918
  };
3634
3919
  default:
3635
3920
  return {
3636
3921
  fen: setupFen,
3637
3922
  arrows: [],
3923
+ lastMoveUci: null,
3638
3924
  animating: false,
3639
3925
  };
3640
3926
  }
@@ -3807,6 +4093,7 @@ function useMissBoard({ feedback, expectedUci, positionFen, answerArrowColor, au
3807
4093
  customArrows,
3808
4094
  boardPosition,
3809
4095
  boardAnimating: missSequence.display.animating,
4096
+ lastMoveUci: missSequence.display.lastMoveUci,
3810
4097
  wrapDropHandler,
3811
4098
  };
3812
4099
  }
@@ -3821,10 +4108,13 @@ exports.AnalysisErrorBoundary = AnalysisErrorBoundary;
3821
4108
  exports.AnalysisPosition = AnalysisPosition;
3822
4109
  exports.BOARD_THEMES = BOARD_THEMES;
3823
4110
  exports.BOARD_THEME_IDS = BOARD_THEME_IDS;
4111
+ exports.CORRECT_MOVE_FEEDBACK_MS = CORRECT_MOVE_FEEDBACK_MS;
3824
4112
  exports.ChessboardDnDProvider = ChessboardDnDProvider;
3825
4113
  exports.ChessboardThemeContext = ChessboardThemeContext;
4114
+ exports.CorrectMoveCheckBadge = CorrectMoveCheckBadge;
3826
4115
  exports.DEFAULT_ANALYSIS_LAYOUT = DEFAULT_ANALYSIS_LAYOUT;
3827
4116
  exports.DEFAULT_BOARD_THEME = DEFAULT_BOARD_THEME;
4117
+ exports.DEFAULT_LAST_MOVE_ARROW_COLOR = DEFAULT_LAST_MOVE_ARROW_COLOR;
3828
4118
  exports.DEFAULT_STOCKFISH_SCRIPT_URL = DEFAULT_STOCKFISH_SCRIPT_URL;
3829
4119
  exports.DefaultAnalysisContainer = DefaultAnalysisContainer;
3830
4120
  exports.DefaultAnalysisSidebar = DefaultAnalysisSidebar;
@@ -3863,8 +4153,11 @@ exports.getStylesForTheme = getStylesForTheme;
3863
4153
  exports.isAnalyzableFen = isAnalyzableFen;
3864
4154
  exports.isBoardThemeId = isBoardThemeId;
3865
4155
  exports.isEditableKeyboardTarget = isEditableKeyboardTarget;
4156
+ exports.lastMoveArrowFromUci = lastMoveArrowFromUci;
4157
+ exports.lastMoveUciAtPly = lastMoveUciAtPly;
3866
4158
  exports.lineEvalCpForGap = lineEvalCpForGap;
3867
4159
  exports.matchesExpectedUci = matchesExpectedUci;
4160
+ exports.mergeCustomArrowsWithLastMove = mergeCustomArrowsWithLastMove;
3868
4161
  exports.navButtonStyle = navButtonStyle;
3869
4162
  exports.navRowStyle = navRowStyle;
3870
4163
  exports.normalizeEvalForWhite = normalizeEvalForWhite;
@@ -3884,11 +4177,13 @@ exports.scrubberInputStyle = scrubberInputStyle;
3884
4177
  exports.splitWorkerLines = splitWorkerLines;
3885
4178
  exports.uciFromDrop = uciFromDrop;
3886
4179
  exports.uciPvToSan = uciPvToSan;
4180
+ exports.uciToArrow = uciToArrow;
3887
4181
  exports.useAnalysisBoardModel = useAnalysisBoardModel;
3888
4182
  exports.useAnalysisEngine = useAnalysisEngine;
3889
4183
  exports.useAnalysisEngineContext = useAnalysisEngineContext;
3890
4184
  exports.useBoardRevision = useBoardRevision;
3891
4185
  exports.useChessboardTheme = useChessboardTheme;
4186
+ exports.useCorrectMoveFeedback = useCorrectMoveFeedback;
3892
4187
  exports.useMissBoard = useMissBoard;
3893
4188
  exports.useMissRefutation = useMissRefutation;
3894
4189
  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.4",
4
4
  "description": "Shared React chessboard theme, highlights, browser Stockfish, and analysis board",
5
5
  "license": "MIT",
6
6
  "author": "Robert Blackwell",