@dxos/react-ui-gameboard 0.7.5-labs.e27f9b9 → 0.7.5-main.e9bb01b

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-gameboard",
3
- "version": "0.7.5-labs.e27f9b9",
3
+ "version": "0.7.5-main.e9bb01b",
4
4
  "description": "Game board.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -24,11 +24,11 @@
24
24
  "@preact/signals-core": "^1.6.0",
25
25
  "chess.js": "^1.0.0",
26
26
  "react-resize-detector": "^11.0.1",
27
- "@dxos/debug": "0.7.5-labs.e27f9b9",
28
- "@dxos/invariant": "0.7.5-labs.e27f9b9",
29
- "@dxos/log": "0.7.5-labs.e27f9b9",
30
- "@dxos/util": "0.7.5-labs.e27f9b9",
31
- "@dxos/node-std": "0.7.5-labs.e27f9b9"
27
+ "@dxos/invariant": "0.7.5-main.e9bb01b",
28
+ "@dxos/log": "0.7.5-main.e9bb01b",
29
+ "@dxos/util": "0.7.5-main.e9bb01b",
30
+ "@dxos/node-std": "0.7.5-main.e9bb01b",
31
+ "@dxos/debug": "0.7.5-main.e9bb01b"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@svgr/cli": "^8.1.0",
@@ -39,15 +39,15 @@
39
39
  "react": "~18.2.0",
40
40
  "react-dom": "~18.2.0",
41
41
  "vite": "5.4.7",
42
- "@dxos/react-ui": "0.7.5-labs.e27f9b9",
43
- "@dxos/react-ui-theme": "0.7.5-labs.e27f9b9",
44
- "@dxos/storybook-utils": "0.7.5-labs.e27f9b9"
42
+ "@dxos/react-ui": "0.7.5-main.e9bb01b",
43
+ "@dxos/storybook-utils": "0.7.5-main.e9bb01b",
44
+ "@dxos/react-ui-theme": "0.7.5-main.e9bb01b"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "react": "~18.2.0",
48
48
  "react-dom": "~18.2.0",
49
- "@dxos/react-ui": "0.7.5-labs.e27f9b9",
50
- "@dxos/react-ui-theme": "0.7.5-labs.e27f9b9"
49
+ "@dxos/react-ui": "0.7.5-main.e9bb01b",
50
+ "@dxos/react-ui-theme": "0.7.5-main.e9bb01b"
51
51
  },
52
52
  "publishConfig": {
53
53
  "access": "public"
@@ -3,15 +3,15 @@
3
3
  //
4
4
 
5
5
  import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
6
- import React, { type PropsWithChildren, useCallback, useEffect, useState } from 'react';
6
+ import React, { type PropsWithChildren, useEffect, useState } from 'react';
7
7
 
8
8
  import { log } from '@dxos/log';
9
9
  import { type ThemedClassName } from '@dxos/react-ui';
10
10
  import { mx } from '@dxos/react-ui-theme';
11
11
 
12
12
  import { Container } from './Container';
13
- import { BoardContext, type BoardContextType } from './context';
14
- import { type BoardModel, isLocation, isPiece, type Move, type PieceRecord } from './types';
13
+ import { BoardContext } from './context';
14
+ import { type BoardModel, isLocation, isPiece, type Move } from './types';
15
15
 
16
16
  type RootProps = ThemedClassName<
17
17
  PropsWithChildren<{
@@ -25,20 +25,8 @@ type RootProps = ThemedClassName<
25
25
  */
26
26
  const Root = ({ children, classNames, model, onDrop }: RootProps) => {
27
27
  const [dragging, setDragging] = useState(false);
28
- const [promoting, setPromoting] = useState<PieceRecord | undefined>();
29
-
30
- // Handle promotion.
31
- const onPromotion = useCallback<BoardContextType['onPromotion']>((move) => {
32
- log('onPromotion', { move });
33
- setPromoting(undefined);
34
- onDrop?.(move);
35
- }, []);
36
28
 
37
29
  useEffect(() => {
38
- if (!model) {
39
- return;
40
- }
41
-
42
30
  // TODO(burdon): Should target specific container.
43
31
  return monitorForElements({
44
32
  onDragStart: ({ source }) => {
@@ -58,20 +46,14 @@ const Root = ({ children, classNames, model, onDrop }: RootProps) => {
58
46
  return;
59
47
  }
60
48
 
61
- const move: Move = { from: piece.location, to: targetLocation, piece: piece.type };
62
- if (model.canPromote?.(move)) {
63
- setPromoting({ ...piece, location: targetLocation });
64
- } else {
65
- onDrop?.(move);
66
- }
67
-
49
+ onDrop?.({ source: piece.location, target: targetLocation, piece: piece.type });
68
50
  setDragging(false);
69
51
  },
70
52
  });
71
- }, [model]);
53
+ }, []);
72
54
 
73
55
  return (
74
- <BoardContext.Provider value={{ model, dragging, promoting, onPromotion }}>
56
+ <BoardContext.Provider value={{ model, dragging }}>
75
57
  <Container classNames={mx('aspect-square', classNames)}>{children}</Container>
76
58
  </BoardContext.Provider>
77
59
  );
@@ -11,7 +11,7 @@ import { createPortal } from 'react-dom';
11
11
 
12
12
  import { invariant } from '@dxos/invariant';
13
13
  import { log } from '@dxos/log';
14
- import { useDynamicRef, useTrackProps, type ThemedClassName } from '@dxos/react-ui';
14
+ import { useTrackProps, type ThemedClassName } from '@dxos/react-ui';
15
15
  import { mx } from '@dxos/react-ui-theme';
16
16
 
17
17
  import { useBoardContext } from './context';
@@ -30,8 +30,7 @@ export const Piece = memo(({ classNames, piece, orientation, bounds, label, Comp
30
30
  useTrackProps({ classNames, piece, orientation, bounds, label, Component }, Piece.displayName, false);
31
31
  const { model } = useBoardContext();
32
32
 
33
- const { dragging: isDragging, promoting } = useBoardContext();
34
- const promotingRef = useDynamicRef(promoting);
33
+ const { dragging: isDragging } = useBoardContext();
35
34
  const [dragging, setDragging] = useState(false);
36
35
  const [preview, setPreview] = useState<HTMLElement>();
37
36
 
@@ -66,20 +65,20 @@ 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: () => model?.turn === piece.side,
70
69
  onDragStart: () => setDragging(true),
71
- onDrop: ({ location: { current } }) => {
72
- const location = current.dropTargets[0].data.location;
73
- if (isLocation(location)) {
74
- setCurrent((current) => ({ ...current, location }));
70
+ onDrop: ({ location }) => {
71
+ const drop = location.current.dropTargets[0].data;
72
+ const loc = drop.location;
73
+ if (isLocation(loc)) {
74
+ setCurrent((current) => ({ ...current, location: loc }));
75
75
  }
76
-
77
76
  setDragging(false);
78
77
  },
79
78
  });
80
79
  }, [model, piece]);
81
80
 
82
- // Must update position independently of render cycle (otherwise animation is interrupted).
81
+ // Update position independently of render cycle (otherwise animation is interrupted).
83
82
  useEffect(() => {
84
83
  requestAnimationFrame(() => {
85
84
  if (!ref.current || !bounds) {
@@ -45,7 +45,7 @@ export const Square = memo(({ location, bounds, label, classNames }: SquareProps
45
45
  return;
46
46
  }
47
47
 
48
- if (model?.isValidMove({ from: piece.location, to: location, piece: piece.type })) {
48
+ if (model?.isValidMove({ source: piece.location, target: location, piece: piece.type })) {
49
49
  setState('validMove');
50
50
  } else {
51
51
  setState('invalidMove');
@@ -6,16 +6,9 @@ import { createContext, useContext } from 'react';
6
6
 
7
7
  import { raise } from '@dxos/debug';
8
8
 
9
- import { type PieceRecord, type BoardModel, type Move } from './types';
9
+ import { type BoardModel } from './types';
10
10
 
11
- export type BoardContextType = {
12
- model?: BoardModel;
13
- dragging?: boolean; // TODO(burdon): Change to PieceRecord.
14
- promoting?: PieceRecord;
15
- onPromotion: (move: Move) => void;
16
- };
17
-
18
- export const BoardContext = createContext<BoardContextType | undefined>(undefined);
11
+ export const BoardContext = createContext<{ model?: BoardModel; dragging: boolean } | undefined>(undefined);
19
12
 
20
13
  export const useBoardContext = () => {
21
14
  return useContext(BoardContext) ?? raise(new Error('Missing BoardContext'));
@@ -7,6 +7,5 @@ export * from './types';
7
7
  export * from './util';
8
8
 
9
9
  export * from './Board';
10
- export * from './Container';
11
10
  export * from './Square';
12
11
  export * from './Piece';
@@ -23,8 +23,8 @@ export type PieceRecord<T extends PieceType = PieceType> = {
23
23
  export type PieceMap<T extends PieceType = PieceType> = Record<string, PieceRecord<T>>;
24
24
 
25
25
  export type Move = {
26
- from: Location;
27
- to: Location;
26
+ source: Location;
27
+ target: Location;
28
28
  piece: PieceType;
29
29
  promotion?: PieceType;
30
30
  };
@@ -47,12 +47,8 @@ export const isLocation = (token: unknown): token is Location =>
47
47
 
48
48
  export const isEqualLocation = (l1: Location, l2: Location): boolean => l1[0] === l2[0] && l1[1] === l2[1];
49
49
 
50
- /**
51
- * Generic board model.
52
- */
53
50
  export interface BoardModel<T extends PieceType = PieceType> {
54
51
  turn: Player;
55
52
  pieces: ReadonlySignal<PieceMap<T>>;
56
53
  isValidMove: (move: Move) => boolean;
57
- canPromote?: (move: Move) => boolean;
58
54
  }
@@ -5,9 +5,8 @@
5
5
  import '@dxos-theme';
6
6
 
7
7
  import type { Meta, StoryObj } from '@storybook/react';
8
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
8
+ import React, { useCallback, useMemo, useState } from 'react';
9
9
 
10
- import { log } from '@dxos/log';
11
10
  import { Button, Toolbar } from '@dxos/react-ui';
12
11
  import { withLayout, withTheme } from '@dxos/storybook-utils';
13
12
 
@@ -23,13 +22,7 @@ const Render = ({ fen, orientation: _orientation, ...props }: RenderProps) => {
23
22
  const model = useMemo(() => new ChessModel(fen), [fen]);
24
23
  const [orientation, setOrientation] = useState<Player | undefined>(_orientation);
25
24
 
26
- const handleDrop = useCallback<NonNullable<BoardRootProps['onDrop']>>(
27
- (move: Move) => {
28
- log.info('handleDrop', { move });
29
- return model.makeMove(move);
30
- },
31
- [model],
32
- );
25
+ const handleDrop = useCallback<NonNullable<BoardRootProps['onDrop']>>((move: Move) => model.makeMove(move), [model]);
33
26
 
34
27
  return (
35
28
  <div className='flex flex-col grow gap-2 overflow-hidden'>
@@ -50,36 +43,11 @@ const Render = ({ fen, orientation: _orientation, ...props }: RenderProps) => {
50
43
  );
51
44
  };
52
45
 
53
- const Grid = (props: RenderProps) => {
54
- const models = useMemo(() => Array.from({ length: 9 }).map(() => new ChessModel()), []);
55
- useEffect(() => {
56
- const i = setInterval(() => {
57
- const model = models[Math.floor(Math.random() * models.length)];
58
- model.makeRandomMove();
59
- }, 100);
60
- return () => clearInterval(i);
61
- }, []);
62
-
63
- return (
64
- <div className='h-full aspect-square mx-auto'>
65
- <div className='grid grid-cols-3 gap-2'>
66
- {models.map((model, i) => (
67
- <div key={i} className='aspect-square'>
68
- <Board.Root model={model}>
69
- <Chessboard />
70
- </Board.Root>
71
- </div>
72
- ))}
73
- </div>
74
- </div>
75
- );
76
- };
77
-
78
46
  const meta: Meta<typeof Render> = {
79
47
  title: 'ui/react-ui-gameboard/Chessboard',
80
48
  component: Chessboard,
81
49
  render: Render,
82
- decorators: [withTheme, withLayout({ fullscreen: true, classNames: '' })],
50
+ decorators: [withTheme, withLayout({ fullscreen: true })],
83
51
  };
84
52
 
85
53
  export default meta;
@@ -88,12 +56,6 @@ type Story = StoryObj<typeof Render>;
88
56
 
89
57
  export const Default: Story = {};
90
58
 
91
- export const Promotion: Story = {
92
- args: {
93
- fen: '4k3/7P/8/8/8/8/1p6/4K3 w - - 0 1',
94
- },
95
- };
96
-
97
59
  export const Debug: Story = {
98
60
  args: {
99
61
  debug: true,
@@ -102,7 +64,3 @@ export const Debug: Story = {
102
64
  fen: 'q3k1nr/1pp1nQpp/3p4/1P2p3/4P3/B1PP1b2/B5PP/5K2 b k - 0 17',
103
65
  },
104
66
  };
105
-
106
- export const Nine: Story = {
107
- render: Grid,
108
- };
@@ -6,10 +6,8 @@ import React, { type PropsWithChildren, useRef, useMemo, useEffect, useState, me
6
6
  import { useResizeDetector } from 'react-resize-detector';
7
7
 
8
8
  import { useTrackProps } from '@dxos/react-ui';
9
- import { mx } from '@dxos/react-ui-theme';
10
- import { isNotFalsy } from '@dxos/util';
11
9
 
12
- import { boardStyles, type ChessPiece, ChessPieces, getSquareColor, locationToPos } from './chess';
10
+ import { type ChessPiece, ChessPieces, getSquareColor, locationToPos } from './chess';
13
11
  import {
14
12
  type DOMRectBounds,
15
13
  type Location,
@@ -36,7 +34,7 @@ export type ChessboardProps = PropsWithChildren<{
36
34
  export const Chessboard = memo(({ orientation, showLabels, debug, rows = 8, cols = 8 }: ChessboardProps) => {
37
35
  useTrackProps({ orientation, showLabels, debug }, Chessboard.displayName, false);
38
36
  const { ref: containerRef, width, height } = useResizeDetector({ refreshRate: 200 });
39
- const { model, promoting, onPromotion } = useBoardContext();
37
+ const { model } = useBoardContext();
40
38
 
41
39
  const locations = useMemo<Location[]>(() => {
42
40
  return Array.from({ length: rows }, (_, i) => (orientation === 'black' ? i : rows - 1 - i)).flatMap((row) =>
@@ -75,22 +73,16 @@ export const Chessboard = memo(({ orientation, showLabels, debug, rows = 8, cols
75
73
  }, [locations, width, height]);
76
74
 
77
75
  // Get the bounds of each square and piece.
78
- const positions = useMemo<{ piece: PieceRecord; bounds: DOMRectBounds }[]>(() => {
76
+ const positions = useMemo<{ bounds: DOMRectBounds; piece: PieceRecord }[]>(() => {
79
77
  if (!gridRef.current) {
80
78
  return [];
81
79
  }
82
80
 
83
- return Object.values(model?.pieces.value ?? {})
84
- .map((piece) => {
85
- if (piece.id === promoting?.id) {
86
- return null;
87
- }
88
-
89
- const bounds = grid[locationToString(piece.location)];
90
- return { piece, bounds };
91
- })
92
- .filter(isNotFalsy);
93
- }, [grid, model?.pieces.value, promoting]);
81
+ return Object.values(model?.pieces.value ?? {}).map((piece) => {
82
+ const bounds = grid[locationToString(piece.location)];
83
+ return { bounds, piece };
84
+ });
85
+ }, [grid, model?.pieces.value]);
94
86
 
95
87
  return (
96
88
  <div ref={containerRef} className='relative'>
@@ -108,7 +100,7 @@ export const Chessboard = memo(({ orientation, showLabels, debug, rows = 8, cols
108
100
  />
109
101
  ))}
110
102
  </div>
111
- <div className={mx(promoting && 'opacity-50')}>
103
+ <div className='grow'>
112
104
  {positions.map(({ bounds, piece }) => (
113
105
  <Piece
114
106
  key={piece.id}
@@ -120,22 +112,6 @@ export const Chessboard = memo(({ orientation, showLabels, debug, rows = 8, cols
120
112
  />
121
113
  ))}
122
114
  </div>
123
- <div>
124
- {promoting && (
125
- <PromotionSelector
126
- grid={grid}
127
- piece={promoting}
128
- onSelect={(piece) => {
129
- onPromotion({
130
- from: Object.values(model!.pieces.value).find((p) => p.id === promoting.id)!.location,
131
- to: piece.location,
132
- piece: promoting.type,
133
- promotion: piece.type,
134
- });
135
- }}
136
- />
137
- )}
138
- </div>
139
115
  </div>
140
116
  );
141
117
  });
@@ -145,46 +121,3 @@ Chessboard.displayName = 'Chessboard';
145
121
  const getSquareLocation = (container: HTMLElement, location: Location): HTMLElement | null => {
146
122
  return container.querySelector(`[data-location="${locationToString(location)}"]`);
147
123
  };
148
-
149
- const PromotionSelector = ({
150
- grid,
151
- piece,
152
- onSelect,
153
- }: {
154
- grid: Record<string, DOMRectBounds>;
155
- piece: PieceRecord;
156
- onSelect: (piece: PieceRecord) => void;
157
- }) => {
158
- const positions = ['Q', 'N', 'R', 'B'].map((pieceType, i) => {
159
- const location = [piece.location[0] + (piece.location[0] === 0 ? i : -i), piece.location[1]] as Location;
160
- return {
161
- piece: {
162
- id: `promotion-${pieceType}`,
163
- type: (piece.side === 'black' ? 'B' : 'W') + pieceType,
164
- side: piece.side,
165
- location,
166
- },
167
- bounds: grid[locationToString(location)],
168
- };
169
- });
170
-
171
- const handleSelect = (selected: PieceRecord) => {
172
- onSelect({ ...piece, type: selected.type });
173
- };
174
-
175
- // TODO(burdon): Circle.
176
- return (
177
- <div>
178
- {positions.map(({ piece, bounds }) => (
179
- <div key={piece.id} style={bounds} onClick={() => handleSelect(piece)}>
180
- <Piece
181
- piece={piece}
182
- bounds={bounds}
183
- Component={ChessPieces[piece.type as ChessPiece]}
184
- classNames={mx('border-2 border-neutral-700 rounded-full', boardStyles.promotion)}
185
- />
186
- </div>
187
- ))}
188
- </div>
189
- );
190
- };
@@ -33,45 +33,30 @@ export const locationToPos = ([row, col]: Location): string => {
33
33
  return String.fromCharCode(col + 'a'.charCodeAt(0)) + (row + 1);
34
34
  };
35
35
 
36
- const styles = {
36
+ export const styles = {
37
37
  neutral: {
38
- black: 'bg-neutral-50',
39
38
  white: 'bg-neutral-200',
40
- promotion: 'bg-neutral-200 hover:bg-neutral-300 opacity-70 hover:opacity-100',
41
- },
42
- original: {
43
- black: 'bg-[#6C95B9]',
44
- white: 'bg-[#CCD3DB]',
45
- promotion: 'duration-500 bg-[#CCD3DB] opacity-70 hover:opacity-100',
39
+ black: 'bg-neutral-50',
46
40
  },
47
41
  blue: {
48
- black: 'bg-[#608BC1]',
49
- white: 'bg-[#CBDCEB]',
50
- promotion: 'duration-500 bg-[#CBDCEB] opacity-70 hover:opacity-100',
51
- },
52
- green: {
53
- black: 'bg-[#8EB486]',
54
- white: 'bg-[#FDF7F4]',
55
- promotion: 'duration-500 bg-[#FDF7F4] opacity-70 hover:opacity-100',
42
+ white: 'bg-[#ccd3db]',
43
+ black: 'bg-[#6c95b9]',
56
44
  },
57
45
  };
58
46
 
59
- export const boardStyles = styles.original;
60
-
61
47
  export const getSquareColor = ([row, col]: Location) => {
62
- return (col + row) % 2 === 0 ? boardStyles.white : boardStyles.black;
48
+ return (col + row) % 2 === 0 ? styles.blue.white : styles.blue.black;
63
49
  };
64
50
 
65
51
  /**
66
52
  * Attempt move.
67
53
  */
68
- const makeMove = (game: Chess, move: Move): Chess | null => {
69
- const from = locationToPos(move.from);
70
- const to = locationToPos(move.to);
54
+ const makeMove = (game: Chess, { source, target }: Move): Chess | null => {
55
+ const s = locationToPos(source);
56
+ const t = locationToPos(target);
71
57
  try {
72
- log('makeMove', { move });
73
- const promotion = move.promotion ? move.promotion[1].toLowerCase() : 'q';
74
- game.move({ from, to, promotion }, { strict: false });
58
+ log('makeMove', { s, t });
59
+ game.move({ from: s, to: t }, { strict: false });
75
60
  return game;
76
61
  } catch (err) {
77
62
  // Ignore.
@@ -116,12 +101,6 @@ export class ChessModel implements BoardModel<ChessPiece> {
116
101
  return makeMove(new Chess(this._game.fen()), move) !== null;
117
102
  }
118
103
 
119
- canPromote(move: Move): boolean {
120
- const isPawnMove = move.piece === 'BP' || move.piece === 'WP';
121
- const isToLastRank = move.to[0] === 0 || move.to[0] === 7;
122
- return isPawnMove && isToLastRank;
123
- }
124
-
125
104
  makeMove(move: Move): boolean {
126
105
  const game = makeMove(this._game, move);
127
106
  if (!game) {