react-chess-puzzle-kit 1.0.1 → 1.0.2

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.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2022-2026 Robert Blackwell
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2022-2026 Robert Blackwell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,331 +1,331 @@
1
- # react-chess-puzzle-kit
2
-
3
- ```bash
4
- npm install
5
- npm run build
6
- ```
7
-
8
- Build output: `dist/` (`index.js`, `index.esm.js`, `index.d.ts`). Re-run after source changes when consuming this package via `file:../react-chess-puzzle-kit`.
9
-
10
- React components for **interactive chess puzzles** and **post-puzzle analysis**, built on [react-chessboard](https://github.com/Clariity/react-chessboard) and [chess.js](https://github.com/jhlywa/chess.js). Used in production at [endchess.com](https://endchess.com).
11
-
12
- The library owns puzzle logic, move history, themes, and optional Stockfish analysis. **Defaults are included** for a full demo (controls + analysis UI); production apps usually replace those with custom render props.
13
-
14
- ---
15
-
16
- ## Install
17
-
18
- ```bash
19
- npm install react-chess-puzzle-kit
20
- ```
21
-
22
- **Peer dependencies:** `react`, `react-chessboard`, `chess.js`, **`react-chess-core`** (required — see `package.json`).
23
-
24
- Install both packages in your app:
25
-
26
- ```bash
27
- npm install react-chess-puzzle-kit react-chess-core
28
- ```
29
-
30
- Local development: `file:../react-chess-core` and `file:../react-chess-puzzle-kit` — build core first (`npm run build` in that repo), then puzzle-kit, then `npm install` in the app.
31
-
32
- **Board theme, `HighlightChessboard`, Stockfish hooks, and eval formatters** are exported from **`react-chess-core`**, not re-exported from puzzle-kit. Puzzle-kit covers puzzle play, analysis orchestration, and optional default analysis UI.
33
-
34
- For engine analysis in the browser, also install Stockfish and copy WASM into your app’s static folder (see [Stockfish](#stockfish-browser-engine) below).
35
-
36
- ---
37
-
38
- ## See it in action (Storybook)
39
-
40
- Storybook is the living docs and playground for every export.
41
-
42
- ```bash
43
- git clone https://github.com/reblackwell3/react-chess-puzzle-kit.git
44
- cd react-chess-puzzle-kit
45
- npm install
46
- npm run storybook # http://localhost:6006
47
- ```
48
-
49
- | Story | What it demonstrates |
50
- |-------|----------------------|
51
- | **PuzzleBoardWithControls** | Full puzzle flow with default controls (via decorator) |
52
- | **PuzzleBoardWithControls → All library defaults** | Default controls + default analysis modal, sidebar, engine panel |
53
- | **PuzzleBoard** | Puzzle board only (drag-and-drop, feedback highlights) |
54
- | **Analysis / AnalysisBoard (defaults)** | Default modal, sidebar, grid, and engine panel |
55
- | **HighlightChessboard** | Themed board with check / hint / incorrect square styles |
56
-
57
- Optional: `npm run copy:stockfish` then open **Analysis → WithStockfishEngine** to exercise the worker.
58
-
59
- Build a static catalog: `npm run build-storybook` → `npm run serve-storybook`.
60
-
61
- ---
62
-
63
- ## Quick start
64
-
65
- `PuzzleBoardWithControls` is the main integration point: puzzle play, then analysis when the puzzle is finished or failed.
66
-
67
- ### All library defaults
68
-
69
- Omit every render prop to use built-in puzzle controls and analysis UI. This matches **PuzzleBoardWithControls → All library defaults** in Storybook.
70
-
71
- ```tsx
72
- import { PuzzleBoardWithControls } from 'react-chess-puzzle-kit';
73
-
74
- export function PuzzleDemo() {
75
- return (
76
- <PuzzleBoardWithControls
77
- theme="dark"
78
- apiProxy={{
79
- onFetch: () => fetchPuzzleFromApi(),
80
- onFeedback: (data) => sendFeedbackToApi(data),
81
- }}
82
- engine={{ enabled: false }}
83
- />
84
- );
85
- }
86
- ```
87
-
88
- Defaults used when props are omitted:
89
-
90
- | Prop | Default |
91
- |------|---------|
92
- | `renderControls` | `DefaultPuzzleControls` (hint, next, analysis, result labels) |
93
- | `renderAnalysis*` | `AnalysisBoard` modal, sidebar, and `EngineEvaluationPanel` |
94
- | `puzzleBoardWidth` | `DEFAULT_PUZZLE_BOARD_WIDTH` (480) |
95
- | `analysisLayout` | `DEFAULT_ANALYSIS_LAYOUT` |
96
-
97
- Reuse the controls alone:
98
-
99
- ```tsx
100
- import {
101
- DefaultPuzzleControls,
102
- defaultRenderControls,
103
- } from 'react-chess-puzzle-kit';
104
-
105
- // Inside your own layout:
106
- <DefaultPuzzleControls
107
- showHint={showHint}
108
- nextPuzzle={nextPuzzle}
109
- resultStatus={resultStatus}
110
- analysis={analysis}
111
- />;
112
- ```
113
-
114
- In Storybook, the `withDefaultPuzzleControls` decorator does the same for stories that leave `renderControls` unset (see `src/stories/withDefaultPuzzleControls.tsx`).
115
-
116
- ### Custom UI (production)
117
-
118
- ```tsx
119
- import {
120
- PuzzleBoardWithControls,
121
- DEFAULT_ANALYSIS_LAYOUT,
122
- } from 'react-chess-puzzle-kit';
123
-
124
- export function PuzzlePage() {
125
- return (
126
- <PuzzleBoardWithControls
127
- theme="dark"
128
- puzzleBoardWidth={560}
129
- analysisLayout={DEFAULT_ANALYSIS_LAYOUT}
130
- apiProxy={{
131
- onFetch: () => fetchPuzzleFromApi(),
132
- onFeedback: (data) => sendFeedbackToApi(data),
133
- }}
134
- renderControls={(showHint, nextPuzzle, resultStatus, analysis) => (
135
- <YourControls
136
- onHint={showHint}
137
- onNext={nextPuzzle}
138
- onOpenAnalysis={analysis.openAnalysis}
139
- canOpenAnalysis={analysis.visible}
140
- />
141
- )}
142
- renderAnalysisContainer={(props) => <YourAnalysisDialog {...props} />}
143
- renderAnalysisSidebar={(props) => <YourMoveList {...props} />}
144
- renderEngineEvaluation={(props) => <YourEnginePanel {...props} />}
145
- engine={{ depth: 16, multiPv: 2 }}
146
- />
147
- );
148
- }
149
- ```
150
-
151
- ---
152
-
153
- ## What’s in the package
154
-
155
- ### Puzzle play
156
-
157
- | Export | Role |
158
- |--------|------|
159
- | **`PuzzleBoardWithControls`** | Orchestrates fetch, puzzle board, controls slot, and analysis mode |
160
- | **`DefaultPuzzleControls`** | Default hint / next / analysis button row |
161
- | **`defaultRenderControls`** | Render-prop function wired to `DefaultPuzzleControls` |
162
- | **`PuzzleBoard`** | Draggable puzzle board with correct/incorrect/hint feedback |
163
- | **`PuzzlePosition`** | FEN + solution line, move index, guess judging |
164
- | **`analysisSidebarColors`** | Default analysis move-list striping (override in your app) |
165
-
166
- **From `react-chess-core` (install separately):** `HighlightChessboard`, `ThemeProvider`, `boardSquareHighlightColors`, `useAnalysisEngine`, `formatEvaluation`, `DEFAULT_STOCKFISH_SCRIPT_URL`, etc.
167
-
168
- ### Analysis
169
-
170
- Analysis is split so apps can use **presets** or go **fully headless**:
171
-
172
- | Layer | Location | Use when |
173
- |-------|----------|----------|
174
- | **Core** | `src/features/analysis/core/` | You want full control of layout and UI |
175
- | **Defaults** | `src/features/analysis/defaults/` | You want a working modal + sidebar out of the box |
176
-
177
- | Export | Role |
178
- |--------|------|
179
- | **`usePuzzleAnalysis`** | When analysis can open; snapshot of puzzle state |
180
- | **`useAnalysisBoardModel`** | FEN, ply navigation, history, engine hook, drop handler (no JSX) |
181
- | **`AnalysisBoardCore`** | Wires model + board; **requires** all `render*` props |
182
- | **`AnalysisBoard`** | Same as core but fills missing render props with library defaults |
183
- | **`AnalysisBoardLayout`** | Optional CSS grid: board column + sidebar column |
184
- | **`AnalysisLayoutConfig`** | `{ boardWidth, sidebarWidth, columnGap }` |
185
-
186
- ### Engine
187
-
188
- | Export | Package | Role |
189
- |--------|---------|------|
190
- | **`useAnalysisEngine`**, eval helpers | `react-chess-core` | Stockfish hook + PV formatting |
191
- | **`EngineEvaluationPanel`** | puzzle-kit | Default engine UI (`AnalysisBoard` preset) |
192
-
193
- ---
194
-
195
- ## Architecture
196
-
197
- ```
198
- ┌─────────────────────────────────────────────────────────────┐
199
- │ PuzzleBoardWithControls (host app) │
200
- │ ├─ puzzle play → PuzzleBoard (puzzleBoardWidth) │
201
- │ └─ analysis open → AnalysisBoardCore | AnalysisBoard │
202
- │ ├─ renderContainer (modal / page shell) │
203
- │ ├─ renderMain (board + sidebar placement) │
204
- │ ├─ renderSidebar (move list, nav) │
205
- │ └─ renderEngineEvaluation (Stockfish lines) │
206
- └─────────────────────────────────────────────────────────────┘
207
- ```
208
-
209
- **Separation of concerns**
210
-
211
- - **Logic** — puzzle position, analysis context, ply navigation (`analysis/core/`); engine hook from **`react-chess-core`**.
212
- - **UI** — host render props, or `defaults/` (`AnalysisBoard`, `DefaultAnalysisSidebar`, `DefaultAnalysisContainer`, `EngineEvaluationPanel`).
213
-
214
- Production apps (e.g. endchess-frontend) typically pass custom `renderAnalysis*` implementations and sizes via `puzzleBoardWidth` + `analysisLayout`, while reusing `useAnalysisBoardModel` behavior through `AnalysisBoardCore`.
215
-
216
- ---
217
-
218
- ## Board sizing
219
-
220
- Puzzle and analysis boards can use **different pixel widths**:
221
-
222
- ```tsx
223
- <PuzzleBoardWithControls
224
- puzzleBoardWidth={560} // live puzzle
225
- analysisLayout={{
226
- boardWidth: 480, // analysis chessboard
227
- sidebarWidth: 500,
228
- columnGap: 16,
229
- }}
230
- />
231
- ```
232
-
233
- Defaults: `DEFAULT_PUZZLE_BOARD_WIDTH` (480) and `DEFAULT_ANALYSIS_LAYOUT`. Override `renderAnalysisMain` for a fully custom layout (flex, MUI grid, etc.).
234
-
235
- ---
236
-
237
- ## Stockfish (browser engine)
238
-
239
- Analysis runs Stockfish in a **Web Worker**. The host must serve the engine assets:
240
-
241
- ```bash
242
- npm install stockfish --save-optional # or a regular dependency
243
- npm run copy:stockfish # copies to public/stockfish/ in this repo
244
- ```
245
-
246
- In your app, copy the same files to `public/stockfish/` (or equivalent). Default script URL:
247
-
248
- `/stockfish/stockfish-18-lite-single.js` (`.wasm` must sit beside it).
249
-
250
- ```tsx
251
- <PuzzleBoardWithControls
252
- engine={{
253
- enabled: true,
254
- depth: 18,
255
- multiPv: 2,
256
- scriptUrl: '/stockfish/stockfish-18-lite-single.js',
257
- }}
258
- />
259
- ```
260
-
261
- Disable the worker when you only need move replay: `engine={{ enabled: false }}`.
262
-
263
- ---
264
-
265
- ## Headless analysis example
266
-
267
- Use `AnalysisBoardCore` when every visual piece comes from the host:
268
-
269
- ```tsx
270
- import {
271
- AnalysisBoardCore,
272
- AnalysisBoardLayout,
273
- DEFAULT_ANALYSIS_LAYOUT,
274
- } from 'react-chess-puzzle-kit';
275
-
276
- <AnalysisBoardCore
277
- analysisContext={snapshot}
278
- theme="dark"
279
- boardWidth={layout.boardWidth}
280
- onClose={close}
281
- renderContainer={(p) => <MyDialog {...p} />}
282
- renderMain={({ board, sidebar, model }) => (
283
- <AnalysisBoardLayout layout={layout} model={model} board={board} sidebar={sidebar} />
284
- )}
285
- renderSidebar={(p) => <MySidebar {...p} />}
286
- renderEngineEvaluation={(p) => <MyEngine {...p} />}
287
- />;
288
- ```
289
-
290
- ---
291
-
292
- ## Development
293
-
294
- ```bash
295
- npm run build # Rollup → dist/
296
- npm run storybook # component docs & examples
297
- npm run build-storybook # static Storybook export
298
- npm run copy:stockfish # engine binaries for local Storybook
299
- ```
300
-
301
- ---
302
-
303
- ## Migration: engine/board imports
304
-
305
- If you previously imported these from `react-chess-puzzle-kit`, switch to `react-chess-core`:
306
-
307
- ```diff
308
- - import { useAnalysisEngine, formatEvaluation, ThemeProvider } from 'react-chess-puzzle-kit';
309
- + import { useAnalysisEngine, formatEvaluation, ThemeProvider } from 'react-chess-core';
310
- ```
311
-
312
- Keep puzzle-specific imports on puzzle-kit (`PuzzleBoardWithControls`, `usePuzzleAnalysis`, `AnalysisBoardCore`, render-prop types, etc.).
313
-
314
- ---
315
-
316
- ## Migration from `react-chessboard-with-controls`
317
-
318
- The package was renamed to **`react-chess-puzzle-kit`** (v1.0.0). Update imports:
319
-
320
- ```diff
321
- - import { PuzzleBoardWithControls } from 'react-chessboard-with-controls';
322
- + import { PuzzleBoardWithControls } from 'react-chess-puzzle-kit';
323
- ```
324
-
325
- Component export names are unchanged for now. Rename the local clone folder to `react-chess-puzzle-kit` and point `file:../react-chess-puzzle-kit` in consuming apps.
326
-
327
- ---
328
-
329
- ## License
330
-
331
- MIT © 2022–2026 Robert Blackwell
1
+ # react-chess-puzzle-kit
2
+
3
+ ```bash
4
+ npm install
5
+ npm run build
6
+ ```
7
+
8
+ Build output: `dist/` (`index.js`, `index.esm.js`, `index.d.ts`). Re-run after source changes when consuming this package via `file:../react-chess-puzzle-kit`.
9
+
10
+ React components for **interactive chess puzzles** and **post-puzzle analysis**, built on [react-chessboard](https://github.com/Clariity/react-chessboard) and [chess.js](https://github.com/jhlywa/chess.js). Used in production at [endchess.com](https://endchess.com).
11
+
12
+ The library owns puzzle logic, move history, themes, and optional Stockfish analysis. **Defaults are included** for a full demo (controls + analysis UI); production apps usually replace those with custom render props.
13
+
14
+ ---
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install react-chess-puzzle-kit
20
+ ```
21
+
22
+ **Peer dependencies:** `react`, `react-chessboard`, `chess.js`, **`react-chess-core`** (required — see `package.json`).
23
+
24
+ Install both packages in your app:
25
+
26
+ ```bash
27
+ npm install react-chess-puzzle-kit react-chess-core
28
+ ```
29
+
30
+ Local development: `file:../react-chess-core` and `file:../react-chess-puzzle-kit` — build core first (`npm run build` in that repo), then puzzle-kit, then `npm install` in the app.
31
+
32
+ **Board theme, `HighlightChessboard`, Stockfish hooks, and eval formatters** are exported from **`react-chess-core`**, not re-exported from puzzle-kit. Puzzle-kit covers puzzle play, analysis orchestration, and optional default analysis UI.
33
+
34
+ For engine analysis in the browser, also install Stockfish and copy WASM into your app’s static folder (see [Stockfish](#stockfish-browser-engine) below).
35
+
36
+ ---
37
+
38
+ ## See it in action (Storybook)
39
+
40
+ Storybook is the living docs and playground for every export.
41
+
42
+ ```bash
43
+ git clone https://github.com/reblackwell3/react-chess-puzzle-kit.git
44
+ cd react-chess-puzzle-kit
45
+ npm install
46
+ npm run storybook # http://localhost:6006
47
+ ```
48
+
49
+ | Story | What it demonstrates |
50
+ |-------|----------------------|
51
+ | **PuzzleBoardWithControls** | Full puzzle flow with default controls (via decorator) |
52
+ | **PuzzleBoardWithControls → All library defaults** | Default controls + default analysis modal, sidebar, engine panel |
53
+ | **PuzzleBoard** | Puzzle board only (drag-and-drop, feedback highlights) |
54
+ | **Analysis / AnalysisBoard (defaults)** | Default modal, sidebar, grid, and engine panel |
55
+ | **HighlightChessboard** | Themed board with check / hint / incorrect square styles |
56
+
57
+ Optional: `npm run copy:stockfish` then open **Analysis → WithStockfishEngine** to exercise the worker.
58
+
59
+ Build a static catalog: `npm run build-storybook` → `npm run serve-storybook`.
60
+
61
+ ---
62
+
63
+ ## Quick start
64
+
65
+ `PuzzleBoardWithControls` is the main integration point: puzzle play, then analysis when the puzzle is finished or failed.
66
+
67
+ ### All library defaults
68
+
69
+ Omit every render prop to use built-in puzzle controls and analysis UI. This matches **PuzzleBoardWithControls → All library defaults** in Storybook.
70
+
71
+ ```tsx
72
+ import { PuzzleBoardWithControls } from 'react-chess-puzzle-kit';
73
+
74
+ export function PuzzleDemo() {
75
+ return (
76
+ <PuzzleBoardWithControls
77
+ theme="dark"
78
+ apiProxy={{
79
+ onFetch: () => fetchPuzzleFromApi(),
80
+ onFeedback: (data) => sendFeedbackToApi(data),
81
+ }}
82
+ engine={{ enabled: false }}
83
+ />
84
+ );
85
+ }
86
+ ```
87
+
88
+ Defaults used when props are omitted:
89
+
90
+ | Prop | Default |
91
+ |------|---------|
92
+ | `renderControls` | `DefaultPuzzleControls` (hint, next, analysis, result labels) |
93
+ | `renderAnalysis*` | `AnalysisBoard` modal, sidebar, and `EngineEvaluationPanel` |
94
+ | `puzzleBoardWidth` | `DEFAULT_PUZZLE_BOARD_WIDTH` (480) |
95
+ | `analysisLayout` | `DEFAULT_ANALYSIS_LAYOUT` |
96
+
97
+ Reuse the controls alone:
98
+
99
+ ```tsx
100
+ import {
101
+ DefaultPuzzleControls,
102
+ defaultRenderControls,
103
+ } from 'react-chess-puzzle-kit';
104
+
105
+ // Inside your own layout:
106
+ <DefaultPuzzleControls
107
+ showHint={showHint}
108
+ nextPuzzle={nextPuzzle}
109
+ resultStatus={resultStatus}
110
+ analysis={analysis}
111
+ />;
112
+ ```
113
+
114
+ In Storybook, the `withDefaultPuzzleControls` decorator does the same for stories that leave `renderControls` unset (see `src/stories/withDefaultPuzzleControls.tsx`).
115
+
116
+ ### Custom UI (production)
117
+
118
+ ```tsx
119
+ import {
120
+ PuzzleBoardWithControls,
121
+ DEFAULT_ANALYSIS_LAYOUT,
122
+ } from 'react-chess-puzzle-kit';
123
+
124
+ export function PuzzlePage() {
125
+ return (
126
+ <PuzzleBoardWithControls
127
+ theme="dark"
128
+ puzzleBoardWidth={560}
129
+ analysisLayout={DEFAULT_ANALYSIS_LAYOUT}
130
+ apiProxy={{
131
+ onFetch: () => fetchPuzzleFromApi(),
132
+ onFeedback: (data) => sendFeedbackToApi(data),
133
+ }}
134
+ renderControls={(showHint, nextPuzzle, resultStatus, analysis) => (
135
+ <YourControls
136
+ onHint={showHint}
137
+ onNext={nextPuzzle}
138
+ onOpenAnalysis={analysis.openAnalysis}
139
+ canOpenAnalysis={analysis.visible}
140
+ />
141
+ )}
142
+ renderAnalysisContainer={(props) => <YourAnalysisDialog {...props} />}
143
+ renderAnalysisSidebar={(props) => <YourMoveList {...props} />}
144
+ renderEngineEvaluation={(props) => <YourEnginePanel {...props} />}
145
+ engine={{ depth: 16, multiPv: 2 }}
146
+ />
147
+ );
148
+ }
149
+ ```
150
+
151
+ ---
152
+
153
+ ## What’s in the package
154
+
155
+ ### Puzzle play
156
+
157
+ | Export | Role |
158
+ |--------|------|
159
+ | **`PuzzleBoardWithControls`** | Orchestrates fetch, puzzle board, controls slot, and analysis mode |
160
+ | **`DefaultPuzzleControls`** | Default hint / next / analysis button row |
161
+ | **`defaultRenderControls`** | Render-prop function wired to `DefaultPuzzleControls` |
162
+ | **`PuzzleBoard`** | Draggable puzzle board with correct/incorrect/hint feedback |
163
+ | **`PuzzlePosition`** | FEN + solution line, move index, guess judging |
164
+ | **`analysisSidebarColors`** | Default analysis move-list striping (override in your app) |
165
+
166
+ **From `react-chess-core` (install separately):** `HighlightChessboard`, `ThemeProvider`, `boardSquareHighlightColors`, `useAnalysisEngine`, `formatEvaluation`, `DEFAULT_STOCKFISH_SCRIPT_URL`, etc.
167
+
168
+ ### Analysis
169
+
170
+ Analysis is split so apps can use **presets** or go **fully headless**:
171
+
172
+ | Layer | Location | Use when |
173
+ |-------|----------|----------|
174
+ | **Core** | `src/features/analysis/core/` | You want full control of layout and UI |
175
+ | **Defaults** | `src/features/analysis/defaults/` | You want a working modal + sidebar out of the box |
176
+
177
+ | Export | Role |
178
+ |--------|------|
179
+ | **`usePuzzleAnalysis`** | When analysis can open; snapshot of puzzle state |
180
+ | **`useAnalysisBoardModel`** | FEN, ply navigation, history, engine hook, drop handler (no JSX) |
181
+ | **`AnalysisBoardCore`** | Wires model + board; **requires** all `render*` props |
182
+ | **`AnalysisBoard`** | Same as core but fills missing render props with library defaults |
183
+ | **`AnalysisBoardLayout`** | Optional CSS grid: board column + sidebar column |
184
+ | **`AnalysisLayoutConfig`** | `{ boardWidth, sidebarWidth, columnGap }` |
185
+
186
+ ### Engine
187
+
188
+ | Export | Package | Role |
189
+ |--------|---------|------|
190
+ | **`useAnalysisEngine`**, eval helpers | `react-chess-core` | Stockfish hook + PV formatting |
191
+ | **`EngineEvaluationPanel`** | puzzle-kit | Default engine UI (`AnalysisBoard` preset) |
192
+
193
+ ---
194
+
195
+ ## Architecture
196
+
197
+ ```
198
+ ┌─────────────────────────────────────────────────────────────┐
199
+ │ PuzzleBoardWithControls (host app) │
200
+ │ ├─ puzzle play → PuzzleBoard (puzzleBoardWidth) │
201
+ │ └─ analysis open → AnalysisBoardCore | AnalysisBoard │
202
+ │ ├─ renderContainer (modal / page shell) │
203
+ │ ├─ renderMain (board + sidebar placement) │
204
+ │ ├─ renderSidebar (move list, nav) │
205
+ │ └─ renderEngineEvaluation (Stockfish lines) │
206
+ └─────────────────────────────────────────────────────────────┘
207
+ ```
208
+
209
+ **Separation of concerns**
210
+
211
+ - **Logic** — puzzle position, analysis context, ply navigation (`analysis/core/`); engine hook from **`react-chess-core`**.
212
+ - **UI** — host render props, or `defaults/` (`AnalysisBoard`, `DefaultAnalysisSidebar`, `DefaultAnalysisContainer`, `EngineEvaluationPanel`).
213
+
214
+ Production apps (e.g. endchess-frontend) typically pass custom `renderAnalysis*` implementations and sizes via `puzzleBoardWidth` + `analysisLayout`, while reusing `useAnalysisBoardModel` behavior through `AnalysisBoardCore`.
215
+
216
+ ---
217
+
218
+ ## Board sizing
219
+
220
+ Puzzle and analysis boards can use **different pixel widths**:
221
+
222
+ ```tsx
223
+ <PuzzleBoardWithControls
224
+ puzzleBoardWidth={560} // live puzzle
225
+ analysisLayout={{
226
+ boardWidth: 480, // analysis chessboard
227
+ sidebarWidth: 500,
228
+ columnGap: 16,
229
+ }}
230
+ />
231
+ ```
232
+
233
+ Defaults: `DEFAULT_PUZZLE_BOARD_WIDTH` (480) and `DEFAULT_ANALYSIS_LAYOUT`. Override `renderAnalysisMain` for a fully custom layout (flex, MUI grid, etc.).
234
+
235
+ ---
236
+
237
+ ## Stockfish (browser engine)
238
+
239
+ Analysis runs Stockfish in a **Web Worker**. The host must serve the engine assets:
240
+
241
+ ```bash
242
+ npm install stockfish --save-optional # or a regular dependency
243
+ npm run copy:stockfish # copies to public/stockfish/ in this repo
244
+ ```
245
+
246
+ In your app, copy the same files to `public/stockfish/` (or equivalent). Default script URL:
247
+
248
+ `/stockfish/stockfish-18-lite-single.js` (`.wasm` must sit beside it).
249
+
250
+ ```tsx
251
+ <PuzzleBoardWithControls
252
+ engine={{
253
+ enabled: true,
254
+ depth: 18,
255
+ multiPv: 2,
256
+ scriptUrl: '/stockfish/stockfish-18-lite-single.js',
257
+ }}
258
+ />
259
+ ```
260
+
261
+ Disable the worker when you only need move replay: `engine={{ enabled: false }}`.
262
+
263
+ ---
264
+
265
+ ## Headless analysis example
266
+
267
+ Use `AnalysisBoardCore` when every visual piece comes from the host:
268
+
269
+ ```tsx
270
+ import {
271
+ AnalysisBoardCore,
272
+ AnalysisBoardLayout,
273
+ DEFAULT_ANALYSIS_LAYOUT,
274
+ } from 'react-chess-puzzle-kit';
275
+
276
+ <AnalysisBoardCore
277
+ analysisContext={snapshot}
278
+ theme="dark"
279
+ boardWidth={layout.boardWidth}
280
+ onClose={close}
281
+ renderContainer={(p) => <MyDialog {...p} />}
282
+ renderMain={({ board, sidebar, model }) => (
283
+ <AnalysisBoardLayout layout={layout} model={model} board={board} sidebar={sidebar} />
284
+ )}
285
+ renderSidebar={(p) => <MySidebar {...p} />}
286
+ renderEngineEvaluation={(p) => <MyEngine {...p} />}
287
+ />;
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Development
293
+
294
+ ```bash
295
+ npm run build # Rollup → dist/
296
+ npm run storybook # component docs & examples
297
+ npm run build-storybook # static Storybook export
298
+ npm run copy:stockfish # engine binaries for local Storybook
299
+ ```
300
+
301
+ ---
302
+
303
+ ## Migration: engine/board imports
304
+
305
+ If you previously imported these from `react-chess-puzzle-kit`, switch to `react-chess-core`:
306
+
307
+ ```diff
308
+ - import { useAnalysisEngine, formatEvaluation, ThemeProvider } from 'react-chess-puzzle-kit';
309
+ + import { useAnalysisEngine, formatEvaluation, ThemeProvider } from 'react-chess-core';
310
+ ```
311
+
312
+ Keep puzzle-specific imports on puzzle-kit (`PuzzleBoardWithControls`, `usePuzzleAnalysis`, `AnalysisBoardCore`, render-prop types, etc.).
313
+
314
+ ---
315
+
316
+ ## Migration from `react-chessboard-with-controls`
317
+
318
+ The package was renamed to **`react-chess-puzzle-kit`** (v1.0.0). Update imports:
319
+
320
+ ```diff
321
+ - import { PuzzleBoardWithControls } from 'react-chessboard-with-controls';
322
+ + import { PuzzleBoardWithControls } from 'react-chess-puzzle-kit';
323
+ ```
324
+
325
+ Component export names are unchanged for now. Rename the local clone folder to `react-chess-puzzle-kit` and point `file:../react-chess-puzzle-kit` in consuming apps.
326
+
327
+ ---
328
+
329
+ ## License
330
+
331
+ MIT © 2022–2026 Robert Blackwell
package/dist/index.esm.js CHANGED
@@ -65,6 +65,7 @@ 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();
69
70
  const boardOrientationRef = useRef('white');
70
71
  const boardFenRef = useRef(EMPTY_BOARD_FEN);
@@ -101,6 +102,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
101
102
  useEffect(() => {
102
103
  setShowAnswerArrow(false);
103
104
  setIncorrectActive(false);
105
+ attemptMissedRef.current = false;
104
106
  onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
105
107
  }, [onMissFeedbackChange, position]);
106
108
  useEffect(() => {
@@ -182,6 +184,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
182
184
  recordIfIncorrect: !(answerArrowVisible && !allowRetryOnIncorrect),
183
185
  });
184
186
  if (!guess.accepted) {
187
+ attemptMissedRef.current = true;
185
188
  onFeedback({
186
189
  index: position.getIndex(),
187
190
  guess: { sourceSquare, targetSquare, piece },
@@ -224,12 +227,21 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
224
227
  setIncorrectActive(false);
225
228
  missBoard.missSequence.clearSequence();
226
229
  onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
227
- onFeedback({
230
+ const assistedByAnswerArrow = answerArrowVisible && attemptMissedRef.current;
231
+ const guessPayload = {
228
232
  index: position.getIndex(),
229
233
  guess: { sourceSquare, targetSquare, piece },
230
- isCorrect: true,
231
- isFinished: guess.finished,
232
- });
234
+ };
235
+ if (assistedByAnswerArrow) {
236
+ // Miss feedback for this ply is already saved; dragging along the answer
237
+ // arrow only continues the line — it must not count as a clean solve.
238
+ if (guess.finished) {
239
+ onFeedback(Object.assign(Object.assign({}, guessPayload), { isCorrect: false, isFinished: true }));
240
+ }
241
+ }
242
+ else {
243
+ onFeedback(Object.assign(Object.assign({}, guessPayload), { isCorrect: true, isFinished: guess.finished }));
244
+ }
233
245
  notifyHost();
234
246
  setTimeout(() => {
235
247
  position.resetInteractions();
package/dist/index.js CHANGED
@@ -66,6 +66,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
66
66
  var _a, _b, _c, _d, _e, _f, _g;
67
67
  const [showAnswerArrow, setShowAnswerArrow] = react.useState(false);
68
68
  const [incorrectActive, setIncorrectActive] = react.useState(false);
69
+ const attemptMissedRef = react.useRef(false);
69
70
  const { revision, bumpRevision } = reactChessCore.useBoardRevision();
70
71
  const boardOrientationRef = react.useRef('white');
71
72
  const boardFenRef = react.useRef(EMPTY_BOARD_FEN);
@@ -102,6 +103,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
102
103
  react.useEffect(() => {
103
104
  setShowAnswerArrow(false);
104
105
  setIncorrectActive(false);
106
+ attemptMissedRef.current = false;
105
107
  onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
106
108
  }, [onMissFeedbackChange, position]);
107
109
  react.useEffect(() => {
@@ -183,6 +185,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
183
185
  recordIfIncorrect: !(answerArrowVisible && !allowRetryOnIncorrect),
184
186
  });
185
187
  if (!guess.accepted) {
188
+ attemptMissedRef.current = true;
186
189
  onFeedback({
187
190
  index: position.getIndex(),
188
191
  guess: { sourceSquare, targetSquare, piece },
@@ -225,12 +228,21 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
225
228
  setIncorrectActive(false);
226
229
  missBoard.missSequence.clearSequence();
227
230
  onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
228
- onFeedback({
231
+ const assistedByAnswerArrow = answerArrowVisible && attemptMissedRef.current;
232
+ const guessPayload = {
229
233
  index: position.getIndex(),
230
234
  guess: { sourceSquare, targetSquare, piece },
231
- isCorrect: true,
232
- isFinished: guess.finished,
233
- });
235
+ };
236
+ if (assistedByAnswerArrow) {
237
+ // Miss feedback for this ply is already saved; dragging along the answer
238
+ // arrow only continues the line — it must not count as a clean solve.
239
+ if (guess.finished) {
240
+ onFeedback(Object.assign(Object.assign({}, guessPayload), { isCorrect: false, isFinished: true }));
241
+ }
242
+ }
243
+ else {
244
+ onFeedback(Object.assign(Object.assign({}, guessPayload), { isCorrect: true, isFinished: guess.finished }));
245
+ }
234
246
  notifyHost();
235
247
  setTimeout(() => {
236
248
  position.resetInteractions();
package/package.json CHANGED
@@ -1,87 +1,87 @@
1
- {
2
- "name": "react-chess-puzzle-kit",
3
- "version": "1.0.1",
4
- "description": "React chess puzzle kit: play, controls, analysis, and browser Stockfish for endchess.training and other apps",
5
- "license": "MIT",
6
- "author": "Robert Blackwell",
7
- "main": "dist/index.js",
8
- "module": "dist/index.esm.js",
9
- "types": "dist/index.d.ts",
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://github.com/reblackwell3/react-chess-puzzle-kit.git"
13
- },
14
- "bugs": {
15
- "url": "https://github.com/reblackwell3/react-chess-puzzle-kit/issues"
16
- },
17
- "homepage": "https://github.com/reblackwell3/react-chess-puzzle-kit#readme",
18
- "keywords": [
19
- "react",
20
- "chess",
21
- "chess.js",
22
- "chess puzzle",
23
- "puzzle",
24
- "analysis",
25
- "stockfish",
26
- "react-chessboard",
27
- "chessboard",
28
- "drag and drop"
29
- ],
30
- "files": [
31
- "dist"
32
- ],
33
- "scripts": {
34
- "build": "rollup -c",
35
- "copy:stockfish": "node scripts/copy-stockfish.mjs",
36
- "prepare": "rollup -c",
37
- "prestorybook": "npm run copy:stockfish",
38
- "test": "jest --config jest.config.cjs",
39
- "storybook": "storybook dev -p 6006",
40
- "build-storybook": "storybook build",
41
- "serve-storybook": "serve storybook-static",
42
- "prepublishOnly": "npm run build"
43
- },
44
- "dependencies": {
45
- "chess.js": "^1.0.0-beta.8",
46
- "rollup": "^4.22.2",
47
- "rollup-plugin-peer-deps-external": "^2.2.4",
48
- "rollup-plugin-typescript2": "^0.36.0",
49
- "typescript": "^5.6.2"
50
- },
51
- "peerDependencies": {
52
- "chess.js": "^1.0.0-beta.8",
53
- "react": "^18.3.1",
54
- "react-chess-core": "^0.1.1",
55
- "react-chessboard": "^4.7.1"
56
- },
57
- "devDependencies": {
58
- "@chromatic-com/storybook": "^1.9.0",
59
- "@rollup/plugin-commonjs": "^26.0.1",
60
- "@rollup/plugin-node-resolve": "^15.2.3",
61
- "@rollup/plugin-terser": "^0.4.4",
62
- "@storybook/addon-essentials": "^8.2.9",
63
- "@storybook/addon-interactions": "^8.2.9",
64
- "@storybook/addon-links": "^8.2.9",
65
- "@storybook/addon-onboarding": "^8.2.9",
66
- "@storybook/blocks": "^8.2.9",
67
- "@storybook/preset-typescript": "^3.0.0",
68
- "@storybook/react": "^8.2.9",
69
- "@storybook/react-vite": "^8.2.9",
70
- "@storybook/test": "^8.2.9",
71
- "@types/jest": "^29.5.12",
72
- "@types/react": "^18.3.12",
73
- "@types/react-dom": "^18.3.1",
74
- "chess.js": "^1.0.0-beta.8",
75
- "jest": "^29.7.0",
76
- "react": "^18.3.1",
77
- "react-chess-core": "^0.1.1",
78
- "react-chessboard": "^4.7.1",
79
- "storybook": "^8.2.9",
80
- "ts-jest": "^29.2.4",
81
- "vite": "^5.4.11",
82
- "vite-tsconfig-paths": "^5.0.1"
83
- },
84
- "optionalDependencies": {
85
- "stockfish": "^18.0.7"
86
- }
87
- }
1
+ {
2
+ "name": "react-chess-puzzle-kit",
3
+ "version": "1.0.2",
4
+ "description": "React chess puzzle kit: play, controls, analysis, and browser Stockfish for endchess.training and other apps",
5
+ "license": "MIT",
6
+ "author": "Robert Blackwell",
7
+ "main": "dist/index.js",
8
+ "module": "dist/index.esm.js",
9
+ "types": "dist/index.d.ts",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/reblackwell3/react-chess-puzzle-kit.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/reblackwell3/react-chess-puzzle-kit/issues"
16
+ },
17
+ "homepage": "https://github.com/reblackwell3/react-chess-puzzle-kit#readme",
18
+ "keywords": [
19
+ "react",
20
+ "chess",
21
+ "chess.js",
22
+ "chess puzzle",
23
+ "puzzle",
24
+ "analysis",
25
+ "stockfish",
26
+ "react-chessboard",
27
+ "chessboard",
28
+ "drag and drop"
29
+ ],
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "scripts": {
34
+ "build": "rollup -c",
35
+ "copy:stockfish": "node scripts/copy-stockfish.mjs",
36
+ "prepare": "rollup -c",
37
+ "prestorybook": "npm run copy:stockfish",
38
+ "test": "jest --config jest.config.cjs",
39
+ "storybook": "storybook dev -p 6006",
40
+ "build-storybook": "storybook build",
41
+ "serve-storybook": "serve storybook-static",
42
+ "prepublishOnly": "npm run build"
43
+ },
44
+ "dependencies": {
45
+ "chess.js": "^1.0.0-beta.8",
46
+ "rollup": "^4.22.2",
47
+ "rollup-plugin-peer-deps-external": "^2.2.4",
48
+ "rollup-plugin-typescript2": "^0.36.0",
49
+ "typescript": "^5.6.2"
50
+ },
51
+ "peerDependencies": {
52
+ "chess.js": "^1.0.0-beta.8",
53
+ "react": "^18.3.1",
54
+ "react-chess-core": "^0.1.1",
55
+ "react-chessboard": "^4.7.1"
56
+ },
57
+ "devDependencies": {
58
+ "@chromatic-com/storybook": "^1.9.0",
59
+ "@rollup/plugin-commonjs": "^26.0.1",
60
+ "@rollup/plugin-node-resolve": "^15.2.3",
61
+ "@rollup/plugin-terser": "^0.4.4",
62
+ "@storybook/addon-essentials": "^8.2.9",
63
+ "@storybook/addon-interactions": "^8.2.9",
64
+ "@storybook/addon-links": "^8.2.9",
65
+ "@storybook/addon-onboarding": "^8.2.9",
66
+ "@storybook/blocks": "^8.2.9",
67
+ "@storybook/preset-typescript": "^3.0.0",
68
+ "@storybook/react": "^8.2.9",
69
+ "@storybook/react-vite": "^8.2.9",
70
+ "@storybook/test": "^8.2.9",
71
+ "@types/jest": "^29.5.12",
72
+ "@types/react": "^18.3.12",
73
+ "@types/react-dom": "^18.3.1",
74
+ "chess.js": "^1.0.0-beta.8",
75
+ "jest": "^29.7.0",
76
+ "react": "^18.3.1",
77
+ "react-chess-core": "^0.1.1",
78
+ "react-chessboard": "^4.7.1",
79
+ "storybook": "^8.2.9",
80
+ "ts-jest": "^29.2.4",
81
+ "vite": "^5.4.11",
82
+ "vite-tsconfig-paths": "^5.0.1"
83
+ },
84
+ "optionalDependencies": {
85
+ "stockfish": "^18.0.7"
86
+ }
87
+ }