gigaboard 1.2.3

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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -0
  3. package/dist/board-model-BvJOtwQ3.d.cts +675 -0
  4. package/dist/board-model-BvJOtwQ3.d.ts +675 -0
  5. package/dist/canvas.cjs +8 -0
  6. package/dist/canvas.cjs.map +1 -0
  7. package/dist/canvas.d.cts +14 -0
  8. package/dist/canvas.d.ts +14 -0
  9. package/dist/canvas.js +6 -0
  10. package/dist/canvas.js.map +1 -0
  11. package/dist/chunk-3HNIF4ZE.js +3 -0
  12. package/dist/chunk-3HNIF4ZE.js.map +1 -0
  13. package/dist/chunk-44YKPXXT.js +63 -0
  14. package/dist/chunk-44YKPXXT.js.map +1 -0
  15. package/dist/chunk-5WAOEWKS.js +16 -0
  16. package/dist/chunk-5WAOEWKS.js.map +1 -0
  17. package/dist/chunk-AYVQCHSG.cjs +10 -0
  18. package/dist/chunk-AYVQCHSG.cjs.map +1 -0
  19. package/dist/chunk-B26QLI2K.cjs +18 -0
  20. package/dist/chunk-B26QLI2K.cjs.map +1 -0
  21. package/dist/chunk-B2TBDDJN.cjs +10 -0
  22. package/dist/chunk-B2TBDDJN.cjs.map +1 -0
  23. package/dist/chunk-BFN5JI3T.js +8 -0
  24. package/dist/chunk-BFN5JI3T.js.map +1 -0
  25. package/dist/chunk-BWNBPN3P.cjs +10 -0
  26. package/dist/chunk-BWNBPN3P.cjs.map +1 -0
  27. package/dist/chunk-CXDGJMYL.js +105 -0
  28. package/dist/chunk-CXDGJMYL.js.map +1 -0
  29. package/dist/chunk-EOSX5UX3.js +8 -0
  30. package/dist/chunk-EOSX5UX3.js.map +1 -0
  31. package/dist/chunk-FNRS6YJI.cjs +67 -0
  32. package/dist/chunk-FNRS6YJI.cjs.map +1 -0
  33. package/dist/chunk-G6ZFOJZR.cjs +18 -0
  34. package/dist/chunk-G6ZFOJZR.cjs.map +1 -0
  35. package/dist/chunk-HSMILJKQ.cjs +18 -0
  36. package/dist/chunk-HSMILJKQ.cjs.map +1 -0
  37. package/dist/chunk-I4BQGFP5.cjs +113 -0
  38. package/dist/chunk-I4BQGFP5.cjs.map +1 -0
  39. package/dist/chunk-J6GGNP5S.js +1104 -0
  40. package/dist/chunk-J6GGNP5S.js.map +1 -0
  41. package/dist/chunk-JQSBAWIZ.cjs +4 -0
  42. package/dist/chunk-JQSBAWIZ.cjs.map +1 -0
  43. package/dist/chunk-KW56FEBY.js +8 -0
  44. package/dist/chunk-KW56FEBY.js.map +1 -0
  45. package/dist/chunk-N7ED2MZS.cjs +10 -0
  46. package/dist/chunk-N7ED2MZS.cjs.map +1 -0
  47. package/dist/chunk-NAA5O4BT.js +16 -0
  48. package/dist/chunk-NAA5O4BT.js.map +1 -0
  49. package/dist/chunk-OQLY5GJF.js +16 -0
  50. package/dist/chunk-OQLY5GJF.js.map +1 -0
  51. package/dist/chunk-P72B3YTG.js +16 -0
  52. package/dist/chunk-P72B3YTG.js.map +1 -0
  53. package/dist/chunk-U6WAD322.js +8 -0
  54. package/dist/chunk-U6WAD322.js.map +1 -0
  55. package/dist/chunk-UUTEFNFO.cjs +1148 -0
  56. package/dist/chunk-UUTEFNFO.cjs.map +1 -0
  57. package/dist/chunk-VYSVIIZA.cjs +18 -0
  58. package/dist/chunk-VYSVIIZA.cjs.map +1 -0
  59. package/dist/core.cjs +180 -0
  60. package/dist/core.cjs.map +1 -0
  61. package/dist/core.d.cts +164 -0
  62. package/dist/core.d.ts +164 -0
  63. package/dist/core.js +3 -0
  64. package/dist/core.js.map +1 -0
  65. package/dist/create-image-piece-set-BcmRKGql.d.cts +100 -0
  66. package/dist/create-image-piece-set-BcmRKGql.d.ts +100 -0
  67. package/dist/gigachess-3BDD--yT.d.cts +22 -0
  68. package/dist/gigachess-DolQlR82.d.ts +22 -0
  69. package/dist/index.cjs +2972 -0
  70. package/dist/index.cjs.map +1 -0
  71. package/dist/index.d.cts +435 -0
  72. package/dist/index.d.ts +435 -0
  73. package/dist/index.js +2898 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/pieces/alpha.cjs +13 -0
  76. package/dist/pieces/alpha.cjs.map +1 -0
  77. package/dist/pieces/alpha.d.cts +17 -0
  78. package/dist/pieces/alpha.d.ts +17 -0
  79. package/dist/pieces/alpha.js +4 -0
  80. package/dist/pieces/alpha.js.map +1 -0
  81. package/dist/pieces/cburnett.cjs +13 -0
  82. package/dist/pieces/cburnett.cjs.map +1 -0
  83. package/dist/pieces/cburnett.d.cts +19 -0
  84. package/dist/pieces/cburnett.d.ts +19 -0
  85. package/dist/pieces/cburnett.js +4 -0
  86. package/dist/pieces/cburnett.js.map +1 -0
  87. package/dist/pieces/chesscom.cjs +14 -0
  88. package/dist/pieces/chesscom.cjs.map +1 -0
  89. package/dist/pieces/chesscom.d.cts +3 -0
  90. package/dist/pieces/chesscom.d.ts +3 -0
  91. package/dist/pieces/chesscom.js +5 -0
  92. package/dist/pieces/chesscom.js.map +1 -0
  93. package/dist/pieces/merida.cjs +13 -0
  94. package/dist/pieces/merida.cjs.map +1 -0
  95. package/dist/pieces/merida.d.cts +17 -0
  96. package/dist/pieces/merida.d.ts +17 -0
  97. package/dist/pieces/merida.js +4 -0
  98. package/dist/pieces/merida.js.map +1 -0
  99. package/dist/pieces/neo.cjs +13 -0
  100. package/dist/pieces/neo.cjs.map +1 -0
  101. package/dist/pieces/neo.d.cts +18 -0
  102. package/dist/pieces/neo.d.ts +18 -0
  103. package/dist/pieces/neo.js +4 -0
  104. package/dist/pieces/neo.js.map +1 -0
  105. package/dist/pieces.cjs +55 -0
  106. package/dist/pieces.cjs.map +1 -0
  107. package/dist/pieces.d.cts +21 -0
  108. package/dist/pieces.d.ts +21 -0
  109. package/dist/pieces.js +24 -0
  110. package/dist/pieces.js.map +1 -0
  111. package/dist/server.cjs +207 -0
  112. package/dist/server.cjs.map +1 -0
  113. package/dist/server.d.cts +60 -0
  114. package/dist/server.d.ts +60 -0
  115. package/dist/server.js +205 -0
  116. package/dist/server.js.map +1 -0
  117. package/dist/sounds/capture.mp3 +0 -0
  118. package/dist/sounds/castle.mp3 +0 -0
  119. package/dist/sounds/game-end.mp3 +0 -0
  120. package/dist/sounds/move-check.mp3 +0 -0
  121. package/dist/sounds/move-opponent.mp3 +0 -0
  122. package/dist/sounds/move-self.mp3 +0 -0
  123. package/dist/sounds/promote.mp3 +0 -0
  124. package/dist/themes/blue.cjs +12 -0
  125. package/dist/themes/blue.cjs.map +1 -0
  126. package/dist/themes/blue.d.cts +102 -0
  127. package/dist/themes/blue.d.ts +102 -0
  128. package/dist/themes/blue.js +3 -0
  129. package/dist/themes/blue.js.map +1 -0
  130. package/dist/themes/brown.cjs +12 -0
  131. package/dist/themes/brown.cjs.map +1 -0
  132. package/dist/themes/brown.d.cts +1 -0
  133. package/dist/themes/brown.d.ts +1 -0
  134. package/dist/themes/brown.js +3 -0
  135. package/dist/themes/brown.js.map +1 -0
  136. package/dist/themes/green.cjs +12 -0
  137. package/dist/themes/green.cjs.map +1 -0
  138. package/dist/themes/green.d.cts +1 -0
  139. package/dist/themes/green.d.ts +1 -0
  140. package/dist/themes/green.js +3 -0
  141. package/dist/themes/green.js.map +1 -0
  142. package/dist/themes/wood.cjs +12 -0
  143. package/dist/themes/wood.cjs.map +1 -0
  144. package/dist/themes/wood.d.cts +1 -0
  145. package/dist/themes/wood.d.ts +1 -0
  146. package/dist/themes/wood.js +3 -0
  147. package/dist/themes/wood.js.map +1 -0
  148. package/dist/themes.cjs +36 -0
  149. package/dist/themes.cjs.map +1 -0
  150. package/dist/themes.d.cts +1 -0
  151. package/dist/themes.d.ts +1 -0
  152. package/dist/themes.js +21 -0
  153. package/dist/themes.js.map +1 -0
  154. package/dist/types-Bg2xiUHE.d.cts +501 -0
  155. package/dist/types-D5eBJGzj.d.ts +501 -0
  156. package/package.json +210 -0
package/dist/index.js ADDED
@@ -0,0 +1,2898 @@
1
+ import { PieceType, Color, createDragController, createGigachessAdapter, createBoardModel, isEmptyCell, pieceTypeOf, colorOf, encodeBoardCell, decodePackedMove } from './chunk-J6GGNP5S.js';
2
+ export { Color, PieceType, createBoardModel, createGigachessAdapter, packMove, preloadGigachessAdapter, unpackMove } from './chunk-J6GGNP5S.js';
3
+ import { CSS_VARS, defaultTheme, defaultPieces, parseFenPlacement, defaultArrowColors, DEFAULT_ILLEGAL_FLASH_TINT, DEFAULT_PREMOVE_TINT } from './chunk-CXDGJMYL.js';
4
+ export { CSS_VARS, DEFAULT_ILLEGAL_FLASH_TINT, DEFAULT_PREMOVE_TINT, defaultArrowColors, defaultPieces, defaultTheme } from './chunk-CXDGJMYL.js';
5
+ import './chunk-OQLY5GJF.js';
6
+ import './chunk-U6WAD322.js';
7
+ import './chunk-44YKPXXT.js';
8
+ import { memo, forwardRef, useRef, useMemo, useImperativeHandle, useLayoutEffect, useEffect, useCallback, useSyncExternalStore, useState } from 'react';
9
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
10
+
11
+ function useBoardSnapshot(model) {
12
+ return useSyncExternalStore(model.subscribe, model.getSnapshot, model.getSnapshot);
13
+ }
14
+ function useBoardSlice(model, selector) {
15
+ const getSnapshot = useCallback(() => selector(model.getSnapshot()), [model, selector]);
16
+ return useSyncExternalStore(model.subscribe, getSnapshot, getSnapshot);
17
+ }
18
+ function useSquareCell(model, index) {
19
+ const subscribe = useCallback(
20
+ (listener) => model ? model.subscribeSquare(index, listener) : () => {
21
+ },
22
+ [model, index]
23
+ );
24
+ const getSnapshot = useCallback(() => {
25
+ if (!model) return 0;
26
+ const snap = model.getSnapshot();
27
+ return snap.board[index] ?? 0;
28
+ }, [model, index]);
29
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
30
+ }
31
+ function squareCentrePct(index, orientation) {
32
+ const file = index & 7;
33
+ const rank = index >> 3;
34
+ const col = orientation === "white" ? file : 7 - file;
35
+ const row = orientation === "white" ? 7 - rank : rank;
36
+ return { cx: col * 12.5 + 6.25, cy: row * 12.5 + 6.25 };
37
+ }
38
+ function isKnightMove(fromIndex, toIndex) {
39
+ const fileDelta = Math.abs((toIndex & 7) - (fromIndex & 7));
40
+ const rankDelta = Math.abs((toIndex >> 3) - (fromIndex >> 3));
41
+ return fileDelta === 1 && rankDelta === 2 || fileDelta === 2 && rankDelta === 1;
42
+ }
43
+ function unitVector(from, to) {
44
+ const dx = to.x - from.x;
45
+ const dy = to.y - from.y;
46
+ const len = Math.hypot(dx, dy);
47
+ if (len === 0) return null;
48
+ return { x: dx / len, y: dy / len };
49
+ }
50
+ function arrowPath(fromIndex, toIndex, start, end, startOffset, tipOffset, headLength) {
51
+ if (!isKnightMove(fromIndex, toIndex)) {
52
+ const unit = unitVector(start, end);
53
+ if (unit === null) return null;
54
+ const tip2 = { x: end.x - unit.x * tipOffset, y: end.y - unit.y * tipOffset };
55
+ return {
56
+ start: { x: start.x + unit.x * startOffset, y: start.y + unit.y * startOffset },
57
+ corner: null,
58
+ shaftEnd: { x: tip2.x - unit.x * headLength, y: tip2.y - unit.y * headLength },
59
+ tip: tip2,
60
+ finalUnit: unit
61
+ };
62
+ }
63
+ const dx = end.x - start.x;
64
+ const dy = end.y - start.y;
65
+ const corner = Math.abs(dx) > Math.abs(dy) ? { x: end.x, y: start.y } : { x: start.x, y: end.y };
66
+ const firstUnit = unitVector(start, corner);
67
+ const finalUnit = unitVector(corner, end);
68
+ if (firstUnit === null || finalUnit === null) return null;
69
+ const tip = { x: end.x - finalUnit.x * tipOffset, y: end.y - finalUnit.y * tipOffset };
70
+ return {
71
+ start: { x: start.x + firstUnit.x * startOffset, y: start.y + firstUnit.y * startOffset },
72
+ corner,
73
+ shaftEnd: { x: tip.x - finalUnit.x * headLength, y: tip.y - finalUnit.y * headLength },
74
+ tip,
75
+ finalUnit
76
+ };
77
+ }
78
+ function resolveColor(arrow, palette) {
79
+ if (arrow.brush !== void 0 && palette !== void 0) {
80
+ const brushed = palette[arrow.brush];
81
+ if (brushed !== void 0) return brushed;
82
+ }
83
+ return arrow.color;
84
+ }
85
+ function drawArrow(ctx, fromIndex, toIndex, color, sqSize, orientation, preview) {
86
+ const a = squareCentrePct(fromIndex, orientation);
87
+ const b = squareCentrePct(toIndex, orientation);
88
+ const boardSize = sqSize * 8;
89
+ const start = { x: a.cx / 100 * boardSize, y: a.cy / 100 * boardSize };
90
+ const end = { x: b.cx / 100 * boardSize, y: b.cy / 100 * boardSize };
91
+ if (fromIndex === toIndex) {
92
+ ctx.strokeStyle = color;
93
+ ctx.lineWidth = sqSize * 0.08;
94
+ ctx.globalAlpha = preview ? 0.65 : 1;
95
+ ctx.beginPath();
96
+ ctx.arc(start.x, start.y, sqSize * 0.44, 0, Math.PI * 2);
97
+ ctx.stroke();
98
+ ctx.globalAlpha = 1;
99
+ return;
100
+ }
101
+ const startOffset = sqSize * 0.28;
102
+ const tipOffset = sqSize * 0.08;
103
+ const headLength = sqSize * 0.36;
104
+ const headHalfWidth = sqSize * 0.28;
105
+ const shaftWidth = sqSize * 0.18;
106
+ const path = arrowPath(fromIndex, toIndex, start, end, startOffset, tipOffset, headLength);
107
+ if (path === null) return;
108
+ ctx.globalAlpha = preview ? 0.65 : 1;
109
+ ctx.strokeStyle = color;
110
+ ctx.lineWidth = shaftWidth;
111
+ ctx.lineCap = "butt";
112
+ ctx.lineJoin = "round";
113
+ ctx.beginPath();
114
+ ctx.moveTo(path.start.x, path.start.y);
115
+ if (path.corner !== null) ctx.lineTo(path.corner.x, path.corner.y);
116
+ ctx.lineTo(path.shaftEnd.x, path.shaftEnd.y);
117
+ ctx.stroke();
118
+ const perpX = -path.finalUnit.y;
119
+ const perpY = path.finalUnit.x;
120
+ ctx.fillStyle = color;
121
+ ctx.beginPath();
122
+ ctx.moveTo(path.tip.x, path.tip.y);
123
+ ctx.lineTo(path.shaftEnd.x + perpX * headHalfWidth, path.shaftEnd.y + perpY * headHalfWidth);
124
+ ctx.lineTo(path.shaftEnd.x - perpX * headHalfWidth, path.shaftEnd.y - perpY * headHalfWidth);
125
+ ctx.closePath();
126
+ ctx.fill();
127
+ ctx.globalAlpha = 1;
128
+ }
129
+ function redraw(canvas, arrows, preview, orientation, palette) {
130
+ const ctx = canvas.getContext("2d");
131
+ if (ctx === null) return;
132
+ const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
133
+ const rect = canvas.getBoundingClientRect();
134
+ const cssWidth = rect.width;
135
+ const cssHeight = rect.height;
136
+ if (cssWidth <= 0 || cssHeight <= 0) return;
137
+ const backingWidth = Math.round(cssWidth * dpr);
138
+ const backingHeight = Math.round(cssHeight * dpr);
139
+ if (canvas.width !== backingWidth) canvas.width = backingWidth;
140
+ if (canvas.height !== backingHeight) canvas.height = backingHeight;
141
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
142
+ ctx.clearRect(0, 0, cssWidth, cssHeight);
143
+ const sqSize = cssWidth / 8;
144
+ for (const arrow of arrows) {
145
+ drawArrow(ctx, arrow.from, arrow.to, resolveColor(arrow, palette), sqSize, orientation, false);
146
+ }
147
+ if (preview !== null) {
148
+ drawArrow(ctx, preview.from, preview.to, preview.color, sqSize, orientation, true);
149
+ }
150
+ }
151
+ function anchorPct(from, to, orientation, center) {
152
+ const a = squareCentrePct(from, orientation);
153
+ const b = squareCentrePct(to, orientation);
154
+ if (center === "orig") return a;
155
+ if (center === "dest") return b;
156
+ return { cx: (a.cx + b.cx) / 2, cy: (a.cy + b.cy) / 2 };
157
+ }
158
+ var ArrowsLayer = memo(
159
+ forwardRef(function ArrowsLayer2({ model, orientation, palette, below = false }, handle) {
160
+ const canvasRef = useRef(null);
161
+ const previewRef = useRef(null);
162
+ const allArrows = useBoardSlice(model, (s) => s.arrows);
163
+ const arrows = useMemo(
164
+ () => allArrows.filter((a) => a.below === true === below),
165
+ [allArrows, below]
166
+ );
167
+ const hasDecorations = useMemo(
168
+ () => arrows.some((a) => a.label !== void 0 || a.customSvg !== void 0),
169
+ [arrows]
170
+ );
171
+ useImperativeHandle(
172
+ handle,
173
+ () => ({
174
+ setPreview(preview) {
175
+ if (below) return;
176
+ previewRef.current = preview;
177
+ const canvas = canvasRef.current;
178
+ if (canvas !== null) redraw(canvas, arrows, preview, orientation, palette);
179
+ }
180
+ }),
181
+ [arrows, orientation, palette, below]
182
+ );
183
+ useLayoutEffect(() => {
184
+ const canvas = canvasRef.current;
185
+ if (canvas === null) return;
186
+ redraw(canvas, arrows, previewRef.current, orientation, palette);
187
+ }, [arrows, orientation, palette]);
188
+ useEffect(() => {
189
+ const canvas = canvasRef.current;
190
+ if (canvas === null) return;
191
+ const parent = canvas.parentElement;
192
+ if (parent === null || typeof ResizeObserver === "undefined") return;
193
+ const observer = new ResizeObserver(() => {
194
+ redraw(canvas, arrows, previewRef.current, orientation, palette);
195
+ });
196
+ observer.observe(parent);
197
+ return () => observer.disconnect();
198
+ }, [arrows, orientation, palette]);
199
+ const layerZIndex = below ? void 0 : 15;
200
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
201
+ /* @__PURE__ */ jsx(
202
+ "canvas",
203
+ {
204
+ ref: canvasRef,
205
+ "data-layer": below ? "arrows-below" : "arrows",
206
+ style: {
207
+ position: "absolute",
208
+ inset: 0,
209
+ width: "100%",
210
+ height: "100%",
211
+ pointerEvents: "none",
212
+ ...layerZIndex !== void 0 ? { zIndex: layerZIndex } : {}
213
+ }
214
+ }
215
+ ),
216
+ hasDecorations ? /* @__PURE__ */ jsx(
217
+ "svg",
218
+ {
219
+ "data-layer": below ? "arrows-below-decor" : "arrows-decor",
220
+ viewBox: "0 0 100 100",
221
+ preserveAspectRatio: "none",
222
+ "aria-hidden": "true",
223
+ style: {
224
+ position: "absolute",
225
+ inset: 0,
226
+ width: "100%",
227
+ height: "100%",
228
+ pointerEvents: "none",
229
+ // Decorations share the z-plane of the canvas they annotate.
230
+ ...layerZIndex !== void 0 ? { zIndex: layerZIndex } : {}
231
+ },
232
+ children: arrows.map((arrow) => {
233
+ const key = `${arrow.from}-${arrow.to}-${arrow.color}-${arrow.brush ?? ""}`;
234
+ const color = resolveColor(arrow, palette);
235
+ const nodes = [];
236
+ if (arrow.label !== void 0) {
237
+ const { cx, cy } = anchorPct(arrow.from, arrow.to, orientation, "label");
238
+ nodes.push(
239
+ /* @__PURE__ */ jsx(
240
+ "text",
241
+ {
242
+ x: cx,
243
+ y: cy,
244
+ fill: arrow.label.fill ?? color,
245
+ fontSize: "4",
246
+ fontWeight: "700",
247
+ textAnchor: "middle",
248
+ dominantBaseline: "central",
249
+ style: { userSelect: "none" },
250
+ children: arrow.label.text
251
+ },
252
+ `${key}-label`
253
+ )
254
+ );
255
+ }
256
+ if (arrow.customSvg !== void 0) {
257
+ const { cx, cy } = anchorPct(
258
+ arrow.from,
259
+ arrow.to,
260
+ orientation,
261
+ arrow.customSvg.center ?? "dest"
262
+ );
263
+ nodes.push(
264
+ /* @__PURE__ */ jsx(
265
+ "g",
266
+ {
267
+ transform: `translate(${cx}, ${cy})`,
268
+ dangerouslySetInnerHTML: { __html: arrow.customSvg.html }
269
+ },
270
+ `${key}-svg`
271
+ )
272
+ );
273
+ }
274
+ return nodes.length === 0 ? null : /* @__PURE__ */ jsx("g", { children: nodes }, key);
275
+ })
276
+ }
277
+ ) : null
278
+ ] });
279
+ })
280
+ );
281
+ var FOCUS_STYLE_ID = "gb-focus-ring-styles";
282
+ function injectFocusRingStyles() {
283
+ if (typeof document === "undefined") return;
284
+ if (document.getElementById(FOCUS_STYLE_ID) !== null) return;
285
+ const style = document.createElement("style");
286
+ style.id = FOCUS_STYLE_ID;
287
+ style.textContent = `
288
+ [data-gb-square]:focus-visible {
289
+ box-shadow: inset 0 0 0 3px rgba(255, 206, 76, 0.95);
290
+ }
291
+ `;
292
+ document.head.appendChild(style);
293
+ }
294
+ function algebraicOf(index) {
295
+ const file = index & 7;
296
+ const rank = index >> 3;
297
+ return `${String.fromCharCode(97 + file)}${rank + 1}`;
298
+ }
299
+ function SquareImpl({
300
+ index,
301
+ onClick,
302
+ renderSquare,
303
+ isFocused,
304
+ setSquareRef,
305
+ model,
306
+ getSquareAriaLabel,
307
+ onFocus
308
+ }) {
309
+ const file = index & 7;
310
+ const rank = index >> 3;
311
+ const isLight = (file + rank) % 2 === 1;
312
+ const cell = useSquareCell(model, index);
313
+ const label = getSquareAriaLabel ? getSquareAriaLabel(index, cell) : algebraicOf(index);
314
+ const internalRef = useRef(null);
315
+ const lastReportedFocusRef = useRef(false);
316
+ const setRef = useCallback(
317
+ (el) => {
318
+ internalRef.current = el;
319
+ setSquareRef?.(index, el);
320
+ },
321
+ [index, setSquareRef]
322
+ );
323
+ useEffect(() => {
324
+ injectFocusRingStyles();
325
+ }, []);
326
+ useEffect(() => {
327
+ if (!isFocused) {
328
+ lastReportedFocusRef.current = false;
329
+ return;
330
+ }
331
+ const el = internalRef.current;
332
+ if (el === null) return;
333
+ const grid = el.closest('[role="grid"]');
334
+ if (grid === null) return;
335
+ if (!grid.contains(document.activeElement)) return;
336
+ if (document.activeElement !== el) el.focus();
337
+ if (!lastReportedFocusRef.current) {
338
+ lastReportedFocusRef.current = true;
339
+ onFocus?.(index, cell);
340
+ }
341
+ }, [isFocused, index, cell, onFocus]);
342
+ const handleFocus = useCallback(() => {
343
+ lastReportedFocusRef.current = true;
344
+ onFocus?.(index, cell);
345
+ }, [index, cell, onFocus]);
346
+ const handleClick = () => onClick(index);
347
+ return /* @__PURE__ */ jsx(
348
+ "div",
349
+ {
350
+ ref: setRef,
351
+ role: "gridcell",
352
+ "aria-label": label,
353
+ "aria-rowindex": rank + 1,
354
+ "aria-colindex": file + 1,
355
+ tabIndex: isFocused ? 0 : -1,
356
+ "data-square": algebraicOf(index),
357
+ "data-gb-square": "",
358
+ "data-light": isLight ? "true" : "false",
359
+ onClick: handleClick,
360
+ onFocus: handleFocus,
361
+ style: {
362
+ position: "relative",
363
+ // `backgroundColor` (not the `background` shorthand) so the
364
+ // CSS `background-image` set by `useSelectionController` can
365
+ // layer on top without clobbering this base colour.
366
+ backgroundColor: `var(${isLight ? CSS_VARS.SQ_LIGHT : CSS_VARS.SQ_DARK})`,
367
+ // Cursor is managed by `useCursorController` via injected CSS so
368
+ // it can flip between `pointer` / `grab` / `grabbing` without a
369
+ // React re-render per hover.
370
+ userSelect: "none",
371
+ // Suppress the browser's default focus ring; the keyboard focus
372
+ // indicator ships via `:focus-visible` (see `injectFocusRingStyles`).
373
+ outline: "none",
374
+ // Squares carry the container-query context so pieces inside can
375
+ // size relative to this cell (85cqh glyph sizing).
376
+ containerType: "size"
377
+ },
378
+ children: renderSquare !== void 0 ? renderSquare({ index, isLight, label }) : null
379
+ }
380
+ );
381
+ }
382
+ var Square = memo(SquareImpl);
383
+ Square.displayName = "Square";
384
+ function buildSquareOrder(orientation) {
385
+ const squares = [];
386
+ if (orientation === "white") {
387
+ for (let rank = 7; rank >= 0; rank--) {
388
+ for (let file = 0; file < 8; file++) {
389
+ squares.push(rank * 8 + file);
390
+ }
391
+ }
392
+ } else {
393
+ for (let rank = 0; rank < 8; rank++) {
394
+ for (let file = 7; file >= 0; file--) {
395
+ squares.push(rank * 8 + file);
396
+ }
397
+ }
398
+ }
399
+ return squares;
400
+ }
401
+ var BoardGrid = memo(function BoardGrid2({
402
+ orientation,
403
+ onSquareClick,
404
+ renderSquare,
405
+ ariaLabel,
406
+ focusedSquare,
407
+ setSquareRef,
408
+ readOnly,
409
+ model,
410
+ getSquareAriaLabel,
411
+ onSquareFocus
412
+ }) {
413
+ const squares = useMemo(() => buildSquareOrder(orientation), [orientation]);
414
+ return /* @__PURE__ */ jsx(
415
+ "div",
416
+ {
417
+ role: "grid",
418
+ "aria-label": ariaLabel ?? "Chess board",
419
+ "aria-rowcount": 8,
420
+ "aria-colcount": 8,
421
+ ...readOnly === true ? { "aria-readonly": true } : {},
422
+ style: {
423
+ position: "absolute",
424
+ inset: 0,
425
+ display: "grid",
426
+ gridTemplateColumns: "repeat(8, 1fr)",
427
+ gridTemplateRows: "repeat(8, 1fr)"
428
+ },
429
+ children: squares.map((index) => /* @__PURE__ */ jsx(
430
+ Square,
431
+ {
432
+ index,
433
+ onClick: onSquareClick,
434
+ isFocused: focusedSquare === index,
435
+ model,
436
+ getSquareAriaLabel,
437
+ onFocus: onSquareFocus,
438
+ ...renderSquare !== void 0 ? { renderSquare } : {},
439
+ ...setSquareRef !== void 0 ? { setSquareRef } : {}
440
+ },
441
+ index
442
+ ))
443
+ }
444
+ );
445
+ });
446
+ var WHITE_KING = 6;
447
+ var BLACK_KING = 12;
448
+ function findKing(board, turn) {
449
+ const code = turn === 0 ? WHITE_KING : BLACK_KING;
450
+ for (let i = 0; i < 64; i++) {
451
+ if (board[i] === code) return i;
452
+ }
453
+ return null;
454
+ }
455
+ function positionOf(index, orientation) {
456
+ const file = index & 7;
457
+ const rank = index >> 3;
458
+ const col = orientation === "white" ? file : 7 - file;
459
+ const row = orientation === "white" ? 7 - rank : rank;
460
+ return { x: col * 12.5, y: row * 12.5 };
461
+ }
462
+ function CheckLayer({ model, orientation }) {
463
+ const inCheck = useBoardSlice(model, (s) => s.inCheck);
464
+ if (!inCheck) return null;
465
+ return /* @__PURE__ */ jsx(CheckLayerInner, { model, orientation });
466
+ }
467
+ function CheckLayerInner({ model, orientation }) {
468
+ const turn = useBoardSlice(model, (s) => s.turn);
469
+ const board = useBoardSlice(model, (s) => s.board);
470
+ const kingSquare = findKing(board, turn);
471
+ if (kingSquare === null) return null;
472
+ const { x, y } = positionOf(kingSquare, orientation);
473
+ return /* @__PURE__ */ jsx(
474
+ "div",
475
+ {
476
+ "data-layer": "check",
477
+ "aria-hidden": "true",
478
+ style: {
479
+ position: "absolute",
480
+ left: `${x}%`,
481
+ top: `${y}%`,
482
+ width: "12.5%",
483
+ height: "12.5%",
484
+ background: `var(${CSS_VARS.CHECK})`,
485
+ pointerEvents: "none",
486
+ zIndex: 2
487
+ }
488
+ }
489
+ );
490
+ }
491
+ var FILE_LETTERS = ["a", "b", "c", "d", "e", "f", "g", "h"];
492
+ function Coordinates({ orientation, ranksPosition = "left" }) {
493
+ const ranksOnRight = ranksPosition === "right";
494
+ return /* @__PURE__ */ jsxs(
495
+ "div",
496
+ {
497
+ "aria-hidden": "true",
498
+ style: {
499
+ position: "absolute",
500
+ inset: 0,
501
+ pointerEvents: "none",
502
+ fontSize: "min(1.6vmin, 11px)",
503
+ fontWeight: 600,
504
+ fontFamily: "system-ui, -apple-system, sans-serif"
505
+ },
506
+ children: [
507
+ FILE_LETTERS.map((letter, idx) => {
508
+ const col = orientation === "white" ? idx : 7 - idx;
509
+ const bottomRank = orientation === "white" ? 0 : 7;
510
+ const isLightSquare = (col + bottomRank) % 2 === 1;
511
+ return /* @__PURE__ */ jsx(
512
+ "span",
513
+ {
514
+ style: {
515
+ position: "absolute",
516
+ left: `${col * 12.5 + 10}%`,
517
+ bottom: "1%",
518
+ color: `var(${isLightSquare ? CSS_VARS.COORDINATE_LIGHT : CSS_VARS.COORDINATE_DARK})`
519
+ },
520
+ children: letter
521
+ },
522
+ `file-${letter}`
523
+ );
524
+ }),
525
+ FILE_LETTERS.map((_, idx) => {
526
+ const rank = idx;
527
+ const row = orientation === "white" ? 7 - rank : rank;
528
+ const edgeFile = ranksOnRight ? orientation === "white" ? 7 : 0 : orientation === "white" ? 0 : 7;
529
+ const isLightSquare = (edgeFile + rank) % 2 === 1;
530
+ const sideStyle = ranksOnRight ? { right: "1%" } : { left: "1%" };
531
+ return /* @__PURE__ */ jsx(
532
+ "span",
533
+ {
534
+ style: {
535
+ position: "absolute",
536
+ top: `${row * 12.5 + 1}%`,
537
+ ...sideStyle,
538
+ color: `var(${isLightSquare ? CSS_VARS.COORDINATE_LIGHT : CSS_VARS.COORDINATE_DARK})`
539
+ },
540
+ children: rank + 1
541
+ },
542
+ `rank-${rank}`
543
+ );
544
+ })
545
+ ]
546
+ }
547
+ );
548
+ }
549
+ var WRAPPER_STYLE = Object.freeze({
550
+ position: "absolute",
551
+ top: 0,
552
+ left: 0,
553
+ width: "12.5%",
554
+ height: "12.5%",
555
+ pointerEvents: "none",
556
+ willChange: "transform",
557
+ zIndex: 20,
558
+ containerType: "size",
559
+ opacity: 0,
560
+ transform: "translate3d(0, 0, 0)"
561
+ });
562
+ var SLOT_STYLE = Object.freeze({
563
+ position: "absolute",
564
+ inset: 0,
565
+ opacity: 0,
566
+ containerType: "size"
567
+ });
568
+ var DragLayer = forwardRef(function DragLayer2({ pieces }, ref) {
569
+ const wrapperRef = useRef(null);
570
+ const slotRefs = useRef(new Array(13).fill(null));
571
+ const slots = useMemo(() => {
572
+ const nodes = [];
573
+ for (let cell = 1; cell <= 12; cell++) {
574
+ const child = pieces({ cell, square: 0 });
575
+ nodes.push(
576
+ /* @__PURE__ */ jsx(
577
+ "div",
578
+ {
579
+ ref: (el) => {
580
+ slotRefs.current[cell] = el;
581
+ },
582
+ "data-drag-cell": cell,
583
+ style: SLOT_STYLE,
584
+ children: child
585
+ },
586
+ cell
587
+ )
588
+ );
589
+ }
590
+ return nodes;
591
+ }, [pieces]);
592
+ useImperativeHandle(
593
+ ref,
594
+ () => ({
595
+ show(cell) {
596
+ const wrapper = wrapperRef.current;
597
+ if (wrapper === null) return;
598
+ wrapper.style.opacity = "1";
599
+ wrapper.dataset["active"] = "true";
600
+ for (let c = 1; c <= 12; c++) {
601
+ const el = slotRefs.current[c];
602
+ if (el !== null && el !== void 0) {
603
+ el.style.opacity = c === cell ? "1" : "0";
604
+ }
605
+ }
606
+ },
607
+ hide() {
608
+ const wrapper = wrapperRef.current;
609
+ if (wrapper === null) return;
610
+ wrapper.style.opacity = "0";
611
+ wrapper.dataset["active"] = "false";
612
+ },
613
+ setTransform(x, y) {
614
+ const wrapper = wrapperRef.current;
615
+ if (wrapper === null) return;
616
+ wrapper.style.transform = `translate3d(${x}px, ${y}px, 0)`;
617
+ },
618
+ element() {
619
+ return wrapperRef.current;
620
+ }
621
+ }),
622
+ []
623
+ );
624
+ return /* @__PURE__ */ jsx(
625
+ "div",
626
+ {
627
+ ref: wrapperRef,
628
+ "data-layer": "drag",
629
+ "data-active": "false",
630
+ "aria-hidden": "true",
631
+ style: WRAPPER_STYLE,
632
+ children: slots
633
+ }
634
+ );
635
+ });
636
+ function positionOf2(index, orientation) {
637
+ const file = index & 7;
638
+ const rank = index >> 3;
639
+ const col = orientation === "white" ? file : 7 - file;
640
+ const row = orientation === "white" ? 7 - rank : rank;
641
+ return { x: col * 12.5, y: row * 12.5 };
642
+ }
643
+ function prefersReducedMotion() {
644
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
645
+ return false;
646
+ }
647
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
648
+ }
649
+ var FLASH_DURATION_MS = 280;
650
+ function IllegalFlashLayer({ at, orientation }) {
651
+ const elRef = useRef(null);
652
+ useLayoutEffect(() => {
653
+ if (at === null) return;
654
+ const el = elRef.current;
655
+ if (el === null || typeof el.animate !== "function") return;
656
+ const duration = prefersReducedMotion() ? 0 : FLASH_DURATION_MS;
657
+ el.animate(
658
+ [
659
+ { opacity: 0, offset: 0 },
660
+ { opacity: 1, offset: 0.15 },
661
+ { opacity: 0, offset: 1 }
662
+ ],
663
+ { duration, easing: "ease-out", fill: "forwards" }
664
+ );
665
+ }, [at]);
666
+ if (at === null) return null;
667
+ const { x, y } = positionOf2(at, orientation);
668
+ return /* @__PURE__ */ jsx(
669
+ "div",
670
+ {
671
+ ref: elRef,
672
+ "data-layer": "illegal-flash",
673
+ "aria-hidden": "true",
674
+ style: {
675
+ position: "absolute",
676
+ left: `${x}%`,
677
+ top: `${y}%`,
678
+ width: "12.5%",
679
+ height: "12.5%",
680
+ background: DEFAULT_ILLEGAL_FLASH_TINT,
681
+ opacity: 0,
682
+ pointerEvents: "none",
683
+ zIndex: 5
684
+ }
685
+ }
686
+ );
687
+ }
688
+ function algebraicOf2(index) {
689
+ const file = index & 7;
690
+ const rank = index >> 3;
691
+ return `${String.fromCharCode(97 + file)}${rank + 1}`;
692
+ }
693
+ function pieceName(cell) {
694
+ if (cell === 0) return "piece";
695
+ const color = cell > 6 ? "black" : "white";
696
+ const type = (cell - 1) % 6;
697
+ switch (type) {
698
+ case 0:
699
+ return `${color} pawn`;
700
+ case 1:
701
+ return `${color} knight`;
702
+ case 2:
703
+ return `${color} bishop`;
704
+ case 3:
705
+ return `${color} rook`;
706
+ case 4:
707
+ return `${color} queen`;
708
+ case 5:
709
+ return `${color} king`;
710
+ default:
711
+ return `${color} piece`;
712
+ }
713
+ }
714
+ function announcementFor(lastMove, board, wasCapture) {
715
+ const { from, to, kind, promotion } = decodePackedMove(lastMove);
716
+ if (kind === "castle") {
717
+ const isKingside = (to & 7) > (from & 7);
718
+ return isKingside ? "Castles kingside." : "Castles queenside.";
719
+ }
720
+ const landed = board[to] ?? 0;
721
+ const name = pieceName(landed);
722
+ const fromSq = algebraicOf2(from);
723
+ const toSq = algebraicOf2(to);
724
+ if (kind === "promotion" && promotion !== null) {
725
+ const promoted = promotion === 1 ? "knight" : promotion === 2 ? "bishop" : promotion === 3 ? "rook" : "queen";
726
+ const action = wasCapture ? "captures and promotes" : "promotes";
727
+ return `Pawn ${fromSq} ${action} to ${promoted} on ${toSq}.`;
728
+ }
729
+ if (kind === "en-passant") {
730
+ return `${capitalise(name)} ${fromSq} captures en passant on ${toSq}.`;
731
+ }
732
+ if (wasCapture) {
733
+ return `${capitalise(name)} ${fromSq} captures on ${toSq}.`;
734
+ }
735
+ return `${capitalise(name)} ${fromSq} to ${toSq}.`;
736
+ }
737
+ function capitalise(s) {
738
+ return s.length === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1);
739
+ }
740
+ var srOnlyStyle = {
741
+ position: "absolute",
742
+ width: "1px",
743
+ height: "1px",
744
+ padding: 0,
745
+ margin: "-1px",
746
+ overflow: "hidden",
747
+ clip: "rect(0, 0, 0, 0)",
748
+ whiteSpace: "nowrap",
749
+ border: 0
750
+ };
751
+ function LiveRegion({ model }) {
752
+ const lastMove = useBoardSlice(model, (s) => s.lastMove);
753
+ const message = useMemo(() => {
754
+ if (lastMove === null) return "";
755
+ const wasCapture = model.lastAnimations.some(
756
+ (d) => d.kind === "capture" || d.kind === "en-passant"
757
+ );
758
+ return announcementFor(lastMove, model.getSnapshot().board, wasCapture);
759
+ }, [lastMove, model]);
760
+ return /* @__PURE__ */ jsx(
761
+ "div",
762
+ {
763
+ role: "status",
764
+ "aria-live": "polite",
765
+ "aria-atomic": "true",
766
+ "data-layer": "live-region",
767
+ style: srOnlyStyle,
768
+ children: message
769
+ }
770
+ );
771
+ }
772
+ function gridCoord(index, orientation) {
773
+ const file = index & 7;
774
+ const rank = index >> 3;
775
+ const col = orientation === "white" ? file + 1 : 8 - file;
776
+ const row = orientation === "white" ? 8 - rank : rank + 1;
777
+ return { col, row };
778
+ }
779
+ function algebraicOf3(index) {
780
+ const file = index & 7;
781
+ const rank = index >> 3;
782
+ return `${String.fromCharCode(97 + file)}${rank + 1}`;
783
+ }
784
+ var SLOT_BASE_STYLE = Object.freeze({
785
+ display: "flex",
786
+ alignItems: "center",
787
+ justifyContent: "center",
788
+ pointerEvents: "none",
789
+ containerType: "size"
790
+ });
791
+ function PieceSlotImpl({ index, model, orientation, pieces }) {
792
+ const cell = useSquareCell(model, index);
793
+ if (cell === 0) return null;
794
+ const { col, row } = gridCoord(index, orientation);
795
+ return /* @__PURE__ */ jsx(
796
+ "div",
797
+ {
798
+ "data-piece-square": algebraicOf3(index),
799
+ "data-piece-cell": cell,
800
+ style: { ...SLOT_BASE_STYLE, gridColumnStart: col, gridRowStart: row },
801
+ children: pieces({ cell, square: index })
802
+ }
803
+ );
804
+ }
805
+ var PieceSlot = memo(PieceSlotImpl);
806
+ PieceSlot.displayName = "PieceSlot";
807
+ var CONTAINER_STYLE = Object.freeze({
808
+ position: "absolute",
809
+ inset: 0,
810
+ display: "grid",
811
+ gridTemplateColumns: "repeat(8, 1fr)",
812
+ gridTemplateRows: "repeat(8, 1fr)",
813
+ pointerEvents: "none"
814
+ });
815
+ var PieceLayer = memo(function PieceLayer2({
816
+ model,
817
+ orientation,
818
+ pieces
819
+ }) {
820
+ const slots = [];
821
+ for (let i = 0; i < 64; i++) {
822
+ slots.push(
823
+ /* @__PURE__ */ jsx(
824
+ PieceSlot,
825
+ {
826
+ index: i,
827
+ model,
828
+ orientation,
829
+ pieces
830
+ },
831
+ i
832
+ )
833
+ );
834
+ }
835
+ return /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: CONTAINER_STYLE, children: slots });
836
+ });
837
+ function positionOf3(index, orientation) {
838
+ const file = index & 7;
839
+ const rank = index >> 3;
840
+ const col = orientation === "white" ? file : 7 - file;
841
+ const row = orientation === "white" ? 7 - rank : rank;
842
+ return { x: col * 12.5, y: row * 12.5 };
843
+ }
844
+ function PremoveLayer({ model, orientation, pieces }) {
845
+ const premoves = useBoardSlice(model, (s) => s.premoves);
846
+ if (premoves.length === 0) return null;
847
+ const head = premoves[0];
848
+ if (head === void 0) return null;
849
+ return /* @__PURE__ */ jsx(
850
+ PremoveLayerInner,
851
+ {
852
+ model,
853
+ orientation,
854
+ pieces,
855
+ from: head.from,
856
+ to: head.to
857
+ }
858
+ );
859
+ }
860
+ function PremoveLayerInner({ model, orientation, pieces, from, to }) {
861
+ const cell = useSquareCell(model, from);
862
+ if (cell === 0) return null;
863
+ const origin = positionOf3(from, orientation);
864
+ const target = positionOf3(to, orientation);
865
+ return /* @__PURE__ */ jsxs(
866
+ "div",
867
+ {
868
+ "data-layer": "premove",
869
+ "aria-hidden": "true",
870
+ style: {
871
+ position: "absolute",
872
+ inset: 0,
873
+ pointerEvents: "none",
874
+ zIndex: 4
875
+ },
876
+ children: [
877
+ /* @__PURE__ */ jsx(
878
+ "div",
879
+ {
880
+ style: {
881
+ position: "absolute",
882
+ left: `${origin.x}%`,
883
+ top: `${origin.y}%`,
884
+ width: "12.5%",
885
+ height: "12.5%",
886
+ background: DEFAULT_PREMOVE_TINT,
887
+ opacity: 0.5
888
+ }
889
+ }
890
+ ),
891
+ /* @__PURE__ */ jsx(
892
+ "div",
893
+ {
894
+ style: {
895
+ position: "absolute",
896
+ left: `${target.x}%`,
897
+ top: `${target.y}%`,
898
+ width: "12.5%",
899
+ height: "12.5%",
900
+ background: DEFAULT_PREMOVE_TINT
901
+ }
902
+ }
903
+ ),
904
+ /* @__PURE__ */ jsx(
905
+ "div",
906
+ {
907
+ "data-premove-ghost": "true",
908
+ style: {
909
+ position: "absolute",
910
+ left: `${target.x}%`,
911
+ top: `${target.y}%`,
912
+ width: "12.5%",
913
+ height: "12.5%",
914
+ display: "flex",
915
+ alignItems: "center",
916
+ justifyContent: "center",
917
+ opacity: 0.55,
918
+ containerType: "size"
919
+ },
920
+ children: pieces({ cell, square: to })
921
+ }
922
+ )
923
+ ]
924
+ }
925
+ );
926
+ }
927
+ function positionOf4(index, orientation) {
928
+ const file = index & 7;
929
+ const rank = index >> 3;
930
+ const col = orientation === "white" ? file : 7 - file;
931
+ const row = orientation === "white" ? 7 - rank : rank;
932
+ return { x: col * 12.5, y: row * 12.5 };
933
+ }
934
+ var CHOICE_ORDER = [
935
+ PieceType.Queen,
936
+ PieceType.Knight,
937
+ PieceType.Rook,
938
+ PieceType.Bishop
939
+ ];
940
+ var SHORTCUTS = {
941
+ q: PieceType.Queen,
942
+ r: PieceType.Rook,
943
+ b: PieceType.Bishop,
944
+ n: PieceType.Knight,
945
+ Q: PieceType.Queen,
946
+ R: PieceType.Rook,
947
+ B: PieceType.Bishop,
948
+ N: PieceType.Knight
949
+ };
950
+ function PromotionOverlay({
951
+ from: _from,
952
+ to,
953
+ color,
954
+ orientation,
955
+ pieces,
956
+ onSelect,
957
+ onCancel,
958
+ getPromotionPieceAriaLabel
959
+ }) {
960
+ const buttonRefs = useRef([]);
961
+ const getLabel = useCallback(
962
+ (piece) => getPromotionPieceAriaLabel ? getPromotionPieceAriaLabel(piece, color) : pieceName2(piece),
963
+ [getPromotionPieceAriaLabel, color]
964
+ );
965
+ const [activeAnnouncement, setActiveAnnouncement] = useState(
966
+ () => getLabel(CHOICE_ORDER[0] ?? PieceType.Queen)
967
+ );
968
+ useEffect(() => {
969
+ buttonRefs.current[0]?.focus();
970
+ }, []);
971
+ useEffect(() => {
972
+ const onKey = (e) => {
973
+ if (e.key === "Escape") {
974
+ e.preventDefault();
975
+ onCancel();
976
+ return;
977
+ }
978
+ if (e.key === "Tab") {
979
+ e.preventDefault();
980
+ const activeEl = document.activeElement;
981
+ const currentIdx = buttonRefs.current.findIndex((el) => el === activeEl);
982
+ let nextIdx;
983
+ if (e.shiftKey) {
984
+ nextIdx = currentIdx <= 0 ? buttonRefs.current.length - 1 : currentIdx - 1;
985
+ } else {
986
+ nextIdx = currentIdx >= buttonRefs.current.length - 1 ? 0 : currentIdx + 1;
987
+ }
988
+ buttonRefs.current[nextIdx]?.focus();
989
+ return;
990
+ }
991
+ if (e.key === "ArrowDown" || e.key === "ArrowRight") {
992
+ e.preventDefault();
993
+ const activeEl = document.activeElement;
994
+ const currentIdx = buttonRefs.current.findIndex((el) => el === activeEl);
995
+ const nextIdx = currentIdx < 0 ? 0 : (currentIdx + 1) % CHOICE_ORDER.length;
996
+ buttonRefs.current[nextIdx]?.focus();
997
+ return;
998
+ }
999
+ if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
1000
+ e.preventDefault();
1001
+ const activeEl = document.activeElement;
1002
+ const currentIdx = buttonRefs.current.findIndex((el) => el === activeEl);
1003
+ const prevIdx = currentIdx <= 0 ? CHOICE_ORDER.length - 1 : currentIdx - 1;
1004
+ buttonRefs.current[prevIdx]?.focus();
1005
+ return;
1006
+ }
1007
+ if (e.key === "Home") {
1008
+ e.preventDefault();
1009
+ buttonRefs.current[0]?.focus();
1010
+ return;
1011
+ }
1012
+ if (e.key === "End") {
1013
+ e.preventDefault();
1014
+ buttonRefs.current[CHOICE_ORDER.length - 1]?.focus();
1015
+ return;
1016
+ }
1017
+ const choice = SHORTCUTS[e.key];
1018
+ if (choice !== void 0) {
1019
+ e.preventDefault();
1020
+ onSelect(choice);
1021
+ }
1022
+ };
1023
+ window.addEventListener("keydown", onKey);
1024
+ return () => window.removeEventListener("keydown", onKey);
1025
+ }, [onSelect, onCancel]);
1026
+ const targetPos = positionOf4(to, orientation);
1027
+ const promotingColorIsBlack = color === 1;
1028
+ const stackUpward = orientation === "white" ? promotingColorIsBlack : !promotingColorIsBlack;
1029
+ return /* @__PURE__ */ jsxs(
1030
+ "div",
1031
+ {
1032
+ role: "presentation",
1033
+ "data-layer": "promotion-overlay",
1034
+ onClick: onCancel,
1035
+ style: {
1036
+ position: "absolute",
1037
+ inset: 0,
1038
+ background: "rgba(0, 0, 0, 0.35)",
1039
+ zIndex: 30,
1040
+ cursor: "pointer"
1041
+ },
1042
+ children: [
1043
+ /* @__PURE__ */ jsx(
1044
+ "div",
1045
+ {
1046
+ role: "dialog",
1047
+ "aria-modal": "true",
1048
+ "aria-label": `Choose promotion piece at ${algebraicOf4(to)}`,
1049
+ onClick: (e) => e.stopPropagation(),
1050
+ style: {
1051
+ position: "absolute",
1052
+ left: `${targetPos.x}%`,
1053
+ top: `${stackUpward ? targetPos.y - 12.5 * 3 : targetPos.y}%`,
1054
+ width: "12.5%",
1055
+ height: `${12.5 * 4}%`,
1056
+ display: "flex",
1057
+ flexDirection: stackUpward ? "column-reverse" : "column",
1058
+ background: "#ffffff",
1059
+ boxShadow: "0 4px 16px rgba(0, 0, 0, 0.28)",
1060
+ borderRadius: "4px",
1061
+ overflow: "hidden"
1062
+ },
1063
+ children: CHOICE_ORDER.map((piece, index) => {
1064
+ const cell = encodeBoardCell(color, piece);
1065
+ const label = getLabel(piece);
1066
+ return /* @__PURE__ */ jsx(
1067
+ "button",
1068
+ {
1069
+ ref: (el) => {
1070
+ buttonRefs.current[index] = el;
1071
+ },
1072
+ type: "button",
1073
+ onClick: () => onSelect(piece),
1074
+ onFocus: () => setActiveAnnouncement(label),
1075
+ "aria-label": label,
1076
+ style: {
1077
+ flex: 1,
1078
+ display: "flex",
1079
+ alignItems: "center",
1080
+ justifyContent: "center",
1081
+ border: "none",
1082
+ background: "transparent",
1083
+ cursor: "pointer",
1084
+ padding: 0,
1085
+ margin: 0,
1086
+ font: "inherit",
1087
+ containerType: "size"
1088
+ },
1089
+ "data-promotion-piece": pieceName2(piece),
1090
+ children: pieces({ cell, square: to })
1091
+ },
1092
+ piece
1093
+ );
1094
+ })
1095
+ }
1096
+ ),
1097
+ /* @__PURE__ */ jsx("span", { className: "sr-only", "aria-live": "polite", "aria-atomic": "true", style: srOnlyStyle2, children: activeAnnouncement }),
1098
+ /* @__PURE__ */ jsx("span", { className: "sr-only", style: srOnlyStyle2, children: "Choose a promotion piece with Q, N, R, or B, or arrow keys. Press Escape to cancel." })
1099
+ ]
1100
+ }
1101
+ );
1102
+ }
1103
+ var srOnlyStyle2 = {
1104
+ position: "absolute",
1105
+ width: "1px",
1106
+ height: "1px",
1107
+ padding: 0,
1108
+ margin: "-1px",
1109
+ overflow: "hidden",
1110
+ clip: "rect(0, 0, 0, 0)",
1111
+ whiteSpace: "nowrap",
1112
+ border: 0
1113
+ };
1114
+ function pieceName2(t) {
1115
+ switch (t) {
1116
+ case PieceType.Queen:
1117
+ return "Queen";
1118
+ case PieceType.Rook:
1119
+ return "Rook";
1120
+ case PieceType.Bishop:
1121
+ return "Bishop";
1122
+ case PieceType.Knight:
1123
+ return "Knight";
1124
+ default:
1125
+ return "Piece";
1126
+ }
1127
+ }
1128
+ function algebraicOf4(index) {
1129
+ const file = index & 7;
1130
+ const rank = index >> 3;
1131
+ return `${String.fromCharCode(97 + file)}${rank + 1}`;
1132
+ }
1133
+ function StaticPieceLayer({ board, orientation, pieces }) {
1134
+ const slots = [];
1135
+ for (let i = 0; i < 64; i++) {
1136
+ const cell = board[i] ?? 0;
1137
+ if (cell === 0) continue;
1138
+ const index = i;
1139
+ const { col, row } = gridCoord(index, orientation);
1140
+ slots.push(
1141
+ /* @__PURE__ */ jsx(
1142
+ "div",
1143
+ {
1144
+ "data-piece-square": algebraicOf3(index),
1145
+ "data-piece-cell": cell,
1146
+ style: { ...SLOT_BASE_STYLE, gridColumnStart: col, gridRowStart: row },
1147
+ children: pieces({ cell, square: index })
1148
+ },
1149
+ i
1150
+ )
1151
+ );
1152
+ }
1153
+ return /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: CONTAINER_STYLE, children: slots });
1154
+ }
1155
+ var DEFAULT_DURATION_MS = 60;
1156
+ var DEFAULT_EASING = "cubic-bezier(0.22, 0.61, 0.36, 1)";
1157
+ function algebraicOf5(index) {
1158
+ const file = index & 7;
1159
+ const rank = index >> 3;
1160
+ return `${String.fromCharCode(97 + file)}${rank + 1}`;
1161
+ }
1162
+ function deltaFromTo(from, to, squareSize, orientation) {
1163
+ const fromFile = from & 7;
1164
+ const fromRank = from >> 3;
1165
+ const toFile = to & 7;
1166
+ const toRank = to >> 3;
1167
+ const fromCol = orientation === "white" ? fromFile : 7 - fromFile;
1168
+ const fromRow = orientation === "white" ? 7 - fromRank : fromRank;
1169
+ const toCol = orientation === "white" ? toFile : 7 - toFile;
1170
+ const toRow = orientation === "white" ? 7 - toRank : toRank;
1171
+ return {
1172
+ dx: (fromCol - toCol) * squareSize,
1173
+ dy: (fromRow - toRow) * squareSize
1174
+ };
1175
+ }
1176
+ function cancelAnimations(element) {
1177
+ if (typeof element.getAnimations !== "function") return;
1178
+ for (const anim of element.getAnimations()) anim.cancel();
1179
+ }
1180
+ function prefersReducedMotion2() {
1181
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
1182
+ return false;
1183
+ }
1184
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1185
+ }
1186
+ function animateGlide(element, dx, dy, duration, easing) {
1187
+ cancelAnimations(element);
1188
+ if (duration <= 0 || typeof element.animate !== "function") return;
1189
+ element.animate(
1190
+ [{ transform: `translate(${dx}px, ${dy}px)` }, { transform: "translate(0, 0)" }],
1191
+ { duration, easing, fill: "none" }
1192
+ );
1193
+ }
1194
+ function runAnimations(descriptors, container, orientation, duration, easing) {
1195
+ const rect = container.getBoundingClientRect();
1196
+ const sqSize = rect.width / 8;
1197
+ if (sqSize <= 0) return;
1198
+ for (const d of descriptors) {
1199
+ switch (d.kind) {
1200
+ case "move":
1201
+ case "capture":
1202
+ case "en-passant":
1203
+ case "promotion": {
1204
+ const toEl = container.querySelector(
1205
+ `[data-piece-square="${algebraicOf5(d.to)}"]`
1206
+ );
1207
+ if (toEl === null) break;
1208
+ const { dx, dy } = deltaFromTo(d.from, d.to, sqSize, orientation);
1209
+ animateGlide(toEl, dx, dy, duration, easing);
1210
+ break;
1211
+ }
1212
+ case "castle": {
1213
+ const kingEl = container.querySelector(
1214
+ `[data-piece-square="${algebraicOf5(d.kingTo)}"]`
1215
+ );
1216
+ const rookEl = container.querySelector(
1217
+ `[data-piece-square="${algebraicOf5(d.rookTo)}"]`
1218
+ );
1219
+ if (kingEl !== null) {
1220
+ const { dx, dy } = deltaFromTo(d.kingFrom, d.kingTo, sqSize, orientation);
1221
+ animateGlide(kingEl, dx, dy, duration, easing);
1222
+ }
1223
+ if (rookEl !== null) {
1224
+ const { dx, dy } = deltaFromTo(d.rookFrom, d.rookTo, sqSize, orientation);
1225
+ animateGlide(rookEl, dx, dy, duration, easing);
1226
+ }
1227
+ break;
1228
+ }
1229
+ }
1230
+ }
1231
+ }
1232
+ function AnimationRunner({
1233
+ model,
1234
+ containerRef,
1235
+ orientation,
1236
+ options,
1237
+ runtime
1238
+ }) {
1239
+ const duration = options?.durationMs ?? DEFAULT_DURATION_MS;
1240
+ const easing = options?.easing ?? DEFAULT_EASING;
1241
+ const skipNextRef = runtime?.skipNextRef;
1242
+ const historyPly = useBoardSlice(model, (s) => s.historyPly);
1243
+ const lastAnimatedPlyRef = useRef(null);
1244
+ useLayoutEffect(() => {
1245
+ if (lastAnimatedPlyRef.current === historyPly) return;
1246
+ lastAnimatedPlyRef.current = historyPly;
1247
+ const container = containerRef.current;
1248
+ if (container === null) return;
1249
+ const descriptors = model.lastAnimations;
1250
+ if (descriptors.length === 0) return;
1251
+ if (skipNextRef?.current) {
1252
+ skipNextRef.current = false;
1253
+ return;
1254
+ }
1255
+ const effectiveDuration = prefersReducedMotion2() ? 0 : duration;
1256
+ if (effectiveDuration <= 0) return;
1257
+ runAnimations(descriptors, container, orientation, effectiveDuration, easing);
1258
+ }, [historyPly, model, containerRef, orientation, duration, easing, skipNextRef]);
1259
+ return null;
1260
+ }
1261
+
1262
+ // src/lib/geometry.ts
1263
+ function getSquareAtPoint(container, clientX, clientY, orientation) {
1264
+ const rect = container.getBoundingClientRect();
1265
+ const relX = clientX - rect.left;
1266
+ const relY = clientY - rect.top;
1267
+ if (relX < 0 || relY < 0 || relX >= rect.width || relY >= rect.height) return null;
1268
+ const col = Math.floor(relX / rect.width * 8);
1269
+ const row = Math.floor(relY / rect.height * 8);
1270
+ const file = orientation === "white" ? col : 7 - col;
1271
+ const rank = orientation === "white" ? 7 - row : row;
1272
+ return rank * 8 + file;
1273
+ }
1274
+ function getPointAtSquareCentre(container, square, orientation) {
1275
+ const rect = container.getBoundingClientRect();
1276
+ const sqW = rect.width / 8;
1277
+ const sqH = rect.height / 8;
1278
+ const file = square & 7;
1279
+ const rank = square >> 3;
1280
+ const col = orientation === "white" ? file : 7 - file;
1281
+ const row = orientation === "white" ? 7 - rank : rank;
1282
+ return {
1283
+ x: rect.left + sqW * col + sqW / 2,
1284
+ y: rect.top + sqH * row + sqH / 2
1285
+ };
1286
+ }
1287
+ function algebraicOf6(square) {
1288
+ const file = square & 7;
1289
+ const rank = square >> 3;
1290
+ return `${String.fromCharCode(97 + file)}${rank + 1}`;
1291
+ }
1292
+
1293
+ // src/hooks/use-arrow-gesture.ts
1294
+ function colorForModifiers(e, palette) {
1295
+ if (e.shiftKey) return palette.shift;
1296
+ if (e.altKey) return palette.alt;
1297
+ if (e.ctrlKey || e.metaKey) return palette.ctrl;
1298
+ return palette.default;
1299
+ }
1300
+ function useArrowGesture(options) {
1301
+ const { game, orientation, containerRef, arrowsLayerRef, enabled, palette, snapToValidMove } = options;
1302
+ useEffect(() => {
1303
+ if (!enabled) return;
1304
+ const container = containerRef.current;
1305
+ if (container === null || game === null) return;
1306
+ let origin = null;
1307
+ let activePointerId = null;
1308
+ const clearPreview = () => {
1309
+ arrowsLayerRef.current?.setPreview(null);
1310
+ };
1311
+ const snap = (candidate) => {
1312
+ if (!snapToValidMove) return candidate;
1313
+ if (origin === null) return candidate;
1314
+ if (candidate === origin) return candidate;
1315
+ const legals = game.legalFrom(origin);
1316
+ if (legals.size === 0 || legals.has(candidate)) return candidate;
1317
+ const cf = candidate & 7;
1318
+ const cr = candidate >> 3;
1319
+ let best = candidate;
1320
+ let bestDist = Infinity;
1321
+ for (const t of legals) {
1322
+ const df = (t & 7) - cf;
1323
+ const dr = (t >> 3) - cr;
1324
+ const d = df * df + dr * dr;
1325
+ if (d < bestDist) {
1326
+ bestDist = d;
1327
+ best = t;
1328
+ }
1329
+ }
1330
+ return best;
1331
+ };
1332
+ const setPreview = (to, color) => {
1333
+ if (origin === null) return;
1334
+ arrowsLayerRef.current?.setPreview({ from: origin, to: snap(to), color });
1335
+ };
1336
+ const onContextMenu = (e) => {
1337
+ e.preventDefault();
1338
+ };
1339
+ const onPointerDown = (e) => {
1340
+ if (e.button !== 2) return;
1341
+ const sq = getSquareAtPoint(container, e.clientX, e.clientY, orientation);
1342
+ if (sq === null) return;
1343
+ origin = sq;
1344
+ activePointerId = e.pointerId;
1345
+ setPreview(sq, colorForModifiers(e, palette));
1346
+ try {
1347
+ container.setPointerCapture(e.pointerId);
1348
+ } catch {
1349
+ }
1350
+ };
1351
+ const onPointerMove = (e) => {
1352
+ if (origin === null) return;
1353
+ if (activePointerId !== null && e.pointerId !== activePointerId) return;
1354
+ const sq = getSquareAtPoint(container, e.clientX, e.clientY, orientation);
1355
+ if (sq === null) {
1356
+ setPreview(origin, colorForModifiers(e, palette));
1357
+ return;
1358
+ }
1359
+ setPreview(sq, colorForModifiers(e, palette));
1360
+ };
1361
+ const onPointerUp = (e) => {
1362
+ if (origin === null) return;
1363
+ if (activePointerId !== null && e.pointerId !== activePointerId) return;
1364
+ const raw = getSquareAtPoint(container, e.clientX, e.clientY, orientation) ?? origin;
1365
+ const target = snap(raw);
1366
+ const color = colorForModifiers(e, palette);
1367
+ game.toggleArrow({ from: origin, to: target, color });
1368
+ origin = null;
1369
+ activePointerId = null;
1370
+ clearPreview();
1371
+ };
1372
+ const onPointerCancel = (e) => {
1373
+ if (origin === null) return;
1374
+ if (activePointerId !== null && e.pointerId !== activePointerId) return;
1375
+ origin = null;
1376
+ activePointerId = null;
1377
+ clearPreview();
1378
+ };
1379
+ container.addEventListener("contextmenu", onContextMenu);
1380
+ container.addEventListener("pointerdown", onPointerDown);
1381
+ container.addEventListener("pointermove", onPointerMove, { passive: true });
1382
+ container.addEventListener("pointerup", onPointerUp);
1383
+ container.addEventListener("pointercancel", onPointerCancel);
1384
+ return () => {
1385
+ container.removeEventListener("contextmenu", onContextMenu);
1386
+ container.removeEventListener("pointerdown", onPointerDown);
1387
+ container.removeEventListener("pointermove", onPointerMove);
1388
+ container.removeEventListener("pointerup", onPointerUp);
1389
+ container.removeEventListener("pointercancel", onPointerCancel);
1390
+ clearPreview();
1391
+ };
1392
+ }, [game, orientation, containerRef, arrowsLayerRef, enabled, palette, snapToValidMove]);
1393
+ }
1394
+ function wouldPromote(fromCell, toIndex) {
1395
+ if (fromCell === 0) return false;
1396
+ const pieceType = (fromCell - 1) % 6;
1397
+ if (pieceType !== PieceType.Pawn) return false;
1398
+ const toRank = toIndex >> 3;
1399
+ return toRank === 0 || toRank === 7;
1400
+ }
1401
+ function useClickToMove(model, onMove, onPromotionNeeded, enabled = true) {
1402
+ return useCallback(
1403
+ (index) => {
1404
+ if (!enabled) return;
1405
+ if (model === null) return;
1406
+ const snap = model.getSnapshot();
1407
+ const cell = snap.board[index] ?? 0;
1408
+ const pieceColour = cell === 0 ? -1 : cell > 6 ? Color.Black : Color.White;
1409
+ if (snap.selected === null) {
1410
+ if (pieceColour === snap.turn) model.selectSquare(index);
1411
+ return;
1412
+ }
1413
+ if (snap.selected === index) {
1414
+ model.selectSquare(null);
1415
+ return;
1416
+ }
1417
+ if (snap.legalTargets.has(index)) {
1418
+ const fromCell = snap.board[snap.selected] ?? 0;
1419
+ if (wouldPromote(fromCell, index)) {
1420
+ if (onPromotionNeeded !== void 0) {
1421
+ onPromotionNeeded(snap.selected, index);
1422
+ return;
1423
+ }
1424
+ const played2 = model.tryMove(snap.selected, index, PieceType.Queen);
1425
+ if (played2 !== null && onMove !== void 0) onMove(played2);
1426
+ return;
1427
+ }
1428
+ const played = model.tryMove(snap.selected, index);
1429
+ if (played !== null && onMove !== void 0) onMove(played);
1430
+ return;
1431
+ }
1432
+ if (pieceColour === snap.turn) {
1433
+ model.selectSquare(index);
1434
+ return;
1435
+ }
1436
+ model.selectSquare(null);
1437
+ },
1438
+ [model, onMove, onPromotionNeeded, enabled]
1439
+ );
1440
+ }
1441
+ var STYLE_ID = "gb-cursor-styles";
1442
+ function injectCursorStyles() {
1443
+ if (typeof document === "undefined") return;
1444
+ if (document.getElementById(STYLE_ID) !== null) return;
1445
+ const style = document.createElement("style");
1446
+ style.id = STYLE_ID;
1447
+ style.textContent = `
1448
+ [data-gb-square] {
1449
+ cursor: pointer;
1450
+ }
1451
+ [data-gb-turn="white"] [data-piece-cell="1"],
1452
+ [data-gb-turn="white"] [data-piece-cell="2"],
1453
+ [data-gb-turn="white"] [data-piece-cell="3"],
1454
+ [data-gb-turn="white"] [data-piece-cell="4"],
1455
+ [data-gb-turn="white"] [data-piece-cell="5"],
1456
+ [data-gb-turn="white"] [data-piece-cell="6"],
1457
+ [data-gb-turn="black"] [data-piece-cell="7"],
1458
+ [data-gb-turn="black"] [data-piece-cell="8"],
1459
+ [data-gb-turn="black"] [data-piece-cell="9"],
1460
+ [data-gb-turn="black"] [data-piece-cell="10"],
1461
+ [data-gb-turn="black"] [data-piece-cell="11"],
1462
+ [data-gb-turn="black"] [data-piece-cell="12"] {
1463
+ pointer-events: auto;
1464
+ cursor: grab;
1465
+ }
1466
+ [data-gb-premove="true"][data-gb-turn="white"] [data-piece-cell="7"],
1467
+ [data-gb-premove="true"][data-gb-turn="white"] [data-piece-cell="8"],
1468
+ [data-gb-premove="true"][data-gb-turn="white"] [data-piece-cell="9"],
1469
+ [data-gb-premove="true"][data-gb-turn="white"] [data-piece-cell="10"],
1470
+ [data-gb-premove="true"][data-gb-turn="white"] [data-piece-cell="11"],
1471
+ [data-gb-premove="true"][data-gb-turn="white"] [data-piece-cell="12"],
1472
+ [data-gb-premove="true"][data-gb-turn="black"] [data-piece-cell="1"],
1473
+ [data-gb-premove="true"][data-gb-turn="black"] [data-piece-cell="2"],
1474
+ [data-gb-premove="true"][data-gb-turn="black"] [data-piece-cell="3"],
1475
+ [data-gb-premove="true"][data-gb-turn="black"] [data-piece-cell="4"],
1476
+ [data-gb-premove="true"][data-gb-turn="black"] [data-piece-cell="5"],
1477
+ [data-gb-premove="true"][data-gb-turn="black"] [data-piece-cell="6"] {
1478
+ pointer-events: auto;
1479
+ cursor: grab;
1480
+ }
1481
+ [data-gb-dragging="true"] [data-gb-square],
1482
+ [data-gb-dragging="true"] [data-piece-cell] {
1483
+ cursor: grabbing;
1484
+ }
1485
+ `;
1486
+ document.head.appendChild(style);
1487
+ }
1488
+ function useCursorController(model, containerRef, allowDrag, allowPremove) {
1489
+ useEffect(() => {
1490
+ injectCursorStyles();
1491
+ }, []);
1492
+ useEffect(() => {
1493
+ if (model === null) return;
1494
+ let lastTurn = null;
1495
+ let lastPremove = null;
1496
+ const write = () => {
1497
+ const el = containerRef.current;
1498
+ if (el === null) return;
1499
+ if (!allowDrag) {
1500
+ if (lastTurn !== null) {
1501
+ delete el.dataset["gbTurn"];
1502
+ lastTurn = null;
1503
+ }
1504
+ if (lastPremove !== null) {
1505
+ delete el.dataset["gbPremove"];
1506
+ lastPremove = null;
1507
+ }
1508
+ return;
1509
+ }
1510
+ const turn = model.getSnapshot().turn === 0 ? "white" : "black";
1511
+ if (turn !== lastTurn) {
1512
+ el.dataset["gbTurn"] = turn;
1513
+ lastTurn = turn;
1514
+ }
1515
+ if (allowPremove !== lastPremove) {
1516
+ if (allowPremove) {
1517
+ el.dataset["gbPremove"] = "true";
1518
+ } else {
1519
+ delete el.dataset["gbPremove"];
1520
+ }
1521
+ lastPremove = allowPremove;
1522
+ }
1523
+ };
1524
+ write();
1525
+ const unsubscribe = model.subscribe(write);
1526
+ return () => {
1527
+ unsubscribe();
1528
+ const el = containerRef.current;
1529
+ if (el !== null) {
1530
+ delete el.dataset["gbTurn"];
1531
+ delete el.dataset["gbPremove"];
1532
+ }
1533
+ };
1534
+ }, [model, containerRef, allowDrag, allowPremove]);
1535
+ }
1536
+ var GHOST_OPACITY_VAR = CSS_VARS.DRAG_GHOST_OPACITY;
1537
+ function useDrag(options) {
1538
+ const {
1539
+ game,
1540
+ orientation,
1541
+ containerRef,
1542
+ dragLayerRef,
1543
+ enabled,
1544
+ activationDistance,
1545
+ onDragStart,
1546
+ onDragEnd,
1547
+ onDrop,
1548
+ canDragPiece
1549
+ } = options;
1550
+ const canDragPieceRef = useRef(canDragPiece);
1551
+ useEffect(() => {
1552
+ canDragPieceRef.current = canDragPiece;
1553
+ });
1554
+ useEffect(() => {
1555
+ if (!enabled) return;
1556
+ const container = containerRef.current;
1557
+ if (container === null || game === null) return;
1558
+ const controller = createDragController(
1559
+ activationDistance === void 0 ? {} : { activationDistance }
1560
+ );
1561
+ let originEl = null;
1562
+ let active = false;
1563
+ let activePointerId = null;
1564
+ const writeTransform = (clientX, clientY) => {
1565
+ const layer = dragLayerRef.current;
1566
+ if (layer === null) return;
1567
+ const rect = container.getBoundingClientRect();
1568
+ const sqSize = rect.width / 8;
1569
+ const x = clientX - rect.left - sqSize / 2;
1570
+ const y = clientY - rect.top - sqSize / 2;
1571
+ layer.setTransform(x, y);
1572
+ };
1573
+ const restoreOrigin = () => {
1574
+ if (originEl !== null) {
1575
+ originEl.style.opacity = "";
1576
+ originEl = null;
1577
+ }
1578
+ };
1579
+ const cleanup = () => {
1580
+ restoreOrigin();
1581
+ dragLayerRef.current?.hide();
1582
+ active = false;
1583
+ activePointerId = null;
1584
+ onDragEnd();
1585
+ };
1586
+ const onPointerDown = (e) => {
1587
+ if (e.button !== 0) return;
1588
+ const sq = getSquareAtPoint(container, e.clientX, e.clientY, orientation);
1589
+ if (sq === null) return;
1590
+ const cell = game.getSnapshot().board[sq] ?? 0;
1591
+ if (cell === 0) return;
1592
+ const canDrag = canDragPieceRef.current;
1593
+ if (canDrag !== void 0 && !canDrag({ square: sq, cell })) {
1594
+ return;
1595
+ }
1596
+ const consumed = controller.pointerDown(sq, e.pointerId, e.clientX, e.clientY);
1597
+ if (consumed) activePointerId = e.pointerId;
1598
+ };
1599
+ const onPointerMove = (e) => {
1600
+ const evt = controller.pointerMove(e.pointerId, e.clientX, e.clientY);
1601
+ if (evt === null) return;
1602
+ if (evt.kind === "drag-start") {
1603
+ const cell = game.getSnapshot().board[evt.from] ?? 0;
1604
+ if (cell === 0) {
1605
+ controller.cancel();
1606
+ return;
1607
+ }
1608
+ const originLabel = algebraicOf6(evt.from);
1609
+ originEl = container.querySelector(`[data-piece-square="${originLabel}"]`);
1610
+ if (originEl !== null) {
1611
+ originEl.style.opacity = `var(${GHOST_OPACITY_VAR}, 0.35)`;
1612
+ }
1613
+ active = true;
1614
+ try {
1615
+ container.setPointerCapture(e.pointerId);
1616
+ } catch {
1617
+ }
1618
+ dragLayerRef.current?.show(cell);
1619
+ writeTransform(e.clientX, e.clientY);
1620
+ onDragStart(evt.from, cell);
1621
+ return;
1622
+ }
1623
+ writeTransform(e.clientX, e.clientY);
1624
+ };
1625
+ const onPointerUp = (e) => {
1626
+ const evt = controller.pointerUp(e.pointerId, e.clientX, e.clientY);
1627
+ if (evt === null) return;
1628
+ if (evt.kind === "drop") {
1629
+ const target = getSquareAtPoint(container, e.clientX, e.clientY, orientation);
1630
+ if (target !== null && target !== evt.from) {
1631
+ onDrop(evt.from, target);
1632
+ }
1633
+ }
1634
+ if (active) cleanup();
1635
+ activePointerId = null;
1636
+ };
1637
+ const onPointerCancel = (e) => {
1638
+ if (activePointerId !== null && e.pointerId !== activePointerId) return;
1639
+ controller.cancel();
1640
+ if (active) cleanup();
1641
+ activePointerId = null;
1642
+ };
1643
+ container.addEventListener("pointerdown", onPointerDown);
1644
+ container.addEventListener("pointermove", onPointerMove, { passive: true });
1645
+ container.addEventListener("pointerup", onPointerUp);
1646
+ container.addEventListener("pointercancel", onPointerCancel);
1647
+ return () => {
1648
+ container.removeEventListener("pointerdown", onPointerDown);
1649
+ container.removeEventListener("pointermove", onPointerMove);
1650
+ container.removeEventListener("pointerup", onPointerUp);
1651
+ container.removeEventListener("pointercancel", onPointerCancel);
1652
+ if (active) cleanup();
1653
+ };
1654
+ }, [
1655
+ game,
1656
+ orientation,
1657
+ containerRef,
1658
+ dragLayerRef,
1659
+ enabled,
1660
+ activationDistance,
1661
+ onDragStart,
1662
+ onDragEnd,
1663
+ onDrop
1664
+ ]);
1665
+ }
1666
+ function useHoverSquare(options) {
1667
+ const { game, orientation, containerRef, enabled, onEnter, onLeave } = options;
1668
+ const onEnterRef = useRef(onEnter);
1669
+ const onLeaveRef = useRef(onLeave);
1670
+ useEffect(() => {
1671
+ onEnterRef.current = onEnter;
1672
+ onLeaveRef.current = onLeave;
1673
+ });
1674
+ useEffect(() => {
1675
+ if (!enabled) return;
1676
+ const container = containerRef.current;
1677
+ if (container === null) return;
1678
+ let current = null;
1679
+ const cellAt = (sq) => game === null ? 0 : game.getSnapshot().board[sq] ?? 0;
1680
+ const onPointerMove = (e) => {
1681
+ const sq = getSquareAtPoint(container, e.clientX, e.clientY, orientation);
1682
+ if (sq === current) return;
1683
+ if (current !== null) onLeaveRef.current?.({ square: current, cell: cellAt(current) });
1684
+ if (sq !== null) onEnterRef.current?.({ square: sq, cell: cellAt(sq) });
1685
+ current = sq;
1686
+ };
1687
+ const onPointerLeave = () => {
1688
+ if (current === null) return;
1689
+ onLeaveRef.current?.({ square: current, cell: cellAt(current) });
1690
+ current = null;
1691
+ };
1692
+ container.addEventListener("pointermove", onPointerMove, { passive: true });
1693
+ container.addEventListener("pointerleave", onPointerLeave);
1694
+ return () => {
1695
+ container.removeEventListener("pointermove", onPointerMove);
1696
+ container.removeEventListener("pointerleave", onPointerLeave);
1697
+ if (current !== null) {
1698
+ onLeaveRef.current?.({ square: current, cell: cellAt(current) });
1699
+ current = null;
1700
+ }
1701
+ };
1702
+ }, [game, orientation, containerRef, enabled]);
1703
+ }
1704
+ function moveFocus(current, direction, orientation) {
1705
+ const file = current & 7;
1706
+ const rank = current >> 3;
1707
+ const col = orientation === "white" ? file : 7 - file;
1708
+ const row = orientation === "white" ? 7 - rank : rank;
1709
+ let newCol = col;
1710
+ let newRow = row;
1711
+ switch (direction) {
1712
+ case "up":
1713
+ newRow = Math.max(0, row - 1);
1714
+ break;
1715
+ case "down":
1716
+ newRow = Math.min(7, row + 1);
1717
+ break;
1718
+ case "left":
1719
+ newCol = Math.max(0, col - 1);
1720
+ break;
1721
+ case "right":
1722
+ newCol = Math.min(7, col + 1);
1723
+ break;
1724
+ }
1725
+ const newFile = orientation === "white" ? newCol : 7 - newCol;
1726
+ const newRank = orientation === "white" ? 7 - newRow : newRow;
1727
+ return newRank * 8 + newFile;
1728
+ }
1729
+ function rankEnd(current, which, orientation) {
1730
+ const rank = current >> 3;
1731
+ if (orientation === "white") {
1732
+ return rank << 3 | (which === "home" ? 0 : 7);
1733
+ }
1734
+ return rank << 3 | (which === "home" ? 7 : 0);
1735
+ }
1736
+ function boardCorner(which, orientation) {
1737
+ if (orientation === "white") {
1738
+ return which === "home" ? 56 : 7;
1739
+ }
1740
+ return which === "home" ? 7 : 56;
1741
+ }
1742
+ function useKeyboardNav(options) {
1743
+ const {
1744
+ containerRef,
1745
+ orientation,
1746
+ focusedSquare,
1747
+ setFocusedSquare,
1748
+ onActivate,
1749
+ onEscape,
1750
+ enabled
1751
+ } = options;
1752
+ useEffect(() => {
1753
+ if (!enabled) return;
1754
+ const container = containerRef.current;
1755
+ if (container === null) return;
1756
+ const onKeyDown = (e) => {
1757
+ if (!container.contains(e.target)) return;
1758
+ const current = focusedSquare;
1759
+ if (current === null) return;
1760
+ switch (e.key) {
1761
+ case "ArrowUp":
1762
+ e.preventDefault();
1763
+ setFocusedSquare(moveFocus(current, "up", orientation));
1764
+ return;
1765
+ case "ArrowDown":
1766
+ e.preventDefault();
1767
+ setFocusedSquare(moveFocus(current, "down", orientation));
1768
+ return;
1769
+ case "ArrowLeft":
1770
+ e.preventDefault();
1771
+ setFocusedSquare(moveFocus(current, "left", orientation));
1772
+ return;
1773
+ case "ArrowRight":
1774
+ e.preventDefault();
1775
+ setFocusedSquare(moveFocus(current, "right", orientation));
1776
+ return;
1777
+ case "Home":
1778
+ e.preventDefault();
1779
+ setFocusedSquare(
1780
+ e.ctrlKey || e.metaKey ? boardCorner("home", orientation) : rankEnd(current, "home", orientation)
1781
+ );
1782
+ return;
1783
+ case "End":
1784
+ e.preventDefault();
1785
+ setFocusedSquare(
1786
+ e.ctrlKey || e.metaKey ? boardCorner("end", orientation) : rankEnd(current, "end", orientation)
1787
+ );
1788
+ return;
1789
+ case "Enter":
1790
+ case " ":
1791
+ e.preventDefault();
1792
+ onActivate(current);
1793
+ return;
1794
+ case "Escape":
1795
+ e.preventDefault();
1796
+ onEscape();
1797
+ return;
1798
+ default:
1799
+ return;
1800
+ }
1801
+ };
1802
+ container.addEventListener("keydown", onKeyDown);
1803
+ return () => container.removeEventListener("keydown", onKeyDown);
1804
+ }, [containerRef, orientation, focusedSquare, setFocusedSquare, onActivate, onEscape, enabled]);
1805
+ }
1806
+ var STYLE_ID2 = "gb-last-move-styles";
1807
+ function injectLastMoveStyles() {
1808
+ if (typeof document === "undefined") return;
1809
+ if (document.getElementById(STYLE_ID2) !== null) return;
1810
+ const style = document.createElement("style");
1811
+ style.id = STYLE_ID2;
1812
+ style.textContent = `
1813
+ [data-gb-square]::after {
1814
+ content: "";
1815
+ position: absolute;
1816
+ inset: 0;
1817
+ background: transparent;
1818
+ pointer-events: none;
1819
+ }
1820
+ [data-gb-square][data-gb-last-move]::after {
1821
+ background: var(--gb-last-move);
1822
+ }
1823
+ `;
1824
+ document.head.appendChild(style);
1825
+ }
1826
+ function useLastMoveController(model, squareRefs, enabled) {
1827
+ useEffect(() => {
1828
+ injectLastMoveStyles();
1829
+ }, []);
1830
+ useEffect(() => {
1831
+ if (model === null) return;
1832
+ let prevFrom = null;
1833
+ let prevTo = null;
1834
+ const write = (i, val) => {
1835
+ const el = squareRefs.current?.[i];
1836
+ if (el === null || el === void 0) return;
1837
+ if (val === null) {
1838
+ delete el.dataset["gbLastMove"];
1839
+ } else {
1840
+ el.dataset["gbLastMove"] = val;
1841
+ }
1842
+ };
1843
+ const clearAll = () => {
1844
+ if (prevFrom !== null) write(prevFrom, null);
1845
+ if (prevTo !== null) write(prevTo, null);
1846
+ prevFrom = null;
1847
+ prevTo = null;
1848
+ };
1849
+ if (!enabled) {
1850
+ return clearAll;
1851
+ }
1852
+ const update = () => {
1853
+ const snap = model.getSnapshot();
1854
+ const lastMove = snap.lastMove;
1855
+ let from = null;
1856
+ let to = null;
1857
+ if (lastMove !== null) {
1858
+ from = lastMove & 63;
1859
+ to = lastMove >> 6 & 63;
1860
+ }
1861
+ if (from === prevFrom && to === prevTo) return;
1862
+ if (prevFrom !== null && prevFrom !== from && prevFrom !== to) {
1863
+ write(prevFrom, null);
1864
+ }
1865
+ if (prevTo !== null && prevTo !== from && prevTo !== to) {
1866
+ write(prevTo, null);
1867
+ }
1868
+ if (from !== null && from !== to) write(from, "from");
1869
+ if (to !== null) write(to, "to");
1870
+ prevFrom = from;
1871
+ prevTo = to;
1872
+ };
1873
+ update();
1874
+ const unsubscribe = model.subscribe(update);
1875
+ return () => {
1876
+ unsubscribe();
1877
+ clearAll();
1878
+ };
1879
+ }, [model, squareRefs, enabled]);
1880
+ }
1881
+
1882
+ // src/lib/haptics.ts
1883
+ var COARSE_POINTER_QUERY = "(hover: none), (pointer: coarse)";
1884
+ var cached;
1885
+ var pending = null;
1886
+ function isCoarsePointer() {
1887
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
1888
+ return false;
1889
+ }
1890
+ return window.matchMedia(COARSE_POINTER_QUERY).matches;
1891
+ }
1892
+ function shouldRunHaptics(options) {
1893
+ if (options.enabled === false) return false;
1894
+ if (options.mobileOnly === false) return true;
1895
+ return isCoarsePointer();
1896
+ }
1897
+ async function getInstance() {
1898
+ if (cached !== void 0) {
1899
+ return cached;
1900
+ }
1901
+ if (pending !== null) {
1902
+ return pending;
1903
+ }
1904
+ if (typeof window === "undefined") {
1905
+ cached = null;
1906
+ return cached;
1907
+ }
1908
+ pending = import('web-haptics').then(({ WebHaptics }) => {
1909
+ try {
1910
+ cached = new WebHaptics();
1911
+ } catch {
1912
+ cached = null;
1913
+ }
1914
+ return cached;
1915
+ }).catch(() => {
1916
+ cached = null;
1917
+ return cached;
1918
+ }).finally(() => {
1919
+ pending = null;
1920
+ });
1921
+ return pending;
1922
+ }
1923
+ function triggerHaptic(input, options = {}) {
1924
+ if (!shouldRunHaptics(options)) {
1925
+ return;
1926
+ }
1927
+ const { enabled: _enabled, mobileOnly: _mobileOnly, ...triggerOptions } = options;
1928
+ void getInstance().then((instance) => {
1929
+ void instance?.trigger(input, triggerOptions);
1930
+ });
1931
+ }
1932
+ function warmupHaptics(options = {}) {
1933
+ if (!shouldRunHaptics(options)) {
1934
+ return;
1935
+ }
1936
+ void getInstance();
1937
+ }
1938
+
1939
+ // src/lib/move-feedback.ts
1940
+ var DEFAULT_MOVE_HAPTIC_PATTERNS = Object.freeze({
1941
+ moveSelf: "selection",
1942
+ moveOpponent: "selection",
1943
+ capture: "medium",
1944
+ castle: "medium",
1945
+ moveCheck: "success",
1946
+ promote: "success",
1947
+ gameEnd: "success"
1948
+ });
1949
+ function classifyMoveFeedback(animations, snapshot, perspective) {
1950
+ if (snapshot.isGameOver) return "gameEnd";
1951
+ let sawPromotion = false;
1952
+ let sawCastle = false;
1953
+ let sawCapture = false;
1954
+ for (const d of animations) {
1955
+ if (d.kind === "promotion") sawPromotion = true;
1956
+ else if (d.kind === "castle") sawCastle = true;
1957
+ else if (d.kind === "capture" || d.kind === "en-passant") sawCapture = true;
1958
+ }
1959
+ if (sawPromotion) return "promote";
1960
+ if (sawCastle) return "castle";
1961
+ if (snapshot.inCheck) return "moveCheck";
1962
+ if (sawCapture) return "capture";
1963
+ if (perspective !== void 0) {
1964
+ const moverWasMe = snapshot.turn !== perspective;
1965
+ return moverWasMe ? "moveSelf" : "moveOpponent";
1966
+ }
1967
+ return "moveSelf";
1968
+ }
1969
+
1970
+ // src/hooks/use-move-haptics.ts
1971
+ var DEFAULT_MIN_INTERVAL_MS = 14;
1972
+ function triggerMoveHaptic(key, {
1973
+ enabled = true,
1974
+ patterns,
1975
+ mobileOnly = true,
1976
+ intensity
1977
+ } = {}) {
1978
+ if (!enabled) {
1979
+ return;
1980
+ }
1981
+ const input = patterns?.[key] ?? DEFAULT_MOVE_HAPTIC_PATTERNS[key];
1982
+ triggerHaptic(input, {
1983
+ enabled,
1984
+ mobileOnly,
1985
+ ...intensity !== void 0 ? { intensity } : {}
1986
+ });
1987
+ }
1988
+ function useMoveHaptics(model, options = {}) {
1989
+ const {
1990
+ enabled = true,
1991
+ patterns,
1992
+ perspective,
1993
+ mobileOnly = true,
1994
+ minIntervalMs = DEFAULT_MIN_INTERVAL_MS,
1995
+ warmupOnFirstInteraction = true,
1996
+ intensity
1997
+ } = options;
1998
+ const patternsRef = useRef(patterns);
1999
+ useEffect(() => {
2000
+ patternsRef.current = patterns;
2001
+ });
2002
+ const perspectiveRef = useRef(perspective);
2003
+ useEffect(() => {
2004
+ perspectiveRef.current = perspective;
2005
+ });
2006
+ const mobileOnlyRef = useRef(mobileOnly);
2007
+ useEffect(() => {
2008
+ mobileOnlyRef.current = mobileOnly;
2009
+ });
2010
+ const intensityRef = useRef(intensity);
2011
+ useEffect(() => {
2012
+ intensityRef.current = intensity;
2013
+ });
2014
+ const lastTriggerAtRef = useRef(0);
2015
+ const trigger = useCallback(
2016
+ (key) => {
2017
+ triggerMoveHaptic(key, {
2018
+ enabled,
2019
+ ...patternsRef.current !== void 0 ? { patterns: patternsRef.current } : {},
2020
+ mobileOnly: mobileOnlyRef.current,
2021
+ ...intensityRef.current !== void 0 ? { intensity: intensityRef.current } : {}
2022
+ });
2023
+ },
2024
+ [enabled]
2025
+ );
2026
+ useEffect(() => {
2027
+ if (!enabled || !warmupOnFirstInteraction) {
2028
+ return;
2029
+ }
2030
+ let warmed = false;
2031
+ const warmup = () => {
2032
+ if (warmed) {
2033
+ return;
2034
+ }
2035
+ warmed = true;
2036
+ warmupHaptics({ mobileOnly: mobileOnlyRef.current });
2037
+ window.removeEventListener("pointerdown", warmup, true);
2038
+ window.removeEventListener("touchstart", warmup, true);
2039
+ window.removeEventListener("keydown", warmup, true);
2040
+ };
2041
+ window.addEventListener("pointerdown", warmup, { capture: true, passive: true });
2042
+ window.addEventListener("touchstart", warmup, { capture: true, passive: true });
2043
+ window.addEventListener("keydown", warmup, { capture: true });
2044
+ return () => {
2045
+ window.removeEventListener("pointerdown", warmup, true);
2046
+ window.removeEventListener("touchstart", warmup, true);
2047
+ window.removeEventListener("keydown", warmup, true);
2048
+ };
2049
+ }, [enabled, warmupOnFirstInteraction]);
2050
+ useEffect(() => {
2051
+ if (!enabled || model === null) return;
2052
+ let prevPly = model.getSnapshot().historyPly;
2053
+ return model.subscribe(() => {
2054
+ const snap = model.getSnapshot();
2055
+ if (snap.historyPly <= prevPly) {
2056
+ prevPly = snap.historyPly;
2057
+ return;
2058
+ }
2059
+ prevPly = snap.historyPly;
2060
+ const nowMs = performance.now();
2061
+ if (nowMs - lastTriggerAtRef.current < minIntervalMs) {
2062
+ return;
2063
+ }
2064
+ lastTriggerAtRef.current = nowMs;
2065
+ const key = classifyMoveFeedback(model.lastAnimations, snap, perspectiveRef.current);
2066
+ trigger(key);
2067
+ });
2068
+ }, [enabled, minIntervalMs, model, trigger]);
2069
+ return trigger;
2070
+ }
2071
+ var DEFAULT_MOVE_SOUND_SOURCES = Object.freeze({
2072
+ moveSelf: typeof import.meta.url === "string" ? new URL("./sounds/move-self.mp3", import.meta.url).href : "./sounds/move-self.mp3",
2073
+ moveOpponent: typeof import.meta.url === "string" ? new URL("./sounds/move-opponent.mp3", import.meta.url).href : "./sounds/move-opponent.mp3",
2074
+ capture: typeof import.meta.url === "string" ? new URL("./sounds/capture.mp3", import.meta.url).href : "./sounds/capture.mp3",
2075
+ castle: typeof import.meta.url === "string" ? new URL("./sounds/castle.mp3", import.meta.url).href : "./sounds/castle.mp3",
2076
+ moveCheck: typeof import.meta.url === "string" ? new URL("./sounds/move-check.mp3", import.meta.url).href : "./sounds/move-check.mp3",
2077
+ promote: typeof import.meta.url === "string" ? new URL("./sounds/promote.mp3", import.meta.url).href : "./sounds/promote.mp3",
2078
+ gameEnd: typeof import.meta.url === "string" ? new URL("./sounds/game-end.mp3", import.meta.url).href : "./sounds/game-end.mp3"
2079
+ });
2080
+ var POOL_SIZE = 4;
2081
+ function createPools(sources, preload) {
2082
+ const pools = {};
2083
+ for (const key of Object.keys(sources)) {
2084
+ const url = sources[key];
2085
+ const elements = [];
2086
+ for (let i = 0; i < POOL_SIZE; i++) {
2087
+ const audio = new Audio(url);
2088
+ audio.preload = preload ?? "none";
2089
+ elements.push(audio);
2090
+ }
2091
+ pools[key] = { elements, cursor: 0 };
2092
+ }
2093
+ return pools;
2094
+ }
2095
+ function useMoveSound(model, options = {}) {
2096
+ const { enabled = true, volume = 1, preload = "none", sources, perspective } = options;
2097
+ const poolsRef = useRef(null);
2098
+ const sourcesRef = useRef(DEFAULT_MOVE_SOUND_SOURCES);
2099
+ const preloadRef = useRef(preload);
2100
+ const volumeRef = useRef(volume);
2101
+ useEffect(() => {
2102
+ volumeRef.current = volume;
2103
+ });
2104
+ const perspectiveRef = useRef(perspective);
2105
+ useEffect(() => {
2106
+ perspectiveRef.current = perspective;
2107
+ });
2108
+ useEffect(() => {
2109
+ sourcesRef.current = {
2110
+ ...DEFAULT_MOVE_SOUND_SOURCES,
2111
+ ...sources ?? {}
2112
+ };
2113
+ poolsRef.current = null;
2114
+ }, [sources]);
2115
+ useEffect(() => {
2116
+ preloadRef.current = preload;
2117
+ poolsRef.current = null;
2118
+ }, [preload]);
2119
+ useEffect(() => {
2120
+ if (!enabled) {
2121
+ poolsRef.current = null;
2122
+ }
2123
+ }, [enabled]);
2124
+ const triggerMoveSound = useCallback(
2125
+ (key) => {
2126
+ if (!enabled) return;
2127
+ if (typeof window === "undefined") return;
2128
+ if (typeof Audio === "undefined") return;
2129
+ const pools = poolsRef.current ?? createPools(sourcesRef.current, preloadRef.current);
2130
+ poolsRef.current = pools;
2131
+ const pool = pools[key];
2132
+ if (pool === void 0) return;
2133
+ pool.cursor = (pool.cursor + 1) % pool.elements.length;
2134
+ const el = pool.elements[pool.cursor];
2135
+ if (el === void 0) return;
2136
+ el.volume = Math.max(0, Math.min(1, volumeRef.current));
2137
+ el.currentTime = 0;
2138
+ void el.play().catch(() => {
2139
+ });
2140
+ },
2141
+ [enabled]
2142
+ );
2143
+ useEffect(() => {
2144
+ if (!enabled || model === null) return;
2145
+ let prevPly = model.getSnapshot().historyPly;
2146
+ return model.subscribe(() => {
2147
+ const snap = model.getSnapshot();
2148
+ if (snap.historyPly <= prevPly) {
2149
+ prevPly = snap.historyPly;
2150
+ return;
2151
+ }
2152
+ prevPly = snap.historyPly;
2153
+ const key = classifyMoveFeedback(model.lastAnimations, snap, perspectiveRef.current);
2154
+ triggerMoveSound(key);
2155
+ });
2156
+ }, [enabled, model, triggerMoveSound]);
2157
+ return triggerMoveSound;
2158
+ }
2159
+ var STYLE_ID3 = "gb-selection-styles";
2160
+ function injectSelectionStyles() {
2161
+ if (typeof document === "undefined") return;
2162
+ if (document.getElementById(STYLE_ID3) !== null) return;
2163
+ const style = document.createElement("style");
2164
+ style.id = STYLE_ID3;
2165
+ style.textContent = `
2166
+ [data-gb-square]::before {
2167
+ content: "";
2168
+ position: absolute;
2169
+ inset: 0;
2170
+ background: transparent;
2171
+ pointer-events: none;
2172
+ }
2173
+ [data-gb-square][data-gb-selection="selected"]::before {
2174
+ background: var(--gb-selected);
2175
+ }
2176
+ [data-gb-square][data-gb-selection="legal-quiet"]::before {
2177
+ background-image: radial-gradient(var(--gb-legal-target) 22%, transparent 24%);
2178
+ }
2179
+ [data-gb-square][data-gb-selection="legal-capture"]::before {
2180
+ background-image: radial-gradient(transparent 0 75%, var(--gb-legal-target-capture) 77% 83%, transparent 85%);
2181
+ }
2182
+ [data-gb-target-style="dots"] [data-gb-square][data-gb-selection="legal-quiet"]::before {
2183
+ background-image: radial-gradient(var(--gb-legal-target) 14%, transparent 16%);
2184
+ }
2185
+ [data-gb-target-style="dots"] [data-gb-square][data-gb-selection="legal-capture"]::before {
2186
+ background-image: radial-gradient(var(--gb-legal-target-capture) 20%, transparent 22%);
2187
+ }
2188
+ `;
2189
+ document.head.appendChild(style);
2190
+ }
2191
+ function useSelectionController(model, squareRefs, style) {
2192
+ useEffect(() => {
2193
+ injectSelectionStyles();
2194
+ }, []);
2195
+ useEffect(() => {
2196
+ if (model === null) return;
2197
+ let prevSelected = null;
2198
+ let prevTargets = /* @__PURE__ */ new Set();
2199
+ const write = (i, val) => {
2200
+ const el = squareRefs.current?.[i];
2201
+ if (el === null || el === void 0) return;
2202
+ if (val === "none") {
2203
+ delete el.dataset["gbSelection"];
2204
+ } else {
2205
+ el.dataset["gbSelection"] = val;
2206
+ }
2207
+ };
2208
+ const clear = () => {
2209
+ if (prevSelected !== null) write(prevSelected, "none");
2210
+ for (const sq of prevTargets) write(sq, "none");
2211
+ prevSelected = null;
2212
+ prevTargets = /* @__PURE__ */ new Set();
2213
+ };
2214
+ const update = () => {
2215
+ const snap = model.getSnapshot();
2216
+ const { selected, legalTargets, board } = snap;
2217
+ if (style === false) {
2218
+ for (const sq of prevTargets) {
2219
+ if (sq !== selected) write(sq, "none");
2220
+ }
2221
+ if (prevSelected !== null && prevSelected !== selected) {
2222
+ write(prevSelected, "none");
2223
+ }
2224
+ if (selected !== null) write(selected, "selected");
2225
+ prevSelected = selected;
2226
+ prevTargets = /* @__PURE__ */ new Set();
2227
+ return;
2228
+ }
2229
+ if (selected === prevSelected && legalTargets === prevTargets) return;
2230
+ if (prevSelected !== null && prevSelected !== selected && !legalTargets.has(prevSelected)) {
2231
+ write(prevSelected, "none");
2232
+ }
2233
+ for (const sq of prevTargets) {
2234
+ if (sq !== selected && !legalTargets.has(sq)) write(sq, "none");
2235
+ }
2236
+ if (selected !== null) write(selected, "selected");
2237
+ for (const sq of legalTargets) {
2238
+ if (sq === selected) continue;
2239
+ const targetCell = board[sq] ?? 0;
2240
+ write(sq, targetCell === 0 ? "legal-quiet" : "legal-capture");
2241
+ }
2242
+ prevSelected = selected;
2243
+ prevTargets = legalTargets;
2244
+ };
2245
+ update();
2246
+ const unsubscribe = model.subscribe(update);
2247
+ return () => {
2248
+ unsubscribe();
2249
+ clear();
2250
+ };
2251
+ }, [model, squareRefs, style]);
2252
+ }
2253
+ function colorOfCell(cell) {
2254
+ return cell > 6 ? 1 : 0;
2255
+ }
2256
+ function movePromotesPawn(fromCell, to) {
2257
+ if (fromCell === 0) return false;
2258
+ const pieceType = (fromCell - 1) % 6;
2259
+ if (pieceType !== 0) return false;
2260
+ const toRank = to >> 3;
2261
+ return toRank === 0 || toRank === 7;
2262
+ }
2263
+ var ILLEGAL_FLASH_HOLD_MS = 320;
2264
+ var DEFAULT_POSITION_SYNC_DURATION_MS = 60;
2265
+ var DEFAULT_POSITION_SYNC_EASING = "cubic-bezier(0.22, 0.61, 0.36, 1)";
2266
+ var UCI_TRANSITION_RE = /^([a-h][1-8])([a-h][1-8])([nbrq])?$/i;
2267
+ function prefersReducedMotion3() {
2268
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
2269
+ return false;
2270
+ }
2271
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2272
+ }
2273
+ function cancelElementAnimations(element) {
2274
+ if (typeof element.getAnimations !== "function") return;
2275
+ for (const anim of element.getAnimations()) anim.cancel();
2276
+ }
2277
+ function queryPieceAt(container, square) {
2278
+ return container.querySelector(`[data-piece-square="${square}"]`);
2279
+ }
2280
+ function squareIndexOfName(square) {
2281
+ const file = square.charCodeAt(0) - 97;
2282
+ const rank = Number(square[1]) - 1;
2283
+ return rank << 3 | file;
2284
+ }
2285
+ function deltaFromSquareNames(from, to, squareSize, orientation) {
2286
+ const fromIndex = squareIndexOfName(from);
2287
+ const toIndex = squareIndexOfName(to);
2288
+ const fromFile = fromIndex & 7;
2289
+ const fromRank = fromIndex >> 3;
2290
+ const toFile = toIndex & 7;
2291
+ const toRank = toIndex >> 3;
2292
+ const fromCol = orientation === "white" ? fromFile : 7 - fromFile;
2293
+ const fromRow = orientation === "white" ? 7 - fromRank : fromRank;
2294
+ const toCol = orientation === "white" ? toFile : 7 - toFile;
2295
+ const toRow = orientation === "white" ? 7 - toRank : toRank;
2296
+ return {
2297
+ dx: (fromCol - toCol) * squareSize,
2298
+ dy: (fromRow - toRow) * squareSize
2299
+ };
2300
+ }
2301
+ function castleRookTransition(from, to) {
2302
+ const fromFile = from.charCodeAt(0);
2303
+ const toFile = to.charCodeAt(0);
2304
+ if (fromFile !== 101 || Math.abs(toFile - fromFile) !== 2) return null;
2305
+ const rank = from[1];
2306
+ if (rank !== "1" && rank !== "8") return null;
2307
+ const kingSide = toFile > fromFile;
2308
+ return {
2309
+ from: `${kingSide ? "h" : "a"}${rank}`,
2310
+ to: `${kingSide ? "f" : "d"}${rank}`
2311
+ };
2312
+ }
2313
+ function transitionSquares(transition) {
2314
+ const parsed = UCI_TRANSITION_RE.exec(transition.uci.trim().toLowerCase());
2315
+ if (parsed === null) return [];
2316
+ const moveFrom = parsed[1] ?? "";
2317
+ const moveTo = parsed[2] ?? "";
2318
+ const forward = [{ from: moveFrom, to: moveTo }];
2319
+ const rook = castleRookTransition(moveFrom, moveTo);
2320
+ if (rook !== null) forward.push(rook);
2321
+ return transition.direction === "forward" ? forward : forward.map((item) => ({ from: item.to, to: item.from }));
2322
+ }
2323
+ function parseTransitionMove(transition) {
2324
+ const parsed = UCI_TRANSITION_RE.exec(transition.uci.trim().toLowerCase());
2325
+ if (parsed === null) return null;
2326
+ const from = parsed[1];
2327
+ const to = parsed[2];
2328
+ if (from === void 0 || to === void 0) return null;
2329
+ return {
2330
+ from: squareIndexOfName(from),
2331
+ to: squareIndexOfName(to),
2332
+ promotion: parsed[3] ?? null
2333
+ };
2334
+ }
2335
+ function isPawn(cell) {
2336
+ return !isEmptyCell(cell) && pieceTypeOf(cell) === 0;
2337
+ }
2338
+ function controlledFeedbackDescriptors(transition, previousBoard, nextBoard) {
2339
+ if (transition === null || transition === void 0) return null;
2340
+ const parsed = parseTransitionMove(transition);
2341
+ if (parsed === null) return null;
2342
+ const before = transition.direction === "forward" ? previousBoard : nextBoard;
2343
+ const after = transition.direction === "forward" ? nextBoard : previousBoard;
2344
+ const movingPiece = before[parsed.from] ?? 0;
2345
+ if (isEmptyCell(movingPiece)) return null;
2346
+ const fromFile = parsed.from & 7;
2347
+ const toFile = parsed.to & 7;
2348
+ const targetBefore = before[parsed.to] ?? 0;
2349
+ const targetAfter = after[parsed.to] ?? 0;
2350
+ if (pieceTypeOf(movingPiece) === 5 && Math.abs(toFile - fromFile) === 2) {
2351
+ return [{ kind: "castle" }];
2352
+ }
2353
+ if (parsed.promotion !== null || isPawn(movingPiece) && (parsed.to >> 3 === 0 || parsed.to >> 3 === 7) && !isEmptyCell(targetAfter) && pieceTypeOf(targetAfter) !== 0) {
2354
+ return [{ kind: "promotion" }];
2355
+ }
2356
+ if (!isEmptyCell(targetBefore) && colorOf(targetBefore) !== colorOf(movingPiece)) {
2357
+ return [{ kind: "capture" }];
2358
+ }
2359
+ if (isPawn(movingPiece) && fromFile !== toFile && isEmptyCell(targetBefore)) {
2360
+ const capturedSquare = parsed.from & -8 | toFile;
2361
+ const capturedBefore = before[capturedSquare] ?? 0;
2362
+ const capturedAfter = after[capturedSquare] ?? 0;
2363
+ if (!isEmptyCell(capturedBefore) && colorOf(capturedBefore) !== colorOf(movingPiece) && isEmptyCell(capturedAfter)) {
2364
+ return [{ kind: "en-passant" }];
2365
+ }
2366
+ }
2367
+ return [{ kind: "move" }];
2368
+ }
2369
+ function schedulePositionAnimation(container, transitions, options, orientation) {
2370
+ const animate = () => {
2371
+ const rect = container.getBoundingClientRect();
2372
+ const squareSize = rect.width / 8;
2373
+ if (squareSize <= 0) return;
2374
+ for (const item of transitions) {
2375
+ const piece = queryPieceAt(container, item.to);
2376
+ if (piece === null || typeof piece.animate !== "function") continue;
2377
+ const { dx, dy } = deltaFromSquareNames(item.from, item.to, squareSize, orientation);
2378
+ if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) continue;
2379
+ cancelElementAnimations(piece);
2380
+ piece.animate(
2381
+ [{ transform: `translate(${dx}px, ${dy}px)` }, { transform: "translate(0, 0)" }],
2382
+ { duration: options.durationMs, easing: options.easing, fill: "none" }
2383
+ );
2384
+ }
2385
+ };
2386
+ if (typeof window.requestAnimationFrame === "function") {
2387
+ window.requestAnimationFrame(animate);
2388
+ } else {
2389
+ window.setTimeout(animate, 0);
2390
+ }
2391
+ }
2392
+ function syncPositionFen(game, targetFen, transition, managedArrows, preserveUserArrows, container, animation, orientation) {
2393
+ const positionChanged = game.engine.fen() !== targetFen;
2394
+ const durationMs = animation?.durationMs ?? DEFAULT_POSITION_SYNC_DURATION_MS;
2395
+ const easing = animation?.easing ?? DEFAULT_POSITION_SYNC_EASING;
2396
+ const shouldAnimate = positionChanged && transition !== null && transition !== void 0 && container !== null && durationMs > 0 && !prefersReducedMotion3();
2397
+ const transitions = [];
2398
+ if (shouldAnimate) {
2399
+ for (const item of transitionSquares(transition)) {
2400
+ if (queryPieceAt(container, item.from) !== null) transitions.push(item);
2401
+ }
2402
+ }
2403
+ try {
2404
+ if (managedArrows === void 0) {
2405
+ if (!positionChanged) return false;
2406
+ game.load(targetFen);
2407
+ } else {
2408
+ game.syncPosition({
2409
+ fen: targetFen,
2410
+ managedArrows,
2411
+ preserveUserArrows: preserveUserArrows === true
2412
+ });
2413
+ }
2414
+ } catch {
2415
+ return false;
2416
+ }
2417
+ if (transitions.length > 0 && container !== null) {
2418
+ schedulePositionAnimation(container, transitions, { durationMs, easing }, orientation);
2419
+ }
2420
+ return positionChanged;
2421
+ }
2422
+ function Chessboard(props) {
2423
+ const {
2424
+ game,
2425
+ fallbackFen,
2426
+ positionFen,
2427
+ positionTransition = null,
2428
+ managedArrows,
2429
+ preserveUserArrowsOnPositionSync,
2430
+ orientation = "white",
2431
+ theme = defaultTheme,
2432
+ pieces = defaultPieces,
2433
+ showCoordinates = true,
2434
+ ranksPosition = "left",
2435
+ showLegalTargets = "rings",
2436
+ highlightLastMove = true,
2437
+ onMove,
2438
+ onPromote,
2439
+ renderSquare,
2440
+ style,
2441
+ className,
2442
+ ariaLabel,
2443
+ animation,
2444
+ allowDrag = true,
2445
+ allowDrawingArrows = true,
2446
+ clearArrowsOnMove = true,
2447
+ clearArrowsOnClick = true,
2448
+ arrowColors,
2449
+ snapArrowsToValidMove = false,
2450
+ allowPremove = false,
2451
+ showCheckHighlight = true,
2452
+ showIllegalFlash = true,
2453
+ sound = true,
2454
+ haptics = false,
2455
+ viewOnly = false,
2456
+ canDragPiece,
2457
+ onSquareMouseEnter,
2458
+ onSquareMouseLeave,
2459
+ disableContextMenu = true,
2460
+ getSquareAriaLabel,
2461
+ onSquareFocus,
2462
+ getPromotionPieceAriaLabel
2463
+ } = props;
2464
+ const dragEnabled = allowDrag && !viewOnly;
2465
+ const arrowGestureEnabled = allowDrawingArrows && !viewOnly;
2466
+ const clickToMoveEnabled = !viewOnly;
2467
+ const keyboardNavEnabled = !viewOnly;
2468
+ const fallbackBoard = useMemo(() => {
2469
+ if (fallbackFen === void 0 || fallbackFen === "") return null;
2470
+ try {
2471
+ return parseFenPlacement(fallbackFen);
2472
+ } catch {
2473
+ return null;
2474
+ }
2475
+ }, [fallbackFen]);
2476
+ const soundOptions = useMemo(() => {
2477
+ if (sound === true) return { enabled: true };
2478
+ if (sound === false || sound === void 0) return { enabled: false };
2479
+ return sound;
2480
+ }, [sound]);
2481
+ const hapticOptions = useMemo(() => {
2482
+ if (haptics === true) return { enabled: true };
2483
+ if (haptics === false || haptics === void 0) return { enabled: false };
2484
+ return haptics;
2485
+ }, [haptics]);
2486
+ const containerRef = useRef(null);
2487
+ const dragLayerRef = useRef(null);
2488
+ const arrowsLayerRef = useRef(null);
2489
+ const lastControlledFeedbackFenRef = useRef(null);
2490
+ const squareRefs = useRef(new Array(64).fill(null));
2491
+ const setSquareRef = useCallback((index, el) => {
2492
+ squareRefs.current[index] = el;
2493
+ }, []);
2494
+ const skipNextAnimationRef = useRef(false);
2495
+ const onMoveRef = useRef(onMove);
2496
+ const onPromoteRef = useRef(onPromote);
2497
+ useEffect(() => {
2498
+ onMoveRef.current = onMove;
2499
+ onPromoteRef.current = onPromote;
2500
+ });
2501
+ const triggerMoveSound = useMoveSound(game, soundOptions);
2502
+ const triggerMoveHaptic2 = useMoveHaptics(game, hapticOptions);
2503
+ useLayoutEffect(() => {
2504
+ if (game === null || positionFen === void 0 || positionFen === "") return;
2505
+ const previousFeedbackFen = lastControlledFeedbackFenRef.current;
2506
+ const previousSnapshot = game.getSnapshot();
2507
+ if (game.engine.fen() === positionFen && managedArrows === void 0) {
2508
+ lastControlledFeedbackFenRef.current = positionFen;
2509
+ return;
2510
+ }
2511
+ const positionChanged = syncPositionFen(
2512
+ game,
2513
+ positionFen,
2514
+ positionTransition,
2515
+ managedArrows,
2516
+ preserveUserArrowsOnPositionSync,
2517
+ containerRef.current,
2518
+ animation,
2519
+ orientation
2520
+ );
2521
+ lastControlledFeedbackFenRef.current = positionFen;
2522
+ if (!positionChanged || previousFeedbackFen === null || previousFeedbackFen === positionFen) {
2523
+ return;
2524
+ }
2525
+ const snapshot = game.getSnapshot();
2526
+ const feedbackDescriptors = controlledFeedbackDescriptors(positionTransition, previousSnapshot.board, snapshot.board) ?? game.lastAnimations;
2527
+ const feedbackSnapshot = positionTransition?.direction === "backward" ? previousSnapshot : snapshot;
2528
+ triggerMoveSound(
2529
+ classifyMoveFeedback(feedbackDescriptors, feedbackSnapshot, soundOptions.perspective)
2530
+ );
2531
+ triggerMoveHaptic2(
2532
+ classifyMoveFeedback(feedbackDescriptors, feedbackSnapshot, hapticOptions.perspective)
2533
+ );
2534
+ }, [
2535
+ animation,
2536
+ game,
2537
+ hapticOptions.perspective,
2538
+ positionFen,
2539
+ positionTransition,
2540
+ managedArrows,
2541
+ preserveUserArrowsOnPositionSync,
2542
+ orientation,
2543
+ soundOptions.perspective,
2544
+ triggerMoveHaptic2,
2545
+ triggerMoveSound
2546
+ ]);
2547
+ const palette = useMemo(() => {
2548
+ const merged = {
2549
+ default: defaultArrowColors.default,
2550
+ shift: defaultArrowColors.shift,
2551
+ alt: defaultArrowColors.alt,
2552
+ ctrl: defaultArrowColors.ctrl
2553
+ };
2554
+ if (arrowColors !== void 0) {
2555
+ for (const k in arrowColors) {
2556
+ const v = arrowColors[k];
2557
+ if (v !== void 0) merged[k] = v;
2558
+ }
2559
+ }
2560
+ return merged;
2561
+ }, [arrowColors]);
2562
+ const [pendingPromotion, setPendingPromotion] = useState(null);
2563
+ const [focusedSquare, setFocusedSquare] = useState(null);
2564
+ const defaultKeyboardFocus = useCallback(
2565
+ () => orientation === "white" ? 12 : 52,
2566
+ [orientation]
2567
+ );
2568
+ const [flashSquare, setFlashSquare] = useState(null);
2569
+ const flashTimerRef = useRef(null);
2570
+ const triggerIllegalFlash = useCallback(
2571
+ (square) => {
2572
+ if (!showIllegalFlash) return;
2573
+ setFlashSquare(square);
2574
+ if (flashTimerRef.current !== null) clearTimeout(flashTimerRef.current);
2575
+ flashTimerRef.current = setTimeout(() => {
2576
+ setFlashSquare(null);
2577
+ flashTimerRef.current = null;
2578
+ }, ILLEGAL_FLASH_HOLD_MS);
2579
+ },
2580
+ [showIllegalFlash]
2581
+ );
2582
+ useEffect(
2583
+ () => () => {
2584
+ if (flashTimerRef.current !== null) clearTimeout(flashTimerRef.current);
2585
+ },
2586
+ []
2587
+ );
2588
+ useEffect(() => {
2589
+ if (game === null || !clearArrowsOnMove) return;
2590
+ let prevPly = game.getSnapshot().historyPly;
2591
+ return game.subscribe(() => {
2592
+ const snap = game.getSnapshot();
2593
+ if (snap.historyPly > prevPly && snap.arrows.length > 0) {
2594
+ game.clearUserArrows();
2595
+ }
2596
+ prevPly = snap.historyPly;
2597
+ });
2598
+ }, [game, clearArrowsOnMove]);
2599
+ const requestPromotion = useCallback(
2600
+ (from, to) => {
2601
+ if (game === null) return;
2602
+ const fromCell = game.getSnapshot().board[from] ?? 0;
2603
+ const color = colorOfCell(fromCell);
2604
+ const completeMove = (piece) => {
2605
+ const played = game.tryMove(from, to, piece);
2606
+ const cb = onMoveRef.current;
2607
+ if (played !== null && cb !== void 0) cb(played);
2608
+ };
2609
+ const promoteHandler = onPromoteRef.current;
2610
+ if (promoteHandler !== void 0) {
2611
+ Promise.resolve(promoteHandler({ from, to, color })).then(completeMove, () => {
2612
+ skipNextAnimationRef.current = false;
2613
+ });
2614
+ return;
2615
+ }
2616
+ setPendingPromotion({
2617
+ from,
2618
+ to,
2619
+ color,
2620
+ resolve: (piece) => {
2621
+ setPendingPromotion(null);
2622
+ completeMove(piece);
2623
+ },
2624
+ cancel: () => {
2625
+ skipNextAnimationRef.current = false;
2626
+ setPendingPromotion(null);
2627
+ }
2628
+ });
2629
+ },
2630
+ [game]
2631
+ );
2632
+ const clickToMove = useClickToMove(game, onMove, requestPromotion, clickToMoveEnabled);
2633
+ const maybeClearArrows = useCallback(() => {
2634
+ if (!clearArrowsOnClick || game === null) return;
2635
+ if (game.getSnapshot().arrows.length > 0) game.clearUserArrows();
2636
+ }, [clearArrowsOnClick, game]);
2637
+ const handleSquareClick = useCallback(
2638
+ (index) => {
2639
+ if (viewOnly) return;
2640
+ maybeClearArrows();
2641
+ clickToMove(index);
2642
+ },
2643
+ [viewOnly, maybeClearArrows, clickToMove]
2644
+ );
2645
+ useEffect(() => {
2646
+ if (!clearArrowsOnClick || game === null || viewOnly) return;
2647
+ const onKey = (e) => {
2648
+ if (e.key === "Escape") maybeClearArrows();
2649
+ };
2650
+ window.addEventListener("keydown", onKey);
2651
+ return () => window.removeEventListener("keydown", onKey);
2652
+ }, [clearArrowsOnClick, game, maybeClearArrows, viewOnly]);
2653
+ useEffect(() => {
2654
+ if (viewOnly || clickToMoveEnabled === false || game === null) return;
2655
+ const container = containerRef.current;
2656
+ if (container === null) return;
2657
+ const onContainerClick = (e) => {
2658
+ if (e.button !== 0) return;
2659
+ const target = e.target;
2660
+ if (target === null) return;
2661
+ if (target.closest("[data-piece-square]") === null) return;
2662
+ const sq = getSquareAtPoint(container, e.clientX, e.clientY, orientation);
2663
+ if (sq === null) return;
2664
+ handleSquareClick(sq);
2665
+ };
2666
+ container.addEventListener("click", onContainerClick);
2667
+ return () => container.removeEventListener("click", onContainerClick);
2668
+ }, [viewOnly, clickToMoveEnabled, game, orientation, handleSquareClick]);
2669
+ useEffect(() => {
2670
+ if (!disableContextMenu) return;
2671
+ const container = containerRef.current;
2672
+ if (container === null) return;
2673
+ const onContextMenu = (e) => {
2674
+ e.preventDefault();
2675
+ };
2676
+ container.addEventListener("contextmenu", onContextMenu);
2677
+ return () => container.removeEventListener("contextmenu", onContextMenu);
2678
+ }, [disableContextMenu]);
2679
+ const onDragStart = useCallback(
2680
+ (from, _cell) => {
2681
+ maybeClearArrows();
2682
+ const container = containerRef.current;
2683
+ if (container !== null) container.dataset["gbDragging"] = "true";
2684
+ game?.selectSquare(from);
2685
+ },
2686
+ [game, maybeClearArrows]
2687
+ );
2688
+ const onDragEnd = useCallback(() => {
2689
+ const container = containerRef.current;
2690
+ if (container !== null) delete container.dataset["gbDragging"];
2691
+ }, []);
2692
+ const onDrop = useCallback(
2693
+ (from, to) => {
2694
+ if (game === null) return;
2695
+ const snap = game.getSnapshot();
2696
+ const fromCell = snap.board[from] ?? 0;
2697
+ const pieceColour = colorOfCell(fromCell);
2698
+ if (fromCell !== 0 && pieceColour !== snap.turn) {
2699
+ if (!allowPremove) {
2700
+ triggerIllegalFlash(from);
2701
+ return;
2702
+ }
2703
+ const promotion = movePromotesPawn(fromCell, to) ? 4 : void 0;
2704
+ game.queuePremove(promotion === void 0 ? { from, to } : { from, to, promotion });
2705
+ return;
2706
+ }
2707
+ skipNextAnimationRef.current = true;
2708
+ if (movePromotesPawn(fromCell, to) && game.isLegal(
2709
+ from,
2710
+ to,
2711
+ 4
2712
+ /* Queen */
2713
+ )) {
2714
+ requestPromotion(from, to);
2715
+ return;
2716
+ }
2717
+ const played = game.tryMove(from, to);
2718
+ const cb = onMoveRef.current;
2719
+ if (played !== null) {
2720
+ if (cb !== void 0) cb(played);
2721
+ } else {
2722
+ skipNextAnimationRef.current = false;
2723
+ triggerIllegalFlash(from);
2724
+ game.selectSquare(from);
2725
+ }
2726
+ },
2727
+ [game, requestPromotion, allowPremove, triggerIllegalFlash]
2728
+ );
2729
+ useDrag({
2730
+ game,
2731
+ orientation,
2732
+ containerRef,
2733
+ dragLayerRef,
2734
+ enabled: dragEnabled && game !== null,
2735
+ onDragStart,
2736
+ onDragEnd,
2737
+ onDrop,
2738
+ ...canDragPiece !== void 0 ? { canDragPiece } : {}
2739
+ });
2740
+ useEffect(() => {
2741
+ if (!keyboardNavEnabled) {
2742
+ setFocusedSquare(null);
2743
+ return;
2744
+ }
2745
+ setFocusedSquare((current) => current ?? defaultKeyboardFocus());
2746
+ }, [defaultKeyboardFocus, keyboardNavEnabled]);
2747
+ const onKeyboardActivate = useCallback(
2748
+ (index) => {
2749
+ maybeClearArrows();
2750
+ clickToMove(index);
2751
+ },
2752
+ [maybeClearArrows, clickToMove]
2753
+ );
2754
+ const onKeyboardEscape = useCallback(() => {
2755
+ maybeClearArrows();
2756
+ game?.selectSquare(null);
2757
+ }, [maybeClearArrows, game]);
2758
+ useKeyboardNav({
2759
+ containerRef,
2760
+ orientation,
2761
+ focusedSquare,
2762
+ setFocusedSquare,
2763
+ onActivate: onKeyboardActivate,
2764
+ onEscape: onKeyboardEscape,
2765
+ enabled: game !== null && keyboardNavEnabled
2766
+ });
2767
+ useHoverSquare({
2768
+ game,
2769
+ orientation,
2770
+ containerRef,
2771
+ enabled: onSquareMouseEnter !== void 0 || onSquareMouseLeave !== void 0,
2772
+ ...onSquareMouseEnter !== void 0 ? { onEnter: onSquareMouseEnter } : {},
2773
+ ...onSquareMouseLeave !== void 0 ? { onLeave: onSquareMouseLeave } : {}
2774
+ });
2775
+ useArrowGesture({
2776
+ game,
2777
+ orientation,
2778
+ containerRef,
2779
+ arrowsLayerRef,
2780
+ enabled: arrowGestureEnabled && game !== null,
2781
+ palette,
2782
+ snapToValidMove: snapArrowsToValidMove
2783
+ });
2784
+ useSelectionController(game, squareRefs, showLegalTargets);
2785
+ useLastMoveController(game, squareRefs, highlightLastMove);
2786
+ useCursorController(game, containerRef, dragEnabled, allowPremove);
2787
+ const animationRuntime = useMemo(() => ({ skipNextRef: skipNextAnimationRef }), []);
2788
+ const containerStyle = {
2789
+ position: "relative",
2790
+ aspectRatio: "1 / 1",
2791
+ width: "100%",
2792
+ touchAction: "none",
2793
+ // let the drag layer own pan gestures
2794
+ ...theme,
2795
+ ...style
2796
+ };
2797
+ return /* @__PURE__ */ jsxs(
2798
+ "div",
2799
+ {
2800
+ ref: containerRef,
2801
+ className,
2802
+ style: containerStyle,
2803
+ "data-gb-orientation": orientation,
2804
+ "data-gb-target-style": showLegalTargets === false ? "off" : showLegalTargets,
2805
+ ...viewOnly ? { "data-gb-view-only": "true" } : {},
2806
+ children: [
2807
+ /* @__PURE__ */ jsx(
2808
+ BoardGrid,
2809
+ {
2810
+ orientation,
2811
+ onSquareClick: handleSquareClick,
2812
+ focusedSquare,
2813
+ setSquareRef,
2814
+ readOnly: viewOnly,
2815
+ model: game,
2816
+ getSquareAriaLabel,
2817
+ onSquareFocus,
2818
+ ...renderSquare !== void 0 ? { renderSquare } : {},
2819
+ ...ariaLabel !== void 0 ? { ariaLabel } : {}
2820
+ }
2821
+ ),
2822
+ game !== null && showCheckHighlight ? /* @__PURE__ */ jsx(CheckLayer, { model: game, orientation }) : null,
2823
+ game !== null ? /* @__PURE__ */ jsx(PremoveLayer, { model: game, orientation, pieces }) : null,
2824
+ game !== null ? /* @__PURE__ */ jsx(ArrowsLayer, { model: game, orientation, palette, below: true }) : null,
2825
+ game !== null ? /* @__PURE__ */ jsx(PieceLayer, { model: game, orientation, pieces }) : fallbackBoard !== null ? /* @__PURE__ */ jsx(StaticPieceLayer, { board: fallbackBoard, orientation, pieces }) : null,
2826
+ game !== null ? /* @__PURE__ */ jsx(
2827
+ AnimationRunner,
2828
+ {
2829
+ model: game,
2830
+ containerRef,
2831
+ orientation,
2832
+ ...animation !== void 0 ? { options: animation } : {},
2833
+ runtime: animationRuntime
2834
+ }
2835
+ ) : null,
2836
+ /* @__PURE__ */ jsx(DragLayer, { ref: dragLayerRef, pieces }),
2837
+ game !== null ? /* @__PURE__ */ jsx(
2838
+ ArrowsLayer,
2839
+ {
2840
+ ref: arrowsLayerRef,
2841
+ model: game,
2842
+ orientation,
2843
+ palette
2844
+ }
2845
+ ) : null,
2846
+ showCoordinates ? /* @__PURE__ */ jsx(Coordinates, { orientation, ranksPosition }) : null,
2847
+ /* @__PURE__ */ jsx(IllegalFlashLayer, { at: flashSquare, orientation }),
2848
+ game !== null ? /* @__PURE__ */ jsx(LiveRegion, { model: game }) : null,
2849
+ pendingPromotion !== null ? /* @__PURE__ */ jsx(
2850
+ PromotionOverlay,
2851
+ {
2852
+ from: pendingPromotion.from,
2853
+ to: pendingPromotion.to,
2854
+ color: pendingPromotion.color,
2855
+ orientation,
2856
+ pieces,
2857
+ onSelect: pendingPromotion.resolve,
2858
+ onCancel: pendingPromotion.cancel,
2859
+ getPromotionPieceAriaLabel
2860
+ }
2861
+ ) : null
2862
+ ]
2863
+ }
2864
+ );
2865
+ }
2866
+ function useChessGame(options = {}) {
2867
+ const { fen, ...modelOptions } = options;
2868
+ const [initialModelOptions] = useState(() => modelOptions);
2869
+ const [model, setModel] = useState(() => {
2870
+ const adapter = createGigachessAdapter(fen);
2871
+ return createBoardModel(adapter, modelOptions);
2872
+ });
2873
+ const prevFenRef = useRef(fen);
2874
+ useEffect(() => {
2875
+ if (prevFenRef.current !== fen) {
2876
+ prevFenRef.current = fen;
2877
+ const adapter = createGigachessAdapter(fen);
2878
+ const nextModel = createBoardModel(adapter, initialModelOptions);
2879
+ setModel((prev) => {
2880
+ prev.dispose();
2881
+ return nextModel;
2882
+ });
2883
+ }
2884
+ }, [fen, initialModelOptions]);
2885
+ useEffect(() => {
2886
+ return () => {
2887
+ model.dispose();
2888
+ };
2889
+ }, [model]);
2890
+ return model;
2891
+ }
2892
+
2893
+ // src/index.ts
2894
+ var PACKAGE_VERSION = "0.1.0-alpha";
2895
+
2896
+ export { AnimationRunner, Chessboard, DEFAULT_MOVE_HAPTIC_PATTERNS, DEFAULT_MOVE_SOUND_SOURCES, PACKAGE_VERSION, classifyMoveFeedback, getPointAtSquareCentre, getSquareAtPoint, triggerHaptic, triggerMoveHaptic, useArrowGesture, useBoardSlice, useBoardSnapshot, useChessGame, useClickToMove, useDrag, useHoverSquare, useKeyboardNav, useMoveHaptics, useMoveSound, useSquareCell, warmupHaptics };
2897
+ //# sourceMappingURL=index.js.map
2898
+ //# sourceMappingURL=index.js.map