@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
|
@@ -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";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { renderHook } from "@testing-library/react";
|
|
3
|
+
import { useChessPuzzleTheme, PuzzleThemeProvider } from "../context";
|
|
4
|
+
import { defaultPuzzleTheme } from "../defaults";
|
|
5
|
+
|
|
6
|
+
describe("useChessPuzzleTheme", () => {
|
|
7
|
+
it("should return default puzzle theme when no provider is present", () => {
|
|
8
|
+
const { result } = renderHook(() => useChessPuzzleTheme());
|
|
9
|
+
expect(result.current).toEqual(defaultPuzzleTheme);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should return provided theme when wrapped in PuzzleThemeProvider", () => {
|
|
13
|
+
const customTheme = {
|
|
14
|
+
...defaultPuzzleTheme,
|
|
15
|
+
puzzle: {
|
|
16
|
+
...defaultPuzzleTheme.puzzle,
|
|
17
|
+
hint: "rgba(0, 255, 255, 0.5)",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
22
|
+
<PuzzleThemeProvider theme={customTheme}>{children}</PuzzleThemeProvider>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const { result } = renderHook(() => useChessPuzzleTheme(), { wrapper });
|
|
26
|
+
expect(result.current.puzzle.hint).toBe("rgba(0, 255, 255, 0.5)");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should return all puzzle-specific properties", () => {
|
|
30
|
+
const { result } = renderHook(() => useChessPuzzleTheme());
|
|
31
|
+
|
|
32
|
+
expect(result.current.puzzle).toHaveProperty("success");
|
|
33
|
+
expect(result.current.puzzle).toHaveProperty("failure");
|
|
34
|
+
expect(result.current.puzzle).toHaveProperty("hint");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should return inherited game theme properties", () => {
|
|
38
|
+
const { result } = renderHook(() => useChessPuzzleTheme());
|
|
39
|
+
|
|
40
|
+
expect(result.current.board).toBeDefined();
|
|
41
|
+
expect(result.current.state).toBeDefined();
|
|
42
|
+
expect(result.current.indicators).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("PuzzleThemeProvider", () => {
|
|
47
|
+
it("should allow nested providers with inner provider winning", () => {
|
|
48
|
+
const outerTheme = {
|
|
49
|
+
...defaultPuzzleTheme,
|
|
50
|
+
puzzle: { ...defaultPuzzleTheme.puzzle, hint: "outer" },
|
|
51
|
+
};
|
|
52
|
+
const innerTheme = {
|
|
53
|
+
...defaultPuzzleTheme,
|
|
54
|
+
puzzle: { ...defaultPuzzleTheme.puzzle, hint: "inner" },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
58
|
+
<PuzzleThemeProvider theme={outerTheme}>
|
|
59
|
+
<PuzzleThemeProvider theme={innerTheme}>{children}</PuzzleThemeProvider>
|
|
60
|
+
</PuzzleThemeProvider>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const { result } = renderHook(() => useChessPuzzleTheme(), { wrapper });
|
|
64
|
+
expect(result.current.puzzle.hint).toBe("inner");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defaultPuzzleTheme } from "../defaults";
|
|
2
|
+
import { defaultGameTheme } from "@react-chess-tools/react-chess-game";
|
|
3
|
+
|
|
4
|
+
describe("defaultPuzzleTheme", () => {
|
|
5
|
+
describe("backward compatibility", () => {
|
|
6
|
+
it("should have the original success color", () => {
|
|
7
|
+
expect(defaultPuzzleTheme.puzzle.success).toBe("rgba(172, 206, 89, 0.5)");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should have the original failure color", () => {
|
|
11
|
+
expect(defaultPuzzleTheme.puzzle.failure).toBe("rgba(201, 52, 48, 0.5)");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should have the original hint color", () => {
|
|
15
|
+
expect(defaultPuzzleTheme.puzzle.hint).toBe("rgba(27, 172, 166, 0.5)");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("inheritance from game theme", () => {
|
|
20
|
+
it("should include all game theme properties", () => {
|
|
21
|
+
expect(defaultPuzzleTheme.board).toBeDefined();
|
|
22
|
+
expect(defaultPuzzleTheme.state).toBeDefined();
|
|
23
|
+
expect(defaultPuzzleTheme.indicators).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should have the same board colors as game theme", () => {
|
|
27
|
+
expect(defaultPuzzleTheme.board).toEqual(defaultGameTheme.board);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should have the same state colors as game theme", () => {
|
|
31
|
+
expect(defaultPuzzleTheme.state).toEqual(defaultGameTheme.state);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should have the same indicator colors as game theme", () => {
|
|
35
|
+
expect(defaultPuzzleTheme.indicators).toEqual(
|
|
36
|
+
defaultGameTheme.indicators,
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("structure", () => {
|
|
42
|
+
it("should have puzzle property with all required colors", () => {
|
|
43
|
+
expect(defaultPuzzleTheme.puzzle).toHaveProperty("success");
|
|
44
|
+
expect(defaultPuzzleTheme.puzzle).toHaveProperty("failure");
|
|
45
|
+
expect(defaultPuzzleTheme.puzzle).toHaveProperty("hint");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { mergePuzzleTheme } from "../utils";
|
|
2
|
+
import { defaultPuzzleTheme } from "../defaults";
|
|
3
|
+
|
|
4
|
+
describe("mergePuzzleTheme", () => {
|
|
5
|
+
it("should return default puzzle theme when no partial theme is provided", () => {
|
|
6
|
+
const result = mergePuzzleTheme();
|
|
7
|
+
expect(result).toEqual(defaultPuzzleTheme);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should return default puzzle theme when undefined is provided", () => {
|
|
11
|
+
const result = mergePuzzleTheme(undefined);
|
|
12
|
+
expect(result).toEqual(defaultPuzzleTheme);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should return a new object, not a reference to default", () => {
|
|
16
|
+
const result = mergePuzzleTheme();
|
|
17
|
+
expect(result).not.toBe(defaultPuzzleTheme);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should override puzzle-specific colors", () => {
|
|
21
|
+
const result = mergePuzzleTheme({
|
|
22
|
+
puzzle: { hint: "rgba(0, 255, 255, 0.5)" },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(result.puzzle.hint).toBe("rgba(0, 255, 255, 0.5)");
|
|
26
|
+
// Other puzzle colors should remain default
|
|
27
|
+
expect(result.puzzle.success).toBe(defaultPuzzleTheme.puzzle.success);
|
|
28
|
+
expect(result.puzzle.failure).toBe(defaultPuzzleTheme.puzzle.failure);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should override all puzzle colors at once", () => {
|
|
32
|
+
const result = mergePuzzleTheme({
|
|
33
|
+
puzzle: {
|
|
34
|
+
success: "rgba(0, 255, 0, 0.5)",
|
|
35
|
+
failure: "rgba(255, 0, 0, 0.5)",
|
|
36
|
+
hint: "rgba(0, 0, 255, 0.5)",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result.puzzle.success).toBe("rgba(0, 255, 0, 0.5)");
|
|
41
|
+
expect(result.puzzle.failure).toBe("rgba(255, 0, 0, 0.5)");
|
|
42
|
+
expect(result.puzzle.hint).toBe("rgba(0, 0, 255, 0.5)");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should override inherited game theme properties", () => {
|
|
46
|
+
const result = mergePuzzleTheme({
|
|
47
|
+
state: { lastMove: "rgba(100, 200, 100, 0.6)" },
|
|
48
|
+
board: { lightSquare: { backgroundColor: "#ffffff" } },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.state.lastMove).toBe("rgba(100, 200, 100, 0.6)");
|
|
52
|
+
expect(result.board.lightSquare).toEqual({ backgroundColor: "#ffffff" });
|
|
53
|
+
// Puzzle colors should remain default
|
|
54
|
+
expect(result.puzzle).toEqual(defaultPuzzleTheme.puzzle);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should allow overriding both game and puzzle properties", () => {
|
|
58
|
+
const result = mergePuzzleTheme({
|
|
59
|
+
state: { check: "rgba(255, 100, 100, 0.8)" },
|
|
60
|
+
puzzle: { success: "rgba(100, 255, 100, 0.8)" },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result.state.check).toBe("rgba(255, 100, 100, 0.8)");
|
|
64
|
+
expect(result.puzzle.success).toBe("rgba(100, 255, 100, 0.8)");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should preserve unmentioned theme properties", () => {
|
|
68
|
+
const result = mergePuzzleTheme({
|
|
69
|
+
puzzle: { hint: "rgba(0, 255, 255, 0.5)" },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result.board).toEqual(defaultPuzzleTheme.board);
|
|
73
|
+
expect(result.state).toEqual(defaultPuzzleTheme.state);
|
|
74
|
+
expect(result.indicators).toEqual(defaultPuzzleTheme.indicators);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
import type { ChessPuzzleTheme } from "./types";
|
|
3
|
+
import { defaultPuzzleTheme } from "./defaults";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Context for ChessPuzzle theme
|
|
7
|
+
*/
|
|
8
|
+
export const ChessPuzzleThemeContext =
|
|
9
|
+
createContext<ChessPuzzleTheme>(defaultPuzzleTheme);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hook to access the current ChessPuzzle theme.
|
|
13
|
+
* Returns the default puzzle theme if no ThemeProvider is present.
|
|
14
|
+
*/
|
|
15
|
+
export const useChessPuzzleTheme = (): ChessPuzzleTheme => {
|
|
16
|
+
return useContext(ChessPuzzleThemeContext);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface PuzzleThemeProviderProps {
|
|
20
|
+
theme: ChessPuzzleTheme;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Internal provider component used by Puzzle Root when a theme prop is provided.
|
|
26
|
+
*/
|
|
27
|
+
export const PuzzleThemeProvider: React.FC<PuzzleThemeProviderProps> = ({
|
|
28
|
+
theme,
|
|
29
|
+
children,
|
|
30
|
+
}) => {
|
|
31
|
+
return (
|
|
32
|
+
<ChessPuzzleThemeContext.Provider value={theme}>
|
|
33
|
+
{children}
|
|
34
|
+
</ChessPuzzleThemeContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defaultGameTheme } from "@react-chess-tools/react-chess-game";
|
|
2
|
+
import type { ChessPuzzleTheme } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default theme for ChessPuzzle component.
|
|
6
|
+
* Extends the default game theme with puzzle-specific colors.
|
|
7
|
+
* These values match the original hardcoded colors for backward compatibility.
|
|
8
|
+
*/
|
|
9
|
+
export const defaultPuzzleTheme: ChessPuzzleTheme = {
|
|
10
|
+
...defaultGameTheme,
|
|
11
|
+
puzzle: {
|
|
12
|
+
success: "rgba(172, 206, 89, 0.5)",
|
|
13
|
+
failure: "rgba(201, 52, 48, 0.5)",
|
|
14
|
+
hint: "rgba(27, 172, 166, 0.5)",
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
ChessPuzzleTheme,
|
|
4
|
+
PuzzleStateTheme,
|
|
5
|
+
PartialChessPuzzleTheme,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
// Default theme
|
|
9
|
+
export { defaultPuzzleTheme } from "./defaults";
|
|
10
|
+
|
|
11
|
+
// Utilities
|
|
12
|
+
export { mergePuzzleTheme } from "./utils";
|
|
13
|
+
|
|
14
|
+
// Context and hook
|
|
15
|
+
export {
|
|
16
|
+
ChessPuzzleThemeContext,
|
|
17
|
+
useChessPuzzleTheme,
|
|
18
|
+
PuzzleThemeProvider,
|
|
19
|
+
} from "./context";
|
|
20
|
+
export type { PuzzleThemeProviderProps } from "./context";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChessGameTheme,
|
|
3
|
+
DeepPartial,
|
|
4
|
+
} from "@react-chess-tools/react-chess-game";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Puzzle-specific state colors (RGBA color strings)
|
|
8
|
+
*/
|
|
9
|
+
export interface PuzzleStateTheme {
|
|
10
|
+
/** Background color for successful moves */
|
|
11
|
+
success: string;
|
|
12
|
+
/** Background color for failed moves */
|
|
13
|
+
failure: string;
|
|
14
|
+
/** Background color for hint squares */
|
|
15
|
+
hint: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Complete theme configuration for ChessPuzzle component.
|
|
20
|
+
* Extends ChessGameTheme with puzzle-specific colors.
|
|
21
|
+
*/
|
|
22
|
+
export interface ChessPuzzleTheme extends ChessGameTheme {
|
|
23
|
+
puzzle: PuzzleStateTheme;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Partial theme for puzzle customization - allows overriding only specific properties
|
|
28
|
+
*/
|
|
29
|
+
export type PartialChessPuzzleTheme = DeepPartial<ChessPuzzleTheme>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { merge } from "lodash";
|
|
2
|
+
import type { ChessPuzzleTheme, PartialChessPuzzleTheme } from "./types";
|
|
3
|
+
import { defaultPuzzleTheme } from "./defaults";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Deep merges a partial puzzle theme with the default puzzle theme.
|
|
7
|
+
* Allows users to override only specific theme properties while keeping defaults for the rest.
|
|
8
|
+
*
|
|
9
|
+
* @param partialTheme - Partial theme with only the properties to override
|
|
10
|
+
* @returns Complete puzzle theme with overridden properties merged with defaults
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const customTheme = mergePuzzleTheme({
|
|
15
|
+
* puzzle: { hint: "rgba(0, 255, 255, 0.5)" }
|
|
16
|
+
* });
|
|
17
|
+
* // Returns full puzzle theme with only hint color changed
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export const mergePuzzleTheme = (
|
|
21
|
+
partialTheme?: PartialChessPuzzleTheme,
|
|
22
|
+
): ChessPuzzleTheme => {
|
|
23
|
+
if (!partialTheme) {
|
|
24
|
+
return { ...defaultPuzzleTheme };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return merge({}, defaultPuzzleTheme, partialTheme);
|
|
28
|
+
};
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Chess, Move } from "chess.js";
|
|
2
|
-
import React from "react";
|
|
3
2
|
import {
|
|
4
3
|
getOrientation,
|
|
5
|
-
isClickableElement,
|
|
6
4
|
getCustomSquareStyles,
|
|
7
5
|
stringToMove,
|
|
8
6
|
Puzzle,
|
|
@@ -40,21 +38,6 @@ describe("Puzzle Utilities", () => {
|
|
|
40
38
|
});
|
|
41
39
|
});
|
|
42
40
|
|
|
43
|
-
describe("isClickableElement", () => {
|
|
44
|
-
it("should return true for valid clickable elements", () => {
|
|
45
|
-
const clickableElement = React.createElement("button", {
|
|
46
|
-
onClick: () => {},
|
|
47
|
-
});
|
|
48
|
-
expect(isClickableElement(clickableElement)).toBe(true);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("should return false for non-React elements", () => {
|
|
52
|
-
expect(isClickableElement("not an element")).toBe(false);
|
|
53
|
-
expect(isClickableElement(null)).toBe(false);
|
|
54
|
-
expect(isClickableElement(undefined)).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
41
|
describe("getCustomSquareStyles", () => {
|
|
59
42
|
const game = new Chess();
|
|
60
43
|
|