react-chess-explorer 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,113 @@ 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
+ const prefetchMove = (move) => {
436
+ const childFen = fenAfterUci(fen, move.uci);
437
+ if (!childFen) {
438
+ return Promise.resolve();
439
+ }
440
+ const gamesKey = gamesSessionKey({
441
+ fen: childFen,
442
+ sources: sourcesParam,
443
+ });
444
+ const gamesPromise = peekSessionGames(gamesKey)
445
+ ? Promise.resolve()
446
+ : fetchPositionGames({ fen: childFen, sources: sourcesParam })
447
+ .then((games) => {
448
+ if (!cancelled) {
449
+ setSessionGames(gamesKey, games);
450
+ }
451
+ })
452
+ .catch(() => undefined);
453
+ if (!fetchPositionVariations) {
454
+ return gamesPromise;
455
+ }
456
+ const variationsKey = variationsSessionKey(childFen);
457
+ const variationsPromise = peekSessionVariations(variationsKey)
458
+ ? Promise.resolve()
459
+ : fetchPositionVariations({
460
+ fen: childFen,
461
+ mode: "variations",
462
+ lineCount: EXPLORER_DEFAULT_VARIATION_LINE_COUNT,
463
+ depth: EXPLORER_DEFAULT_VARIATION_DEPTH,
464
+ })
465
+ .then((result) => {
466
+ var _a;
467
+ if (!cancelled) {
468
+ setSessionVariations(variationsKey, (_a = result === null || result === void 0 ? void 0 : result.lines) !== null && _a !== void 0 ? _a : []);
469
+ }
470
+ })
471
+ .catch(() => undefined);
472
+ return Promise.all([gamesPromise, variationsPromise]).then(() => undefined);
473
+ };
474
+ void (() => __awaiter(this, void 0, void 0, function* () {
475
+ const movesToPrefetch = moves.slice(0, childCount);
476
+ for (let index = 0; index < movesToPrefetch.length; index += 2) {
477
+ if (cancelled) {
478
+ return;
479
+ }
480
+ yield Promise.all(movesToPrefetch.slice(index, index + 2).map(prefetchMove));
481
+ }
482
+ }))();
483
+ };
484
+ if (typeof window.requestIdleCallback === "function") {
485
+ idleHandle = window.requestIdleCallback(prefetchChildren, {
486
+ timeout: 1500,
487
+ });
488
+ }
489
+ else {
490
+ deferTimer = setTimeout(prefetchChildren, 750);
491
+ }
492
+ return () => {
493
+ cancelled = true;
494
+ if (idleHandle !== undefined) {
495
+ window.cancelIdleCallback(idleHandle);
496
+ }
497
+ if (deferTimer !== undefined) {
498
+ clearTimeout(deferTimer);
499
+ }
500
+ };
501
+ }, [
502
+ fen,
503
+ moves,
504
+ positionReady,
505
+ sources,
506
+ fetchPositionGames,
507
+ fetchPositionVariations,
508
+ childCount,
387
509
  ]);
388
- const [historyIndex, setHistoryIndex] = useState(0);
510
+ }
511
+
512
+ function initialHistoryState(initialFen, initialLineSans) {
513
+ if (!(initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length)) {
514
+ return { history: [{ fen: initialFen }], historyIndex: 0 };
515
+ }
516
+ const result = applyLineSans(initialFen, initialLineSans);
517
+ if (!result) {
518
+ return { history: [{ fen: initialFen }], historyIndex: 0 };
519
+ }
520
+ return {
521
+ history: [{ fen: initialFen }, ...result.entries],
522
+ historyIndex: result.entries.length,
523
+ };
524
+ }
525
+ function usePositionHistory(initialFen, initialLineSans) {
526
+ var _a, _b;
527
+ const [history, setHistory] = useState(() => initialHistoryState(initialFen, initialLineSans).history);
528
+ const [historyIndex, setHistoryIndex] = useState(() => initialHistoryState(initialFen, initialLineSans).historyIndex);
389
529
  const canGoBack = historyIndex > 0;
390
530
  const canGoForward = historyIndex < history.length - 1;
391
531
  const pushEntry = useCallback((fen, lastSan) => {
@@ -413,6 +553,20 @@ function usePositionHistory(initialFen) {
413
553
  setHistoryIndex(nextIndex);
414
554
  return entry;
415
555
  }, [history, historyIndex]);
556
+ const goFirst = useCallback(() => {
557
+ if (historyIndex <= 0)
558
+ return null;
559
+ const entry = history[0];
560
+ setHistoryIndex(0);
561
+ return entry;
562
+ }, [history, historyIndex]);
563
+ const goLast = useCallback(() => {
564
+ if (historyIndex >= history.length - 1)
565
+ return null;
566
+ const entry = history[history.length - 1];
567
+ setHistoryIndex(history.length - 1);
568
+ return entry;
569
+ }, [history, historyIndex]);
416
570
  const resetHistory = useCallback((fen) => {
417
571
  const entry = { fen };
418
572
  setHistory([entry]);
@@ -437,39 +591,54 @@ function usePositionHistory(initialFen) {
437
591
  .slice(historyIndex + 1)
438
592
  .map((entry) => entry.lastSan)
439
593
  .filter((san) => Boolean(san));
594
+ const currentFen = (_b = (_a = history[historyIndex]) === null || _a === void 0 ? void 0 : _a.fen) !== null && _b !== void 0 ? _b : initialFen;
595
+ const replaceLineEntries = useCallback((startFen, entries) => {
596
+ setHistory([{ fen: startFen }, ...entries]);
597
+ setHistoryIndex(entries.length);
598
+ }, []);
440
599
  return {
441
600
  canGoBack,
442
601
  canGoForward,
443
602
  lineSans,
444
603
  forwardSans,
604
+ currentFen,
445
605
  pushEntry,
446
606
  pushEntries,
607
+ replaceLineEntries,
447
608
  goBack,
448
609
  goForward,
610
+ goFirst,
611
+ goLast,
449
612
  resetHistory,
450
613
  };
451
614
  }
452
615
 
453
- function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, defaultMinElo, defaultMaxElo, }) {
616
+ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLineSansChange, fetchPosition, fetchPositionGames, fetchPositionVariations, }) {
617
+ var _a, _b, _c;
454
618
  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);
619
+ const bootstrappedRef = useRef(initialHistoryState(initialFen, initialLineSans));
620
+ const bootstrappedFen = (_b = (_a = bootstrappedRef.current.history[bootstrappedRef.current.historyIndex]) === null || _a === void 0 ? void 0 : _a.fen) !== null && _b !== void 0 ? _b : initialFen;
621
+ const initialGamesKey = gamesSessionKey({ fen: bootstrappedFen });
622
+ const initialCachedGames = peekSessionGames(initialGamesKey);
623
+ const [fen, setFen] = useState(bootstrappedFen);
624
+ /** Board display FEN; may lead {@link queryFen} while a variation line is animating. */
625
+ const [boardFen, setBoardFen] = useState(bootstrappedFen);
458
626
  const [position, setPosition] = useState(null);
459
- const [games, setGames] = useState(null);
627
+ const [games, setGames] = useState(() => initialCachedGames !== null && initialCachedGames !== void 0 ? initialCachedGames : null);
460
628
  /** Filter games to those that played this UCI from the current FEN (optional). */
461
629
  const [gamesMoveFilterUci, setGamesMoveFilterUci] = useState();
462
- const [minElo, setMinElo] = useState(defaultMinElo);
463
- const [maxElo, setMaxElo] = useState(defaultMaxElo);
464
- const [topOnly, setTopOnly] = useState(false);
465
630
  const [sources, setSources] = useState([...ALL_GAME_SOURCES]);
466
631
  const [loading, setLoading] = useState(false);
632
+ const [gamesLoading, setGamesLoading] = useState(() => initialCachedGames === undefined);
633
+ /** FEN that {@link position} was loaded for (may lag {@link fen} while fetching). */
634
+ const [loadedPositionFen, setLoadedPositionFen] = useState(null);
635
+ const [loadedPositionFilterKey, setLoadedPositionFilterKey] = useState(null);
467
636
  const [error, setError] = useState(null);
468
- const [variationsTab, setVariationsTab] = useState("variations");
469
637
  const [selectedVariationKey, setSelectedVariationKey] = useState();
470
638
  const variationAnimationTimerRef = useRef(null);
471
639
  const isAnimatingVariationRef = useRef(false);
472
- const readyForLineSyncRef = useRef(false);
640
+ const readyForLineSyncRef = useRef(bootstrappedRef.current.historyIndex > 0 ||
641
+ ((_c = initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length) !== null && _c !== void 0 ? _c : 0) === 0);
473
642
  const cancelVariationAnimation = useCallback(() => {
474
643
  if (variationAnimationTimerRef.current !== null) {
475
644
  clearTimeout(variationAnimationTimerRef.current);
@@ -477,7 +646,17 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
477
646
  }
478
647
  isAnimatingVariationRef.current = false;
479
648
  }, []);
480
- const { canGoBack, canGoForward, lineSans, forwardSans, pushEntry, pushEntries, goBack, goForward, resetHistory, } = usePositionHistory(initialFen);
649
+ const { canGoBack, canGoForward, lineSans, forwardSans, currentFen, pushEntry, pushEntries, replaceLineEntries, goBack, goForward, goFirst, goLast, resetHistory, } = usePositionHistory(initialFen, initialLineSans);
650
+ /** FEN used for explorer API queries — follows move history, not animation frames. */
651
+ const queryFen = currentFen;
652
+ const initialLineKey = useMemo(() => { var _a; return (_a = initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.join("|")) !== null && _a !== void 0 ? _a : ""; }, [initialLineSans]);
653
+ const expectedLineFen = useMemo(() => {
654
+ var _a, _b;
655
+ if (!(initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length)) {
656
+ return EXPLORER_START_FEN;
657
+ }
658
+ return ((_b = (_a = applyLineSans(EXPLORER_START_FEN, initialLineSans)) === null || _a === void 0 ? void 0 : _a.fen) !== null && _b !== void 0 ? _b : EXPLORER_START_FEN);
659
+ }, [initialLineKey, initialLineSans]);
481
660
  const applyNavigation = useCallback((nextFen, clearMoveFilter = true) => {
482
661
  setFen(nextFen);
483
662
  setBoardFen(nextFen);
@@ -486,39 +665,72 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
486
665
  }
487
666
  onFenChange === null || onFenChange === void 0 ? void 0 : onFenChange(nextFen);
488
667
  }, [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);
668
+ const queryFenRef = useRef(queryFen);
669
+ queryFenRef.current = queryFen;
670
+ const positionsByFenRef = useRef(new Map());
671
+ const positionCacheKey = useCallback((fenValue) => JSON.stringify({
672
+ fen: fenValue,
673
+ sources: sources.length < ALL_GAME_SOURCES.length
674
+ ? sources.slice().sort().join(",")
675
+ : "",
676
+ }), [sources]);
677
+ const gamesCacheKey = useCallback((fenValue, uci) => gamesSessionKey({
678
+ fen: fenValue,
679
+ uci,
680
+ sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
681
+ }), [sources]);
682
+ const lastAppliedLineKeyRef = useRef(initialLineKey);
491
683
  useEffect(() => () => cancelVariationAnimation(), [cancelVariationAnimation]);
684
+ // Apply URL line changes only when the external line key changes — not when the
685
+ // user clicks moves (internal lineSans updates must not re-sync from stale props).
492
686
  useEffect(() => {
493
- if (lastAppliedLineKeyRef.current === initialLineKey)
494
- return;
495
- const currentLineKey = lineSans.join("|");
496
- if (currentLineKey === initialLineKey) {
497
- lastAppliedLineKeyRef.current = initialLineKey;
687
+ if (initialLineSans === undefined && onLineSansChange === undefined)
498
688
  return;
689
+ const fenMatches = normalizeFen(queryFen) === normalizeFen(expectedLineFen);
690
+ if (lastAppliedLineKeyRef.current === initialLineKey) {
691
+ if (fenMatches)
692
+ return;
693
+ }
694
+ else {
695
+ const currentLineKey = lineSans.join("|");
696
+ if (currentLineKey === initialLineKey && fenMatches) {
697
+ lastAppliedLineKeyRef.current = initialLineKey;
698
+ return;
699
+ }
499
700
  }
500
701
  lastAppliedLineKeyRef.current = initialLineKey;
501
702
  cancelVariationAnimation();
502
- resetHistory(EXPLORER_START_FEN);
503
703
  setSelectedVariationKey(undefined);
504
704
  if (initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length) {
505
705
  const result = applyLineSans(EXPLORER_START_FEN, initialLineSans);
506
706
  if (result) {
507
- pushEntries(result.entries);
707
+ replaceLineEntries(EXPLORER_START_FEN, result.entries);
508
708
  applyNavigation(result.fen, true);
509
709
  return;
510
710
  }
511
711
  }
712
+ replaceLineEntries(EXPLORER_START_FEN, []);
512
713
  applyNavigation(EXPLORER_START_FEN, true);
714
+ // lineSans / queryFen intentionally omitted — only re-sync when the URL line key changes.
715
+ // eslint-disable-next-line react-hooks/exhaustive-deps
513
716
  }, [
514
717
  initialLineKey,
515
718
  initialLineSans,
516
- lineSans,
719
+ expectedLineFen,
517
720
  cancelVariationAnimation,
518
- resetHistory,
519
- pushEntries,
721
+ replaceLineEntries,
520
722
  applyNavigation,
521
723
  ]);
724
+ // Keep legacy fen state aligned with move history when they drift apart.
725
+ useEffect(() => {
726
+ if (isAnimatingVariationRef.current)
727
+ return;
728
+ if (normalizeFen(fen) === normalizeFen(queryFen))
729
+ return;
730
+ setFen(queryFen);
731
+ setBoardFen(queryFen);
732
+ onFenChange === null || onFenChange === void 0 ? void 0 : onFenChange(queryFen);
733
+ }, [fen, queryFen, onFenChange]);
522
734
  useEffect(() => {
523
735
  if (readyForLineSyncRef.current)
524
736
  return;
@@ -530,90 +742,171 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
530
742
  }
531
743
  }, [lineSans, initialLineSans]);
532
744
  useEffect(() => {
745
+ if (!onLineSansChange)
746
+ return;
533
747
  if (!readyForLineSyncRef.current)
534
748
  return;
535
- onLineSansChange === null || onLineSansChange === void 0 ? void 0 : onLineSansChange(lineSans);
749
+ onLineSansChange(lineSans);
536
750
  }, [lineSans, onLineSansChange]);
537
751
  useEffect(() => {
538
- if (fenProp === undefined || fenProp === fen)
752
+ if (fenProp === undefined)
753
+ return;
754
+ if (fenProp === queryFenRef.current)
539
755
  return;
540
756
  cancelVariationAnimation();
541
757
  resetHistory(fenProp);
542
758
  applyNavigation(fenProp, true);
543
- }, [fenProp, fen, resetHistory, applyNavigation, cancelVariationAnimation]);
759
+ }, [fenProp, cancelVariationAnimation, resetHistory, applyNavigation]);
544
760
  useEffect(() => {
545
761
  let cancelled = false;
546
- setLoading(true);
547
- setError(null);
762
+ const requestedFen = queryFen;
763
+ const requestedFilterKey = positionCacheKey(requestedFen);
764
+ if (positionsByFenRef.current.has(requestedFilterKey)) {
765
+ setPosition(positionsByFenRef.current.get(requestedFilterKey));
766
+ setLoadedPositionFen(requestedFen);
767
+ setLoadedPositionFilterKey(requestedFilterKey);
768
+ setLoading(false);
769
+ setError(null);
770
+ }
771
+ else {
772
+ setLoading(true);
773
+ setError(null);
774
+ }
548
775
  (() => __awaiter(this, void 0, void 0, function* () {
549
776
  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)
777
+ const pos = yield fetchPosition({
778
+ fen: requestedFen,
779
+ sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
780
+ });
781
+ if (cancelled || requestedFen !== queryFenRef.current)
562
782
  return;
563
- setPosition(pos);
564
- setGames(gameList);
565
- if (!pos) {
566
- setError("No explorer data for this position yet");
783
+ if (pos) {
784
+ positionsByFenRef.current.set(positionCacheKey(requestedFen), pos);
567
785
  }
786
+ setPosition(pos);
787
+ setLoadedPositionFen(requestedFen);
788
+ setLoadedPositionFilterKey(positionCacheKey(requestedFen));
568
789
  }
569
790
  catch (e) {
791
+ if (cancelled || requestedFen !== queryFenRef.current)
792
+ return;
793
+ setPosition(null);
794
+ setLoadedPositionFen(null);
795
+ setLoadedPositionFilterKey(null);
796
+ setError(e instanceof Error ? e.message : "Failed to load explorer data");
797
+ }
798
+ finally {
799
+ if (!cancelled && requestedFen === queryFenRef.current) {
800
+ setLoading(false);
801
+ }
802
+ }
803
+ }))();
804
+ return () => {
805
+ cancelled = true;
806
+ };
807
+ }, [queryFen, sources, fetchPosition, positionCacheKey]);
808
+ const activePositionFilterKey = positionCacheKey(queryFen);
809
+ const cachedPosition = positionsByFenRef.current.get(activePositionFilterKey);
810
+ const positionReady = loadedPositionFen === queryFen &&
811
+ loadedPositionFilterKey === activePositionFilterKey;
812
+ const displayPosition = cachedPosition !== null && cachedPosition !== void 0 ? cachedPosition : (positionReady ? position : null);
813
+ const displayMoves = Array.isArray(displayPosition === null || displayPosition === void 0 ? void 0 : displayPosition.moves)
814
+ ? displayPosition.moves
815
+ : [];
816
+ const showPositionLoading = loading && displayMoves.length === 0;
817
+ const isStartPosition = normalizeFen(queryFen) === normalizeFen(EXPLORER_START_FEN);
818
+ useExplorerPrefetch({
819
+ fen: queryFen,
820
+ moves: displayMoves,
821
+ positionReady,
822
+ sources,
823
+ fetchPositionGames,
824
+ fetchPositionVariations,
825
+ });
826
+ useEffect(() => {
827
+ return subscribeExplorerSessionCache(() => {
828
+ const key = gamesCacheKey(queryFen, gamesMoveFilterUci);
829
+ const cached = peekSessionGames(key);
830
+ if (!cached) {
831
+ return;
832
+ }
833
+ setGames(cached);
834
+ setGamesLoading(false);
835
+ });
836
+ }, [queryFen, gamesMoveFilterUci, gamesCacheKey]);
837
+ useEffect(() => {
838
+ const canLoadGames = positionReady || (isStartPosition && !gamesMoveFilterUci);
839
+ if (!canLoadGames)
840
+ return;
841
+ let cancelled = false;
842
+ const requestedKey = gamesCacheKey(queryFen, gamesMoveFilterUci);
843
+ const cachedGames = peekSessionGames(requestedKey);
844
+ if (cachedGames) {
845
+ setGames(cachedGames);
846
+ setGamesLoading(false);
847
+ }
848
+ else {
849
+ setGamesLoading(true);
850
+ }
851
+ void (() => __awaiter(this, void 0, void 0, function* () {
852
+ try {
853
+ const gameList = yield fetchPositionGames({
854
+ fen: queryFen,
855
+ uci: gamesMoveFilterUci,
856
+ sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
857
+ });
858
+ if (cancelled)
859
+ return;
860
+ setSessionGames(requestedKey, gameList);
861
+ setGames(gameList);
862
+ }
863
+ catch (_a) {
570
864
  if (!cancelled) {
571
- setError(e instanceof Error ? e.message : "Failed to load explorer data");
865
+ setGames(null);
572
866
  }
573
867
  }
574
868
  finally {
575
869
  if (!cancelled)
576
- setLoading(false);
870
+ setGamesLoading(false);
577
871
  }
578
872
  }))();
579
873
  return () => {
580
874
  cancelled = true;
581
875
  };
582
876
  }, [
583
- fen,
584
- minElo,
585
- maxElo,
877
+ queryFen,
878
+ positionReady,
879
+ isStartPosition,
586
880
  gamesMoveFilterUci,
587
- topOnly,
588
881
  sources,
589
- fetchPosition,
590
882
  fetchPositionGames,
883
+ gamesCacheKey,
591
884
  ]);
592
885
  const handleMoveSelect = useCallback((move) => {
593
886
  if (isAnimatingVariationRef.current) {
594
887
  cancelVariationAnimation();
595
- setBoardFen(fen);
888
+ setBoardFen(queryFen);
596
889
  setSelectedVariationKey(undefined);
597
890
  }
598
- const nextFen = fenAfterUci(fen, move.uci);
891
+ const nextFen = fenAfterUci(queryFen, move.uci);
599
892
  if (!nextFen)
600
893
  return;
601
894
  pushEntry(nextFen, move.san);
602
895
  setSelectedVariationKey(undefined);
603
896
  applyNavigation(nextFen, true);
604
- }, [fen, pushEntry, applyNavigation, cancelVariationAnimation]);
897
+ }, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
605
898
  const handleLineSelect = useCallback((line) => {
606
899
  if (isAnimatingVariationRef.current)
607
900
  return;
608
901
  cancelVariationAnimation();
609
- let currentFen = fen;
902
+ let lineFen = queryFen;
610
903
  const entries = [];
611
904
  for (let i = 0; i < line.uciPath.length; i += 1) {
612
- const nextFen = fenAfterUci(currentFen, line.uciPath[i]);
905
+ const nextFen = fenAfterUci(lineFen, line.uciPath[i]);
613
906
  if (!nextFen)
614
907
  return;
615
908
  entries.push({ fen: nextFen, lastSan: line.moves[i].san });
616
- currentFen = nextFen;
909
+ lineFen = nextFen;
617
910
  }
618
911
  if (entries.length === 0)
619
912
  return;
@@ -637,109 +930,162 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
637
930
  }
638
931
  };
639
932
  tick();
640
- }, [fen, pushEntries, applyNavigation, cancelVariationAnimation]);
933
+ }, [queryFen, pushEntries, applyNavigation, cancelVariationAnimation]);
641
934
  const handlePieceDrop = useCallback((sourceSquare, targetSquare, piece) => {
642
935
  if (isAnimatingVariationRef.current) {
643
936
  cancelVariationAnimation();
644
- setBoardFen(fen);
937
+ setBoardFen(queryFen);
645
938
  setSelectedVariationKey(undefined);
646
939
  }
647
- const result = applyBoardMove(fen, sourceSquare, targetSquare, piece);
940
+ const result = applyBoardMove(queryFen, sourceSquare, targetSquare, piece);
648
941
  if (!result)
649
942
  return false;
650
943
  pushEntry(result.fen, result.san);
651
944
  setSelectedVariationKey(undefined);
652
945
  applyNavigation(result.fen, true);
653
946
  return true;
654
- }, [fen, pushEntry, applyNavigation, cancelVariationAnimation]);
947
+ }, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
655
948
  const handleBack = useCallback(() => {
656
949
  if (isAnimatingVariationRef.current) {
657
950
  cancelVariationAnimation();
658
- setBoardFen(fen);
951
+ setBoardFen(queryFen);
659
952
  setSelectedVariationKey(undefined);
660
953
  return;
661
954
  }
662
955
  const entry = goBack();
663
956
  if (entry)
664
957
  applyNavigation(entry.fen, true);
665
- }, [fen, goBack, applyNavigation, cancelVariationAnimation]);
958
+ }, [queryFen, goBack, applyNavigation, cancelVariationAnimation]);
666
959
  const handleForward = useCallback(() => {
667
960
  if (isAnimatingVariationRef.current) {
668
961
  cancelVariationAnimation();
669
- setBoardFen(fen);
962
+ setBoardFen(queryFen);
670
963
  setSelectedVariationKey(undefined);
671
964
  return;
672
965
  }
673
966
  const entry = goForward();
674
967
  if (entry)
675
968
  applyNavigation(entry.fen, true);
676
- }, [fen, goForward, applyNavigation, cancelVariationAnimation]);
969
+ }, [queryFen, goForward, applyNavigation, cancelVariationAnimation]);
970
+ const handleFirst = useCallback(() => {
971
+ if (isAnimatingVariationRef.current) {
972
+ cancelVariationAnimation();
973
+ setBoardFen(queryFen);
974
+ setSelectedVariationKey(undefined);
975
+ return;
976
+ }
977
+ const entry = goFirst();
978
+ if (entry)
979
+ applyNavigation(entry.fen, true);
980
+ }, [queryFen, goFirst, applyNavigation, cancelVariationAnimation]);
981
+ const handleLast = useCallback(() => {
982
+ if (isAnimatingVariationRef.current) {
983
+ cancelVariationAnimation();
984
+ setBoardFen(queryFen);
985
+ setSelectedVariationKey(undefined);
986
+ return;
987
+ }
988
+ const entry = goLast();
989
+ if (entry)
990
+ applyNavigation(entry.fen, true);
991
+ }, [queryFen, goLast, applyNavigation, cancelVariationAnimation]);
677
992
  const lineLabel = useMemo(() => {
678
993
  if (lineSans.length > 0) {
679
- return lineSans.join(" ");
994
+ return formatNumberedLineSans(lineSans);
680
995
  }
681
- if (position) {
996
+ if (position && typeof position.totalGames === "number") {
682
997
  return `Starting position (${position.totalGames.toLocaleString()} games)`;
683
998
  }
684
999
  return "";
685
1000
  }, [lineSans, position]);
686
1001
  return {
687
- fen,
1002
+ fen: queryFen,
688
1003
  boardFen,
689
1004
  position,
690
1005
  games,
691
1006
  gamesMoveFilterUci,
692
- minElo,
693
- maxElo,
694
- topOnly,
695
1007
  sources,
1008
+ lineSans,
696
1009
  loading,
1010
+ showPositionLoading,
1011
+ gamesLoading,
1012
+ positionReady,
1013
+ displayMoves,
697
1014
  error,
698
1015
  lineLabel,
699
1016
  canGoBack,
700
1017
  canGoForward,
701
- variationsTab,
702
1018
  forwardSans,
703
1019
  selectedVariationKey,
704
- setMinElo,
705
- setMaxElo,
706
- setTopOnly,
707
1020
  setSources,
708
- setVariationsTab,
709
1021
  setGamesMoveFilterUci,
710
1022
  handleMoveSelect,
711
1023
  handleLineSelect,
712
1024
  handlePieceDrop,
713
1025
  handleBack,
714
1026
  handleForward,
1027
+ handleFirst,
1028
+ handleLast,
715
1029
  };
716
1030
  }
717
1031
 
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);
1032
+ function useVariationLines({ fen, fetchPositionVariations, lineCount = EXPLORER_DEFAULT_VARIATION_LINE_COUNT, lineDepth = EXPLORER_DEFAULT_VARIATION_DEPTH, enabled = true, }) {
1033
+ const cacheKey = useMemo(() => variationsSessionKey(fen, lineCount, lineDepth), [fen, lineCount, lineDepth]);
1034
+ const [lines, setLines] = useState(() => {
1035
+ var _a;
1036
+ if (!enabled) {
1037
+ return [];
1038
+ }
1039
+ return (_a = peekSessionVariations(cacheKey)) !== null && _a !== void 0 ? _a : [];
1040
+ });
1041
+ const [loading, setLoading] = useState(() => {
1042
+ if (!enabled) {
1043
+ return false;
1044
+ }
1045
+ return peekSessionVariations(cacheKey) === undefined;
1046
+ });
1047
+ useEffect(() => {
1048
+ return subscribeExplorerSessionCache(() => {
1049
+ if (!enabled) {
1050
+ return;
1051
+ }
1052
+ const cachedLines = peekSessionVariations(cacheKey);
1053
+ if (!cachedLines) {
1054
+ return;
1055
+ }
1056
+ setLines(cachedLines);
1057
+ setLoading(false);
1058
+ });
1059
+ }, [enabled, cacheKey]);
721
1060
  useEffect(() => {
722
- if (!enabled || tab === "endgames") {
1061
+ if (!enabled) {
723
1062
  setLines([]);
724
1063
  setLoading(false);
725
1064
  return;
726
1065
  }
727
1066
  let cancelled = false;
728
- setLoading(true);
729
- (() => __awaiter(this, void 0, void 0, function* () {
1067
+ const cachedLines = peekSessionVariations(cacheKey);
1068
+ if (cachedLines) {
1069
+ setLines(cachedLines);
1070
+ setLoading(false);
1071
+ }
1072
+ else {
1073
+ setLoading(true);
1074
+ }
1075
+ void (() => __awaiter(this, void 0, void 0, function* () {
730
1076
  var _a;
731
1077
  try {
732
1078
  const result = yield fetchPositionVariations({
733
1079
  fen,
734
- mode: tab,
735
- minElo,
736
- maxElo,
1080
+ mode: "variations",
737
1081
  lineCount,
738
1082
  depth: lineDepth,
739
1083
  });
740
1084
  if (cancelled)
741
1085
  return;
742
- setLines((_a = result === null || result === void 0 ? void 0 : result.lines) !== null && _a !== void 0 ? _a : []);
1086
+ const nextLines = (_a = result === null || result === void 0 ? void 0 : result.lines) !== null && _a !== void 0 ? _a : [];
1087
+ setSessionVariations(cacheKey, nextLines);
1088
+ setLines(nextLines);
743
1089
  }
744
1090
  catch (_b) {
745
1091
  if (!cancelled) {
@@ -755,21 +1101,11 @@ function useVariationLines({ fen, tab, minElo, maxElo, fetchPositionVariations,
755
1101
  return () => {
756
1102
  cancelled = true;
757
1103
  };
758
- }, [
759
- enabled,
760
- fen,
761
- tab,
762
- minElo,
763
- maxElo,
764
- fetchPositionVariations,
765
- lineCount,
766
- lineDepth,
767
- ]);
768
- return { lines, loading };
1104
+ }, [enabled, fen, fetchPositionVariations, lineCount, lineDepth, cacheKey]);
1105
+ return { lines, loading: enabled ? loading : false };
769
1106
  }
770
1107
 
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;
1108
+ 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
1109
  const referenceData = usePositionReferenceData({
774
1110
  fenProp,
775
1111
  onFenChange,
@@ -777,59 +1113,96 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
777
1113
  onLineSansChange,
778
1114
  fetchPosition,
779
1115
  fetchPositionGames,
780
- defaultMinElo,
781
- defaultMaxElo,
1116
+ fetchPositionVariations,
1117
+ });
1118
+ 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;
1119
+ usePositionKeyboardNav({
1120
+ enabled: keyboardNav,
1121
+ canPrev: canGoBack,
1122
+ canNext: canGoForward,
1123
+ onPrev: handleBack,
1124
+ onNext: handleForward,
1125
+ onFirst: handleFirst,
1126
+ onLast: handleLast,
782
1127
  });
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;
1128
+ const [variationsEnabled, setVariationsEnabled] = useState(() => normalizeFen(fen) === normalizeFen(EXPLORER_START_FEN));
1129
+ const isStartPosition = normalizeFen(fen) === normalizeFen(EXPLORER_START_FEN);
1130
+ useEffect(() => {
1131
+ const cachedVariations = peekSessionVariations(variationsSessionKey(fen, EXPLORER_DEFAULT_VARIATION_LINE_COUNT, EXPLORER_DEFAULT_VARIATION_DEPTH));
1132
+ if (cachedVariations) {
1133
+ setVariationsEnabled(true);
1134
+ return;
1135
+ }
1136
+ if (!positionReady && !isStartPosition) {
1137
+ setVariationsEnabled(false);
1138
+ return;
1139
+ }
1140
+ if (loading && !isStartPosition) {
1141
+ setVariationsEnabled(false);
1142
+ return;
1143
+ }
1144
+ setVariationsEnabled(true);
1145
+ }, [fen, positionReady, loading, isStartPosition]);
784
1146
  const { lines: variationLines, loading: variationLinesLoading } = useVariationLines({
785
1147
  fen,
786
- tab: variationsTab,
787
- minElo,
788
- maxElo,
789
1148
  fetchPositionVariations,
790
- enabled: Boolean(position),
1149
+ enabled: variationsEnabled && (positionReady || isStartPosition),
791
1150
  });
1151
+ const [internalBoardOrientation, setInternalBoardOrientation] = useState(defaultBoardOrientation);
1152
+ const boardOrientation = boardOrientationProp !== null && boardOrientationProp !== void 0 ? boardOrientationProp : internalBoardOrientation;
1153
+ const handleFlipBoard = useCallback(() => {
1154
+ const nextOrientation = boardOrientation === "white" ? "black" : "white";
1155
+ if (boardOrientationProp === undefined) {
1156
+ setInternalBoardOrientation(nextOrientation);
1157
+ }
1158
+ onBoardOrientationChange === null || onBoardOrientationChange === void 0 ? void 0 : onBoardOrientationChange(nextOrientation);
1159
+ }, [boardOrientation, boardOrientationProp, onBoardOrientationChange]);
1160
+ const boardNavProps = useMemo(() => ({
1161
+ canGoBack,
1162
+ canGoForward,
1163
+ onBack: handleBack,
1164
+ onForward: handleForward,
1165
+ boardOrientation,
1166
+ onFlipBoard: handleFlipBoard,
1167
+ }), [
1168
+ boardOrientation,
1169
+ canGoBack,
1170
+ canGoForward,
1171
+ handleBack,
1172
+ handleFlipBoard,
1173
+ handleForward,
1174
+ ]);
792
1175
  const outerStyle = {
793
1176
  width: "100%",
794
1177
  height: fillHeight ? "100%" : "auto",
795
1178
  minHeight: layoutMinHeight !== null && layoutMinHeight !== void 0 ? layoutMinHeight : DEFAULT_REFERENCE_LAYOUT.minHeight,
796
- overflow: "hidden",
1179
+ overflow: fillHeight ? "hidden" : "visible",
797
1180
  boxSizing: "border-box",
798
1181
  };
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 : [],
1182
+ 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)] }));
1183
+ const referencePanel = (jsx(DefaultReferencePanel, { theme: theme, fillHeight: fillHeight, status: renderStatus({
1184
+ error,
1185
+ loading: showPositionLoading,
1186
+ }), moveStats: renderMoveStats({
1187
+ moves: displayMoves,
807
1188
  onMoveSelect: handleMoveSelect,
808
1189
  }), variationsStrip: renderVariationsStrip({
809
1190
  theme,
810
- tab: variationsTab,
811
- onTabChange: setVariationsTab,
812
1191
  lines: variationLines,
813
- loading: variationLinesLoading,
1192
+ loading: !variationsEnabled || variationLinesLoading,
814
1193
  selectedLineKey: selectedVariationKey,
815
1194
  forwardSans,
816
1195
  onLineSelect: handleLineSelect,
817
1196
  }), gamesPanel: renderGamesPanel({
818
- games: (_b = games === null || games === void 0 ? void 0 : games.games) !== null && _b !== void 0 ? _b : [],
1197
+ games: Array.isArray(games === null || games === void 0 ? void 0 : games.games) ? games.games : [],
1198
+ loading: gamesLoading,
819
1199
  lineLabel,
820
- minElo,
821
- maxElo,
822
- defaultMinElo,
823
- defaultMaxElo,
824
- topOnly,
1200
+ lineSans,
825
1201
  sources,
826
- onMinEloChange: setMinElo,
827
- onMaxEloChange: setMaxElo,
828
- onTopOnlyChange: setTopOnly,
829
1202
  onSourcesChange: setSources,
830
1203
  onGameSelect,
831
1204
  }) }));
832
- return (jsx(ThemeProvider, { theme: theme, children: jsx("div", { style: outerStyle, children: renderLayout({ theme, board, referencePanel }) }) }));
1205
+ return (jsx(ThemeProvider, { theme: theme, boardTheme: boardTheme, children: jsx("div", { style: outerStyle, children: renderLayout({ theme, board, referencePanel }) }) }));
833
1206
  };
834
1207
 
835
1208
  /** Reference explorer with library default layout and renderers (ChessBase-style grid). */
@@ -1040,10 +1413,6 @@ const mockPosition = {
1040
1413
  };
1041
1414
  const mockGames = {
1042
1415
  positionKey: "mock-start",
1043
- fen: EXPLORER_START_FEN,
1044
- minElo: 2200,
1045
- maxElo: 2800,
1046
- topOnly: false,
1047
1416
  games: [
1048
1417
  {
1049
1418
  gameId: "abc123",
@@ -1075,7 +1444,7 @@ const mockGames = {
1075
1444
  },
1076
1445
  ],
1077
1446
  };
1078
- function mockFetchPosition(fen) {
1447
+ function mockFetchPosition(params) {
1079
1448
  return __awaiter(this, void 0, void 0, function* () {
1080
1449
  return mockPosition;
1081
1450
  });
@@ -1109,7 +1478,11 @@ function mockVariationLines(params, position) {
1109
1478
  }
1110
1479
  function mockFetchPositionVariations(params) {
1111
1480
  return __awaiter(this, void 0, void 0, function* () {
1112
- const position = yield mockFetchPosition(params.fen);
1481
+ const position = yield mockFetchPosition({
1482
+ fen: params.fen,
1483
+ minElo: params.minElo,
1484
+ maxElo: params.maxElo,
1485
+ });
1113
1486
  if (!position)
1114
1487
  return null;
1115
1488
  return mockVariationLines(params, position);
@@ -1117,15 +1490,26 @@ function mockFetchPositionVariations(params) {
1117
1490
  }
1118
1491
  function mockFetchPositionGames(params) {
1119
1492
  return __awaiter(this, void 0, void 0, function* () {
1120
- var _a;
1493
+ var _a, _b, _c;
1121
1494
  let filtered = params.uci
1122
1495
  ? mockGames.games.filter((g) => g.nextUci === params.uci)
1123
1496
  : mockGames.games;
1124
1497
  if ((_a = params.sources) === null || _a === void 0 ? void 0 : _a.length) {
1125
1498
  filtered = filtered.filter((game) => params.sources.includes(game.source));
1126
1499
  }
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 });
1500
+ const offset = (_b = params.offset) !== null && _b !== void 0 ? _b : 0;
1501
+ const limit = (_c = params.limit) !== null && _c !== void 0 ? _c : filtered.length;
1502
+ const page = filtered.slice(offset, offset + limit);
1503
+ const hasMore = offset + limit < filtered.length;
1504
+ return {
1505
+ positionKey: mockGames.positionKey,
1506
+ fen: params.fen,
1507
+ uci: params.uci,
1508
+ offset,
1509
+ hasMore,
1510
+ games: page,
1511
+ };
1128
1512
  });
1129
1513
  }
1130
1514
 
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 };
1515
+ 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 };