@react-chess-tools/react-chess-puzzle 0.6.2 → 1.0.1
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/CHANGELOG.md +28 -0
- package/README.md +568 -0
- package/dist/index.cjs +513 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +128 -0
- package/dist/index.d.ts +128 -0
- package/dist/{index.mjs → index.js} +143 -92
- package/dist/index.js.map +1 -0
- package/package.json +19 -9
- package/src/components/ChessPuzzle/ThemePuzzle.stories.tsx +300 -0
- package/src/components/ChessPuzzle/parts/Hint.tsx +32 -23
- package/src/components/ChessPuzzle/parts/PuzzleBoard.tsx +33 -27
- package/src/components/ChessPuzzle/parts/Reset.tsx +56 -36
- package/src/components/ChessPuzzle/parts/Root.tsx +29 -5
- package/src/components/ChessPuzzle/parts/__tests__/Hint.test.tsx +158 -0
- package/src/components/ChessPuzzle/parts/__tests__/PuzzleBoard.test.tsx +140 -0
- package/src/components/ChessPuzzle/parts/__tests__/Reset.test.tsx +341 -0
- package/src/components/ChessPuzzle/parts/__tests__/Root.test.tsx +42 -0
- package/src/docs/Theming.mdx +255 -0
- package/src/index.ts +14 -0
- package/src/theme/__tests__/context.test.tsx +66 -0
- package/src/theme/__tests__/defaults.test.ts +48 -0
- package/src/theme/__tests__/utils.test.ts +76 -0
- package/src/theme/context.tsx +36 -0
- package/src/theme/defaults.ts +16 -0
- package/src/theme/index.ts +20 -0
- package/src/theme/types.ts +29 -0
- package/src/theme/utils.ts +28 -0
- package/src/utils/__tests__/index.test.ts +0 -17
- package/src/utils/index.ts +21 -21
- package/README.MD +0 -344
- package/dist/index.d.mts +0 -57
- package/dist/index.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-chess-tools/react-chess-puzzle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "A lightweight, customizable React component library for rendering and interacting with chess puzzles.",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"require": "./dist/index.cjs",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
8
17
|
"scripts": {
|
|
9
18
|
"build": "tsup src/index.ts",
|
|
10
19
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -32,14 +41,15 @@
|
|
|
32
41
|
"author": "Daniele Cammareri <daniele.cammareri@gmail.com>",
|
|
33
42
|
"license": "MIT",
|
|
34
43
|
"dependencies": {
|
|
35
|
-
"@
|
|
36
|
-
"chess
|
|
44
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
45
|
+
"@react-chess-tools/react-chess-game": "1.0.1",
|
|
46
|
+
"chess.js": "^1.4.0",
|
|
37
47
|
"lodash": "^4.17.21"
|
|
38
48
|
},
|
|
39
49
|
"devDependencies": {
|
|
40
|
-
"@types/lodash": "^4.17.
|
|
41
|
-
"react": "^19.
|
|
42
|
-
"react-dom": "^19.
|
|
50
|
+
"@types/lodash": "^4.17.21",
|
|
51
|
+
"react": "^19.2.3",
|
|
52
|
+
"react-dom": "^19.2.3"
|
|
43
53
|
},
|
|
44
54
|
"peerDependencies": {
|
|
45
55
|
"react": ">=16.14.0",
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import type { Meta } from "@storybook/react";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { ChessPuzzle } from "./index";
|
|
4
|
+
import { defaultPuzzleTheme } from "../../theme/defaults";
|
|
5
|
+
import type { ChessPuzzleTheme } from "../../theme/types";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "react-chess-puzzle/Theme/Playground",
|
|
9
|
+
component: ChessPuzzle.Root,
|
|
10
|
+
tags: ["theme", "puzzle"],
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<div style={{ maxWidth: "900px" }}>
|
|
14
|
+
<Story />
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
],
|
|
18
|
+
} satisfies Meta<typeof ChessPuzzle.Root>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
const samplePuzzle = {
|
|
23
|
+
fen: "4kb1r/p2r1ppp/4qn2/1B2p1B1/4P3/1Q6/PPP2PPP/2KR4 w k - 0 1",
|
|
24
|
+
moves: ["Bxd7+", "Nxd7", "Qb8+", "Nxb8", "Rd8#"],
|
|
25
|
+
makeFirstMove: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Color picker component
|
|
29
|
+
const ColorInput: React.FC<{
|
|
30
|
+
label: string;
|
|
31
|
+
value: string;
|
|
32
|
+
onChange: (value: string) => void;
|
|
33
|
+
}> = ({ label, value, onChange }) => {
|
|
34
|
+
const rgbaToHex = (rgba: string): string => {
|
|
35
|
+
const match = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
36
|
+
if (match) {
|
|
37
|
+
const r = parseInt(match[1]).toString(16).padStart(2, "0");
|
|
38
|
+
const g = parseInt(match[2]).toString(16).padStart(2, "0");
|
|
39
|
+
const b = parseInt(match[3]).toString(16).padStart(2, "0");
|
|
40
|
+
return `#${r}${g}${b}`;
|
|
41
|
+
}
|
|
42
|
+
return value.startsWith("#") ? value : "#000000";
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const hexToRgba = (hex: string, alpha: number = 0.5): string => {
|
|
46
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
47
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
48
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
49
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
style={{
|
|
55
|
+
display: "flex",
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
gap: "8px",
|
|
58
|
+
marginBottom: "8px",
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<input
|
|
62
|
+
type="color"
|
|
63
|
+
value={rgbaToHex(value)}
|
|
64
|
+
onChange={(e) => onChange(hexToRgba(e.target.value))}
|
|
65
|
+
style={{ width: "40px", height: "30px", cursor: "pointer" }}
|
|
66
|
+
/>
|
|
67
|
+
<span style={{ fontSize: "12px", minWidth: "100px" }}>{label}</span>
|
|
68
|
+
<input
|
|
69
|
+
type="text"
|
|
70
|
+
value={value}
|
|
71
|
+
onChange={(e) => onChange(e.target.value)}
|
|
72
|
+
style={{ fontSize: "11px", width: "180px", padding: "4px" }}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const PuzzlePlayground = () => {
|
|
79
|
+
const [theme, setTheme] = useState<ChessPuzzleTheme>(defaultPuzzleTheme);
|
|
80
|
+
const [copied, setCopied] = useState(false);
|
|
81
|
+
const [puzzleKey, setPuzzleKey] = useState(0);
|
|
82
|
+
|
|
83
|
+
const updatePuzzleColor = (
|
|
84
|
+
key: keyof ChessPuzzleTheme["puzzle"],
|
|
85
|
+
value: string,
|
|
86
|
+
) => {
|
|
87
|
+
setTheme((prev) => ({
|
|
88
|
+
...prev,
|
|
89
|
+
puzzle: {
|
|
90
|
+
...prev.puzzle,
|
|
91
|
+
[key]: value,
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const copyTheme = () => {
|
|
97
|
+
const themeCode = `const myPuzzleTheme: PartialChessPuzzleTheme = {
|
|
98
|
+
puzzle: ${JSON.stringify(theme.puzzle, null, 4)}
|
|
99
|
+
};`;
|
|
100
|
+
navigator.clipboard.writeText(themeCode);
|
|
101
|
+
setCopied(true);
|
|
102
|
+
setTimeout(() => setCopied(false), 2000);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const resetPuzzle = () => {
|
|
106
|
+
setPuzzleKey((k) => k + 1);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div style={{ display: "flex", gap: "24px", flexWrap: "wrap" }}>
|
|
111
|
+
<div style={{ flex: "1", minWidth: "300px" }}>
|
|
112
|
+
<h3 style={{ marginBottom: "16px" }}>Puzzle Theme Editor</h3>
|
|
113
|
+
|
|
114
|
+
<div style={{ marginBottom: "16px" }}>
|
|
115
|
+
<strong>Puzzle Colors</strong>
|
|
116
|
+
<div style={{ marginTop: "8px" }}>
|
|
117
|
+
<ColorInput
|
|
118
|
+
label="Success"
|
|
119
|
+
value={theme.puzzle.success}
|
|
120
|
+
onChange={(v) => updatePuzzleColor("success", v)}
|
|
121
|
+
/>
|
|
122
|
+
<ColorInput
|
|
123
|
+
label="Failure"
|
|
124
|
+
value={theme.puzzle.failure}
|
|
125
|
+
onChange={(v) => updatePuzzleColor("failure", v)}
|
|
126
|
+
/>
|
|
127
|
+
<ColorInput
|
|
128
|
+
label="Hint"
|
|
129
|
+
value={theme.puzzle.hint}
|
|
130
|
+
onChange={(v) => updatePuzzleColor("hint", v)}
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div style={{ display: "flex", gap: "8px", marginBottom: "16px" }}>
|
|
136
|
+
<button
|
|
137
|
+
onClick={copyTheme}
|
|
138
|
+
style={{
|
|
139
|
+
padding: "8px 16px",
|
|
140
|
+
backgroundColor: copied ? "#4caf50" : "#2196f3",
|
|
141
|
+
color: "white",
|
|
142
|
+
border: "none",
|
|
143
|
+
borderRadius: "4px",
|
|
144
|
+
cursor: "pointer",
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{copied ? "Copied!" : "Copy Theme Code"}
|
|
148
|
+
</button>
|
|
149
|
+
<button
|
|
150
|
+
onClick={resetPuzzle}
|
|
151
|
+
style={{
|
|
152
|
+
padding: "8px 16px",
|
|
153
|
+
backgroundColor: "#666",
|
|
154
|
+
color: "white",
|
|
155
|
+
border: "none",
|
|
156
|
+
borderRadius: "4px",
|
|
157
|
+
cursor: "pointer",
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
Reset Puzzle
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div style={{ fontSize: "12px", color: "#666" }}>
|
|
165
|
+
<p>
|
|
166
|
+
<strong>How to test colors:</strong>
|
|
167
|
+
</p>
|
|
168
|
+
<ul style={{ paddingLeft: "16px" }}>
|
|
169
|
+
<li>Click "Hint" to see the hint color</li>
|
|
170
|
+
<li>Make a correct move to see success color</li>
|
|
171
|
+
<li>Make a wrong move to see failure color</li>
|
|
172
|
+
<li>Click "Reset" to try again</li>
|
|
173
|
+
</ul>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div style={{ flex: "1", minWidth: "350px" }}>
|
|
178
|
+
<h3 style={{ marginBottom: "16px" }}>Preview</h3>
|
|
179
|
+
<div style={{ maxWidth: "400px" }}>
|
|
180
|
+
<ChessPuzzle.Root key={puzzleKey} puzzle={samplePuzzle} theme={theme}>
|
|
181
|
+
<ChessPuzzle.Board />
|
|
182
|
+
<div style={{ marginTop: "8px", display: "flex", gap: "8px" }}>
|
|
183
|
+
<ChessPuzzle.Hint asChild>
|
|
184
|
+
<button style={{ padding: "6px 12px" }}>Hint</button>
|
|
185
|
+
</ChessPuzzle.Hint>
|
|
186
|
+
<ChessPuzzle.Reset asChild>
|
|
187
|
+
<button style={{ padding: "6px 12px" }}>Reset</button>
|
|
188
|
+
</ChessPuzzle.Reset>
|
|
189
|
+
</div>
|
|
190
|
+
</ChessPuzzle.Root>
|
|
191
|
+
</div>
|
|
192
|
+
<p style={{ fontSize: "12px", color: "#666", marginTop: "8px" }}>
|
|
193
|
+
Solution: Bxd7+, Nxd7, Qb8+, Nxb8, Rd8#
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const PuzzleThemeExamples = () => {
|
|
201
|
+
const customThemes = {
|
|
202
|
+
default: defaultPuzzleTheme,
|
|
203
|
+
neon: {
|
|
204
|
+
...defaultPuzzleTheme,
|
|
205
|
+
puzzle: {
|
|
206
|
+
success: "rgba(0, 255, 127, 0.6)",
|
|
207
|
+
failure: "rgba(255, 0, 127, 0.6)",
|
|
208
|
+
hint: "rgba(0, 191, 255, 0.6)",
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
pastel: {
|
|
212
|
+
...defaultPuzzleTheme,
|
|
213
|
+
puzzle: {
|
|
214
|
+
success: "rgba(152, 251, 152, 0.6)",
|
|
215
|
+
failure: "rgba(255, 182, 193, 0.6)",
|
|
216
|
+
hint: "rgba(173, 216, 230, 0.6)",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div>
|
|
223
|
+
<h2 style={{ marginBottom: "24px" }}>Puzzle Theme Examples</h2>
|
|
224
|
+
<div
|
|
225
|
+
style={{
|
|
226
|
+
display: "grid",
|
|
227
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
|
|
228
|
+
gap: "24px",
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
{Object.entries(customThemes).map(([name, theme]) => (
|
|
232
|
+
<div key={name}>
|
|
233
|
+
<h4 style={{ marginBottom: "8px", textTransform: "capitalize" }}>
|
|
234
|
+
{name}
|
|
235
|
+
</h4>
|
|
236
|
+
<ChessPuzzle.Root puzzle={samplePuzzle} theme={theme}>
|
|
237
|
+
<ChessPuzzle.Board />
|
|
238
|
+
<div style={{ marginTop: "8px", display: "flex", gap: "8px" }}>
|
|
239
|
+
<ChessPuzzle.Hint asChild>
|
|
240
|
+
<button style={{ padding: "4px 8px", fontSize: "12px" }}>
|
|
241
|
+
Hint
|
|
242
|
+
</button>
|
|
243
|
+
</ChessPuzzle.Hint>
|
|
244
|
+
<ChessPuzzle.Reset asChild>
|
|
245
|
+
<button style={{ padding: "4px 8px", fontSize: "12px" }}>
|
|
246
|
+
Reset
|
|
247
|
+
</button>
|
|
248
|
+
</ChessPuzzle.Reset>
|
|
249
|
+
</div>
|
|
250
|
+
</ChessPuzzle.Root>
|
|
251
|
+
</div>
|
|
252
|
+
))}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export const PartialPuzzleTheme = () => {
|
|
259
|
+
// Only override puzzle colors, inherit game colors from default
|
|
260
|
+
const partialTheme = {
|
|
261
|
+
puzzle: {
|
|
262
|
+
hint: "rgba(255, 215, 0, 0.6)", // Gold
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div style={{ maxWidth: "500px" }}>
|
|
268
|
+
<h3>Partial Puzzle Theme</h3>
|
|
269
|
+
<p style={{ fontSize: "14px", color: "#666", marginBottom: "16px" }}>
|
|
270
|
+
Only override the hint color to gold. Success and failure use defaults.
|
|
271
|
+
</p>
|
|
272
|
+
<ChessPuzzle.Root puzzle={samplePuzzle} theme={partialTheme}>
|
|
273
|
+
<ChessPuzzle.Board />
|
|
274
|
+
<div style={{ marginTop: "8px", display: "flex", gap: "8px" }}>
|
|
275
|
+
<ChessPuzzle.Hint asChild>
|
|
276
|
+
<button style={{ padding: "6px 12px" }}>Show Gold Hint</button>
|
|
277
|
+
</ChessPuzzle.Hint>
|
|
278
|
+
<ChessPuzzle.Reset asChild>
|
|
279
|
+
<button style={{ padding: "6px 12px" }}>Reset</button>
|
|
280
|
+
</ChessPuzzle.Reset>
|
|
281
|
+
</div>
|
|
282
|
+
</ChessPuzzle.Root>
|
|
283
|
+
<details style={{ marginTop: "16px" }}>
|
|
284
|
+
<summary style={{ cursor: "pointer", fontSize: "14px" }}>
|
|
285
|
+
View theme code
|
|
286
|
+
</summary>
|
|
287
|
+
<pre
|
|
288
|
+
style={{
|
|
289
|
+
fontSize: "11px",
|
|
290
|
+
background: "#f5f5f5",
|
|
291
|
+
padding: "12px",
|
|
292
|
+
overflow: "auto",
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
{JSON.stringify(partialTheme, null, 2)}
|
|
296
|
+
</pre>
|
|
297
|
+
</details>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
};
|
|
@@ -1,46 +1,55 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { Status } from "../../../utils";
|
|
3
4
|
import { useChessPuzzleContext } from "../../..";
|
|
4
5
|
|
|
5
|
-
export interface HintProps
|
|
6
|
+
export interface HintProps extends Omit<
|
|
7
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
8
|
+
"onClick"
|
|
9
|
+
> {
|
|
6
10
|
asChild?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The puzzle statuses in which the hint button should be visible.
|
|
13
|
+
* @default ["not-started", "in-progress"]
|
|
14
|
+
*/
|
|
7
15
|
showOn?: Status[];
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
const defaultShowOn: Status[] = ["not-started", "in-progress"];
|
|
11
19
|
|
|
12
|
-
export const Hint
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}) => {
|
|
20
|
+
export const Hint = React.forwardRef<
|
|
21
|
+
HTMLElement,
|
|
22
|
+
React.PropsWithChildren<HintProps>
|
|
23
|
+
>(({ children, asChild, showOn = defaultShowOn, className, ...rest }, ref) => {
|
|
17
24
|
const puzzleContext = useChessPuzzleContext();
|
|
18
25
|
if (!puzzleContext) {
|
|
19
26
|
throw new Error("PuzzleContext not found");
|
|
20
27
|
}
|
|
21
28
|
const { onHint, status } = puzzleContext;
|
|
22
|
-
|
|
29
|
+
|
|
30
|
+
const handleClick = React.useCallback(() => {
|
|
23
31
|
onHint();
|
|
24
|
-
};
|
|
32
|
+
}, [onHint]);
|
|
25
33
|
|
|
26
34
|
if (!showOn.includes(status)) {
|
|
27
35
|
return null;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<button type="button" onClick={handleClick}>
|
|
38
|
+
return asChild ? (
|
|
39
|
+
<Slot ref={ref} onClick={handleClick} className={className} {...rest}>
|
|
40
|
+
{children}
|
|
41
|
+
</Slot>
|
|
42
|
+
) : (
|
|
43
|
+
<button
|
|
44
|
+
ref={ref as React.RefObject<HTMLButtonElement>}
|
|
45
|
+
type="button"
|
|
46
|
+
className={className}
|
|
47
|
+
onClick={handleClick}
|
|
48
|
+
{...rest}
|
|
49
|
+
>
|
|
43
50
|
{children}
|
|
44
51
|
</button>
|
|
45
52
|
);
|
|
46
|
-
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
Hint.displayName = "ChessPuzzle.Hint";
|
|
@@ -6,35 +6,41 @@ import {
|
|
|
6
6
|
} from "@react-chess-tools/react-chess-game";
|
|
7
7
|
import { getCustomSquareStyles, stringToMove } from "../../../utils";
|
|
8
8
|
import { useChessPuzzleContext } from "../../..";
|
|
9
|
+
import { useChessPuzzleTheme } from "../../../theme/context";
|
|
9
10
|
|
|
10
|
-
export interface PuzzleBoardProps
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
options = {},
|
|
14
|
-
...rest
|
|
15
|
-
}) => {
|
|
16
|
-
const puzzleContext = useChessPuzzleContext();
|
|
17
|
-
const gameContext = useChessGameContext();
|
|
11
|
+
export interface PuzzleBoardProps extends React.ComponentProps<
|
|
12
|
+
typeof ChessGame.Board
|
|
13
|
+
> {}
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
15
|
+
export const PuzzleBoard = React.forwardRef<HTMLDivElement, PuzzleBoardProps>(
|
|
16
|
+
({ options, ...rest }, ref) => {
|
|
17
|
+
const puzzleContext = useChessPuzzleContext();
|
|
18
|
+
const gameContext = useChessGameContext();
|
|
19
|
+
const theme = useChessPuzzleTheme();
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
if (!puzzleContext) {
|
|
22
|
+
throw new Error("PuzzleContext not found");
|
|
23
|
+
}
|
|
24
|
+
if (!gameContext) {
|
|
25
|
+
throw new Error("ChessGameContext not found");
|
|
26
|
+
}
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
status,
|
|
32
|
-
hint,
|
|
33
|
-
isPlayerTurn,
|
|
34
|
-
game,
|
|
35
|
-
stringToMove(game, nextMove),
|
|
36
|
-
),
|
|
37
|
-
});
|
|
28
|
+
const { game } = gameContext;
|
|
29
|
+
const { status, hint, isPlayerTurn, nextMove } = puzzleContext;
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
const mergedOptions = deepMergeChessboardOptions(options || {}, {
|
|
32
|
+
squareStyles: getCustomSquareStyles(
|
|
33
|
+
status,
|
|
34
|
+
hint,
|
|
35
|
+
isPlayerTurn,
|
|
36
|
+
game,
|
|
37
|
+
stringToMove(game, nextMove),
|
|
38
|
+
theme,
|
|
39
|
+
),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return <ChessGame.Board ref={ref} {...rest} options={mergedOptions} />;
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
PuzzleBoard.displayName = "ChessPuzzle.PuzzleBoard";
|
|
@@ -1,51 +1,71 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { type Puzzle, type Status } from "../../../utils";
|
|
3
4
|
import { useChessPuzzleContext, type ChessPuzzleContextType } from "../../..";
|
|
4
5
|
|
|
5
|
-
export interface ResetProps
|
|
6
|
+
export interface ResetProps extends Omit<
|
|
7
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
8
|
+
"onReset"
|
|
9
|
+
> {
|
|
6
10
|
asChild?: boolean;
|
|
7
11
|
puzzle?: Puzzle;
|
|
8
12
|
onReset?: (puzzleContext: ChessPuzzleContextType) => void;
|
|
13
|
+
/**
|
|
14
|
+
* The puzzle statuses in which the reset button should be visible.
|
|
15
|
+
* @default ["failed", "solved"]
|
|
16
|
+
*/
|
|
9
17
|
showOn?: Status[];
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
const defaultShowOn: Status[] = ["failed", "solved"];
|
|
13
21
|
|
|
14
|
-
export const Reset
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
export const Reset = React.forwardRef<
|
|
23
|
+
HTMLElement,
|
|
24
|
+
React.PropsWithChildren<ResetProps>
|
|
25
|
+
>(
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
children,
|
|
29
|
+
asChild,
|
|
30
|
+
puzzle,
|
|
31
|
+
onReset,
|
|
32
|
+
showOn = defaultShowOn,
|
|
33
|
+
className,
|
|
34
|
+
...rest
|
|
35
|
+
},
|
|
36
|
+
ref,
|
|
37
|
+
) => {
|
|
38
|
+
const puzzleContext = useChessPuzzleContext();
|
|
39
|
+
if (!puzzleContext) {
|
|
40
|
+
throw new Error("PuzzleContext not found");
|
|
41
|
+
}
|
|
42
|
+
const { changePuzzle, puzzle: contextPuzzle, status } = puzzleContext;
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
const handleClick = React.useCallback(() => {
|
|
45
|
+
changePuzzle(puzzle || contextPuzzle);
|
|
46
|
+
onReset?.(puzzleContext);
|
|
47
|
+
}, [changePuzzle, puzzle, contextPuzzle, puzzleContext, onReset]);
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (isClickableElement(child)) {
|
|
38
|
-
return React.cloneElement(child, {
|
|
39
|
-
onClick: handleClick,
|
|
40
|
-
});
|
|
41
|
-
} else {
|
|
42
|
-
throw new Error("Change child must be a clickable element");
|
|
49
|
+
if (!showOn.includes(status)) {
|
|
50
|
+
return null;
|
|
43
51
|
}
|
|
44
|
-
}
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
return asChild ? (
|
|
54
|
+
<Slot ref={ref} onClick={handleClick} className={className} {...rest}>
|
|
55
|
+
{children}
|
|
56
|
+
</Slot>
|
|
57
|
+
) : (
|
|
58
|
+
<button
|
|
59
|
+
ref={ref as React.RefObject<HTMLButtonElement>}
|
|
60
|
+
type="button"
|
|
61
|
+
className={className}
|
|
62
|
+
onClick={handleClick}
|
|
63
|
+
{...rest}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</button>
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
Reset.displayName = "ChessPuzzle.Reset";
|
|
@@ -6,14 +6,26 @@ import {
|
|
|
6
6
|
} from "../../../hooks/useChessPuzzle";
|
|
7
7
|
import { ChessGame } from "@react-chess-tools/react-chess-game";
|
|
8
8
|
import { ChessPuzzleContext } from "../../../hooks/useChessPuzzleContext";
|
|
9
|
+
import { PuzzleThemeProvider } from "../../../theme/context";
|
|
10
|
+
import { mergePuzzleTheme } from "../../../theme/utils";
|
|
11
|
+
import type { PartialChessPuzzleTheme } from "../../../theme/types";
|
|
9
12
|
|
|
10
13
|
export interface RootProps {
|
|
11
14
|
puzzle: Puzzle;
|
|
12
15
|
onSolve?: (puzzleContext: ChessPuzzleContextType) => void;
|
|
13
16
|
onFail?: (puzzleContext: ChessPuzzleContextType) => void;
|
|
17
|
+
/** Optional theme configuration. Supports partial themes - only override the colors you need. */
|
|
18
|
+
theme?: PartialChessPuzzleTheme;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
interface PuzzleRootInnerProps {
|
|
22
|
+
puzzle: Puzzle;
|
|
23
|
+
onSolve?: (puzzleContext: ChessPuzzleContextType) => void;
|
|
24
|
+
onFail?: (puzzleContext: ChessPuzzleContextType) => void;
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const PuzzleRootInner: React.FC<PuzzleRootInnerProps> = ({
|
|
17
29
|
puzzle,
|
|
18
30
|
onSolve,
|
|
19
31
|
onFail,
|
|
@@ -32,13 +44,25 @@ export const Root: React.FC<React.PropsWithChildren<RootProps>> = ({
|
|
|
32
44
|
puzzle,
|
|
33
45
|
onSolve,
|
|
34
46
|
onFail,
|
|
47
|
+
theme,
|
|
35
48
|
children,
|
|
36
49
|
}) => {
|
|
50
|
+
// Merge partial theme with defaults
|
|
51
|
+
const mergedTheme = React.useMemo(() => mergePuzzleTheme(theme), [theme]);
|
|
52
|
+
|
|
37
53
|
return (
|
|
38
|
-
<ChessGame.Root
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
<ChessGame.Root
|
|
55
|
+
fen={puzzle.fen}
|
|
56
|
+
orientation={getOrientation(puzzle)}
|
|
57
|
+
theme={mergedTheme}
|
|
58
|
+
>
|
|
59
|
+
<PuzzleThemeProvider theme={mergedTheme}>
|
|
60
|
+
<PuzzleRootInner puzzle={puzzle} onSolve={onSolve} onFail={onFail}>
|
|
61
|
+
{children}
|
|
62
|
+
</PuzzleRootInner>
|
|
63
|
+
</PuzzleThemeProvider>
|
|
42
64
|
</ChessGame.Root>
|
|
43
65
|
);
|
|
44
66
|
};
|
|
67
|
+
|
|
68
|
+
Root.displayName = "ChessPuzzle.Root";
|