@dxos/react-ui-gameboard 0.8.4-main.b97322e → 0.8.4-main.bc2380dfbc

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 (93) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +2 -0
  3. package/dist/lib/browser/index.mjs +676 -788
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/node-esm/index.mjs +676 -788
  7. package/dist/lib/node-esm/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/meta.json +1 -1
  9. package/dist/types/src/components/Chessboard/Chessboard.d.ts +20 -0
  10. package/dist/types/src/components/Chessboard/Chessboard.d.ts.map +1 -0
  11. package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts +31 -0
  12. package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts.map +1 -0
  13. package/dist/types/src/components/Chessboard/chess.d.ts +60 -0
  14. package/dist/types/src/components/Chessboard/chess.d.ts.map +1 -0
  15. package/dist/types/src/components/Chessboard/chess.test.d.ts +2 -0
  16. package/dist/types/src/components/Chessboard/chess.test.d.ts.map +1 -0
  17. package/dist/types/src/components/Chessboard/index.d.ts.map +1 -0
  18. package/dist/types/src/components/Gameboard/Gameboard.d.ts +41 -0
  19. package/dist/types/src/components/Gameboard/Gameboard.d.ts.map +1 -0
  20. package/dist/types/src/{Gameboard → components/Gameboard}/Piece.d.ts +3 -2
  21. package/dist/types/src/components/Gameboard/Piece.d.ts.map +1 -0
  22. package/dist/types/src/components/Gameboard/Square.d.ts.map +1 -0
  23. package/dist/types/src/components/Gameboard/index.d.ts +4 -0
  24. package/dist/types/src/components/Gameboard/index.d.ts.map +1 -0
  25. package/dist/types/src/{Gameboard → components/Gameboard}/types.d.ts +3 -2
  26. package/dist/types/src/components/Gameboard/types.d.ts.map +1 -0
  27. package/dist/types/src/components/Gameboard/util.d.ts.map +1 -0
  28. package/dist/types/src/components/index.d.ts +3 -0
  29. package/dist/types/src/components/index.d.ts.map +1 -0
  30. package/dist/types/src/gen/pieces/chess/alpha/bB.d.ts.map +1 -1
  31. package/dist/types/src/gen/pieces/chess/alpha/bK.d.ts.map +1 -1
  32. package/dist/types/src/gen/pieces/chess/alpha/bN.d.ts.map +1 -1
  33. package/dist/types/src/gen/pieces/chess/alpha/bP.d.ts.map +1 -1
  34. package/dist/types/src/gen/pieces/chess/alpha/bQ.d.ts.map +1 -1
  35. package/dist/types/src/gen/pieces/chess/alpha/bR.d.ts.map +1 -1
  36. package/dist/types/src/gen/pieces/chess/alpha/wB.d.ts.map +1 -1
  37. package/dist/types/src/gen/pieces/chess/alpha/wK.d.ts.map +1 -1
  38. package/dist/types/src/gen/pieces/chess/alpha/wN.d.ts.map +1 -1
  39. package/dist/types/src/gen/pieces/chess/alpha/wP.d.ts.map +1 -1
  40. package/dist/types/src/gen/pieces/chess/alpha/wQ.d.ts.map +1 -1
  41. package/dist/types/src/gen/pieces/chess/alpha/wR.d.ts.map +1 -1
  42. package/dist/types/src/gen/pieces/chess/cburnett/bB.d.ts.map +1 -1
  43. package/dist/types/src/gen/pieces/chess/cburnett/bK.d.ts.map +1 -1
  44. package/dist/types/src/gen/pieces/chess/cburnett/bN.d.ts.map +1 -1
  45. package/dist/types/src/gen/pieces/chess/cburnett/bP.d.ts.map +1 -1
  46. package/dist/types/src/gen/pieces/chess/cburnett/bQ.d.ts.map +1 -1
  47. package/dist/types/src/gen/pieces/chess/cburnett/bR.d.ts.map +1 -1
  48. package/dist/types/src/gen/pieces/chess/cburnett/wB.d.ts.map +1 -1
  49. package/dist/types/src/gen/pieces/chess/cburnett/wK.d.ts.map +1 -1
  50. package/dist/types/src/gen/pieces/chess/cburnett/wN.d.ts.map +1 -1
  51. package/dist/types/src/gen/pieces/chess/cburnett/wP.d.ts.map +1 -1
  52. package/dist/types/src/gen/pieces/chess/cburnett/wQ.d.ts.map +1 -1
  53. package/dist/types/src/gen/pieces/chess/cburnett/wR.d.ts.map +1 -1
  54. package/dist/types/src/index.d.ts +1 -2
  55. package/dist/types/src/index.d.ts.map +1 -1
  56. package/dist/types/tsconfig.tsbuildinfo +1 -1
  57. package/package.json +28 -26
  58. package/src/{Chessboard → components/Chessboard}/Chessboard.stories.tsx +32 -29
  59. package/src/{Chessboard → components/Chessboard}/Chessboard.tsx +71 -60
  60. package/src/components/Chessboard/chess.test.ts +19 -0
  61. package/src/components/Chessboard/chess.ts +325 -0
  62. package/src/components/Gameboard/Gameboard.tsx +138 -0
  63. package/src/{Gameboard → components/Gameboard}/Piece.tsx +27 -23
  64. package/src/{Gameboard → components/Gameboard}/Square.tsx +8 -6
  65. package/src/{Gameboard → components/Gameboard}/index.ts +0 -3
  66. package/src/{Gameboard → components/Gameboard}/types.ts +4 -2
  67. package/src/components/index.ts +6 -0
  68. package/src/index.ts +1 -2
  69. package/dist/types/src/Chessboard/Chessboard.d.ts +0 -15
  70. package/dist/types/src/Chessboard/Chessboard.d.ts.map +0 -1
  71. package/dist/types/src/Chessboard/Chessboard.stories.d.ts +0 -16
  72. package/dist/types/src/Chessboard/Chessboard.stories.d.ts.map +0 -1
  73. package/dist/types/src/Chessboard/chess.d.ts +0 -40
  74. package/dist/types/src/Chessboard/chess.d.ts.map +0 -1
  75. package/dist/types/src/Chessboard/index.d.ts.map +0 -1
  76. package/dist/types/src/Gameboard/Gameboard.d.ts +0 -23
  77. package/dist/types/src/Gameboard/Gameboard.d.ts.map +0 -1
  78. package/dist/types/src/Gameboard/Piece.d.ts.map +0 -1
  79. package/dist/types/src/Gameboard/Square.d.ts.map +0 -1
  80. package/dist/types/src/Gameboard/context.d.ts +0 -10
  81. package/dist/types/src/Gameboard/context.d.ts.map +0 -1
  82. package/dist/types/src/Gameboard/index.d.ts +0 -7
  83. package/dist/types/src/Gameboard/index.d.ts.map +0 -1
  84. package/dist/types/src/Gameboard/types.d.ts.map +0 -1
  85. package/dist/types/src/Gameboard/util.d.ts.map +0 -1
  86. package/src/Chessboard/chess.ts +0 -213
  87. package/src/Gameboard/Gameboard.tsx +0 -103
  88. package/src/Gameboard/context.ts +0 -22
  89. /package/dist/types/src/{Chessboard → components/Chessboard}/index.d.ts +0 -0
  90. /package/dist/types/src/{Gameboard → components/Gameboard}/Square.d.ts +0 -0
  91. /package/dist/types/src/{Gameboard → components/Gameboard}/util.d.ts +0 -0
  92. /package/src/{Chessboard → components/Chessboard}/index.ts +0 -0
  93. /package/src/{Gameboard → components/Gameboard}/util.ts +0 -0
package/package.json CHANGED
@@ -1,58 +1,60 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-gameboard",
3
- "version": "0.8.4-main.b97322e",
3
+ "version": "0.8.4-main.bc2380dfbc",
4
4
  "description": "Game board.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
- "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
11
+ "license": "FSL-1.1-Apache-2.0",
8
12
  "author": "DXOS.org",
9
13
  "type": "module",
10
14
  "exports": {
11
15
  ".": {
16
+ "source": "./src/index.ts",
12
17
  "types": "./dist/types/src/index.d.ts",
13
18
  "browser": "./dist/lib/browser/index.mjs",
14
19
  "node": "./dist/lib/node-esm/index.mjs"
15
20
  }
16
21
  },
17
22
  "types": "dist/types/src/index.d.ts",
18
- "typesVersions": {
19
- "*": {}
20
- },
21
23
  "files": [
22
24
  "dist",
23
25
  "src"
24
26
  ],
25
27
  "dependencies": {
26
- "@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
27
- "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
28
- "@preact-signals/safe-react": "^0.9.0",
29
- "@preact/signals-core": "^1.9.0",
28
+ "@atlaskit/pragmatic-drag-and-drop": "1.7.7",
29
+ "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
30
+ "@effect-atom/atom-react": "^0.5.0",
31
+ "@radix-ui/react-context": "1.1.1",
30
32
  "chess.js": "^1.0.0",
31
33
  "react-resize-detector": "^11.0.1",
32
- "@dxos/debug": "0.8.4-main.b97322e",
33
- "@dxos/invariant": "0.8.4-main.b97322e",
34
- "@dxos/node-std": "0.8.4-main.b97322e",
35
- "@dxos/log": "0.8.4-main.b97322e",
36
- "@dxos/util": "0.8.4-main.b97322e"
34
+ "@dxos/debug": "0.8.4-main.bc2380dfbc",
35
+ "@dxos/invariant": "0.8.4-main.bc2380dfbc",
36
+ "@dxos/log": "0.8.4-main.bc2380dfbc",
37
+ "@dxos/util": "0.8.4-main.bc2380dfbc",
38
+ "@dxos/node-std": "0.8.4-main.bc2380dfbc"
37
39
  },
38
40
  "devDependencies": {
39
41
  "@svgr/cli": "^8.1.0",
40
42
  "@types/lodash.defaultsdeep": "^4.6.6",
41
- "@types/react": "~18.2.0",
42
- "@types/react-dom": "~18.2.0",
43
+ "@types/react": "~19.2.7",
44
+ "@types/react-dom": "~19.2.3",
43
45
  "lodash.defaultsdeep": "^4.6.1",
44
- "react": "~18.2.0",
45
- "react-dom": "~18.2.0",
46
- "vite": "5.4.7",
47
- "@dxos/react-ui-theme": "0.8.4-main.b97322e",
48
- "@dxos/react-ui": "0.8.4-main.b97322e",
49
- "@dxos/storybook-utils": "0.8.4-main.b97322e"
46
+ "react": "~19.2.3",
47
+ "react-dom": "~19.2.3",
48
+ "vite": "^8.0.13",
49
+ "@dxos/storybook-utils": "0.8.4-main.bc2380dfbc",
50
+ "@dxos/ui-theme": "0.8.4-main.bc2380dfbc",
51
+ "@dxos/react-ui": "0.8.4-main.bc2380dfbc"
50
52
  },
51
53
  "peerDependencies": {
52
- "react": "~18.2.0",
53
- "react-dom": "~18.2.0",
54
- "@dxos/react-ui": "0.8.4-main.b97322e",
55
- "@dxos/react-ui-theme": "0.8.4-main.b97322e"
54
+ "react": "~19.2.3",
55
+ "react-dom": "~19.2.3",
56
+ "@dxos/react-ui": "0.8.4-main.bc2380dfbc",
57
+ "@dxos/ui-theme": "0.8.4-main.bc2380dfbc"
56
58
  },
57
59
  "publishConfig": {
58
60
  "access": "public"
@@ -2,31 +2,32 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
+ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
6
8
 
7
- import type { Meta, StoryObj } from '@storybook/react-vite';
8
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
9
-
10
- import { log } from '@dxos/log';
11
9
  import { Button, Toolbar } from '@dxos/react-ui';
12
- import { withLayout, withTheme } from '@dxos/storybook-utils';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+ import { withRegistry } from '@dxos/storybook-utils';
13
12
 
14
- import { Chessboard, type ChessboardProps } from './Chessboard';
13
+ import { Gameboard, type GameboardRootProps, type Move, type Player } from '../Gameboard';
15
14
  import { ChessModel } from './chess';
16
- import { Gameboard, type GameboardRootProps, type Player, type Move } from '../Gameboard';
15
+ import { Chessboard, type ChessboardProps } from './Chessboard';
17
16
 
18
- type RenderProps = Pick<ChessboardProps, 'orientation' | 'showLabels' | 'debug'> & {
19
- fen: string;
17
+ type DefaultStoryProps = Pick<ChessboardProps, 'orientation' | 'showLabels' | 'debug'> & {
18
+ pgn?: string;
20
19
  };
21
20
 
22
- const DefaultStory = ({ fen, orientation: _orientation, ...props }: RenderProps) => {
23
- const model = useMemo(() => new ChessModel(fen), [fen]);
21
+ const DefaultStory = ({ orientation: _orientation, pgn, ...props }: DefaultStoryProps) => {
22
+ const registry = useContext(RegistryContext);
23
+ const model = useMemo(() => new ChessModel(registry, pgn), [registry, pgn]);
24
24
  const [orientation, setOrientation] = useState<Player | undefined>(_orientation);
25
25
 
26
- const handleDrop = useCallback<NonNullable<GameboardRootProps['onDrop']>>(
26
+ const handleDrop = useCallback<NonNullable<GameboardRootProps<ChessModel>['onDrop']>>(
27
27
  (move: Move) => {
28
- log.info('handleDrop', { move });
29
- return model.makeMove(move);
28
+ const result = model.makeMove(move);
29
+ console.log(model.pgn);
30
+ return result;
30
31
  },
31
32
  [model],
32
33
  );
@@ -34,7 +35,7 @@ const DefaultStory = ({ fen, orientation: _orientation, ...props }: RenderProps)
34
35
  return (
35
36
  <div className='flex flex-col grow gap-2 overflow-hidden'>
36
37
  <Toolbar.Root>
37
- <Button onClick={() => model.initialize()}>Reset</Button>
38
+ <Button onClick={() => model.update()}>Reset</Button>
38
39
  <Button onClick={() => model.makeRandomMove()}>Move</Button>
39
40
  <div className='grow'></div>
40
41
  <Button
@@ -44,7 +45,7 @@ const DefaultStory = ({ fen, orientation: _orientation, ...props }: RenderProps)
44
45
  </Button>
45
46
  </Toolbar.Root>
46
47
  <Gameboard.Root model={model} onDrop={handleDrop}>
47
- <Gameboard.Content>
48
+ <Gameboard.Content grow contain>
48
49
  <Chessboard orientation={orientation} {...props} />
49
50
  </Gameboard.Content>
50
51
  </Gameboard.Root>
@@ -52,8 +53,9 @@ const DefaultStory = ({ fen, orientation: _orientation, ...props }: RenderProps)
52
53
  );
53
54
  };
54
55
 
55
- const Grid = (props: RenderProps) => {
56
- const models = useMemo(() => Array.from({ length: 9 }).map(() => new ChessModel()), []);
56
+ const GridStory = () => {
57
+ const registry = useContext(RegistryContext);
58
+ const models = useMemo(() => Array.from({ length: 9 }).map(() => new ChessModel(registry)), [registry]);
57
59
  useEffect(() => {
58
60
  const i = setInterval(() => {
59
61
  const model = models[Math.floor(Math.random() * models.length)];
@@ -77,34 +79,35 @@ const Grid = (props: RenderProps) => {
77
79
  );
78
80
  };
79
81
 
80
- const meta: Meta<typeof DefaultStory> = {
82
+ const meta = {
81
83
  title: 'ui/react-ui-gameboard/Chessboard',
82
84
  component: Chessboard,
83
85
  render: DefaultStory,
84
- decorators: [withTheme, withLayout({ fullscreen: true, classNames: '' })],
85
- };
86
+ decorators: [withRegistry, withTheme(), withLayout({ layout: 'column' })],
87
+ } satisfies Meta<typeof Chessboard>;
86
88
 
87
89
  export default meta;
88
90
 
89
- type Story = StoryObj<typeof DefaultStory>;
91
+ type Story = StoryObj<typeof meta>;
90
92
 
91
93
  export const Default: Story = {};
92
94
 
93
95
  export const Promotion: Story = {
94
96
  args: {
95
- fen: '4k3/7P/8/8/8/8/1p6/4K3 w - - 0 1',
97
+ pgn: '1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. c3 Nf6 5. d4 exd4 6. cxd4 Bb4+ 7. Nc3 d5 8. exd5 Nxd5 9. O-O Be6 10. Qb3 Na5 11. Qa4+ c6 12. Bxd5 Bxc3 13. Bxe6 fxe6 14. d5 Qg5 15. dxe6 Kf8 16. e7+ Kg8 *',
96
98
  },
97
99
  };
98
100
 
99
101
  export const Debug: Story = {
100
102
  args: {
101
- debug: true,
102
- showLabels: true,
103
+ pgn: '1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. c3 Nf6 5. d4 exd4 6. cxd4 Bb4+ 7. Nc3 d5 8. exd5 Nxd5 9. O-O Be6 10. Qb3 Na5 11. Qa4+ c6 12. Bxd5 Bxc3 13. Bxe6 fxe6 *',
103
104
  orientation: 'black',
104
- fen: 'q3k1nr/1pp1nQpp/3p4/1P2p3/4P3/B1PP1b2/B5PP/5K2 b k - 0 17',
105
+ showLabels: true,
106
+ debug: true,
105
107
  },
106
108
  };
107
109
 
108
- export const Nine: Story = {
109
- render: Grid,
110
+ export const Grid = {
111
+ decorators: [withRegistry, withTheme(), withLayout({ layout: 'fullscreen' })],
112
+ render: GridStory,
110
113
  };
@@ -2,25 +2,34 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { type PropsWithChildren, useRef, useMemo, useEffect, useState, memo } from 'react';
5
+ import { Atom, useAtomValue } from '@effect-atom/atom-react';
6
+ import React, { type PropsWithChildren, forwardRef, memo, useEffect, useMemo, useRef, useState } from 'react';
6
7
  import { useResizeDetector } from 'react-resize-detector';
7
8
 
8
- import { type ThemedClassName, useTrackProps } from '@dxos/react-ui';
9
- import { mx } from '@dxos/react-ui-theme';
10
- import { isNotFalsy } from '@dxos/util';
9
+ import { type ThemedClassName, useForwardedRef } from '@dxos/react-ui';
10
+ import { mx } from '@dxos/ui-theme';
11
+ import { isNonNullable } from '@dxos/util';
11
12
 
12
- import { boardStyles, type ChessPiece, ChessPieces, getSquareColor, locationToPos } from './chess';
13
13
  import {
14
14
  type DOMRectBounds,
15
+ Gameboard,
15
16
  type Location,
17
+ type PieceMap,
16
18
  type PieceRecord,
17
19
  type Player,
18
- Piece,
19
- Square,
20
20
  getRelativeBounds,
21
21
  locationToString,
22
- useBoardContext,
22
+ useGameboardContext,
23
23
  } from '../Gameboard';
24
+ import { type ChessModel, type ChessPiece, ChessPieces, boardStyles, getSquareColor, locationToPos } from './chess';
25
+
26
+ /** Fallback atom for when model is undefined. */
27
+ const EMPTY_PIECES_ATOM = Atom.make<PieceMap<ChessPiece>>({});
28
+
29
+ /**
30
+ * Chessboard layout.
31
+ */
32
+ const CHESSBOARD_NAME = 'Chessboard';
24
33
 
25
34
  export type ChessboardProps = ThemedClassName<
26
35
  PropsWithChildren<{
@@ -32,16 +41,15 @@ export type ChessboardProps = ThemedClassName<
32
41
  }>
33
42
  >;
34
43
 
35
- /**
36
- * Chessboard layout.
37
- */
38
- export const Chessboard = memo(
39
- ({ orientation, showLabels, debug, rows = 8, cols = 8, classNames }: ChessboardProps) => {
40
- useTrackProps({ orientation, showLabels, debug }, Chessboard.displayName, false);
41
- const { ref: containerRef, width, height } = useResizeDetector({ refreshRate: 200 });
42
- const { model, promoting, onPromotion } = useBoardContext();
44
+ const ChessboardComponent = forwardRef<HTMLDivElement, ChessboardProps>(
45
+ ({ classNames, orientation, showLabels, debug, rows = 8, cols = 8 }, forwardedRef) => {
46
+ const targetRef = useForwardedRef(forwardedRef);
47
+ const { width, height } = useResizeDetector({ targetRef, refreshRate: 200 });
48
+ const { model, promoting, onPromotion } = useGameboardContext<ChessModel>(CHESSBOARD_NAME);
49
+ const pieces = useAtomValue(model?.pieces ?? EMPTY_PIECES_ATOM);
43
50
 
44
- const locations = useMemo<Location[]>(() => {
51
+ // Board squares.
52
+ const squares = useMemo<Location[]>(() => {
45
53
  return Array.from({ length: rows }, (_, i) => (orientation === 'black' ? i : rows - 1 - i)).flatMap((row) =>
46
54
  Array.from({ length: cols }).map((_, col) => [row, col] as Location),
47
55
  );
@@ -49,24 +57,24 @@ export const Chessboard = memo(
49
57
 
50
58
  // Use DOM grid layout to position squares.
51
59
  const layout = useMemo(() => {
52
- return locations.map((location) => {
60
+ return squares.map((location) => {
53
61
  return (
54
62
  <div
55
63
  key={locationToString(location)}
56
64
  {...{
57
- ['data-location' as const]: locationToString(location),
65
+ 'data-location': locationToString(location),
58
66
  }}
59
67
  />
60
68
  );
61
69
  });
62
- }, [locations]);
70
+ }, [squares]);
63
71
 
64
72
  // Build map of square locations to bounds.
65
73
  const [grid, setGrid] = useState<Record<string, DOMRectBounds>>({});
66
74
  const gridRef = useRef<HTMLDivElement>(null);
67
75
  useEffect(() => {
68
76
  setGrid(
69
- locations.reduce(
77
+ squares.reduce(
70
78
  (acc, location) => {
71
79
  const square = getSquareLocation(gridRef.current!, location)!;
72
80
  const bounds = getRelativeBounds(gridRef.current!, square);
@@ -75,7 +83,7 @@ export const Chessboard = memo(
75
83
  {} as Record<string, DOMRectBounds>,
76
84
  ),
77
85
  );
78
- }, [locations, width, height]);
86
+ }, [squares, width, height]);
79
87
 
80
88
  // Get the bounds of each square and piece.
81
89
  const positions = useMemo<{ piece: PieceRecord; bounds: DOMRectBounds }[]>(() => {
@@ -83,7 +91,7 @@ export const Chessboard = memo(
83
91
  return [];
84
92
  }
85
93
 
86
- return Object.values(model?.pieces.value ?? {})
94
+ return Object.values(pieces)
87
95
  .map((piece) => {
88
96
  if (piece.id === promoting?.id) {
89
97
  return null;
@@ -92,17 +100,19 @@ export const Chessboard = memo(
92
100
  const bounds = grid[locationToString(piece.location)];
93
101
  return { piece, bounds };
94
102
  })
95
- .filter(isNotFalsy);
96
- }, [grid, model?.pieces.value, promoting]);
103
+ .filter(isNonNullable);
104
+ }, [grid, pieces, promoting]);
97
105
 
98
106
  return (
99
- <div ref={containerRef} className={mx('relative', classNames)}>
107
+ <div ref={targetRef} tabIndex={0} className={mx('dx-expander relative outline-hidden', classNames)}>
108
+ {/* DOM Layout. */}
100
109
  <div ref={gridRef} className='grid grid-rows-8 grid-cols-8 aspect-square select-none'>
101
110
  {layout}
102
111
  </div>
112
+ {/* Squares. */}
103
113
  <div>
104
- {locations.map((location) => (
105
- <Square
114
+ {squares.map((location) => (
115
+ <Gameboard.Square
106
116
  key={locationToString(location)}
107
117
  location={location}
108
118
  label={showLabels ? locationToPos(location) : undefined}
@@ -111,9 +121,10 @@ export const Chessboard = memo(
111
121
  />
112
122
  ))}
113
123
  </div>
124
+ {/* Pieces. */}
114
125
  <div className={mx(promoting && 'opacity-50')}>
115
126
  {positions.map(({ bounds, piece }) => (
116
- <Piece
127
+ <Gameboard.Piece
117
128
  key={piece.id}
118
129
  piece={piece}
119
130
  bounds={bounds}
@@ -123,32 +134,29 @@ export const Chessboard = memo(
123
134
  />
124
135
  ))}
125
136
  </div>
126
- <div>
127
- {promoting && (
128
- <PromotionSelector
129
- grid={grid}
130
- piece={promoting}
131
- onSelect={(piece) => {
132
- onPromotion({
133
- from: Object.values(model!.pieces.value).find((p) => p.id === promoting.id)!.location,
134
- to: piece.location,
135
- piece: promoting.type,
136
- promotion: piece.type,
137
- });
138
- }}
139
- />
140
- )}
141
- </div>
137
+ {/* Promotion selector. */}
138
+ {promoting && (
139
+ <PromotionSelector
140
+ grid={grid}
141
+ piece={promoting}
142
+ onSelect={(piece) => {
143
+ onPromotion({
144
+ from: Object.values(pieces).find((p) => p.id === promoting.id)!.location,
145
+ to: piece.location,
146
+ piece: promoting.type,
147
+ promotion: piece.type,
148
+ });
149
+ }}
150
+ />
151
+ )}
142
152
  </div>
143
153
  );
144
154
  },
145
155
  );
146
156
 
147
- Chessboard.displayName = 'Chessboard';
157
+ ChessboardComponent.displayName = CHESSBOARD_NAME;
148
158
 
149
- const getSquareLocation = (container: HTMLElement, location: Location): HTMLElement | null => {
150
- return container.querySelector(`[data-location="${locationToString(location)}"]`);
151
- };
159
+ export const Chessboard = memo(ChessboardComponent);
152
160
 
153
161
  const PromotionSelector = ({
154
162
  grid,
@@ -176,19 +184,22 @@ const PromotionSelector = ({
176
184
  onSelect({ ...piece, type: selected.type });
177
185
  };
178
186
 
179
- // TODO(burdon): Circle.
180
187
  return (
181
- <div>
188
+ <>
182
189
  {positions.map(({ piece, bounds }) => (
183
- <div key={piece.id} style={bounds} onClick={() => handleSelect(piece)}>
184
- <Piece
185
- piece={piece}
186
- bounds={bounds}
187
- Component={ChessPieces[piece.type as ChessPiece]}
188
- classNames={mx('border-2 border-neutral-700 rounded-full', boardStyles.promotion)}
189
- />
190
- </div>
190
+ <Gameboard.Piece
191
+ key={piece.id}
192
+ classNames={mx('border-2 border-neutral-700 rounded-full', boardStyles.promotion)}
193
+ piece={piece}
194
+ bounds={bounds}
195
+ Component={ChessPieces[piece.type as ChessPiece]}
196
+ onClick={() => handleSelect(piece)}
197
+ />
191
198
  ))}
192
- </div>
199
+ </>
193
200
  );
194
201
  };
202
+
203
+ const getSquareLocation = (container: HTMLElement, location: Location): HTMLElement | null => {
204
+ return container.querySelector(`[data-location="${locationToString(location)}"]`);
205
+ };
@@ -0,0 +1,19 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Chess as ChessJS } from 'chess.js';
6
+ import { describe, it } from 'vitest';
7
+
8
+ import { createPieceMap } from './chess';
9
+
10
+ describe('ChessModel', () => {
11
+ it('should update pieces', ({ expect }) => {
12
+ const chess = new ChessJS();
13
+ chess.loadPgn(
14
+ '1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. c3 Nf6 5. d4 exd4 6. cxd4 Bb4+ 7. Nc3 d5 8. exd5 Nxd5 9. O-O Be6 10. Qb3 Na5 11. Qa4+ c6 12. Bxd5 Bxc3 13. Bxe6 fxe6 *',
15
+ );
16
+ const pieces = createPieceMap(chess);
17
+ expect(pieces).to.exist;
18
+ });
19
+ });