@react-chess-tools/react-chess-puzzle 0.6.2 → 1.0.0
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 +20 -0
- package/dist/index.cjs +510 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +116 -0
- package/dist/index.d.ts +116 -0
- package/dist/{index.mjs → index.js} +76 -28
- package/dist/index.js.map +1 -0
- package/package.json +18 -9
- package/src/components/ChessPuzzle/ThemePuzzle.stories.tsx +300 -0
- package/src/components/ChessPuzzle/parts/PuzzleBoard.tsx +7 -2
- package/src/components/ChessPuzzle/parts/Root.tsx +27 -5
- 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/index.ts +21 -11
- package/dist/index.d.mts +0 -57
- package/dist/index.mjs.map +0 -1
|
@@ -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
|
+
};
|
|
@@ -6,15 +6,19 @@ 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";
|
|
10
|
+
|
|
11
|
+
export interface PuzzleBoardProps extends React.ComponentProps<
|
|
12
|
+
typeof ChessGame.Board
|
|
13
|
+
> {}
|
|
9
14
|
|
|
10
|
-
export interface PuzzleBoardProps
|
|
11
|
-
extends React.ComponentProps<typeof ChessGame.Board> {}
|
|
12
15
|
export const PuzzleBoard: React.FC<PuzzleBoardProps> = ({
|
|
13
16
|
options = {},
|
|
14
17
|
...rest
|
|
15
18
|
}) => {
|
|
16
19
|
const puzzleContext = useChessPuzzleContext();
|
|
17
20
|
const gameContext = useChessGameContext();
|
|
21
|
+
const theme = useChessPuzzleTheme();
|
|
18
22
|
|
|
19
23
|
if (!puzzleContext) {
|
|
20
24
|
throw new Error("PuzzleContext not found");
|
|
@@ -33,6 +37,7 @@ export const PuzzleBoard: React.FC<PuzzleBoardProps> = ({
|
|
|
33
37
|
isPlayerTurn,
|
|
34
38
|
game,
|
|
35
39
|
stringToMove(game, nextMove),
|
|
40
|
+
theme,
|
|
36
41
|
),
|
|
37
42
|
});
|
|
38
43
|
|
|
@@ -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,23 @@ 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
|
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
{/* Theming.mdx */}
|
|
2
|
+
import { Meta, ColorPalette, ColorItem } from '@storybook/blocks';
|
|
3
|
+
|
|
4
|
+
<Meta title="react-chess-puzzle/Theming" />
|
|
5
|
+
|
|
6
|
+
# Puzzle Theming
|
|
7
|
+
|
|
8
|
+
React Chess Puzzle extends the game theming system with puzzle-specific colors for hints, success, and failure states.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
Pass a theme to `ChessPuzzle.Root` to customize puzzle appearance:
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { ChessPuzzle } from "@react-chess-tools/react-chess-puzzle";
|
|
16
|
+
|
|
17
|
+
const puzzle = {
|
|
18
|
+
fen: "4kb1r/p2r1ppp/4qn2/1B2p1B1/4P3/1Q6/PPP2PPP/2KR4 w k - 0 1",
|
|
19
|
+
moves: ["Bxd7+", "Nxd7", "Qb8+", "Nxb8", "Rd8#"],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function App() {
|
|
23
|
+
return (
|
|
24
|
+
<ChessPuzzle.Root
|
|
25
|
+
puzzle={puzzle}
|
|
26
|
+
theme={{ puzzle: { hint: "rgba(255, 215, 0, 0.6)" } }}
|
|
27
|
+
>
|
|
28
|
+
<ChessPuzzle.Board />
|
|
29
|
+
<ChessPuzzle.Hint>Show Hint</ChessPuzzle.Hint>
|
|
30
|
+
</ChessPuzzle.Root>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Puzzle Theme Structure
|
|
36
|
+
|
|
37
|
+
The puzzle theme extends `ChessGameTheme` with an additional `puzzle` section:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
interface ChessPuzzleTheme extends ChessGameTheme {
|
|
41
|
+
puzzle: {
|
|
42
|
+
success: string; // Color for correct moves (RGBA)
|
|
43
|
+
failure: string; // Color for incorrect moves (RGBA)
|
|
44
|
+
hint: string; // Color for hint squares (RGBA)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Puzzle-Specific Colors
|
|
50
|
+
|
|
51
|
+
The puzzle theme adds three new color options:
|
|
52
|
+
|
|
53
|
+
<table>
|
|
54
|
+
<thead>
|
|
55
|
+
<tr>
|
|
56
|
+
<th>Color</th>
|
|
57
|
+
<th>Usage</th>
|
|
58
|
+
</tr>
|
|
59
|
+
</thead>
|
|
60
|
+
<tbody>
|
|
61
|
+
<tr>
|
|
62
|
+
<td>
|
|
63
|
+
<code>puzzle.success</code>
|
|
64
|
+
</td>
|
|
65
|
+
<td>Highlights squares when a correct move is made</td>
|
|
66
|
+
</tr>
|
|
67
|
+
<tr>
|
|
68
|
+
<td>
|
|
69
|
+
<code>puzzle.failure</code>
|
|
70
|
+
</td>
|
|
71
|
+
<td>Highlights squares when an incorrect move is made</td>
|
|
72
|
+
</tr>
|
|
73
|
+
<tr>
|
|
74
|
+
<td>
|
|
75
|
+
<code>puzzle.hint</code>
|
|
76
|
+
</td>
|
|
77
|
+
<td>Highlights the hint square(s) when hint is requested</td>
|
|
78
|
+
</tr>
|
|
79
|
+
</tbody>
|
|
80
|
+
</table>
|
|
81
|
+
|
|
82
|
+
## Creating a Custom Puzzle Theme
|
|
83
|
+
|
|
84
|
+
You can customize both game and puzzle colors:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { ChessPuzzle } from "@react-chess-tools/react-chess-puzzle";
|
|
88
|
+
import type { PartialChessPuzzleTheme } from "@react-chess-tools/react-chess-puzzle";
|
|
89
|
+
|
|
90
|
+
const neonTheme: PartialChessPuzzleTheme = {
|
|
91
|
+
// Customize board colors (inherited from game theme)
|
|
92
|
+
board: {
|
|
93
|
+
lightSquare: { backgroundColor: "#2a2a2a" },
|
|
94
|
+
darkSquare: { backgroundColor: "#1a1a1a" },
|
|
95
|
+
},
|
|
96
|
+
// Customize game state colors
|
|
97
|
+
state: {
|
|
98
|
+
lastMove: "rgba(0, 255, 255, 0.4)",
|
|
99
|
+
},
|
|
100
|
+
// Customize puzzle-specific colors
|
|
101
|
+
puzzle: {
|
|
102
|
+
success: "rgba(0, 255, 127, 0.6)",
|
|
103
|
+
failure: "rgba(255, 0, 127, 0.6)",
|
|
104
|
+
hint: "rgba(0, 191, 255, 0.6)",
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function App() {
|
|
109
|
+
return (
|
|
110
|
+
<ChessPuzzle.Root puzzle={puzzle} theme={neonTheme}>
|
|
111
|
+
<ChessPuzzle.Board />
|
|
112
|
+
</ChessPuzzle.Root>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Partial Theme Overrides
|
|
118
|
+
|
|
119
|
+
Override only the puzzle colors you need:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
// Only change the hint color to gold
|
|
123
|
+
const goldHint: PartialChessPuzzleTheme = {
|
|
124
|
+
puzzle: {
|
|
125
|
+
hint: "rgba(255, 215, 0, 0.6)",
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Only change success/failure colors
|
|
130
|
+
const customFeedback: PartialChessPuzzleTheme = {
|
|
131
|
+
puzzle: {
|
|
132
|
+
success: "rgba(0, 200, 100, 0.7)",
|
|
133
|
+
failure: "rgba(200, 0, 50, 0.7)",
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Theme Inheritance
|
|
139
|
+
|
|
140
|
+
Puzzle themes inherit all game theme properties. You can:
|
|
141
|
+
|
|
142
|
+
1. **Only specify puzzle colors** - game colors use defaults
|
|
143
|
+
2. **Only specify game colors** - puzzle colors use defaults
|
|
144
|
+
3. **Mix both** - customize any combination
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
// Example: Custom board + default puzzle colors
|
|
148
|
+
const customBoard: PartialChessPuzzleTheme = {
|
|
149
|
+
board: {
|
|
150
|
+
lightSquare: { backgroundColor: "#e8e8e8" },
|
|
151
|
+
darkSquare: { backgroundColor: "#4a4a4a" },
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Example: Default board + custom puzzle colors
|
|
156
|
+
const customPuzzle: PartialChessPuzzleTheme = {
|
|
157
|
+
puzzle: {
|
|
158
|
+
success: "rgba(100, 255, 100, 0.5)",
|
|
159
|
+
failure: "rgba(255, 100, 100, 0.5)",
|
|
160
|
+
hint: "rgba(100, 100, 255, 0.5)",
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Using Theme Utilities
|
|
166
|
+
|
|
167
|
+
### mergePuzzleTheme
|
|
168
|
+
|
|
169
|
+
Merge a partial puzzle theme with defaults:
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import {
|
|
173
|
+
mergePuzzleTheme,
|
|
174
|
+
defaultPuzzleTheme,
|
|
175
|
+
} from "@react-chess-tools/react-chess-puzzle";
|
|
176
|
+
|
|
177
|
+
const myTheme = mergePuzzleTheme({
|
|
178
|
+
puzzle: { hint: "rgba(255, 215, 0, 0.6)" },
|
|
179
|
+
});
|
|
180
|
+
// myTheme is now a complete ChessPuzzleTheme
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### useChessPuzzleTheme Hook
|
|
184
|
+
|
|
185
|
+
Access the current puzzle theme from any component:
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import { useChessPuzzleTheme } from "@react-chess-tools/react-chess-puzzle";
|
|
189
|
+
|
|
190
|
+
function HintButton() {
|
|
191
|
+
const theme = useChessPuzzleTheme();
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<button style={{ backgroundColor: theme.puzzle.hint }}>Show Hint</button>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Default Puzzle Colors
|
|
200
|
+
|
|
201
|
+
<ColorPalette>
|
|
202
|
+
<ColorItem
|
|
203
|
+
title="Puzzle Colors"
|
|
204
|
+
subtitle="Puzzle state feedback"
|
|
205
|
+
colors={{
|
|
206
|
+
Success: "rgba(172, 206, 89, 0.5)",
|
|
207
|
+
Failure: "rgba(201, 52, 48, 0.5)",
|
|
208
|
+
Hint: "rgba(27, 172, 166, 0.5)",
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
</ColorPalette>
|
|
212
|
+
|
|
213
|
+
## Example Theme Variations
|
|
214
|
+
|
|
215
|
+
### Pastel Theme
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
const pastelTheme: PartialChessPuzzleTheme = {
|
|
219
|
+
puzzle: {
|
|
220
|
+
success: "rgba(152, 251, 152, 0.6)", // Pale green
|
|
221
|
+
failure: "rgba(255, 182, 193, 0.6)", // Light pink
|
|
222
|
+
hint: "rgba(173, 216, 230, 0.6)", // Light blue
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### High Contrast Theme
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
const highContrastTheme: PartialChessPuzzleTheme = {
|
|
231
|
+
puzzle: {
|
|
232
|
+
success: "rgba(0, 255, 0, 0.7)",
|
|
233
|
+
failure: "rgba(255, 0, 0, 0.7)",
|
|
234
|
+
hint: "rgba(0, 255, 255, 0.7)",
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## TypeScript Support
|
|
240
|
+
|
|
241
|
+
All puzzle theme types are exported:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import type {
|
|
245
|
+
ChessPuzzleTheme,
|
|
246
|
+
PartialChessPuzzleTheme,
|
|
247
|
+
PuzzleStateTheme,
|
|
248
|
+
} from "@react-chess-tools/react-chess-puzzle";
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Next Steps
|
|
252
|
+
|
|
253
|
+
- Try the **[Puzzle Theme Playground](/story/react-chess-puzzle-theme-playground--puzzle-playground)** to experiment with colors
|
|
254
|
+
- See **[Puzzle Theme Examples](/story/react-chess-puzzle-theme-playground--puzzle-theme-examples)** for inspiration
|
|
255
|
+
- Check out **[Game Theming](/docs/react-chess-game-theming--docs)** for board and state color options
|
package/src/index.ts
CHANGED
|
@@ -13,3 +13,17 @@ export type { HintProps } from "./components/ChessPuzzle/parts/Hint";
|
|
|
13
13
|
export type { ResetProps } from "./components/ChessPuzzle/parts/Reset";
|
|
14
14
|
export type { PuzzleBoardProps } from "./components/ChessPuzzle/parts/PuzzleBoard";
|
|
15
15
|
export type { RootProps } from "./components/ChessPuzzle/parts/Root";
|
|
16
|
+
|
|
17
|
+
// Theme - Types
|
|
18
|
+
export type {
|
|
19
|
+
ChessPuzzleTheme,
|
|
20
|
+
PuzzleStateTheme,
|
|
21
|
+
PartialChessPuzzleTheme,
|
|
22
|
+
} from "./theme/types";
|
|
23
|
+
|
|
24
|
+
// Theme - Values
|
|
25
|
+
export { defaultPuzzleTheme } from "./theme/defaults";
|
|
26
|
+
|
|
27
|
+
// Theme - Utilities
|
|
28
|
+
export { mergePuzzleTheme } from "./theme/utils";
|
|
29
|
+
export { useChessPuzzleTheme, ChessPuzzleThemeContext } from "./theme/context";
|