@mirasen/chessboard 1.3.5 → 1.5.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,6 +1,25 @@
1
1
  # @mirasen/chessboard
2
2
 
3
- ## 1.3.5
3
+ ## 1.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5c12439: Extend the check extension's `square` input to accept a color (`ColorInput`) in addition to a `SquareString`. When a color is provided, the highlight is placed on that color's king square.
8
+
9
+ ## 1.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 63415ac: feat: add first-party `check` extension for king-in-check square highlighting
14
+
15
+ 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.
16
+
17
+ ```ts
18
+ board.extensions.check.square = 'e1'; // highlight
19
+ board.extensions.check.square = null; // clear
20
+ ```
21
+
22
+ 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 })`.
4
23
 
5
24
  ### Patch Changes
6
25
 
@@ -18,19 +37,23 @@
18
37
  - 4a6f1d3: dependabot: directory '/', update eslint
19
38
  - 4a6f1d3: dependabot: directory '/', update publint
20
39
  - 4a6f1d3: dependabot: directory '/', update svelte-check
21
- - 4a6f1d3: dependabot: directory '/', update typescript-eslint
22
- - 4a6f1d3: dependabot: directory '/', update vite
40
+ - 2c8eea7: dependabot: directory '/', update typescript-eslint
41
+ - 2c8eea7: dependabot: directory '/', update vite
23
42
  - 6bf7293: dependabot: directory '/', update es-toolkit
24
43
  - 6bf7293: dependabot: directory '/', update eslint-plugin-svelte
25
- - 6bf7293: dependabot: directory '/', update globals
26
- - 6bf7293: dependabot: directory '/', update svelte-check
44
+ - 2c8eea7: dependabot: directory '/', update globals
45
+ - 4a6f1d3: dependabot: directory '/', update svelte-check
27
46
  - 6bf7293: dependabot: directory '/', update svelte
28
- - ae1574c: dependabot: directory '/', update @sveltejs/kit
47
+ - 2c8eea7: dependabot: directory '/', update @sveltejs/kit
29
48
  - ae1574c: dependabot: directory '/', update @vitest/browser-playwright
30
- - ae1574c: dependabot: directory '/', update eslint
31
- - ae1574c: dependabot: directory '/', update publint
32
- - ae1574c: dependabot: directory '/', update svelte
33
- - ae1574c: dependabot: directory '/', update vite
49
+ - 4a6f1d3: dependabot: directory '/', update eslint
50
+ - 4a6f1d3: dependabot: directory '/', update publint
51
+ - 6bf7293: dependabot: directory '/', update svelte
52
+ - 2c8eea7: dependabot: directory '/', update vite
53
+ - dee1c04: dependabot: directory '/', update @changesets/cli from 2.31.1 to 3.0.1
54
+ - 4a6f1d3: dependabot: directory '/', update @types/node
55
+ - 4a6f1d3: dependabot: directory '/', update eslint
56
+ - 2c8eea7: dependabot: directory '/', update typescript-eslint
34
57
 
35
58
  ## 1.3.4
36
59
 
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,168 @@
1
+ import assert from '@ktarmyshov/assert';
2
+ import { toMerged } from 'es-toolkit';
3
+ import { createSvgElement, updateSvgElementAttributes } from '../../../render/svg/helpers.js';
4
+ import { isColorInput, isSquareString } from '../../../state/board/check.js';
5
+ import { normalizeColor, normalizeSquare } from '../../../state/board/normalize.js';
6
+ import { toPieceCode } from '../../../state/board/piece.js';
7
+ import { SQUARE_COUNT } from '../../../state/board/types/internal.js';
8
+ import { isUpdateContextRenderable } from '../../types/context/update.js';
9
+ import { extensionCreateInternalBase, extensionDestroyBase, extensionMountBase, extensionUnmountBase } from '../common/helpers.js';
10
+ import { markHighlightDirtyAndRequestRender } from './invalidation.js';
11
+ import { DEFAULT_CONFIG, EXTENSION_ID, EXTENSION_SLOTS, GRADIENT_STOPS } from './types.js';
12
+ export function createCheck(config = {}) {
13
+ const mergedConfig = toMerged(DEFAULT_CONFIG, config);
14
+ return {
15
+ id: EXTENSION_ID,
16
+ slots: EXTENSION_SLOTS,
17
+ createInstance(options) {
18
+ return createCheckInstance(options, mergedConfig);
19
+ }
20
+ };
21
+ }
22
+ function createCheckInternal(options, config) {
23
+ return {
24
+ ...extensionCreateInternalBase(options),
25
+ square: null,
26
+ inputSquare: null,
27
+ svgRect: null,
28
+ svgGradient: null,
29
+ runtimeSurface: options.runtimeSurface,
30
+ config
31
+ };
32
+ }
33
+ function extensionClean(state) {
34
+ state.svgRect = null;
35
+ state.svgGradient = null;
36
+ }
37
+ function createCheckInstancePublic(state) {
38
+ return {
39
+ get square() {
40
+ return state.inputSquare;
41
+ },
42
+ set square(value) {
43
+ let next;
44
+ if (value === null) {
45
+ next = null;
46
+ }
47
+ else if (isSquareString(value)) {
48
+ next = normalizeSquare(value);
49
+ }
50
+ else if (isColorInput(value)) {
51
+ next = findKing(state, value);
52
+ }
53
+ else {
54
+ throw new TypeError(`Invalid check square: ${String(value)}`);
55
+ }
56
+ state.inputSquare = value;
57
+ if (next === state.square) {
58
+ return; // no-op: same square (or both null)
59
+ }
60
+ state.square = next;
61
+ markHighlightDirtyAndRequestRender(state);
62
+ }
63
+ };
64
+ }
65
+ function findKing(state, color) {
66
+ const snapshot = state.runtimeSurface.commands.getSnapshot();
67
+ const pieces = snapshot.state.board.pieces;
68
+ const colorCode = normalizeColor(color);
69
+ const kingPieceCode = toPieceCode(6 /* RoleCode.King */, colorCode);
70
+ for (let sq = 0; sq < SQUARE_COUNT; sq++) {
71
+ const piece = pieces[sq];
72
+ if (piece === kingPieceCode) {
73
+ return sq;
74
+ }
75
+ }
76
+ throw new Error("King of color '" + color + "' not found");
77
+ }
78
+ function removeHighlight(state) {
79
+ if (state.svgRect !== null) {
80
+ state.svgRect.remove();
81
+ state.svgRect = null;
82
+ assert(state.svgGradient, 'svgGradient should be available if svgRect is available');
83
+ state.svgGradient.remove();
84
+ state.svgGradient = null;
85
+ }
86
+ }
87
+ function gradientId(state) {
88
+ return state.svgIds.makeId(EXTENSION_ID, 'highlight-gradient');
89
+ }
90
+ function createGradient(state) {
91
+ assert(state.slotRoots, 'Slot roots should be available when render is called');
92
+ const gradient = createSvgElement(state.slotRoots.defs, 'radialGradient', {
93
+ 'data-chessboard-id': 'check-highlight-gradient',
94
+ id: gradientId(state),
95
+ cx: '0.5',
96
+ cy: '0.5',
97
+ r: '0.707'
98
+ });
99
+ for (const stop of GRADIENT_STOPS) {
100
+ createSvgElement(gradient, 'stop', {
101
+ 'data-chessboard-id': `check-highlight-gradient-stop-${stop.offset}`,
102
+ offset: stop.offset,
103
+ 'stop-color': state.config.color,
104
+ 'stop-opacity': (Number(state.config.opacity) * stop.opacityRatio).toString()
105
+ });
106
+ }
107
+ return gradient;
108
+ }
109
+ function createCheckInstance(options, config) {
110
+ const internalState = createCheckInternal(options, config);
111
+ const publicInterface = createCheckInstancePublic(internalState);
112
+ return {
113
+ id: EXTENSION_ID,
114
+ mount(env) {
115
+ extensionMountBase(internalState, env.slotRoots);
116
+ },
117
+ onUpdate(context) {
118
+ const needsRender = context.mutation.hasMutation({ causes: ['layout.refreshGeometry'] }) &&
119
+ isUpdateContextRenderable(context);
120
+ if (!needsRender) {
121
+ return; // no-op
122
+ }
123
+ context.invalidation.markDirty(1 /* DirtyLayer.Highlight */);
124
+ },
125
+ render(context) {
126
+ assert(context.invalidation.dirtyLayers !== 0, 'Render should only be called when there are dirty layers');
127
+ if (internalState.square === null) {
128
+ removeHighlight(internalState);
129
+ return;
130
+ }
131
+ const geometry = context.currentFrame.layout.geometry;
132
+ const rect = geometry.getSquareRect(internalState.square);
133
+ const rectAttributes = {
134
+ x: rect.x.toString(),
135
+ y: rect.y.toString(),
136
+ width: rect.width.toString(),
137
+ height: rect.height.toString(),
138
+ fill: `url(#${gradientId(internalState)})`
139
+ };
140
+ if (internalState.svgRect === null) {
141
+ // The gradient is static (config-driven) and created exactly once,
142
+ // alongside the rect. It is never updated on subsequent renders and is
143
+ // removed together with the rect so <defs> keeps no orphans.
144
+ internalState.svgGradient = createGradient(internalState);
145
+ assert(internalState.slotRoots, 'Slot roots should be available when render is called');
146
+ internalState.svgRect = createSvgElement(internalState.slotRoots.underPieces, 'rect', {
147
+ 'data-chessboard-id': 'check-square-highlight',
148
+ ...rectAttributes
149
+ });
150
+ }
151
+ else {
152
+ // Square changed or geometry refreshed: only the rect moves.
153
+ updateSvgElementAttributes(internalState.svgRect, rectAttributes);
154
+ }
155
+ },
156
+ unmount() {
157
+ extensionUnmountBase(internalState);
158
+ extensionClean(internalState);
159
+ },
160
+ destroy() {
161
+ extensionDestroyBase(internalState);
162
+ extensionClean(internalState);
163
+ },
164
+ getPublic() {
165
+ return publicInterface;
166
+ }
167
+ };
168
+ }
@@ -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,41 @@
1
+ import { PartialDeep } from 'type-fest';
2
+ import type { ColorInput, SquareString } from '../../../state/board/types/input.js';
3
+ import type { Square } from '../../../state/board/types/internal.js';
4
+ import { ExtensionDefinition, ExtensionInstance } from '../../types/extension.js';
5
+ import type { ExtensionRuntimeSurface } from '../../types/surface/main.js';
6
+ import { ExtensionInternalBase, OpaqueColor } from '../common/types.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 | ColorInput | 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
+ inputSquare: SquareString | ColorInput | null;
34
+ svgRect: SVGRectElement | null;
35
+ svgGradient: SVGRadialGradientElement | null;
36
+ readonly runtimeSurface: ExtensionRuntimeSurface;
37
+ readonly config: CheckConfig;
38
+ }
39
+ export declare const enum DirtyLayer {
40
+ Highlight = 1
41
+ }
@@ -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';
@@ -1,4 +1,4 @@
1
- import { Color, ColorShort, MoveRequestInput, Piece, PiecePositionInput, PiecePositionRecord, PiecePositionRecordShort, PiecePositionRecordString, PieceShort, PieceString, Role, RoleShort, SquareString } from './types/input.js';
1
+ import { Color, ColorInput, ColorShort, MoveRequestInput, Piece, PiecePositionInput, PiecePositionRecord, PiecePositionRecordShort, PiecePositionRecordString, PieceShort, PieceString, Role, RoleShort, SquareString } from './types/input.js';
2
2
  import { EmptyPieceCode, MoveRequest, NonEmptyPieceCode, PieceCode, PiecePositionSnapshot, PositionSnapshot, RolePromotionCode, Square, SquareFile, SquareRank } from './types/internal.js';
3
3
  import type { BoardStateSnapshot } from './types/main.js';
4
4
  export declare function isWhitePieceCode(code: NonEmptyPieceCode): boolean;
@@ -17,6 +17,7 @@ export declare function isPieceString(value: unknown): value is PieceString;
17
17
  export declare function isSquareString(value: unknown): value is SquareString;
18
18
  export declare function isPositionMapString(pos: PiecePositionInput): pos is PiecePositionRecordString;
19
19
  export declare function isColorShort(value: unknown): value is ColorShort;
20
+ export declare function isColorInput(value: unknown): value is ColorInput;
20
21
  export declare function isRoleShort(value: unknown): value is RoleShort;
21
22
  export declare function isPieceShort(value: unknown): value is PieceShort;
22
23
  export declare function isPositionMapShort(pos: PiecePositionInput): pos is PiecePositionRecordShort;
@@ -74,6 +74,9 @@ export function isPositionMapString(pos) {
74
74
  export function isColorShort(value) {
75
75
  return value === 'w' || value === 'b';
76
76
  }
77
+ export function isColorInput(value) {
78
+ return isColor(value) || isColorShort(value);
79
+ }
77
80
  export function isRoleShort(value) {
78
81
  return (value === 'K' ||
79
82
  value === 'Q' ||
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.5",
4
+ "version": "1.5.0",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Mirasen"
@@ -82,7 +82,7 @@
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",