@mirasen/chessboard 1.3.4 → 1.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # @mirasen/chessboard
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 63415ac: feat: add first-party `check` extension for king-in-check square highlighting
8
+
9
+ Adds a built-in `check` extension that renders a red radial glow on a consumer-supplied square to indicate a king in check. The board remains rules-agnostic — the highlighted square is entirely determined by the caller.
10
+
11
+ ```ts
12
+ board.extensions.check.square = 'e1'; // highlight
13
+ board.extensions.check.square = null; // clear
14
+ ```
15
+
16
+ The extension is included in the default built-in set, so no extra registration is needed. The default visual is a red radial gradient (center-to-transparent) rendered below pieces in the `underPieces` slot. Color and opacity are configurable via `createCheck({ color, opacity })`.
17
+
18
+ ### Patch Changes
19
+
20
+ - 9dda2cf: chore: switch coverage provider from istanbul to v8
21
+ - 2c8eea7: dependabot: directory '/', update @ktarmyshov/assert
22
+ - 2c8eea7: dependabot: directory '/', update @playwright/test
23
+ - 2c8eea7: dependabot: directory '/', update @sveltejs/kit
24
+ - 2c8eea7: dependabot: directory '/', update globals
25
+ - 2c8eea7: dependabot: directory '/', update jsdom
26
+ - 2c8eea7: dependabot: directory '/', update playwright
27
+ - 2c8eea7: dependabot: directory '/', update typescript-eslint
28
+ - 2c8eea7: dependabot: directory '/', update vite
29
+ - 4a6f1d3: dependabot: directory '/', update @sveltejs/vite-plugin-svelte
30
+ - 4a6f1d3: dependabot: directory '/', update @types/node
31
+ - 4a6f1d3: dependabot: directory '/', update eslint
32
+ - 4a6f1d3: dependabot: directory '/', update publint
33
+ - 4a6f1d3: dependabot: directory '/', update svelte-check
34
+ - 2c8eea7: dependabot: directory '/', update typescript-eslint
35
+ - 2c8eea7: dependabot: directory '/', update vite
36
+ - 6bf7293: dependabot: directory '/', update es-toolkit
37
+ - 6bf7293: dependabot: directory '/', update eslint-plugin-svelte
38
+ - 2c8eea7: dependabot: directory '/', update globals
39
+ - 4a6f1d3: dependabot: directory '/', update svelte-check
40
+ - 6bf7293: dependabot: directory '/', update svelte
41
+ - 2c8eea7: dependabot: directory '/', update @sveltejs/kit
42
+ - ae1574c: dependabot: directory '/', update @vitest/browser-playwright
43
+ - 4a6f1d3: dependabot: directory '/', update eslint
44
+ - 4a6f1d3: dependabot: directory '/', update publint
45
+ - 6bf7293: dependabot: directory '/', update svelte
46
+ - 2c8eea7: dependabot: directory '/', update vite
47
+ - dee1c04: dependabot: directory '/', update @changesets/cli from 2.31.1 to 3.0.1
48
+ - 4a6f1d3: dependabot: directory '/', update @types/node
49
+ - 4a6f1d3: dependabot: directory '/', update eslint
50
+ - 2c8eea7: dependabot: directory '/', update typescript-eslint
51
+
3
52
  ## 1.3.4
4
53
 
5
54
  ### Patch Changes
package/README.md CHANGED
@@ -45,7 +45,7 @@ This is not just a board renderer. It is a chessboard platform designed for real
45
45
 
46
46
  ## What you get out of the box
47
47
 
48
- The default first-party extension baseline includes rendering, events, selection, active target feedback, legal move hints, last move feedback, annotations, promotion UI, and optional auto-promotion.
48
+ The default first-party extension baseline includes rendering, events, selection, active target feedback, legal move hints, last move feedback, check square highlight, annotations, promotion UI, and optional auto-promotion.
49
49
 
50
50
  - `renderer` — the first-party rendering extension that validates the same extension architecture used for board features
51
51
  - `events`
@@ -53,6 +53,7 @@ The default first-party extension baseline includes rendering, events, selection
53
53
  - `activeTarget`
54
54
  - `legalMoves`
55
55
  - `lastMove`
56
+ - `check`
56
57
  - `annotations`
57
58
  - `promotion`
58
59
  - `autoPromote`
@@ -0,0 +1,2 @@
1
+ import { CheckDefinition, CheckInitConfig } from './types.js';
2
+ export declare function createCheck(config?: CheckInitConfig): CheckDefinition;
@@ -0,0 +1,146 @@
1
+ import assert from '@ktarmyshov/assert';
2
+ import { toMerged } from 'es-toolkit';
3
+ import { isSquareString } from '../../../state/board/check.js';
4
+ import { denormalizeSquare } from '../../../state/board/denormalize.js';
5
+ import { normalizeSquare } from '../../../state/board/normalize.js';
6
+ import { createSvgElement, updateSvgElementAttributes } from '../../../render/svg/helpers.js';
7
+ import { isUpdateContextRenderable } from '../../types/context/update.js';
8
+ import { extensionCreateInternalBase, extensionDestroyBase, extensionMountBase, extensionUnmountBase } from '../common/helpers.js';
9
+ import { markHighlightDirtyAndRequestRender } from './invalidation.js';
10
+ import { DEFAULT_CONFIG, EXTENSION_ID, EXTENSION_SLOTS, GRADIENT_STOPS } from './types.js';
11
+ export function createCheck(config = {}) {
12
+ const mergedConfig = toMerged(DEFAULT_CONFIG, config);
13
+ return {
14
+ id: EXTENSION_ID,
15
+ slots: EXTENSION_SLOTS,
16
+ createInstance(options) {
17
+ return createCheckInstance(options, mergedConfig);
18
+ }
19
+ };
20
+ }
21
+ function createCheckInternal(options, config) {
22
+ return {
23
+ ...extensionCreateInternalBase(options),
24
+ square: null,
25
+ svgRect: null,
26
+ svgGradient: null,
27
+ runtimeSurface: options.runtimeSurface,
28
+ config
29
+ };
30
+ }
31
+ function extensionClean(state) {
32
+ state.svgRect = null;
33
+ state.svgGradient = null;
34
+ }
35
+ function createCheckInstancePublic(state) {
36
+ return {
37
+ get square() {
38
+ return state.square === null ? null : denormalizeSquare(state.square);
39
+ },
40
+ set square(value) {
41
+ const next = value === null ? null : normalizeSquare(assertSquareString(value));
42
+ if (next === state.square) {
43
+ return; // no-op: same square (or both null)
44
+ }
45
+ state.square = next;
46
+ markHighlightDirtyAndRequestRender(state);
47
+ }
48
+ };
49
+ }
50
+ function assertSquareString(value) {
51
+ if (!isSquareString(value)) {
52
+ throw new TypeError(`Invalid check square: ${String(value)}`);
53
+ }
54
+ return value;
55
+ }
56
+ function removeHighlight(state) {
57
+ if (state.svgRect !== null) {
58
+ state.svgRect.remove();
59
+ state.svgRect = null;
60
+ assert(state.svgGradient, 'svgGradient should be available if svgRect is available');
61
+ state.svgGradient.remove();
62
+ state.svgGradient = null;
63
+ }
64
+ }
65
+ function gradientId(state) {
66
+ return state.svgIds.makeId(EXTENSION_ID, 'highlight-gradient');
67
+ }
68
+ function createGradient(state) {
69
+ assert(state.slotRoots, 'Slot roots should be available when render is called');
70
+ const gradient = createSvgElement(state.slotRoots.defs, 'radialGradient', {
71
+ 'data-chessboard-id': 'check-highlight-gradient',
72
+ id: gradientId(state),
73
+ cx: '0.5',
74
+ cy: '0.5',
75
+ r: '0.707'
76
+ });
77
+ for (const stop of GRADIENT_STOPS) {
78
+ createSvgElement(gradient, 'stop', {
79
+ 'data-chessboard-id': `check-highlight-gradient-stop-${stop.offset}`,
80
+ offset: stop.offset,
81
+ 'stop-color': state.config.color,
82
+ 'stop-opacity': (Number(state.config.opacity) * stop.opacityRatio).toString()
83
+ });
84
+ }
85
+ return gradient;
86
+ }
87
+ function createCheckInstance(options, config) {
88
+ const internalState = createCheckInternal(options, config);
89
+ const publicInterface = createCheckInstancePublic(internalState);
90
+ return {
91
+ id: EXTENSION_ID,
92
+ mount(env) {
93
+ extensionMountBase(internalState, env.slotRoots);
94
+ },
95
+ onUpdate(context) {
96
+ const needsRender = context.mutation.hasMutation({ causes: ['layout.refreshGeometry'] }) &&
97
+ isUpdateContextRenderable(context);
98
+ if (!needsRender) {
99
+ return; // no-op
100
+ }
101
+ context.invalidation.markDirty(1 /* DirtyLayer.Highlight */);
102
+ },
103
+ render(context) {
104
+ assert(context.invalidation.dirtyLayers !== 0, 'Render should only be called when there are dirty layers');
105
+ if (internalState.square === null) {
106
+ removeHighlight(internalState);
107
+ return;
108
+ }
109
+ const geometry = context.currentFrame.layout.geometry;
110
+ const rect = geometry.getSquareRect(internalState.square);
111
+ const rectAttributes = {
112
+ x: rect.x.toString(),
113
+ y: rect.y.toString(),
114
+ width: rect.width.toString(),
115
+ height: rect.height.toString(),
116
+ fill: `url(#${gradientId(internalState)})`
117
+ };
118
+ if (internalState.svgRect === null) {
119
+ // The gradient is static (config-driven) and created exactly once,
120
+ // alongside the rect. It is never updated on subsequent renders and is
121
+ // removed together with the rect so <defs> keeps no orphans.
122
+ internalState.svgGradient = createGradient(internalState);
123
+ assert(internalState.slotRoots, 'Slot roots should be available when render is called');
124
+ internalState.svgRect = createSvgElement(internalState.slotRoots.underPieces, 'rect', {
125
+ 'data-chessboard-id': 'check-square-highlight',
126
+ ...rectAttributes
127
+ });
128
+ }
129
+ else {
130
+ // Square changed or geometry refreshed: only the rect moves.
131
+ updateSvgElementAttributes(internalState.svgRect, rectAttributes);
132
+ }
133
+ },
134
+ unmount() {
135
+ extensionUnmountBase(internalState);
136
+ extensionClean(internalState);
137
+ },
138
+ destroy() {
139
+ extensionDestroyBase(internalState);
140
+ extensionClean(internalState);
141
+ },
142
+ getPublic() {
143
+ return publicInterface;
144
+ }
145
+ };
146
+ }
@@ -0,0 +1,2 @@
1
+ import type { CheckInstanceInternal } from './types.js';
2
+ export declare function markHighlightDirtyAndRequestRender(state: CheckInstanceInternal): void;
@@ -0,0 +1,4 @@
1
+ export function markHighlightDirtyAndRequestRender(state) {
2
+ state.runtimeSurface.invalidation.markDirty(1 /* DirtyLayer.Highlight */);
3
+ state.runtimeSurface.commands.requestRender({ state: true });
4
+ }
@@ -0,0 +1,40 @@
1
+ import { PartialDeep } from 'type-fest';
2
+ import type { Square } from '../../../state/board/types/internal.js';
3
+ import { ExtensionDefinition, ExtensionInstance } from '../../types/extension.js';
4
+ import { ExtensionInternalBase, OpaqueColor } from '../common/types.js';
5
+ import type { ExtensionRuntimeSurface } from '../../types/surface/main.js';
6
+ import type { SquareString } from '../../../state/board/types/input.js';
7
+ export declare const EXTENSION_SLOTS: readonly ["defs", "underPieces"];
8
+ export type ExtensionSlotsType = typeof EXTENSION_SLOTS;
9
+ export declare const EXTENSION_ID: "check";
10
+ export type CheckConfig = OpaqueColor;
11
+ export declare const DEFAULT_CONFIG: CheckConfig;
12
+ export declare const GRADIENT_STOPS: readonly [{
13
+ readonly offset: "0%";
14
+ readonly opacityRatio: 1;
15
+ }, {
16
+ readonly offset: "25%";
17
+ readonly opacityRatio: 1;
18
+ }, {
19
+ readonly offset: "89%";
20
+ readonly opacityRatio: 0;
21
+ }, {
22
+ readonly offset: "100%";
23
+ readonly opacityRatio: 0;
24
+ }];
25
+ export type CheckInitConfig = PartialDeep<CheckConfig>;
26
+ export type CheckPublic = {
27
+ square: SquareString | null;
28
+ };
29
+ export type CheckDefinition = ExtensionDefinition<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, CheckPublic>;
30
+ export type CheckInstance = ExtensionInstance<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, CheckPublic>;
31
+ export interface CheckInstanceInternal extends ExtensionInternalBase<ExtensionSlotsType> {
32
+ square: Square | null;
33
+ svgRect: SVGRectElement | null;
34
+ svgGradient: SVGRadialGradientElement | null;
35
+ readonly runtimeSurface: ExtensionRuntimeSurface;
36
+ readonly config: CheckConfig;
37
+ }
38
+ export declare const enum DirtyLayer {
39
+ Highlight = 1
40
+ }
@@ -0,0 +1,18 @@
1
+ export const EXTENSION_SLOTS = ['defs', 'underPieces'];
2
+ export const EXTENSION_ID = 'check';
3
+ // A recognizable check glow: a solid red core fading to transparent toward the
4
+ // square edges. The center color/opacity are configurable; the edge fade is
5
+ // fixed by GRADIENT_STOPS below.
6
+ export const DEFAULT_CONFIG = {
7
+ color: 'rgb(255, 0, 0)',
8
+ opacity: 1
9
+ };
10
+ // Radial gradient stops with fixed offsets. Each stop's stop-color comes from
11
+ // config.color; stop-opacity multiplies config.opacity by the ratio here so the
12
+ // glow is opaque at the center and transparent at the edges.
13
+ export const GRADIENT_STOPS = [
14
+ { offset: '0%', opacityRatio: 1 },
15
+ { offset: '25%', opacityRatio: 1 },
16
+ { offset: '89%', opacityRatio: 0 },
17
+ { offset: '100%', opacityRatio: 0 }
18
+ ];
@@ -2,6 +2,7 @@ import { createActiveTarget } from '../first-party/active-target/factory.js';
2
2
  import { createAnnotations } from '../first-party/annotations/factory.js';
3
3
  import { createAutoPromote } from '../first-party/auto-promote/factory.js';
4
4
  import { createBoardEvents } from '../first-party/board-events/factory.js';
5
+ import { createCheck } from '../first-party/check/factory.js';
5
6
  import { createLastMove } from '../first-party/last-move/factory.js';
6
7
  import { createLegalMoves } from '../first-party/legal-moves/factory.js';
7
8
  import { createMainRenderer } from '../first-party/main-renderer/factory.js';
@@ -19,10 +20,11 @@ export declare const builtInExtensionFactoryMap: {
19
20
  autoPromote: typeof createAutoPromote;
20
21
  promotion: typeof createPromotion;
21
22
  annotations: typeof createAnnotations;
23
+ check: typeof createCheck;
22
24
  };
23
25
  export type BuiltInExtensionId = keyof typeof builtInExtensionFactoryMap;
24
26
  export type BuiltInExtensionDefinitionMap = {
25
27
  [Id in BuiltInExtensionId]: ReturnType<(typeof builtInExtensionFactoryMap)[Id]>;
26
28
  };
27
- export declare const DefaultBuiltinChessboardExtensions: readonly ["renderer", "watermark", "selectedSquare", "lastMove", "activeTarget", "legalMoves", "events", "autoPromote", "promotion", "annotations"];
29
+ export declare const DefaultBuiltinChessboardExtensions: readonly ["renderer", "watermark", "selectedSquare", "lastMove", "activeTarget", "legalMoves", "events", "autoPromote", "promotion", "annotations", "check"];
28
30
  export type DefaultBuiltinChessboardExtensions = typeof DefaultBuiltinChessboardExtensions;
@@ -6,6 +6,8 @@ import { createAutoPromote } from '../first-party/auto-promote/factory.js';
6
6
  import { EXTENSION_ID as EXTENSION_ID_AUTO_PROMOTE } from '../first-party/auto-promote/types.js';
7
7
  import { createBoardEvents } from '../first-party/board-events/factory.js';
8
8
  import { EXTENSION_ID as EXTENSION_ID_BOARD_EVENTS } from '../first-party/board-events/types.js';
9
+ import { createCheck } from '../first-party/check/factory.js';
10
+ import { EXTENSION_ID as EXTENSION_ID_CHECK } from '../first-party/check/types.js';
9
11
  import { createLastMove } from '../first-party/last-move/factory.js';
10
12
  import { EXTENSION_ID as EXTENSION_ID_LAST_MOVE } from '../first-party/last-move/types.js';
11
13
  import { createLegalMoves } from '../first-party/legal-moves/factory.js';
@@ -28,7 +30,8 @@ export const builtInExtensionFactoryMap = {
28
30
  [EXTENSION_ID_BOARD_EVENTS]: createBoardEvents,
29
31
  [EXTENSION_ID_AUTO_PROMOTE]: createAutoPromote,
30
32
  [EXTENSION_ID_PROMOTION]: createPromotion,
31
- [EXTENSION_ID_ANNOTATIONS]: createAnnotations
33
+ [EXTENSION_ID_ANNOTATIONS]: createAnnotations,
34
+ [EXTENSION_ID_CHECK]: createCheck
32
35
  };
33
36
  export const DefaultBuiltinChessboardExtensions = [
34
37
  EXTENSION_ID_RENDERER,
@@ -40,5 +43,6 @@ export const DefaultBuiltinChessboardExtensions = [
40
43
  EXTENSION_ID_BOARD_EVENTS,
41
44
  EXTENSION_ID_AUTO_PROMOTE,
42
45
  EXTENSION_ID_PROMOTION,
43
- EXTENSION_ID_ANNOTATIONS
46
+ EXTENSION_ID_ANNOTATIONS,
47
+ EXTENSION_ID_CHECK
44
48
  ];
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { MainRendererConfigPublic } from './extensions/first-party/main-renderer/types/public.js';
2
2
  import { DefaultInteractionDesktopConfig, DefaultInteractionMobileConfig } from './state/interaction/config.js';
3
3
  import type { InteractionConfig } from './state/interaction/types/config.js';
4
- export type { ColorInput, MoveRequestInput, PiecePositionRecordString, PositionInput } from './state/board/types/input.js';
4
+ export type { ColorInput, MoveRequestInput, PiecePositionRecordString, PositionInput, SquareString } from './state/board/types/input.js';
5
5
  export type { MoveOutput } from './state/board/types/output.js';
6
6
  export type { MovabilityInput, MoveDestinationInput } from './state/interaction/types/input.js';
7
7
  export { createBoard } from './wrapper/factory.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mirasen/chessboard",
3
3
  "description": "Production-grade framework-agnostic TypeScript chessboard platform with real chess interaction built in.",
4
- "version": "1.3.4",
4
+ "version": "1.4.0",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Mirasen"
@@ -82,11 +82,11 @@
82
82
  "!benchmarks"
83
83
  ],
84
84
  "devDependencies": {
85
- "@changesets/cli": "^2.28.1",
85
+ "@changesets/cli": "^3.0.1",
86
86
  "@eslint/compat": "^2.0.2",
87
87
  "@eslint/js": "^10.0.1",
88
88
  "@types/node": "^26.0.1",
89
- "@vitest/coverage-istanbul": "^4.1.0",
89
+ "@vitest/coverage-v8": "^4.1.0",
90
90
  "chess.js": "^1.4.0",
91
91
  "eslint": "^10.0.2",
92
92
  "eslint-config-prettier": "^10.1.2",