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