@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/index.cjs +775 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/{index.d.mts → index.d.cts} +118 -12
  5. package/dist/index.d.ts +264 -0
  6. package/dist/{index.mjs → index.js} +171 -38
  7. package/dist/index.js.map +1 -0
  8. package/package.json +18 -9
  9. package/src/components/ChessGame/Theme.stories.tsx +242 -0
  10. package/src/components/ChessGame/ThemePresets.stories.tsx +144 -0
  11. package/src/components/ChessGame/parts/Board.tsx +6 -4
  12. package/src/components/ChessGame/parts/Root.tsx +11 -1
  13. package/src/docs/Theming.mdx +281 -0
  14. package/src/hooks/useChessGame.ts +23 -7
  15. package/src/index.ts +19 -0
  16. package/src/theme/__tests__/context.test.tsx +75 -0
  17. package/src/theme/__tests__/defaults.test.ts +61 -0
  18. package/src/theme/__tests__/utils.test.ts +106 -0
  19. package/src/theme/context.tsx +37 -0
  20. package/src/theme/defaults.ts +22 -0
  21. package/src/theme/index.ts +36 -0
  22. package/src/theme/presets.ts +41 -0
  23. package/src/theme/types.ts +56 -0
  24. package/src/theme/utils.ts +47 -0
  25. package/src/utils/__tests__/board.test.ts +118 -0
  26. package/src/utils/board.ts +18 -9
  27. package/src/utils/chess.ts +25 -5
  28. package/coverage/clover.xml +0 -6
  29. package/coverage/coverage-final.json +0 -1
  30. package/coverage/lcov-report/base.css +0 -224
  31. package/coverage/lcov-report/block-navigation.js +0 -87
  32. package/coverage/lcov-report/favicon.png +0 -0
  33. package/coverage/lcov-report/index.html +0 -101
  34. package/coverage/lcov-report/prettify.css +0 -1
  35. package/coverage/lcov-report/prettify.js +0 -2
  36. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  37. package/coverage/lcov-report/sorter.js +0 -196
  38. package/coverage/lcov.info +0 -0
  39. 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(new Chess(fen));
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
- setGame(new Chess(fen));
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
- const newGame = new Chess();
48
- newGame.load(fen);
49
- setOrientation(orientation);
50
- setGame(newGame);
51
- setCurrentMoveIndex(-1);
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";