@react-chess-tools/react-chess-puzzle 1.0.4 → 1.0.6
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 +16 -0
- package/README.md +38 -0
- package/package.json +2 -2
- package/src/components/ChessPuzzle/ChessPuzzle.stories.tsx +98 -336
- package/src/components/ChessPuzzle/ThemePuzzle.stories.tsx +114 -170
- package/src/docs/Theming.mdx +0 -255
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @react-chess-tools/react-chess-puzzle
|
|
2
2
|
|
|
3
|
+
## 1.0.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- df8dc01: chore: upgrade dependencies
|
|
8
|
+
- Updated dependencies [df8dc01]
|
|
9
|
+
- @react-chess-tools/react-chess-game@1.0.5
|
|
10
|
+
|
|
11
|
+
## 1.0.5
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 5b74626: docs: add comprehensive documentation and Storybook redesign
|
|
16
|
+
- Updated dependencies [5b74626]
|
|
17
|
+
- @react-chess-tools/react-chess-game@1.0.4
|
|
18
|
+
|
|
3
19
|
## 1.0.4
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -45,6 +45,44 @@
|
|
|
45
45
|
- **TypeScript** - Full TypeScript support with comprehensive type definitions
|
|
46
46
|
- **Multiple solutions** - Accept any checkmate as a solution (configurable via `solveOnCheckmate`)
|
|
47
47
|
|
|
48
|
+
## Styling
|
|
49
|
+
|
|
50
|
+
All components accept standard HTML attributes (`className`, `style`, `id`, `data-*`, `aria-*`), making them compatible with any CSS approach:
|
|
51
|
+
|
|
52
|
+
### Tailwind CSS
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
const puzzle = {
|
|
56
|
+
fen: "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3",
|
|
57
|
+
moves: ["d2d4", "e5d4", "f3d4"],
|
|
58
|
+
makeFirstMove: false,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
<ChessPuzzle.Root puzzle={puzzle}>
|
|
62
|
+
<ChessPuzzle.Board className="rounded-lg shadow-lg" />
|
|
63
|
+
<div className="flex gap-2 mt-4">
|
|
64
|
+
<ChessPuzzle.Reset className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
|
|
65
|
+
Try Again
|
|
66
|
+
</ChessPuzzle.Reset>
|
|
67
|
+
<ChessPuzzle.Hint className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">
|
|
68
|
+
Show Hint
|
|
69
|
+
</ChessPuzzle.Hint>
|
|
70
|
+
</div>
|
|
71
|
+
</ChessPuzzle.Root>;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### CSS Modules
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import styles from "./Puzzle.module.css";
|
|
78
|
+
|
|
79
|
+
<ChessPuzzle.Board className={styles.board} />;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Custom Theme
|
|
83
|
+
|
|
84
|
+
See the [ChessPuzzle.Root props](#chesspuzzleroot) for the `theme` prop to customize puzzle-specific colors (success, failure, hint).
|
|
85
|
+
|
|
48
86
|
## Installation
|
|
49
87
|
|
|
50
88
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-chess-tools/react-chess-puzzle",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "A lightweight, customizable React component library for rendering and interacting with chess puzzles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"license": "MIT",
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@radix-ui/react-slot": "^1.2.4",
|
|
45
|
-
"@react-chess-tools/react-chess-game": "1.0.
|
|
45
|
+
"@react-chess-tools/react-chess-game": "1.0.5",
|
|
46
46
|
"chess.js": "^1.4.0",
|
|
47
47
|
"lodash": "^4.17.21"
|
|
48
48
|
},
|
|
@@ -4,6 +4,13 @@ import React from "react";
|
|
|
4
4
|
import { RootProps } from "./parts/Root";
|
|
5
5
|
import { ChessPuzzle } from ".";
|
|
6
6
|
import { ChessGame } from "@react-chess-tools/react-chess-game";
|
|
7
|
+
import {
|
|
8
|
+
StoryHeader,
|
|
9
|
+
StoryContainer,
|
|
10
|
+
BoardWrapper,
|
|
11
|
+
Kbd,
|
|
12
|
+
Button,
|
|
13
|
+
} from "@story-helpers";
|
|
7
14
|
|
|
8
15
|
const puzzles = [
|
|
9
16
|
{
|
|
@@ -18,108 +25,8 @@ const puzzles = [
|
|
|
18
25
|
},
|
|
19
26
|
];
|
|
20
27
|
|
|
21
|
-
// ============================================================================
|
|
22
|
-
// Shared Story Styles
|
|
23
|
-
// ============================================================================
|
|
24
|
-
const storyStyles = {
|
|
25
|
-
container: {
|
|
26
|
-
display: "flex",
|
|
27
|
-
flexDirection: "column" as const,
|
|
28
|
-
alignItems: "center",
|
|
29
|
-
gap: "20px",
|
|
30
|
-
padding: "24px",
|
|
31
|
-
backgroundColor: "#f8f9fa",
|
|
32
|
-
borderRadius: "12px",
|
|
33
|
-
maxWidth: "500px",
|
|
34
|
-
margin: "0 auto",
|
|
35
|
-
},
|
|
36
|
-
header: {
|
|
37
|
-
display: "flex",
|
|
38
|
-
flexDirection: "column" as const,
|
|
39
|
-
alignItems: "center",
|
|
40
|
-
gap: "8px",
|
|
41
|
-
},
|
|
42
|
-
title: {
|
|
43
|
-
fontSize: "22px",
|
|
44
|
-
fontWeight: 700,
|
|
45
|
-
color: "#2c3e50",
|
|
46
|
-
margin: 0,
|
|
47
|
-
textAlign: "center" as const,
|
|
48
|
-
},
|
|
49
|
-
subtitle: {
|
|
50
|
-
fontSize: "14px",
|
|
51
|
-
color: "#6c757d",
|
|
52
|
-
margin: 0,
|
|
53
|
-
textAlign: "center" as const,
|
|
54
|
-
},
|
|
55
|
-
boardWrapper: {
|
|
56
|
-
backgroundColor: "#fff",
|
|
57
|
-
padding: "16px",
|
|
58
|
-
borderRadius: "10px",
|
|
59
|
-
boxShadow: "0 2px 12px rgba(0,0,0,0.08)",
|
|
60
|
-
},
|
|
61
|
-
controlsSection: {
|
|
62
|
-
display: "flex",
|
|
63
|
-
gap: "10px",
|
|
64
|
-
justifyContent: "center",
|
|
65
|
-
flexWrap: "wrap" as const,
|
|
66
|
-
},
|
|
67
|
-
button: {
|
|
68
|
-
padding: "10px 20px",
|
|
69
|
-
fontSize: "14px",
|
|
70
|
-
fontWeight: 600,
|
|
71
|
-
cursor: "pointer",
|
|
72
|
-
border: "none",
|
|
73
|
-
borderRadius: "8px",
|
|
74
|
-
backgroundColor: "#fff",
|
|
75
|
-
boxShadow: "0 2px 6px rgba(0,0,0,0.08)",
|
|
76
|
-
color: "#495057",
|
|
77
|
-
transition: "all 0.2s ease",
|
|
78
|
-
} as React.CSSProperties,
|
|
79
|
-
buttonPrimary: {
|
|
80
|
-
backgroundColor: "#4dabf7",
|
|
81
|
-
color: "#fff",
|
|
82
|
-
boxShadow: "0 2px 6px rgba(77, 171, 247, 0.3)",
|
|
83
|
-
} as React.CSSProperties,
|
|
84
|
-
buttonSuccess: {
|
|
85
|
-
backgroundColor: "#51cf66",
|
|
86
|
-
color: "#fff",
|
|
87
|
-
boxShadow: "0 2px 6px rgba(81, 207, 102, 0.3)",
|
|
88
|
-
} as React.CSSProperties,
|
|
89
|
-
hintButton: {
|
|
90
|
-
padding: "8px 16px",
|
|
91
|
-
fontSize: "13px",
|
|
92
|
-
fontWeight: 500,
|
|
93
|
-
cursor: "pointer",
|
|
94
|
-
border: "1px dashed #adb5bd",
|
|
95
|
-
borderRadius: "8px",
|
|
96
|
-
backgroundColor: "transparent",
|
|
97
|
-
color: "#868e96",
|
|
98
|
-
} as React.CSSProperties,
|
|
99
|
-
infoBox: {
|
|
100
|
-
padding: "12px 16px",
|
|
101
|
-
backgroundColor: "#e7f5ff",
|
|
102
|
-
borderRadius: "8px",
|
|
103
|
-
fontSize: "13px",
|
|
104
|
-
color: "#1864ab",
|
|
105
|
-
textAlign: "center" as const,
|
|
106
|
-
},
|
|
107
|
-
statusBadge: {
|
|
108
|
-
display: "inline-flex",
|
|
109
|
-
alignItems: "center",
|
|
110
|
-
gap: "6px",
|
|
111
|
-
padding: "6px 14px",
|
|
112
|
-
fontSize: "12px",
|
|
113
|
-
fontWeight: 600,
|
|
114
|
-
backgroundColor: "#e9ecef",
|
|
115
|
-
borderRadius: "20px",
|
|
116
|
-
color: "#495057",
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
|
121
28
|
const meta = {
|
|
122
|
-
title: "react-chess-puzzle/
|
|
29
|
+
title: "Packages/react-chess-puzzle/ChessPuzzle",
|
|
123
30
|
component: ChessPuzzle.Root,
|
|
124
31
|
tags: ["components", "puzzle"],
|
|
125
32
|
argTypes: {
|
|
@@ -127,51 +34,44 @@ const meta = {
|
|
|
127
34
|
onFail: { action: "onFail" },
|
|
128
35
|
},
|
|
129
36
|
parameters: {
|
|
130
|
-
actions: { argTypesRegex: "^_on.*" },
|
|
131
37
|
layout: "centered",
|
|
132
38
|
},
|
|
133
39
|
} satisfies Meta<typeof ChessPuzzle.Root>;
|
|
134
40
|
|
|
135
41
|
export default meta;
|
|
136
42
|
|
|
137
|
-
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
|
138
|
-
|
|
139
43
|
export const Example = (args: RootProps) => {
|
|
140
44
|
const [puzzleIndex, setPuzzleIndex] = React.useState(0);
|
|
141
45
|
const puzzle = puzzles[puzzleIndex];
|
|
142
46
|
return (
|
|
143
47
|
<ChessPuzzle.Root {...args} puzzle={puzzle}>
|
|
144
|
-
<
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
</
|
|
152
|
-
<
|
|
48
|
+
<StoryContainer>
|
|
49
|
+
<StoryHeader
|
|
50
|
+
title="Chess Puzzle"
|
|
51
|
+
subtitle="Find the best move sequence"
|
|
52
|
+
/>
|
|
53
|
+
<span className="inline-flex items-center gap-1.5 px-3.5 py-1.5 text-size-xs font-semibold bg-surface-alt rounded-full text-text-secondary">
|
|
54
|
+
Puzzle {puzzleIndex + 1} of {puzzles.length}
|
|
55
|
+
</span>
|
|
56
|
+
<BoardWrapper>
|
|
153
57
|
<ChessPuzzle.Board />
|
|
154
|
-
</
|
|
155
|
-
<div
|
|
58
|
+
</BoardWrapper>
|
|
59
|
+
<div className="flex gap-2.5 justify-center flex-wrap">
|
|
156
60
|
<ChessPuzzle.Reset asChild>
|
|
157
|
-
<
|
|
61
|
+
<Button variant="outline">Restart</Button>
|
|
158
62
|
</ChessPuzzle.Reset>
|
|
159
63
|
<ChessPuzzle.Reset
|
|
160
64
|
asChild
|
|
161
65
|
puzzle={puzzles[(puzzleIndex + 1) % puzzles.length]}
|
|
162
66
|
onReset={() => setPuzzleIndex((puzzleIndex + 1) % puzzles.length)}
|
|
163
67
|
>
|
|
164
|
-
<
|
|
165
|
-
style={{ ...storyStyles.button, ...storyStyles.buttonPrimary }}
|
|
166
|
-
>
|
|
167
|
-
Next Puzzle
|
|
168
|
-
</button>
|
|
68
|
+
<Button variant="default">Next Puzzle</Button>
|
|
169
69
|
</ChessPuzzle.Reset>
|
|
170
|
-
<ChessPuzzle.Hint
|
|
171
|
-
|
|
70
|
+
<ChessPuzzle.Hint asChild>
|
|
71
|
+
<Button variant="outline">Hint</Button>
|
|
172
72
|
</ChessPuzzle.Hint>
|
|
173
73
|
</div>
|
|
174
|
-
</
|
|
74
|
+
</StoryContainer>
|
|
175
75
|
</ChessPuzzle.Root>
|
|
176
76
|
);
|
|
177
77
|
};
|
|
@@ -184,25 +84,23 @@ export const WithOrientation = (args: RootProps) => {
|
|
|
184
84
|
};
|
|
185
85
|
return (
|
|
186
86
|
<ChessPuzzle.Root {...args} puzzle={puzzle}>
|
|
187
|
-
<
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
</div>
|
|
194
|
-
<div style={storyStyles.boardWrapper}>
|
|
87
|
+
<StoryContainer>
|
|
88
|
+
<StoryHeader
|
|
89
|
+
title="Black to Move"
|
|
90
|
+
subtitle="Board oriented from Black's perspective"
|
|
91
|
+
/>
|
|
92
|
+
<BoardWrapper>
|
|
195
93
|
<ChessPuzzle.Board options={{ boardOrientation: "black" }} />
|
|
196
|
-
</
|
|
197
|
-
<div
|
|
94
|
+
</BoardWrapper>
|
|
95
|
+
<div className="flex gap-2.5 justify-center flex-wrap">
|
|
198
96
|
<ChessPuzzle.Reset asChild>
|
|
199
|
-
<
|
|
97
|
+
<Button variant="outline">Restart</Button>
|
|
200
98
|
</ChessPuzzle.Reset>
|
|
201
|
-
<ChessPuzzle.Hint
|
|
202
|
-
|
|
99
|
+
<ChessPuzzle.Hint asChild>
|
|
100
|
+
<Button variant="outline">Hint</Button>
|
|
203
101
|
</ChessPuzzle.Hint>
|
|
204
102
|
</div>
|
|
205
|
-
</
|
|
103
|
+
</StoryContainer>
|
|
206
104
|
</ChessPuzzle.Root>
|
|
207
105
|
);
|
|
208
106
|
};
|
|
@@ -215,29 +113,23 @@ export const Underpromotion = (args: RootProps) => {
|
|
|
215
113
|
};
|
|
216
114
|
return (
|
|
217
115
|
<ChessPuzzle.Root {...args} puzzle={puzzle}>
|
|
218
|
-
<
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
</div>
|
|
225
|
-
<div style={storyStyles.boardWrapper}>
|
|
116
|
+
<StoryContainer>
|
|
117
|
+
<StoryHeader
|
|
118
|
+
title="Underpromotion Challenge"
|
|
119
|
+
subtitle="Promote to a knight instead of a queen"
|
|
120
|
+
/>
|
|
121
|
+
<BoardWrapper>
|
|
226
122
|
<ChessPuzzle.Board />
|
|
227
|
-
</
|
|
228
|
-
<div
|
|
123
|
+
</BoardWrapper>
|
|
124
|
+
<div className="flex gap-2.5 justify-center flex-wrap">
|
|
229
125
|
<ChessPuzzle.Reset asChild>
|
|
230
|
-
<
|
|
231
|
-
style={{ ...storyStyles.button, ...storyStyles.buttonSuccess }}
|
|
232
|
-
>
|
|
233
|
-
✓ Solved! Restart
|
|
234
|
-
</button>
|
|
126
|
+
<Button variant="default">Solved! Restart</Button>
|
|
235
127
|
</ChessPuzzle.Reset>
|
|
236
128
|
</div>
|
|
237
|
-
<div
|
|
129
|
+
<div className="px-4 py-3 bg-info border border-info-border rounded-md text-size-sm text-info-text text-center">
|
|
238
130
|
Sometimes promoting to a knight is better than a queen!
|
|
239
131
|
</div>
|
|
240
|
-
</
|
|
132
|
+
</StoryContainer>
|
|
241
133
|
</ChessPuzzle.Root>
|
|
242
134
|
);
|
|
243
135
|
};
|
|
@@ -246,18 +138,18 @@ export const WithSounds = (args: RootProps) => {
|
|
|
246
138
|
return (
|
|
247
139
|
<ChessPuzzle.Root {...args} puzzle={puzzles[0]}>
|
|
248
140
|
<ChessGame.Sounds />
|
|
249
|
-
<
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
<
|
|
141
|
+
<StoryContainer>
|
|
142
|
+
<StoryHeader
|
|
143
|
+
title="Puzzle with Sound"
|
|
144
|
+
subtitle="Audio feedback on every move"
|
|
145
|
+
/>
|
|
146
|
+
<BoardWrapper>
|
|
255
147
|
<ChessPuzzle.Board />
|
|
256
|
-
</
|
|
257
|
-
<p
|
|
148
|
+
</BoardWrapper>
|
|
149
|
+
<p className="text-size-xs text-text-muted text-center m-0">
|
|
258
150
|
Move pieces to hear different sounds
|
|
259
151
|
</p>
|
|
260
|
-
</
|
|
152
|
+
</StoryContainer>
|
|
261
153
|
</ChessPuzzle.Root>
|
|
262
154
|
);
|
|
263
155
|
};
|
|
@@ -274,186 +166,64 @@ export const WithKeyboardControls = (args: RootProps) => {
|
|
|
274
166
|
d: (context) => context.methods.goToNextMove(),
|
|
275
167
|
}}
|
|
276
168
|
/>
|
|
277
|
-
<
|
|
278
|
-
<
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
<
|
|
169
|
+
<StoryContainer>
|
|
170
|
+
<StoryHeader
|
|
171
|
+
title="Keyboard Navigation"
|
|
172
|
+
subtitle="Use keyboard shortcuts to navigate"
|
|
173
|
+
/>
|
|
174
|
+
<BoardWrapper>
|
|
283
175
|
<ChessPuzzle.Board />
|
|
284
|
-
</
|
|
285
|
-
<div
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
gridTemplateColumns: "repeat(3, auto)",
|
|
289
|
-
gap: "8px",
|
|
290
|
-
justifyContent: "center",
|
|
291
|
-
marginTop: "12px",
|
|
292
|
-
}}
|
|
293
|
-
>
|
|
294
|
-
<div
|
|
295
|
-
style={{
|
|
296
|
-
display: "flex",
|
|
297
|
-
alignItems: "center",
|
|
298
|
-
gap: "6px",
|
|
299
|
-
fontSize: "12px",
|
|
300
|
-
color: "#495057",
|
|
301
|
-
}}
|
|
302
|
-
>
|
|
303
|
-
<kbd
|
|
304
|
-
style={{
|
|
305
|
-
padding: "2px 8px",
|
|
306
|
-
backgroundColor: "#e9ecef",
|
|
307
|
-
border: "1px solid #ced4da",
|
|
308
|
-
borderRadius: "4px",
|
|
309
|
-
fontFamily: "monospace",
|
|
310
|
-
fontSize: "11px",
|
|
311
|
-
fontWeight: 600,
|
|
312
|
-
}}
|
|
313
|
-
>
|
|
314
|
-
W
|
|
315
|
-
</kbd>{" "}
|
|
316
|
-
Start
|
|
176
|
+
</BoardWrapper>
|
|
177
|
+
<div className="grid grid-cols-3 gap-2 justify-center mt-3">
|
|
178
|
+
<div className="flex items-center gap-1.5 text-size-xs text-text">
|
|
179
|
+
<Kbd>W</Kbd> Start
|
|
317
180
|
</div>
|
|
318
|
-
<div
|
|
319
|
-
|
|
320
|
-
display: "flex",
|
|
321
|
-
alignItems: "center",
|
|
322
|
-
gap: "6px",
|
|
323
|
-
fontSize: "12px",
|
|
324
|
-
color: "#495057",
|
|
325
|
-
}}
|
|
326
|
-
>
|
|
327
|
-
<kbd
|
|
328
|
-
style={{
|
|
329
|
-
padding: "2px 8px",
|
|
330
|
-
backgroundColor: "#e9ecef",
|
|
331
|
-
border: "1px solid #ced4da",
|
|
332
|
-
borderRadius: "4px",
|
|
333
|
-
fontFamily: "monospace",
|
|
334
|
-
fontSize: "11px",
|
|
335
|
-
fontWeight: 600,
|
|
336
|
-
}}
|
|
337
|
-
>
|
|
338
|
-
A
|
|
339
|
-
</kbd>{" "}
|
|
340
|
-
Previous
|
|
181
|
+
<div className="flex items-center gap-1.5 text-size-xs text-text">
|
|
182
|
+
<Kbd>A</Kbd> Previous
|
|
341
183
|
</div>
|
|
342
|
-
<div
|
|
343
|
-
|
|
344
|
-
display: "flex",
|
|
345
|
-
alignItems: "center",
|
|
346
|
-
gap: "6px",
|
|
347
|
-
fontSize: "12px",
|
|
348
|
-
color: "#495057",
|
|
349
|
-
}}
|
|
350
|
-
>
|
|
351
|
-
<kbd
|
|
352
|
-
style={{
|
|
353
|
-
padding: "2px 8px",
|
|
354
|
-
backgroundColor: "#e9ecef",
|
|
355
|
-
border: "1px solid #ced4da",
|
|
356
|
-
borderRadius: "4px",
|
|
357
|
-
fontFamily: "monospace",
|
|
358
|
-
fontSize: "11px",
|
|
359
|
-
fontWeight: 600,
|
|
360
|
-
}}
|
|
361
|
-
>
|
|
362
|
-
F
|
|
363
|
-
</kbd>{" "}
|
|
364
|
-
Flip
|
|
184
|
+
<div className="flex items-center gap-1.5 text-size-xs text-text">
|
|
185
|
+
<Kbd>F</Kbd> Flip
|
|
365
186
|
</div>
|
|
366
|
-
<div
|
|
367
|
-
|
|
368
|
-
display: "flex",
|
|
369
|
-
alignItems: "center",
|
|
370
|
-
gap: "6px",
|
|
371
|
-
fontSize: "12px",
|
|
372
|
-
color: "#495057",
|
|
373
|
-
}}
|
|
374
|
-
>
|
|
375
|
-
<kbd
|
|
376
|
-
style={{
|
|
377
|
-
padding: "2px 8px",
|
|
378
|
-
backgroundColor: "#e9ecef",
|
|
379
|
-
border: "1px solid #ced4da",
|
|
380
|
-
borderRadius: "4px",
|
|
381
|
-
fontFamily: "monospace",
|
|
382
|
-
fontSize: "11px",
|
|
383
|
-
fontWeight: 600,
|
|
384
|
-
}}
|
|
385
|
-
>
|
|
386
|
-
S
|
|
387
|
-
</kbd>{" "}
|
|
388
|
-
End
|
|
187
|
+
<div className="flex items-center gap-1.5 text-size-xs text-text">
|
|
188
|
+
<Kbd>S</Kbd> End
|
|
389
189
|
</div>
|
|
390
|
-
<div
|
|
391
|
-
|
|
392
|
-
display: "flex",
|
|
393
|
-
alignItems: "center",
|
|
394
|
-
gap: "6px",
|
|
395
|
-
fontSize: "12px",
|
|
396
|
-
color: "#495057",
|
|
397
|
-
}}
|
|
398
|
-
>
|
|
399
|
-
<kbd
|
|
400
|
-
style={{
|
|
401
|
-
padding: "2px 8px",
|
|
402
|
-
backgroundColor: "#e9ecef",
|
|
403
|
-
border: "1px solid #ced4da",
|
|
404
|
-
borderRadius: "4px",
|
|
405
|
-
fontFamily: "monospace",
|
|
406
|
-
fontSize: "11px",
|
|
407
|
-
fontWeight: 600,
|
|
408
|
-
}}
|
|
409
|
-
>
|
|
410
|
-
D
|
|
411
|
-
</kbd>{" "}
|
|
412
|
-
Next
|
|
190
|
+
<div className="flex items-center gap-1.5 text-size-xs text-text">
|
|
191
|
+
<Kbd>D</Kbd> Next
|
|
413
192
|
</div>
|
|
414
193
|
</div>
|
|
415
|
-
</
|
|
194
|
+
</StoryContainer>
|
|
416
195
|
</ChessPuzzle.Root>
|
|
417
196
|
);
|
|
418
197
|
};
|
|
419
198
|
|
|
420
|
-
// Puzzle with multiple checkmate solutions for testing solveOnCheckmate prop
|
|
421
199
|
const multiMatePuzzle = {
|
|
422
200
|
fen: "7k/R7/1R6/2Q5/4Q3/8/8/7K w - - 0 1",
|
|
423
|
-
moves: ["a7a8"],
|
|
201
|
+
moves: ["a7a8"],
|
|
424
202
|
makeFirstMove: false,
|
|
425
203
|
};
|
|
426
204
|
|
|
427
205
|
export const MultiMatePuzzle = (args: RootProps) => {
|
|
428
206
|
return (
|
|
429
207
|
<ChessPuzzle.Root {...args} puzzle={multiMatePuzzle}>
|
|
430
|
-
<
|
|
431
|
-
<
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
</div>
|
|
437
|
-
<div
|
|
438
|
-
style={{
|
|
439
|
-
...storyStyles.infoBox,
|
|
440
|
-
backgroundColor: "#d3f9d8",
|
|
441
|
-
color: "#2b8a3e",
|
|
442
|
-
}}
|
|
443
|
-
>
|
|
208
|
+
<StoryContainer>
|
|
209
|
+
<StoryHeader
|
|
210
|
+
title="Flexible Checkmate"
|
|
211
|
+
subtitle="Any checkmate move solves the puzzle"
|
|
212
|
+
/>
|
|
213
|
+
<div className="px-4 py-3 bg-success-bg border border-success rounded-md text-size-sm text-success-text text-center">
|
|
444
214
|
<strong>solveOnCheckmate=true (default)</strong>
|
|
445
215
|
<br />
|
|
446
216
|
Try Qc8#, Qf8#, Rb8#, or the canonical Ra8#
|
|
447
217
|
</div>
|
|
448
|
-
<
|
|
218
|
+
<BoardWrapper>
|
|
449
219
|
<ChessPuzzle.Board />
|
|
450
|
-
</
|
|
451
|
-
<div
|
|
220
|
+
</BoardWrapper>
|
|
221
|
+
<div className="flex gap-2.5 justify-center flex-wrap">
|
|
452
222
|
<ChessPuzzle.Reset asChild>
|
|
453
|
-
<
|
|
223
|
+
<Button variant="outline">Restart</Button>
|
|
454
224
|
</ChessPuzzle.Reset>
|
|
455
225
|
</div>
|
|
456
|
-
</
|
|
226
|
+
</StoryContainer>
|
|
457
227
|
</ChessPuzzle.Root>
|
|
458
228
|
);
|
|
459
229
|
};
|
|
@@ -465,33 +235,25 @@ export const MultiMatePuzzleStrict = (args: RootProps) => {
|
|
|
465
235
|
puzzle={multiMatePuzzle}
|
|
466
236
|
solveOnCheckmate={false}
|
|
467
237
|
>
|
|
468
|
-
<
|
|
469
|
-
<
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
</div>
|
|
475
|
-
<div
|
|
476
|
-
style={{
|
|
477
|
-
...storyStyles.infoBox,
|
|
478
|
-
backgroundColor: "#ffe3e3",
|
|
479
|
-
color: "#c92a2a",
|
|
480
|
-
}}
|
|
481
|
-
>
|
|
238
|
+
<StoryContainer>
|
|
239
|
+
<StoryHeader
|
|
240
|
+
title="Strict Checkmate"
|
|
241
|
+
subtitle="Only the canonical solution is accepted"
|
|
242
|
+
/>
|
|
243
|
+
<div className="px-4 py-3 bg-danger-bg border border-danger rounded-md text-size-sm text-danger-text text-center">
|
|
482
244
|
<strong>solveOnCheckmate=false</strong>
|
|
483
245
|
<br />
|
|
484
246
|
Only Ra8# is accepted. Alternative mates like Qc8# will fail!
|
|
485
247
|
</div>
|
|
486
|
-
<
|
|
248
|
+
<BoardWrapper>
|
|
487
249
|
<ChessPuzzle.Board />
|
|
488
|
-
</
|
|
489
|
-
<div
|
|
250
|
+
</BoardWrapper>
|
|
251
|
+
<div className="flex gap-2.5 justify-center flex-wrap">
|
|
490
252
|
<ChessPuzzle.Reset asChild>
|
|
491
|
-
<
|
|
253
|
+
<Button variant="outline">Restart</Button>
|
|
492
254
|
</ChessPuzzle.Reset>
|
|
493
255
|
</div>
|
|
494
|
-
</
|
|
256
|
+
</StoryContainer>
|
|
495
257
|
</ChessPuzzle.Root>
|
|
496
258
|
);
|
|
497
259
|
};
|
|
@@ -3,19 +3,20 @@ import React, { useState } from "react";
|
|
|
3
3
|
import { ChessPuzzle } from "./index";
|
|
4
4
|
import { defaultPuzzleTheme } from "../../theme/defaults";
|
|
5
5
|
import type { ChessPuzzleTheme } from "../../theme/types";
|
|
6
|
+
import { ColorInput, copyToClipboard } from "@story-helpers";
|
|
6
7
|
|
|
7
|
-
const meta = {
|
|
8
|
-
title: "react-chess-puzzle/
|
|
8
|
+
const meta: Meta<typeof ChessPuzzle.Root> = {
|
|
9
|
+
title: "Packages/react-chess-puzzle/Theming/Playground",
|
|
9
10
|
component: ChessPuzzle.Root,
|
|
10
11
|
tags: ["theme", "puzzle"],
|
|
11
12
|
decorators: [
|
|
12
13
|
(Story) => (
|
|
13
|
-
<div
|
|
14
|
+
<div className="max-w-story-xl">
|
|
14
15
|
<Story />
|
|
15
16
|
</div>
|
|
16
17
|
),
|
|
17
18
|
],
|
|
18
|
-
}
|
|
19
|
+
};
|
|
19
20
|
|
|
20
21
|
export default meta;
|
|
21
22
|
|
|
@@ -25,60 +26,10 @@ const samplePuzzle = {
|
|
|
25
26
|
makeFirstMove: false,
|
|
26
27
|
};
|
|
27
28
|
|
|
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
29
|
export const PuzzlePlayground = () => {
|
|
79
30
|
const [theme, setTheme] = useState<ChessPuzzleTheme>(defaultPuzzleTheme);
|
|
80
31
|
const [copied, setCopied] = useState(false);
|
|
81
|
-
const [
|
|
32
|
+
const [copyError, setCopyError] = useState(false);
|
|
82
33
|
|
|
83
34
|
const updatePuzzleColor = (
|
|
84
35
|
key: keyof ChessPuzzleTheme["puzzle"],
|
|
@@ -93,105 +44,109 @@ export const PuzzlePlayground = () => {
|
|
|
93
44
|
}));
|
|
94
45
|
};
|
|
95
46
|
|
|
96
|
-
const
|
|
97
|
-
const themeCode = `const myPuzzleTheme: PartialChessPuzzleTheme = {
|
|
47
|
+
const themeCode = `const myPuzzleTheme: PartialChessPuzzleTheme = {
|
|
98
48
|
puzzle: ${JSON.stringify(theme.puzzle, null, 4)}
|
|
99
49
|
};`;
|
|
100
|
-
navigator.clipboard.writeText(themeCode);
|
|
101
|
-
setCopied(true);
|
|
102
|
-
setTimeout(() => setCopied(false), 2000);
|
|
103
|
-
};
|
|
104
50
|
|
|
105
|
-
const
|
|
106
|
-
|
|
51
|
+
const handleCopy = async () => {
|
|
52
|
+
const success = await copyToClipboard(themeCode);
|
|
53
|
+
if (success) {
|
|
54
|
+
setCopied(true);
|
|
55
|
+
setCopyError(false);
|
|
56
|
+
setTimeout(() => setCopied(false), 2000);
|
|
57
|
+
} else {
|
|
58
|
+
setCopyError(true);
|
|
59
|
+
setTimeout(() => setCopyError(false), 3000);
|
|
60
|
+
}
|
|
107
61
|
};
|
|
108
62
|
|
|
109
63
|
return (
|
|
110
|
-
<div
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<div
|
|
115
|
-
<
|
|
116
|
-
<div
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
64
|
+
<div className="flex flex-col gap-6 font-sans">
|
|
65
|
+
{/* Top row: Preview + Controls */}
|
|
66
|
+
<div className="flex gap-6 items-start">
|
|
67
|
+
{/* Preview */}
|
|
68
|
+
<div className="flex flex-col gap-2">
|
|
69
|
+
<h3 className="text-size-sm font-semibold text-text">Preview</h3>
|
|
70
|
+
<div className="max-w-story-lg">
|
|
71
|
+
<ChessPuzzle.Root puzzle={samplePuzzle} theme={theme}>
|
|
72
|
+
<ChessPuzzle.Board />
|
|
73
|
+
<div className="mt-2 flex gap-2">
|
|
74
|
+
<ChessPuzzle.Hint asChild>
|
|
75
|
+
<button className="py-1 px-3 text-size-xs border border-border rounded bg-surface hover:bg-surface-alt">
|
|
76
|
+
Hint
|
|
77
|
+
</button>
|
|
78
|
+
</ChessPuzzle.Hint>
|
|
79
|
+
<ChessPuzzle.Reset asChild>
|
|
80
|
+
<button className="py-1 px-3 text-size-xs border border-border rounded bg-surface hover:bg-surface-alt">
|
|
81
|
+
Reset
|
|
82
|
+
</button>
|
|
83
|
+
</ChessPuzzle.Reset>
|
|
84
|
+
</div>
|
|
85
|
+
</ChessPuzzle.Root>
|
|
132
86
|
</div>
|
|
87
|
+
<p className="text-size-xs text-text-muted">
|
|
88
|
+
Solution: Bxd7+, Nxd7, Qb8+, Nxb8, Rd8#
|
|
89
|
+
</p>
|
|
133
90
|
</div>
|
|
134
91
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
>
|
|
160
|
-
Reset Puzzle
|
|
161
|
-
</button>
|
|
162
|
-
</div>
|
|
92
|
+
{/* Controls */}
|
|
93
|
+
<div className="flex flex-col gap-3 flex-1">
|
|
94
|
+
<h3 className="text-size-sm font-semibold text-text">
|
|
95
|
+
Puzzle Colors
|
|
96
|
+
</h3>
|
|
97
|
+
|
|
98
|
+
<div className="p-3 bg-surface-alt rounded border border-border">
|
|
99
|
+
<div className="space-y-2">
|
|
100
|
+
<ColorInput
|
|
101
|
+
label="Success"
|
|
102
|
+
value={theme.puzzle.success}
|
|
103
|
+
onChange={(v) => updatePuzzleColor("success", v)}
|
|
104
|
+
/>
|
|
105
|
+
<ColorInput
|
|
106
|
+
label="Failure"
|
|
107
|
+
value={theme.puzzle.failure}
|
|
108
|
+
onChange={(v) => updatePuzzleColor("failure", v)}
|
|
109
|
+
/>
|
|
110
|
+
<ColorInput
|
|
111
|
+
label="Hint"
|
|
112
|
+
value={theme.puzzle.hint}
|
|
113
|
+
onChange={(v) => updatePuzzleColor("hint", v)}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
163
117
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
<li>Click "Reset" to try again</li>
|
|
173
|
-
</ul>
|
|
118
|
+
<div className="p-3 bg-surface-alt rounded border border-border text-size-xs text-text-muted">
|
|
119
|
+
<p className="font-semibold text-text mb-2">How to test:</p>
|
|
120
|
+
<ul className="pl-4 space-y-1">
|
|
121
|
+
<li>Click "Hint" to see the hint color</li>
|
|
122
|
+
<li>Make a correct move to see success color</li>
|
|
123
|
+
<li>Make a wrong move to see failure color</li>
|
|
124
|
+
</ul>
|
|
125
|
+
</div>
|
|
174
126
|
</div>
|
|
175
127
|
</div>
|
|
176
128
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
129
|
+
{/* Bottom: Generated Code */}
|
|
130
|
+
<div className="flex flex-col gap-2">
|
|
131
|
+
<h3 className="text-size-sm font-semibold text-text">Generated Code</h3>
|
|
132
|
+
<div className="relative">
|
|
133
|
+
<pre className="text-size-xs font-mono bg-surface-alt p-4 rounded border border-border overflow-auto text-text">
|
|
134
|
+
{themeCode}
|
|
135
|
+
</pre>
|
|
136
|
+
<button
|
|
137
|
+
onClick={handleCopy}
|
|
138
|
+
className={`absolute top-3 right-3 px-2 py-1 text-size-xs rounded ${
|
|
139
|
+
copyError
|
|
140
|
+
? "bg-danger text-white"
|
|
141
|
+
: copied
|
|
142
|
+
? "bg-success text-white"
|
|
143
|
+
: "bg-accent text-white hover:opacity-90"
|
|
144
|
+
}`}
|
|
145
|
+
aria-label="Copy theme code"
|
|
146
|
+
>
|
|
147
|
+
{copyError ? "Failed" : copied ? "Copied!" : "Copy"}
|
|
148
|
+
</button>
|
|
191
149
|
</div>
|
|
192
|
-
<p style={{ fontSize: "12px", color: "#666", marginTop: "8px" }}>
|
|
193
|
-
Solution: Bxd7+, Nxd7, Qb8+, Nxb8, Rd8#
|
|
194
|
-
</p>
|
|
195
150
|
</div>
|
|
196
151
|
</div>
|
|
197
152
|
);
|
|
@@ -219,30 +174,22 @@ export const PuzzleThemeExamples = () => {
|
|
|
219
174
|
};
|
|
220
175
|
|
|
221
176
|
return (
|
|
222
|
-
<div>
|
|
223
|
-
<h2
|
|
224
|
-
<div
|
|
225
|
-
style={{
|
|
226
|
-
display: "grid",
|
|
227
|
-
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
|
|
228
|
-
gap: "24px",
|
|
229
|
-
}}
|
|
230
|
-
>
|
|
177
|
+
<div className="font-sans">
|
|
178
|
+
<h2 className="mb-6">Puzzle Theme Examples</h2>
|
|
179
|
+
<div className="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-6">
|
|
231
180
|
{Object.entries(customThemes).map(([name, theme]) => (
|
|
232
181
|
<div key={name}>
|
|
233
|
-
<h4
|
|
234
|
-
{name}
|
|
235
|
-
</h4>
|
|
182
|
+
<h4 className="mb-2 capitalize">{name}</h4>
|
|
236
183
|
<ChessPuzzle.Root puzzle={samplePuzzle} theme={theme}>
|
|
237
184
|
<ChessPuzzle.Board />
|
|
238
|
-
<div
|
|
185
|
+
<div className="mt-2 flex gap-2">
|
|
239
186
|
<ChessPuzzle.Hint asChild>
|
|
240
|
-
<button
|
|
187
|
+
<button className="py-1 px-2 text-size-xs border border-border rounded-sm bg-surface">
|
|
241
188
|
Hint
|
|
242
189
|
</button>
|
|
243
190
|
</ChessPuzzle.Hint>
|
|
244
191
|
<ChessPuzzle.Reset asChild>
|
|
245
|
-
<button
|
|
192
|
+
<button className="py-1 px-2 text-size-xs border border-border rounded-sm bg-surface">
|
|
246
193
|
Reset
|
|
247
194
|
</button>
|
|
248
195
|
</ChessPuzzle.Reset>
|
|
@@ -264,34 +211,31 @@ export const PartialPuzzleTheme = () => {
|
|
|
264
211
|
};
|
|
265
212
|
|
|
266
213
|
return (
|
|
267
|
-
<div
|
|
214
|
+
<div className="max-w-story-lg font-sans">
|
|
268
215
|
<h3>Partial Puzzle Theme</h3>
|
|
269
|
-
<p
|
|
216
|
+
<p className="text-size-sm text-text-muted mb-4 m-0">
|
|
270
217
|
Only override the hint color to gold. Success and failure use defaults.
|
|
271
218
|
</p>
|
|
272
219
|
<ChessPuzzle.Root puzzle={samplePuzzle} theme={partialTheme}>
|
|
273
220
|
<ChessPuzzle.Board />
|
|
274
|
-
<div
|
|
221
|
+
<div className="mt-2 flex gap-2">
|
|
275
222
|
<ChessPuzzle.Hint asChild>
|
|
276
|
-
<button
|
|
223
|
+
<button className="py-1.5 px-3 text-size-sm border border-border rounded-sm bg-surface">
|
|
224
|
+
Show Gold Hint
|
|
225
|
+
</button>
|
|
277
226
|
</ChessPuzzle.Hint>
|
|
278
227
|
<ChessPuzzle.Reset asChild>
|
|
279
|
-
<button
|
|
228
|
+
<button className="py-1.5 px-3 text-size-sm border border-border rounded-sm bg-surface">
|
|
229
|
+
Reset
|
|
230
|
+
</button>
|
|
280
231
|
</ChessPuzzle.Reset>
|
|
281
232
|
</div>
|
|
282
233
|
</ChessPuzzle.Root>
|
|
283
|
-
<details
|
|
284
|
-
<summary
|
|
234
|
+
<details className="mt-4">
|
|
235
|
+
<summary className="cursor-pointer text-size-sm">
|
|
285
236
|
View theme code
|
|
286
237
|
</summary>
|
|
287
|
-
<pre
|
|
288
|
-
style={{
|
|
289
|
-
fontSize: "11px",
|
|
290
|
-
background: "#f5f5f5",
|
|
291
|
-
padding: "12px",
|
|
292
|
-
overflow: "auto",
|
|
293
|
-
}}
|
|
294
|
-
>
|
|
238
|
+
<pre className="text-size-xs bg-surface-alt p-3 overflow-auto border border-border rounded-sm">
|
|
295
239
|
{JSON.stringify(partialTheme, null, 2)}
|
|
296
240
|
</pre>
|
|
297
241
|
</details>
|
package/src/docs/Theming.mdx
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
{/* Theming.mdx */}
|
|
2
|
-
import { Meta, ColorPalette, ColorItem } from '@storybook/addon-docs/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
|