react-chess-puzzle-kit 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,8 @@ export interface LineBoardProps {
4
4
  orientation: 'white' | 'black';
5
5
  trainSide: LineTrainSide;
6
6
  draggable: boolean;
7
+ correctMoveSquare?: string | null;
8
+ lastMoveUci?: string | null;
7
9
  onPieceDrop: (source: string, target: string, piece: string) => boolean;
8
10
  boardWidth: number;
9
11
  }
@@ -12,4 +14,4 @@ export interface LineBoardProps {
12
14
  * side's pieces be dragged. Move validation and sequencing live in
13
15
  * {@link LineBoardWithControls}.
14
16
  */
15
- export declare const LineBoard: ({ fen, orientation, trainSide, draggable, onPieceDrop, boardWidth, }: LineBoardProps) => import("react").JSX.Element;
17
+ export declare const LineBoard: ({ fen, orientation, trainSide, draggable, correctMoveSquare, lastMoveUci, onPieceDrop, boardWidth, }: LineBoardProps) => import("react").JSX.Element;
@@ -29,6 +29,8 @@ export type BoardCaptionRenderProps = {
29
29
  answerArrowVisible?: boolean;
30
30
  /** True when the card finished after a wrong move, hint, or solution reveal. */
31
31
  completedAfterMiss?: boolean;
32
+ /** True when the user requested a hint on the current card. */
33
+ hintUsed?: boolean;
32
34
  };
33
35
  export type BoardFeedbackRenderProps = {
34
36
  resultStatus: Extract<PuzzleResultStatus, 'complete' | 'incorrect'>;
@@ -42,6 +44,8 @@ export type BoardFeedbackRenderProps = {
42
44
  answerArrowVisible?: boolean;
43
45
  /** True when the card finished after a wrong move, hint, or solution reveal. */
44
46
  completedAfterMiss?: boolean;
47
+ /** True when the user requested a hint on the current card. */
48
+ hintUsed?: boolean;
45
49
  };
46
50
  export type PuzzleFetchResult = {
47
51
  fen: string;
@@ -84,6 +88,8 @@ export interface PuzzleBoardWithControlsProps {
84
88
  puzzleBoardWidth?: number;
85
89
  /** Board + sidebar grid sizes when analysis is open. */
86
90
  analysisLayout?: AnalysisLayoutConfig;
91
+ /** Chessboard pixel width in analysis (defaults to {@link analysisLayout}.boardWidth). */
92
+ analysisBoardWidth?: number;
87
93
  /** Custom board/sidebar placement (overrides {@link analysisLayout} grid). */
88
94
  renderAnalysisMain?: (props: AnalysisMainRenderProps) => React.ReactNode;
89
95
  engine?: AnalysisEngineOptions;
@@ -105,4 +111,4 @@ export interface PuzzleBoardWithControlsProps {
105
111
  refutationEngine?: AnalysisEngineOptions;
106
112
  answerArrowColor?: string;
107
113
  }
108
- export declare const PuzzleBoardWithControls: ({ theme, boardTheme, apiProxy, renderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth, analysisLayout, renderAnalysisMain, engine, autoAdvanceOnComplete, autoAdvanceOnCompleteAfterIncorrect, revealAnswerOnIncorrect, showAnswerArrowOnIncorrect, allowRetryOnIncorrect, showRefutationOnIncorrect, autoShowWrongMoves, refutationEngine, answerArrowColor, }: PuzzleBoardWithControlsProps) => React.JSX.Element;
114
+ export declare const PuzzleBoardWithControls: ({ theme, boardTheme, apiProxy, renderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth, analysisLayout, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete, autoAdvanceOnCompleteAfterIncorrect, revealAnswerOnIncorrect, showAnswerArrowOnIncorrect, allowRetryOnIncorrect, showRefutationOnIncorrect, autoShowWrongMoves, refutationEngine, answerArrowColor, }: PuzzleBoardWithControlsProps) => React.JSX.Element;
package/dist/index.d.ts CHANGED
@@ -23,4 +23,7 @@ export declare const squareHighlightColors: {
23
23
  readonly check: "rgba(255, 127, 127, 0.8)";
24
24
  readonly hint: "rgba(119, 177, 212, 0.75)";
25
25
  readonly incorrect: "rgba(140, 38, 38, 0.82)";
26
+ readonly selected: "rgba(255, 255, 0, 0.45)";
27
+ readonly moveTarget: "radial-gradient(circle, rgba(0, 0, 0, 0.18) 22%, transparent 22%)";
28
+ readonly captureTarget: "radial-gradient(circle, rgba(0, 0, 0, 0.18) 72%, transparent 72%)";
26
29
  };
package/dist/index.esm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
2
2
  import { jsx, jsxs } from 'react/jsx-runtime';
3
- import { useBoardRevision, useMissBoard, ChessboardDnDProvider, HighlightChessboard, uciFromDrop, ThemeProvider, AnalysisErrorBoundary, AnalysisBoardCore, AnalysisBoardLayout, AnalysisBoard, DEFAULT_ANALYSIS_LAYOUT, evaluateExpectedMoveDrop, boardSquareHighlightColors, analysisBoardHighlightColors } from 'react-chess-core';
3
+ import { useBoardRevision, useCorrectMoveFeedback, useMissBoard, ChessboardDnDProvider, HighlightChessboard, uciFromDrop, ThemeProvider, AnalysisErrorBoundary, AnalysisBoardCore, AnalysisBoardLayout, AnalysisBoard, DEFAULT_ANALYSIS_LAYOUT, lastMoveUciAtPly, evaluateExpectedMoveDrop, fenAfterUci, boardSquareHighlightColors, analysisBoardHighlightColors } from 'react-chess-core';
4
4
  export { DEFAULT_ANALYSIS_LAYOUT } from 'react-chess-core';
5
5
  import { Chess } from 'chess.js';
6
6
 
@@ -67,6 +67,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
67
67
  const [incorrectActive, setIncorrectActive] = useState(false);
68
68
  const attemptMissedRef = useRef(false);
69
69
  const { revision, bumpRevision } = useBoardRevision();
70
+ const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, } = useCorrectMoveFeedback();
70
71
  const boardOrientationRef = useRef('white');
71
72
  const boardFenRef = useRef(EMPTY_BOARD_FEN);
72
73
  const notifyHost = () => {
@@ -92,7 +93,9 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
92
93
  expectedUci: expectedUci || null,
93
94
  positionFen,
94
95
  answerArrowColor,
95
- autoShowWrongMoves,
96
+ // Refutation + answer-arrow flows must run the full wrong→refutation→answer
97
+ // sequence; the replay "retry without arrow" setting does not apply here.
98
+ autoShowWrongMoves: useRefutation ? true : autoShowWrongMoves,
96
99
  engineOptions: refutationEngine,
97
100
  });
98
101
  const missPhase = (_c = missBoard.missSequence.sequence) === null || _c === void 0 ? void 0 : _c.phase;
@@ -103,8 +106,9 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
103
106
  setShowAnswerArrow(false);
104
107
  setIncorrectActive(false);
105
108
  attemptMissedRef.current = false;
109
+ clearCorrectMoveFeedback();
106
110
  onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
107
- }, [onMissFeedbackChange, position]);
111
+ }, [clearCorrectMoveFeedback, onMissFeedbackChange, position]);
108
112
  useEffect(() => {
109
113
  var _a, _b;
110
114
  if (!onMissFeedbackChange) {
@@ -162,7 +166,10 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
162
166
  (missBoard.boardAnimating ||
163
167
  missPhase === 'wrong' ||
164
168
  missPhase === 'refutation');
165
- const arePiecesDraggable = position !== null && !positionLocked && !missLocked;
169
+ const arePiecesDraggable = position !== null &&
170
+ !positionLocked &&
171
+ !missLocked &&
172
+ correctMoveSquare === null;
166
173
  const onPieceDrop = (sourceSquare, targetSquare, piece) => {
167
174
  if (!position || positionLocked || position.isSolutionRevealed()) {
168
175
  return false;
@@ -227,6 +234,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
227
234
  setIncorrectActive(false);
228
235
  missBoard.missSequence.clearSequence();
229
236
  onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
237
+ clearCorrectMoveFeedback();
230
238
  const assistedByAnswerArrow = answerArrowVisible && attemptMissedRef.current;
231
239
  const guessPayload = {
232
240
  index: position.getIndex(),
@@ -242,32 +250,31 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
242
250
  else {
243
251
  onFeedback(Object.assign(Object.assign({}, guessPayload), { isCorrect: true, isFinished: guess.finished }));
244
252
  }
253
+ position.next();
254
+ boardFenRef.current = position.fen();
245
255
  notifyHost();
246
- setTimeout(() => {
256
+ const finishCorrectFeedback = () => {
247
257
  position.resetInteractions();
248
258
  notifyHost();
249
- }, 500);
250
- if (position.isAlternativeCheckmate()) {
251
- notifyHost();
252
- return true;
253
- }
254
- position.next();
255
- notifyHost();
256
- if (position.hasResumeConfig()) {
257
- onResumeCorrect === null || onResumeCorrect === void 0 ? void 0 : onResumeCorrect(position);
258
- return true;
259
- }
260
- setTimeout(() => {
259
+ if (position.isAlternativeCheckmate()) {
260
+ return;
261
+ }
262
+ if (position.hasResumeConfig()) {
263
+ onResumeCorrect === null || onResumeCorrect === void 0 ? void 0 : onResumeCorrect(position);
264
+ return;
265
+ }
261
266
  if (!position.isFinished()) {
262
267
  position.next();
268
+ boardFenRef.current = position.fen();
263
269
  }
264
270
  notifyHost();
265
- }, 500);
271
+ };
272
+ showCorrectMove(targetSquare, finishCorrectFeedback);
266
273
  return true;
267
274
  };
268
275
  return (jsx(ChessboardDnDProvider, { children: hasBoard ? (jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: (_e = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _e !== void 0 ? _e : '', hintSquare: (_f = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _f !== void 0 ? _f : null, incorrectMoveSquare: showAnswerArrowOnIncorrect
269
276
  ? null
270
- : ((_g = position === null || position === void 0 ? void 0 : position.getIncorrectMoveSquare()) !== null && _g !== void 0 ? _g : null), customArrows: customArrows, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: 0 }, revision)) : null }));
277
+ : ((_g = position === null || position === void 0 ? void 0 : position.getIncorrectMoveSquare()) !== null && _g !== void 0 ? _g : null), correctMoveSquare: correctMoveSquare, customArrows: customArrows, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: 0 }, revision)) : null }));
271
278
  };
272
279
 
273
280
  const PuzzleBoard = ({ position, onFeedback, incInteractionNum, boardWidth, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, answerArrowColor, }) => (jsx(PuzzlePlaySurface, { position: position, onFeedback: onFeedback, incInteractionNum: incInteractionNum, boardWidth: boardWidth, revealAnswerOnIncorrect: revealAnswerOnIncorrect, showAnswerArrowOnIncorrect: showAnswerArrowOnIncorrect, allowRetryOnIncorrect: allowRetryOnIncorrect, answerArrowColor: answerArrowColor }));
@@ -732,7 +739,7 @@ const puzzlePositionFromFetch = (fen, moves, resume) => {
732
739
  const AUTO_ADVANCE_ON_COMPLETE_DELAY_MS = 700;
733
740
  const SOLUTION_STEP_MS = 500;
734
741
  const RESUME_AUTO_STEP_MS = 500;
735
- const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls = defaultRenderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, analysisLayout = DEFAULT_ANALYSIS_LAYOUT, renderAnalysisMain, engine, autoAdvanceOnComplete = false, autoAdvanceOnCompleteAfterIncorrect = false, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect, autoShowWrongMoves = true, refutationEngine, answerArrowColor, }) => {
742
+ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls = defaultRenderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, analysisLayout = DEFAULT_ANALYSIS_LAYOUT, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete = false, autoAdvanceOnCompleteAfterIncorrect = false, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect, autoShowWrongMoves = true, refutationEngine, answerArrowColor, }) => {
736
743
  var _a, _b, _c, _d;
737
744
  const refutationOnIncorrect = showRefutationOnIncorrect !== null && showRefutationOnIncorrect !== void 0 ? showRefutationOnIncorrect : showAnswerArrowOnIncorrect;
738
745
  const stackControlsBelow = useStackPuzzleControlsBelow();
@@ -744,6 +751,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
744
751
  const [loadingNextPuzzle, setLoadingNextPuzzle] = useState(true);
745
752
  const [puzzleNum, setPuzzleNum] = useState(0);
746
753
  const [hasIncorrectAttempt, setHasIncorrectAttempt] = useState(false);
754
+ const [hintUsed, setHintUsed] = useState(false);
747
755
  const [puzzleComplete, setPuzzleComplete] = useState(false);
748
756
  const [completedAfterMiss, setCompletedAfterMiss] = useState(false);
749
757
  const [missFeedback, setMissFeedback] = useState(null);
@@ -769,6 +777,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
769
777
  let cancelled = false;
770
778
  setLoadingNextPuzzle(true);
771
779
  setHasIncorrectAttempt(false);
780
+ setHintUsed(false);
772
781
  setPuzzleComplete(false);
773
782
  setCompletedAfterMiss(false);
774
783
  setMissFeedback(null);
@@ -805,12 +814,18 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
805
814
  const incorrectThisFeedback = feedbackData.hintRequested ||
806
815
  feedbackData.solutionShown ||
807
816
  feedbackData.isCorrect === false;
817
+ if (feedbackData.hintRequested) {
818
+ setHintUsed(true);
819
+ }
808
820
  if (incorrectThisFeedback) {
809
821
  setHasIncorrectAttempt(true);
810
822
  }
811
823
  if (feedbackData.isFinished) {
812
824
  setPuzzleComplete(true);
813
- setCompletedAfterMiss(hasIncorrectAttempt || incorrectThisFeedback);
825
+ setCompletedAfterMiss((prev) => prev ||
826
+ hasIncorrectAttempt ||
827
+ incorrectThisFeedback ||
828
+ feedbackData.hintRequested === true);
814
829
  }
815
830
  onFeedback(feedbackData);
816
831
  };
@@ -1014,10 +1029,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1014
1029
  };
1015
1030
  const analysis = usePuzzleAnalysis(position, resultStatus, puzzleNum);
1016
1031
  const analysisSnapshot = analysis.isOpen && analysis.snapshot ? analysis.snapshot : null;
1032
+ const resolvedAnalysisBoardWidth = analysisBoardWidth !== null && analysisBoardWidth !== void 0 ? analysisBoardWidth : analysisLayout.boardWidth;
1017
1033
  const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
1018
1034
  renderAnalysisContainer &&
1019
1035
  (renderEngineEvaluation || (engine === null || engine === void 0 ? void 0 : engine.enabled) === false));
1020
- return (jsx(ThemeProvider, { theme: theme, boardTheme: boardTheme, children: analysisSnapshot ? (jsx(AnalysisErrorBoundary, { onClose: analysis.closeAnalysis, children: useHostAnalysisUi ? (jsx(AnalysisBoardCore, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, boardWidth: analysisLayout.boardWidth, engine: engine, renderMain: renderAnalysisMain !== null && renderAnalysisMain !== void 0 ? renderAnalysisMain : (({ board, sidebar, model }) => (jsx(AnalysisBoardLayout, { layout: analysisLayout, model: model, board: board, sidebar: sidebar }))), renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation !== null && renderEngineEvaluation !== void 0 ? renderEngineEvaluation : (() => null) })) : (jsx(AnalysisBoard, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, layout: analysisLayout, engine: engine, renderMain: renderAnalysisMain, renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation })) })) : (jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsxs("div", { style: puzzleBoardColumnStyle(puzzleBoardWidth, controlsPlacement), children: [jsx("div", { style: puzzleBoardSlotWrapperStyle(), children: jsx("div", { style: puzzleBoardSlotStyle(), children: jsx(PuzzlePlaySurface, { position: position, boardWidth: puzzleBoardWidth, onFeedback: handleFeedback, incInteractionNum: incInteractionNum, onResumeCorrect: runResumeAutoAdvance, revealAnswerOnIncorrect: revealAnswerOnIncorrect, showAnswerArrowOnIncorrect: showAnswerArrowOnIncorrect, allowRetryOnIncorrect: allowRetryOnIncorrect, showRefutationOnIncorrect: refutationOnIncorrect, autoShowWrongMoves: autoShowWrongMoves, refutationEngine: refutationEngine !== null && refutationEngine !== void 0 ? refutationEngine : engine, answerArrowColor: answerArrowColor, positionLocked: loadingNextPuzzle, onMissFeedbackChange: setMissFeedback }) }) }), renderBoardCaption && (jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
1036
+ return (jsx(ThemeProvider, { theme: theme, boardTheme: boardTheme, children: analysisSnapshot ? (jsx(AnalysisErrorBoundary, { onClose: analysis.closeAnalysis, children: useHostAnalysisUi ? (jsx(AnalysisBoardCore, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, boardWidth: resolvedAnalysisBoardWidth, engine: engine, renderMain: renderAnalysisMain !== null && renderAnalysisMain !== void 0 ? renderAnalysisMain : (({ board, sidebar, model }) => (jsx(AnalysisBoardLayout, { layout: analysisLayout, model: model, board: board, sidebar: sidebar }))), renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation !== null && renderEngineEvaluation !== void 0 ? renderEngineEvaluation : (() => null) })) : (jsx(AnalysisBoard, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, layout: analysisLayout, engine: engine, renderMain: renderAnalysisMain, renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation })) })) : (jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsxs("div", { style: puzzleBoardColumnStyle(puzzleBoardWidth, controlsPlacement), children: [jsx("div", { style: puzzleBoardSlotWrapperStyle(), children: jsx("div", { style: puzzleBoardSlotStyle(), children: jsx(PuzzlePlaySurface, { position: position, boardWidth: puzzleBoardWidth, onFeedback: handleFeedback, incInteractionNum: incInteractionNum, onResumeCorrect: runResumeAutoAdvance, revealAnswerOnIncorrect: revealAnswerOnIncorrect, showAnswerArrowOnIncorrect: showAnswerArrowOnIncorrect, allowRetryOnIncorrect: allowRetryOnIncorrect, showRefutationOnIncorrect: refutationOnIncorrect, autoShowWrongMoves: autoShowWrongMoves, refutationEngine: refutationEngine !== null && refutationEngine !== void 0 ? refutationEngine : engine, answerArrowColor: answerArrowColor, positionLocked: loadingNextPuzzle, onMissFeedbackChange: setMissFeedback }) }) }), renderBoardCaption && (jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
1021
1037
  sideToMove: (_a = position === null || position === void 0 ? void 0 : position.getSideToMove()) !== null && _a !== void 0 ? _a : null,
1022
1038
  playerColor: position
1023
1039
  ? position.getPlayerColor()
@@ -1029,6 +1045,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1029
1045
  missPhase: (_c = missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.phase) !== null && _c !== void 0 ? _c : null,
1030
1046
  answerArrowVisible: (_d = missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.answerArrowVisible) !== null && _d !== void 0 ? _d : false,
1031
1047
  completedAfterMiss,
1048
+ hintUsed,
1032
1049
  }) }))] }), jsxs("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: [renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
1033
1050
  visible: analysis.canOpen,
1034
1051
  openAnalysis: analysis.openAnalysis,
@@ -1037,6 +1054,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1037
1054
  cleanSolve: !hasIncorrectAttempt,
1038
1055
  refutationSan: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan,
1039
1056
  missPhase: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.phase,
1057
+ hintUsed,
1040
1058
  }) }))] })] })) }));
1041
1059
  };
1042
1060
 
@@ -1045,7 +1063,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1045
1063
  * side's pieces be dragged. Move validation and sequencing live in
1046
1064
  * {@link LineBoardWithControls}.
1047
1065
  */
1048
- const LineBoard = ({ fen, orientation, trainSide, draggable, onPieceDrop, boardWidth, }) => (jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, position: fen, boardOrientation: orientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => piece[0] === trainSide, onPieceDrop: (source, target, piece) => onPieceDrop(source, target, piece), autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }) }));
1066
+ const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, lastMoveUci = null, onPieceDrop, boardWidth, }) => (jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, correctMoveSquare: correctMoveSquare, position: fen, boardOrientation: orientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => piece[0] === trainSide, onPieceDrop: (source, target, piece) => onPieceDrop(source, target, piece), lastMoveUci: lastMoveUci, autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }) }));
1049
1067
 
1050
1068
  /** Library default line-drill status controls (unstyled). */
1051
1069
  const DefaultLineControls = ({ moveNumber, total, finished, isUserTurn, feedback, }) => (jsxs("div", { style: rowStyle, children: [jsx("span", { style: statusStyle, children: finished ? 'Line complete' : `Move ${moveNumber} of ${total}` }), feedback && !finished && (jsx("span", { style: Object.assign(Object.assign({}, statusStyle), { color: feedback.isCorrect ? '#2e7d32' : '#c62828' }), children: feedback.isCorrect
@@ -1092,6 +1110,8 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1092
1110
  const [currentIndex, setCurrentIndex] = useState(0);
1093
1111
  const [finished, setFinished] = useState(false);
1094
1112
  const [feedback, setFeedback] = useState(null);
1113
+ const [displayFen, setDisplayFen] = useState(null);
1114
+ const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = useCorrectMoveFeedback();
1095
1115
  const total = line.movesUci.length;
1096
1116
  const orientation = boardOrientationForLine(line.trainSide);
1097
1117
  const applyMove = useCallback((index) => {
@@ -1116,7 +1136,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1116
1136
  }, [line.movesUci]);
1117
1137
  // Auto-play opponent moves and detect the end of the line.
1118
1138
  useEffect(() => {
1119
- if (finished) {
1139
+ if (finished || isShowingCorrectMove) {
1120
1140
  return;
1121
1141
  }
1122
1142
  if (currentIndex >= total) {
@@ -1135,6 +1155,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1135
1155
  line.trainSide,
1136
1156
  applyMove,
1137
1157
  opponentMoveDelayMs,
1158
+ isShowingCorrectMove,
1138
1159
  ]);
1139
1160
  // Emit the completion event exactly once.
1140
1161
  useEffect(() => {
@@ -1146,15 +1167,16 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1146
1167
  }, [finished]);
1147
1168
  const handleDrop = (source, target, piece) => {
1148
1169
  var _a, _b, _c;
1149
- if (finished) {
1170
+ if (finished || isShowingCorrectMove) {
1150
1171
  return false;
1151
1172
  }
1152
- if (turnFromFen(chessRef.current.fen()) !== line.trainSide) {
1173
+ const setupFen = displayFen !== null && displayFen !== void 0 ? displayFen : chessRef.current.fen();
1174
+ if (turnFromFen(setupFen) !== line.trainSide) {
1153
1175
  return false;
1154
1176
  }
1155
1177
  const index = currentIndex;
1156
1178
  const expected = line.movesUci[index];
1157
- const dropResult = evaluateExpectedMoveDrop(chessRef.current.fen(), source, target, piece, expected, true);
1179
+ const dropResult = evaluateExpectedMoveDrop(setupFen, source, target, piece, expected, true);
1158
1180
  if (dropResult.kind === 'illegal' || dropResult.kind === 'ignored') {
1159
1181
  return false;
1160
1182
  }
@@ -1164,16 +1186,38 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1164
1186
  const moveFeedback = { index, isCorrect, expectedSan };
1165
1187
  setFeedback(moveFeedback);
1166
1188
  (_c = onMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(onMoveRef, moveFeedback);
1167
- applyMove(index);
1189
+ if (isCorrect) {
1190
+ const nextFen = fenAfterUci(setupFen, dropResult.uci);
1191
+ if (nextFen) {
1192
+ setDisplayFen(nextFen);
1193
+ }
1194
+ showCorrectMove(target, () => {
1195
+ setDisplayFen(null);
1196
+ setFeedback(null);
1197
+ clearCorrectMoveFeedback();
1198
+ applyMove(index);
1199
+ });
1200
+ }
1168
1201
  return isCorrect;
1169
1202
  };
1203
+ const boardFen = displayFen !== null && displayFen !== void 0 ? displayFen : fen;
1204
+ const lastMoveUci = useMemo(() => {
1205
+ var _a;
1206
+ if (displayFen) {
1207
+ return (_a = line.movesUci[currentIndex]) !== null && _a !== void 0 ? _a : null;
1208
+ }
1209
+ return lastMoveUciAtPly(line.movesUci, currentIndex);
1210
+ }, [currentIndex, displayFen, line.movesUci]);
1170
1211
  const moveNumber = Math.min(currentIndex + 1, total);
1171
- const isUserTurn = !finished && turnFromFen(fen) === line.trainSide && currentIndex < total;
1212
+ const isUserTurn = !finished &&
1213
+ !isShowingCorrectMove &&
1214
+ turnFromFen(boardFen) === line.trainSide &&
1215
+ currentIndex < total;
1172
1216
  const stackControlsBelow = useStackPuzzleControlsBelow();
1173
1217
  const controlsPlacement = stackControlsBelow
1174
1218
  ? 'below'
1175
1219
  : 'beside';
1176
- return (jsx(ThemeProvider, { theme: theme, boardTheme: boardTheme, children: jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsx("div", { style: puzzleBoardColumnStyle(boardWidth, controlsPlacement), children: jsx("div", { style: puzzleBoardSlotStyle(), children: jsx(LineBoard, { fen: fen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
1220
+ return (jsx(ThemeProvider, { theme: theme, boardTheme: boardTheme, children: jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsx("div", { style: puzzleBoardColumnStyle(boardWidth, controlsPlacement), children: jsx("div", { style: puzzleBoardSlotStyle(), children: jsx(LineBoard, { fen: boardFen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, correctMoveSquare: correctMoveSquare, lastMoveUci: lastMoveUci, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
1177
1221
  trainSide: line.trainSide,
1178
1222
  moveNumber,
1179
1223
  total,
package/dist/index.js CHANGED
@@ -68,6 +68,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
68
68
  const [incorrectActive, setIncorrectActive] = react.useState(false);
69
69
  const attemptMissedRef = react.useRef(false);
70
70
  const { revision, bumpRevision } = reactChessCore.useBoardRevision();
71
+ const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, } = reactChessCore.useCorrectMoveFeedback();
71
72
  const boardOrientationRef = react.useRef('white');
72
73
  const boardFenRef = react.useRef(EMPTY_BOARD_FEN);
73
74
  const notifyHost = () => {
@@ -93,7 +94,9 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
93
94
  expectedUci: expectedUci || null,
94
95
  positionFen,
95
96
  answerArrowColor,
96
- autoShowWrongMoves,
97
+ // Refutation + answer-arrow flows must run the full wrong→refutation→answer
98
+ // sequence; the replay "retry without arrow" setting does not apply here.
99
+ autoShowWrongMoves: useRefutation ? true : autoShowWrongMoves,
97
100
  engineOptions: refutationEngine,
98
101
  });
99
102
  const missPhase = (_c = missBoard.missSequence.sequence) === null || _c === void 0 ? void 0 : _c.phase;
@@ -104,8 +107,9 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
104
107
  setShowAnswerArrow(false);
105
108
  setIncorrectActive(false);
106
109
  attemptMissedRef.current = false;
110
+ clearCorrectMoveFeedback();
107
111
  onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
108
- }, [onMissFeedbackChange, position]);
112
+ }, [clearCorrectMoveFeedback, onMissFeedbackChange, position]);
109
113
  react.useEffect(() => {
110
114
  var _a, _b;
111
115
  if (!onMissFeedbackChange) {
@@ -163,7 +167,10 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
163
167
  (missBoard.boardAnimating ||
164
168
  missPhase === 'wrong' ||
165
169
  missPhase === 'refutation');
166
- const arePiecesDraggable = position !== null && !positionLocked && !missLocked;
170
+ const arePiecesDraggable = position !== null &&
171
+ !positionLocked &&
172
+ !missLocked &&
173
+ correctMoveSquare === null;
167
174
  const onPieceDrop = (sourceSquare, targetSquare, piece) => {
168
175
  if (!position || positionLocked || position.isSolutionRevealed()) {
169
176
  return false;
@@ -228,6 +235,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
228
235
  setIncorrectActive(false);
229
236
  missBoard.missSequence.clearSequence();
230
237
  onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
238
+ clearCorrectMoveFeedback();
231
239
  const assistedByAnswerArrow = answerArrowVisible && attemptMissedRef.current;
232
240
  const guessPayload = {
233
241
  index: position.getIndex(),
@@ -243,32 +251,31 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
243
251
  else {
244
252
  onFeedback(Object.assign(Object.assign({}, guessPayload), { isCorrect: true, isFinished: guess.finished }));
245
253
  }
254
+ position.next();
255
+ boardFenRef.current = position.fen();
246
256
  notifyHost();
247
- setTimeout(() => {
257
+ const finishCorrectFeedback = () => {
248
258
  position.resetInteractions();
249
259
  notifyHost();
250
- }, 500);
251
- if (position.isAlternativeCheckmate()) {
252
- notifyHost();
253
- return true;
254
- }
255
- position.next();
256
- notifyHost();
257
- if (position.hasResumeConfig()) {
258
- onResumeCorrect === null || onResumeCorrect === void 0 ? void 0 : onResumeCorrect(position);
259
- return true;
260
- }
261
- setTimeout(() => {
260
+ if (position.isAlternativeCheckmate()) {
261
+ return;
262
+ }
263
+ if (position.hasResumeConfig()) {
264
+ onResumeCorrect === null || onResumeCorrect === void 0 ? void 0 : onResumeCorrect(position);
265
+ return;
266
+ }
262
267
  if (!position.isFinished()) {
263
268
  position.next();
269
+ boardFenRef.current = position.fen();
264
270
  }
265
271
  notifyHost();
266
- }, 500);
272
+ };
273
+ showCorrectMove(targetSquare, finishCorrectFeedback);
267
274
  return true;
268
275
  };
269
276
  return (jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: hasBoard ? (jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: (_e = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _e !== void 0 ? _e : '', hintSquare: (_f = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _f !== void 0 ? _f : null, incorrectMoveSquare: showAnswerArrowOnIncorrect
270
277
  ? null
271
- : ((_g = position === null || position === void 0 ? void 0 : position.getIncorrectMoveSquare()) !== null && _g !== void 0 ? _g : null), customArrows: customArrows, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: 0 }, revision)) : null }));
278
+ : ((_g = position === null || position === void 0 ? void 0 : position.getIncorrectMoveSquare()) !== null && _g !== void 0 ? _g : null), correctMoveSquare: correctMoveSquare, customArrows: customArrows, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: 0 }, revision)) : null }));
272
279
  };
273
280
 
274
281
  const PuzzleBoard = ({ position, onFeedback, incInteractionNum, boardWidth, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, answerArrowColor, }) => (jsxRuntime.jsx(PuzzlePlaySurface, { position: position, onFeedback: onFeedback, incInteractionNum: incInteractionNum, boardWidth: boardWidth, revealAnswerOnIncorrect: revealAnswerOnIncorrect, showAnswerArrowOnIncorrect: showAnswerArrowOnIncorrect, allowRetryOnIncorrect: allowRetryOnIncorrect, answerArrowColor: answerArrowColor }));
@@ -733,7 +740,7 @@ const puzzlePositionFromFetch = (fen, moves, resume) => {
733
740
  const AUTO_ADVANCE_ON_COMPLETE_DELAY_MS = 700;
734
741
  const SOLUTION_STEP_MS = 500;
735
742
  const RESUME_AUTO_STEP_MS = 500;
736
- const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls = defaultRenderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, analysisLayout = reactChessCore.DEFAULT_ANALYSIS_LAYOUT, renderAnalysisMain, engine, autoAdvanceOnComplete = false, autoAdvanceOnCompleteAfterIncorrect = false, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect, autoShowWrongMoves = true, refutationEngine, answerArrowColor, }) => {
743
+ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls = defaultRenderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, analysisLayout = reactChessCore.DEFAULT_ANALYSIS_LAYOUT, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete = false, autoAdvanceOnCompleteAfterIncorrect = false, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect, autoShowWrongMoves = true, refutationEngine, answerArrowColor, }) => {
737
744
  var _a, _b, _c, _d;
738
745
  const refutationOnIncorrect = showRefutationOnIncorrect !== null && showRefutationOnIncorrect !== void 0 ? showRefutationOnIncorrect : showAnswerArrowOnIncorrect;
739
746
  const stackControlsBelow = useStackPuzzleControlsBelow();
@@ -745,6 +752,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
745
752
  const [loadingNextPuzzle, setLoadingNextPuzzle] = react.useState(true);
746
753
  const [puzzleNum, setPuzzleNum] = react.useState(0);
747
754
  const [hasIncorrectAttempt, setHasIncorrectAttempt] = react.useState(false);
755
+ const [hintUsed, setHintUsed] = react.useState(false);
748
756
  const [puzzleComplete, setPuzzleComplete] = react.useState(false);
749
757
  const [completedAfterMiss, setCompletedAfterMiss] = react.useState(false);
750
758
  const [missFeedback, setMissFeedback] = react.useState(null);
@@ -770,6 +778,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
770
778
  let cancelled = false;
771
779
  setLoadingNextPuzzle(true);
772
780
  setHasIncorrectAttempt(false);
781
+ setHintUsed(false);
773
782
  setPuzzleComplete(false);
774
783
  setCompletedAfterMiss(false);
775
784
  setMissFeedback(null);
@@ -806,12 +815,18 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
806
815
  const incorrectThisFeedback = feedbackData.hintRequested ||
807
816
  feedbackData.solutionShown ||
808
817
  feedbackData.isCorrect === false;
818
+ if (feedbackData.hintRequested) {
819
+ setHintUsed(true);
820
+ }
809
821
  if (incorrectThisFeedback) {
810
822
  setHasIncorrectAttempt(true);
811
823
  }
812
824
  if (feedbackData.isFinished) {
813
825
  setPuzzleComplete(true);
814
- setCompletedAfterMiss(hasIncorrectAttempt || incorrectThisFeedback);
826
+ setCompletedAfterMiss((prev) => prev ||
827
+ hasIncorrectAttempt ||
828
+ incorrectThisFeedback ||
829
+ feedbackData.hintRequested === true);
815
830
  }
816
831
  onFeedback(feedbackData);
817
832
  };
@@ -1015,10 +1030,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1015
1030
  };
1016
1031
  const analysis = usePuzzleAnalysis(position, resultStatus, puzzleNum);
1017
1032
  const analysisSnapshot = analysis.isOpen && analysis.snapshot ? analysis.snapshot : null;
1033
+ const resolvedAnalysisBoardWidth = analysisBoardWidth !== null && analysisBoardWidth !== void 0 ? analysisBoardWidth : analysisLayout.boardWidth;
1018
1034
  const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
1019
1035
  renderAnalysisContainer &&
1020
1036
  (renderEngineEvaluation || (engine === null || engine === void 0 ? void 0 : engine.enabled) === false));
1021
- return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, boardTheme: boardTheme, children: analysisSnapshot ? (jsxRuntime.jsx(reactChessCore.AnalysisErrorBoundary, { onClose: analysis.closeAnalysis, children: useHostAnalysisUi ? (jsxRuntime.jsx(reactChessCore.AnalysisBoardCore, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, boardWidth: analysisLayout.boardWidth, engine: engine, renderMain: renderAnalysisMain !== null && renderAnalysisMain !== void 0 ? renderAnalysisMain : (({ board, sidebar, model }) => (jsxRuntime.jsx(reactChessCore.AnalysisBoardLayout, { layout: analysisLayout, model: model, board: board, sidebar: sidebar }))), renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation !== null && renderEngineEvaluation !== void 0 ? renderEngineEvaluation : (() => null) })) : (jsxRuntime.jsx(reactChessCore.AnalysisBoard, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, layout: analysisLayout, engine: engine, renderMain: renderAnalysisMain, renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation })) })) : (jsxRuntime.jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsxRuntime.jsxs("div", { style: puzzleBoardColumnStyle(puzzleBoardWidth, controlsPlacement), children: [jsxRuntime.jsx("div", { style: puzzleBoardSlotWrapperStyle(), children: jsxRuntime.jsx("div", { style: puzzleBoardSlotStyle(), children: jsxRuntime.jsx(PuzzlePlaySurface, { position: position, boardWidth: puzzleBoardWidth, onFeedback: handleFeedback, incInteractionNum: incInteractionNum, onResumeCorrect: runResumeAutoAdvance, revealAnswerOnIncorrect: revealAnswerOnIncorrect, showAnswerArrowOnIncorrect: showAnswerArrowOnIncorrect, allowRetryOnIncorrect: allowRetryOnIncorrect, showRefutationOnIncorrect: refutationOnIncorrect, autoShowWrongMoves: autoShowWrongMoves, refutationEngine: refutationEngine !== null && refutationEngine !== void 0 ? refutationEngine : engine, answerArrowColor: answerArrowColor, positionLocked: loadingNextPuzzle, onMissFeedbackChange: setMissFeedback }) }) }), renderBoardCaption && (jsxRuntime.jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
1037
+ return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, boardTheme: boardTheme, children: analysisSnapshot ? (jsxRuntime.jsx(reactChessCore.AnalysisErrorBoundary, { onClose: analysis.closeAnalysis, children: useHostAnalysisUi ? (jsxRuntime.jsx(reactChessCore.AnalysisBoardCore, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, boardWidth: resolvedAnalysisBoardWidth, engine: engine, renderMain: renderAnalysisMain !== null && renderAnalysisMain !== void 0 ? renderAnalysisMain : (({ board, sidebar, model }) => (jsxRuntime.jsx(reactChessCore.AnalysisBoardLayout, { layout: analysisLayout, model: model, board: board, sidebar: sidebar }))), renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation !== null && renderEngineEvaluation !== void 0 ? renderEngineEvaluation : (() => null) })) : (jsxRuntime.jsx(reactChessCore.AnalysisBoard, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, layout: analysisLayout, engine: engine, renderMain: renderAnalysisMain, renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation })) })) : (jsxRuntime.jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsxRuntime.jsxs("div", { style: puzzleBoardColumnStyle(puzzleBoardWidth, controlsPlacement), children: [jsxRuntime.jsx("div", { style: puzzleBoardSlotWrapperStyle(), children: jsxRuntime.jsx("div", { style: puzzleBoardSlotStyle(), children: jsxRuntime.jsx(PuzzlePlaySurface, { position: position, boardWidth: puzzleBoardWidth, onFeedback: handleFeedback, incInteractionNum: incInteractionNum, onResumeCorrect: runResumeAutoAdvance, revealAnswerOnIncorrect: revealAnswerOnIncorrect, showAnswerArrowOnIncorrect: showAnswerArrowOnIncorrect, allowRetryOnIncorrect: allowRetryOnIncorrect, showRefutationOnIncorrect: refutationOnIncorrect, autoShowWrongMoves: autoShowWrongMoves, refutationEngine: refutationEngine !== null && refutationEngine !== void 0 ? refutationEngine : engine, answerArrowColor: answerArrowColor, positionLocked: loadingNextPuzzle, onMissFeedbackChange: setMissFeedback }) }) }), renderBoardCaption && (jsxRuntime.jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
1022
1038
  sideToMove: (_a = position === null || position === void 0 ? void 0 : position.getSideToMove()) !== null && _a !== void 0 ? _a : null,
1023
1039
  playerColor: position
1024
1040
  ? position.getPlayerColor()
@@ -1030,6 +1046,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1030
1046
  missPhase: (_c = missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.phase) !== null && _c !== void 0 ? _c : null,
1031
1047
  answerArrowVisible: (_d = missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.answerArrowVisible) !== null && _d !== void 0 ? _d : false,
1032
1048
  completedAfterMiss,
1049
+ hintUsed,
1033
1050
  }) }))] }), jsxRuntime.jsxs("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: [renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
1034
1051
  visible: analysis.canOpen,
1035
1052
  openAnalysis: analysis.openAnalysis,
@@ -1038,6 +1055,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1038
1055
  cleanSolve: !hasIncorrectAttempt,
1039
1056
  refutationSan: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan,
1040
1057
  missPhase: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.phase,
1058
+ hintUsed,
1041
1059
  }) }))] })] })) }));
1042
1060
  };
1043
1061
 
@@ -1046,7 +1064,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1046
1064
  * side's pieces be dragged. Move validation and sequencing live in
1047
1065
  * {@link LineBoardWithControls}.
1048
1066
  */
1049
- const LineBoard = ({ fen, orientation, trainSide, draggable, onPieceDrop, boardWidth, }) => (jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, position: fen, boardOrientation: orientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => piece[0] === trainSide, onPieceDrop: (source, target, piece) => onPieceDrop(source, target, piece), autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }) }));
1067
+ const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, lastMoveUci = null, onPieceDrop, boardWidth, }) => (jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, correctMoveSquare: correctMoveSquare, position: fen, boardOrientation: orientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => piece[0] === trainSide, onPieceDrop: (source, target, piece) => onPieceDrop(source, target, piece), lastMoveUci: lastMoveUci, autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }) }));
1050
1068
 
1051
1069
  /** Library default line-drill status controls (unstyled). */
1052
1070
  const DefaultLineControls = ({ moveNumber, total, finished, isUserTurn, feedback, }) => (jsxRuntime.jsxs("div", { style: rowStyle, children: [jsxRuntime.jsx("span", { style: statusStyle, children: finished ? 'Line complete' : `Move ${moveNumber} of ${total}` }), feedback && !finished && (jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, statusStyle), { color: feedback.isCorrect ? '#2e7d32' : '#c62828' }), children: feedback.isCorrect
@@ -1093,6 +1111,8 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1093
1111
  const [currentIndex, setCurrentIndex] = react.useState(0);
1094
1112
  const [finished, setFinished] = react.useState(false);
1095
1113
  const [feedback, setFeedback] = react.useState(null);
1114
+ const [displayFen, setDisplayFen] = react.useState(null);
1115
+ const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = reactChessCore.useCorrectMoveFeedback();
1096
1116
  const total = line.movesUci.length;
1097
1117
  const orientation = boardOrientationForLine(line.trainSide);
1098
1118
  const applyMove = react.useCallback((index) => {
@@ -1117,7 +1137,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1117
1137
  }, [line.movesUci]);
1118
1138
  // Auto-play opponent moves and detect the end of the line.
1119
1139
  react.useEffect(() => {
1120
- if (finished) {
1140
+ if (finished || isShowingCorrectMove) {
1121
1141
  return;
1122
1142
  }
1123
1143
  if (currentIndex >= total) {
@@ -1136,6 +1156,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1136
1156
  line.trainSide,
1137
1157
  applyMove,
1138
1158
  opponentMoveDelayMs,
1159
+ isShowingCorrectMove,
1139
1160
  ]);
1140
1161
  // Emit the completion event exactly once.
1141
1162
  react.useEffect(() => {
@@ -1147,15 +1168,16 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1147
1168
  }, [finished]);
1148
1169
  const handleDrop = (source, target, piece) => {
1149
1170
  var _a, _b, _c;
1150
- if (finished) {
1171
+ if (finished || isShowingCorrectMove) {
1151
1172
  return false;
1152
1173
  }
1153
- if (turnFromFen(chessRef.current.fen()) !== line.trainSide) {
1174
+ const setupFen = displayFen !== null && displayFen !== void 0 ? displayFen : chessRef.current.fen();
1175
+ if (turnFromFen(setupFen) !== line.trainSide) {
1154
1176
  return false;
1155
1177
  }
1156
1178
  const index = currentIndex;
1157
1179
  const expected = line.movesUci[index];
1158
- const dropResult = reactChessCore.evaluateExpectedMoveDrop(chessRef.current.fen(), source, target, piece, expected, true);
1180
+ const dropResult = reactChessCore.evaluateExpectedMoveDrop(setupFen, source, target, piece, expected, true);
1159
1181
  if (dropResult.kind === 'illegal' || dropResult.kind === 'ignored') {
1160
1182
  return false;
1161
1183
  }
@@ -1165,16 +1187,38 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1165
1187
  const moveFeedback = { index, isCorrect, expectedSan };
1166
1188
  setFeedback(moveFeedback);
1167
1189
  (_c = onMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(onMoveRef, moveFeedback);
1168
- applyMove(index);
1190
+ if (isCorrect) {
1191
+ const nextFen = reactChessCore.fenAfterUci(setupFen, dropResult.uci);
1192
+ if (nextFen) {
1193
+ setDisplayFen(nextFen);
1194
+ }
1195
+ showCorrectMove(target, () => {
1196
+ setDisplayFen(null);
1197
+ setFeedback(null);
1198
+ clearCorrectMoveFeedback();
1199
+ applyMove(index);
1200
+ });
1201
+ }
1169
1202
  return isCorrect;
1170
1203
  };
1204
+ const boardFen = displayFen !== null && displayFen !== void 0 ? displayFen : fen;
1205
+ const lastMoveUci = react.useMemo(() => {
1206
+ var _a;
1207
+ if (displayFen) {
1208
+ return (_a = line.movesUci[currentIndex]) !== null && _a !== void 0 ? _a : null;
1209
+ }
1210
+ return reactChessCore.lastMoveUciAtPly(line.movesUci, currentIndex);
1211
+ }, [currentIndex, displayFen, line.movesUci]);
1171
1212
  const moveNumber = Math.min(currentIndex + 1, total);
1172
- const isUserTurn = !finished && turnFromFen(fen) === line.trainSide && currentIndex < total;
1213
+ const isUserTurn = !finished &&
1214
+ !isShowingCorrectMove &&
1215
+ turnFromFen(boardFen) === line.trainSide &&
1216
+ currentIndex < total;
1173
1217
  const stackControlsBelow = useStackPuzzleControlsBelow();
1174
1218
  const controlsPlacement = stackControlsBelow
1175
1219
  ? 'below'
1176
1220
  : 'beside';
1177
- return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, boardTheme: boardTheme, children: jsxRuntime.jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsxRuntime.jsx("div", { style: puzzleBoardColumnStyle(boardWidth, controlsPlacement), children: jsxRuntime.jsx("div", { style: puzzleBoardSlotStyle(), children: jsxRuntime.jsx(LineBoard, { fen: fen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsxRuntime.jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
1221
+ return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, boardTheme: boardTheme, children: jsxRuntime.jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsxRuntime.jsx("div", { style: puzzleBoardColumnStyle(boardWidth, controlsPlacement), children: jsxRuntime.jsx("div", { style: puzzleBoardSlotStyle(), children: jsxRuntime.jsx(LineBoard, { fen: boardFen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, correctMoveSquare: correctMoveSquare, lastMoveUci: lastMoveUci, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsxRuntime.jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
1178
1222
  trainSide: line.trainSide,
1179
1223
  moveNumber,
1180
1224
  total,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-chess-puzzle-kit",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "React chess puzzle kit: play, controls, analysis, and browser Stockfish for endchess.training and other apps",
5
5
  "license": "MIT",
6
6
  "author": "Robert Blackwell",
@@ -76,6 +76,7 @@
76
76
  "react": "^18.3.1",
77
77
  "react-chess-core": "^0.1.1",
78
78
  "react-chessboard": "^4.7.1",
79
+ "react-dom": "^18.3.1",
79
80
  "storybook": "^8.2.9",
80
81
  "ts-jest": "^29.2.4",
81
82
  "vite": "^5.4.11",