react-chess-core 0.1.4 → 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,
@@ -3225,7 +3284,7 @@ class AnalysisPosition {
3225
3284
  }
3226
3285
 
3227
3286
  const useAnalysisBoardModel = ({ analysisContext, onClose, theme, boardWidth, engine, }) => {
3228
- var _a, _b, _c, _d;
3287
+ var _a;
3229
3288
  const skipBackdropCloseRef = useRef(true);
3230
3289
  const analysisPosition = useMemo(() => new AnalysisPosition(analysisContext), [analysisContext]);
3231
3290
  const [navRevision, setNavRevision] = useState(0);
@@ -3241,12 +3300,7 @@ const useAnalysisBoardModel = ({ analysisContext, onClose, theme, boardWidth, en
3241
3300
  }, [analysisContext, analysisPosition]);
3242
3301
  const fen = analysisPosition.fen();
3243
3302
  const engineEnabled = (_a = engine === null || engine === void 0 ? void 0 : engine.enabled) !== null && _a !== void 0 ? _a : true;
3244
- const engineEvaluation = useAnalysisEngine(fen, {
3245
- enabled: engineEnabled,
3246
- depth: (_b = engine === null || engine === void 0 ? void 0 : engine.depth) !== null && _b !== void 0 ? _b : 16,
3247
- multiPv: (_c = engine === null || engine === void 0 ? void 0 : engine.multiPv) !== null && _c !== void 0 ? _c : 2,
3248
- scriptUrl: (_d = engine === null || engine === void 0 ? void 0 : engine.scriptUrl) !== null && _d !== void 0 ? _d : DEFAULT_STOCKFISH_SCRIPT_URL,
3249
- });
3303
+ const engineEvaluation = useAnalysisEngine(fen, Object.assign(Object.assign({ depth: 16, multiPv: 2, scriptUrl: DEFAULT_STOCKFISH_SCRIPT_URL }, engine), { enabled: engineEnabled }));
3250
3304
  return {
3251
3305
  theme,
3252
3306
  boardWidth,
@@ -3717,20 +3771,25 @@ function evaluateExpectedMoveDrop(fen, sourceSquare, targetSquare, piece, expect
3717
3771
  if (!uci) {
3718
3772
  return { kind: 'illegal' };
3719
3773
  }
3774
+ const attempt = {
3775
+ uci,
3776
+ sourceSquare,
3777
+ targetSquare,
3778
+ };
3720
3779
  if (matchesExpectedUci(uci, expectedUci)) {
3721
- return { kind: 'correct', uci };
3780
+ return { kind: 'correct', attempt };
3722
3781
  }
3723
- return { kind: 'incorrect', uci };
3782
+ return { kind: 'incorrect', attempt };
3724
3783
  }
3725
3784
  function createExpectedMoveDropHandler({ fen, expectedUci, enabled, onCorrect, onIncorrect, }) {
3726
3785
  return (sourceSquare, targetSquare, piece) => {
3727
3786
  const result = evaluateExpectedMoveDrop(fen, sourceSquare, targetSquare, piece, expectedUci, enabled);
3728
3787
  switch (result.kind) {
3729
3788
  case 'correct':
3730
- onCorrect(result.uci);
3789
+ onCorrect(result.attempt);
3731
3790
  return true;
3732
3791
  case 'incorrect':
3733
- onIncorrect(result.uci);
3792
+ onIncorrect(result.attempt);
3734
3793
  return false;
3735
3794
  default:
3736
3795
  return false;
@@ -3782,6 +3841,69 @@ function useCorrectMoveFeedback(delayMs = CORRECT_MOVE_FEEDBACK_MS) {
3782
3841
  };
3783
3842
  }
3784
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
+
3785
3907
  /** Minimum eval loss (pawns) from the wrong move before showing a refutation. */
3786
3908
  const REFUTATION_EVAL_GAP_PAWNS = 0.5;
3787
3909
  const REFUTATION_EVAL_GAP_CP = REFUTATION_EVAL_GAP_PAWNS * 100;
@@ -3884,9 +4006,9 @@ function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor)
3884
4006
  switch (phase) {
3885
4007
  case 'wrong':
3886
4008
  return {
3887
- fen: fenAfterWrong !== null && fenAfterWrong !== void 0 ? fenAfterWrong : setupFen,
4009
+ fen: setupFen,
3888
4010
  arrows: [],
3889
- lastMoveUci: attemptedUci,
4011
+ lastMoveUci: null,
3890
4012
  animating: false,
3891
4013
  };
3892
4014
  case 'refutation': {
@@ -4096,4 +4218,4 @@ function useMissBoard({ feedback, expectedUci, positionFen, answerArrowColor, au
4096
4218
  };
4097
4219
  }
4098
4220
 
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 };
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,
@@ -3227,7 +3286,7 @@ class AnalysisPosition {
3227
3286
  }
3228
3287
 
3229
3288
  const useAnalysisBoardModel = ({ analysisContext, onClose, theme, boardWidth, engine, }) => {
3230
- var _a, _b, _c, _d;
3289
+ var _a;
3231
3290
  const skipBackdropCloseRef = react.useRef(true);
3232
3291
  const analysisPosition = react.useMemo(() => new AnalysisPosition(analysisContext), [analysisContext]);
3233
3292
  const [navRevision, setNavRevision] = react.useState(0);
@@ -3243,12 +3302,7 @@ const useAnalysisBoardModel = ({ analysisContext, onClose, theme, boardWidth, en
3243
3302
  }, [analysisContext, analysisPosition]);
3244
3303
  const fen = analysisPosition.fen();
3245
3304
  const engineEnabled = (_a = engine === null || engine === void 0 ? void 0 : engine.enabled) !== null && _a !== void 0 ? _a : true;
3246
- const engineEvaluation = useAnalysisEngine(fen, {
3247
- enabled: engineEnabled,
3248
- depth: (_b = engine === null || engine === void 0 ? void 0 : engine.depth) !== null && _b !== void 0 ? _b : 16,
3249
- multiPv: (_c = engine === null || engine === void 0 ? void 0 : engine.multiPv) !== null && _c !== void 0 ? _c : 2,
3250
- scriptUrl: (_d = engine === null || engine === void 0 ? void 0 : engine.scriptUrl) !== null && _d !== void 0 ? _d : DEFAULT_STOCKFISH_SCRIPT_URL,
3251
- });
3305
+ const engineEvaluation = useAnalysisEngine(fen, Object.assign(Object.assign({ depth: 16, multiPv: 2, scriptUrl: DEFAULT_STOCKFISH_SCRIPT_URL }, engine), { enabled: engineEnabled }));
3252
3306
  return {
3253
3307
  theme,
3254
3308
  boardWidth,
@@ -3719,20 +3773,25 @@ function evaluateExpectedMoveDrop(fen, sourceSquare, targetSquare, piece, expect
3719
3773
  if (!uci) {
3720
3774
  return { kind: 'illegal' };
3721
3775
  }
3776
+ const attempt = {
3777
+ uci,
3778
+ sourceSquare,
3779
+ targetSquare,
3780
+ };
3722
3781
  if (matchesExpectedUci(uci, expectedUci)) {
3723
- return { kind: 'correct', uci };
3782
+ return { kind: 'correct', attempt };
3724
3783
  }
3725
- return { kind: 'incorrect', uci };
3784
+ return { kind: 'incorrect', attempt };
3726
3785
  }
3727
3786
  function createExpectedMoveDropHandler({ fen, expectedUci, enabled, onCorrect, onIncorrect, }) {
3728
3787
  return (sourceSquare, targetSquare, piece) => {
3729
3788
  const result = evaluateExpectedMoveDrop(fen, sourceSquare, targetSquare, piece, expectedUci, enabled);
3730
3789
  switch (result.kind) {
3731
3790
  case 'correct':
3732
- onCorrect(result.uci);
3791
+ onCorrect(result.attempt);
3733
3792
  return true;
3734
3793
  case 'incorrect':
3735
- onIncorrect(result.uci);
3794
+ onIncorrect(result.attempt);
3736
3795
  return false;
3737
3796
  default:
3738
3797
  return false;
@@ -3784,6 +3843,69 @@ function useCorrectMoveFeedback(delayMs = CORRECT_MOVE_FEEDBACK_MS) {
3784
3843
  };
3785
3844
  }
3786
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
+
3787
3909
  /** Minimum eval loss (pawns) from the wrong move before showing a refutation. */
3788
3910
  const REFUTATION_EVAL_GAP_PAWNS = 0.5;
3789
3911
  const REFUTATION_EVAL_GAP_CP = REFUTATION_EVAL_GAP_PAWNS * 100;
@@ -3886,9 +4008,9 @@ function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor)
3886
4008
  switch (phase) {
3887
4009
  case 'wrong':
3888
4010
  return {
3889
- fen: fenAfterWrong !== null && fenAfterWrong !== void 0 ? fenAfterWrong : setupFen,
4011
+ fen: setupFen,
3890
4012
  arrows: [],
3891
- lastMoveUci: attemptedUci,
4013
+ lastMoveUci: null,
3892
4014
  animating: false,
3893
4015
  };
3894
4016
  case 'refutation': {
@@ -4108,11 +4230,13 @@ exports.AnalysisErrorBoundary = AnalysisErrorBoundary;
4108
4230
  exports.AnalysisPosition = AnalysisPosition;
4109
4231
  exports.BOARD_THEMES = BOARD_THEMES;
4110
4232
  exports.BOARD_THEME_IDS = BOARD_THEME_IDS;
4233
+ exports.BoardCompleteCheckOverlay = BoardCompleteCheckOverlay;
4111
4234
  exports.CORRECT_MOVE_FEEDBACK_MS = CORRECT_MOVE_FEEDBACK_MS;
4112
4235
  exports.ChessboardDnDProvider = ChessboardDnDProvider;
4113
4236
  exports.ChessboardThemeContext = ChessboardThemeContext;
4114
4237
  exports.CorrectMoveCheckBadge = CorrectMoveCheckBadge;
4115
4238
  exports.DEFAULT_ANALYSIS_LAYOUT = DEFAULT_ANALYSIS_LAYOUT;
4239
+ exports.DEFAULT_ANSWER_ARROW_COLOR = DEFAULT_ANSWER_ARROW_COLOR;
4116
4240
  exports.DEFAULT_BOARD_THEME = DEFAULT_BOARD_THEME;
4117
4241
  exports.DEFAULT_LAST_MOVE_ARROW_COLOR = DEFAULT_LAST_MOVE_ARROW_COLOR;
4118
4242
  exports.DEFAULT_STOCKFISH_SCRIPT_URL = DEFAULT_STOCKFISH_SCRIPT_URL;
@@ -4121,6 +4245,7 @@ exports.DefaultAnalysisSidebar = DefaultAnalysisSidebar;
4121
4245
  exports.DefaultPlyNavigation = DefaultPlyNavigation;
4122
4246
  exports.EngineEvaluationPanel = EngineEvaluationPanel;
4123
4247
  exports.HighlightChessboard = HighlightChessboard;
4248
+ exports.IncorrectMoveXBadge = IncorrectMoveXBadge;
4124
4249
  exports.MISS_MOVE_ANIMATION_MS = MISS_MOVE_ANIMATION_MS;
4125
4250
  exports.MISS_REFUTATION_MAX_WAIT_MS = MISS_REFUTATION_MAX_WAIT_MS;
4126
4251
  exports.MISS_REFUTATION_PAUSE_MS = MISS_REFUTATION_PAUSE_MS;
@@ -4133,6 +4258,7 @@ exports.ThemeProvider = ThemeProvider;
4133
4258
  exports.analysisBoardHighlightColors = analysisBoardHighlightColors;
4134
4259
  exports.analysisSidebarColors = analysisSidebarColors;
4135
4260
  exports.applyUciMove = applyUciMove;
4261
+ exports.boardArrowColors = boardArrowColors;
4136
4262
  exports.boardSquareHighlightColors = boardSquareHighlightColors;
4137
4263
  exports.boardThemeFromLegacyUiTheme = boardThemeFromLegacyUiTheme;
4138
4264
  exports.createExpectedMoveDropHandler = createExpectedMoveDropHandler;
@@ -4184,8 +4310,10 @@ exports.useAnalysisEngineContext = useAnalysisEngineContext;
4184
4310
  exports.useBoardRevision = useBoardRevision;
4185
4311
  exports.useChessboardTheme = useChessboardTheme;
4186
4312
  exports.useCorrectMoveFeedback = useCorrectMoveFeedback;
4313
+ exports.useIncorrectMoveFeedback = useIncorrectMoveFeedback;
4187
4314
  exports.useMissBoard = useMissBoard;
4188
4315
  exports.useMissRefutation = useMissRefutation;
4189
4316
  exports.useMissSequence = useMissSequence;
4190
4317
  exports.usePositionKeyboardNav = usePositionKeyboardNav;
4191
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.4",
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",