@react-chess-tools/react-chess-game 0.5.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 +15 -0
- package/dist/index.cjs +775 -0
- package/dist/index.cjs.map +1 -0
- package/dist/{index.d.mts → index.d.cts} +118 -12
- package/dist/index.d.ts +264 -0
- package/dist/{index.mjs → index.js} +171 -38
- package/dist/index.js.map +1 -0
- package/package.json +18 -9
- package/src/components/ChessGame/Theme.stories.tsx +242 -0
- package/src/components/ChessGame/ThemePresets.stories.tsx +144 -0
- package/src/components/ChessGame/parts/Board.tsx +6 -4
- package/src/components/ChessGame/parts/Root.tsx +11 -1
- package/src/docs/Theming.mdx +281 -0
- package/src/hooks/useChessGame.ts +23 -7
- package/src/index.ts +19 -0
- package/src/theme/__tests__/context.test.tsx +75 -0
- package/src/theme/__tests__/defaults.test.ts +61 -0
- package/src/theme/__tests__/utils.test.ts +106 -0
- package/src/theme/context.tsx +37 -0
- package/src/theme/defaults.ts +22 -0
- package/src/theme/index.ts +36 -0
- package/src/theme/presets.ts +41 -0
- package/src/theme/types.ts +56 -0
- package/src/theme/utils.ts +47 -0
- package/src/utils/__tests__/board.test.ts +118 -0
- package/src/utils/board.ts +18 -9
- package/src/utils/chess.ts +25 -5
- package/coverage/clover.xml +0 -6
- package/coverage/coverage-final.json +0 -1
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -101
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -196
- package/coverage/lcov.info +0 -0
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
{/* Theming.mdx */}
|
|
2
|
+
import { Meta, ColorPalette, ColorItem } from '@storybook/blocks';
|
|
3
|
+
|
|
4
|
+
<Meta title="react-chess-game/Theming" />
|
|
5
|
+
|
|
6
|
+
# Theming
|
|
7
|
+
|
|
8
|
+
React Chess Tools provides a comprehensive theming system that allows you to customize the look and feel of your chess boards. You can customize board colors, move highlights, check indicators, and more.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
The simplest way to use a theme is to pass it to the `theme` prop on `ChessGame.Root`:
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { ChessGame, themes } from "@react-chess-tools/react-chess-game";
|
|
16
|
+
|
|
17
|
+
function App() {
|
|
18
|
+
return (
|
|
19
|
+
<ChessGame.Root theme={themes.lichess}>
|
|
20
|
+
<ChessGame.Board />
|
|
21
|
+
</ChessGame.Root>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Available Preset Themes
|
|
27
|
+
|
|
28
|
+
We provide three built-in themes:
|
|
29
|
+
|
|
30
|
+
<table>
|
|
31
|
+
<thead>
|
|
32
|
+
<tr>
|
|
33
|
+
<th>Theme</th>
|
|
34
|
+
<th>Description</th>
|
|
35
|
+
</tr>
|
|
36
|
+
</thead>
|
|
37
|
+
<tbody>
|
|
38
|
+
<tr>
|
|
39
|
+
<td>
|
|
40
|
+
<code>themes.default</code>
|
|
41
|
+
</td>
|
|
42
|
+
<td>Classic brown/beige board with yellow highlights</td>
|
|
43
|
+
</tr>
|
|
44
|
+
<tr>
|
|
45
|
+
<td>
|
|
46
|
+
<code>themes.lichess</code>
|
|
47
|
+
</td>
|
|
48
|
+
<td>Lichess.org inspired with green highlights</td>
|
|
49
|
+
</tr>
|
|
50
|
+
<tr>
|
|
51
|
+
<td>
|
|
52
|
+
<code>themes.chessCom</code>
|
|
53
|
+
</td>
|
|
54
|
+
<td>Chess.com inspired with green board and yellow highlights</td>
|
|
55
|
+
</tr>
|
|
56
|
+
</tbody>
|
|
57
|
+
</table>
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { themes } from "@react-chess-tools/react-chess-game";
|
|
61
|
+
|
|
62
|
+
// Use a preset theme
|
|
63
|
+
<ChessGame.Root theme={themes.lichess}>
|
|
64
|
+
<ChessGame.Board />
|
|
65
|
+
</ChessGame.Root>;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Theme Structure
|
|
69
|
+
|
|
70
|
+
A complete theme has the following structure:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
interface ChessGameTheme {
|
|
74
|
+
board: {
|
|
75
|
+
lightSquare: CSSProperties; // Style for light squares
|
|
76
|
+
darkSquare: CSSProperties; // Style for dark squares
|
|
77
|
+
};
|
|
78
|
+
state: {
|
|
79
|
+
lastMove: string; // Color for last move highlight (RGBA)
|
|
80
|
+
check: string; // Color for check highlight (RGBA)
|
|
81
|
+
activeSquare: string; // Color for selected piece (RGBA)
|
|
82
|
+
dropSquare: CSSProperties; // Style for valid drop target
|
|
83
|
+
};
|
|
84
|
+
indicators: {
|
|
85
|
+
move: string; // Color for move dot indicators (RGBA)
|
|
86
|
+
capture: string; // Color for capture ring indicators (RGBA)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Creating a Custom Theme
|
|
92
|
+
|
|
93
|
+
You can create a fully custom theme by defining all properties:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { ChessGame } from "@react-chess-tools/react-chess-game";
|
|
97
|
+
import type { ChessGameTheme } from "@react-chess-tools/react-chess-game";
|
|
98
|
+
|
|
99
|
+
const darkTheme: ChessGameTheme = {
|
|
100
|
+
board: {
|
|
101
|
+
lightSquare: { backgroundColor: "#4a4a4a" },
|
|
102
|
+
darkSquare: { backgroundColor: "#2d2d2d" },
|
|
103
|
+
},
|
|
104
|
+
state: {
|
|
105
|
+
lastMove: "rgba(100, 150, 255, 0.5)",
|
|
106
|
+
check: "rgba(255, 50, 50, 0.6)",
|
|
107
|
+
activeSquare: "rgba(100, 150, 255, 0.5)",
|
|
108
|
+
dropSquare: { backgroundColor: "rgba(100, 150, 255, 0.3)" },
|
|
109
|
+
},
|
|
110
|
+
indicators: {
|
|
111
|
+
move: "rgba(200, 200, 200, 0.2)",
|
|
112
|
+
capture: "rgba(255, 100, 100, 0.3)",
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
function App() {
|
|
117
|
+
return (
|
|
118
|
+
<ChessGame.Root theme={darkTheme}>
|
|
119
|
+
<ChessGame.Board />
|
|
120
|
+
</ChessGame.Root>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Partial Theme Overrides
|
|
126
|
+
|
|
127
|
+
You don't need to specify all theme properties. You can override only the colors you want to change, and the rest will use the default values:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { ChessGame } from "@react-chess-tools/react-chess-game";
|
|
131
|
+
import type { PartialChessGameTheme } from "@react-chess-tools/react-chess-game";
|
|
132
|
+
|
|
133
|
+
// Only override specific colors
|
|
134
|
+
const customTheme: PartialChessGameTheme = {
|
|
135
|
+
state: {
|
|
136
|
+
lastMove: "rgba(147, 112, 219, 0.5)", // Purple
|
|
137
|
+
check: "rgba(255, 165, 0, 0.6)", // Orange
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
function App() {
|
|
142
|
+
return (
|
|
143
|
+
<ChessGame.Root theme={customTheme}>
|
|
144
|
+
<ChessGame.Board />
|
|
145
|
+
</ChessGame.Root>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Theme Utilities
|
|
151
|
+
|
|
152
|
+
### mergeTheme
|
|
153
|
+
|
|
154
|
+
Use `mergeTheme` to merge a partial theme with the default theme:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import {
|
|
158
|
+
mergeTheme,
|
|
159
|
+
defaultGameTheme,
|
|
160
|
+
} from "@react-chess-tools/react-chess-game";
|
|
161
|
+
|
|
162
|
+
const myTheme = mergeTheme({
|
|
163
|
+
state: { lastMove: "rgba(0, 255, 0, 0.5)" },
|
|
164
|
+
});
|
|
165
|
+
// myTheme is now a complete ChessGameTheme with only lastMove changed
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### mergeThemeWith
|
|
169
|
+
|
|
170
|
+
Use `mergeThemeWith` to extend any base theme:
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import {
|
|
174
|
+
mergeThemeWith,
|
|
175
|
+
lichessTheme,
|
|
176
|
+
} from "@react-chess-tools/react-chess-game";
|
|
177
|
+
|
|
178
|
+
const customLichess = mergeThemeWith(lichessTheme, {
|
|
179
|
+
board: {
|
|
180
|
+
lightSquare: { backgroundColor: "#e8e8e8" },
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Using the Theme Hook
|
|
186
|
+
|
|
187
|
+
For advanced use cases, you can access the current theme from any component inside `ChessGame.Root`:
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
import { useChessGameTheme } from "@react-chess-tools/react-chess-game";
|
|
191
|
+
|
|
192
|
+
function CustomComponent() {
|
|
193
|
+
const theme = useChessGameTheme();
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div style={{ backgroundColor: theme.state.lastMove }}>
|
|
197
|
+
Current last move color: {theme.state.lastMove}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Runtime Theme Switching
|
|
204
|
+
|
|
205
|
+
Themes can be switched at runtime by updating the `theme` prop:
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { useState } from "react";
|
|
209
|
+
import { ChessGame, themes } from "@react-chess-tools/react-chess-game";
|
|
210
|
+
|
|
211
|
+
function App() {
|
|
212
|
+
const [currentTheme, setCurrentTheme] = useState(themes.default);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div>
|
|
216
|
+
<ChessGame.Root theme={currentTheme}>
|
|
217
|
+
<ChessGame.Board />
|
|
218
|
+
</ChessGame.Root>
|
|
219
|
+
|
|
220
|
+
<div>
|
|
221
|
+
<button onClick={() => setCurrentTheme(themes.default)}>Default</button>
|
|
222
|
+
<button onClick={() => setCurrentTheme(themes.lichess)}>Lichess</button>
|
|
223
|
+
<button onClick={() => setCurrentTheme(themes.chessCom)}>
|
|
224
|
+
Chess.com
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Default Theme Colors
|
|
233
|
+
|
|
234
|
+
<ColorPalette>
|
|
235
|
+
<ColorItem
|
|
236
|
+
title="Board Colors"
|
|
237
|
+
subtitle="Light and dark squares"
|
|
238
|
+
colors={{
|
|
239
|
+
"Light Square": "#f0d9b5",
|
|
240
|
+
"Dark Square": "#b58863",
|
|
241
|
+
}}
|
|
242
|
+
/>
|
|
243
|
+
<ColorItem
|
|
244
|
+
title="State Colors"
|
|
245
|
+
subtitle="Game state highlights"
|
|
246
|
+
colors={{
|
|
247
|
+
"Last Move": "rgba(255, 255, 0, 0.5)",
|
|
248
|
+
Check: "rgba(255, 0, 0, 0.5)",
|
|
249
|
+
"Active Square": "rgba(255, 255, 0, 0.5)",
|
|
250
|
+
"Drop Target": "rgba(255, 255, 0, 0.4)",
|
|
251
|
+
}}
|
|
252
|
+
/>
|
|
253
|
+
<ColorItem
|
|
254
|
+
title="Indicator Colors"
|
|
255
|
+
subtitle="Move and capture indicators"
|
|
256
|
+
colors={{
|
|
257
|
+
"Move Dot": "rgba(0, 0, 0, 0.1)",
|
|
258
|
+
"Capture Ring": "rgba(1, 0, 0, 0.1)",
|
|
259
|
+
}}
|
|
260
|
+
/>
|
|
261
|
+
</ColorPalette>
|
|
262
|
+
|
|
263
|
+
## TypeScript Support
|
|
264
|
+
|
|
265
|
+
All theme types are fully exported for TypeScript users:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
import type {
|
|
269
|
+
ChessGameTheme,
|
|
270
|
+
PartialChessGameTheme,
|
|
271
|
+
BoardTheme,
|
|
272
|
+
StateTheme,
|
|
273
|
+
IndicatorTheme,
|
|
274
|
+
} from "@react-chess-tools/react-chess-game";
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Next Steps
|
|
278
|
+
|
|
279
|
+
- Try the **[Theme Playground](/story/react-chess-game-theme-playground--playground)** to experiment with colors interactively
|
|
280
|
+
- See **[Theme Presets](/story/react-chess-game-theme-presets--all-presets)** for visual comparison of built-in themes
|
|
281
|
+
- Check out **[Puzzle Theming](/docs/react-chess-puzzle-theming--docs)** for puzzle-specific theme options
|
|
@@ -11,10 +11,22 @@ export const useChessGame = ({
|
|
|
11
11
|
fen,
|
|
12
12
|
orientation: initialOrientation,
|
|
13
13
|
}: useChessGameProps = {}) => {
|
|
14
|
-
const [game, setGame] = React.useState(
|
|
14
|
+
const [game, setGame] = React.useState(() => {
|
|
15
|
+
try {
|
|
16
|
+
return new Chess(fen);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error("Invalid FEN:", fen, e);
|
|
19
|
+
return new Chess(); // Return empty board
|
|
20
|
+
}
|
|
21
|
+
});
|
|
15
22
|
|
|
16
23
|
useEffect(() => {
|
|
17
|
-
|
|
24
|
+
try {
|
|
25
|
+
setGame(new Chess(fen));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error("Invalid FEN:", fen, e);
|
|
28
|
+
setGame(new Chess());
|
|
29
|
+
}
|
|
18
30
|
}, [fen]);
|
|
19
31
|
|
|
20
32
|
const [orientation, setOrientation] = React.useState<Color>(
|
|
@@ -44,11 +56,15 @@ export const useChessGame = ({
|
|
|
44
56
|
);
|
|
45
57
|
|
|
46
58
|
const setPosition = React.useCallback((fen: string, orientation: Color) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
try {
|
|
60
|
+
const newGame = new Chess();
|
|
61
|
+
newGame.load(fen);
|
|
62
|
+
setOrientation(orientation);
|
|
63
|
+
setGame(newGame);
|
|
64
|
+
setCurrentMoveIndex(-1);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.error("Failed to load FEN:", fen, e);
|
|
67
|
+
}
|
|
52
68
|
}, []);
|
|
53
69
|
|
|
54
70
|
const makeMove = React.useCallback(
|
package/src/index.ts
CHANGED
|
@@ -21,3 +21,22 @@ export { deepMergeChessboardOptions } from "./utils/board";
|
|
|
21
21
|
// Component Props
|
|
22
22
|
export type { ChessGameProps } from "./components/ChessGame/parts/Board";
|
|
23
23
|
export type { RootProps } from "./components/ChessGame/parts/Root";
|
|
24
|
+
|
|
25
|
+
// Theme - Types
|
|
26
|
+
export type {
|
|
27
|
+
ChessGameTheme,
|
|
28
|
+
BoardTheme,
|
|
29
|
+
StateTheme,
|
|
30
|
+
IndicatorTheme,
|
|
31
|
+
PartialChessGameTheme,
|
|
32
|
+
DeepPartial,
|
|
33
|
+
} from "./theme/types";
|
|
34
|
+
|
|
35
|
+
// Theme - Values
|
|
36
|
+
export { defaultGameTheme } from "./theme/defaults";
|
|
37
|
+
export { lichessTheme, chessComTheme } from "./theme/presets";
|
|
38
|
+
export { themes } from "./theme";
|
|
39
|
+
|
|
40
|
+
// Theme - Utilities
|
|
41
|
+
export { mergeTheme, mergeThemeWith } from "./theme/utils";
|
|
42
|
+
export { useChessGameTheme, ChessGameThemeContext } from "./theme/context";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { renderHook } from "@testing-library/react";
|
|
3
|
+
import { useChessGameTheme, ThemeProvider } from "../context";
|
|
4
|
+
import { defaultGameTheme } from "../defaults";
|
|
5
|
+
import { lichessTheme } from "../presets";
|
|
6
|
+
|
|
7
|
+
describe("useChessGameTheme", () => {
|
|
8
|
+
it("should return default theme when no provider is present", () => {
|
|
9
|
+
const { result } = renderHook(() => useChessGameTheme());
|
|
10
|
+
expect(result.current).toEqual(defaultGameTheme);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should return provided theme when wrapped in ThemeProvider", () => {
|
|
14
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
15
|
+
<ThemeProvider theme={lichessTheme}>{children}</ThemeProvider>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const { result } = renderHook(() => useChessGameTheme(), { wrapper });
|
|
19
|
+
expect(result.current).toEqual(lichessTheme);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should return custom theme with all properties", () => {
|
|
23
|
+
const customTheme = {
|
|
24
|
+
...defaultGameTheme,
|
|
25
|
+
state: {
|
|
26
|
+
...defaultGameTheme.state,
|
|
27
|
+
lastMove: "rgba(100, 200, 100, 0.6)",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
32
|
+
<ThemeProvider theme={customTheme}>{children}</ThemeProvider>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const { result } = renderHook(() => useChessGameTheme(), { wrapper });
|
|
36
|
+
expect(result.current.state.lastMove).toBe("rgba(100, 200, 100, 0.6)");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("ThemeProvider", () => {
|
|
41
|
+
it("should render children", () => {
|
|
42
|
+
const TestComponent = () => {
|
|
43
|
+
const theme = useChessGameTheme();
|
|
44
|
+
return <div data-testid="theme-check">{theme.state.lastMove}</div>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const { result } = renderHook(() => useChessGameTheme(), {
|
|
48
|
+
wrapper: ({ children }) => (
|
|
49
|
+
<ThemeProvider theme={defaultGameTheme}>{children}</ThemeProvider>
|
|
50
|
+
),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(result.current).toBeTruthy();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should allow nested providers with inner provider winning", () => {
|
|
57
|
+
const outerTheme = {
|
|
58
|
+
...defaultGameTheme,
|
|
59
|
+
state: { ...defaultGameTheme.state, lastMove: "outer" },
|
|
60
|
+
};
|
|
61
|
+
const innerTheme = {
|
|
62
|
+
...defaultGameTheme,
|
|
63
|
+
state: { ...defaultGameTheme.state, lastMove: "inner" },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
67
|
+
<ThemeProvider theme={outerTheme}>
|
|
68
|
+
<ThemeProvider theme={innerTheme}>{children}</ThemeProvider>
|
|
69
|
+
</ThemeProvider>
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const { result } = renderHook(() => useChessGameTheme(), { wrapper });
|
|
73
|
+
expect(result.current.state.lastMove).toBe("inner");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { defaultGameTheme } from "../defaults";
|
|
2
|
+
|
|
3
|
+
describe("defaultGameTheme", () => {
|
|
4
|
+
describe("backward compatibility", () => {
|
|
5
|
+
it("should have the original lastMove color", () => {
|
|
6
|
+
expect(defaultGameTheme.state.lastMove).toBe("rgba(255, 255, 0, 0.5)");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("should have the original check color", () => {
|
|
10
|
+
expect(defaultGameTheme.state.check).toBe("rgba(255, 0, 0, 0.5)");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should have the original activeSquare color", () => {
|
|
14
|
+
expect(defaultGameTheme.state.activeSquare).toBe(
|
|
15
|
+
"rgba(255, 255, 0, 0.5)",
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should have the original dropSquare style", () => {
|
|
20
|
+
expect(defaultGameTheme.state.dropSquare).toEqual({
|
|
21
|
+
backgroundColor: "rgba(255, 255, 0, 0.4)",
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should have the original move indicator color", () => {
|
|
26
|
+
expect(defaultGameTheme.indicators.move).toBe("rgba(0, 0, 0, 0.1)");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should have the original capture indicator color", () => {
|
|
30
|
+
expect(defaultGameTheme.indicators.capture).toBe("rgba(1, 0, 0, 0.1)");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("structure", () => {
|
|
35
|
+
it("should have board property with lightSquare and darkSquare", () => {
|
|
36
|
+
expect(defaultGameTheme.board).toHaveProperty("lightSquare");
|
|
37
|
+
expect(defaultGameTheme.board).toHaveProperty("darkSquare");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should have state property with all required colors", () => {
|
|
41
|
+
expect(defaultGameTheme.state).toHaveProperty("lastMove");
|
|
42
|
+
expect(defaultGameTheme.state).toHaveProperty("check");
|
|
43
|
+
expect(defaultGameTheme.state).toHaveProperty("activeSquare");
|
|
44
|
+
expect(defaultGameTheme.state).toHaveProperty("dropSquare");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should have indicators property with move and capture", () => {
|
|
48
|
+
expect(defaultGameTheme.indicators).toHaveProperty("move");
|
|
49
|
+
expect(defaultGameTheme.indicators).toHaveProperty("capture");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should have valid CSS properties for board squares", () => {
|
|
53
|
+
expect(defaultGameTheme.board.lightSquare).toHaveProperty(
|
|
54
|
+
"backgroundColor",
|
|
55
|
+
);
|
|
56
|
+
expect(defaultGameTheme.board.darkSquare).toHaveProperty(
|
|
57
|
+
"backgroundColor",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { mergeTheme, mergeThemeWith } from "../utils";
|
|
2
|
+
import { defaultGameTheme } from "../defaults";
|
|
3
|
+
import { lichessTheme } from "../presets";
|
|
4
|
+
|
|
5
|
+
describe("mergeTheme", () => {
|
|
6
|
+
it("should return default theme when no partial theme is provided", () => {
|
|
7
|
+
const result = mergeTheme();
|
|
8
|
+
expect(result).toEqual(defaultGameTheme);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should return default theme when undefined is provided", () => {
|
|
12
|
+
const result = mergeTheme(undefined);
|
|
13
|
+
expect(result).toEqual(defaultGameTheme);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should return a new object, not a reference to default", () => {
|
|
17
|
+
const result = mergeTheme();
|
|
18
|
+
expect(result).not.toBe(defaultGameTheme);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should override a single nested property", () => {
|
|
22
|
+
const result = mergeTheme({
|
|
23
|
+
state: { lastMove: "rgba(100, 200, 100, 0.6)" },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result.state.lastMove).toBe("rgba(100, 200, 100, 0.6)");
|
|
27
|
+
// Other state properties should remain default
|
|
28
|
+
expect(result.state.check).toBe(defaultGameTheme.state.check);
|
|
29
|
+
expect(result.state.activeSquare).toBe(defaultGameTheme.state.activeSquare);
|
|
30
|
+
expect(result.state.dropSquare).toEqual(defaultGameTheme.state.dropSquare);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should override multiple properties in different groups", () => {
|
|
34
|
+
const result = mergeTheme({
|
|
35
|
+
state: { lastMove: "rgba(100, 200, 100, 0.6)" },
|
|
36
|
+
board: { lightSquare: { backgroundColor: "#eeeeee" } },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(result.state.lastMove).toBe("rgba(100, 200, 100, 0.6)");
|
|
40
|
+
expect(result.board.lightSquare).toEqual({ backgroundColor: "#eeeeee" });
|
|
41
|
+
// Dark square should remain default
|
|
42
|
+
expect(result.board.darkSquare).toEqual(defaultGameTheme.board.darkSquare);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should override indicator colors", () => {
|
|
46
|
+
const result = mergeTheme({
|
|
47
|
+
indicators: {
|
|
48
|
+
move: "rgba(50, 50, 50, 0.2)",
|
|
49
|
+
capture: "rgba(200, 50, 50, 0.3)",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(result.indicators.move).toBe("rgba(50, 50, 50, 0.2)");
|
|
54
|
+
expect(result.indicators.capture).toBe("rgba(200, 50, 50, 0.3)");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should override dropSquare style completely", () => {
|
|
58
|
+
const result = mergeTheme({
|
|
59
|
+
state: {
|
|
60
|
+
dropSquare: {
|
|
61
|
+
backgroundColor: "rgba(0, 255, 0, 0.5)",
|
|
62
|
+
border: "2px solid green",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result.state.dropSquare).toEqual({
|
|
68
|
+
backgroundColor: "rgba(0, 255, 0, 0.5)",
|
|
69
|
+
border: "2px solid green",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should preserve unmentioned theme properties", () => {
|
|
74
|
+
const result = mergeTheme({
|
|
75
|
+
state: { check: "rgba(255, 100, 100, 0.8)" },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(result.board).toEqual(defaultGameTheme.board);
|
|
79
|
+
expect(result.indicators).toEqual(defaultGameTheme.indicators);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("mergeThemeWith", () => {
|
|
84
|
+
it("should merge partial theme with provided base theme", () => {
|
|
85
|
+
const result = mergeThemeWith(lichessTheme, {
|
|
86
|
+
state: { check: "rgba(255, 0, 0, 0.9)" },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Check should be overridden
|
|
90
|
+
expect(result.state.check).toBe("rgba(255, 0, 0, 0.9)");
|
|
91
|
+
// Other lichess theme values should be preserved
|
|
92
|
+
expect(result.state.lastMove).toBe(lichessTheme.state.lastMove);
|
|
93
|
+
expect(result.board).toEqual(lichessTheme.board);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should return copy of base theme when no partial is provided", () => {
|
|
97
|
+
const result = mergeThemeWith(lichessTheme);
|
|
98
|
+
expect(result).toEqual(lichessTheme);
|
|
99
|
+
expect(result).not.toBe(lichessTheme);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should return copy of base theme when undefined partial is provided", () => {
|
|
103
|
+
const result = mergeThemeWith(lichessTheme, undefined);
|
|
104
|
+
expect(result).toEqual(lichessTheme);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
import type { ChessGameTheme } from "./types";
|
|
3
|
+
import { defaultGameTheme } from "./defaults";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Context for ChessGame theme
|
|
7
|
+
*/
|
|
8
|
+
export const ChessGameThemeContext =
|
|
9
|
+
createContext<ChessGameTheme>(defaultGameTheme);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hook to access the current ChessGame theme.
|
|
13
|
+
* Returns the default theme if no ThemeProvider is present.
|
|
14
|
+
*/
|
|
15
|
+
export const useChessGameTheme = (): ChessGameTheme => {
|
|
16
|
+
return useContext(ChessGameThemeContext);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface ThemeProviderProps {
|
|
20
|
+
theme: ChessGameTheme;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Internal provider component used by Root when a theme prop is provided.
|
|
26
|
+
* This is not exported directly - users pass theme via Root's theme prop.
|
|
27
|
+
*/
|
|
28
|
+
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
29
|
+
theme,
|
|
30
|
+
children,
|
|
31
|
+
}) => {
|
|
32
|
+
return (
|
|
33
|
+
<ChessGameThemeContext.Provider value={theme}>
|
|
34
|
+
{children}
|
|
35
|
+
</ChessGameThemeContext.Provider>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ChessGameTheme } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default theme for ChessGame component.
|
|
5
|
+
* These values match the original hardcoded colors for backward compatibility.
|
|
6
|
+
*/
|
|
7
|
+
export const defaultGameTheme: ChessGameTheme = {
|
|
8
|
+
board: {
|
|
9
|
+
lightSquare: { backgroundColor: "#f0d9b5" },
|
|
10
|
+
darkSquare: { backgroundColor: "#b58863" },
|
|
11
|
+
},
|
|
12
|
+
state: {
|
|
13
|
+
lastMove: "rgba(255, 255, 0, 0.5)",
|
|
14
|
+
check: "rgba(255, 0, 0, 0.5)",
|
|
15
|
+
activeSquare: "rgba(255, 255, 0, 0.5)",
|
|
16
|
+
dropSquare: { backgroundColor: "rgba(255, 255, 0, 0.4)" },
|
|
17
|
+
},
|
|
18
|
+
indicators: {
|
|
19
|
+
move: "rgba(0, 0, 0, 0.1)",
|
|
20
|
+
capture: "rgba(1, 0, 0, 0.1)",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
ChessGameTheme,
|
|
4
|
+
BoardTheme,
|
|
5
|
+
StateTheme,
|
|
6
|
+
IndicatorTheme,
|
|
7
|
+
PartialChessGameTheme,
|
|
8
|
+
DeepPartial,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
// Default theme
|
|
12
|
+
export { defaultGameTheme } from "./defaults";
|
|
13
|
+
|
|
14
|
+
// Preset themes
|
|
15
|
+
export { lichessTheme, chessComTheme } from "./presets";
|
|
16
|
+
|
|
17
|
+
// All themes as a single object
|
|
18
|
+
import { defaultGameTheme } from "./defaults";
|
|
19
|
+
import { lichessTheme, chessComTheme } from "./presets";
|
|
20
|
+
|
|
21
|
+
export const themes = {
|
|
22
|
+
default: defaultGameTheme,
|
|
23
|
+
lichess: lichessTheme,
|
|
24
|
+
chessCom: chessComTheme,
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
// Utilities
|
|
28
|
+
export { mergeTheme, mergeThemeWith } from "./utils";
|
|
29
|
+
|
|
30
|
+
// Context and hook
|
|
31
|
+
export {
|
|
32
|
+
ChessGameThemeContext,
|
|
33
|
+
useChessGameTheme,
|
|
34
|
+
ThemeProvider,
|
|
35
|
+
} from "./context";
|
|
36
|
+
export type { ThemeProviderProps } from "./context";
|