@dxos/react-ui-gameboard 0.8.3 → 0.8.4-main.1068cf700f

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 (76) hide show
  1. package/dist/lib/browser/index.mjs +764 -825
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +764 -825
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Chessboard/Chessboard.d.ts +20 -0
  8. package/dist/types/src/components/Chessboard/Chessboard.d.ts.map +1 -0
  9. package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts +30 -0
  10. package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts.map +1 -0
  11. package/dist/types/src/components/Chessboard/chess.d.ts +60 -0
  12. package/dist/types/src/components/Chessboard/chess.d.ts.map +1 -0
  13. package/dist/types/src/components/Chessboard/chess.test.d.ts +2 -0
  14. package/dist/types/src/components/Chessboard/chess.test.d.ts.map +1 -0
  15. package/dist/types/src/components/Chessboard/index.d.ts.map +1 -0
  16. package/dist/types/src/components/Gameboard/Gameboard.d.ts +38 -0
  17. package/dist/types/src/components/Gameboard/Gameboard.d.ts.map +1 -0
  18. package/dist/types/src/{Board → components/Gameboard}/Piece.d.ts +3 -2
  19. package/dist/types/src/components/Gameboard/Piece.d.ts.map +1 -0
  20. package/dist/types/src/components/Gameboard/Square.d.ts.map +1 -0
  21. package/dist/types/src/components/Gameboard/index.d.ts +4 -0
  22. package/dist/types/src/components/Gameboard/index.d.ts.map +1 -0
  23. package/dist/types/src/{Board → components/Gameboard}/types.d.ts +4 -3
  24. package/dist/types/src/components/Gameboard/types.d.ts.map +1 -0
  25. package/dist/types/src/components/Gameboard/util.d.ts.map +1 -0
  26. package/dist/types/src/components/index.d.ts +3 -0
  27. package/dist/types/src/components/index.d.ts.map +1 -0
  28. package/dist/types/src/index.d.ts +1 -2
  29. package/dist/types/src/index.d.ts.map +1 -1
  30. package/dist/types/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +32 -24
  32. package/src/components/Chessboard/Chessboard.stories.tsx +113 -0
  33. package/src/components/Chessboard/Chessboard.tsx +206 -0
  34. package/src/components/Chessboard/chess.test.ts +19 -0
  35. package/src/components/Chessboard/chess.ts +325 -0
  36. package/src/components/Gameboard/Gameboard.tsx +145 -0
  37. package/src/{Board → components/Gameboard}/Piece.tsx +27 -23
  38. package/src/{Board → components/Gameboard}/Square.tsx +8 -6
  39. package/src/components/Gameboard/index.ts +8 -0
  40. package/src/{Board → components/Gameboard}/types.ts +5 -3
  41. package/src/components/index.ts +6 -0
  42. package/src/index.ts +1 -2
  43. package/dist/lib/node/index.cjs +0 -1039
  44. package/dist/lib/node/index.cjs.map +0 -7
  45. package/dist/lib/node/meta.json +0 -1
  46. package/dist/types/src/Board/Board.d.ts +0 -15
  47. package/dist/types/src/Board/Board.d.ts.map +0 -1
  48. package/dist/types/src/Board/Container.d.ts +0 -14
  49. package/dist/types/src/Board/Container.d.ts.map +0 -1
  50. package/dist/types/src/Board/Piece.d.ts.map +0 -1
  51. package/dist/types/src/Board/Square.d.ts.map +0 -1
  52. package/dist/types/src/Board/context.d.ts +0 -10
  53. package/dist/types/src/Board/context.d.ts.map +0 -1
  54. package/dist/types/src/Board/index.d.ts +0 -8
  55. package/dist/types/src/Board/index.d.ts.map +0 -1
  56. package/dist/types/src/Board/types.d.ts.map +0 -1
  57. package/dist/types/src/Board/util.d.ts.map +0 -1
  58. package/dist/types/src/Chessboard/Chessboard.d.ts +0 -14
  59. package/dist/types/src/Chessboard/Chessboard.d.ts.map +0 -1
  60. package/dist/types/src/Chessboard/Chessboard.stories.d.ts +0 -16
  61. package/dist/types/src/Chessboard/Chessboard.stories.d.ts.map +0 -1
  62. package/dist/types/src/Chessboard/chess.d.ts +0 -40
  63. package/dist/types/src/Chessboard/chess.d.ts.map +0 -1
  64. package/dist/types/src/Chessboard/index.d.ts.map +0 -1
  65. package/src/Board/Board.tsx +0 -86
  66. package/src/Board/Container.tsx +0 -25
  67. package/src/Board/context.ts +0 -22
  68. package/src/Board/index.ts +0 -12
  69. package/src/Chessboard/Chessboard.stories.tsx +0 -108
  70. package/src/Chessboard/Chessboard.tsx +0 -190
  71. package/src/Chessboard/chess.ts +0 -213
  72. /package/dist/types/src/{Chessboard → components/Chessboard}/index.d.ts +0 -0
  73. /package/dist/types/src/{Board → components/Gameboard}/Square.d.ts +0 -0
  74. /package/dist/types/src/{Board → components/Gameboard}/util.d.ts +0 -0
  75. /package/src/{Chessboard → components/Chessboard}/index.ts +0 -0
  76. /package/src/{Board → components/Gameboard}/util.ts +0 -0
@@ -0,0 +1,325 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Atom, type Registry } from '@effect-atom/atom-react';
6
+ import { Chess as ChessJS } from 'chess.js';
7
+ import { type FC, type SVGProps } from 'react';
8
+
9
+ import { invariant } from '@dxos/invariant';
10
+ import { log } from '@dxos/log';
11
+
12
+ import * as Alpha from '../../gen/pieces/chess/alpha';
13
+ import {
14
+ type GameboardModel,
15
+ type Location,
16
+ type Move,
17
+ type PieceMap,
18
+ type PieceType,
19
+ type Player,
20
+ locationToString,
21
+ } from '../Gameboard';
22
+
23
+ export type ChessPiece = 'BK' | 'BQ' | 'BR' | 'BB' | 'BN' | 'BP' | 'WK' | 'WQ' | 'WR' | 'WB' | 'WN' | 'WP';
24
+
25
+ export const ChessPieces: Record<ChessPiece, FC<SVGProps<SVGSVGElement>>> = Alpha;
26
+
27
+ export const posToLocation = (pos: string): Location => {
28
+ const col = pos.charCodeAt(0) - 'a'.charCodeAt(0);
29
+ const row = parseInt(pos[1]) - 1;
30
+ return [row, col];
31
+ };
32
+
33
+ export const locationToPos = ([row, col]: Location): string => {
34
+ return String.fromCharCode(col + 'a'.charCodeAt(0)) + (row + 1);
35
+ };
36
+
37
+ export const getRawPgn = (pgn: string) => {
38
+ return pgn.replace(/\[.*?\]/g, '').trim();
39
+ };
40
+
41
+ const styles = {
42
+ neutral: {
43
+ black: 'bg-neutral-50',
44
+ white: 'bg-neutral-200',
45
+ promotion: 'bg-neutral-200 hover:bg-neutral-300 opacity-70 hover:opacity-100',
46
+ },
47
+ original: {
48
+ black: 'bg-[#6C95B9]',
49
+ white: 'bg-[#CCD3DB]',
50
+ promotion: 'duration-500 bg-[#CCD3DB] opacity-70 hover:opacity-100',
51
+ },
52
+ blue: {
53
+ black: 'bg-[#608BC1]',
54
+ white: 'bg-[#CBDCEB]',
55
+ promotion: 'duration-500 bg-[#CBDCEB] opacity-70 hover:opacity-100',
56
+ },
57
+ green: {
58
+ black: 'bg-[#8EB486]',
59
+ white: 'bg-[#FDF7F4]',
60
+ promotion: 'duration-500 bg-[#FDF7F4] opacity-70 hover:opacity-100',
61
+ },
62
+ };
63
+
64
+ export const boardStyles = styles.original;
65
+
66
+ export const getSquareColor = ([row, col]: Location) => {
67
+ return (col + row) % 2 === 0 ? boardStyles.black : boardStyles.white;
68
+ };
69
+
70
+ export const createChess = (pgn?: string): ChessJS => {
71
+ const chess = new ChessJS();
72
+ if (pgn) {
73
+ try {
74
+ chess.loadPgn(pgn);
75
+ } catch {
76
+ log.warn(pgn);
77
+ }
78
+ }
79
+
80
+ return chess;
81
+ };
82
+
83
+ /**
84
+ * Chess model.
85
+ */
86
+ export class ChessModel implements GameboardModel<ChessPiece> {
87
+ private readonly _chess = new ChessJS();
88
+ private readonly _pieces = Atom.make<PieceMap<ChessPiece>>({});
89
+ private readonly _moveIndex = Atom.make(0);
90
+
91
+ constructor(
92
+ private readonly _registry: Registry.Registry,
93
+ pgn?: string,
94
+ ) {
95
+ this.update(pgn);
96
+ }
97
+
98
+ get readonly(): boolean {
99
+ return this._registry.get(this._moveIndex) !== this._chess.history().length;
100
+ }
101
+
102
+ get turn(): Player {
103
+ return this._chess.turn() === 'w' ? 'white' : 'black';
104
+ }
105
+
106
+ get game(): ChessJS {
107
+ return this._chess;
108
+ }
109
+
110
+ get pieces(): Atom.Atom<PieceMap<ChessPiece>> {
111
+ return this._pieces;
112
+ }
113
+
114
+ get moveIndex(): Atom.Atom<number> {
115
+ return this._moveIndex;
116
+ }
117
+
118
+ get fen() {
119
+ return this._chess.fen();
120
+ }
121
+
122
+ /**
123
+ * PGN with headers.
124
+ *
125
+ * [Event "?"]
126
+ * [Site "?"]
127
+ * [Date "2025.08.05"]
128
+ * [Round "?"]
129
+ * [White "?"]
130
+ * [Black "?"]
131
+ * [Result "*"]
132
+ */
133
+ // TODO(burdon): Update headers.
134
+ get pgn(): string {
135
+ return getRawPgn(this._chess.pgn());
136
+ }
137
+
138
+ setMoveIndex(index: number) {
139
+ const temp = new ChessJS();
140
+ const history = this._chess.history({ verbose: true });
141
+ for (let i = 0; i < index && i < history.length; i++) {
142
+ temp.move(history[i]);
143
+ }
144
+ this._updateBoard(temp);
145
+ }
146
+
147
+ update(pgn = ''): void {
148
+ const previous = this._chess.history();
149
+ try {
150
+ this._chess.loadPgn(pgn);
151
+ // TODO(burdon): Get from TS.
152
+ // TODO(burdon): Update if not set.
153
+ this._chess.setHeader('Date', createDate());
154
+ this._chess.setHeader('Site', 'dxos.org');
155
+ // TODO(burdon): Update player keys.
156
+ // this._chess.setHeader('White', 'White');
157
+ // this._chess.setHeader('Black', 'Black');
158
+ } catch {
159
+ // Ignore.
160
+ }
161
+
162
+ const current = this._chess.history();
163
+ if (!isValidNextMove(previous, current)) {
164
+ this._registry.set(this._pieces, {});
165
+ }
166
+
167
+ this._updateBoard(this._chess);
168
+ }
169
+
170
+ isValidMove(move: Move): boolean {
171
+ return tryMove(new ChessJS(this._chess.fen()), move) !== null;
172
+ }
173
+
174
+ canPromote(move: Move): boolean {
175
+ const isPawnMove = move.piece === 'BP' || move.piece === 'WP';
176
+ const isToLastRank = move.to[0] === 0 || move.to[0] === 7;
177
+ return isPawnMove && isToLastRank;
178
+ }
179
+
180
+ makeMove(move: Move): boolean {
181
+ const game = tryMove(this._chess, move);
182
+ if (!game) {
183
+ return false;
184
+ }
185
+
186
+ this._updateBoard(this._chess);
187
+ return true;
188
+ }
189
+
190
+ makeRandomMove(): boolean {
191
+ const moves = this._chess.moves();
192
+ if (!moves.length) {
193
+ return false;
194
+ }
195
+
196
+ const move = moves[Math.floor(Math.random() * moves.length)];
197
+ this._chess.move(move);
198
+
199
+ this._updateBoard(this._chess);
200
+ return true;
201
+ }
202
+
203
+ /**
204
+ * Update pieces preserving identity.
205
+ */
206
+ private _updateBoard(chess: ChessJS): void {
207
+ this._registry.set(this._pieces, createPieceMap(chess));
208
+ this._registry.set(this._moveIndex, chess.history().length);
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Attempt move.
214
+ */
215
+ const tryMove = (chess: ChessJS, move: Move): ChessJS | null => {
216
+ const from = locationToPos(move.from);
217
+ const to = locationToPos(move.to);
218
+ try {
219
+ const promotion = move.promotion ? move.promotion[1].toLowerCase() : 'q';
220
+ chess.move({ from, to, promotion }, { strict: false });
221
+ return chess;
222
+ } catch {
223
+ return null;
224
+ }
225
+ };
226
+
227
+ const isValidNextMove = (previous: string[], current: string[]) => {
228
+ if (current.length > previous.length + 1) {
229
+ return false;
230
+ }
231
+
232
+ for (let i = 0; i < previous.length; i++) {
233
+ if (previous[i] !== current[i]) {
234
+ return false;
235
+ }
236
+ }
237
+
238
+ return true;
239
+ };
240
+
241
+ /**
242
+ * Starting from a new game, assign piece IDs based on their starting position.
243
+ * Then iterate through the history of the provided game and update the piece map.
244
+ */
245
+ export const createPieceMap = (chess: ChessJS): PieceMap<ChessPiece> => {
246
+ const temp = new ChessJS();
247
+ let pieces = _createPieceMap(temp);
248
+ const history = chess.history({ verbose: true });
249
+ for (let i = 0; i < history.length; i++) {
250
+ const move = history[i];
251
+ temp.move(move);
252
+ pieces = _diffPieces(pieces, _createPieceMap(temp));
253
+ const test = new Set();
254
+ Object.values(pieces).forEach((piece) => {
255
+ invariant(!test.has(piece.id), 'Duplicate: ' + piece.id);
256
+ test.add(piece.id);
257
+ });
258
+ }
259
+
260
+ return pieces;
261
+ };
262
+
263
+ /**
264
+ * Create a map of pieces from the board positions; assign each piece the ID of the current square.
265
+ */
266
+ const _createPieceMap = (chess: ChessJS): PieceMap<ChessPiece> => {
267
+ const pieces: PieceMap<ChessPiece> = {};
268
+ chess.board().flatMap((row) =>
269
+ row.forEach((record) => {
270
+ if (!record) {
271
+ return;
272
+ }
273
+
274
+ const { square, type, color } = record;
275
+ const pieceType = `${color.toUpperCase()}${type.toUpperCase()}` as ChessPiece;
276
+ const location = posToLocation(square);
277
+ pieces[locationToString(location)] = {
278
+ id: `${square}-${pieceType}`,
279
+ type: pieceType,
280
+ side: color === 'w' ? 'white' : 'black',
281
+ location,
282
+ };
283
+ }),
284
+ );
285
+
286
+ return pieces;
287
+ };
288
+
289
+ /**
290
+ * Preserve the original piece objects (and IDs).
291
+ */
292
+ const _diffPieces = <T extends PieceType>(before: PieceMap<T>, after: PieceMap<T>): PieceMap<T> => {
293
+ const difference: { added: PieceMap; removed: PieceMap } = {
294
+ removed: {},
295
+ added: {},
296
+ };
297
+
298
+ // Removed.
299
+ (Object.keys(before) as Array<keyof typeof before>).forEach((square) => {
300
+ if (after[square]?.type !== before[square]?.type) {
301
+ difference.removed[square] = before[square];
302
+ }
303
+ });
304
+
305
+ // Added.
306
+ (Object.keys(after) as Array<keyof typeof after>).forEach((square) => {
307
+ if (before[square]?.type !== after[square]?.type) {
308
+ difference.added[square] = after[square];
309
+ } else {
310
+ after[square] = before[square];
311
+ }
312
+ });
313
+
314
+ // Preserve IDs.
315
+ for (const piece of Object.values(difference.added)) {
316
+ const previous = Object.values(difference.removed).find((p) => p.type === piece.type);
317
+ if (previous) {
318
+ piece.id = previous.id;
319
+ }
320
+ }
321
+
322
+ return after;
323
+ };
324
+
325
+ const createDate = (date = new Date()) => date.toISOString().slice(0, 10).replace(/-/g, '.'); // e.g., "2025.08.05"
@@ -0,0 +1,145 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
6
+ import { createContext } from '@radix-ui/react-context';
7
+ import React, { type PropsWithChildren, forwardRef, useCallback, useEffect, useState } from 'react';
8
+
9
+ import { log } from '@dxos/log';
10
+ import { type ThemedClassName } from '@dxos/react-ui';
11
+ import { mx } from '@dxos/ui-theme';
12
+
13
+ import { Piece, type PieceProps } from './Piece';
14
+ import { Square, type SquareProps } from './Square';
15
+ import { type GameboardModel, type Move, type PieceRecord, isLocation, isPiece } from './types';
16
+
17
+ export type GameboardContextValue<M extends GameboardModel<any>> = {
18
+ model: M;
19
+ dragging?: boolean; // TODO(burdon): Change to PieceRecord.
20
+ promoting?: PieceRecord;
21
+ onPromotion: (move: Move) => void;
22
+ };
23
+
24
+ const [GameboardContextProvider, useRadixGameboardContext] = createContext<GameboardContextValue<any>>('Gameboard');
25
+
26
+ const useGameboardContext = <M extends GameboardModel<any>>(consumerName: string): GameboardContextValue<M> => {
27
+ return useRadixGameboardContext(consumerName);
28
+ };
29
+
30
+ //
31
+ // Root
32
+ //
33
+
34
+ type GameboardRootProps<M extends GameboardModel<any>> = PropsWithChildren<{
35
+ model?: M;
36
+ moveNumber?: number;
37
+ onDrop?: (move: Move) => boolean;
38
+ }>;
39
+
40
+ /**
41
+ * Generic board container.
42
+ */
43
+ const GameboardRoot = <M extends GameboardModel<any>>({
44
+ children,
45
+ model,
46
+ moveNumber,
47
+ onDrop,
48
+ }: GameboardRootProps<M>) => {
49
+ const [dragging, setDragging] = useState(false);
50
+ const [promoting, setPromoting] = useState<PieceRecord | undefined>();
51
+
52
+ const handlePromotion = useCallback<GameboardContextValue<GameboardModel<any>>['onPromotion']>((move) => {
53
+ log('onPromotion', { move });
54
+ setPromoting(undefined);
55
+ onDrop?.(move);
56
+ }, []);
57
+
58
+ useEffect(() => {
59
+ if (!model) {
60
+ return;
61
+ }
62
+
63
+ // TODO(burdon): Should target specific container.
64
+ return monitorForElements({
65
+ onDragStart: ({ source }) => {
66
+ log('onDragStart', { source });
67
+ setDragging(true);
68
+ },
69
+ onDrop: ({ source, location }) => {
70
+ log('onDrop', { source, location });
71
+ const target = location.current.dropTargets[0];
72
+ if (!target) {
73
+ return;
74
+ }
75
+
76
+ const targetLocation = target.data.location;
77
+ const piece = source.data.piece;
78
+ if (!isLocation(targetLocation) || !isPiece(piece)) {
79
+ return;
80
+ }
81
+
82
+ const move: Move = { from: piece.location, to: targetLocation, piece: piece.type };
83
+ if (model.isValidMove(move)) {
84
+ if (model.canPromote?.(move)) {
85
+ setPromoting({ ...piece, location: targetLocation });
86
+ } else {
87
+ onDrop?.(move);
88
+ }
89
+ }
90
+
91
+ setDragging(false);
92
+ },
93
+ });
94
+ }, [model]);
95
+
96
+ return (
97
+ <GameboardContextProvider model={model} dragging={dragging} promoting={promoting} onPromotion={handlePromotion}>
98
+ {children}
99
+ </GameboardContextProvider>
100
+ );
101
+ };
102
+
103
+ GameboardRoot.displayName = 'Gameboard.Root';
104
+
105
+ //
106
+ // Content
107
+ //
108
+
109
+ type GameboardContentProps = ThemedClassName<PropsWithChildren<{ grow?: boolean; contain?: boolean }>>;
110
+
111
+ const GameboardContent = forwardRef<HTMLDivElement, GameboardContentProps>(
112
+ ({ children, classNames, grow, contain }, forwardedRef) => {
113
+ return (
114
+ <div
115
+ role='none'
116
+ className={mx(grow && 'grid is-full bs-full size-container place-content-center', classNames)}
117
+ ref={forwardedRef}
118
+ >
119
+ {contain ? <div className='is-[min(100cqw,100cqh)] bs-[min(100cqw,100cqh)]'>{children}</div> : children}
120
+ </div>
121
+ );
122
+ },
123
+ );
124
+
125
+ GameboardContent.displayName = 'Gameboard.Content';
126
+
127
+ //
128
+ // Gameboard
129
+ //
130
+
131
+ export const Gameboard = {
132
+ Root: GameboardRoot,
133
+ Content: GameboardContent,
134
+ Piece,
135
+ Square,
136
+ };
137
+
138
+ export { useGameboardContext };
139
+
140
+ export type {
141
+ GameboardRootProps,
142
+ GameboardContentProps,
143
+ PieceProps as GameboardPieceProps,
144
+ SquareProps as GameboardSquareProps,
145
+ };
@@ -3,34 +3,33 @@
3
3
  //
4
4
 
5
5
  import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
6
- // import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';
7
6
  import { centerUnderPointer } from '@atlaskit/pragmatic-drag-and-drop/element/center-under-pointer';
8
7
  import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
9
- import React, { useState, useRef, useEffect, type FC, type SVGProps, memo } from 'react';
8
+ import React, { type FC, type SVGProps, memo, useEffect, useRef, useState } from 'react';
10
9
  import { createPortal } from 'react-dom';
11
10
 
12
11
  import { invariant } from '@dxos/invariant';
13
12
  import { log } from '@dxos/log';
14
- import { useDynamicRef, useTrackProps, type ThemedClassName } from '@dxos/react-ui';
15
- import { mx } from '@dxos/react-ui-theme';
13
+ import { type ThemedClassName, useDynamicRef } from '@dxos/react-ui';
14
+ import { mx } from '@dxos/ui-theme';
16
15
 
17
- import { useBoardContext } from './context';
18
- import { isEqualLocation, isLocation, type Location, type PieceRecord, type Player } from './types';
16
+ import { useGameboardContext } from './Gameboard';
17
+ import { type Location, type PieceRecord, type Player, isEqualLocation, isLocation } from './types';
19
18
  import { type DOMRectBounds } from './util';
20
19
 
21
20
  export type PieceProps = ThemedClassName<{
21
+ Component: FC<SVGProps<SVGSVGElement>>;
22
22
  piece: PieceRecord;
23
23
  bounds: DOMRectBounds;
24
24
  label?: string;
25
25
  orientation?: Player;
26
- Component: FC<SVGProps<SVGSVGElement>>;
26
+ onClick?: () => void;
27
27
  }>;
28
28
 
29
- export const Piece = memo(({ classNames, piece, orientation, bounds, label, Component }: PieceProps) => {
30
- useTrackProps({ classNames, piece, orientation, bounds, label, Component }, Piece.displayName, false);
31
- const { model } = useBoardContext();
29
+ const PIECE_NAME = 'Piece';
32
30
 
33
- const { dragging: isDragging, promoting } = useBoardContext();
31
+ export const Piece = memo(({ classNames, Component, piece, bounds, label, onClick }: PieceProps) => {
32
+ const { model, dragging: isDragging, promoting } = useGameboardContext(PIECE_NAME);
34
33
  const promotingRef = useDynamicRef(promoting);
35
34
  const [dragging, setDragging] = useState(false);
36
35
  const [preview, setPreview] = useState<HTMLElement>();
@@ -40,20 +39,20 @@ export const Piece = memo(({ classNames, piece, orientation, bounds, label, Comp
40
39
 
41
40
  const ref = useRef<HTMLDivElement>(null);
42
41
  useEffect(() => {
42
+ if (!model) {
43
+ return;
44
+ }
45
+
43
46
  const el = ref.current;
44
47
  invariant(el);
45
48
 
46
49
  return draggable({
47
50
  element: el,
48
51
  getInitialData: () => ({ piece }),
49
- onGenerateDragPreview: ({ nativeSetDragImage, location, source }) => {
52
+ onGenerateDragPreview: ({ nativeSetDragImage, source }) => {
50
53
  log('onGenerateDragPreview', { source: source.data });
51
54
  setCustomNativeDragPreview({
52
55
  getOffset: centerUnderPointer,
53
- // getOffset: preserveOffsetOnSource({
54
- // element: source.element,
55
- // input: location.current.input,
56
- // }),
57
56
  render: ({ container }) => {
58
57
  setPreview(container);
59
58
  const { width, height } = el.getBoundingClientRect();
@@ -66,12 +65,17 @@ export const Piece = memo(({ classNames, piece, orientation, bounds, label, Comp
66
65
  nativeSetDragImage,
67
66
  });
68
67
  },
69
- canDrag: () => !promotingRef.current && model?.turn === piece.side,
68
+ canDrag: () => !promotingRef.current && !model.readonly && model.turn === piece.side,
70
69
  onDragStart: () => setDragging(true),
71
70
  onDrop: ({ location: { current } }) => {
72
- const location = current.dropTargets[0].data.location;
73
- if (isLocation(location)) {
74
- setCurrent((current) => ({ ...current, location }));
71
+ // TODO(burdon): Create wrapper function to catch errors.
72
+ try {
73
+ const location = current.dropTargets[0]?.data.location;
74
+ if (isLocation(location)) {
75
+ setCurrent((current) => ({ ...current, location }));
76
+ }
77
+ } catch {
78
+ // Ignore.
75
79
  }
76
80
 
77
81
  setDragging(false);
@@ -88,7 +92,7 @@ export const Piece = memo(({ classNames, piece, orientation, bounds, label, Comp
88
92
 
89
93
  // Check if piece moved.
90
94
  if (!current.location || !isEqualLocation(current.location, piece.location)) {
91
- ref.current.style.transition = 'top 400ms ease-out, left 400ms ease-out';
95
+ ref.current.style.transition = 'top 250ms ease-out, left 250ms ease-out';
92
96
  ref.current.style.top = bounds.top + 'px';
93
97
  ref.current.style.left = bounds.left + 'px';
94
98
  setCurrent({ location: piece.location, bounds });
@@ -112,10 +116,10 @@ export const Piece = memo(({ classNames, piece, orientation, bounds, label, Comp
112
116
  className={mx(
113
117
  'absolute',
114
118
  classNames,
115
- // orientation === 'black' && '_rotate-180',
116
119
  dragging && 'opacity-20', // Must not unmount component while dragging.
117
120
  isDragging && 'pointer-events-none', // Don't block the square's drop target.
118
121
  )}
122
+ onClick={onClick}
119
123
  >
120
124
  <Component className='grow' />
121
125
  {label && <div className='absolute inset-1 text-xs text-black'>{label}</div>}
@@ -132,4 +136,4 @@ export const Piece = memo(({ classNames, piece, orientation, bounds, label, Comp
132
136
  );
133
137
  });
134
138
 
135
- Piece.displayName = 'Piece';
139
+ Piece.displayName = PIECE_NAME;
@@ -3,15 +3,15 @@
3
3
  //
4
4
 
5
5
  import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
6
- import React, { useRef, useState, useEffect, memo } from 'react';
6
+ import React, { memo, useEffect, useRef, useState } from 'react';
7
7
 
8
8
  import { invariant } from '@dxos/invariant';
9
9
  import { log } from '@dxos/log';
10
10
  import { type ThemedClassName } from '@dxos/react-ui';
11
- import { mx } from '@dxos/react-ui-theme';
11
+ import { mx } from '@dxos/ui-theme';
12
12
 
13
- import { useBoardContext } from './context';
14
- import { isPiece, type Location } from './types';
13
+ import { useGameboardContext } from './Gameboard';
14
+ import { type Location, isPiece } from './types';
15
15
  import { type DOMRectBounds } from './util';
16
16
 
17
17
  type HoveredState = 'idle' | 'validMove' | 'invalidMove';
@@ -22,10 +22,12 @@ export type SquareProps = ThemedClassName<{
22
22
  label?: string;
23
23
  }>;
24
24
 
25
+ const SQUARE_NAME = 'Square';
26
+
25
27
  export const Square = memo(({ location, bounds, label, classNames }: SquareProps) => {
26
28
  const ref = useRef<HTMLDivElement>(null);
27
29
  const [state, setState] = useState<HoveredState>('idle');
28
- const { model } = useBoardContext();
30
+ const { model } = useGameboardContext(SQUARE_NAME);
29
31
 
30
32
  useEffect(() => {
31
33
  const el = ref.current;
@@ -71,4 +73,4 @@ export const Square = memo(({ location, bounds, label, classNames }: SquareProps
71
73
  );
72
74
  });
73
75
 
74
- Square.displayName = 'Square';
76
+ Square.displayName = SQUARE_NAME;
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './types';
6
+ export * from './util';
7
+
8
+ export * from './Gameboard';
@@ -2,8 +2,9 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type ReadonlySignal } from '@preact/signals-core';
5
+ import { type Atom } from '@effect-atom/atom-react';
6
6
 
7
+ // TODO(burdon): Don't make this assumption.
7
8
  export type Player = 'black' | 'white';
8
9
 
9
10
  export type Location = [number, number];
@@ -50,9 +51,10 @@ export const isEqualLocation = (l1: Location, l2: Location): boolean => l1[0] ==
50
51
  /**
51
52
  * Generic board model.
52
53
  */
53
- export interface BoardModel<T extends PieceType = PieceType> {
54
+ export interface GameboardModel<T extends PieceType = PieceType> {
55
+ readonly: boolean;
54
56
  turn: Player;
55
- pieces: ReadonlySignal<PieceMap<T>>;
57
+ pieces: Atom.Atom<PieceMap<T>>;
56
58
  isValidMove: (move: Move) => boolean;
57
59
  canPromote?: (move: Move) => boolean;
58
60
  }
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './Gameboard';
6
+ export * from './Chessboard';
package/src/index.ts CHANGED
@@ -2,5 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './Board';
6
- export * from './Chessboard';
5
+ export * from './components';