react-chess-explorer 0.0.2 → 0.0.3

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