@mirasen/chessboard 1.0.4 → 1.0.6

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.6
4
+
5
+ ### Patch Changes
6
+
7
+ - e81b3fd: Fix scheduler `flushNow()` so pending frames flush synchronously.
8
+
9
+ ## 1.0.5
10
+
11
+ ### Patch Changes
12
+
13
+ - 0422020: Fix fast drag-and-drop sometimes leaving a stale drag visual due to an unsynced target square.
14
+ - 0422020: Update dependencies
15
+ - 0422020: Fix promotion extension to accept renderer-compatible piece URL maps without throwing on extra entries.
16
+ - 0422020: Allow createBoard options to configure built-in extensions without direct factory calls.
17
+
3
18
  ## 1.0.4
4
19
 
5
20
  ### Patch Changes
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  [![NPM Version](https://img.shields.io/npm/v/%40mirasen%2Fchessboard)](https://www.npmjs.com/package/@mirasen/chessboard)
2
2
  [![CI](https://github.com/mirasen-io/chessboard/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mirasen-io/chessboard/actions/workflows/ci.yml)
3
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mirasen-io_chessboard&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=mirasen-io_chessboard)
4
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=mirasen-io_chessboard&metric=coverage)](https://sonarcloud.io/summary/new_code?id=mirasen-io_chessboard)
3
5
  [![License](https://img.shields.io/npm/l/@mirasen/chessboard)](./LICENSE)
4
6
 
5
7
  # @mirasen/chessboard
@@ -37,9 +39,15 @@ The default first-party extension baseline includes rendering, events, selection
37
39
 
38
40
  That means the board already covers a meaningful baseline chess UX instead of stopping at “draw squares and pieces”.
39
41
 
42
+ ### Built-in chessboard behavior
43
+
44
+ Mirasen Chessboard provides a complete modern interaction and visual baseline out of the box: selection, deselection, reselection, drag-to-move, release targeting, legal destinations, promotion, auto-promotion, and state-diff animation. Moves are resolved on release rather than committed immediately on press, giving users a more forgiving interaction flow. Because animation is driven by board-state transitions, pieces can move, appear, disappear, promote, or return smoothly without app-level glue code.
45
+
40
46
  ## Highlights
41
47
 
42
48
  - Real built-in chess interaction
49
+ - Easy game/rules layer integration
50
+ - State-diff animation beyond simple move events
43
51
  - Extension-driven architecture
44
52
  - First-party baseline extensions
45
53
  - Explicit SVG slot ownership
@@ -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';
@@ -3,16 +3,14 @@ export function createScheduler(opts) {
3
3
  let scheduled = false;
4
4
  let runId = 0;
5
5
  let rafHandle = null;
6
- const flushCore = () => {
7
- if (!scheduled && rafHandle == null) {
6
+ const flushCore = (force = false) => {
7
+ if (!force && !scheduled && rafHandle == null) {
8
8
  // Maybe was cancelled after the rAF callback was scheduled but before it fired.
9
9
  return;
10
10
  }
11
11
  // Reset scheduled flags first to allow schedule() during render to queue the next frame.
12
12
  scheduled = false;
13
- if (rafHandle != null) {
14
- rafHandle = null;
15
- }
13
+ rafHandle = null;
16
14
  render();
17
15
  };
18
16
  return {
@@ -29,17 +27,16 @@ export function createScheduler(opts) {
29
27
  });
30
28
  },
31
29
  flushNow() {
30
+ if (!scheduled && rafHandle == null)
31
+ return;
32
32
  // Invalidate any pending rAF render immediately.
33
- if (scheduled) {
34
- runId++;
35
- scheduled = false;
36
- }
33
+ runId++;
37
34
  if (rafHandle != null) {
38
35
  const caf = globalThis.cancelAnimationFrame;
39
36
  caf(rafHandle);
40
37
  rafHandle = null;
41
38
  }
42
- flushCore();
39
+ flushCore(true);
43
40
  },
44
41
  cancel() {
45
42
  scheduled = false;
@@ -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) {
@@ -19,7 +19,7 @@ export function createInputAdapter(options) {
19
19
  internalState.container.addEventListener(type, onEventHander);
20
20
  },
21
21
  unsubscribeEvent(type) {
22
- assert(NEED_EVENT_TYPES.has(type), `Unsubscribe for event type ${type} is not supported. This is required event type for the board to function properly.`);
22
+ assert(!NEED_EVENT_TYPES.has(type), `Unsubscribe for event type ${type} is not supported. This is required event type for the board to function properly.`);
23
23
  internalState.container.removeEventListener(type, onEventHander);
24
24
  },
25
25
  destroy() {
@@ -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.4",
4
+ "version": "1.0.6",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Mirasen",