react-chess-puzzle-kit 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ export interface LineBoardProps {
4
4
  orientation: 'white' | 'black';
5
5
  trainSide: LineTrainSide;
6
6
  draggable: boolean;
7
+ correctMoveSquare?: string | null;
7
8
  onPieceDrop: (source: string, target: string, piece: string) => boolean;
8
9
  boardWidth: number;
9
10
  }
@@ -12,4 +13,4 @@ export interface LineBoardProps {
12
13
  * side's pieces be dragged. Move validation and sequencing live in
13
14
  * {@link LineBoardWithControls}.
14
15
  */
15
- export declare const LineBoard: ({ fen, orientation, trainSide, draggable, onPieceDrop, boardWidth, }: LineBoardProps) => import("react").JSX.Element;
16
+ export declare const LineBoard: ({ fen, orientation, trainSide, draggable, correctMoveSquare, 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, 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
 
@@ -65,7 +65,9 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
65
65
  var _a, _b, _c, _d, _e, _f, _g;
66
66
  const [showAnswerArrow, setShowAnswerArrow] = useState(false);
67
67
  const [incorrectActive, setIncorrectActive] = useState(false);
68
+ const attemptMissedRef = useRef(false);
68
69
  const { revision, bumpRevision } = useBoardRevision();
70
+ const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, } = useCorrectMoveFeedback();
69
71
  const boardOrientationRef = useRef('white');
70
72
  const boardFenRef = useRef(EMPTY_BOARD_FEN);
71
73
  const notifyHost = () => {
@@ -91,7 +93,9 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
91
93
  expectedUci: expectedUci || null,
92
94
  positionFen,
93
95
  answerArrowColor,
94
- 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,
95
99
  engineOptions: refutationEngine,
96
100
  });
97
101
  const missPhase = (_c = missBoard.missSequence.sequence) === null || _c === void 0 ? void 0 : _c.phase;
@@ -101,8 +105,10 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
101
105
  useEffect(() => {
102
106
  setShowAnswerArrow(false);
103
107
  setIncorrectActive(false);
108
+ attemptMissedRef.current = false;
109
+ clearCorrectMoveFeedback();
104
110
  onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
105
- }, [onMissFeedbackChange, position]);
111
+ }, [clearCorrectMoveFeedback, onMissFeedbackChange, position]);
106
112
  useEffect(() => {
107
113
  var _a, _b;
108
114
  if (!onMissFeedbackChange) {
@@ -160,7 +166,10 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
160
166
  (missBoard.boardAnimating ||
161
167
  missPhase === 'wrong' ||
162
168
  missPhase === 'refutation');
163
- const arePiecesDraggable = position !== null && !positionLocked && !missLocked;
169
+ const arePiecesDraggable = position !== null &&
170
+ !positionLocked &&
171
+ !missLocked &&
172
+ correctMoveSquare === null;
164
173
  const onPieceDrop = (sourceSquare, targetSquare, piece) => {
165
174
  if (!position || positionLocked || position.isSolutionRevealed()) {
166
175
  return false;
@@ -182,6 +191,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
182
191
  recordIfIncorrect: !(answerArrowVisible && !allowRetryOnIncorrect),
183
192
  });
184
193
  if (!guess.accepted) {
194
+ attemptMissedRef.current = true;
185
195
  onFeedback({
186
196
  index: position.getIndex(),
187
197
  guess: { sourceSquare, targetSquare, piece },
@@ -224,38 +234,47 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
224
234
  setIncorrectActive(false);
225
235
  missBoard.missSequence.clearSequence();
226
236
  onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
227
- onFeedback({
237
+ clearCorrectMoveFeedback();
238
+ const assistedByAnswerArrow = answerArrowVisible && attemptMissedRef.current;
239
+ const guessPayload = {
228
240
  index: position.getIndex(),
229
241
  guess: { sourceSquare, targetSquare, piece },
230
- isCorrect: true,
231
- isFinished: guess.finished,
232
- });
233
- notifyHost();
234
- setTimeout(() => {
235
- position.resetInteractions();
236
- notifyHost();
237
- }, 500);
238
- if (position.isAlternativeCheckmate()) {
239
- notifyHost();
240
- return true;
242
+ };
243
+ if (assistedByAnswerArrow) {
244
+ // Miss feedback for this ply is already saved; dragging along the answer
245
+ // arrow only continues the line — it must not count as a clean solve.
246
+ if (guess.finished) {
247
+ onFeedback(Object.assign(Object.assign({}, guessPayload), { isCorrect: false, isFinished: true }));
248
+ }
249
+ }
250
+ else {
251
+ onFeedback(Object.assign(Object.assign({}, guessPayload), { isCorrect: true, isFinished: guess.finished }));
241
252
  }
242
253
  position.next();
254
+ boardFenRef.current = position.fen();
243
255
  notifyHost();
244
- if (position.hasResumeConfig()) {
245
- onResumeCorrect === null || onResumeCorrect === void 0 ? void 0 : onResumeCorrect(position);
246
- return true;
247
- }
248
- setTimeout(() => {
256
+ const finishCorrectFeedback = () => {
257
+ position.resetInteractions();
258
+ notifyHost();
259
+ if (position.isAlternativeCheckmate()) {
260
+ return;
261
+ }
262
+ if (position.hasResumeConfig()) {
263
+ onResumeCorrect === null || onResumeCorrect === void 0 ? void 0 : onResumeCorrect(position);
264
+ return;
265
+ }
249
266
  if (!position.isFinished()) {
250
267
  position.next();
268
+ boardFenRef.current = position.fen();
251
269
  }
252
270
  notifyHost();
253
- }, 500);
271
+ };
272
+ showCorrectMove(targetSquare, finishCorrectFeedback);
254
273
  return true;
255
274
  };
256
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
257
276
  ? null
258
- : ((_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 }));
259
278
  };
260
279
 
261
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 }));
@@ -720,7 +739,7 @@ const puzzlePositionFromFetch = (fen, moves, resume) => {
720
739
  const AUTO_ADVANCE_ON_COMPLETE_DELAY_MS = 700;
721
740
  const SOLUTION_STEP_MS = 500;
722
741
  const RESUME_AUTO_STEP_MS = 500;
723
- 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, }) => {
724
743
  var _a, _b, _c, _d;
725
744
  const refutationOnIncorrect = showRefutationOnIncorrect !== null && showRefutationOnIncorrect !== void 0 ? showRefutationOnIncorrect : showAnswerArrowOnIncorrect;
726
745
  const stackControlsBelow = useStackPuzzleControlsBelow();
@@ -732,6 +751,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
732
751
  const [loadingNextPuzzle, setLoadingNextPuzzle] = useState(true);
733
752
  const [puzzleNum, setPuzzleNum] = useState(0);
734
753
  const [hasIncorrectAttempt, setHasIncorrectAttempt] = useState(false);
754
+ const [hintUsed, setHintUsed] = useState(false);
735
755
  const [puzzleComplete, setPuzzleComplete] = useState(false);
736
756
  const [completedAfterMiss, setCompletedAfterMiss] = useState(false);
737
757
  const [missFeedback, setMissFeedback] = useState(null);
@@ -757,6 +777,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
757
777
  let cancelled = false;
758
778
  setLoadingNextPuzzle(true);
759
779
  setHasIncorrectAttempt(false);
780
+ setHintUsed(false);
760
781
  setPuzzleComplete(false);
761
782
  setCompletedAfterMiss(false);
762
783
  setMissFeedback(null);
@@ -793,12 +814,18 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
793
814
  const incorrectThisFeedback = feedbackData.hintRequested ||
794
815
  feedbackData.solutionShown ||
795
816
  feedbackData.isCorrect === false;
817
+ if (feedbackData.hintRequested) {
818
+ setHintUsed(true);
819
+ }
796
820
  if (incorrectThisFeedback) {
797
821
  setHasIncorrectAttempt(true);
798
822
  }
799
823
  if (feedbackData.isFinished) {
800
824
  setPuzzleComplete(true);
801
- setCompletedAfterMiss(hasIncorrectAttempt || incorrectThisFeedback);
825
+ setCompletedAfterMiss((prev) => prev ||
826
+ hasIncorrectAttempt ||
827
+ incorrectThisFeedback ||
828
+ feedbackData.hintRequested === true);
802
829
  }
803
830
  onFeedback(feedbackData);
804
831
  };
@@ -1002,10 +1029,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1002
1029
  };
1003
1030
  const analysis = usePuzzleAnalysis(position, resultStatus, puzzleNum);
1004
1031
  const analysisSnapshot = analysis.isOpen && analysis.snapshot ? analysis.snapshot : null;
1032
+ const resolvedAnalysisBoardWidth = analysisBoardWidth !== null && analysisBoardWidth !== void 0 ? analysisBoardWidth : analysisLayout.boardWidth;
1005
1033
  const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
1006
1034
  renderAnalysisContainer &&
1007
1035
  (renderEngineEvaluation || (engine === null || engine === void 0 ? void 0 : engine.enabled) === false));
1008
- 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({
1009
1037
  sideToMove: (_a = position === null || position === void 0 ? void 0 : position.getSideToMove()) !== null && _a !== void 0 ? _a : null,
1010
1038
  playerColor: position
1011
1039
  ? position.getPlayerColor()
@@ -1017,6 +1045,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1017
1045
  missPhase: (_c = missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.phase) !== null && _c !== void 0 ? _c : null,
1018
1046
  answerArrowVisible: (_d = missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.answerArrowVisible) !== null && _d !== void 0 ? _d : false,
1019
1047
  completedAfterMiss,
1048
+ hintUsed,
1020
1049
  }) }))] }), jsxs("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: [renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
1021
1050
  visible: analysis.canOpen,
1022
1051
  openAnalysis: analysis.openAnalysis,
@@ -1025,6 +1054,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1025
1054
  cleanSolve: !hasIncorrectAttempt,
1026
1055
  refutationSan: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan,
1027
1056
  missPhase: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.phase,
1057
+ hintUsed,
1028
1058
  }) }))] })] })) }));
1029
1059
  };
1030
1060
 
@@ -1033,7 +1063,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1033
1063
  * side's pieces be dragged. Move validation and sequencing live in
1034
1064
  * {@link LineBoardWithControls}.
1035
1065
  */
1036
- 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, 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), autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }) }));
1037
1067
 
1038
1068
  /** Library default line-drill status controls (unstyled). */
1039
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
@@ -1080,6 +1110,8 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1080
1110
  const [currentIndex, setCurrentIndex] = useState(0);
1081
1111
  const [finished, setFinished] = useState(false);
1082
1112
  const [feedback, setFeedback] = useState(null);
1113
+ const [displayFen, setDisplayFen] = useState(null);
1114
+ const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = useCorrectMoveFeedback();
1083
1115
  const total = line.movesUci.length;
1084
1116
  const orientation = boardOrientationForLine(line.trainSide);
1085
1117
  const applyMove = useCallback((index) => {
@@ -1104,7 +1136,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1104
1136
  }, [line.movesUci]);
1105
1137
  // Auto-play opponent moves and detect the end of the line.
1106
1138
  useEffect(() => {
1107
- if (finished) {
1139
+ if (finished || isShowingCorrectMove) {
1108
1140
  return;
1109
1141
  }
1110
1142
  if (currentIndex >= total) {
@@ -1123,6 +1155,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1123
1155
  line.trainSide,
1124
1156
  applyMove,
1125
1157
  opponentMoveDelayMs,
1158
+ isShowingCorrectMove,
1126
1159
  ]);
1127
1160
  // Emit the completion event exactly once.
1128
1161
  useEffect(() => {
@@ -1134,15 +1167,16 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1134
1167
  }, [finished]);
1135
1168
  const handleDrop = (source, target, piece) => {
1136
1169
  var _a, _b, _c;
1137
- if (finished) {
1170
+ if (finished || isShowingCorrectMove) {
1138
1171
  return false;
1139
1172
  }
1140
- 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) {
1141
1175
  return false;
1142
1176
  }
1143
1177
  const index = currentIndex;
1144
1178
  const expected = line.movesUci[index];
1145
- const dropResult = evaluateExpectedMoveDrop(chessRef.current.fen(), source, target, piece, expected, true);
1179
+ const dropResult = evaluateExpectedMoveDrop(setupFen, source, target, piece, expected, true);
1146
1180
  if (dropResult.kind === 'illegal' || dropResult.kind === 'ignored') {
1147
1181
  return false;
1148
1182
  }
@@ -1152,16 +1186,31 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1152
1186
  const moveFeedback = { index, isCorrect, expectedSan };
1153
1187
  setFeedback(moveFeedback);
1154
1188
  (_c = onMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(onMoveRef, moveFeedback);
1155
- 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
+ }
1156
1201
  return isCorrect;
1157
1202
  };
1203
+ const boardFen = displayFen !== null && displayFen !== void 0 ? displayFen : fen;
1158
1204
  const moveNumber = Math.min(currentIndex + 1, total);
1159
- const isUserTurn = !finished && turnFromFen(fen) === line.trainSide && currentIndex < total;
1205
+ const isUserTurn = !finished &&
1206
+ !isShowingCorrectMove &&
1207
+ turnFromFen(boardFen) === line.trainSide &&
1208
+ currentIndex < total;
1160
1209
  const stackControlsBelow = useStackPuzzleControlsBelow();
1161
1210
  const controlsPlacement = stackControlsBelow
1162
1211
  ? 'below'
1163
1212
  : 'beside';
1164
- 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({
1213
+ 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, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
1165
1214
  trainSide: line.trainSide,
1166
1215
  moveNumber,
1167
1216
  total,