react-chess-explorer 0.0.1 → 0.0.3

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