react-chess-explorer 0.0.2 → 0.0.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 (26) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +63 -63
  3. package/dist/features/explorer/components/DefaultBoardNav.d.ts +2 -7
  4. package/dist/features/explorer/components/PositionGamesPanel.d.ts +2 -9
  5. package/dist/features/explorer/core/PositionReferenceExplorerCore.d.ts +1 -1
  6. package/dist/features/explorer/core/renderProps.d.ts +22 -15
  7. package/dist/features/explorer/defaults/DefaultReferencePanel.d.ts +2 -1
  8. package/dist/features/explorer/defaults/DefaultVariationsStrip.d.ts +1 -1
  9. package/dist/features/explorer/explorerSessionCache.d.ts +15 -0
  10. package/dist/features/explorer/hooks/useExplorerPrefetch.d.ts +11 -0
  11. package/dist/features/explorer/hooks/usePositionHistory.d.ts +12 -1
  12. package/dist/features/explorer/hooks/usePositionReferenceData.d.ts +11 -14
  13. package/dist/features/explorer/hooks/useVariationLines.d.ts +1 -5
  14. package/dist/features/explorer/index.d.ts +4 -3
  15. package/dist/features/explorer/mocks.d.ts +2 -2
  16. package/dist/features/explorer/positionUtils.d.ts +2 -0
  17. package/dist/features/explorer/seedExplorerStartSession.d.ts +3 -0
  18. package/dist/features/explorer/types.d.ts +13 -10
  19. package/dist/features/explorer/variationLines.d.ts +0 -1
  20. package/dist/index.esm.js +666 -292
  21. package/dist/index.js +676 -290
  22. package/dist/stories/fixtures/nc6MockApi.d.ts +2 -2
  23. package/dist/stories/fixtures/nc6SampleGames.d.ts +0 -3
  24. package/package.json +59 -59
  25. package/dist/features/explorer/components/EloRangeFilter.d.ts +0 -9
  26. package/dist/features/explorer/components/LineHeader.d.ts +0 -4
package/dist/index.esm.js CHANGED
@@ -1,14 +1,152 @@
1
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import { ThemeProvider, HighlightChessboard } from 'react-chess-core';
3
- import { ChessboardDnDProvider } from 'react-chessboard';
4
1
  import { Chess } from 'chess.js';
5
- import { useState, useCallback, useRef, useMemo, useEffect } from 'react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { ThemeProvider, HighlightChessboard, usePositionKeyboardNav, ChessboardDnDProvider } from 'react-chess-core';
4
+ import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
6
5
 
7
6
  /** Standard start position — placeholder until game replay is implemented. */
8
7
  const EXPLORER_START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
9
8
  /** Delay between plies when animating a clicked variation line. */
10
9
  const VARIATION_LINE_STEP_MS = 500;
11
10
 
11
+ /** Match endchess-backend / mass-games-import (first four FEN fields). */
12
+ function normalizeFen(fen) {
13
+ const parts = fen.trim().split(/\s+/);
14
+ if (parts.length < 4) {
15
+ throw new Error(`Invalid FEN: ${fen}`);
16
+ }
17
+ return `${parts[0]} ${parts[1]} ${parts[2]} ${parts[3]}`;
18
+ }
19
+ /** Legal move from a board drag/drop (react-chessboard piece string, e.g. wQ). */
20
+ function applyBoardMove(fen, sourceSquare, targetSquare, piece) {
21
+ var _a, _b;
22
+ const chess = new Chess(fen);
23
+ const pieceType = (_a = piece[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
24
+ const legal = chess
25
+ .moves({ square: sourceSquare, verbose: true })
26
+ .find((move) => move.to === targetSquare &&
27
+ (!move.promotion || move.promotion === pieceType));
28
+ if (!legal)
29
+ return null;
30
+ const played = chess.move({
31
+ from: legal.from,
32
+ to: legal.to,
33
+ promotion: legal.promotion,
34
+ });
35
+ if (!played)
36
+ return null;
37
+ const uci = `${played.from}${played.to}${(_b = played.promotion) !== null && _b !== void 0 ? _b : ""}`;
38
+ return { fen: chess.fen(), uci, san: played.san };
39
+ }
40
+ /** Play a SAN sequence from a start FEN; returns null if any move is illegal. */
41
+ function applyLineSans(startFen, sans) {
42
+ const chess = new Chess(startFen);
43
+ const entries = [];
44
+ for (const san of sans) {
45
+ try {
46
+ const move = chess.move(san);
47
+ if (!move)
48
+ return null;
49
+ entries.push({ fen: chess.fen(), lastSan: move.san });
50
+ }
51
+ catch (_a) {
52
+ return null;
53
+ }
54
+ }
55
+ return { fen: chess.fen(), entries };
56
+ }
57
+ /** Apply a UCI move to a FEN; returns null if illegal. */
58
+ function fenAfterUci(fen, uci) {
59
+ const chess = new Chess(fen);
60
+ const from = uci.slice(0, 2);
61
+ const to = uci.slice(2, 4);
62
+ const promotion = uci.length > 4 ? uci[4] : undefined;
63
+ try {
64
+ const move = chess.move({ from, to, promotion });
65
+ if (!move)
66
+ return null;
67
+ return chess.fen();
68
+ }
69
+ catch (_a) {
70
+ return null;
71
+ }
72
+ }
73
+ function whiteScorePercent(whiteWins, draws, blackWins) {
74
+ const total = whiteWins + draws + blackWins;
75
+ if (total === 0)
76
+ return null;
77
+ return Math.round((100 * (whiteWins + 0.5 * draws)) / total);
78
+ }
79
+ /** Format SAN plies from the starting position as `1.d4 Nf6 2.c4 e6`. */
80
+ function formatNumberedLineSans(sans) {
81
+ if (sans.length === 0) {
82
+ return "";
83
+ }
84
+ const parts = [];
85
+ for (let i = 0; i < sans.length; i += 2) {
86
+ const moveNumber = i / 2 + 1;
87
+ const white = sans[i];
88
+ const black = sans[i + 1];
89
+ parts.push(black ? `${moveNumber}.${white} ${black}` : `${moveNumber}.${white}`);
90
+ }
91
+ return parts.join(" ");
92
+ }
93
+
94
+ const EXPLORER_PREFETCH_CHILD_COUNT = 4;
95
+ const EXPLORER_DEFAULT_VARIATION_LINE_COUNT = 8;
96
+ const EXPLORER_DEFAULT_VARIATION_DEPTH = 4;
97
+ function gamesSessionKey(params) {
98
+ var _a, _b, _c, _d, _e;
99
+ return JSON.stringify({
100
+ fen: normalizeFen(params.fen),
101
+ uci: (_a = params.uci) !== null && _a !== void 0 ? _a : "",
102
+ sources: (_c = (_b = params.sources) === null || _b === void 0 ? void 0 : _b.slice().sort().join(",")) !== null && _c !== void 0 ? _c : "",
103
+ limit: (_d = params.limit) !== null && _d !== void 0 ? _d : 50,
104
+ offset: (_e = params.offset) !== null && _e !== void 0 ? _e : 0,
105
+ });
106
+ }
107
+ function variationsSessionKey(fen, lineCount = EXPLORER_DEFAULT_VARIATION_LINE_COUNT, lineDepth = EXPLORER_DEFAULT_VARIATION_DEPTH) {
108
+ return JSON.stringify({
109
+ fen: normalizeFen(fen),
110
+ mode: "variations",
111
+ lineCount,
112
+ lineDepth,
113
+ });
114
+ }
115
+ const sessionGames = new Map();
116
+ const sessionVariations = new Map();
117
+ const sessionCacheListeners = new Set();
118
+ function subscribeExplorerSessionCache(listener) {
119
+ sessionCacheListeners.add(listener);
120
+ return () => {
121
+ sessionCacheListeners.delete(listener);
122
+ };
123
+ }
124
+ function notifySessionCacheListeners() {
125
+ for (const listener of sessionCacheListeners) {
126
+ listener();
127
+ }
128
+ }
129
+ function peekSessionGames(key) {
130
+ return sessionGames.get(key);
131
+ }
132
+ function setSessionGames(key, data) {
133
+ sessionGames.set(key, data);
134
+ notifySessionCacheListeners();
135
+ }
136
+ function peekSessionVariations(key) {
137
+ return sessionVariations.get(key);
138
+ }
139
+ function setSessionVariations(key, lines) {
140
+ sessionVariations.set(key, lines);
141
+ notifySessionCacheListeners();
142
+ }
143
+
144
+ /** Push start-hub payloads into the explorer session cache (used by app preload). */
145
+ function seedExplorerStartSession(games, variations) {
146
+ setSessionGames(gamesSessionKey({ fen: EXPLORER_START_FEN }), games);
147
+ setSessionVariations(variationsSessionKey(EXPLORER_START_FEN), variations.lines);
148
+ }
149
+
12
150
  /**
13
151
  * Scaffold UI — proves core wiring and Rollup build.
14
152
  * Replace with game replay once requirements are defined.
@@ -47,15 +185,8 @@ const thStyle = {
47
185
  const tdStyle = {
48
186
  padding: "3px 8px",
49
187
  borderBottom: "1px solid rgba(128,128,128,0.12)",
50
- whiteSpace: "nowrap",
51
- };
52
- const sectionTitleStyle = {
53
- margin: 0,
54
- fontSize: 12,
55
- fontWeight: 600,
56
- padding: "6px 8px",
57
- borderBottom: "1px solid rgba(128,128,128,0.2)",
58
- background: "rgba(128,128,128,0.06)",
188
+ whiteSpace: "normal",
189
+ wordBreak: "break-word",
59
190
  };
60
191
  /** Full-width shell: board | reference — no wrap, no footer below. */
61
192
  const referenceShellStyle = {
@@ -104,19 +235,6 @@ const referencePanelStyle = {
104
235
  borderLeft: "1px solid rgba(128,128,128,0.3)",
105
236
  boxSizing: "border-box",
106
237
  };
107
- const referenceTabBarStyle = {
108
- flex: "0 0 auto",
109
- display: "flex",
110
- alignItems: "center",
111
- gap: 4,
112
- padding: "6px 10px",
113
- borderBottom: "1px solid rgba(128,128,128,0.35)",
114
- background: "rgba(128,128,128,0.08)",
115
- fontSize: 12,
116
- };
117
- const referenceTabLabelStyle = {
118
- padding: "2px 8px",
119
- };
120
238
  const moveStatsSectionStyle = {
121
239
  flex: "0 1 auto",
122
240
  maxHeight: "38%",
@@ -134,9 +252,6 @@ const variationsStripStyle = {
134
252
  fontSize: 12,
135
253
  background: "rgba(128,128,128,0.04)",
136
254
  };
137
- const variationsTabStyle = {
138
- cursor: "default",
139
- };
140
255
  const gamesSectionStyle = {
141
256
  flex: "1 1 auto",
142
257
  display: "flex",
@@ -175,9 +290,11 @@ const statusInlineStyle = {
175
290
  };
176
291
 
177
292
  /** Right column shell: tab bar + stacked sections (no page footer below). */
178
- const DefaultReferencePanel = ({ theme, status, moveStats, variationsStrip, gamesPanel, }) => (jsxs("div", { style: Object.assign(Object.assign({}, referencePanelStyle), panelStyle), "data-theme": theme, children: [jsx("div", { style: referenceTabBarStyle, children: jsx("span", { style: Object.assign(Object.assign({}, referenceTabLabelStyle), { fontWeight: 600 }), children: "Reference" }) }), status, jsx("div", { style: { flex: "0 0 auto", minHeight: 0 }, children: moveStats }), variationsStrip, gamesPanel] }));
293
+ const DefaultReferencePanel = ({ theme, fillHeight = true, status, moveStats, variationsStrip, gamesPanel, }) => (jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, referencePanelStyle), panelStyle), (fillHeight
294
+ ? {}
295
+ : { height: "auto", minHeight: 0, overflow: "visible" })), "data-theme": theme, children: [status, jsx("div", { style: { flex: "0 0 auto", minHeight: 0 }, children: moveStats }), variationsStrip, gamesPanel] }));
179
296
 
180
- const DefaultBoardNav = ({ canGoBack, canGoForward, onBack, onForward, }) => (jsxs("div", { style: boardNavStyle, children: [jsx("button", { type: "button", style: boardNavButtonStyle, onClick: onBack, disabled: !canGoBack, "aria-label": "Previous position", title: "Back", children: "\u25C0" }), jsx("button", { type: "button", style: boardNavButtonStyle, onClick: onForward, disabled: !canGoForward, "aria-label": "Next position", title: "Forward", children: "\u25B6" })] }));
297
+ const DefaultBoardNav = ({ canGoBack, canGoForward, onBack, onForward, onFlipBoard, }) => (jsxs("div", { style: boardNavStyle, children: [jsx("button", { type: "button", style: boardNavButtonStyle, onClick: onBack, disabled: !canGoBack, "aria-label": "Previous position", title: "Back", children: "\u25C0" }), jsx("button", { type: "button", style: boardNavButtonStyle, onClick: onForward, disabled: !canGoForward, "aria-label": "Next position", title: "Forward", children: "\u25B6" }), jsx("button", { type: "button", style: boardNavButtonStyle, onClick: onFlipBoard, "aria-label": "Flip board", title: "Flip board", children: "\u21C5" })] }));
181
298
  const defaultRenderBoardNav = (props) => (jsx(DefaultBoardNav, Object.assign({}, props)));
182
299
 
183
300
  const ExplorerStatusBanner = ({ error, loading, }) => {
@@ -186,97 +303,15 @@ const ExplorerStatusBanner = ({ error, loading, }) => {
186
303
  return (jsxs(Fragment, { children: [error && (jsx("p", { style: Object.assign(Object.assign({}, statusInlineStyle), { color: "#e57373" }), role: "alert", children: error })), loading && (jsx("p", { style: Object.assign(Object.assign({}, statusInlineStyle), { opacity: 0.7 }), children: "Loading\u2026" }))] }));
187
304
  };
188
305
 
189
- /** Match endchess-backend / mass-games-import (first four FEN fields). */
190
- function normalizeFen(fen) {
191
- const parts = fen.trim().split(/\s+/);
192
- if (parts.length < 4) {
193
- throw new Error(`Invalid FEN: ${fen}`);
194
- }
195
- return `${parts[0]} ${parts[1]} ${parts[2]} ${parts[3]}`;
196
- }
197
- /** Legal move from a board drag/drop (react-chessboard piece string, e.g. wQ). */
198
- function applyBoardMove(fen, sourceSquare, targetSquare, piece) {
199
- var _a, _b;
200
- const chess = new Chess(fen);
201
- const pieceType = (_a = piece[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
202
- const legal = chess
203
- .moves({ square: sourceSquare, verbose: true })
204
- .find((move) => move.to === targetSquare &&
205
- (!move.promotion || move.promotion === pieceType));
206
- if (!legal)
207
- return null;
208
- const played = chess.move({
209
- from: legal.from,
210
- to: legal.to,
211
- promotion: legal.promotion,
212
- });
213
- if (!played)
214
- return null;
215
- const uci = `${played.from}${played.to}${(_b = played.promotion) !== null && _b !== void 0 ? _b : ""}`;
216
- return { fen: chess.fen(), uci, san: played.san };
217
- }
218
- /** Play a SAN sequence from a start FEN; returns null if any move is illegal. */
219
- function applyLineSans(startFen, sans) {
220
- const chess = new Chess(startFen);
221
- const entries = [];
222
- for (const san of sans) {
223
- try {
224
- const move = chess.move(san);
225
- if (!move)
226
- return null;
227
- entries.push({ fen: chess.fen(), lastSan: move.san });
228
- }
229
- catch (_a) {
230
- return null;
231
- }
232
- }
233
- return { fen: chess.fen(), entries };
234
- }
235
- /** Apply a UCI move to a FEN; returns null if illegal. */
236
- function fenAfterUci(fen, uci) {
237
- const chess = new Chess(fen);
238
- const from = uci.slice(0, 2);
239
- const to = uci.slice(2, 4);
240
- const promotion = uci.length > 4 ? uci[4] : undefined;
241
- try {
242
- const move = chess.move({ from, to, promotion });
243
- if (!move)
244
- return null;
245
- return chess.fen();
246
- }
247
- catch (_a) {
248
- return null;
249
- }
250
- }
251
- function whiteScorePercent(whiteWins, draws, blackWins) {
252
- const total = whiteWins + draws + blackWins;
253
- if (total === 0)
254
- return null;
255
- return Math.round((100 * (whiteWins + 0.5 * draws)) / total);
256
- }
257
-
258
- const MoveStatsTable = ({ moves, selectedUci, onMoveSelect, }) => (jsxs("div", { style: moveStatsSectionStyle, children: [jsx("div", { style: sectionTitleStyle, children: "Moves" }), jsxs("table", { style: tableStyle, children: [jsx("thead", { children: jsxs("tr", { children: [jsx("th", { style: thStyle, children: "Move" }), jsx("th", { style: thStyle, children: "Games" }), jsx("th", { style: Object.assign(Object.assign({}, thStyle), { textAlign: "right" }), children: "Score %" }), jsx("th", { style: Object.assign(Object.assign({}, thStyle), { textAlign: "right" }), children: "Avg" })] }) }), jsx("tbody", { children: moves.map((move) => {
259
- var _a;
260
- const score = whiteScorePercent(move.whiteWins, move.draws, move.blackWins);
261
- const selected = move.uci === selectedUci;
262
- return (jsxs("tr", { onClick: () => onMoveSelect(move), style: {
263
- cursor: "pointer",
264
- background: selected ? "rgba(100,149,237,0.25)" : undefined,
265
- }, children: [jsx("td", { style: tdStyle, children: move.san }), jsx("td", { style: tdStyle, children: move.games.toLocaleString() }), jsx("td", { style: Object.assign(Object.assign({}, tdStyle), { textAlign: "right" }), children: score !== null ? `${score}` : "—" }), jsx("td", { style: Object.assign(Object.assign({}, tdStyle), { textAlign: "right" }), children: (_a = move.avgElo) !== null && _a !== void 0 ? _a : "—" })] }, move.uci));
266
- }) })] })] }));
267
-
268
- const inputStyle = {
269
- width: 64,
270
- padding: "2px 6px",
271
- fontSize: 12,
272
- };
273
- const rowStyle = {
274
- display: "flex",
275
- flexWrap: "wrap",
276
- gap: 6,
277
- alignItems: "center",
278
- };
279
- const EloRangeFilter = ({ minElo, maxElo, defaultMinElo, defaultMaxElo, onMinEloChange, onMaxEloChange, }) => (jsxs("div", { style: rowStyle, children: [jsx("span", { style: { fontWeight: 600 }, children: "Filter" }), jsx("input", { type: "number", value: minElo, min: 0, max: 3000, onChange: (e) => onMinEloChange(Number(e.target.value) || defaultMinElo), style: inputStyle, "aria-label": "Minimum Elo" }), jsx("span", { children: "\u2013" }), jsx("input", { type: "number", value: maxElo, min: 0, max: 3000, onChange: (e) => onMaxEloChange(Number(e.target.value) || defaultMaxElo), style: inputStyle, "aria-label": "Maximum Elo" })] }));
306
+ const MoveStatsTable = ({ moves, selectedUci, onMoveSelect, }) => (jsx("div", { style: moveStatsSectionStyle, children: jsxs("table", { style: tableStyle, children: [jsx("thead", { children: jsxs("tr", { children: [jsx("th", { style: thStyle, children: "Move" }), jsx("th", { style: thStyle, children: "Games" }), jsx("th", { style: Object.assign(Object.assign({}, thStyle), { textAlign: "right" }), children: "Score %" }), jsx("th", { style: Object.assign(Object.assign({}, thStyle), { textAlign: "right" }), children: "Avg" })] }) }), jsx("tbody", { children: moves.map((move) => {
307
+ var _a;
308
+ const score = whiteScorePercent(move.whiteWins, move.draws, move.blackWins);
309
+ const selected = move.uci === selectedUci;
310
+ return (jsxs("tr", { onClick: () => onMoveSelect(move), style: {
311
+ cursor: "pointer",
312
+ background: selected ? "rgba(100,149,237,0.25)" : undefined,
313
+ }, children: [jsx("td", { style: tdStyle, children: move.san }), jsx("td", { style: tdStyle, children: move.games.toLocaleString() }), jsx("td", { style: Object.assign(Object.assign({}, tdStyle), { textAlign: "right" }), children: score !== null ? `${score}` : "—" }), jsx("td", { style: Object.assign(Object.assign({}, tdStyle), { textAlign: "right" }), children: (_a = move.avgElo) !== null && _a !== void 0 ? _a : "—" })] }, move.uci));
314
+ }) })] }) }));
280
315
 
281
316
  const linkStyle = {
282
317
  color: "inherit",
@@ -284,12 +319,12 @@ const linkStyle = {
284
319
  textUnderlineOffset: 2,
285
320
  };
286
321
  const LichessGameLink = ({ url, name }) => (jsx("a", { href: url, target: "_blank", rel: "noopener noreferrer", style: linkStyle, children: name }));
287
- const GamesTable = ({ games, onGameSelect }) => (jsxs("table", { style: tableStyle, children: [jsx("thead", { children: jsxs("tr", { children: [jsx("th", { style: thStyle, children: "White" }), jsx("th", { style: thStyle, children: "Elo" }), jsx("th", { style: thStyle, children: "Black" }), jsx("th", { style: thStyle, children: "Elo" }), jsx("th", { style: thStyle, children: "Result" }), jsx("th", { style: thStyle, children: "Source" }), onGameSelect && jsx("th", { style: thStyle })] }) }), jsx("tbody", { children: games.length === 0 ? (jsx("tr", { children: jsx("td", { colSpan: onGameSelect ? 7 : 6, style: Object.assign(Object.assign({}, tdStyle), { opacity: 0.7, fontStyle: "italic" }), children: "No games match this position and filter. Widen the Elo range or turn off Top games." }) })) : (games.map((g) => (jsxs("tr", { children: [jsx("td", { style: tdStyle, children: jsx(LichessGameLink, { url: g.url, name: g.white }) }), jsx("td", { style: tdStyle, children: g.whiteElo }), jsx("td", { style: tdStyle, children: jsx(LichessGameLink, { url: g.url, name: g.black }) }), jsx("td", { style: tdStyle, children: g.blackElo }), jsx("td", { style: tdStyle, children: g.result }), jsx("td", { style: tdStyle, children: g.source === "twic" ? "Master" : "Lichess" }), onGameSelect && (jsx("td", { style: tdStyle, children: jsx("button", { type: "button", onClick: () => onGameSelect(g), children: "Train" }) }))] }, g.gameId)))) })] }));
322
+ const GamesTable = ({ games, onGameSelect }) => (jsxs("table", { style: tableStyle, children: [jsx("thead", { children: jsxs("tr", { children: [jsx("th", { style: thStyle, children: "White" }), jsx("th", { style: thStyle, children: "Elo" }), jsx("th", { style: thStyle, children: "Black" }), jsx("th", { style: thStyle, children: "Elo" }), jsx("th", { style: thStyle, children: "Result" }), jsx("th", { style: thStyle, children: "Source" }), onGameSelect && jsx("th", { style: thStyle })] }) }), jsx("tbody", { children: games.length === 0 ? (jsx("tr", { children: jsx("td", { colSpan: onGameSelect ? 7 : 6, style: Object.assign(Object.assign({}, tdStyle), { opacity: 0.7, fontStyle: "italic" }), children: "No games match this position and source filter." }) })) : (games.map((g) => (jsxs("tr", { children: [jsx("td", { style: tdStyle, children: jsx(LichessGameLink, { url: g.url, name: g.white }) }), jsx("td", { style: tdStyle, children: g.whiteElo }), jsx("td", { style: tdStyle, children: jsx(LichessGameLink, { url: g.url, name: g.black }) }), jsx("td", { style: tdStyle, children: g.blackElo }), jsx("td", { style: tdStyle, children: g.result }), jsx("td", { style: tdStyle, children: g.source === "twic" ? "Master" : "Lichess" }), onGameSelect && (jsx("td", { style: tdStyle, children: jsx("button", { type: "button", onClick: () => onGameSelect(g), children: "Train" }) }))] }, g.gameId)))) })] }));
288
323
 
289
324
  const mainLineTitleStyle = {
290
325
  fontWeight: 600,
291
326
  };
292
- const PositionGamesPanel = ({ games, lineLabel, minElo, maxElo, defaultMinElo, defaultMaxElo, topOnly, sources, onMinEloChange, onMaxEloChange, onTopOnlyChange, onSourcesChange, onGameSelect, }) => {
327
+ const PositionGamesPanel = ({ games, lineLabel, lineSans: _lineSans, sources, onSourcesChange, onGameSelect, }) => {
293
328
  const toggleSource = (source) => {
294
329
  if (sources.includes(source)) {
295
330
  if (sources.length === 1) {
@@ -300,7 +335,7 @@ const PositionGamesPanel = ({ games, lineLabel, minElo, maxElo, defaultMinElo, d
300
335
  }
301
336
  onSourcesChange([...sources, source]);
302
337
  };
303
- return (jsxs("div", { style: gamesSectionStyle, children: [jsx("div", { style: gamesHeaderStyle, children: lineLabel ? (jsxs(Fragment, { children: [jsx("span", { style: mainLineTitleStyle, children: "Main line: " }), lineLabel, jsxs("span", { style: { opacity: 0.75 }, children: [" (", games.length, " games)"] })] })) : (jsxs("span", { children: ["Games ", jsxs("span", { style: { opacity: 0.75 }, children: ["(", games.length, ")"] })] })) }), jsx("div", { style: gamesScrollStyle, children: jsx(GamesTable, { games: games, onGameSelect: onGameSelect }) }), jsxs("div", { style: gamesToolbarStyle, children: [jsx(EloRangeFilter, { minElo: minElo, maxElo: maxElo, defaultMinElo: defaultMinElo, defaultMaxElo: defaultMaxElo, onMinEloChange: onMinEloChange, onMaxEloChange: onMaxEloChange }), jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [jsx("input", { type: "checkbox", checked: topOnly, onChange: (e) => onTopOnlyChange(e.target.checked) }), "Top games"] }), jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [jsx("input", { type: "checkbox", checked: sources.includes("lichess"), onChange: () => toggleSource("lichess") }), "Lichess"] }), jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [jsx("input", { type: "checkbox", checked: sources.includes("twic"), onChange: () => toggleSource("twic") }), "Master"] })] })] }));
338
+ return (jsxs("div", { style: gamesSectionStyle, children: [jsx("div", { style: gamesHeaderStyle, children: lineLabel ? (jsxs(Fragment, { children: [jsx("span", { style: mainLineTitleStyle, children: "Main line: " }), lineLabel, jsxs("span", { style: { opacity: 0.75 }, children: [" (", games.length, " games)"] })] })) : (jsxs("span", { children: ["Games ", jsxs("span", { style: { opacity: 0.75 }, children: ["(", games.length, ")"] })] })) }), jsx("div", { style: gamesScrollStyle, children: jsx(GamesTable, { games: games, onGameSelect: onGameSelect }) }), jsxs("div", { style: gamesToolbarStyle, children: [jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [jsx("input", { type: "checkbox", checked: sources.includes("lichess"), onChange: () => toggleSource("lichess") }), "Lichess"] }), jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [jsx("input", { type: "checkbox", checked: sources.includes("twic"), onChange: () => toggleSource("twic") }), "Master"] })] })] }));
304
339
  };
305
340
 
306
341
  const DefaultReferenceLayout = ({ board, referencePanel, }) => (jsxs("div", { style: referenceShellStyle, children: [jsx("div", { style: boardColumnStyle, children: board }), referencePanel] }));
@@ -313,34 +348,37 @@ function isVariationLineActive(line, selectedLineKey, forwardSans = []) {
313
348
  if (forwardSans.length === 0) {
314
349
  return false;
315
350
  }
316
- return forwardSans.every((san, index) => { var _a; return ((_a = line.moves[index]) === null || _a === void 0 ? void 0 : _a.san) === san; });
351
+ const moves = line.moves;
352
+ if (!Array.isArray(moves)) {
353
+ return false;
354
+ }
355
+ return forwardSans.every((san, index) => { var _a; return ((_a = moves[index]) === null || _a === void 0 ? void 0 : _a.san) === san; });
317
356
  }
318
357
 
319
- const tabButtonStyle = (active) => (Object.assign(Object.assign({}, variationsTabStyle), { fontWeight: active ? 600 : 400, opacity: active ? 1 : 0.55, cursor: "pointer", border: "none", background: "transparent", padding: 0, color: "inherit", font: "inherit" }));
320
- const DefaultVariationsStrip = ({ theme, tab, onTabChange, lines, loading, selectedLineKey, forwardSans, onLineSelect, }) => (jsxs("div", { style: Object.assign(Object.assign({}, variationsStripStyle), { flexDirection: "column", alignItems: "stretch", gap: 6 }), "data-theme": theme, children: [jsxs("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [jsx("button", { type: "button", style: tabButtonStyle(tab === "variations"), onClick: () => onTabChange("variations"), children: "Variations" }), jsx("button", { type: "button", style: tabButtonStyle(tab === "popularity"), onClick: () => onTabChange("popularity"), children: "Popularity" }), jsx("button", { type: "button", style: tabButtonStyle(tab === "endgames"), onClick: () => onTabChange("endgames"), children: "Endgames" })] }), jsx("div", { style: {
321
- minWidth: 0,
322
- maxHeight: 132,
323
- overflow: "auto",
324
- }, children: tab === "endgames" ? (jsx("span", { style: { fontSize: 11, opacity: 0.55 }, children: "Coming soon" })) : loading ? (jsx("span", { style: { fontSize: 11, opacity: 0.55 }, children: "Loading\u2026" })) : lines.length === 0 ? (jsx("span", { style: { fontSize: 11, opacity: 0.55 }, children: "No lines" })) : (lines.map((line) => {
325
- var _a, _b;
326
- const active = isVariationLineActive(line, selectedLineKey, forwardSans);
327
- return (jsxs("button", { type: "button", onClick: () => onLineSelect(line), style: {
328
- display: "flex",
329
- width: "100%",
330
- gap: 12,
331
- alignItems: "baseline",
332
- border: "none",
333
- background: "transparent",
334
- padding: "2px 0",
335
- cursor: "pointer",
336
- textAlign: "left",
337
- color: active ? "#2e7d32" : "inherit",
338
- font: "inherit",
339
- fontSize: 12,
340
- }, children: [jsx("span", { style: { flex: 1, minWidth: 0 }, children: line.label }), jsxs("span", { children: ["N = ", line.games.toLocaleString()] }), jsxs("span", { children: [(_a = line.scorePercent) !== null && _a !== void 0 ? _a : "—", "%"] }), jsx("span", { children: line.lastPlayedYear
341
- ? `Last played ${line.lastPlayedYear}`
342
- : "Last played —" }), jsx("span", { children: (_b = line.avgElo) !== null && _b !== void 0 ? _b : "—" })] }, line.key));
343
- })) })] }));
358
+ const DefaultVariationsStrip = ({ theme, lines, loading, selectedLineKey, forwardSans, onLineSelect, }) => (jsx("div", { style: Object.assign(Object.assign({}, variationsStripStyle), { flexDirection: "column", alignItems: "stretch", gap: 6 }), "data-theme": theme, children: jsx("div", { style: {
359
+ minWidth: 0,
360
+ maxHeight: 132,
361
+ overflow: "auto",
362
+ }, children: loading ? (jsx("span", { style: { fontSize: 11, opacity: 0.55 }, children: "Loading\u2026" })) : lines.length === 0 ? (jsx("span", { style: { fontSize: 11, opacity: 0.55 }, children: "No lines" })) : (lines.map((line) => {
363
+ var _a, _b;
364
+ const active = isVariationLineActive(line, selectedLineKey, forwardSans);
365
+ return (jsxs("button", { type: "button", onClick: () => onLineSelect(line), style: {
366
+ display: "flex",
367
+ width: "100%",
368
+ gap: 12,
369
+ alignItems: "baseline",
370
+ border: "none",
371
+ background: "transparent",
372
+ padding: "2px 0",
373
+ cursor: "pointer",
374
+ textAlign: "left",
375
+ color: active ? "#2e7d32" : "inherit",
376
+ font: "inherit",
377
+ fontSize: 12,
378
+ }, children: [jsx("span", { style: { flex: 1, minWidth: 0 }, children: line.label }), jsxs("span", { children: ["N = ", line.games.toLocaleString()] }), jsxs("span", { children: [(_a = line.scorePercent) !== null && _a !== void 0 ? _a : "—", "%"] }), jsx("span", { children: line.lastPlayedYear
379
+ ? `Last played ${line.lastPlayedYear}`
380
+ : "Last played —" }), jsx("span", { children: (_b = line.avgElo) !== null && _b !== void 0 ? _b : "—" })] }, line.key));
381
+ })) }) }));
344
382
  const defaultRenderVariationsStrip = (props) => jsx(DefaultVariationsStrip, Object.assign({}, props));
345
383
 
346
384
  const defaultRenderStatus = (props) => (jsx(ExplorerStatusBanner, Object.assign({}, props)));
@@ -381,11 +419,103 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
381
419
 
382
420
  const ALL_GAME_SOURCES = ["lichess", "twic"];
383
421
 
384
- function usePositionHistory(initialFen) {
385
- const [history, setHistory] = useState([
386
- { fen: initialFen },
422
+ function useExplorerPrefetch({ fen, moves, positionReady, sources, fetchPositionGames, fetchPositionVariations, childCount = EXPLORER_PREFETCH_CHILD_COUNT, }) {
423
+ useEffect(() => {
424
+ if (!positionReady || moves.length === 0) {
425
+ return;
426
+ }
427
+ let cancelled = false;
428
+ let idleHandle;
429
+ let deferTimer;
430
+ const prefetchChildren = () => {
431
+ if (cancelled) {
432
+ return;
433
+ }
434
+ const sourcesParam = sources.length < ALL_GAME_SOURCES.length ? sources : undefined;
435
+ for (const move of moves.slice(0, childCount)) {
436
+ const childFen = fenAfterUci(fen, move.uci);
437
+ if (!childFen) {
438
+ continue;
439
+ }
440
+ const gamesKey = gamesSessionKey({
441
+ fen: childFen,
442
+ sources: sourcesParam,
443
+ });
444
+ if (!peekSessionGames(gamesKey)) {
445
+ void fetchPositionGames({ fen: childFen, sources: sourcesParam })
446
+ .then((games) => {
447
+ if (!cancelled) {
448
+ setSessionGames(gamesKey, games);
449
+ }
450
+ })
451
+ .catch(() => undefined);
452
+ }
453
+ if (!fetchPositionVariations) {
454
+ continue;
455
+ }
456
+ const variationsKey = variationsSessionKey(childFen);
457
+ if (!peekSessionVariations(variationsKey)) {
458
+ void fetchPositionVariations({
459
+ fen: childFen,
460
+ mode: "variations",
461
+ lineCount: EXPLORER_DEFAULT_VARIATION_LINE_COUNT,
462
+ depth: EXPLORER_DEFAULT_VARIATION_DEPTH,
463
+ })
464
+ .then((result) => {
465
+ var _a;
466
+ if (!cancelled) {
467
+ setSessionVariations(variationsKey, (_a = result === null || result === void 0 ? void 0 : result.lines) !== null && _a !== void 0 ? _a : []);
468
+ }
469
+ })
470
+ .catch(() => undefined);
471
+ }
472
+ }
473
+ };
474
+ if (typeof window.requestIdleCallback === "function") {
475
+ idleHandle = window.requestIdleCallback(prefetchChildren, {
476
+ timeout: 1500,
477
+ });
478
+ }
479
+ else {
480
+ deferTimer = setTimeout(prefetchChildren, 750);
481
+ }
482
+ return () => {
483
+ cancelled = true;
484
+ if (idleHandle !== undefined) {
485
+ window.cancelIdleCallback(idleHandle);
486
+ }
487
+ if (deferTimer !== undefined) {
488
+ clearTimeout(deferTimer);
489
+ }
490
+ };
491
+ }, [
492
+ fen,
493
+ moves,
494
+ positionReady,
495
+ sources,
496
+ fetchPositionGames,
497
+ fetchPositionVariations,
498
+ childCount,
387
499
  ]);
388
- const [historyIndex, setHistoryIndex] = useState(0);
500
+ }
501
+
502
+ function initialHistoryState(initialFen, initialLineSans) {
503
+ if (!(initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length)) {
504
+ return { history: [{ fen: initialFen }], historyIndex: 0 };
505
+ }
506
+ const result = applyLineSans(initialFen, initialLineSans);
507
+ if (!result) {
508
+ return { history: [{ fen: initialFen }], historyIndex: 0 };
509
+ }
510
+ return {
511
+ history: [{ fen: initialFen }, ...result.entries],
512
+ historyIndex: result.entries.length,
513
+ };
514
+ }
515
+ function usePositionHistory(initialFen, initialLineSans) {
516
+ var _a, _b;
517
+ const [history, setHistory] = useState(() => initialHistoryState(initialFen, initialLineSans).history);
518
+ const [historyIndex, setHistoryIndex] = useState(() => initialHistoryState(initialFen, initialLineSans).historyIndex);
389
519
  const canGoBack = historyIndex > 0;
390
520
  const canGoForward = historyIndex < history.length - 1;
391
521
  const pushEntry = useCallback((fen, lastSan) => {
@@ -413,6 +543,20 @@ function usePositionHistory(initialFen) {
413
543
  setHistoryIndex(nextIndex);
414
544
  return entry;
415
545
  }, [history, historyIndex]);
546
+ const goFirst = useCallback(() => {
547
+ if (historyIndex <= 0)
548
+ return null;
549
+ const entry = history[0];
550
+ setHistoryIndex(0);
551
+ return entry;
552
+ }, [history, historyIndex]);
553
+ const goLast = useCallback(() => {
554
+ if (historyIndex >= history.length - 1)
555
+ return null;
556
+ const entry = history[history.length - 1];
557
+ setHistoryIndex(history.length - 1);
558
+ return entry;
559
+ }, [history, historyIndex]);
416
560
  const resetHistory = useCallback((fen) => {
417
561
  const entry = { fen };
418
562
  setHistory([entry]);
@@ -437,39 +581,54 @@ function usePositionHistory(initialFen) {
437
581
  .slice(historyIndex + 1)
438
582
  .map((entry) => entry.lastSan)
439
583
  .filter((san) => Boolean(san));
584
+ const currentFen = (_b = (_a = history[historyIndex]) === null || _a === void 0 ? void 0 : _a.fen) !== null && _b !== void 0 ? _b : initialFen;
585
+ const replaceLineEntries = useCallback((startFen, entries) => {
586
+ setHistory([{ fen: startFen }, ...entries]);
587
+ setHistoryIndex(entries.length);
588
+ }, []);
440
589
  return {
441
590
  canGoBack,
442
591
  canGoForward,
443
592
  lineSans,
444
593
  forwardSans,
594
+ currentFen,
445
595
  pushEntry,
446
596
  pushEntries,
597
+ replaceLineEntries,
447
598
  goBack,
448
599
  goForward,
600
+ goFirst,
601
+ goLast,
449
602
  resetHistory,
450
603
  };
451
604
  }
452
605
 
453
- function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, defaultMinElo, defaultMaxElo, }) {
606
+ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, }) {
607
+ var _a, _b, _c;
454
608
  const initialFen = fenProp !== null && fenProp !== void 0 ? fenProp : EXPLORER_START_FEN;
455
- const [fen, setFen] = useState(initialFen);
456
- /** Board display FEN; may lead {@link fen} while a variation line is animating. */
457
- const [boardFen, setBoardFen] = useState(initialFen);
609
+ const bootstrappedRef = useRef(initialHistoryState(initialFen, initialLineSans));
610
+ const bootstrappedFen = (_b = (_a = bootstrappedRef.current.history[bootstrappedRef.current.historyIndex]) === null || _a === void 0 ? void 0 : _a.fen) !== null && _b !== void 0 ? _b : initialFen;
611
+ const initialGamesKey = gamesSessionKey({ fen: bootstrappedFen });
612
+ const initialCachedGames = peekSessionGames(initialGamesKey);
613
+ const [fen, setFen] = useState(bootstrappedFen);
614
+ /** Board display FEN; may lead {@link queryFen} while a variation line is animating. */
615
+ const [boardFen, setBoardFen] = useState(bootstrappedFen);
458
616
  const [position, setPosition] = useState(null);
459
- const [games, setGames] = useState(null);
617
+ const [games, setGames] = useState(() => initialCachedGames !== null && initialCachedGames !== void 0 ? initialCachedGames : null);
460
618
  /** Filter games to those that played this UCI from the current FEN (optional). */
461
619
  const [gamesMoveFilterUci, setGamesMoveFilterUci] = useState();
462
- const [minElo, setMinElo] = useState(defaultMinElo);
463
- const [maxElo, setMaxElo] = useState(defaultMaxElo);
464
- const [topOnly, setTopOnly] = useState(false);
465
620
  const [sources, setSources] = useState([...ALL_GAME_SOURCES]);
466
621
  const [loading, setLoading] = useState(false);
622
+ const [gamesLoading, setGamesLoading] = useState(() => initialCachedGames === undefined);
623
+ /** FEN that {@link position} was loaded for (may lag {@link fen} while fetching). */
624
+ const [loadedPositionFen, setLoadedPositionFen] = useState(null);
625
+ const [loadedPositionFilterKey, setLoadedPositionFilterKey] = useState(null);
467
626
  const [error, setError] = useState(null);
468
- const [variationsTab, setVariationsTab] = useState("variations");
469
627
  const [selectedVariationKey, setSelectedVariationKey] = useState();
470
628
  const variationAnimationTimerRef = useRef(null);
471
629
  const isAnimatingVariationRef = useRef(false);
472
- const readyForLineSyncRef = useRef(false);
630
+ const readyForLineSyncRef = useRef(bootstrappedRef.current.historyIndex > 0 ||
631
+ ((_c = initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length) !== null && _c !== void 0 ? _c : 0) === 0);
473
632
  const cancelVariationAnimation = useCallback(() => {
474
633
  if (variationAnimationTimerRef.current !== null) {
475
634
  clearTimeout(variationAnimationTimerRef.current);
@@ -477,7 +636,17 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
477
636
  }
478
637
  isAnimatingVariationRef.current = false;
479
638
  }, []);
480
- const { canGoBack, canGoForward, lineSans, forwardSans, pushEntry, pushEntries, goBack, goForward, resetHistory, } = usePositionHistory(initialFen);
639
+ const { canGoBack, canGoForward, lineSans, forwardSans, currentFen, pushEntry, pushEntries, replaceLineEntries, goBack, goForward, goFirst, goLast, resetHistory, } = usePositionHistory(initialFen, initialLineSans);
640
+ /** FEN used for explorer API queries — follows move history, not animation frames. */
641
+ const queryFen = currentFen;
642
+ const initialLineKey = useMemo(() => { var _a; return (_a = initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.join("|")) !== null && _a !== void 0 ? _a : ""; }, [initialLineSans]);
643
+ const expectedLineFen = useMemo(() => {
644
+ var _a, _b;
645
+ if (!(initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length)) {
646
+ return EXPLORER_START_FEN;
647
+ }
648
+ return ((_b = (_a = applyLineSans(EXPLORER_START_FEN, initialLineSans)) === null || _a === void 0 ? void 0 : _a.fen) !== null && _b !== void 0 ? _b : EXPLORER_START_FEN);
649
+ }, [initialLineKey, initialLineSans]);
481
650
  const applyNavigation = useCallback((nextFen, clearMoveFilter = true) => {
482
651
  setFen(nextFen);
483
652
  setBoardFen(nextFen);
@@ -486,39 +655,72 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
486
655
  }
487
656
  onFenChange === null || onFenChange === void 0 ? void 0 : onFenChange(nextFen);
488
657
  }, [onFenChange]);
489
- const initialLineKey = useMemo(() => { var _a; return (_a = initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.join("|")) !== null && _a !== void 0 ? _a : ""; }, [initialLineSans]);
490
- const lastAppliedLineKeyRef = useRef(undefined);
658
+ const queryFenRef = useRef(queryFen);
659
+ queryFenRef.current = queryFen;
660
+ const positionsByFenRef = useRef(new Map());
661
+ const positionCacheKey = useCallback((fenValue) => JSON.stringify({
662
+ fen: fenValue,
663
+ sources: sources.length < ALL_GAME_SOURCES.length
664
+ ? sources.slice().sort().join(",")
665
+ : "",
666
+ }), [sources]);
667
+ const gamesCacheKey = useCallback((fenValue, uci) => gamesSessionKey({
668
+ fen: fenValue,
669
+ uci,
670
+ sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
671
+ }), [sources]);
672
+ const lastAppliedLineKeyRef = useRef(initialLineKey);
491
673
  useEffect(() => () => cancelVariationAnimation(), [cancelVariationAnimation]);
674
+ // Apply URL line changes only when the external line key changes — not when the
675
+ // user clicks moves (internal lineSans updates must not re-sync from stale props).
492
676
  useEffect(() => {
493
- if (lastAppliedLineKeyRef.current === initialLineKey)
494
- return;
495
- const currentLineKey = lineSans.join("|");
496
- if (currentLineKey === initialLineKey) {
497
- lastAppliedLineKeyRef.current = initialLineKey;
677
+ if (initialLineSans === undefined && onLineSansChange === undefined)
498
678
  return;
679
+ const fenMatches = normalizeFen(queryFen) === normalizeFen(expectedLineFen);
680
+ if (lastAppliedLineKeyRef.current === initialLineKey) {
681
+ if (fenMatches)
682
+ return;
683
+ }
684
+ else {
685
+ const currentLineKey = lineSans.join("|");
686
+ if (currentLineKey === initialLineKey && fenMatches) {
687
+ lastAppliedLineKeyRef.current = initialLineKey;
688
+ return;
689
+ }
499
690
  }
500
691
  lastAppliedLineKeyRef.current = initialLineKey;
501
692
  cancelVariationAnimation();
502
- resetHistory(EXPLORER_START_FEN);
503
693
  setSelectedVariationKey(undefined);
504
694
  if (initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length) {
505
695
  const result = applyLineSans(EXPLORER_START_FEN, initialLineSans);
506
696
  if (result) {
507
- pushEntries(result.entries);
697
+ replaceLineEntries(EXPLORER_START_FEN, result.entries);
508
698
  applyNavigation(result.fen, true);
509
699
  return;
510
700
  }
511
701
  }
702
+ replaceLineEntries(EXPLORER_START_FEN, []);
512
703
  applyNavigation(EXPLORER_START_FEN, true);
704
+ // lineSans / queryFen intentionally omitted — only re-sync when the URL line key changes.
705
+ // eslint-disable-next-line react-hooks/exhaustive-deps
513
706
  }, [
514
707
  initialLineKey,
515
708
  initialLineSans,
516
- lineSans,
709
+ expectedLineFen,
517
710
  cancelVariationAnimation,
518
- resetHistory,
519
- pushEntries,
711
+ replaceLineEntries,
520
712
  applyNavigation,
521
713
  ]);
714
+ // Keep legacy fen state aligned with move history when they drift apart.
715
+ useEffect(() => {
716
+ if (isAnimatingVariationRef.current)
717
+ return;
718
+ if (normalizeFen(fen) === normalizeFen(queryFen))
719
+ return;
720
+ setFen(queryFen);
721
+ setBoardFen(queryFen);
722
+ onFenChange === null || onFenChange === void 0 ? void 0 : onFenChange(queryFen);
723
+ }, [fen, queryFen, onFenChange]);
522
724
  useEffect(() => {
523
725
  if (readyForLineSyncRef.current)
524
726
  return;
@@ -530,90 +732,171 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
530
732
  }
531
733
  }, [lineSans, initialLineSans]);
532
734
  useEffect(() => {
735
+ if (!onLineSansChange)
736
+ return;
533
737
  if (!readyForLineSyncRef.current)
534
738
  return;
535
- onLineSansChange === null || onLineSansChange === void 0 ? void 0 : onLineSansChange(lineSans);
739
+ onLineSansChange(lineSans);
536
740
  }, [lineSans, onLineSansChange]);
537
741
  useEffect(() => {
538
- if (fenProp === undefined || fenProp === fen)
742
+ if (fenProp === undefined)
743
+ return;
744
+ if (fenProp === queryFenRef.current)
539
745
  return;
540
746
  cancelVariationAnimation();
541
747
  resetHistory(fenProp);
542
748
  applyNavigation(fenProp, true);
543
- }, [fenProp, fen, resetHistory, applyNavigation, cancelVariationAnimation]);
749
+ }, [fenProp, cancelVariationAnimation, resetHistory, applyNavigation]);
544
750
  useEffect(() => {
545
751
  let cancelled = false;
546
- setLoading(true);
547
- setError(null);
752
+ const requestedFen = queryFen;
753
+ const requestedFilterKey = positionCacheKey(requestedFen);
754
+ if (positionsByFenRef.current.has(requestedFilterKey)) {
755
+ setPosition(positionsByFenRef.current.get(requestedFilterKey));
756
+ setLoadedPositionFen(requestedFen);
757
+ setLoadedPositionFilterKey(requestedFilterKey);
758
+ setLoading(false);
759
+ setError(null);
760
+ }
761
+ else {
762
+ setLoading(true);
763
+ setError(null);
764
+ }
548
765
  (() => __awaiter(this, void 0, void 0, function* () {
549
766
  try {
550
- const [pos, gameList] = yield Promise.all([
551
- fetchPosition(fen),
552
- fetchPositionGames({
553
- fen,
554
- minElo,
555
- maxElo,
556
- uci: gamesMoveFilterUci,
557
- topOnly,
558
- sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
559
- }),
560
- ]);
561
- if (cancelled)
767
+ const pos = yield fetchPosition({
768
+ fen: requestedFen,
769
+ sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
770
+ });
771
+ if (cancelled || requestedFen !== queryFenRef.current)
562
772
  return;
563
- setPosition(pos);
564
- setGames(gameList);
565
- if (!pos) {
566
- setError("No explorer data for this position yet");
773
+ if (pos) {
774
+ positionsByFenRef.current.set(positionCacheKey(requestedFen), pos);
567
775
  }
776
+ setPosition(pos);
777
+ setLoadedPositionFen(requestedFen);
778
+ setLoadedPositionFilterKey(positionCacheKey(requestedFen));
568
779
  }
569
780
  catch (e) {
781
+ if (cancelled || requestedFen !== queryFenRef.current)
782
+ return;
783
+ setPosition(null);
784
+ setLoadedPositionFen(null);
785
+ setLoadedPositionFilterKey(null);
786
+ setError(e instanceof Error ? e.message : "Failed to load explorer data");
787
+ }
788
+ finally {
789
+ if (!cancelled && requestedFen === queryFenRef.current) {
790
+ setLoading(false);
791
+ }
792
+ }
793
+ }))();
794
+ return () => {
795
+ cancelled = true;
796
+ };
797
+ }, [queryFen, sources, fetchPosition, positionCacheKey]);
798
+ const activePositionFilterKey = positionCacheKey(queryFen);
799
+ const cachedPosition = positionsByFenRef.current.get(activePositionFilterKey);
800
+ const positionReady = loadedPositionFen === queryFen &&
801
+ loadedPositionFilterKey === activePositionFilterKey;
802
+ const displayPosition = cachedPosition !== null && cachedPosition !== void 0 ? cachedPosition : (positionReady ? position : null);
803
+ const displayMoves = Array.isArray(displayPosition === null || displayPosition === void 0 ? void 0 : displayPosition.moves)
804
+ ? displayPosition.moves
805
+ : [];
806
+ const showPositionLoading = loading && displayMoves.length === 0;
807
+ const isStartPosition = normalizeFen(queryFen) === normalizeFen(EXPLORER_START_FEN);
808
+ useExplorerPrefetch({
809
+ fen: queryFen,
810
+ moves: displayMoves,
811
+ positionReady,
812
+ sources,
813
+ fetchPositionGames,
814
+ fetchPositionVariations,
815
+ });
816
+ useEffect(() => {
817
+ return subscribeExplorerSessionCache(() => {
818
+ const key = gamesCacheKey(queryFen, gamesMoveFilterUci);
819
+ const cached = peekSessionGames(key);
820
+ if (!cached) {
821
+ return;
822
+ }
823
+ setGames(cached);
824
+ setGamesLoading(false);
825
+ });
826
+ }, [queryFen, gamesMoveFilterUci, gamesCacheKey]);
827
+ useEffect(() => {
828
+ const canLoadGames = positionReady || (isStartPosition && !gamesMoveFilterUci);
829
+ if (!canLoadGames)
830
+ return;
831
+ let cancelled = false;
832
+ const requestedKey = gamesCacheKey(queryFen, gamesMoveFilterUci);
833
+ const cachedGames = peekSessionGames(requestedKey);
834
+ if (cachedGames) {
835
+ setGames(cachedGames);
836
+ setGamesLoading(false);
837
+ }
838
+ else {
839
+ setGamesLoading(true);
840
+ }
841
+ void (() => __awaiter(this, void 0, void 0, function* () {
842
+ try {
843
+ const gameList = yield fetchPositionGames({
844
+ fen: queryFen,
845
+ uci: gamesMoveFilterUci,
846
+ sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
847
+ });
848
+ if (cancelled)
849
+ return;
850
+ setSessionGames(requestedKey, gameList);
851
+ setGames(gameList);
852
+ }
853
+ catch (_a) {
570
854
  if (!cancelled) {
571
- setError(e instanceof Error ? e.message : "Failed to load explorer data");
855
+ setGames(null);
572
856
  }
573
857
  }
574
858
  finally {
575
859
  if (!cancelled)
576
- setLoading(false);
860
+ setGamesLoading(false);
577
861
  }
578
862
  }))();
579
863
  return () => {
580
864
  cancelled = true;
581
865
  };
582
866
  }, [
583
- fen,
584
- minElo,
585
- maxElo,
867
+ queryFen,
868
+ positionReady,
869
+ isStartPosition,
586
870
  gamesMoveFilterUci,
587
- topOnly,
588
871
  sources,
589
- fetchPosition,
590
872
  fetchPositionGames,
873
+ gamesCacheKey,
591
874
  ]);
592
875
  const handleMoveSelect = useCallback((move) => {
593
876
  if (isAnimatingVariationRef.current) {
594
877
  cancelVariationAnimation();
595
- setBoardFen(fen);
878
+ setBoardFen(queryFen);
596
879
  setSelectedVariationKey(undefined);
597
880
  }
598
- const nextFen = fenAfterUci(fen, move.uci);
881
+ const nextFen = fenAfterUci(queryFen, move.uci);
599
882
  if (!nextFen)
600
883
  return;
601
884
  pushEntry(nextFen, move.san);
602
885
  setSelectedVariationKey(undefined);
603
886
  applyNavigation(nextFen, true);
604
- }, [fen, pushEntry, applyNavigation, cancelVariationAnimation]);
887
+ }, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
605
888
  const handleLineSelect = useCallback((line) => {
606
889
  if (isAnimatingVariationRef.current)
607
890
  return;
608
891
  cancelVariationAnimation();
609
- let currentFen = fen;
892
+ let lineFen = queryFen;
610
893
  const entries = [];
611
894
  for (let i = 0; i < line.uciPath.length; i += 1) {
612
- const nextFen = fenAfterUci(currentFen, line.uciPath[i]);
895
+ const nextFen = fenAfterUci(lineFen, line.uciPath[i]);
613
896
  if (!nextFen)
614
897
  return;
615
898
  entries.push({ fen: nextFen, lastSan: line.moves[i].san });
616
- currentFen = nextFen;
899
+ lineFen = nextFen;
617
900
  }
618
901
  if (entries.length === 0)
619
902
  return;
@@ -637,109 +920,162 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
637
920
  }
638
921
  };
639
922
  tick();
640
- }, [fen, pushEntries, applyNavigation, cancelVariationAnimation]);
923
+ }, [queryFen, pushEntries, applyNavigation, cancelVariationAnimation]);
641
924
  const handlePieceDrop = useCallback((sourceSquare, targetSquare, piece) => {
642
925
  if (isAnimatingVariationRef.current) {
643
926
  cancelVariationAnimation();
644
- setBoardFen(fen);
927
+ setBoardFen(queryFen);
645
928
  setSelectedVariationKey(undefined);
646
929
  }
647
- const result = applyBoardMove(fen, sourceSquare, targetSquare, piece);
930
+ const result = applyBoardMove(queryFen, sourceSquare, targetSquare, piece);
648
931
  if (!result)
649
932
  return false;
650
933
  pushEntry(result.fen, result.san);
651
934
  setSelectedVariationKey(undefined);
652
935
  applyNavigation(result.fen, true);
653
936
  return true;
654
- }, [fen, pushEntry, applyNavigation, cancelVariationAnimation]);
937
+ }, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
655
938
  const handleBack = useCallback(() => {
656
939
  if (isAnimatingVariationRef.current) {
657
940
  cancelVariationAnimation();
658
- setBoardFen(fen);
941
+ setBoardFen(queryFen);
659
942
  setSelectedVariationKey(undefined);
660
943
  return;
661
944
  }
662
945
  const entry = goBack();
663
946
  if (entry)
664
947
  applyNavigation(entry.fen, true);
665
- }, [fen, goBack, applyNavigation, cancelVariationAnimation]);
948
+ }, [queryFen, goBack, applyNavigation, cancelVariationAnimation]);
666
949
  const handleForward = useCallback(() => {
667
950
  if (isAnimatingVariationRef.current) {
668
951
  cancelVariationAnimation();
669
- setBoardFen(fen);
952
+ setBoardFen(queryFen);
670
953
  setSelectedVariationKey(undefined);
671
954
  return;
672
955
  }
673
956
  const entry = goForward();
674
957
  if (entry)
675
958
  applyNavigation(entry.fen, true);
676
- }, [fen, goForward, applyNavigation, cancelVariationAnimation]);
959
+ }, [queryFen, goForward, applyNavigation, cancelVariationAnimation]);
960
+ const handleFirst = useCallback(() => {
961
+ if (isAnimatingVariationRef.current) {
962
+ cancelVariationAnimation();
963
+ setBoardFen(queryFen);
964
+ setSelectedVariationKey(undefined);
965
+ return;
966
+ }
967
+ const entry = goFirst();
968
+ if (entry)
969
+ applyNavigation(entry.fen, true);
970
+ }, [queryFen, goFirst, applyNavigation, cancelVariationAnimation]);
971
+ const handleLast = useCallback(() => {
972
+ if (isAnimatingVariationRef.current) {
973
+ cancelVariationAnimation();
974
+ setBoardFen(queryFen);
975
+ setSelectedVariationKey(undefined);
976
+ return;
977
+ }
978
+ const entry = goLast();
979
+ if (entry)
980
+ applyNavigation(entry.fen, true);
981
+ }, [queryFen, goLast, applyNavigation, cancelVariationAnimation]);
677
982
  const lineLabel = useMemo(() => {
678
983
  if (lineSans.length > 0) {
679
- return lineSans.join(" ");
984
+ return formatNumberedLineSans(lineSans);
680
985
  }
681
- if (position) {
986
+ if (position && typeof position.totalGames === "number") {
682
987
  return `Starting position (${position.totalGames.toLocaleString()} games)`;
683
988
  }
684
989
  return "";
685
990
  }, [lineSans, position]);
686
991
  return {
687
- fen,
992
+ fen: queryFen,
688
993
  boardFen,
689
994
  position,
690
995
  games,
691
996
  gamesMoveFilterUci,
692
- minElo,
693
- maxElo,
694
- topOnly,
695
997
  sources,
998
+ lineSans,
696
999
  loading,
1000
+ showPositionLoading,
1001
+ gamesLoading,
1002
+ positionReady,
1003
+ displayMoves,
697
1004
  error,
698
1005
  lineLabel,
699
1006
  canGoBack,
700
1007
  canGoForward,
701
- variationsTab,
702
1008
  forwardSans,
703
1009
  selectedVariationKey,
704
- setMinElo,
705
- setMaxElo,
706
- setTopOnly,
707
1010
  setSources,
708
- setVariationsTab,
709
1011
  setGamesMoveFilterUci,
710
1012
  handleMoveSelect,
711
1013
  handleLineSelect,
712
1014
  handlePieceDrop,
713
1015
  handleBack,
714
1016
  handleForward,
1017
+ handleFirst,
1018
+ handleLast,
715
1019
  };
716
1020
  }
717
1021
 
718
- function useVariationLines({ fen, tab, minElo, maxElo, fetchPositionVariations, lineCount = 8, lineDepth = 4, enabled = true, }) {
719
- const [lines, setLines] = useState([]);
720
- const [loading, setLoading] = useState(false);
1022
+ function useVariationLines({ fen, fetchPositionVariations, lineCount = EXPLORER_DEFAULT_VARIATION_LINE_COUNT, lineDepth = EXPLORER_DEFAULT_VARIATION_DEPTH, enabled = true, }) {
1023
+ const cacheKey = useMemo(() => variationsSessionKey(fen, lineCount, lineDepth), [fen, lineCount, lineDepth]);
1024
+ const [lines, setLines] = useState(() => {
1025
+ var _a;
1026
+ if (!enabled) {
1027
+ return [];
1028
+ }
1029
+ return (_a = peekSessionVariations(cacheKey)) !== null && _a !== void 0 ? _a : [];
1030
+ });
1031
+ const [loading, setLoading] = useState(() => {
1032
+ if (!enabled) {
1033
+ return false;
1034
+ }
1035
+ return peekSessionVariations(cacheKey) === undefined;
1036
+ });
1037
+ useEffect(() => {
1038
+ return subscribeExplorerSessionCache(() => {
1039
+ if (!enabled) {
1040
+ return;
1041
+ }
1042
+ const cachedLines = peekSessionVariations(cacheKey);
1043
+ if (!cachedLines) {
1044
+ return;
1045
+ }
1046
+ setLines(cachedLines);
1047
+ setLoading(false);
1048
+ });
1049
+ }, [enabled, cacheKey]);
721
1050
  useEffect(() => {
722
- if (!enabled || tab === "endgames") {
1051
+ if (!enabled) {
723
1052
  setLines([]);
724
1053
  setLoading(false);
725
1054
  return;
726
1055
  }
727
1056
  let cancelled = false;
728
- setLoading(true);
729
- (() => __awaiter(this, void 0, void 0, function* () {
1057
+ const cachedLines = peekSessionVariations(cacheKey);
1058
+ if (cachedLines) {
1059
+ setLines(cachedLines);
1060
+ setLoading(false);
1061
+ }
1062
+ else {
1063
+ setLoading(true);
1064
+ }
1065
+ void (() => __awaiter(this, void 0, void 0, function* () {
730
1066
  var _a;
731
1067
  try {
732
1068
  const result = yield fetchPositionVariations({
733
1069
  fen,
734
- mode: tab,
735
- minElo,
736
- maxElo,
1070
+ mode: "variations",
737
1071
  lineCount,
738
1072
  depth: lineDepth,
739
1073
  });
740
1074
  if (cancelled)
741
1075
  return;
742
- setLines((_a = result === null || result === void 0 ? void 0 : result.lines) !== null && _a !== void 0 ? _a : []);
1076
+ const nextLines = (_a = result === null || result === void 0 ? void 0 : result.lines) !== null && _a !== void 0 ? _a : [];
1077
+ setSessionVariations(cacheKey, nextLines);
1078
+ setLines(nextLines);
743
1079
  }
744
1080
  catch (_b) {
745
1081
  if (!cancelled) {
@@ -755,21 +1091,11 @@ function useVariationLines({ fen, tab, minElo, maxElo, fetchPositionVariations,
755
1091
  return () => {
756
1092
  cancelled = true;
757
1093
  };
758
- }, [
759
- enabled,
760
- fen,
761
- tab,
762
- minElo,
763
- maxElo,
764
- fetchPositionVariations,
765
- lineCount,
766
- lineDepth,
767
- ]);
768
- return { lines, loading };
1094
+ }, [enabled, fen, fetchPositionVariations, lineCount, lineDepth, cacheKey]);
1095
+ return { lines, loading: enabled ? loading : false };
769
1096
  }
770
1097
 
771
- const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, theme = "dark", boardWidth = DEFAULT_REFERENCE_LAYOUT.boardWidth, defaultMinElo = 2200, defaultMaxElo = 2800, fillHeight = true, layoutMinHeight, renderLayout = defaultRenderLayout, renderStatus = defaultRenderStatus, renderMoveStats = defaultRenderMoveStats, renderVariationsStrip = defaultRenderVariationsStrip, renderGamesPanel = defaultRenderGamesPanel, renderBoardNav = defaultRenderBoardNav, onGameSelect, }) => {
772
- var _a, _b;
1098
+ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, theme = "dark", boardTheme, boardWidth = DEFAULT_REFERENCE_LAYOUT.boardWidth, boardOrientation: boardOrientationProp, defaultBoardOrientation = "white", onBoardOrientationChange, fillHeight = true, layoutMinHeight, renderLayout = defaultRenderLayout, renderStatus = defaultRenderStatus, renderMoveStats = defaultRenderMoveStats, renderVariationsStrip = defaultRenderVariationsStrip, renderGamesPanel = defaultRenderGamesPanel, renderBoardNav = defaultRenderBoardNav, onGameSelect, keyboardNav = true, }) => {
773
1099
  const referenceData = usePositionReferenceData({
774
1100
  fenProp,
775
1101
  onFenChange,
@@ -777,59 +1103,96 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
777
1103
  onLineSansChange,
778
1104
  fetchPosition,
779
1105
  fetchPositionGames,
780
- defaultMinElo,
781
- defaultMaxElo,
1106
+ fetchPositionVariations,
1107
+ });
1108
+ const { fen, boardFen, games, sources, lineSans, loading, showPositionLoading, gamesLoading, positionReady, displayMoves, error, lineLabel, canGoBack, canGoForward, forwardSans, selectedVariationKey, setSources, handleMoveSelect, handleLineSelect, handlePieceDrop, handleBack, handleForward, handleFirst, handleLast, } = referenceData;
1109
+ usePositionKeyboardNav({
1110
+ enabled: keyboardNav,
1111
+ canPrev: canGoBack,
1112
+ canNext: canGoForward,
1113
+ onPrev: handleBack,
1114
+ onNext: handleForward,
1115
+ onFirst: handleFirst,
1116
+ onLast: handleLast,
782
1117
  });
783
- const { fen, boardFen, position, games, minElo, maxElo, topOnly, sources, loading, error, lineLabel, canGoBack, canGoForward, variationsTab, forwardSans, selectedVariationKey, setMinElo, setMaxElo, setTopOnly, setSources, setVariationsTab, handleMoveSelect, handleLineSelect, handlePieceDrop, handleBack, handleForward, } = referenceData;
1118
+ const [variationsEnabled, setVariationsEnabled] = useState(() => normalizeFen(fen) === normalizeFen(EXPLORER_START_FEN));
1119
+ const isStartPosition = normalizeFen(fen) === normalizeFen(EXPLORER_START_FEN);
1120
+ useEffect(() => {
1121
+ const cachedVariations = peekSessionVariations(variationsSessionKey(fen, EXPLORER_DEFAULT_VARIATION_LINE_COUNT, EXPLORER_DEFAULT_VARIATION_DEPTH));
1122
+ if (cachedVariations) {
1123
+ setVariationsEnabled(true);
1124
+ return;
1125
+ }
1126
+ if (!positionReady && !isStartPosition) {
1127
+ setVariationsEnabled(false);
1128
+ return;
1129
+ }
1130
+ if (loading && !isStartPosition) {
1131
+ setVariationsEnabled(false);
1132
+ return;
1133
+ }
1134
+ setVariationsEnabled(true);
1135
+ }, [fen, positionReady, loading, isStartPosition]);
784
1136
  const { lines: variationLines, loading: variationLinesLoading } = useVariationLines({
785
1137
  fen,
786
- tab: variationsTab,
787
- minElo,
788
- maxElo,
789
1138
  fetchPositionVariations,
790
- enabled: Boolean(position),
1139
+ enabled: variationsEnabled && (positionReady || isStartPosition),
791
1140
  });
1141
+ const [internalBoardOrientation, setInternalBoardOrientation] = useState(defaultBoardOrientation);
1142
+ const boardOrientation = boardOrientationProp !== null && boardOrientationProp !== void 0 ? boardOrientationProp : internalBoardOrientation;
1143
+ const handleFlipBoard = useCallback(() => {
1144
+ const nextOrientation = boardOrientation === "white" ? "black" : "white";
1145
+ if (boardOrientationProp === undefined) {
1146
+ setInternalBoardOrientation(nextOrientation);
1147
+ }
1148
+ onBoardOrientationChange === null || onBoardOrientationChange === void 0 ? void 0 : onBoardOrientationChange(nextOrientation);
1149
+ }, [boardOrientation, boardOrientationProp, onBoardOrientationChange]);
1150
+ const boardNavProps = useMemo(() => ({
1151
+ canGoBack,
1152
+ canGoForward,
1153
+ onBack: handleBack,
1154
+ onForward: handleForward,
1155
+ boardOrientation,
1156
+ onFlipBoard: handleFlipBoard,
1157
+ }), [
1158
+ boardOrientation,
1159
+ canGoBack,
1160
+ canGoForward,
1161
+ handleBack,
1162
+ handleFlipBoard,
1163
+ handleForward,
1164
+ ]);
792
1165
  const outerStyle = {
793
1166
  width: "100%",
794
1167
  height: fillHeight ? "100%" : "auto",
795
1168
  minHeight: layoutMinHeight !== null && layoutMinHeight !== void 0 ? layoutMinHeight : DEFAULT_REFERENCE_LAYOUT.minHeight,
796
- overflow: "hidden",
1169
+ overflow: fillHeight ? "hidden" : "visible",
797
1170
  boxSizing: "border-box",
798
1171
  };
799
- const board = (jsxs(Fragment, { children: [jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, position: boardFen, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, onPieceDrop: handlePieceDrop, promotionDialogVariant: "modal" }) }), renderBoardNav({
800
- canGoBack,
801
- canGoForward,
802
- onBack: handleBack,
803
- onForward: handleForward,
804
- })] }));
805
- const referencePanel = (jsx(DefaultReferencePanel, { theme: theme, status: renderStatus({ error, loading }), moveStats: renderMoveStats({
806
- moves: (_a = position === null || position === void 0 ? void 0 : position.moves) !== null && _a !== void 0 ? _a : [],
1172
+ const board = (jsxs(Fragment, { children: [jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, position: boardFen, boardOrientation: boardOrientation, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, onPieceDrop: handlePieceDrop, promotionDialogVariant: "modal" }, boardOrientation) }), renderBoardNav(boardNavProps)] }));
1173
+ const referencePanel = (jsx(DefaultReferencePanel, { theme: theme, fillHeight: fillHeight, status: renderStatus({
1174
+ error,
1175
+ loading: showPositionLoading,
1176
+ }), moveStats: renderMoveStats({
1177
+ moves: displayMoves,
807
1178
  onMoveSelect: handleMoveSelect,
808
1179
  }), variationsStrip: renderVariationsStrip({
809
1180
  theme,
810
- tab: variationsTab,
811
- onTabChange: setVariationsTab,
812
1181
  lines: variationLines,
813
- loading: variationLinesLoading,
1182
+ loading: !variationsEnabled || variationLinesLoading,
814
1183
  selectedLineKey: selectedVariationKey,
815
1184
  forwardSans,
816
1185
  onLineSelect: handleLineSelect,
817
1186
  }), gamesPanel: renderGamesPanel({
818
- games: (_b = games === null || games === void 0 ? void 0 : games.games) !== null && _b !== void 0 ? _b : [],
1187
+ games: Array.isArray(games === null || games === void 0 ? void 0 : games.games) ? games.games : [],
1188
+ loading: gamesLoading,
819
1189
  lineLabel,
820
- minElo,
821
- maxElo,
822
- defaultMinElo,
823
- defaultMaxElo,
824
- topOnly,
1190
+ lineSans,
825
1191
  sources,
826
- onMinEloChange: setMinElo,
827
- onMaxEloChange: setMaxElo,
828
- onTopOnlyChange: setTopOnly,
829
1192
  onSourcesChange: setSources,
830
1193
  onGameSelect,
831
1194
  }) }));
832
- return (jsx(ThemeProvider, { theme: theme, children: jsx("div", { style: outerStyle, children: renderLayout({ theme, board, referencePanel }) }) }));
1195
+ return (jsx(ThemeProvider, { theme: theme, boardTheme: boardTheme, children: jsx("div", { style: outerStyle, children: renderLayout({ theme, board, referencePanel }) }) }));
833
1196
  };
834
1197
 
835
1198
  /** Reference explorer with library default layout and renderers (ChessBase-style grid). */
@@ -1040,10 +1403,6 @@ const mockPosition = {
1040
1403
  };
1041
1404
  const mockGames = {
1042
1405
  positionKey: "mock-start",
1043
- fen: EXPLORER_START_FEN,
1044
- minElo: 2200,
1045
- maxElo: 2800,
1046
- topOnly: false,
1047
1406
  games: [
1048
1407
  {
1049
1408
  gameId: "abc123",
@@ -1075,7 +1434,7 @@ const mockGames = {
1075
1434
  },
1076
1435
  ],
1077
1436
  };
1078
- function mockFetchPosition(fen) {
1437
+ function mockFetchPosition(params) {
1079
1438
  return __awaiter(this, void 0, void 0, function* () {
1080
1439
  return mockPosition;
1081
1440
  });
@@ -1109,7 +1468,11 @@ function mockVariationLines(params, position) {
1109
1468
  }
1110
1469
  function mockFetchPositionVariations(params) {
1111
1470
  return __awaiter(this, void 0, void 0, function* () {
1112
- const position = yield mockFetchPosition(params.fen);
1471
+ const position = yield mockFetchPosition({
1472
+ fen: params.fen,
1473
+ minElo: params.minElo,
1474
+ maxElo: params.maxElo,
1475
+ });
1113
1476
  if (!position)
1114
1477
  return null;
1115
1478
  return mockVariationLines(params, position);
@@ -1117,15 +1480,26 @@ function mockFetchPositionVariations(params) {
1117
1480
  }
1118
1481
  function mockFetchPositionGames(params) {
1119
1482
  return __awaiter(this, void 0, void 0, function* () {
1120
- var _a;
1483
+ var _a, _b, _c;
1121
1484
  let filtered = params.uci
1122
1485
  ? mockGames.games.filter((g) => g.nextUci === params.uci)
1123
1486
  : mockGames.games;
1124
1487
  if ((_a = params.sources) === null || _a === void 0 ? void 0 : _a.length) {
1125
1488
  filtered = filtered.filter((game) => params.sources.includes(game.source));
1126
1489
  }
1127
- return Object.assign(Object.assign({}, mockGames), { fen: params.fen, minElo: params.minElo, maxElo: params.maxElo, uci: params.uci, topOnly: params.topOnly, games: params.topOnly ? filtered.filter((g) => g.avgElo >= 2500) : filtered });
1490
+ const offset = (_b = params.offset) !== null && _b !== void 0 ? _b : 0;
1491
+ const limit = (_c = params.limit) !== null && _c !== void 0 ? _c : filtered.length;
1492
+ const page = filtered.slice(offset, offset + limit);
1493
+ const hasMore = offset + limit < filtered.length;
1494
+ return {
1495
+ positionKey: mockGames.positionKey,
1496
+ fen: params.fen,
1497
+ uci: params.uci,
1498
+ offset,
1499
+ hasMore,
1500
+ games: page,
1501
+ };
1128
1502
  });
1129
1503
  }
1130
1504
 
1131
- export { ALL_GAME_SOURCES, DEFAULT_REFERENCE_LAYOUT, DefaultBoardNav, DefaultReferenceLayout, DefaultReferencePanel, DefaultVariationsStrip, EXPLORER_START_FEN, ExplorerPlaceholder, GameReplayTrainer, PositionReferenceExplorer, PositionReferenceExplorerCore, applyLineSans, defaultRenderBoardNav, defaultRenderGamesPanel, defaultRenderLayout, defaultRenderMoveStats, defaultRenderStatus, defaultRenderVariationsStrip, fenAfterUci, fenAtPly, findPlyIndexForFen, isVariationLineActive, mockFetchPosition, mockFetchPositionGames, mockFetchPositionVariations, normalizeFen, useGameReplayTraining, whiteScorePercent };
1505
+ export { ALL_GAME_SOURCES, DEFAULT_REFERENCE_LAYOUT, DefaultBoardNav, DefaultReferenceLayout, DefaultReferencePanel, DefaultVariationsStrip, EXPLORER_DEFAULT_VARIATION_DEPTH, EXPLORER_DEFAULT_VARIATION_LINE_COUNT, EXPLORER_PREFETCH_CHILD_COUNT, EXPLORER_START_FEN, ExplorerPlaceholder, GameReplayTrainer, PositionReferenceExplorer, PositionReferenceExplorerCore, applyLineSans, defaultRenderBoardNav, defaultRenderGamesPanel, defaultRenderLayout, defaultRenderMoveStats, defaultRenderStatus, defaultRenderVariationsStrip, fenAfterUci, fenAtPly, findPlyIndexForFen, formatNumberedLineSans, gamesSessionKey, isVariationLineActive, mockFetchPosition, mockFetchPositionGames, mockFetchPositionVariations, normalizeFen, peekSessionGames, peekSessionVariations, seedExplorerStartSession, setSessionGames, setSessionVariations, subscribeExplorerSessionCache, useGameReplayTraining, variationsSessionKey, whiteScorePercent };