@mirasen/chessboard 1.2.4 → 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 (72) hide show
  1. package/CHANGELOG.md +36 -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/update.js +1 -1
  6. package/dist/extensions/first-party/board-events/factory.js +2 -2
  7. package/dist/extensions/first-party/main-renderer/animation/factory.d.ts +2 -1
  8. package/dist/extensions/first-party/main-renderer/animation/factory.js +3 -2
  9. package/dist/extensions/first-party/main-renderer/animation/types.d.ts +2 -0
  10. package/dist/extensions/first-party/main-renderer/animation/update.js +4 -2
  11. package/dist/extensions/first-party/main-renderer/board/factory.d.ts +1 -1
  12. package/dist/extensions/first-party/main-renderer/board/factory.js +4 -4
  13. package/dist/extensions/first-party/main-renderer/board/render.js +1 -1
  14. package/dist/extensions/first-party/main-renderer/board/types.d.ts +1 -1
  15. package/dist/extensions/first-party/main-renderer/coordinates/factory.d.ts +1 -1
  16. package/dist/extensions/first-party/main-renderer/coordinates/factory.js +2 -2
  17. package/dist/extensions/first-party/main-renderer/coordinates/render.js +1 -1
  18. package/dist/extensions/first-party/main-renderer/coordinates/types.d.ts +1 -1
  19. package/dist/extensions/first-party/main-renderer/denormalize.d.ts +3 -0
  20. package/dist/extensions/first-party/main-renderer/denormalize.js +18 -0
  21. package/dist/extensions/first-party/main-renderer/drag/factory.d.ts +2 -1
  22. package/dist/extensions/first-party/main-renderer/drag/factory.js +2 -1
  23. package/dist/extensions/first-party/main-renderer/drag/render.js +18 -11
  24. package/dist/extensions/first-party/main-renderer/drag/types.d.ts +2 -0
  25. package/dist/extensions/first-party/main-renderer/drag/update.js +4 -2
  26. package/dist/extensions/first-party/main-renderer/factory.d.ts +2 -1
  27. package/dist/extensions/first-party/main-renderer/factory.js +46 -12
  28. package/dist/extensions/first-party/main-renderer/normalize.d.ts +3 -3
  29. package/dist/extensions/first-party/main-renderer/normalize.js +19 -7
  30. package/dist/extensions/first-party/main-renderer/pieces/suppress.js +2 -1
  31. package/dist/extensions/first-party/main-renderer/public-defaults.d.ts +3 -0
  32. package/dist/extensions/first-party/main-renderer/public-defaults.js +4 -0
  33. package/dist/extensions/first-party/main-renderer/types/extension.d.ts +8 -5
  34. package/dist/extensions/first-party/main-renderer/types/instance.d.ts +10 -8
  35. package/dist/extensions/first-party/main-renderer/types/internal.d.ts +6 -7
  36. package/dist/extensions/first-party/main-renderer/types/internal.js +24 -1
  37. package/dist/extensions/first-party/main-renderer/types/public.d.ts +9 -0
  38. package/dist/extensions/first-party/main-renderer/types/template.d.ts +12 -1
  39. package/dist/extensions/first-party/promotion/types/input.d.ts +2 -2
  40. package/dist/extensions/index.d.ts +1 -0
  41. package/dist/extensions/index.js +1 -0
  42. package/dist/extensions/types/basic/events.d.ts +30 -16
  43. package/dist/extensions/types/basic/interaction.d.ts +12 -2
  44. package/dist/extensions/types/surface/commands.d.ts +4 -2
  45. package/dist/runtime/factory/input.js +50 -24
  46. package/dist/runtime/factory/main.js +11 -0
  47. package/dist/runtime/input/adapter/types.js +2 -1
  48. package/dist/runtime/input/controller/factory.js +36 -9
  49. package/dist/runtime/input/controller/helpers.d.ts +5 -0
  50. package/dist/runtime/input/controller/helpers.js +11 -0
  51. package/dist/runtime/input/controller/pointer.js +48 -21
  52. package/dist/runtime/input/controller/types.d.ts +13 -4
  53. package/dist/runtime/mutation/interaction.js +1 -1
  54. package/dist/runtime/mutation/types.d.ts +2 -2
  55. package/dist/state/interaction/config.d.ts +6 -0
  56. package/dist/state/interaction/config.js +30 -0
  57. package/dist/state/interaction/factory.js +15 -3
  58. package/dist/state/interaction/helpers.d.ts +18 -2
  59. package/dist/state/interaction/helpers.js +12 -0
  60. package/dist/state/interaction/mutation.d.ts +2 -0
  61. package/dist/state/interaction/reducers.d.ts +3 -0
  62. package/dist/state/interaction/reducers.js +22 -7
  63. package/dist/state/interaction/types/config.d.ts +9 -0
  64. package/dist/state/interaction/types/config.js +1 -0
  65. package/dist/state/interaction/types/input.d.ts +3 -0
  66. package/dist/state/interaction/types/internal.d.ts +14 -4
  67. package/dist/state/interaction/types/main.d.ts +11 -5
  68. package/dist/wrapper/factory.js +2 -0
  69. package/dist/wrapper/types.d.ts +1 -1
  70. package/package.json +5 -6
  71. package/dist/extensions/first-party/main-renderer/types/input.d.ts +0 -4
  72. /package/dist/extensions/first-party/main-renderer/types/{input.js → public.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @mirasen/chessboard
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - eaed694: Add configurable desktop and mobile drag behavior.
8
+
9
+ The board now exposes interaction config APIs through `setInteractionConfig()` and `getInteractionConfig()`, including `drag.liftedActivation.thresholdPx`. This enables desktop-style immediate drag activation and mobile-style delayed drag activation from the same interaction model.
10
+
11
+ The first-party main renderer now exposes runtime config APIs through `renderer.setConfig()` and `renderer.getConfig()`. Renderer config now includes dragged-piece visual settings and animation duration:
12
+ - `drag.pieceScale`
13
+ - `drag.pieceAnchor`
14
+ - `drag.pieceAnchorOffsetY`
15
+ - `animation.durationMs`
16
+
17
+ The package also exports public desktop/mobile main-renderer defaults from `@mirasen/chessboard/extensions` as `DefaultMainRendererDesktopConfig` and `DefaultMainRendererMobileConfig`.
18
+
19
+ Set `animation.durationMs` to `0` to disable piece movement animation by skipping animation creation.
20
+
21
+ Custom extension authors may need to update runtime interaction action names and payload fields to the new drag-session naming.
22
+
23
+ ### Patch Changes
24
+
25
+ - dde200f: dependabot: directory '/', update eslint
26
+ - dfe6882: dependabot: directory '/', update typescript from 5.9.3 to 6.0.3
27
+ - eb1fcb5: dependabot: directory '/', update @types/node
28
+ - eb1fcb5: dependabot: directory '/', update @vitest/browser-playwright
29
+ - eb1fcb5: dependabot: directory '/', update @vitest/coverage-istanbul
30
+ - eb1fcb5: dependabot: directory '/', update svelte
31
+ - eb1fcb5: dependabot: directory '/', update typescript-eslint
32
+ - eb1fcb5: dependabot: directory '/', update vitest
33
+ - d1da3c1: Prevent fast mouse dragging from triggering native browser drag-and-drop on SVG piece images.
34
+
35
+ Browsers can treat SVG images inside pieces as draggable content during quick mouse movement. When that native drag fallback starts, it can interrupt the board's pointer-driven drag flow by causing `pointercancel` / `lostpointercapture` and, in some cases, preventing the board from receiving the expected `pointerup`.
36
+
37
+ The board now suppresses native `dragstart` by default when no extension consumes the event, reducing one common source of interrupted fast mouse drags while keeping the existing `lostpointercapture` recovery path for other platform edge cases. `dragstart` remains observable by extensions and does not produce a core interaction action.
38
+
3
39
  ## 1.2.4
4
40
 
5
41
  ### Patch Changes
package/README.md CHANGED
@@ -11,6 +11,7 @@ A framework-agnostic TypeScript chessboard platform with real chess interaction
11
11
  ## Try it live (including annotations)
12
12
 
13
13
  - [Play a chess.js game](https://mirasen.io/chessboard/examples/chessjs.html)
14
+ - [Watch 12 live boards](https://mirasen.io/chessboard/examples/live-games-grid.html)
14
15
  - [Try the promotion flow](https://mirasen.io/chessboard/examples/promotion.html)
15
16
  - [Try the minimal example](https://mirasen.io/chessboard/examples/minimal.html)
16
17
 
@@ -18,6 +19,14 @@ A framework-agnostic TypeScript chessboard platform with real chess interaction
18
19
 
19
20
  Use [`@mirasen/react-chessboard`](https://www.npmjs.com/package/@mirasen/react-chessboard) for a React component with the same built-in interaction, annotations, promotion, animation, and chess.js integration.
20
21
 
22
+ ## Native mobile apps
23
+
24
+ `@mirasen/chessboard` is not tied to React or any other web framework. It is plain TypeScript/JavaScript on top of DOM and SVG, so it should fit WebView-based native iOS or Android integrations.
25
+
26
+ A common integration pattern is to bundle a WebView host page, mount the board there, and communicate with the native app through the platform bridge.
27
+
28
+ This keeps the integration model lightweight while avoiding a separate native chessboard UI implementation.
29
+
21
30
  ## Why this exists
22
31
 
23
32
  Most chessboards fall into one of two buckets:
@@ -3,6 +3,7 @@ import { isNonEmptyPieceCode } from '../state/board/check.js';
3
3
  import { fileOf, rankOf } from '../state/board/coords.js';
4
4
  import { SQUARE_COUNT } from '../state/board/types/internal.js';
5
5
  import { baseMovesEqual } from '../state/change/helpers.js';
6
+ import { isDragSessionActiveLiftedPiece } from '../state/interaction/helpers.js';
6
7
  // ---------------------------------------------------------------------------
7
8
  // Precomputed squared distance: (dFile² + dRank²) for all square pairs.
8
9
  // Max value = 7² + 7² = 98, fits in Uint8. Index: a * 64 + b.
@@ -31,7 +32,7 @@ const EMPTY_EXCLUDE = {
31
32
  */
32
33
  function detectLiftedPieceDrop(previous, current) {
33
34
  const prevDrag = previous.interaction.dragSession;
34
- if (!prevDrag || prevDrag.type !== 'lifted-piece-drag')
35
+ if (!prevDrag || !isDragSessionActiveLiftedPiece(prevDrag))
35
36
  return null;
36
37
  if (current.interaction.dragSession !== null)
37
38
  return null;
@@ -10,8 +10,8 @@ export type { ColorCode, EmptyPieceCode, NonEmptyPieceCode, PieceCode, RoleCode,
10
10
  export type { MovabilityModeCode } from '../../state/interaction/types/internal.js';
11
11
  export { extensionCreateInternalBase, extensionDestroyBase, extensionIsDestroyedBase, extensionIsMountedBase, extensionMountBase, extensionUnmountBase } from '../first-party/common/helpers.js';
12
12
  export type { ExtensionInternalBase, OpaqueColor } from '../first-party/common/types.js';
13
- export type { PieceUrlsInput } from '../first-party/main-renderer/types/input.js';
14
13
  export type { PieceUrls } from '../first-party/main-renderer/types/internal.js';
14
+ export type { PieceUrlsPublic } from '../first-party/main-renderer/types/public.js';
15
15
  export type { PromotionPieceCode } from '../first-party/promotion/types/internal.js';
16
16
  export type { ExtensionSlotName, ExtensionSlotSvgRoots } from '../types/basic/mount.js';
17
17
  export { isFrameMounted, isFrameRenderable } from '../types/basic/update.js';
@@ -5,7 +5,7 @@ export function annotationsOnUpdate(state, context) {
5
5
  if (state.config.clearOnCoreInteraction &&
6
6
  hasCommittedAnnotations(state) &&
7
7
  context.mutation.hasMutation({
8
- causes: ['runtime.interaction.completeCoreDragTo']
8
+ causes: ['runtime.interaction.completeCoreDragSessionTo']
9
9
  })) {
10
10
  clearCommittedAnnotations(state);
11
11
  context.invalidation.markDirty(1 /* DirtyLayer.COMMITTED */);
@@ -45,8 +45,8 @@ function createBoardEventsInstance(options) {
45
45
  });
46
46
  const interactionCompleted = context.mutation.hasMutation({
47
47
  causes: [
48
- 'runtime.interaction.completeCoreDragTo',
49
- 'runtime.interaction.completeExtensionDragTo',
48
+ 'runtime.interaction.completeCoreDragSessionTo',
49
+ 'runtime.interaction.completeExtensionDragSession',
50
50
  'runtime.interaction.resolveDeferredUIMoveRequest'
51
51
  ]
52
52
  });
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
2
2
  import type { PieceSymbolResolver } from '../piece-symbols.js';
3
+ import type { MainRendererConfigAnimation } from '../types/template.js';
3
4
  import type { MainRendererAnimation } from './types.js';
4
- export declare function createMainRendererAnimation(runtimeSurface: ExtensionRuntimeSurface, resolver: PieceSymbolResolver): MainRendererAnimation;
5
+ export declare function createMainRendererAnimation(runtimeSurface: ExtensionRuntimeSurface, resolver: PieceSymbolResolver, getAnimationConfig: () => MainRendererConfigAnimation): MainRendererAnimation;
@@ -1,11 +1,12 @@
1
1
  import { cleanAnimationPlan } from '../../../../animation/render/plan.js';
2
2
  import { getAnimationSuppressedSquares, rendererAnimationClean, rendererAnimationPrepare, rendererAnimationRender } from './render.js';
3
3
  import { rendererAnimationOnUpdate } from './update.js';
4
- export function createMainRendererAnimation(runtimeSurface, resolver) {
4
+ export function createMainRendererAnimation(runtimeSurface, resolver, getAnimationConfig) {
5
5
  const state = {
6
6
  runtimeSurface,
7
7
  resolver,
8
- entries: new Map()
8
+ entries: new Map(),
9
+ getAnimationConfig
9
10
  };
10
11
  return {
11
12
  onUpdate(context) {
@@ -5,6 +5,7 @@ import { ExtensionCleanAnimationContext, ExtensionPrepareAnimationContext, Exten
5
5
  import { ExtensionUpdateContext } from '../../../types/context/update.js';
6
6
  import { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
7
7
  import type { PieceSymbolResolver } from '../piece-symbols.js';
8
+ import type { MainRendererConfigAnimation } from '../types/template.js';
8
9
  export interface MainRendererAnimationEntry {
9
10
  plan: AnimationPlan;
10
11
  nodes: PreparedNodeMap | null;
@@ -13,6 +14,7 @@ export interface MainRendererAnimationInternal {
13
14
  readonly runtimeSurface: ExtensionRuntimeSurface;
14
15
  readonly resolver: PieceSymbolResolver;
15
16
  readonly entries: Map<number, MainRendererAnimationEntry>;
17
+ readonly getAnimationConfig: () => MainRendererConfigAnimation;
16
18
  }
17
19
  export interface MainRendererAnimation {
18
20
  onUpdate(context: ExtensionUpdateContext): void;
@@ -2,7 +2,6 @@ import { calculateAnimationPlan } from '../../../../animation/planner.js';
2
2
  import { piecePositionsEqual } from '../../../../state/board/check.js';
3
3
  import { changeStatesEqual } from '../../../../state/change/helpers.js';
4
4
  import { isUpdateContextRenderable } from '../../../types/context/update.js';
5
- const DEFAULT_ANIMATION_DURATION_MS = 180;
6
5
  export function rendererAnimationOnUpdate(state, context) {
7
6
  if (!isUpdateContextRenderable(context) ||
8
7
  !context.mutation.hasMutation({ prefixes: ['state.board.', 'state.change.'] }) ||
@@ -12,6 +11,9 @@ export function rendererAnimationOnUpdate(state, context) {
12
11
  if (piecePositionsEqual(context.previousFrame.state.board, context.currentFrame.state.board) &&
13
12
  changeStatesEqual(context.previousFrame.state.change, context.currentFrame.state.change))
14
13
  return;
14
+ const durationMs = state.getAnimationConfig().durationMs;
15
+ if (durationMs === 0)
16
+ return;
15
17
  // Pass raw snapshots to the planner — it normalizes internally
16
18
  const plan = calculateAnimationPlan({
17
19
  previous: context.previousFrame.state,
@@ -20,7 +22,7 @@ export function rendererAnimationOnUpdate(state, context) {
20
22
  if (plan.tracks.length === 0)
21
23
  return;
22
24
  const session = state.runtimeSurface.animation.submit({
23
- duration: DEFAULT_ANIMATION_DURATION_MS
25
+ duration: durationMs
24
26
  });
25
27
  state.entries.set(session.id, { plan, nodes: null });
26
28
  }
@@ -1,3 +1,3 @@
1
1
  import { ConfigColorPair } from '../types/internal.js';
2
2
  import { MainRendererBoard } from './types.js';
3
- export declare function createMainRendererBoard(config: ConfigColorPair): MainRendererBoard;
3
+ export declare function createMainRendererBoard(getColorConfig: () => ConfigColorPair): MainRendererBoard;
@@ -1,10 +1,10 @@
1
1
  import { rendererBoardRender } from './render.js';
2
2
  import { rendererBoardOnUpdate } from './update.js';
3
- function createMainRendererBoardInternal(config) {
4
- return { config };
3
+ function createMainRendererBoardInternal(getColorConfig) {
4
+ return { getColorConfig };
5
5
  }
6
- export function createMainRendererBoard(config) {
7
- const internalState = createMainRendererBoardInternal(config);
6
+ export function createMainRendererBoard(getColorConfig) {
7
+ const internalState = createMainRendererBoardInternal(getColorConfig);
8
8
  return {
9
9
  onUpdate(context) {
10
10
  rendererBoardOnUpdate(internalState, context);
@@ -8,7 +8,7 @@ export function rendererBoardRender(state, context, slot) {
8
8
  }
9
9
  const geometry = context.currentFrame.layout.geometry;
10
10
  clearSvgElementChildren(slot);
11
- const { light, dark } = state.config;
11
+ const { light, dark } = state.getColorConfig();
12
12
  for (let sq = 0; sq < SQUARE_COUNT; sq++) {
13
13
  const r = geometry.getSquareRect(sq);
14
14
  createSvgElement(slot, 'rect', {
@@ -2,7 +2,7 @@ import { ExtensionRenderContext } from '../../../types/context/render.js';
2
2
  import { ExtensionUpdateContext } from '../../../types/context/update.js';
3
3
  import { ConfigColorPair } from '../types/internal.js';
4
4
  export interface MainRendererBoardInternal {
5
- readonly config: ConfigColorPair;
5
+ readonly getColorConfig: () => ConfigColorPair;
6
6
  }
7
7
  export interface MainRendererBoard {
8
8
  onUpdate(context: ExtensionUpdateContext): void;
@@ -1,3 +1,3 @@
1
1
  import { ConfigColorPair } from '../types/internal.js';
2
2
  import { MainRendererCoordinates } from './types.js';
3
- export declare function createMainRendererCoordinates(config: ConfigColorPair): MainRendererCoordinates;
3
+ export declare function createMainRendererCoordinates(getColorConfig: () => ConfigColorPair): MainRendererCoordinates;
@@ -1,6 +1,6 @@
1
1
  import { rendererCoordinatesRender } from './render.js';
2
- export function createMainRendererCoordinates(config) {
3
- const internalState = { config };
2
+ export function createMainRendererCoordinates(getColorConfig) {
3
+ const internalState = { getColorConfig };
4
4
  return {
5
5
  render(context, slot) {
6
6
  rendererCoordinatesRender(internalState, context, slot);
@@ -10,7 +10,7 @@ export function rendererCoordinatesRender(state, context, slot) {
10
10
  }
11
11
  const geometry = context.currentFrame.layout.geometry;
12
12
  clearSvgElementChildren(slot);
13
- const coords = state.config;
13
+ const coords = state.getColorConfig();
14
14
  const fontSize = geometry.squareSize * 0.12;
15
15
  const offset = 3;
16
16
  // Rank labels on the visual left edge
@@ -1,7 +1,7 @@
1
1
  import { ExtensionRenderContext } from '../../../types/context/render.js';
2
2
  import { ConfigColorPair } from '../types/internal.js';
3
3
  export interface MainRendererCoordinatesInternal {
4
- readonly config: ConfigColorPair;
4
+ readonly getColorConfig: () => ConfigColorPair;
5
5
  }
6
6
  export interface MainRendererCoordinates {
7
7
  render(context: ExtensionRenderContext, slot: SVGGElement): void;
@@ -0,0 +1,3 @@
1
+ import type { MainRendererConfig } from './types/internal.js';
2
+ import type { MainRendererConfigPublic } from './types/public.js';
3
+ export declare function denormalizeMainRendererConfig(config: MainRendererConfig): MainRendererConfigPublic;
@@ -0,0 +1,18 @@
1
+ import { cloneDeep } from 'es-toolkit';
2
+ import { denormalizePieceString } from '../../../state/board/denormalize.js';
3
+ function denormalizePieceUrls(pieceUrls) {
4
+ const result = {};
5
+ for (const [codeKey, url] of Object.entries(pieceUrls)) {
6
+ const code = Number(codeKey);
7
+ result[denormalizePieceString(code)] = url;
8
+ }
9
+ return result;
10
+ }
11
+ export function denormalizeMainRendererConfig(config) {
12
+ return {
13
+ colors: cloneDeep(config.colors),
14
+ drag: cloneDeep(config.drag),
15
+ animation: cloneDeep(config.animation),
16
+ pieceUrls: denormalizePieceUrls(config.pieceUrls)
17
+ };
18
+ }
@@ -1,4 +1,5 @@
1
1
  import { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
2
2
  import type { PieceSymbolResolver } from '../piece-symbols.js';
3
+ import type { MainRendererConfigDrag } from '../types/template.js';
3
4
  import { MainRendererDrag } from './types.js';
4
- export declare function createMainRendererDrag(runtimeSurface: ExtensionRuntimeSurface, resolver: PieceSymbolResolver): MainRendererDrag;
5
+ export declare function createMainRendererDrag(runtimeSurface: ExtensionRuntimeSurface, resolver: PieceSymbolResolver, getDragConfig: () => MainRendererConfigDrag): MainRendererDrag;
@@ -1,9 +1,10 @@
1
1
  import { rendererDragRenderTransientVisuals } from './render.js';
2
2
  import { rendererDragOnUpdate } from './update.js';
3
- export function createMainRendererDrag(runtimeSurface, resolver) {
3
+ export function createMainRendererDrag(runtimeSurface, resolver, getDragConfig) {
4
4
  const internalState = {
5
5
  runtimeSurface,
6
6
  resolver,
7
+ getDragConfig,
7
8
  isDragActive: false,
8
9
  pieceCode: null,
9
10
  pieceNode: null
@@ -1,28 +1,35 @@
1
1
  import { createSvgElement, updateSvgElementAttributes } from '../../../../render/svg/helpers.js';
2
2
  export function rendererDragRenderTransientVisuals(state, context, slot) {
3
+ const config = state.getDragConfig();
3
4
  const geometry = context.currentFrame.layout.geometry;
4
5
  const point = context.transientInput.boardClampedPoint;
5
- const squareSize = geometry.squareSize.toString();
6
- const x = (point.x - geometry.squareSize / 2).toString();
7
- const y = (point.y - geometry.squareSize / 2).toString();
6
+ const renderedSize = geometry.squareSize * config.pieceScale;
7
+ const anchorOffsetY = geometry.squareSize * config.pieceAnchorOffsetY;
8
+ const x = point.x - renderedSize / 2;
9
+ const y = config.pieceAnchor === 'bottom'
10
+ ? point.y - renderedSize + anchorOffsetY
11
+ : point.y - renderedSize / 2 + anchorOffsetY;
12
+ const renderedSizeStr = renderedSize.toString();
13
+ const xStr = x.toString();
14
+ const yStr = y.toString();
8
15
  if (state.isDragActive && state.pieceCode && !state.pieceNode) {
9
16
  // Ok this is first render pass, let's create the piece node
10
17
  const href = state.resolver.getHref(state.pieceCode);
11
18
  state.pieceNode = createSvgElement(slot, 'use', {
12
19
  'data-chessboard-id': 'dragged-piece',
13
- width: squareSize,
14
- height: squareSize,
15
- x,
16
- y,
20
+ width: renderedSizeStr,
21
+ height: renderedSizeStr,
22
+ x: xStr,
23
+ y: yStr,
17
24
  href
18
25
  });
19
26
  }
20
27
  else if (state.isDragActive && state.pieceNode) {
21
28
  updateSvgElementAttributes(state.pieceNode, {
22
- width: squareSize,
23
- height: squareSize,
24
- x,
25
- y
29
+ width: renderedSizeStr,
30
+ height: renderedSizeStr,
31
+ x: xStr,
32
+ y: yStr
26
33
  });
27
34
  }
28
35
  }
@@ -3,9 +3,11 @@ import { ExtensionRenderTransientVisualsContext } from '../../../types/context/t
3
3
  import { ExtensionUpdateContext } from '../../../types/context/update.js';
4
4
  import { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
5
5
  import type { PieceSymbolResolver } from '../piece-symbols.js';
6
+ import type { MainRendererConfigDrag } from '../types/template.js';
6
7
  export interface MainRendererDragInternal {
7
8
  readonly runtimeSurface: ExtensionRuntimeSurface;
8
9
  readonly resolver: PieceSymbolResolver;
10
+ readonly getDragConfig: () => MainRendererConfigDrag;
9
11
  isDragActive: boolean;
10
12
  pieceCode: NonEmptyPieceCode | null;
11
13
  pieceNode: SVGUseElement | null;
@@ -1,10 +1,12 @@
1
1
  import assert from '@ktarmyshov/assert';
2
2
  import { isNonEmptyPieceCode } from '../../../../state/board/check.js';
3
+ import { isDragSessionActiveLiftedPiece } from '../../../../state/interaction/helpers.js';
3
4
  export function rendererDragOnUpdate(state, context) {
4
- const isLiftedDragActive = context.currentFrame.state.interaction.dragSession?.type === 'lifted-piece-drag';
5
+ const dragSession = context.currentFrame.state.interaction.dragSession;
6
+ const isLiftedDragActive = dragSession !== null && isDragSessionActiveLiftedPiece(dragSession);
5
7
  if (isLiftedDragActive) {
6
8
  if (!state.isDragActive) {
7
- const pieceCode = context.currentFrame.state.interaction.dragSession.sourcePieceCode;
9
+ const pieceCode = dragSession.sourcePieceCode;
8
10
  assert(isNonEmptyPieceCode(pieceCode), 'Invalid piece code in drag session');
9
11
  state.pieceCode = pieceCode;
10
12
  state.runtimeSurface.transientVisuals.subscribe();
@@ -1,2 +1,3 @@
1
- import { MainRendererDefinition, MainRendererInitOptions } from './types/extension.js';
1
+ import { MainRendererDefinition } from './types/extension.js';
2
+ import type { MainRendererInitOptions } from './types/public.js';
2
3
  export declare function createMainRenderer(options?: MainRendererInitOptions): MainRendererDefinition;
@@ -1,16 +1,16 @@
1
- import { toMerged } from 'es-toolkit';
2
1
  import { extensionCreateInternalBase, extensionDestroyBase, extensionMountBase, extensionUnmountBase } from '../common/helpers.js';
3
2
  import { createMainRendererAnimation } from './animation/factory.js';
4
3
  import { createMainRendererBoard } from './board/factory.js';
5
4
  import { createMainRendererCoordinates } from './coordinates/factory.js';
5
+ import { denormalizeMainRendererConfig } from './denormalize.js';
6
6
  import { createMainRendererDrag } from './drag/factory.js';
7
7
  import { normalizeMainRendererConfig } from './normalize.js';
8
8
  import { createPieceSymbolResolver, ensurePieceSymbolsDefined } from './piece-symbols.js';
9
9
  import { createMainRendererPieces } from './pieces/factory.js';
10
- import { EXTENSION_ID, EXTENSION_SLOTS } from './types/extension.js';
11
- import { DEFAULT_MAIN_RENDERER_CONFIG } from './types/internal.js';
12
- export function createMainRenderer(options = {}) {
13
- const config = toMerged(DEFAULT_MAIN_RENDERER_CONFIG, normalizeMainRendererConfig(options));
10
+ import { DirtyLayer, EXTENSION_ID, EXTENSION_SLOTS } from './types/extension.js';
11
+ import { DefaultMainRendererDesktopConfig } from './types/internal.js';
12
+ export function createMainRenderer(options) {
13
+ const config = normalizeMainRendererConfig(options, DefaultMainRendererDesktopConfig);
14
14
  return {
15
15
  id: EXTENSION_ID,
16
16
  slots: EXTENSION_SLOTS,
@@ -21,12 +21,15 @@ export function createMainRenderer(options = {}) {
21
21
  }
22
22
  function createMainRendererInternal(options, config) {
23
23
  const pieceSymbolResolver = createPieceSymbolResolver(options.svgIds);
24
- const board = createMainRendererBoard(config.colors.board);
25
- const coordinates = createMainRendererCoordinates(config.colors.coordinates);
24
+ // Forward reference: subsystems read color config through getters that close over the
25
+ // internal state, so a runtime setConfig({ colors }) update is observed on the next render
26
+ // without re-creating the subsystems.
27
+ const board = createMainRendererBoard(() => internalState.config.colors.board);
28
+ const coordinates = createMainRendererCoordinates(() => internalState.config.colors.coordinates);
26
29
  const pieces = createMainRendererPieces(pieceSymbolResolver);
27
- const drag = createMainRendererDrag(options.runtimeSurface, pieceSymbolResolver);
28
- const animation = createMainRendererAnimation(options.runtimeSurface, pieceSymbolResolver);
29
- return {
30
+ const drag = createMainRendererDrag(options.runtimeSurface, pieceSymbolResolver, () => internalState.config.drag);
31
+ const animation = createMainRendererAnimation(options.runtimeSurface, pieceSymbolResolver, () => internalState.config.animation);
32
+ const internalState = {
30
33
  ...extensionCreateInternalBase(options),
31
34
  board,
32
35
  coordinates,
@@ -34,7 +37,34 @@ function createMainRendererInternal(options, config) {
34
37
  drag,
35
38
  animation,
36
39
  runtimeSurface: options.runtimeSurface,
37
- pieceSymbolResolver
40
+ pieceSymbolResolver,
41
+ config
42
+ };
43
+ return internalState;
44
+ }
45
+ function createMainRendererInstancePublic(state) {
46
+ return {
47
+ getConfig() {
48
+ return denormalizeMainRendererConfig(state.config);
49
+ },
50
+ setConfig(options) {
51
+ // Defensively strip pieceUrls so a non-TS caller cannot bypass the lifecycle.
52
+ const { pieceUrls: _ignored, ...safeInput } = (options ?? {});
53
+ void _ignored;
54
+ const previous = state.config;
55
+ state.config = normalizeMainRendererConfig(safeInput, state.config);
56
+ let dirty = 0;
57
+ if (state.config.colors.board !== previous.colors.board) {
58
+ dirty |= DirtyLayer.Board;
59
+ }
60
+ if (state.config.colors.coordinates !== previous.colors.coordinates) {
61
+ dirty |= DirtyLayer.Coordinates;
62
+ }
63
+ if (dirty !== 0) {
64
+ state.runtimeSurface.invalidation.markDirty(dirty);
65
+ state.runtimeSurface.commands.requestRender({ state: true });
66
+ }
67
+ }
38
68
  };
39
69
  }
40
70
  function extensionUnmountLocal(state) {
@@ -51,11 +81,12 @@ function validateIsMounted(state) {
51
81
  }
52
82
  function createMainRendererInstance(options, config) {
53
83
  const internalState = createMainRendererInternal(options, config);
84
+ const publicInterface = createMainRendererInstancePublic(internalState);
54
85
  return {
55
86
  id: EXTENSION_ID,
56
87
  mount(env) {
57
88
  extensionMountBase(internalState, env.slotRoots);
58
- ensurePieceSymbolsDefined(env.slotRoots.defs, config.pieceUrls, internalState.pieceSymbolResolver);
89
+ ensurePieceSymbolsDefined(env.slotRoots.defs, internalState.config.pieceUrls, internalState.pieceSymbolResolver);
59
90
  },
60
91
  onUpdate(context) {
61
92
  internalState.board.onUpdate(context);
@@ -94,6 +125,9 @@ function createMainRendererInstance(options, config) {
94
125
  destroy() {
95
126
  extensionUnmountLocal(internalState);
96
127
  extensionDestroyBase(internalState);
128
+ },
129
+ getPublic() {
130
+ return publicInterface;
97
131
  }
98
132
  };
99
133
  }
@@ -1,3 +1,3 @@
1
- import { MainRendererConfigInput } from './types/input.js';
2
- import { MainRendererConfig } from './types/internal.js';
3
- export declare function normalizeMainRendererConfig(input: Partial<MainRendererConfigInput>): MainRendererConfig;
1
+ import type { MainRendererConfig } from './types/internal.js';
2
+ import type { MainRendererInitOptions } from './types/public.js';
3
+ export declare function normalizeMainRendererConfig(input: MainRendererInitOptions | undefined, base: MainRendererConfig): MainRendererConfig;
@@ -1,7 +1,7 @@
1
1
  import assert from '@ktarmyshov/assert';
2
+ import { toMerged } from 'es-toolkit/object';
2
3
  import { normalizePiece } from '../../../state/board/normalize.js';
3
4
  import { ALL_NON_EMPTY_PIECE_CODES } from '../../../state/board/types/internal.js';
4
- import { DEFAULT_MAIN_RENDERER_CONFIG } from './types/internal.js';
5
5
  function assertPieceUrlsComplete(pieceUrls) {
6
6
  for (const pieceCode of ALL_NON_EMPTY_PIECE_CODES) {
7
7
  assert(pieceCode in pieceUrls, `Missing URL for piece code: ${pieceCode}`);
@@ -17,11 +17,23 @@ function normalizePieceUrls(input) {
17
17
  assertPieceUrlsComplete(normalized);
18
18
  return normalized;
19
19
  }
20
- export function normalizeMainRendererConfig(input) {
21
- return {
22
- colors: input.colors ?? DEFAULT_MAIN_RENDERER_CONFIG.colors,
23
- pieceUrls: input.pieceUrls
24
- ? normalizePieceUrls(input.pieceUrls)
25
- : DEFAULT_MAIN_RENDERER_CONFIG.pieceUrls
20
+ function validateMainRendererConfig(config) {
21
+ const { pieceScale, pieceAnchor, pieceAnchorOffsetY } = config.drag;
22
+ assert(Number.isFinite(pieceScale), 'drag.pieceScale must be a finite number');
23
+ assert(pieceScale > 0, 'drag.pieceScale must be > 0');
24
+ assert(pieceAnchor === 'center' || pieceAnchor === 'bottom', `drag.pieceAnchor must be 'center' or 'bottom', received: ${String(pieceAnchor)}`);
25
+ assert(Number.isFinite(pieceAnchorOffsetY), 'drag.pieceAnchorOffsetY must be a finite number');
26
+ const { durationMs } = config.animation;
27
+ assert(Number.isFinite(durationMs), 'animation.durationMs must be a finite number');
28
+ assert(durationMs >= 0, 'animation.durationMs must be >= 0');
29
+ }
30
+ export function normalizeMainRendererConfig(input, base) {
31
+ const { pieceUrls: inputPieceUrls, ...rest } = input ?? {};
32
+ const merged = toMerged(base, rest);
33
+ const result = {
34
+ ...merged,
35
+ pieceUrls: inputPieceUrls !== undefined ? normalizePieceUrls(inputPieceUrls) : merged.pieceUrls
26
36
  };
37
+ validateMainRendererConfig(result);
38
+ return result;
27
39
  }
@@ -1,6 +1,7 @@
1
+ import { isDragSessionActiveLiftedPiece } from '../../../../state/interaction/helpers.js';
1
2
  export function calculateSuppressedSquares(_state, context, animationSuppressedSquares) {
2
3
  const dragSession = context.currentFrame.state.interaction.dragSession;
3
- const dragSquare = dragSession?.type === 'lifted-piece-drag' ? dragSession.sourceSquare : null;
4
+ const dragSquare = dragSession && isDragSessionActiveLiftedPiece(dragSession) ? dragSession.sourceSquare : null;
4
5
  const result = new Set(animationSuppressedSquares);
5
6
  if (dragSquare !== null) {
6
7
  result.add(dragSquare);
@@ -0,0 +1,3 @@
1
+ import type { MainRendererConfigPublic } from './types/public.js';
2
+ export declare const DefaultMainRendererDesktopConfigPublic: MainRendererConfigPublic;
3
+ export declare const DefaultMainRendererMobileConfigPublic: MainRendererConfigPublic;
@@ -0,0 +1,4 @@
1
+ import { denormalizeMainRendererConfig } from './denormalize.js';
2
+ import { DefaultMainRendererDesktopConfig, DefaultMainRendererMobileConfig } from './types/internal.js';
3
+ export const DefaultMainRendererDesktopConfigPublic = denormalizeMainRendererConfig(DefaultMainRendererDesktopConfig);
4
+ export const DefaultMainRendererMobileConfigPublic = denormalizeMainRendererConfig(DefaultMainRendererMobileConfig);
@@ -1,11 +1,14 @@
1
- import { ExtensionDefinition, ExtensionInstance } from '../../../types/extension.js';
2
- import { MainRendererConfigInput } from './input.js';
3
- export type MainRendererInitOptions = Partial<MainRendererConfigInput>;
1
+ import type { ExtensionDefinition, ExtensionInstance } from '../../../types/extension.js';
2
+ import type { MainRendererConfigPublic, MainRendererSetConfigOptions } from './public.js';
4
3
  export declare const EXTENSION_SLOTS: readonly ["defs", "board", "coordinates", "pieces", "animation", "drag"];
5
4
  export type ExtensionSlotsType = typeof EXTENSION_SLOTS;
6
5
  export declare const EXTENSION_ID: "renderer";
7
- export type MainRendererDefinition = ExtensionDefinition<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, never>;
8
- export type MainRendererInstance = ExtensionInstance<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, never>;
6
+ export interface RendererPublicAPI {
7
+ setConfig(options: MainRendererSetConfigOptions): void;
8
+ getConfig(): MainRendererConfigPublic;
9
+ }
10
+ export type MainRendererDefinition = ExtensionDefinition<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, RendererPublicAPI>;
11
+ export type MainRendererInstance = ExtensionInstance<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, RendererPublicAPI>;
9
12
  export declare enum DirtyLayer {
10
13
  Board = 1,// 1 << 0,
11
14
  Coordinates = 2,// 1 << 1,
@@ -1,12 +1,13 @@
1
- import { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
2
- import { ExtensionInternalBase } from '../../common/types.js';
3
- import { MainRendererAnimation } from '../animation/types.js';
4
- import { MainRendererBoard } from '../board/types.js';
5
- import { MainRendererCoordinates } from '../coordinates/types.js';
6
- import { MainRendererDrag } from '../drag/types.js';
1
+ import type { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
2
+ import type { ExtensionInternalBase } from '../../common/types.js';
3
+ import type { MainRendererAnimation } from '../animation/types.js';
4
+ import type { MainRendererBoard } from '../board/types.js';
5
+ import type { MainRendererCoordinates } from '../coordinates/types.js';
6
+ import type { MainRendererDrag } from '../drag/types.js';
7
7
  import type { PieceSymbolResolver } from '../piece-symbols.js';
8
- import { MainRendererPieces } from '../pieces/types.js';
9
- import { ExtensionSlotsType } from './extension.js';
8
+ import type { MainRendererPieces } from '../pieces/types.js';
9
+ import type { ExtensionSlotsType } from './extension.js';
10
+ import type { MainRendererConfig } from './internal.js';
10
11
  export interface MainRendererInstanceInternal extends ExtensionInternalBase<ExtensionSlotsType> {
11
12
  readonly board: MainRendererBoard;
12
13
  readonly coordinates: MainRendererCoordinates;
@@ -15,4 +16,5 @@ export interface MainRendererInstanceInternal extends ExtensionInternalBase<Exte
15
16
  readonly animation: MainRendererAnimation;
16
17
  readonly runtimeSurface: ExtensionRuntimeSurface;
17
18
  readonly pieceSymbolResolver: PieceSymbolResolver;
19
+ config: MainRendererConfig;
18
20
  }