@mirasen/chessboard 1.2.3 → 1.3.0

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 (73) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +9 -0
  3. package/dist/animation/planner.js +2 -1
  4. package/dist/extensions/build/index.d.ts +1 -1
  5. package/dist/extensions/first-party/annotations/interaction.js +4 -2
  6. package/dist/extensions/first-party/annotations/update.js +1 -1
  7. package/dist/extensions/first-party/board-events/factory.js +2 -2
  8. package/dist/extensions/first-party/main-renderer/animation/factory.d.ts +2 -1
  9. package/dist/extensions/first-party/main-renderer/animation/factory.js +3 -2
  10. package/dist/extensions/first-party/main-renderer/animation/types.d.ts +2 -0
  11. package/dist/extensions/first-party/main-renderer/animation/update.js +4 -2
  12. package/dist/extensions/first-party/main-renderer/board/factory.d.ts +1 -1
  13. package/dist/extensions/first-party/main-renderer/board/factory.js +4 -4
  14. package/dist/extensions/first-party/main-renderer/board/render.js +1 -1
  15. package/dist/extensions/first-party/main-renderer/board/types.d.ts +1 -1
  16. package/dist/extensions/first-party/main-renderer/coordinates/factory.d.ts +1 -1
  17. package/dist/extensions/first-party/main-renderer/coordinates/factory.js +2 -2
  18. package/dist/extensions/first-party/main-renderer/coordinates/render.js +1 -1
  19. package/dist/extensions/first-party/main-renderer/coordinates/types.d.ts +1 -1
  20. package/dist/extensions/first-party/main-renderer/denormalize.d.ts +3 -0
  21. package/dist/extensions/first-party/main-renderer/denormalize.js +18 -0
  22. package/dist/extensions/first-party/main-renderer/drag/factory.d.ts +2 -1
  23. package/dist/extensions/first-party/main-renderer/drag/factory.js +2 -1
  24. package/dist/extensions/first-party/main-renderer/drag/render.js +18 -11
  25. package/dist/extensions/first-party/main-renderer/drag/types.d.ts +2 -0
  26. package/dist/extensions/first-party/main-renderer/drag/update.js +4 -2
  27. package/dist/extensions/first-party/main-renderer/factory.d.ts +2 -1
  28. package/dist/extensions/first-party/main-renderer/factory.js +46 -12
  29. package/dist/extensions/first-party/main-renderer/normalize.d.ts +3 -3
  30. package/dist/extensions/first-party/main-renderer/normalize.js +19 -7
  31. package/dist/extensions/first-party/main-renderer/pieces/suppress.js +2 -1
  32. package/dist/extensions/first-party/main-renderer/public-defaults.d.ts +3 -0
  33. package/dist/extensions/first-party/main-renderer/public-defaults.js +4 -0
  34. package/dist/extensions/first-party/main-renderer/types/extension.d.ts +8 -5
  35. package/dist/extensions/first-party/main-renderer/types/instance.d.ts +10 -8
  36. package/dist/extensions/first-party/main-renderer/types/internal.d.ts +6 -7
  37. package/dist/extensions/first-party/main-renderer/types/internal.js +24 -1
  38. package/dist/extensions/first-party/main-renderer/types/public.d.ts +9 -0
  39. package/dist/extensions/first-party/main-renderer/types/template.d.ts +12 -1
  40. package/dist/extensions/first-party/promotion/types/input.d.ts +2 -2
  41. package/dist/extensions/index.d.ts +1 -0
  42. package/dist/extensions/index.js +1 -0
  43. package/dist/extensions/types/basic/events.d.ts +32 -16
  44. package/dist/extensions/types/basic/interaction.d.ts +13 -2
  45. package/dist/extensions/types/surface/commands.d.ts +4 -2
  46. package/dist/runtime/factory/input.js +50 -22
  47. package/dist/runtime/factory/main.js +11 -0
  48. package/dist/runtime/input/adapter/types.js +2 -1
  49. package/dist/runtime/input/controller/factory.js +36 -9
  50. package/dist/runtime/input/controller/helpers.d.ts +6 -0
  51. package/dist/runtime/input/controller/helpers.js +27 -0
  52. package/dist/runtime/input/controller/pointer.js +71 -52
  53. package/dist/runtime/input/controller/types.d.ts +13 -4
  54. package/dist/runtime/mutation/interaction.js +1 -1
  55. package/dist/runtime/mutation/types.d.ts +2 -2
  56. package/dist/state/interaction/config.d.ts +6 -0
  57. package/dist/state/interaction/config.js +30 -0
  58. package/dist/state/interaction/factory.js +15 -3
  59. package/dist/state/interaction/helpers.d.ts +18 -2
  60. package/dist/state/interaction/helpers.js +12 -0
  61. package/dist/state/interaction/mutation.d.ts +2 -0
  62. package/dist/state/interaction/reducers.d.ts +3 -0
  63. package/dist/state/interaction/reducers.js +22 -7
  64. package/dist/state/interaction/types/config.d.ts +9 -0
  65. package/dist/state/interaction/types/config.js +1 -0
  66. package/dist/state/interaction/types/input.d.ts +3 -0
  67. package/dist/state/interaction/types/internal.d.ts +14 -4
  68. package/dist/state/interaction/types/main.d.ts +11 -5
  69. package/dist/wrapper/factory.js +2 -0
  70. package/dist/wrapper/types.d.ts +1 -1
  71. package/package.json +5 -6
  72. package/dist/extensions/first-party/main-renderer/types/input.d.ts +0 -4
  73. /package/dist/extensions/first-party/main-renderer/types/{input.js → public.js} +0 -0
@@ -1,8 +1,8 @@
1
1
  import assert from '@ktarmyshov/assert';
2
2
  import { isEmptyPieceCode, isNonEmptyPieceCode } from '../../../state/board/check.js';
3
3
  import { fromPieceCode } from '../../../state/board/piece.js';
4
- import { isDragSessionCoreOwned } from '../../../state/interaction/helpers.js';
5
- import { canMoveTo } from './helpers.js';
4
+ import { isDragSessionActiveLiftedPiece, isDragSessionCoreOwned, isDragSessionPendingLiftedPiece, isDragSessionReleaseTargeting } from '../../../state/interaction/helpers.js';
5
+ import { buttonToButtonsMask, canMoveTo, isMovementBeyondThreshold } from './helpers.js';
6
6
  export function determineActionPointerDown(state, context) {
7
7
  assert(context.rawEvent.type === 'pointerdown', 'determineActionPointerDown should only be called for pointerdown events');
8
8
  const rawEvent = context.rawEvent;
@@ -29,9 +29,10 @@ export function determineActionPointerDown(state, context) {
29
29
  const targetPieceCode = state.surface.getPieceCodeAt(sceneEvent.targetSquare);
30
30
  if (isEmptyPieceCode(targetPieceCode)) {
31
31
  return {
32
- type: 'startReleaseTargetingDrag',
33
- source: interaction.selected.square,
34
- target: sceneEvent.targetSquare
32
+ type: 'startReleaseTargetingDragSession',
33
+ sourceSquare: interaction.selected.square,
34
+ targetSquare: sceneEvent.targetSquare,
35
+ startButton: rawEvent.button
35
36
  };
36
37
  }
37
38
  const isLegalMoveTarget = interaction.movability.mode === 1 /* MovabilityModeCode.Free */ ||
@@ -40,9 +41,10 @@ export function determineActionPointerDown(state, context) {
40
41
  if (fromPieceCode(targetPieceCode).color !== fromPieceCode(selectedPieceCode).color &&
41
42
  isLegalMoveTarget) {
42
43
  return {
43
- type: 'startReleaseTargetingDrag',
44
- source: interaction.selected.square,
45
- target: sceneEvent.targetSquare
44
+ type: 'startReleaseTargetingDragSession',
45
+ sourceSquare: interaction.selected.square,
46
+ targetSquare: sceneEvent.targetSquare,
47
+ startButton: rawEvent.button
46
48
  };
47
49
  }
48
50
  }
@@ -52,10 +54,24 @@ export function determineActionPointerDown(state, context) {
52
54
  */
53
55
  const pieceCode = state.surface.getPieceCodeAt(sceneEvent.targetSquare);
54
56
  if (!isEmptyPieceCode(pieceCode)) {
57
+ const thresholdPx = interaction.config.drag.liftedActivation.thresholdPx;
58
+ if (thresholdPx > 0) {
59
+ return {
60
+ type: 'startLiftedDragSession',
61
+ phase: 'pending',
62
+ sourceSquare: sceneEvent.targetSquare,
63
+ targetSquare: sceneEvent.targetSquare,
64
+ startButton: rawEvent.button,
65
+ startPoint: sceneEvent.point,
66
+ thresholdPx
67
+ };
68
+ }
55
69
  return {
56
- type: 'startLiftedDrag',
57
- source: sceneEvent.targetSquare,
58
- target: sceneEvent.targetSquare
70
+ type: 'startLiftedDragSession',
71
+ phase: 'active',
72
+ sourceSquare: sceneEvent.targetSquare,
73
+ targetSquare: sceneEvent.targetSquare,
74
+ startButton: rawEvent.button
59
75
  };
60
76
  }
61
77
  }
@@ -64,67 +80,65 @@ export function determineActionPointerDown(state, context) {
64
80
  export function determineActionPointerMove(state, context) {
65
81
  assert(context.rawEvent.type === 'pointermove', 'determineActionPointerMove should only be called for pointermove events');
66
82
  const interaction = state.surface.getInteractionStateSnapshot();
67
- if (interaction.dragSession) {
68
- return {
69
- type: 'updateDragSessionCurrentTarget',
70
- target: context.sceneEvent?.targetSquare ?? null
71
- };
83
+ const dragSession = interaction.dragSession;
84
+ if (!dragSession) {
85
+ return null;
72
86
  }
73
- return null;
87
+ const sceneEvent = context.sceneEvent;
88
+ assert(sceneEvent, 'pointermove requires a scene event');
89
+ if (isDragSessionPendingLiftedPiece(dragSession)) {
90
+ if (isMovementBeyondThreshold(dragSession.startPoint, sceneEvent.point, dragSession.thresholdPx)) {
91
+ return {
92
+ type: 'activatePendingLiftedDragSession',
93
+ targetSquare: sceneEvent.targetSquare
94
+ };
95
+ }
96
+ return { type: 'updateDragSessionCurrentTarget', targetSquare: sceneEvent.targetSquare };
97
+ }
98
+ return {
99
+ type: 'updateDragSessionCurrentTarget',
100
+ targetSquare: sceneEvent.targetSquare
101
+ };
74
102
  }
75
- export function determineActionPointerUp(state, context) {
76
- assert(context.rawEvent.type === 'pointerup', 'determineActionPointerUp should only be called for pointerup events');
103
+ function determineActionTerminalRelease(state, context) {
77
104
  const interaction = state.surface.getInteractionStateSnapshot();
78
105
  const dragSession = interaction.dragSession;
79
106
  if (!dragSession) {
80
- // No active drag session, so nothing to do on pointer up
81
107
  return null;
82
108
  }
83
109
  const sceneEvent = context.sceneEvent;
84
- assert(sceneEvent, 'Scene event should be present for pointer events');
110
+ assert(sceneEvent, 'Scene event should be present for terminal release');
85
111
  if (!isDragSessionCoreOwned(dragSession)) {
86
- // For extension-owned drag sessions, we simply end the session without attempting to make a move,
87
- // since the runtime doesn't have enough information about the semantics of the drag session.
88
112
  return {
89
- type: 'completeExtensionDrag',
90
- target: sceneEvent.targetSquare
113
+ type: 'completeExtensionDragSession',
114
+ targetSquare: sceneEvent.targetSquare
91
115
  };
92
116
  }
93
- // Check if the square is the same as the source square of the drag session.
94
- // If it is, then we can end the drag session without making a move.
95
117
  if (sceneEvent.targetSquare === dragSession.sourceSquare) {
96
- return {
97
- type: 'cancelActiveInteraction'
98
- };
118
+ return { type: 'cancelActiveInteraction' };
99
119
  }
100
- // Check if the target square is a valid destination for the selected piece.
101
- if (sceneEvent.targetSquare !== null && canMoveTo(interaction, sceneEvent.targetSquare)) {
102
- return {
103
- type: 'completeCoreDragTo',
104
- target: sceneEvent.targetSquare
105
- };
120
+ if (sceneEvent.targetSquare !== null &&
121
+ canMoveTo(interaction, sceneEvent.targetSquare) &&
122
+ (isDragSessionActiveLiftedPiece(dragSession) || isDragSessionReleaseTargeting(dragSession))) {
123
+ return { type: 'completeCoreDragSessionTo', targetSquare: sceneEvent.targetSquare };
106
124
  }
107
- // Invalid target: piece returns to source for lifted drag (selection preserved),
108
- // or selection is cleared for release targeting.
109
125
  if (dragSession.type === 'lifted-piece-drag') {
110
- return {
111
- type: 'cancelActiveInteraction'
112
- };
126
+ return { type: 'cancelActiveInteraction' };
113
127
  }
114
128
  else {
115
- return {
116
- type: 'cancelInteraction'
117
- };
129
+ return { type: 'cancelInteraction' };
118
130
  }
119
131
  }
132
+ export function determineActionPointerUp(state, context) {
133
+ assert(context.rawEvent.type === 'pointerup', 'determineActionPointerUp should only be called for pointerup events');
134
+ return determineActionTerminalRelease(state, context);
135
+ }
120
136
  export function determineActionPointerCancel(state, context) {
121
137
  assert(context.rawEvent.type === 'pointercancel', 'determineActionPointerCancel should only be called for pointercancel events');
122
138
  const interaction = state.surface.getInteractionStateSnapshot();
123
139
  if (!interaction.dragSession) {
124
- // No active drag session, so nothing to do on pointer cancel
125
140
  return null;
126
141
  }
127
- // Cancel the active interaction on pointer cancel
128
142
  return {
129
143
  type: 'cancelActiveInteraction'
130
144
  };
@@ -132,12 +146,17 @@ export function determineActionPointerCancel(state, context) {
132
146
  export function determineActionLostPointerCapture(state, context) {
133
147
  assert(context.rawEvent.type === 'lostpointercapture', 'determineActionLostPointerCapture should only be called for lostpointercapture events');
134
148
  const interaction = state.surface.getInteractionStateSnapshot();
135
- if (!interaction.dragSession) {
136
- // No active drag session, so nothing to do on lost pointer capture
149
+ const dragSession = interaction.dragSession;
150
+ if (!dragSession) {
137
151
  return null;
138
152
  }
139
- // Cancel the active interaction on lost pointer capture
140
- return {
141
- type: 'cancelActiveInteraction'
142
- };
153
+ const rawEvent = context.rawEvent;
154
+ const mask = buttonToButtonsMask(dragSession.startButton);
155
+ if (mask === null) {
156
+ return { type: 'cancelActiveInteraction' };
157
+ }
158
+ if ((rawEvent.buttons & mask) === 0) {
159
+ return determineActionTerminalRelease(state, context);
160
+ }
161
+ return { type: 'cancelActiveInteraction' };
143
162
  }
@@ -1,14 +1,23 @@
1
1
  import type { TransientInput } from '../../../extensions/types/basic/transient-visuals.js';
2
2
  import type { ExtensionOnEventContext } from '../../../extensions/types/context/events.js';
3
3
  import type { PieceCode, Square } from '../../../state/board/types/internal.js';
4
+ import type { DragSessionActiveLiftedPieceCoreOwned, DragSessionPendingLiftedPieceCoreOwned, DragSessionReleaseTargetingCoreOwned } from '../../../state/interaction/types/internal.js';
4
5
  import type { InteractionStateSnapshot } from '../../../state/interaction/types/main.js';
6
+ export type StartPendingLiftedDragSessionInput = Pick<DragSessionPendingLiftedPieceCoreOwned, 'phase' | 'sourceSquare' | 'targetSquare' | 'startButton' | 'startPoint' | 'thresholdPx'>;
7
+ export type StartActiveLiftedDragSessionInput = Pick<DragSessionActiveLiftedPieceCoreOwned, 'phase' | 'sourceSquare' | 'targetSquare' | 'startButton'>;
8
+ export type StartLiftedDragSessionInput = StartPendingLiftedDragSessionInput | StartActiveLiftedDragSessionInput;
9
+ export type ActivatePendingLiftedDragSessionInput = {
10
+ targetSquare: Square | null;
11
+ };
12
+ export type StartReleaseTargetingDragSessionInput = Pick<DragSessionReleaseTargetingCoreOwned, 'sourceSquare' | 'targetSquare' | 'startButton'>;
5
13
  export interface RuntimeInteractionSurface {
6
14
  getInteractionStateSnapshot(): InteractionStateSnapshot;
7
15
  getPieceCodeAt(square: Square): PieceCode;
8
- startLiftedDrag(source: Square, target: Square): void;
9
- startReleaseTargetingDrag(source: Square, target: Square): void;
10
- completeCoreDragTo(target: Square): void;
11
- completeExtensionDrag(target: Square | null): void;
16
+ startLiftedDragSession(input: StartLiftedDragSessionInput): void;
17
+ activatePendingLiftedDragSession(input: ActivatePendingLiftedDragSessionInput): void;
18
+ startReleaseTargetingDragSession(input: StartReleaseTargetingDragSessionInput): void;
19
+ completeCoreDragSessionTo(target: Square): void;
20
+ completeExtensionDragSession(target: Square | null): void;
12
21
  updateDragSessionCurrentTarget(target: Square | null): void;
13
22
  cancelActiveInteraction(): void;
14
23
  cancelInteraction(): void;
@@ -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.completeCoreDragTo']
7
+ causes: ['state.interaction.clear', 'runtime.interaction.completeCoreDragSessionTo']
8
8
  })) {
9
9
  return;
10
10
  }
@@ -6,8 +6,8 @@ import type { InteractionStateMutationPayloadByCause } from '../../state/interac
6
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.completeCoreDragTo': DragSessionCoreOwnedSnapshot;
10
- 'runtime.interaction.completeExtensionDragTo': DragSessionExtensionOwnedSnapshot;
9
+ 'runtime.interaction.completeCoreDragSessionTo': DragSessionCoreOwnedSnapshot;
10
+ 'runtime.interaction.completeExtensionDragSession': DragSessionExtensionOwnedSnapshot;
11
11
  'runtime.interaction.resolveDeferredUIMoveRequest': undefined;
12
12
  'runtime.interaction.cancelDeferredUIMoveRequest': undefined;
13
13
  };
@@ -0,0 +1,6 @@
1
+ import type { InteractionConfig } from './types/config.js';
2
+ import type { InteractionConfigInput } from './types/input.js';
3
+ export declare const DefaultInteractionDesktopConfig: InteractionConfig;
4
+ export declare const DefaultInteractionMobileConfig: InteractionConfig;
5
+ export declare function normalizeInteractionConfig(input: InteractionConfigInput | undefined, base: InteractionConfig): InteractionConfig;
6
+ export declare function areInteractionConfigsEqual(a: InteractionConfig, b: InteractionConfig): boolean;
@@ -0,0 +1,30 @@
1
+ import assert from '@ktarmyshov/assert';
2
+ import { toMerged } from 'es-toolkit/object';
3
+ import { isEqual } from 'es-toolkit/predicate';
4
+ export const DefaultInteractionDesktopConfig = {
5
+ drag: {
6
+ liftedActivation: {
7
+ thresholdPx: 0
8
+ }
9
+ }
10
+ };
11
+ export const DefaultInteractionMobileConfig = {
12
+ drag: {
13
+ liftedActivation: {
14
+ thresholdPx: 5
15
+ }
16
+ }
17
+ };
18
+ function validateInteractionConfig(config) {
19
+ const { thresholdPx } = config.drag.liftedActivation;
20
+ assert(Number.isFinite(thresholdPx), 'drag.liftedActivation.thresholdPx must be a finite number');
21
+ assert(thresholdPx >= 0, 'drag.liftedActivation.thresholdPx must be >= 0');
22
+ }
23
+ export function normalizeInteractionConfig(input, base) {
24
+ const merged = toMerged(base, input ?? {});
25
+ validateInteractionConfig(merged);
26
+ return merged;
27
+ }
28
+ export function areInteractionConfigsEqual(a, b) {
29
+ return isEqual(a, b);
30
+ }
@@ -1,8 +1,9 @@
1
1
  import assert from '@ktarmyshov/assert';
2
2
  import { cloneDeep } from 'es-toolkit';
3
+ import { DefaultInteractionDesktopConfig, normalizeInteractionConfig } from './config.js';
3
4
  import { updateActiveDestinations } from './helpers.js';
4
5
  import { normalizeMovability } from './normalize.js';
5
- import { interactionClear, interactionClearActive, interactionSetDragSession, interactionSetMovability, interactionSetSelected, interactionUpdateDragSessionCurrentTarget } from './reducers.js';
6
+ import { interactionActivatePendingLiftedDragSession, interactionClear, interactionClearActive, interactionSetConfig, interactionSetDragSession, interactionSetMovability, interactionSetSelected, interactionUpdateDragSessionCurrentTarget } from './reducers.js';
6
7
  const DefaultMovabilityDisabled = { mode: 0 /* MovabilityModeCode.Disabled */ };
7
8
  /**
8
9
  * Create a fresh interaction state with all fields set to null or false.
@@ -15,7 +16,8 @@ function createInteractionStateInternal(options) {
15
16
  movability,
16
17
  selected: null,
17
18
  activeDestinations: new Map(),
18
- dragSession: null
19
+ dragSession: null,
20
+ config: normalizeInteractionConfig(options.config, DefaultInteractionDesktopConfig)
19
21
  };
20
22
  }
21
23
  export function createInteractionState(options) {
@@ -48,7 +50,7 @@ export function createInteractionState(options) {
48
50
  return updateActiveDestinations(internalState, mutationSession);
49
51
  },
50
52
  get dragSession() {
51
- return internalState.dragSession ? { ...internalState.dragSession } : null;
53
+ return cloneDeep(internalState.dragSession);
52
54
  },
53
55
  setDragSession(session, mutationSession) {
54
56
  // assert that there is no active session when setting a new session.
@@ -64,6 +66,9 @@ export function createInteractionState(options) {
64
66
  }
65
67
  return mutationSession.addMutation('state.interaction.setDragSession', interactionSetDragSession(internalState, session));
66
68
  },
69
+ activatePendingLiftedDragSession(targetSquare, mutationSession) {
70
+ return mutationSession.addMutation('state.interaction.activatePendingLiftedDragSession', interactionActivatePendingLiftedDragSession(internalState, targetSquare));
71
+ },
67
72
  updateDragSessionCurrentTarget(sq, mutationSession) {
68
73
  assert(internalState.dragSession, 'There must be an active drag session to update its current target');
69
74
  return mutationSession.addMutation('state.interaction.updateDragSessionCurrentTarget', interactionUpdateDragSessionCurrentTarget(internalState, sq));
@@ -78,6 +83,13 @@ export function createInteractionState(options) {
78
83
  clearActive(mutationSession) {
79
84
  return mutationSession.addMutation('state.interaction.clearActive', interactionClearActive(internalState));
80
85
  },
86
+ setConfig(config, mutationSession) {
87
+ const nextConfig = normalizeInteractionConfig(config, internalState.config);
88
+ return mutationSession.addMutation('state.interaction.setConfig', interactionSetConfig(internalState, nextConfig));
89
+ },
90
+ getConfig() {
91
+ return cloneDeep(internalState.config);
92
+ },
81
93
  getSnapshot() {
82
94
  return cloneDeep(internalState);
83
95
  }
@@ -1,6 +1,22 @@
1
1
  import { InteractionStateMutationSession } from './mutation.js';
2
- import { DragSessionCoreOwnedSnapshot, DragSessionSnapshot } from './types/internal.js';
2
+ import { DragSessionSnapshot } from './types/internal.js';
3
3
  import { InteractionStateInternal, InteractionStateSelected } from './types/main.js';
4
4
  export declare function selectedEqual(a: InteractionStateSelected | null, b: InteractionStateSelected | null): boolean;
5
5
  export declare function updateActiveDestinations(state: InteractionStateInternal, mutationSession: InteractionStateMutationSession): boolean;
6
- export declare function isDragSessionCoreOwned(session: DragSessionSnapshot): session is DragSessionCoreOwnedSnapshot;
6
+ export declare function isDragSessionCoreOwned<T extends DragSessionSnapshot>(session: T): session is Extract<T, {
7
+ owner: 'core';
8
+ }>;
9
+ export declare function isDragSessionReleaseTargeting<T extends DragSessionSnapshot>(session: T): session is Extract<T, {
10
+ type: 'release-targeting';
11
+ }>;
12
+ export declare function isDragSessionLiftedPiece<T extends DragSessionSnapshot>(session: T): session is Extract<T, {
13
+ type: 'lifted-piece-drag';
14
+ }>;
15
+ export declare function isDragSessionPendingLiftedPiece<T extends DragSessionSnapshot>(session: T): session is Extract<T, {
16
+ type: 'lifted-piece-drag';
17
+ phase: 'pending';
18
+ }>;
19
+ export declare function isDragSessionActiveLiftedPiece<T extends DragSessionSnapshot>(session: T): session is Extract<T, {
20
+ type: 'lifted-piece-drag';
21
+ phase: 'active';
22
+ }>;
@@ -17,3 +17,15 @@ export function updateActiveDestinations(state, mutationSession) {
17
17
  export function isDragSessionCoreOwned(session) {
18
18
  return session.owner === 'core';
19
19
  }
20
+ export function isDragSessionReleaseTargeting(session) {
21
+ return session.type === 'release-targeting';
22
+ }
23
+ export function isDragSessionLiftedPiece(session) {
24
+ return session.type === 'lifted-piece-drag';
25
+ }
26
+ export function isDragSessionPendingLiftedPiece(session) {
27
+ return isDragSessionLiftedPiece(session) && session.phase === 'pending';
28
+ }
29
+ export function isDragSessionActiveLiftedPiece(session) {
30
+ return isDragSessionLiftedPiece(session) && session.phase === 'active';
31
+ }
@@ -2,10 +2,12 @@ import type { MutationSession } from '../../mutation/types.js';
2
2
  export type InteractionStateMutationPayloadByCause = {
3
3
  'state.interaction.setSelectedSquare': undefined;
4
4
  'state.interaction.setDragSession': undefined;
5
+ 'state.interaction.activatePendingLiftedDragSession': undefined;
5
6
  'state.interaction.updateDragSessionCurrentTarget': undefined;
6
7
  'state.interaction.clear': undefined;
7
8
  'state.interaction.clearActive': undefined;
8
9
  'state.interaction.setMovability': undefined;
9
10
  'state.interaction.updateActiveDestinations': undefined;
11
+ 'state.interaction.setConfig': undefined;
10
12
  };
11
13
  export type InteractionStateMutationSession = MutationSession<InteractionStateMutationPayloadByCause>;
@@ -1,11 +1,14 @@
1
1
  import type { ReadonlyDeep } from 'type-fest';
2
2
  import type { Square } from '../board/types/internal.js';
3
+ import type { InteractionConfig } from './types/config.js';
3
4
  import { Movability, type DragSessionSnapshot, type MoveDestination } from './types/internal.js';
4
5
  import { InteractionStateInternal, InteractionStateSelected } from './types/main.js';
5
6
  export declare function interactionSetSelected(state: InteractionStateInternal, selected: InteractionStateSelected | null): boolean;
6
7
  export declare function interactionSetMovability(state: InteractionStateInternal, m: Movability): boolean;
8
+ export declare function interactionSetConfig(state: InteractionStateInternal, config: InteractionConfig): boolean;
7
9
  export declare function interactionSetActiveDestinations(state: InteractionStateInternal, dests: ReadonlyMap<Square, ReadonlyDeep<MoveDestination>>): boolean;
8
10
  export declare function interactionSetDragSession(state: InteractionStateInternal, session: DragSessionSnapshot | null): boolean;
11
+ export declare function interactionActivatePendingLiftedDragSession(state: InteractionStateInternal, targetSquare: Square | null): boolean;
9
12
  export declare function interactionUpdateDragSessionCurrentTarget(state: InteractionStateInternal, sq: Square | null): boolean;
10
13
  export declare function interactionClear(state: InteractionStateInternal): boolean;
11
14
  export declare function interactionClearActive(state: InteractionStateInternal): boolean;
@@ -1,7 +1,9 @@
1
1
  import assert from '@ktarmyshov/assert';
2
2
  import { cloneDeep } from 'es-toolkit/object';
3
+ import { isEqual } from 'es-toolkit/predicate';
3
4
  import { setsEqual } from '../../helpers/util.js';
4
- import { selectedEqual } from './helpers.js';
5
+ import { areInteractionConfigsEqual } from './config.js';
6
+ import { isDragSessionPendingLiftedPiece, selectedEqual } from './helpers.js';
5
7
  import { movabilitiesEqual } from './movability.js';
6
8
  export function interactionSetSelected(state, selected) {
7
9
  const notChanged = selectedEqual(state.selected, selected);
@@ -16,6 +18,12 @@ export function interactionSetMovability(state, m) {
16
18
  state.movability = cloneDeep(m); // Defensive copy to prevent external mutations
17
19
  return true;
18
20
  }
21
+ export function interactionSetConfig(state, config) {
22
+ if (areInteractionConfigsEqual(state.config, config))
23
+ return false; // no-op
24
+ state.config = cloneDeep(config); // Defensive copy to prevent external mutations
25
+ return true;
26
+ }
19
27
  function promotedTosEqual(a, b) {
20
28
  if (a === b)
21
29
  return true;
@@ -46,15 +54,22 @@ export function interactionSetActiveDestinations(state, dests) {
46
54
  return true;
47
55
  }
48
56
  export function interactionSetDragSession(state, session) {
49
- const changed = state.dragSession?.type !== session?.type ||
50
- state.dragSession?.sourceSquare !== session?.sourceSquare ||
51
- state.dragSession?.sourcePieceCode !== session?.sourcePieceCode ||
52
- state.dragSession?.targetSquare !== session?.targetSquare;
53
- if (!changed)
54
- return false; // no-op
57
+ if (state.dragSession === session)
58
+ return false;
59
+ if (isEqual(state.dragSession, session))
60
+ return false;
55
61
  state.dragSession = session ? cloneDeep(session) : null;
56
62
  return true;
57
63
  }
64
+ export function interactionActivatePendingLiftedDragSession(state, targetSquare) {
65
+ const current = state.dragSession;
66
+ assert(current !== null, 'activatePendingLiftedDragSession requires an active drag session');
67
+ assert(isDragSessionPendingLiftedPiece(current), 'activatePendingLiftedDragSession requires a pending lifted-piece drag session');
68
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
69
+ const { startPoint: _startPoint, thresholdPx: _thresholdPx, ...rest } = current;
70
+ state.dragSession = { ...rest, phase: 'active', targetSquare };
71
+ return true;
72
+ }
58
73
  export function interactionUpdateDragSessionCurrentTarget(state, sq) {
59
74
  const changed = state.dragSession?.targetSquare !== sq;
60
75
  if (!changed)
@@ -0,0 +1,9 @@
1
+ export interface InteractionConfigDragLiftedActivation {
2
+ thresholdPx: number;
3
+ }
4
+ export interface InteractionConfigDrag {
5
+ liftedActivation: InteractionConfigDragLiftedActivation;
6
+ }
7
+ export interface InteractionConfig {
8
+ drag: InteractionConfigDrag;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,6 @@
1
+ import { PartialDeep } from 'type-fest';
1
2
  import { MoveRequestInput, RolePromotionInput, SquareString } from '../../board/types/input.js';
3
+ import { InteractionConfig } from './config.js';
2
4
  export interface MoveDestinationInput extends Omit<MoveRequestInput, 'from' | 'promotedTo'> {
3
5
  promotedTo?: RolePromotionInput[];
4
6
  }
@@ -16,3 +18,4 @@ export type DisabledMovabilityInput = {
16
18
  mode: 'disabled';
17
19
  };
18
20
  export type MovabilityInput = StrictMovabilityInput | FreeMovabilityInput | DisabledMovabilityInput;
21
+ export type InteractionConfigInput = PartialDeep<InteractionConfig>;
@@ -1,17 +1,26 @@
1
1
  import type { ReadonlyDeep } from 'type-fest';
2
- import type { ExtensionDragSession, ExtensionDragSessionBase } from '../../../extensions/types/basic/interaction.js';
2
+ import type { ExtensionDragSession, ExtensionDragSessionActiveLiftedPiece, ExtensionDragSessionBase, ExtensionDragSessionPendingLiftedPiece } from '../../../extensions/types/basic/interaction.js';
3
3
  import type { MoveRequest, PieceCode, RolePromotionCode, Square } from '../../board/types/internal.js';
4
+ import type { InteractionConfig } from './config.js';
4
5
  export type DragSessionExtensionOwned = ExtensionDragSession & {
5
6
  owner: string;
6
7
  };
7
8
  export type DragSessionExtensionOwnedSnapshot = ReadonlyDeep<DragSessionExtensionOwned>;
8
- export interface DragSessionCoreOwned extends ExtensionDragSessionBase {
9
+ export interface DragSessionPendingLiftedPieceCoreOwned extends ExtensionDragSessionPendingLiftedPiece {
9
10
  owner: 'core';
10
- type: 'lifted-piece-drag' | 'release-targeting';
11
+ sourceSquare: Square;
12
+ }
13
+ export interface DragSessionActiveLiftedPieceCoreOwned extends ExtensionDragSessionActiveLiftedPiece {
14
+ owner: 'core';
15
+ sourceSquare: Square;
16
+ }
17
+ export interface DragSessionReleaseTargetingCoreOwned extends ExtensionDragSessionBase {
18
+ owner: 'core';
19
+ type: 'release-targeting';
11
20
  sourceSquare: Square;
12
21
  sourcePieceCode: PieceCode;
13
- targetSquare: Square | null;
14
22
  }
23
+ export type DragSessionCoreOwned = DragSessionPendingLiftedPieceCoreOwned | DragSessionActiveLiftedPieceCoreOwned | DragSessionReleaseTargetingCoreOwned;
15
24
  export type DragSessionCoreOwnedSnapshot = ReadonlyDeep<DragSessionCoreOwned>;
16
25
  export type DragSession = DragSessionCoreOwned | DragSessionExtensionOwned;
17
26
  export type DragSessionSnapshot = ReadonlyDeep<DragSession>;
@@ -41,3 +50,4 @@ export interface MovabilityDisabled {
41
50
  }
42
51
  export type Movability = MovabilityStrict | MovabilityFree | MovabilityDisabled;
43
52
  export type MovabilitySnapshot = ReadonlyDeep<Movability>;
53
+ export type InteractionConfigSnapshot = ReadonlyDeep<InteractionConfig>;
@@ -1,8 +1,9 @@
1
- import { ReadonlyDeep } from 'type-fest';
2
- import { NonEmptyPieceCode, Square } from '../../board/types/internal.js';
3
- import { InteractionStateMutationSession } from '../mutation.js';
4
- import { MovabilityInput } from './input.js';
5
- import { DragSession, DragSessionSnapshot, Movability, MovabilitySnapshot, MoveDestinationSnapshot } from './internal.js';
1
+ import type { ReadonlyDeep } from 'type-fest';
2
+ import type { NonEmptyPieceCode, Square } from '../../board/types/internal.js';
3
+ import type { InteractionStateMutationSession } from '../mutation.js';
4
+ import type { InteractionConfig } from './config.js';
5
+ import type { InteractionConfigInput, MovabilityInput } from './input.js';
6
+ import type { DragSession, DragSessionSnapshot, InteractionConfigSnapshot, Movability, MovabilitySnapshot, MoveDestinationSnapshot } from './internal.js';
6
7
  export interface InteractionStateSelected {
7
8
  square: Square;
8
9
  pieceCode: NonEmptyPieceCode;
@@ -12,10 +13,12 @@ export interface InteractionStateInternal {
12
13
  movability: Movability;
13
14
  activeDestinations: ReadonlyMap<Square, MoveDestinationSnapshot>;
14
15
  dragSession: DragSession | null;
16
+ config: InteractionConfig;
15
17
  }
16
18
  export type InteractionStateSnapshot = ReadonlyDeep<InteractionStateInternal>;
17
19
  export interface InteractionStateInitOptions {
18
20
  movability?: MovabilityInput;
21
+ config?: InteractionConfigInput;
19
22
  }
20
23
  export interface InteractionState {
21
24
  readonly selected: InteractionStateSelected | null;
@@ -26,8 +29,11 @@ export interface InteractionState {
26
29
  updateActiveDestinations(mutationSession: InteractionStateMutationSession): boolean;
27
30
  readonly dragSession: DragSessionSnapshot | null;
28
31
  setDragSession(session: DragSessionSnapshot | null, mutationSession: InteractionStateMutationSession): boolean;
32
+ activatePendingLiftedDragSession(targetSquare: Square | null, mutationSession: InteractionStateMutationSession): boolean;
29
33
  updateDragSessionCurrentTarget(sq: Square | null, mutationSession: InteractionStateMutationSession): boolean;
30
34
  clear(mutationSession: InteractionStateMutationSession): boolean;
31
35
  clearActive(mutationSession: InteractionStateMutationSession): boolean;
36
+ setConfig(config: InteractionConfigInput, mutationSession: InteractionStateMutationSession): boolean;
37
+ getConfig(): InteractionConfigSnapshot;
32
38
  getSnapshot(): InteractionStateSnapshot;
33
39
  }
@@ -34,6 +34,8 @@ export function createBoard(options) {
34
34
  setOrientation: runtime.setOrientation,
35
35
  setMovability: runtime.setMovability,
36
36
  select: runtime.select,
37
+ setInteractionConfig: runtime.setInteractionConfig,
38
+ getInteractionConfig: runtime.getInteractionConfig,
37
39
  getSnapshot: runtime.getSnapshot,
38
40
  extensions: runtime.getExtensionsPublicRecord()
39
41
  };
@@ -51,7 +51,7 @@ export type ChessboardInitOptions<TExtensions extends readonly ChessboardExtensi
51
51
  /**
52
52
  * Public board interface returned by createBoard(...)
53
53
  */
54
- export type ChessboardRuntimeSurface = Pick<Runtime, 'destroy' | 'move' | 'setPosition' | 'setPiecePosition' | 'setTurn' | 'setOrientation' | 'setMovability' | 'select' | 'getSnapshot'>;
54
+ export type ChessboardRuntimeSurface = Pick<Runtime, 'destroy' | 'move' | 'setPosition' | 'setPiecePosition' | 'setTurn' | 'setOrientation' | 'setMovability' | 'select' | 'setInteractionConfig' | 'getInteractionConfig' | 'getSnapshot'>;
55
55
  export interface Chessboard<TExtensions extends readonly ChessboardExtensionInput[] = DefaultBuiltinChessboardExtensions> extends ChessboardRuntimeSurface {
56
56
  readonly extensions: ChessboardExtensionsPublicMap<TExtensions>;
57
57
  }
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@mirasen/chessboard",
3
3
  "description": "Framework-agnostic TypeScript chessboard platform with real chess interaction built in.",
4
- "version": "1.2.3",
4
+ "version": "1.3.0",
5
5
  "license": "MIT",
6
6
  "author": {
7
- "name": "Mirasen",
8
- "email": "konstantin@mirasen.io"
7
+ "name": "Mirasen"
9
8
  },
10
9
  "keywords": [
11
10
  "mirasen",
@@ -63,8 +62,8 @@
63
62
  "prepack": "publint",
64
63
  "format": "prettier --write .",
65
64
  "lint": "prettier --check . && eslint .",
66
- "check": "tsc --skipLibCheck --noEmit",
67
- "check:test": "tsc --project tsconfig-test.json --skipLibCheck --noEmit",
65
+ "check": "tsc --project tsconfig.json --noEmit",
66
+ "check:test": "tsc --project tsconfig-test.json",
68
67
  "test": "npm run check:test && vitest run --run",
69
68
  "coverage": "npm run check:test && vitest run --coverage",
70
69
  "changeset:version": "changeset version && npm install && npm run format && git add --all",
@@ -97,7 +96,7 @@
97
96
  "publint": "^0.3.11",
98
97
  "rimraf": "^6.0.1",
99
98
  "serve": "^14.2.6",
100
- "typescript": "^5.8.2",
99
+ "typescript": "^6.0.3",
101
100
  "typescript-eslint": "^8.57.0",
102
101
  "vitest": "^4.0.18"
103
102
  },
@@ -1,4 +0,0 @@
1
- import { PieceString } from '../../../../state/board/types/input.js';
2
- import { TMainRendererConfig } from './template.js';
3
- export type PieceUrlsInput = Record<PieceString, string>;
4
- export type MainRendererConfigInput = TMainRendererConfig<PieceUrlsInput>;