@mirasen/chessboard 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,20 @@
1
1
  # @mirasen/chessboard
2
2
 
3
+ ## 1.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 0422020: Fix fast drag-and-drop sometimes leaving a stale drag visual due to an unsynced target square.
8
+ - 0422020: Update dependencies
9
+ - 0422020: Fix promotion extension to accept renderer-compatible piece URL maps without throwing on extra entries.
10
+ - 0422020: Allow createBoard options to configure built-in extensions without direct factory calls.
11
+
12
+ ## 1.0.4
13
+
14
+ ### Patch Changes
15
+
16
+ - f586eea: Update README integration examples and export move-related public types.
17
+
3
18
  ## 1.0.3
4
19
 
5
20
  ### Patch Changes
package/README.md CHANGED
@@ -24,7 +24,7 @@ This is not just a board renderer. It is a chessboard platform designed for real
24
24
 
25
25
  ## What you get out of the box
26
26
 
27
- The built-in first-party extension baseline currently includes both foundational runtime-facing extensions and user-facing board features.
27
+ The default first-party extension baseline includes rendering, events, selection, active target feedback, legal move hints, last move feedback, promotion UI, and optional auto-promotion.
28
28
 
29
29
  - `renderer` — the first-party rendering extension that validates the same extension architecture used for board features
30
30
  - `events`
@@ -78,44 +78,48 @@ if (!element) {
78
78
  const board = createBoard({
79
79
  element
80
80
  });
81
- // Set free movability, default - disabled
81
+
82
+ // User moves are disabled by default.
83
+ // Enable free movement when you want unconstrained UI moves.
82
84
  board.setMovability({
83
85
  mode: 'free'
84
86
  });
85
87
  ```
86
88
 
87
- ### Explicit extensions
88
-
89
- ```ts
90
- import { createBoard, type Chessboard, type ChessboardExtensionInput } from '@mirasen/chessboard';
91
-
92
- const extensionList = [
93
- // Always add renderer first, otherwise nothing will be rendered
94
- 'renderer',
95
- 'lastMove',
96
- // Keep autoPromote before promotion, otherwise promotion may defer the move first
97
- 'autoPromote',
98
- 'promotion'
99
- // customExtensionDefinition
100
- ] as const satisfies readonly ChessboardExtensionInput[];
101
- let board: Chessboard<typeof extensionList> | null = null;
102
-
103
- const element = document.getElementById('board');
104
-
105
- if (!element) {
106
- throw new Error('Missing board element');
107
- }
89
+ ### Connect to a game/rules layer
108
90
 
109
- board = createBoard({
110
- element,
111
- extensions: extensionList
112
- });
113
- ```
91
+ `@mirasen/chessboard` handles board interaction and move intent. Your application can keep chess rules and game state in its own game/rules layer.
114
92
 
115
- ### setOnUIMove
93
+ A typical integration uses a small adapter between your game move format and the board move format. The `game` object below represents your own chess rules layer, such as chess.js, a server-backed game model, or a custom engine.
116
94
 
117
95
  ```ts
118
- import { createBoard } from '@mirasen/chessboard';
96
+ import {
97
+ createBoard,
98
+ type MoveDestinationInput,
99
+ type MoveOutput,
100
+ type MoveRequestInput
101
+ } from '@mirasen/chessboard';
102
+
103
+ type GameMove = {
104
+ from: string;
105
+ to: string;
106
+ capturedSquare?: string;
107
+ secondary?: {
108
+ from: string;
109
+ to: string;
110
+ };
111
+ promotedTo?: 'queen' | 'rook' | 'bishop' | 'knight';
112
+ };
113
+
114
+ type GameDestination = {
115
+ to: string;
116
+ capturedSquare?: string;
117
+ secondary?: {
118
+ from: string;
119
+ to: string;
120
+ };
121
+ promotedTo?: ('queen' | 'rook' | 'bishop' | 'knight')[];
122
+ };
119
123
 
120
124
  const element = document.getElementById('board');
121
125
 
@@ -123,95 +127,72 @@ if (!element) {
123
127
  throw new Error('Missing board element');
124
128
  }
125
129
 
130
+ const game = createYourGameEngine(); // Your app/game layer.
126
131
  const board = createBoard({
127
- element,
128
- state: {
129
- board: 'start',
130
- interaction: {
131
- movability: {
132
- mode: 'free'
133
- }
134
- }
135
- }
136
- });
137
-
138
- board.extensions.events.setOnUIMove((move) => {
139
- console.log('UI move:', move);
132
+ element
140
133
  });
141
- ```
142
-
143
- ### setMovability - strict with explicit destinations
144
134
 
145
- ```ts
146
- import { createBoard } from '@mirasen/chessboard';
135
+ board.setPosition(game.getPosition());
147
136
 
148
- const element = document.getElementById('board');
137
+ function toBoardDestination(gameDestination: GameDestination): MoveDestinationInput {
138
+ return {
139
+ to: gameDestination.to,
140
+ capturedSquare: gameDestination.capturedSquare,
141
+ secondary: gameDestination.secondary, // e.g. castling.
142
+ promotedTo: gameDestination.promotedTo
143
+ };
144
+ }
149
145
 
150
- if (!element) {
151
- throw new Error('Missing board element');
146
+ function toBoardMoveRequest(gameMove: GameMove): MoveRequestInput {
147
+ return {
148
+ from: gameMove.from,
149
+ to: gameMove.to,
150
+ capturedSquare: gameMove.capturedSquare,
151
+ secondary: gameMove.secondary,
152
+ promotedTo: gameMove.promotedTo
153
+ };
152
154
  }
153
155
 
154
- const board = createBoard({
155
- element
156
- });
156
+ function toGameMove(move: MoveOutput): GameMove {
157
+ return {
158
+ from: move.from,
159
+ to: move.to,
160
+ capturedSquare: move.capturedSquare,
161
+ secondary: move.secondary,
162
+ promotedTo: move.promotedTo
163
+ };
164
+ }
157
165
 
158
166
  board.setMovability({
159
167
  mode: 'strict',
160
- destinations: {
161
- e2: [{ to: 'e3' }, { to: 'e4' }],
162
- d7: [
163
- { to: 'c8', promotedTo: ['queen', 'rook', 'bishop', 'knight'] },
164
- { to: 'd8', promotedTo: ['queen', 'rook', 'bishop', 'knight'] }
165
- ],
166
- g1: [{ to: 'f3' }, { to: 'h3' }]
168
+ destinations: (source): MoveDestinationInput[] | undefined => {
169
+ const gameDestinations = game.getDestinationsFor(source);
170
+
171
+ return gameDestinations.length > 0 ? gameDestinations.map(toBoardDestination) : undefined;
167
172
  }
168
173
  });
169
- ```
170
-
171
- ### setMovability - strict with a resolver
172
174
 
173
- ```ts
174
- import { createBoard } from '@mirasen/chessboard';
175
+ board.extensions.events.setOnUIMove((move) => {
176
+ // The board accepted and applied this UI move.
177
+ // Keep your game/rules layer in sync.
178
+ game.move(toGameMove(move));
179
+ });
175
180
 
176
- const element = document.getElementById('board');
181
+ function applyExternalMove(gameMove: GameMove) {
182
+ // Apply moves that come from outside the board UI:
183
+ // server, engine, opponent, imported game, replay, etc.
184
+ game.move(gameMove);
177
185
 
178
- if (!element) {
179
- throw new Error('Missing board element');
186
+ // Then animate/apply the same move on the board.
187
+ board.move(toBoardMoveRequest(gameMove));
180
188
  }
189
+ ```
181
190
 
182
- const board = createBoard({
183
- element
184
- });
191
+ `MoveDestinationInput` can describe more than a target square. For example, `secondary` can represent paired piece movement (e.g. castling), `capturedSquare` can represent captures away from the destination square (e.g. en passant), and `promotedTo` can represent promotion choices.
185
192
 
186
- board.setMovability({
187
- mode: 'strict',
188
- destinations: (source) => {
189
- if (source === 'e2')
190
- return [
191
- { to: 'e8', promotedTo: ['bishop', 'queen'] },
192
- { to: 'd8', promotedTo: ['rook', 'knight', 'queen'] }
193
- ];
194
- if (source === 'f7')
195
- return [
196
- { to: 'f8', promotedTo: ['bishop', 'rook', 'knight', 'queen'] },
197
- { to: 'g8', promotedTo: ['bishop', 'rook', 'knight', 'queen'] }
198
- ];
199
- if (source === 'd7')
200
- return [
201
- { to: 'd1', promotedTo: ['bishop', 'knight'] },
202
- { to: 'e1', promotedTo: ['rook', 'knight', 'queen'] }
203
- ];
204
- if (source === 'c2')
205
- return [
206
- { to: 'c1', promotedTo: ['bishop', 'rook', 'knight', 'queen'] },
207
- { to: 'b1', promotedTo: ['bishop', 'rook', 'knight', 'queen'] }
208
- ];
209
- return undefined;
210
- }
211
- });
212
- ```
193
+ This lets your game/rules layer own chess legality and game state while the board owns interaction, selection, targeting, visual feedback, promotion UI, animation, and its own displayed position.
213
194
 
214
- ## Promotion and auto-promotion
195
+ ### Promotion and auto-promotion
215
196
 
216
197
  Promotion is handled inside the board interaction pipeline through a natural deferred move flow.
217
198
 
@@ -249,6 +230,44 @@ const board = createBoard({
249
230
  board.extensions.autoPromote.toQueen = true;
250
231
  ```
251
232
 
233
+ ### Movability modes
234
+
235
+ Movability controls how the board accepts user-initiated moves.
236
+
237
+ - `disabled` — user moves are ignored
238
+ - `free` — user moves are accepted without legal destination filtering
239
+ - `strict` — user moves are accepted only when the target is allowed by the destinations provided by your app/rules layer
240
+
241
+ ### Advanced: explicit extension lists
242
+
243
+ Most users can rely on the default first-party extension baseline. Use an explicit extension list only when you want to control which built-in or custom extensions are installed.
244
+
245
+ ```ts
246
+ import { createBoard, type Chessboard, type ChessboardExtensionInput } from '@mirasen/chessboard';
247
+
248
+ const extensionList = [
249
+ // Always add renderer first, otherwise nothing will be rendered
250
+ 'renderer',
251
+ 'lastMove',
252
+ // Keep autoPromote before promotion, otherwise promotion may defer the move first
253
+ 'autoPromote',
254
+ 'promotion'
255
+ // customExtensionDefinition
256
+ ] as const satisfies readonly ChessboardExtensionInput[];
257
+ let board: Chessboard<typeof extensionList> | null = null;
258
+
259
+ const element = document.getElementById('board');
260
+
261
+ if (!element) {
262
+ throw new Error('Missing board element');
263
+ }
264
+
265
+ board = createBoard({
266
+ element,
267
+ extensions: extensionList
268
+ });
269
+ ```
270
+
252
271
  ## Architecture
253
272
 
254
273
  `@mirasen/chessboard` is built around explicit boundaries:
@@ -1,3 +1,4 @@
1
+ export { cleanAnimationPlan, prepareAnimationPlan, renderAnimationPlan } from '../../animation/render/plan.js';
1
2
  export { clearElementChildren, createSvgElement, updateElementAttributes } from '../../render/svg/helpers.js';
2
3
  export type { RuntimeReadonlyMutationSession } from '../../runtime/mutation/types.js';
3
4
  export { isNonEmptyPieceCode } from '../../state/board/check.js';
@@ -1,3 +1,4 @@
1
+ export { cleanAnimationPlan, prepareAnimationPlan, renderAnimationPlan } from '../../animation/render/plan.js';
1
2
  export { clearElementChildren, createSvgElement, updateElementAttributes } from '../../render/svg/helpers.js';
2
3
  export { isNonEmptyPieceCode } from '../../state/board/check.js';
3
4
  export { fileOf, rankOf, squareOf } from '../../state/board/coords.js';
@@ -6,21 +6,23 @@ function assertPieceUrlsComplete(pieceUrls) {
6
6
  assert(pieceCode in pieceUrls, `Missing URL for piece code: ${pieceCode}`);
7
7
  }
8
8
  }
9
- function assertPieceCodeIsPromotionPieceCode(pieceCode) {
10
- assert(pieceCode === 5 /* PieceCode.WhiteQueen */ ||
9
+ function isPromotionPieceCode(pieceCode) {
10
+ return (pieceCode === 5 /* PieceCode.WhiteQueen */ ||
11
11
  pieceCode === 4 /* PieceCode.WhiteRook */ ||
12
12
  pieceCode === 3 /* PieceCode.WhiteBishop */ ||
13
13
  pieceCode === 2 /* PieceCode.WhiteKnight */ ||
14
14
  pieceCode === 13 /* PieceCode.BlackQueen */ ||
15
15
  pieceCode === 12 /* PieceCode.BlackRook */ ||
16
16
  pieceCode === 11 /* PieceCode.BlackBishop */ ||
17
- pieceCode === 10 /* PieceCode.BlackKnight */, `Invalid promotion piece code: ${pieceCode}`);
17
+ pieceCode === 10 /* PieceCode.BlackKnight */);
18
18
  }
19
19
  function normalizePromotionPieceUrls(input) {
20
20
  const normalized = {};
21
21
  for (const [key, url] of Object.entries(input)) {
22
22
  const pieceCode = normalizePiece(key);
23
- assertPieceCodeIsPromotionPieceCode(pieceCode);
23
+ if (!isPromotionPieceCode(pieceCode)) {
24
+ continue;
25
+ }
24
26
  normalized[pieceCode] = url;
25
27
  }
26
28
  assertPieceUrlsComplete(normalized);
@@ -1,6 +1,3 @@
1
- /**
2
- * Public built-in extension ids accepted by createBoard({ extensions })
3
- */
4
1
  import { createActiveTarget } from '../first-party/active-target/factory.js';
5
2
  import { createAutoPromote } from '../first-party/auto-promote/factory.js';
6
3
  import { createBoardEvents } from '../first-party/board-events/factory.js';
@@ -1,6 +1,3 @@
1
- /**
2
- * Public built-in extension ids accepted by createBoard({ extensions })
3
- */
4
1
  import { createActiveTarget } from '../first-party/active-target/factory.js';
5
2
  import { EXTENSION_ID as EXTENSION_ID_ACTIVE_TARGET } from '../first-party/active-target/types.js';
6
3
  import { createAutoPromote } from '../first-party/auto-promote/factory.js';
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type { MoveOutput } from './extensions/build/index.js';
2
+ export type { MoveRequestInput, PiecePositionRecordString } from './state/board/types/input.js';
3
+ export type { MoveDestinationInput } from './state/interaction/types/input.js';
1
4
  export { createBoard } from './wrapper/factory.js';
2
5
  export type { Chessboard, ChessboardExtensionInput, ChessboardInitOptions } from './wrapper/types.js';
3
- export type { MoveRequestInput, PiecePositionRecordString } from './state/board/types/input.js';
@@ -42,23 +42,24 @@ export function createRuntimeInteractionSurface(state) {
42
42
  const internalState = state();
43
43
  const mutationSession = internalState.mutation.getSession();
44
44
  const interaction = internalState.state.interaction;
45
+ interaction.updateDragSessionCurrentTarget(target, mutationSession);
45
46
  const dragSession = interaction.dragSession;
46
- assert(dragSession !== null, 'completeDragTo requires an active drag session');
47
47
  assert(isDragSessionCoreOwned(dragSession), 'completeDragTo can only be called for core-owned drag sessions');
48
48
  uiMoveCompleteTo(internalState, target);
49
- mutationSession.addMutation('runtime.interaction.completeDragTo', true, dragSession);
49
+ mutationSession.addMutation('runtime.interaction.completeCoreDragTo', true, dragSession);
50
50
  runtimeRunMutationPipeline(internalState);
51
51
  },
52
52
  completeExtensionDrag(target) {
53
53
  const internalState = state();
54
54
  const mutationSession = internalState.mutation.getSession();
55
55
  const interaction = internalState.state.interaction;
56
+ interaction.updateDragSessionCurrentTarget(target, mutationSession);
56
57
  const dragSession = interaction.dragSession;
57
- assert(dragSession !== null, 'completeExtensionDrag requires an active drag session');
58
58
  assert(!isDragSessionCoreOwned(dragSession), 'completeExtensionDrag can only be called for extension-owned drag sessions');
59
- assert(dragSession.targetSquare === target, 'The target square in completeExtensionDrag must match the current target square of the drag session');
60
59
  internalState.extensionSystem.completeDrag(dragSession);
61
60
  internalState.state.interaction.setDragSession(null, mutationSession);
61
+ mutationSession.addMutation('runtime.interaction.completeExtensionDragTo', true, dragSession);
62
+ runtimeRunMutationPipeline(internalState);
62
63
  },
63
64
  startReleaseTargetingDrag(source, target) {
64
65
  const internalState = state();
@@ -92,9 +93,7 @@ export function createRuntimeInteractionSurface(state) {
92
93
  const internalState = state();
93
94
  const interactionMutationSession = internalState.mutation.getSession();
94
95
  const interaction = internalState.state.interaction;
95
- const currentDragSession = interaction.dragSession;
96
- assert(currentDragSession !== null, 'No active drag session to update');
97
- internalState.state.interaction.updateDragSessionCurrentTarget(target, interactionMutationSession);
96
+ interaction.updateDragSessionCurrentTarget(target, interactionMutationSession);
98
97
  runtimeRunMutationPipeline(internalState);
99
98
  },
100
99
  onEvent(context) {
@@ -4,7 +4,7 @@ export const reconcileInteractionSelectionAfterBoardStateChange = (context, muta
4
4
  const { current } = context;
5
5
  if (!mutationSession.hasMutation({ prefixes: ['state.board.'] }) ||
6
6
  mutationSession.hasMutation({
7
- causes: ['state.interaction.clear', 'runtime.interaction.completeDragTo']
7
+ causes: ['state.interaction.clear', 'runtime.interaction.completeCoreDragTo']
8
8
  })) {
9
9
  return;
10
10
  }
@@ -3,10 +3,11 @@ import type { MutationSession, ReadonlyMutationSession } from '../../mutation/ty
3
3
  import type { BoardStateMutationPayloadByCause } from '../../state/board/mutation.js';
4
4
  import type { ChangeStateMutationPayloadByCause } from '../../state/change/mutation.js';
5
5
  import type { InteractionStateMutationPayloadByCause } from '../../state/interaction/mutation.js';
6
- import type { DragSessionCoreOwnedSnapshot } from '../../state/interaction/types/internal.js';
6
+ import type { DragSessionCoreOwnedSnapshot, DragSessionExtensionOwnedSnapshot } from '../../state/interaction/types/internal.js';
7
7
  import type { ViewStateMutationPayloadByCause } from '../../state/view/mutation.js';
8
8
  type RuntimeOwnMutationPayloadByCause = {
9
- 'runtime.interaction.completeDragTo': DragSessionCoreOwnedSnapshot;
9
+ 'runtime.interaction.completeCoreDragTo': DragSessionCoreOwnedSnapshot;
10
+ 'runtime.interaction.completeExtensionDragTo': DragSessionExtensionOwnedSnapshot;
10
11
  'runtime.interaction.resolveDeferredUIMoveRequest': undefined;
11
12
  'runtime.interaction.cancelDeferredUIMoveRequest': undefined;
12
13
  };
@@ -1,3 +1,3 @@
1
1
  import { DefaultBuiltinChessboardExtensions } from '../extensions/types/wrapper.js';
2
- import { Chessboard, ChessboardExtensionInput, ChessboardInitOptions } from './types.js';
2
+ import { type Chessboard, type ChessboardExtensionInput, type ChessboardInitOptions } from './types.js';
3
3
  export declare function createBoard<TExtensions extends readonly ChessboardExtensionInput[] = DefaultBuiltinChessboardExtensions>(options: ChessboardInitOptions<TExtensions>): Chessboard<TExtensions>;
@@ -1,5 +1,9 @@
1
1
  import { builtInExtensionFactoryMap, DefaultBuiltinChessboardExtensions } from '../extensions/types/wrapper.js';
2
2
  import { createRuntime } from '../runtime/factory/main.js';
3
+ function resolveBuiltInExtensionWithOptions(input) {
4
+ const factory = builtInExtensionFactoryMap[input.builtin];
5
+ return factory(input.options);
6
+ }
3
7
  export function createBoard(options) {
4
8
  // prepare init options for runtime
5
9
  const extensionsInput = options.extensions ?? DefaultBuiltinChessboardExtensions;
@@ -8,6 +12,10 @@ export function createBoard(options) {
8
12
  // built-in extension, convert to definition
9
13
  return builtInExtensionFactoryMap[ext]();
10
14
  }
15
+ if ('builtin' in ext) {
16
+ // built-in extension with options, convert to definition
17
+ return resolveBuiltInExtensionWithOptions(ext);
18
+ }
11
19
  // assume it's already a definition
12
20
  return ext;
13
21
  });
@@ -1,15 +1,25 @@
1
1
  import type { AnyExtensionDefinition, ExtensionDefinitionId, ExtensionDefinitionPublicApi } from '../extensions/types/extension.js';
2
- import { BuiltInExtensionDefinitionMap, BuiltInExtensionId, DefaultBuiltinChessboardExtensions } from '../extensions/types/wrapper.js';
3
- import { Runtime } from '../runtime/types/main.js';
4
- import { RuntimeStateInitOptions } from '../state/types.js';
2
+ import { type BuiltInExtensionDefinitionMap, type BuiltInExtensionId, type DefaultBuiltinChessboardExtensions, builtInExtensionFactoryMap } from '../extensions/types/wrapper.js';
3
+ import type { Runtime } from '../runtime/types/main.js';
4
+ import type { RuntimeStateInitOptions } from '../state/types.js';
5
5
  /**
6
6
  * What createBoard accepts in options.extensions
7
7
  */
8
- export type ChessboardExtensionInput = BuiltInExtensionId | AnyExtensionDefinition;
8
+ export type BuiltInExtensionFactoryInitOptions<TId extends BuiltInExtensionId> = Parameters<(typeof builtInExtensionFactoryMap)[TId]> extends [] ? never : NonNullable<Parameters<(typeof builtInExtensionFactoryMap)[TId]>[0]>;
9
+ type BuiltInExtensionIdsWithOptions = {
10
+ [TId in BuiltInExtensionId]: [BuiltInExtensionFactoryInitOptions<TId>] extends [never] ? never : TId;
11
+ }[BuiltInExtensionId];
12
+ export type BuiltInExtensionInitOptions = {
13
+ [TId in BuiltInExtensionIdsWithOptions]: {
14
+ builtin: TId;
15
+ options: BuiltInExtensionFactoryInitOptions<TId>;
16
+ };
17
+ }[BuiltInExtensionIdsWithOptions];
18
+ export type ChessboardExtensionInput = BuiltInExtensionId | BuiltInExtensionInitOptions | AnyExtensionDefinition;
9
19
  /**
10
20
  * Resolve one public input item into a concrete extension definition type
11
21
  */
12
- export type ResolveChessboardExtensionInput<T> = T extends keyof BuiltInExtensionDefinitionMap ? BuiltInExtensionDefinitionMap[T] : T extends AnyExtensionDefinition ? T : never;
22
+ export type ResolveChessboardExtensionInput<T> = T extends keyof BuiltInExtensionDefinitionMap ? BuiltInExtensionDefinitionMap[T] : T extends BuiltInExtensionInitOptions ? T['builtin'] extends keyof BuiltInExtensionDefinitionMap ? BuiltInExtensionDefinitionMap[T['builtin']] : never : T extends AnyExtensionDefinition ? T : never;
13
23
  /**
14
24
  * Resolve an input tuple into a tuple of concrete extension definitions
15
25
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mirasen/chessboard",
3
3
  "description": "Framework-agnostic TypeScript chessboard platform with real chess interaction built in.",
4
- "version": "1.0.3",
4
+ "version": "1.0.5",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Mirasen",