react-chess-core 0.1.5 → 0.1.6

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,2 @@
1
+ /** Large green check centered over the full chessboard when a line completes. */
2
+ export declare const BoardCompleteCheckOverlay: () => import("react").JSX.Element;
@@ -1,15 +1,14 @@
1
1
  export interface HighlightChessboardProps {
2
2
  checkSquare: string;
3
3
  hintSquare: string | null;
4
- incorrectMoveSquare: string | null;
4
+ /** Origin square of a rejected training move — shows a red X on the snapped-back piece. */
5
+ incorrectMoveSquare?: string | null;
5
6
  /** Destination square of the last correct training move — shows a green check. */
6
7
  correctMoveSquare?: string | null;
7
8
  /** UCI of the move that led to the current position (shows a last-move arrow). */
8
9
  lastMoveUci?: string | null;
9
- /** Override the default last-move arrow color. */
10
- lastMoveArrowColor?: string;
11
10
  /** Enable click-to-move when `onPieceDrop` is provided. Defaults to true. */
12
11
  clickToMove?: boolean;
13
12
  [key: string]: any;
14
13
  }
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;
14
+ export declare const HighlightChessboard: ({ checkSquare, hintSquare, incorrectMoveSquare, correctMoveSquare, lastMoveUci, clickToMove, customSquareStyles: extraSquareStyles, customArrows, customBoardStyle, onPieceDrop, position, arePiecesDraggable, autoPromoteToQueen, isDraggablePiece, onPromotionCheck, onSquareClick, onPromotionPieceSelect, onPieceDragBegin, showPromotionDialog: showPromotionDialogProp, promotionToSquare: promotionToSquareProp, ...props }: HighlightChessboardProps) => import("react").JSX.Element;
@@ -0,0 +1,2 @@
1
+ /** Red circle with a white X, anchored to the bottom-right of a square. */
2
+ export declare const IncorrectMoveXBadge: () => import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Board arrow colors — change these values to update arrows app-wide.
3
+ */
4
+ export declare const boardArrowColors: {
5
+ /** Arrow showing the move that reached the current position. */
6
+ readonly lastMove: "rgba(130, 130, 130, 0.75)";
7
+ /** Arrow showing the correct / answer move during training. */
8
+ readonly answer: "#1976d2";
9
+ };
10
+ /** @deprecated Use {@link boardArrowColors.lastMove} */
11
+ export declare const DEFAULT_LAST_MOVE_ARROW_COLOR: "rgba(130, 130, 130, 0.75)";
12
+ /** @deprecated Use {@link boardArrowColors.answer} */
13
+ export declare const DEFAULT_ANSWER_ARROW_COLOR: "#1976d2";
@@ -1,3 +1,7 @@
1
1
  import type { FC } from 'react';
2
2
  import type { CustomSquareProps } from 'react-chessboard/dist/chessboard/types';
3
- export declare function createFeedbackSquareRenderer(correctMoveSquare: string): FC<CustomSquareProps>;
3
+ export type FeedbackSquareRendererOptions = {
4
+ correctMoveSquare?: string | null;
5
+ incorrectMoveSquare?: string | null;
6
+ };
7
+ export declare function createFeedbackSquareRenderer({ correctMoveSquare, incorrectMoveSquare, }: FeedbackSquareRendererOptions): FC<CustomSquareProps> | undefined;
@@ -3,5 +3,8 @@ export * from './chessboardTheme';
3
3
  export * from './ChessboardDnDProvider';
4
4
  export * from './HighlightChessboard';
5
5
  export * from './CorrectMoveCheckBadge';
6
+ export * from './IncorrectMoveXBadge';
7
+ export * from './BoardCompleteCheckOverlay';
6
8
  export * from './boardSquareHighlightColors';
9
+ export * from './boardArrowColors';
7
10
  export * from './lastMoveArrow';
@@ -1,8 +1,6 @@
1
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[];
2
+ export declare const uciToArrow: (uci: string) => ChessboardArrow;
3
+ export declare const lastMoveArrowFromUci: (uci: string | null | undefined) => ChessboardArrow[];
6
4
  /** UCI of the move that produced the position at {@link plyIndex}. */
7
5
  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[];
6
+ export declare const mergeCustomArrowsWithLastMove: (customArrows: ChessboardArrow[] | undefined, lastMoveUci: string | null | undefined) => ChessboardArrow[];
@@ -1,20 +1,25 @@
1
+ export type ExpectedMoveAttempt = {
2
+ uci: string;
3
+ sourceSquare: string;
4
+ targetSquare: string;
5
+ };
1
6
  export type ExpectedMoveDropResult = {
2
7
  kind: 'ignored';
3
8
  } | {
4
9
  kind: 'illegal';
5
10
  } | {
6
11
  kind: 'correct';
7
- uci: string;
12
+ attempt: ExpectedMoveAttempt;
8
13
  } | {
9
14
  kind: 'incorrect';
10
- uci: string;
15
+ attempt: ExpectedMoveAttempt;
11
16
  };
12
17
  export type CreateExpectedMoveDropHandlerOptions = {
13
18
  fen: string;
14
19
  expectedUci: string | null | undefined;
15
20
  enabled: boolean;
16
- onCorrect: (uci: string) => void;
17
- onIncorrect: (uci: string) => void;
21
+ onCorrect: (attempt: ExpectedMoveAttempt) => void;
22
+ onIncorrect: (attempt: ExpectedMoveAttempt) => void;
18
23
  };
19
24
  /**
20
25
  * Evaluate a training drop without mutating board position.
@@ -3,4 +3,6 @@ export * from './expectedMoveDrop';
3
3
  export * from './useBoardRevision';
4
4
  export * from './correctMoveFeedbackMs';
5
5
  export * from './useCorrectMoveFeedback';
6
+ export * from './useIncorrectMoveFeedback';
7
+ export * from './useTrainingMoveFeedback';
6
8
  export * from './miss';
@@ -0,0 +1,6 @@
1
+ export declare function useIncorrectMoveFeedback(delayMs?: number): {
2
+ incorrectMoveSquare: string | null;
3
+ showIncorrectMove: (originSquare: string, onComplete?: () => void) => void;
4
+ clearIncorrectMoveFeedback: () => void;
5
+ isShowingIncorrectMove: boolean;
6
+ };
@@ -0,0 +1,22 @@
1
+ import { type CreateExpectedMoveDropHandlerOptions, type ExpectedMoveAttempt } from './expectedMoveDrop';
2
+ export type CreateTrainingDropHandlerOptions = Omit<CreateExpectedMoveDropHandlerOptions, 'onCorrect' | 'onIncorrect'> & {
3
+ onCorrect?: (attempt: ExpectedMoveAttempt) => void;
4
+ onIncorrect?: (attempt: ExpectedMoveAttempt) => void;
5
+ };
6
+ /**
7
+ * Shared correct/incorrect move overlays for training boards.
8
+ * Incorrect feedback always uses the drag origin square (snapped-back piece).
9
+ */
10
+ export declare function useTrainingMoveFeedback(delayMs?: number): {
11
+ correctMoveSquare: string | null;
12
+ incorrectMoveSquare: string | null;
13
+ showCorrectMove: (targetSquare: string, onComplete?: () => void) => void;
14
+ showIncorrectMove: (originSquare: string, onComplete?: () => void) => void;
15
+ clearMoveFeedback: () => void;
16
+ clearCorrectMoveFeedback: () => void;
17
+ clearIncorrectMoveFeedback: () => void;
18
+ isShowingCorrectMove: boolean;
19
+ isShowingIncorrectMove: boolean;
20
+ isShowingMoveFeedback: boolean;
21
+ createDropHandler: ({ onCorrect, onIncorrect, ...options }: CreateTrainingDropHandlerOptions) => (sourceSquare: string, targetSquare: string, piece: string) => boolean;
22
+ };
package/dist/index.esm.js CHANGED
@@ -1746,7 +1746,7 @@ const boardSquareHighlightColors = {
1746
1746
  captureTarget: 'radial-gradient(circle, rgba(0, 0, 0, 0.18) 72%, transparent 72%)',
1747
1747
  };
1748
1748
 
1749
- const badgeStyle = {
1749
+ const badgeStyle$2 = {
1750
1750
  position: 'absolute',
1751
1751
  right: '6%',
1752
1752
  bottom: '6%',
@@ -1766,29 +1766,69 @@ const badgeStyle = {
1766
1766
  };
1767
1767
  /** Green circle with a white check, anchored to the bottom-right of a square
1768
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" }) }) }));
1769
+ const CorrectMoveCheckBadge = () => (jsx("span", { "aria-hidden": true, style: badgeStyle$2, 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
1770
 
1771
- const overlayStyle$1 = {
1771
+ const badgeStyle$1 = {
1772
+ position: 'absolute',
1773
+ right: '6%',
1774
+ bottom: '6%',
1775
+ width: '26%',
1776
+ height: '26%',
1777
+ minWidth: 14,
1778
+ minHeight: 14,
1779
+ maxWidth: 26,
1780
+ maxHeight: 26,
1781
+ borderRadius: '50%',
1782
+ backgroundColor: '#c62828',
1783
+ display: 'flex',
1784
+ alignItems: 'center',
1785
+ justifyContent: 'center',
1786
+ boxShadow: '0 1px 3px rgba(0, 0, 0, 0.35)',
1787
+ pointerEvents: 'none',
1788
+ };
1789
+ /** Red circle with a white X, anchored to the bottom-right of a square. */
1790
+ const IncorrectMoveXBadge = () => (jsx("span", { "aria-hidden": true, style: badgeStyle$1, children: jsx("svg", { viewBox: "0 0 12 12", width: "58%", height: "58%", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsx("path", { d: "M3.2 3.2 8.8 8.8M8.8 3.2 3.2 8.8", stroke: "#fff", strokeWidth: "1.8", strokeLinecap: "round" }) }) }));
1791
+
1792
+ const overlayStyle$2 = {
1772
1793
  position: 'absolute',
1773
1794
  inset: 0,
1774
1795
  pointerEvents: 'none',
1775
1796
  zIndex: 20,
1776
1797
  overflow: 'visible',
1777
1798
  };
1778
- function createFeedbackSquareRenderer(correctMoveSquare) {
1799
+ function createFeedbackSquareRenderer({ correctMoveSquare = null, incorrectMoveSquare = null, }) {
1800
+ if (!correctMoveSquare && !incorrectMoveSquare) {
1801
+ return undefined;
1802
+ }
1779
1803
  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] }));
1804
+ return (jsxs("div", { ref: ref, style: Object.assign(Object.assign({}, style), { position: 'relative', overflow: 'visible' }), children: [children, square === correctMoveSquare ? (jsx("div", { style: overlayStyle$2, children: jsx(CorrectMoveCheckBadge, {}) })) : null, square === incorrectMoveSquare ? (jsx("div", { style: overlayStyle$2, children: jsx(IncorrectMoveXBadge, {}) })) : null] }));
1781
1805
  };
1782
1806
  }
1783
1807
 
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) => {
1808
+ /**
1809
+ * Board arrow colors change these values to update arrows app-wide.
1810
+ */
1811
+ const boardArrowColors = {
1812
+ /** Arrow showing the move that reached the current position. */
1813
+ lastMove: 'rgba(130, 130, 130, 0.75)',
1814
+ /** Arrow showing the correct / answer move during training. */
1815
+ answer: '#1976d2',
1816
+ };
1817
+ /** @deprecated Use {@link boardArrowColors.lastMove} */
1818
+ const DEFAULT_LAST_MOVE_ARROW_COLOR = boardArrowColors.lastMove;
1819
+ /** @deprecated Use {@link boardArrowColors.answer} */
1820
+ const DEFAULT_ANSWER_ARROW_COLOR = boardArrowColors.answer;
1821
+
1822
+ const uciToArrow = (uci) => [
1823
+ uci.slice(0, 2),
1824
+ uci.slice(2, 4),
1825
+ boardArrowColors.lastMove,
1826
+ ];
1827
+ const lastMoveArrowFromUci = (uci) => {
1788
1828
  if (!uci || uci.length < 4) {
1789
1829
  return [];
1790
1830
  }
1791
- return [uciToArrow(uci, color)];
1831
+ return [uciToArrow(uci)];
1792
1832
  };
1793
1833
  /** UCI of the move that produced the position at {@link plyIndex}. */
1794
1834
  const lastMoveUciAtPly = (movesUci, plyIndex) => {
@@ -1798,8 +1838,8 @@ const lastMoveUciAtPly = (movesUci, plyIndex) => {
1798
1838
  }
1799
1839
  return (_a = movesUci[plyIndex - 1]) !== null && _a !== void 0 ? _a : null;
1800
1840
  };
1801
- const mergeCustomArrowsWithLastMove = (customArrows, lastMoveUci, lastMoveArrowColor) => {
1802
- const lastMove = lastMoveArrowFromUci(lastMoveUci, lastMoveArrowColor);
1841
+ const mergeCustomArrowsWithLastMove = (customArrows, lastMoveUci) => {
1842
+ const lastMove = lastMoveArrowFromUci(lastMoveUci);
1803
1843
  if (!lastMove.length) {
1804
1844
  return customArrows !== null && customArrows !== void 0 ? customArrows : [];
1805
1845
  }
@@ -1965,20 +2005,15 @@ const getCheckHighlighting = (checkSquare) => {
1965
2005
  styles[checkSquare] = { backgroundColor: boardSquareHighlightColors.check };
1966
2006
  return styles;
1967
2007
  };
1968
- const getFeedbackHighlighting = (hintSquare, incorrectMoveSquare) => {
2008
+ const getFeedbackHighlighting = (hintSquare) => {
1969
2009
  const styles = {};
1970
2010
  if (hintSquare) {
1971
2011
  styles[hintSquare] = { backgroundColor: boardSquareHighlightColors.hint };
1972
2012
  }
1973
- if (incorrectMoveSquare) {
1974
- styles[incorrectMoveSquare] = {
1975
- backgroundColor: boardSquareHighlightColors.incorrect,
1976
- };
1977
- }
1978
2013
  return styles;
1979
2014
  };
1980
2015
  const HighlightChessboard = (_a) => {
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"]);
2016
+ var { checkSquare, hintSquare, incorrectMoveSquare = null, correctMoveSquare = null, lastMoveUci = null, 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", "clickToMove", "customSquareStyles", "customArrows", "customBoardStyle", "onPieceDrop", "position", "arePiecesDraggable", "autoPromoteToQueen", "isDraggablePiece", "onPromotionCheck", "onSquareClick", "onPromotionPieceSelect", "onPieceDragBegin", "showPromotionDialog", "promotionToSquare"]);
1982
2017
  const { customDarkSquareStyle, customLightSquareStyle } = useChessboardTheme();
1983
2018
  const clickToMoveEnabled = clickToMove !== false && typeof onPieceDrop === 'function';
1984
2019
  const { clickSquareStyles, handleSquareClick, handlePromotionPieceSelect, handlePieceDragBegin, showPromotionDialog: clickPromotionDialog, promotionToSquare: clickPromotionToSquare, } = useClickToMove({
@@ -1994,12 +2029,13 @@ const HighlightChessboard = (_a) => {
1994
2029
  onPieceDragBegin,
1995
2030
  });
1996
2031
  const checkStyles = getCheckHighlighting(checkSquare);
1997
- const feedbackStyles = getFeedbackHighlighting(hintSquare, incorrectMoveSquare);
2032
+ const feedbackStyles = getFeedbackHighlighting(hintSquare);
1998
2033
  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]);
2034
+ const customSquare = useMemo(() => createFeedbackSquareRenderer({
2035
+ correctMoveSquare,
2036
+ incorrectMoveSquare,
2037
+ }), [correctMoveSquare, incorrectMoveSquare]);
2038
+ const mergedCustomArrows = useMemo(() => mergeCustomArrowsWithLastMove(customArrows, lastMoveUci), [customArrows, lastMoveUci]);
2003
2039
  const promotionControlProps = clickPromotionDialog
2004
2040
  ? {
2005
2041
  showPromotionDialog: true,
@@ -2015,6 +2051,29 @@ const HighlightChessboard = (_a) => {
2015
2051
  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)));
2016
2052
  };
2017
2053
 
2054
+ const overlayStyle$1 = {
2055
+ position: 'absolute',
2056
+ inset: 0,
2057
+ display: 'flex',
2058
+ alignItems: 'center',
2059
+ justifyContent: 'center',
2060
+ pointerEvents: 'none',
2061
+ zIndex: 10,
2062
+ borderRadius: 4,
2063
+ };
2064
+ const badgeStyle = {
2065
+ width: '58%',
2066
+ height: '58%',
2067
+ borderRadius: '50%',
2068
+ backgroundColor: '#2e7d32',
2069
+ display: 'flex',
2070
+ alignItems: 'center',
2071
+ justifyContent: 'center',
2072
+ boxShadow: '0 4px 20px rgba(0, 0, 0, 0.4)',
2073
+ };
2074
+ /** Large green check centered over the full chessboard when a line completes. */
2075
+ const BoardCompleteCheckOverlay = () => (jsx("div", { "aria-hidden": true, style: overlayStyle$1, children: jsx("span", { style: badgeStyle, children: jsx("svg", { viewBox: "0 0 24 24", width: "58%", height: "58%", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsx("path", { d: "M5 13l4 4L19 7", stroke: "#fff", strokeWidth: "3", strokeLinecap: "round", strokeLinejoin: "round" }) }) }) }));
2076
+
2018
2077
  const emptyEngineEvaluation = () => ({
2019
2078
  status: 'idle',
2020
2079
  depth: 0,
@@ -3712,20 +3771,25 @@ function evaluateExpectedMoveDrop(fen, sourceSquare, targetSquare, piece, expect
3712
3771
  if (!uci) {
3713
3772
  return { kind: 'illegal' };
3714
3773
  }
3774
+ const attempt = {
3775
+ uci,
3776
+ sourceSquare,
3777
+ targetSquare,
3778
+ };
3715
3779
  if (matchesExpectedUci(uci, expectedUci)) {
3716
- return { kind: 'correct', uci };
3780
+ return { kind: 'correct', attempt };
3717
3781
  }
3718
- return { kind: 'incorrect', uci };
3782
+ return { kind: 'incorrect', attempt };
3719
3783
  }
3720
3784
  function createExpectedMoveDropHandler({ fen, expectedUci, enabled, onCorrect, onIncorrect, }) {
3721
3785
  return (sourceSquare, targetSquare, piece) => {
3722
3786
  const result = evaluateExpectedMoveDrop(fen, sourceSquare, targetSquare, piece, expectedUci, enabled);
3723
3787
  switch (result.kind) {
3724
3788
  case 'correct':
3725
- onCorrect(result.uci);
3789
+ onCorrect(result.attempt);
3726
3790
  return true;
3727
3791
  case 'incorrect':
3728
- onIncorrect(result.uci);
3792
+ onIncorrect(result.attempt);
3729
3793
  return false;
3730
3794
  default:
3731
3795
  return false;
@@ -3777,6 +3841,69 @@ function useCorrectMoveFeedback(delayMs = CORRECT_MOVE_FEEDBACK_MS) {
3777
3841
  };
3778
3842
  }
3779
3843
 
3844
+ function useIncorrectMoveFeedback(delayMs = CORRECT_MOVE_FEEDBACK_MS) {
3845
+ const [incorrectMoveSquare, setIncorrectMoveSquare] = useState(null);
3846
+ const timeoutRef = useRef(null);
3847
+ const clearIncorrectMoveFeedback = useCallback(() => {
3848
+ if (timeoutRef.current !== null) {
3849
+ window.clearTimeout(timeoutRef.current);
3850
+ timeoutRef.current = null;
3851
+ }
3852
+ setIncorrectMoveSquare(null);
3853
+ }, []);
3854
+ const showIncorrectMove = useCallback((originSquare, onComplete) => {
3855
+ clearIncorrectMoveFeedback();
3856
+ setIncorrectMoveSquare(originSquare);
3857
+ timeoutRef.current = window.setTimeout(() => {
3858
+ timeoutRef.current = null;
3859
+ setIncorrectMoveSquare(null);
3860
+ onComplete === null || onComplete === void 0 ? void 0 : onComplete();
3861
+ }, delayMs);
3862
+ }, [clearIncorrectMoveFeedback, delayMs]);
3863
+ useEffect(() => clearIncorrectMoveFeedback, [clearIncorrectMoveFeedback]);
3864
+ return {
3865
+ incorrectMoveSquare,
3866
+ showIncorrectMove,
3867
+ clearIncorrectMoveFeedback,
3868
+ isShowingIncorrectMove: incorrectMoveSquare !== null,
3869
+ };
3870
+ }
3871
+
3872
+ /**
3873
+ * Shared correct/incorrect move overlays for training boards.
3874
+ * Incorrect feedback always uses the drag origin square (snapped-back piece).
3875
+ */
3876
+ function useTrainingMoveFeedback(delayMs) {
3877
+ const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = useCorrectMoveFeedback(delayMs);
3878
+ const { incorrectMoveSquare, showIncorrectMove, clearIncorrectMoveFeedback, isShowingIncorrectMove, } = useIncorrectMoveFeedback(delayMs);
3879
+ const clearMoveFeedback = useCallback(() => {
3880
+ clearCorrectMoveFeedback();
3881
+ clearIncorrectMoveFeedback();
3882
+ }, [clearCorrectMoveFeedback, clearIncorrectMoveFeedback]);
3883
+ const createDropHandler = useCallback((_a) => {
3884
+ var { onCorrect, onIncorrect } = _a, options = __rest(_a, ["onCorrect", "onIncorrect"]);
3885
+ return createExpectedMoveDropHandler(Object.assign(Object.assign({}, options), { onCorrect: (attempt) => {
3886
+ onCorrect === null || onCorrect === void 0 ? void 0 : onCorrect(attempt);
3887
+ }, onIncorrect: (attempt) => {
3888
+ showIncorrectMove(attempt.sourceSquare);
3889
+ onIncorrect === null || onIncorrect === void 0 ? void 0 : onIncorrect(attempt);
3890
+ } }));
3891
+ }, [showIncorrectMove]);
3892
+ return {
3893
+ correctMoveSquare,
3894
+ incorrectMoveSquare,
3895
+ showCorrectMove,
3896
+ showIncorrectMove,
3897
+ clearMoveFeedback,
3898
+ clearCorrectMoveFeedback,
3899
+ clearIncorrectMoveFeedback,
3900
+ isShowingCorrectMove,
3901
+ isShowingIncorrectMove,
3902
+ isShowingMoveFeedback: isShowingCorrectMove || isShowingIncorrectMove,
3903
+ createDropHandler,
3904
+ };
3905
+ }
3906
+
3780
3907
  /** Minimum eval loss (pawns) from the wrong move before showing a refutation. */
3781
3908
  const REFUTATION_EVAL_GAP_PAWNS = 0.5;
3782
3909
  const REFUTATION_EVAL_GAP_CP = REFUTATION_EVAL_GAP_PAWNS * 100;
@@ -3879,9 +4006,9 @@ function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor)
3879
4006
  switch (phase) {
3880
4007
  case 'wrong':
3881
4008
  return {
3882
- fen: fenAfterWrong !== null && fenAfterWrong !== void 0 ? fenAfterWrong : setupFen,
4009
+ fen: setupFen,
3883
4010
  arrows: [],
3884
- lastMoveUci: attemptedUci,
4011
+ lastMoveUci: null,
3885
4012
  animating: false,
3886
4013
  };
3887
4014
  case 'refutation': {
@@ -4091,4 +4218,4 @@ function useMissBoard({ feedback, expectedUci, positionFen, answerArrowColor, au
4091
4218
  };
4092
4219
  }
4093
4220
 
4094
- 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 };
4221
+ export { AnalysisBoard, AnalysisBoardCore, AnalysisBoardCoreView, AnalysisBoardLayout, AnalysisChessboardView, AnalysisEngineProvider, AnalysisErrorBoundary, AnalysisPosition, BOARD_THEMES, BOARD_THEME_IDS, BoardCompleteCheckOverlay, CORRECT_MOVE_FEEDBACK_MS, ChessboardDnDProvider, ChessboardThemeContext, CorrectMoveCheckBadge, DEFAULT_ANALYSIS_LAYOUT, DEFAULT_ANSWER_ARROW_COLOR, DEFAULT_BOARD_THEME, DEFAULT_LAST_MOVE_ARROW_COLOR, DEFAULT_STOCKFISH_SCRIPT_URL, DefaultAnalysisContainer, DefaultAnalysisSidebar, DefaultPlyNavigation, EngineEvaluationPanel, HighlightChessboard, IncorrectMoveXBadge, 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, boardArrowColors, 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, useIncorrectMoveFeedback, useMissBoard, useMissRefutation, useMissSequence, usePositionKeyboardNav, useTheme, useTrainingMoveFeedback };
package/dist/index.js CHANGED
@@ -1748,7 +1748,7 @@ const boardSquareHighlightColors = {
1748
1748
  captureTarget: 'radial-gradient(circle, rgba(0, 0, 0, 0.18) 72%, transparent 72%)',
1749
1749
  };
1750
1750
 
1751
- const badgeStyle = {
1751
+ const badgeStyle$2 = {
1752
1752
  position: 'absolute',
1753
1753
  right: '6%',
1754
1754
  bottom: '6%',
@@ -1768,29 +1768,69 @@ const badgeStyle = {
1768
1768
  };
1769
1769
  /** Green circle with a white check, anchored to the bottom-right of a square
1770
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" }) }) }));
1771
+ const CorrectMoveCheckBadge = () => (jsxRuntime.jsx("span", { "aria-hidden": true, style: badgeStyle$2, 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
1772
 
1773
- const overlayStyle$1 = {
1773
+ const badgeStyle$1 = {
1774
+ position: 'absolute',
1775
+ right: '6%',
1776
+ bottom: '6%',
1777
+ width: '26%',
1778
+ height: '26%',
1779
+ minWidth: 14,
1780
+ minHeight: 14,
1781
+ maxWidth: 26,
1782
+ maxHeight: 26,
1783
+ borderRadius: '50%',
1784
+ backgroundColor: '#c62828',
1785
+ display: 'flex',
1786
+ alignItems: 'center',
1787
+ justifyContent: 'center',
1788
+ boxShadow: '0 1px 3px rgba(0, 0, 0, 0.35)',
1789
+ pointerEvents: 'none',
1790
+ };
1791
+ /** Red circle with a white X, anchored to the bottom-right of a square. */
1792
+ const IncorrectMoveXBadge = () => (jsxRuntime.jsx("span", { "aria-hidden": true, style: badgeStyle$1, children: jsxRuntime.jsx("svg", { viewBox: "0 0 12 12", width: "58%", height: "58%", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsxRuntime.jsx("path", { d: "M3.2 3.2 8.8 8.8M8.8 3.2 3.2 8.8", stroke: "#fff", strokeWidth: "1.8", strokeLinecap: "round" }) }) }));
1793
+
1794
+ const overlayStyle$2 = {
1774
1795
  position: 'absolute',
1775
1796
  inset: 0,
1776
1797
  pointerEvents: 'none',
1777
1798
  zIndex: 20,
1778
1799
  overflow: 'visible',
1779
1800
  };
1780
- function createFeedbackSquareRenderer(correctMoveSquare) {
1801
+ function createFeedbackSquareRenderer({ correctMoveSquare = null, incorrectMoveSquare = null, }) {
1802
+ if (!correctMoveSquare && !incorrectMoveSquare) {
1803
+ return undefined;
1804
+ }
1781
1805
  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] }));
1806
+ 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$2, children: jsxRuntime.jsx(CorrectMoveCheckBadge, {}) })) : null, square === incorrectMoveSquare ? (jsxRuntime.jsx("div", { style: overlayStyle$2, children: jsxRuntime.jsx(IncorrectMoveXBadge, {}) })) : null] }));
1783
1807
  };
1784
1808
  }
1785
1809
 
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) => {
1810
+ /**
1811
+ * Board arrow colors change these values to update arrows app-wide.
1812
+ */
1813
+ const boardArrowColors = {
1814
+ /** Arrow showing the move that reached the current position. */
1815
+ lastMove: 'rgba(130, 130, 130, 0.75)',
1816
+ /** Arrow showing the correct / answer move during training. */
1817
+ answer: '#1976d2',
1818
+ };
1819
+ /** @deprecated Use {@link boardArrowColors.lastMove} */
1820
+ const DEFAULT_LAST_MOVE_ARROW_COLOR = boardArrowColors.lastMove;
1821
+ /** @deprecated Use {@link boardArrowColors.answer} */
1822
+ const DEFAULT_ANSWER_ARROW_COLOR = boardArrowColors.answer;
1823
+
1824
+ const uciToArrow = (uci) => [
1825
+ uci.slice(0, 2),
1826
+ uci.slice(2, 4),
1827
+ boardArrowColors.lastMove,
1828
+ ];
1829
+ const lastMoveArrowFromUci = (uci) => {
1790
1830
  if (!uci || uci.length < 4) {
1791
1831
  return [];
1792
1832
  }
1793
- return [uciToArrow(uci, color)];
1833
+ return [uciToArrow(uci)];
1794
1834
  };
1795
1835
  /** UCI of the move that produced the position at {@link plyIndex}. */
1796
1836
  const lastMoveUciAtPly = (movesUci, plyIndex) => {
@@ -1800,8 +1840,8 @@ const lastMoveUciAtPly = (movesUci, plyIndex) => {
1800
1840
  }
1801
1841
  return (_a = movesUci[plyIndex - 1]) !== null && _a !== void 0 ? _a : null;
1802
1842
  };
1803
- const mergeCustomArrowsWithLastMove = (customArrows, lastMoveUci, lastMoveArrowColor) => {
1804
- const lastMove = lastMoveArrowFromUci(lastMoveUci, lastMoveArrowColor);
1843
+ const mergeCustomArrowsWithLastMove = (customArrows, lastMoveUci) => {
1844
+ const lastMove = lastMoveArrowFromUci(lastMoveUci);
1805
1845
  if (!lastMove.length) {
1806
1846
  return customArrows !== null && customArrows !== void 0 ? customArrows : [];
1807
1847
  }
@@ -1967,20 +2007,15 @@ const getCheckHighlighting = (checkSquare) => {
1967
2007
  styles[checkSquare] = { backgroundColor: boardSquareHighlightColors.check };
1968
2008
  return styles;
1969
2009
  };
1970
- const getFeedbackHighlighting = (hintSquare, incorrectMoveSquare) => {
2010
+ const getFeedbackHighlighting = (hintSquare) => {
1971
2011
  const styles = {};
1972
2012
  if (hintSquare) {
1973
2013
  styles[hintSquare] = { backgroundColor: boardSquareHighlightColors.hint };
1974
2014
  }
1975
- if (incorrectMoveSquare) {
1976
- styles[incorrectMoveSquare] = {
1977
- backgroundColor: boardSquareHighlightColors.incorrect,
1978
- };
1979
- }
1980
2015
  return styles;
1981
2016
  };
1982
2017
  const HighlightChessboard = (_a) => {
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"]);
2018
+ var { checkSquare, hintSquare, incorrectMoveSquare = null, correctMoveSquare = null, lastMoveUci = null, 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", "clickToMove", "customSquareStyles", "customArrows", "customBoardStyle", "onPieceDrop", "position", "arePiecesDraggable", "autoPromoteToQueen", "isDraggablePiece", "onPromotionCheck", "onSquareClick", "onPromotionPieceSelect", "onPieceDragBegin", "showPromotionDialog", "promotionToSquare"]);
1984
2019
  const { customDarkSquareStyle, customLightSquareStyle } = useChessboardTheme();
1985
2020
  const clickToMoveEnabled = clickToMove !== false && typeof onPieceDrop === 'function';
1986
2021
  const { clickSquareStyles, handleSquareClick, handlePromotionPieceSelect, handlePieceDragBegin, showPromotionDialog: clickPromotionDialog, promotionToSquare: clickPromotionToSquare, } = useClickToMove({
@@ -1996,12 +2031,13 @@ const HighlightChessboard = (_a) => {
1996
2031
  onPieceDragBegin,
1997
2032
  });
1998
2033
  const checkStyles = getCheckHighlighting(checkSquare);
1999
- const feedbackStyles = getFeedbackHighlighting(hintSquare, incorrectMoveSquare);
2034
+ const feedbackStyles = getFeedbackHighlighting(hintSquare);
2000
2035
  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]);
2036
+ const customSquare = react.useMemo(() => createFeedbackSquareRenderer({
2037
+ correctMoveSquare,
2038
+ incorrectMoveSquare,
2039
+ }), [correctMoveSquare, incorrectMoveSquare]);
2040
+ const mergedCustomArrows = react.useMemo(() => mergeCustomArrowsWithLastMove(customArrows, lastMoveUci), [customArrows, lastMoveUci]);
2005
2041
  const promotionControlProps = clickPromotionDialog
2006
2042
  ? {
2007
2043
  showPromotionDialog: true,
@@ -2017,6 +2053,29 @@ const HighlightChessboard = (_a) => {
2017
2053
  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)));
2018
2054
  };
2019
2055
 
2056
+ const overlayStyle$1 = {
2057
+ position: 'absolute',
2058
+ inset: 0,
2059
+ display: 'flex',
2060
+ alignItems: 'center',
2061
+ justifyContent: 'center',
2062
+ pointerEvents: 'none',
2063
+ zIndex: 10,
2064
+ borderRadius: 4,
2065
+ };
2066
+ const badgeStyle = {
2067
+ width: '58%',
2068
+ height: '58%',
2069
+ borderRadius: '50%',
2070
+ backgroundColor: '#2e7d32',
2071
+ display: 'flex',
2072
+ alignItems: 'center',
2073
+ justifyContent: 'center',
2074
+ boxShadow: '0 4px 20px rgba(0, 0, 0, 0.4)',
2075
+ };
2076
+ /** Large green check centered over the full chessboard when a line completes. */
2077
+ const BoardCompleteCheckOverlay = () => (jsxRuntime.jsx("div", { "aria-hidden": true, style: overlayStyle$1, children: jsxRuntime.jsx("span", { style: badgeStyle, children: jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", width: "58%", height: "58%", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsxRuntime.jsx("path", { d: "M5 13l4 4L19 7", stroke: "#fff", strokeWidth: "3", strokeLinecap: "round", strokeLinejoin: "round" }) }) }) }));
2078
+
2020
2079
  const emptyEngineEvaluation = () => ({
2021
2080
  status: 'idle',
2022
2081
  depth: 0,
@@ -3714,20 +3773,25 @@ function evaluateExpectedMoveDrop(fen, sourceSquare, targetSquare, piece, expect
3714
3773
  if (!uci) {
3715
3774
  return { kind: 'illegal' };
3716
3775
  }
3776
+ const attempt = {
3777
+ uci,
3778
+ sourceSquare,
3779
+ targetSquare,
3780
+ };
3717
3781
  if (matchesExpectedUci(uci, expectedUci)) {
3718
- return { kind: 'correct', uci };
3782
+ return { kind: 'correct', attempt };
3719
3783
  }
3720
- return { kind: 'incorrect', uci };
3784
+ return { kind: 'incorrect', attempt };
3721
3785
  }
3722
3786
  function createExpectedMoveDropHandler({ fen, expectedUci, enabled, onCorrect, onIncorrect, }) {
3723
3787
  return (sourceSquare, targetSquare, piece) => {
3724
3788
  const result = evaluateExpectedMoveDrop(fen, sourceSquare, targetSquare, piece, expectedUci, enabled);
3725
3789
  switch (result.kind) {
3726
3790
  case 'correct':
3727
- onCorrect(result.uci);
3791
+ onCorrect(result.attempt);
3728
3792
  return true;
3729
3793
  case 'incorrect':
3730
- onIncorrect(result.uci);
3794
+ onIncorrect(result.attempt);
3731
3795
  return false;
3732
3796
  default:
3733
3797
  return false;
@@ -3779,6 +3843,69 @@ function useCorrectMoveFeedback(delayMs = CORRECT_MOVE_FEEDBACK_MS) {
3779
3843
  };
3780
3844
  }
3781
3845
 
3846
+ function useIncorrectMoveFeedback(delayMs = CORRECT_MOVE_FEEDBACK_MS) {
3847
+ const [incorrectMoveSquare, setIncorrectMoveSquare] = react.useState(null);
3848
+ const timeoutRef = react.useRef(null);
3849
+ const clearIncorrectMoveFeedback = react.useCallback(() => {
3850
+ if (timeoutRef.current !== null) {
3851
+ window.clearTimeout(timeoutRef.current);
3852
+ timeoutRef.current = null;
3853
+ }
3854
+ setIncorrectMoveSquare(null);
3855
+ }, []);
3856
+ const showIncorrectMove = react.useCallback((originSquare, onComplete) => {
3857
+ clearIncorrectMoveFeedback();
3858
+ setIncorrectMoveSquare(originSquare);
3859
+ timeoutRef.current = window.setTimeout(() => {
3860
+ timeoutRef.current = null;
3861
+ setIncorrectMoveSquare(null);
3862
+ onComplete === null || onComplete === void 0 ? void 0 : onComplete();
3863
+ }, delayMs);
3864
+ }, [clearIncorrectMoveFeedback, delayMs]);
3865
+ react.useEffect(() => clearIncorrectMoveFeedback, [clearIncorrectMoveFeedback]);
3866
+ return {
3867
+ incorrectMoveSquare,
3868
+ showIncorrectMove,
3869
+ clearIncorrectMoveFeedback,
3870
+ isShowingIncorrectMove: incorrectMoveSquare !== null,
3871
+ };
3872
+ }
3873
+
3874
+ /**
3875
+ * Shared correct/incorrect move overlays for training boards.
3876
+ * Incorrect feedback always uses the drag origin square (snapped-back piece).
3877
+ */
3878
+ function useTrainingMoveFeedback(delayMs) {
3879
+ const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = useCorrectMoveFeedback(delayMs);
3880
+ const { incorrectMoveSquare, showIncorrectMove, clearIncorrectMoveFeedback, isShowingIncorrectMove, } = useIncorrectMoveFeedback(delayMs);
3881
+ const clearMoveFeedback = react.useCallback(() => {
3882
+ clearCorrectMoveFeedback();
3883
+ clearIncorrectMoveFeedback();
3884
+ }, [clearCorrectMoveFeedback, clearIncorrectMoveFeedback]);
3885
+ const createDropHandler = react.useCallback((_a) => {
3886
+ var { onCorrect, onIncorrect } = _a, options = __rest(_a, ["onCorrect", "onIncorrect"]);
3887
+ return createExpectedMoveDropHandler(Object.assign(Object.assign({}, options), { onCorrect: (attempt) => {
3888
+ onCorrect === null || onCorrect === void 0 ? void 0 : onCorrect(attempt);
3889
+ }, onIncorrect: (attempt) => {
3890
+ showIncorrectMove(attempt.sourceSquare);
3891
+ onIncorrect === null || onIncorrect === void 0 ? void 0 : onIncorrect(attempt);
3892
+ } }));
3893
+ }, [showIncorrectMove]);
3894
+ return {
3895
+ correctMoveSquare,
3896
+ incorrectMoveSquare,
3897
+ showCorrectMove,
3898
+ showIncorrectMove,
3899
+ clearMoveFeedback,
3900
+ clearCorrectMoveFeedback,
3901
+ clearIncorrectMoveFeedback,
3902
+ isShowingCorrectMove,
3903
+ isShowingIncorrectMove,
3904
+ isShowingMoveFeedback: isShowingCorrectMove || isShowingIncorrectMove,
3905
+ createDropHandler,
3906
+ };
3907
+ }
3908
+
3782
3909
  /** Minimum eval loss (pawns) from the wrong move before showing a refutation. */
3783
3910
  const REFUTATION_EVAL_GAP_PAWNS = 0.5;
3784
3911
  const REFUTATION_EVAL_GAP_CP = REFUTATION_EVAL_GAP_PAWNS * 100;
@@ -3881,9 +4008,9 @@ function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor)
3881
4008
  switch (phase) {
3882
4009
  case 'wrong':
3883
4010
  return {
3884
- fen: fenAfterWrong !== null && fenAfterWrong !== void 0 ? fenAfterWrong : setupFen,
4011
+ fen: setupFen,
3885
4012
  arrows: [],
3886
- lastMoveUci: attemptedUci,
4013
+ lastMoveUci: null,
3887
4014
  animating: false,
3888
4015
  };
3889
4016
  case 'refutation': {
@@ -4103,11 +4230,13 @@ exports.AnalysisErrorBoundary = AnalysisErrorBoundary;
4103
4230
  exports.AnalysisPosition = AnalysisPosition;
4104
4231
  exports.BOARD_THEMES = BOARD_THEMES;
4105
4232
  exports.BOARD_THEME_IDS = BOARD_THEME_IDS;
4233
+ exports.BoardCompleteCheckOverlay = BoardCompleteCheckOverlay;
4106
4234
  exports.CORRECT_MOVE_FEEDBACK_MS = CORRECT_MOVE_FEEDBACK_MS;
4107
4235
  exports.ChessboardDnDProvider = ChessboardDnDProvider;
4108
4236
  exports.ChessboardThemeContext = ChessboardThemeContext;
4109
4237
  exports.CorrectMoveCheckBadge = CorrectMoveCheckBadge;
4110
4238
  exports.DEFAULT_ANALYSIS_LAYOUT = DEFAULT_ANALYSIS_LAYOUT;
4239
+ exports.DEFAULT_ANSWER_ARROW_COLOR = DEFAULT_ANSWER_ARROW_COLOR;
4111
4240
  exports.DEFAULT_BOARD_THEME = DEFAULT_BOARD_THEME;
4112
4241
  exports.DEFAULT_LAST_MOVE_ARROW_COLOR = DEFAULT_LAST_MOVE_ARROW_COLOR;
4113
4242
  exports.DEFAULT_STOCKFISH_SCRIPT_URL = DEFAULT_STOCKFISH_SCRIPT_URL;
@@ -4116,6 +4245,7 @@ exports.DefaultAnalysisSidebar = DefaultAnalysisSidebar;
4116
4245
  exports.DefaultPlyNavigation = DefaultPlyNavigation;
4117
4246
  exports.EngineEvaluationPanel = EngineEvaluationPanel;
4118
4247
  exports.HighlightChessboard = HighlightChessboard;
4248
+ exports.IncorrectMoveXBadge = IncorrectMoveXBadge;
4119
4249
  exports.MISS_MOVE_ANIMATION_MS = MISS_MOVE_ANIMATION_MS;
4120
4250
  exports.MISS_REFUTATION_MAX_WAIT_MS = MISS_REFUTATION_MAX_WAIT_MS;
4121
4251
  exports.MISS_REFUTATION_PAUSE_MS = MISS_REFUTATION_PAUSE_MS;
@@ -4128,6 +4258,7 @@ exports.ThemeProvider = ThemeProvider;
4128
4258
  exports.analysisBoardHighlightColors = analysisBoardHighlightColors;
4129
4259
  exports.analysisSidebarColors = analysisSidebarColors;
4130
4260
  exports.applyUciMove = applyUciMove;
4261
+ exports.boardArrowColors = boardArrowColors;
4131
4262
  exports.boardSquareHighlightColors = boardSquareHighlightColors;
4132
4263
  exports.boardThemeFromLegacyUiTheme = boardThemeFromLegacyUiTheme;
4133
4264
  exports.createExpectedMoveDropHandler = createExpectedMoveDropHandler;
@@ -4179,8 +4310,10 @@ exports.useAnalysisEngineContext = useAnalysisEngineContext;
4179
4310
  exports.useBoardRevision = useBoardRevision;
4180
4311
  exports.useChessboardTheme = useChessboardTheme;
4181
4312
  exports.useCorrectMoveFeedback = useCorrectMoveFeedback;
4313
+ exports.useIncorrectMoveFeedback = useIncorrectMoveFeedback;
4182
4314
  exports.useMissBoard = useMissBoard;
4183
4315
  exports.useMissRefutation = useMissRefutation;
4184
4316
  exports.useMissSequence = useMissSequence;
4185
4317
  exports.usePositionKeyboardNav = usePositionKeyboardNav;
4186
4318
  exports.useTheme = useTheme;
4319
+ exports.useTrainingMoveFeedback = useTrainingMoveFeedback;
@@ -7,3 +7,4 @@ export declare const StartingPosition: Story;
7
7
  export declare const WithHint: Story;
8
8
  export declare const ClickToMove: Story;
9
9
  export declare const WithCorrectMoveCheck: Story;
10
+ export declare const WithIncorrectMoveX: Story;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-chess-core",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Shared React chessboard theme, highlights, browser Stockfish, and analysis board",
5
5
  "license": "MIT",
6
6
  "author": "Robert Blackwell",