@react-chess-tools/react-chess-puzzle 1.0.3 → 1.0.5

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @react-chess-tools/react-chess-puzzle
2
2
 
3
+ ## 1.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 5b74626: docs: add comprehensive documentation and Storybook redesign
8
+ - Updated dependencies [5b74626]
9
+ - @react-chess-tools/react-chess-game@1.0.4
10
+
11
+ ## 1.0.4
12
+
13
+ ### Patch Changes
14
+
15
+ - e678d58: chore: update dependencies
16
+ - Updated dependencies [1c4f876]
17
+ - Updated dependencies [e678d58]
18
+ - @react-chess-tools/react-chess-game@1.0.3
19
+
3
20
  ## 1.0.3
4
21
 
5
22
  ### 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",
3
+ "version": "1.0.5",
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.2",
45
+ "@react-chess-tools/react-chess-game": "1.0.4",
46
46
  "chess.js": "^1.4.0",
47
47
  "lodash": "^4.17.21"
48
48
  },
@@ -1,9 +1,16 @@
1
- import type { Meta } from "@storybook/react";
1
+ import type { Meta } from "@storybook/react-vite";
2
2
 
3
3
  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/Components/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
- <div style={storyStyles.container}>
145
- <div style={storyStyles.header}>
146
- <h3 style={storyStyles.title}>Chess Puzzle</h3>
147
- <p style={storyStyles.subtitle}>Find the best move sequence</p>
148
- <span style={storyStyles.statusBadge}>
149
- Puzzle {puzzleIndex + 1} of {puzzles.length}
150
- </span>
151
- </div>
152
- <div style={storyStyles.boardWrapper}>
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
- </div>
155
- <div style={storyStyles.controlsSection}>
58
+ </BoardWrapper>
59
+ <div className="flex gap-2.5 justify-center flex-wrap">
156
60
  <ChessPuzzle.Reset asChild>
157
- <button style={storyStyles.button}>Restart</button>
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
- <button
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 style={storyStyles.hintButton}>
171
- 💡 Hint
70
+ <ChessPuzzle.Hint asChild>
71
+ <Button variant="outline">Hint</Button>
172
72
  </ChessPuzzle.Hint>
173
73
  </div>
174
- </div>
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
- <div style={storyStyles.container}>
188
- <div style={storyStyles.header}>
189
- <h3 style={storyStyles.title}>Black to Move</h3>
190
- <p style={storyStyles.subtitle}>
191
- Board oriented from Black's perspective
192
- </p>
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
- </div>
197
- <div style={storyStyles.controlsSection}>
94
+ </BoardWrapper>
95
+ <div className="flex gap-2.5 justify-center flex-wrap">
198
96
  <ChessPuzzle.Reset asChild>
199
- <button style={storyStyles.button}>Restart</button>
97
+ <Button variant="outline">Restart</Button>
200
98
  </ChessPuzzle.Reset>
201
- <ChessPuzzle.Hint style={storyStyles.hintButton}>
202
- 💡 Hint
99
+ <ChessPuzzle.Hint asChild>
100
+ <Button variant="outline">Hint</Button>
203
101
  </ChessPuzzle.Hint>
204
102
  </div>
205
- </div>
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
- <div style={storyStyles.container}>
219
- <div style={storyStyles.header}>
220
- <h3 style={storyStyles.title}>Underpromotion Challenge</h3>
221
- <p style={storyStyles.subtitle}>
222
- Promote to a knight instead of a queen
223
- </p>
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
- </div>
228
- <div style={storyStyles.controlsSection}>
123
+ </BoardWrapper>
124
+ <div className="flex gap-2.5 justify-center flex-wrap">
229
125
  <ChessPuzzle.Reset asChild>
230
- <button
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 style={storyStyles.infoBox}>
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
- </div>
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
- <div style={storyStyles.container}>
250
- <div style={storyStyles.header}>
251
- <h3 style={storyStyles.title}>Puzzle with Sound</h3>
252
- <p style={storyStyles.subtitle}>Audio feedback on every move</p>
253
- </div>
254
- <div style={storyStyles.boardWrapper}>
141
+ <StoryContainer>
142
+ <StoryHeader
143
+ title="Puzzle with Sound"
144
+ subtitle="Audio feedback on every move"
145
+ />
146
+ <BoardWrapper>
255
147
  <ChessPuzzle.Board />
256
- </div>
257
- <p style={{ fontSize: "12px", color: "#868e96", textAlign: "center" }}>
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
- </div>
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
- <div style={storyStyles.container}>
278
- <div style={storyStyles.header}>
279
- <h3 style={storyStyles.title}>Keyboard Navigation</h3>
280
- <p style={storyStyles.subtitle}>Use keyboard shortcuts to navigate</p>
281
- </div>
282
- <div style={storyStyles.boardWrapper}>
169
+ <StoryContainer>
170
+ <StoryHeader
171
+ title="Keyboard Navigation"
172
+ subtitle="Use keyboard shortcuts to navigate"
173
+ />
174
+ <BoardWrapper>
283
175
  <ChessPuzzle.Board />
284
- </div>
285
- <div
286
- style={{
287
- display: "grid",
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
- style={{
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
- style={{
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
- style={{
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
- style={{
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
- </div>
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"], // Canonical solution
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
- <div style={storyStyles.container}>
431
- <div style={storyStyles.header}>
432
- <h3 style={storyStyles.title}>Flexible Checkmate</h3>
433
- <p style={storyStyles.subtitle}>
434
- Any checkmate move solves the puzzle
435
- </p>
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
- <div style={storyStyles.boardWrapper}>
218
+ <BoardWrapper>
449
219
  <ChessPuzzle.Board />
450
- </div>
451
- <div style={storyStyles.controlsSection}>
220
+ </BoardWrapper>
221
+ <div className="flex gap-2.5 justify-center flex-wrap">
452
222
  <ChessPuzzle.Reset asChild>
453
- <button style={storyStyles.button}>Restart</button>
223
+ <Button variant="outline">Restart</Button>
454
224
  </ChessPuzzle.Reset>
455
225
  </div>
456
- </div>
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
- <div style={storyStyles.container}>
469
- <div style={storyStyles.header}>
470
- <h3 style={storyStyles.title}>Strict Checkmate</h3>
471
- <p style={storyStyles.subtitle}>
472
- Only the canonical solution is accepted
473
- </p>
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
- <div style={storyStyles.boardWrapper}>
248
+ <BoardWrapper>
487
249
  <ChessPuzzle.Board />
488
- </div>
489
- <div style={storyStyles.controlsSection}>
250
+ </BoardWrapper>
251
+ <div className="flex gap-2.5 justify-center flex-wrap">
490
252
  <ChessPuzzle.Reset asChild>
491
- <button style={storyStyles.button}>Restart</button>
253
+ <Button variant="outline">Restart</Button>
492
254
  </ChessPuzzle.Reset>
493
255
  </div>
494
- </div>
256
+ </StoryContainer>
495
257
  </ChessPuzzle.Root>
496
258
  );
497
259
  };
@@ -1,21 +1,22 @@
1
- import type { Meta } from "@storybook/react";
1
+ import type { Meta } from "@storybook/react-vite";
2
2
  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/Theme/Playground",
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 style={{ maxWidth: "900px" }}>
14
+ <div className="max-w-story-xl">
14
15
  <Story />
15
16
  </div>
16
17
  ),
17
18
  ],
18
- } satisfies Meta<typeof ChessPuzzle.Root>;
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 [puzzleKey, setPuzzleKey] = useState(0);
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 copyTheme = () => {
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 resetPuzzle = () => {
106
- setPuzzleKey((k) => k + 1);
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 style={{ display: "flex", gap: "24px", flexWrap: "wrap" }}>
111
- <div style={{ flex: "1", minWidth: "300px" }}>
112
- <h3 style={{ marginBottom: "16px" }}>Puzzle Theme Editor</h3>
113
-
114
- <div style={{ marginBottom: "16px" }}>
115
- <strong>Puzzle Colors</strong>
116
- <div style={{ marginTop: "8px" }}>
117
- <ColorInput
118
- label="Success"
119
- value={theme.puzzle.success}
120
- onChange={(v) => updatePuzzleColor("success", v)}
121
- />
122
- <ColorInput
123
- label="Failure"
124
- value={theme.puzzle.failure}
125
- onChange={(v) => updatePuzzleColor("failure", v)}
126
- />
127
- <ColorInput
128
- label="Hint"
129
- value={theme.puzzle.hint}
130
- onChange={(v) => updatePuzzleColor("hint", v)}
131
- />
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
- <div style={{ display: "flex", gap: "8px", marginBottom: "16px" }}>
136
- <button
137
- onClick={copyTheme}
138
- style={{
139
- padding: "8px 16px",
140
- backgroundColor: copied ? "#4caf50" : "#2196f3",
141
- color: "white",
142
- border: "none",
143
- borderRadius: "4px",
144
- cursor: "pointer",
145
- }}
146
- >
147
- {copied ? "Copied!" : "Copy Theme Code"}
148
- </button>
149
- <button
150
- onClick={resetPuzzle}
151
- style={{
152
- padding: "8px 16px",
153
- backgroundColor: "#666",
154
- color: "white",
155
- border: "none",
156
- borderRadius: "4px",
157
- cursor: "pointer",
158
- }}
159
- >
160
- Reset Puzzle
161
- </button>
162
- </div>
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
- <div style={{ fontSize: "12px", color: "#666" }}>
165
- <p>
166
- <strong>How to test colors:</strong>
167
- </p>
168
- <ul style={{ paddingLeft: "16px" }}>
169
- <li>Click "Hint" to see the hint color</li>
170
- <li>Make a correct move to see success color</li>
171
- <li>Make a wrong move to see failure color</li>
172
- <li>Click "Reset" to try again</li>
173
- </ul>
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
- <div style={{ flex: "1", minWidth: "350px" }}>
178
- <h3 style={{ marginBottom: "16px" }}>Preview</h3>
179
- <div style={{ maxWidth: "400px" }}>
180
- <ChessPuzzle.Root key={puzzleKey} puzzle={samplePuzzle} theme={theme}>
181
- <ChessPuzzle.Board />
182
- <div style={{ marginTop: "8px", display: "flex", gap: "8px" }}>
183
- <ChessPuzzle.Hint asChild>
184
- <button style={{ padding: "6px 12px" }}>Hint</button>
185
- </ChessPuzzle.Hint>
186
- <ChessPuzzle.Reset asChild>
187
- <button style={{ padding: "6px 12px" }}>Reset</button>
188
- </ChessPuzzle.Reset>
189
- </div>
190
- </ChessPuzzle.Root>
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 style={{ marginBottom: "24px" }}>Puzzle Theme Examples</h2>
224
- <div
225
- style={{
226
- display: "grid",
227
- gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
228
- gap: "24px",
229
- }}
230
- >
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 style={{ marginBottom: "8px", textTransform: "capitalize" }}>
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 style={{ marginTop: "8px", display: "flex", gap: "8px" }}>
185
+ <div className="mt-2 flex gap-2">
239
186
  <ChessPuzzle.Hint asChild>
240
- <button style={{ padding: "4px 8px", fontSize: "12px" }}>
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 style={{ padding: "4px 8px", fontSize: "12px" }}>
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 style={{ maxWidth: "500px" }}>
214
+ <div className="max-w-story-lg font-sans">
268
215
  <h3>Partial Puzzle Theme</h3>
269
- <p style={{ fontSize: "14px", color: "#666", marginBottom: "16px" }}>
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 style={{ marginTop: "8px", display: "flex", gap: "8px" }}>
221
+ <div className="mt-2 flex gap-2">
275
222
  <ChessPuzzle.Hint asChild>
276
- <button style={{ padding: "6px 12px" }}>Show Gold Hint</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 style={{ padding: "6px 12px" }}>Reset</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 style={{ marginTop: "16px" }}>
284
- <summary style={{ cursor: "pointer", fontSize: "14px" }}>
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>
@@ -1,255 +0,0 @@
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