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.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,24 @@ 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" }), onGameSelect && jsxRuntime.jsx("th", { style: thStyle })] }) }), jsxRuntime.jsx("tbody", { children: games.length === 0 ? (jsxRuntime.jsx("tr", { children: jsxRuntime.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) => (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 }), 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, onMinEloChange, onMaxEloChange, onTopOnlyChange, onGameSelect, }) => (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"] })] })] }));
329
+ const PositionGamesPanel = ({ games, lineLabel, lineSans: _lineSans, sources, onSourcesChange, onGameSelect, }) => {
330
+ const toggleSource = (source) => {
331
+ if (sources.includes(source)) {
332
+ if (sources.length === 1) {
333
+ return;
334
+ }
335
+ onSourcesChange(sources.filter((value) => value !== source));
336
+ return;
337
+ }
338
+ onSourcesChange([...sources, source]);
339
+ };
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"] })] })] }));
341
+ };
295
342
 
296
343
  const DefaultReferenceLayout = ({ board, referencePanel, }) => (jsxRuntime.jsxs("div", { style: referenceShellStyle, children: [jsxRuntime.jsx("div", { style: boardColumnStyle, children: board }), referencePanel] }));
297
344
  const defaultRenderLayout = (props) => (jsxRuntime.jsx(DefaultReferenceLayout, Object.assign({}, props)));
@@ -303,34 +350,37 @@ function isVariationLineActive(line, selectedLineKey, forwardSans = []) {
303
350
  if (forwardSans.length === 0) {
304
351
  return false;
305
352
  }
306
- 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; });
307
358
  }
308
359
 
309
- 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" }));
310
- 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: {
311
- minWidth: 0,
312
- maxHeight: 132,
313
- overflow: "auto",
314
- }, 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) => {
315
- var _a, _b;
316
- const active = isVariationLineActive(line, selectedLineKey, forwardSans);
317
- return (jsxRuntime.jsxs("button", { type: "button", onClick: () => onLineSelect(line), style: {
318
- display: "flex",
319
- width: "100%",
320
- gap: 12,
321
- alignItems: "baseline",
322
- border: "none",
323
- background: "transparent",
324
- padding: "2px 0",
325
- cursor: "pointer",
326
- textAlign: "left",
327
- color: active ? "#2e7d32" : "inherit",
328
- font: "inherit",
329
- fontSize: 12,
330
- }, 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
331
- ? `Last played ${line.lastPlayedYear}`
332
- : "Last played —" }), jsxRuntime.jsx("span", { children: (_b = line.avgElo) !== null && _b !== void 0 ? _b : "—" })] }, line.key));
333
- })) })] }));
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
+ })) }) }));
334
384
  const defaultRenderVariationsStrip = (props) => jsxRuntime.jsx(DefaultVariationsStrip, Object.assign({}, props));
335
385
 
336
386
  const defaultRenderStatus = (props) => (jsxRuntime.jsx(ExplorerStatusBanner, Object.assign({}, props)));
@@ -369,11 +419,105 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
369
419
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
370
420
  };
371
421
 
372
- function usePositionHistory(initialFen) {
373
- const [history, setHistory] = react.useState([
374
- { fen: initialFen },
422
+ const ALL_GAME_SOURCES = ["lichess", "twic"];
423
+
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,
375
501
  ]);
376
- 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);
377
521
  const canGoBack = historyIndex > 0;
378
522
  const canGoForward = historyIndex < history.length - 1;
379
523
  const pushEntry = react.useCallback((fen, lastSan) => {
@@ -401,6 +545,20 @@ function usePositionHistory(initialFen) {
401
545
  setHistoryIndex(nextIndex);
402
546
  return entry;
403
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]);
404
562
  const resetHistory = react.useCallback((fen) => {
405
563
  const entry = { fen };
406
564
  setHistory([entry]);
@@ -425,38 +583,54 @@ function usePositionHistory(initialFen) {
425
583
  .slice(historyIndex + 1)
426
584
  .map((entry) => entry.lastSan)
427
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
+ }, []);
428
591
  return {
429
592
  canGoBack,
430
593
  canGoForward,
431
594
  lineSans,
432
595
  forwardSans,
596
+ currentFen,
433
597
  pushEntry,
434
598
  pushEntries,
599
+ replaceLineEntries,
435
600
  goBack,
436
601
  goForward,
602
+ goFirst,
603
+ goLast,
437
604
  resetHistory,
438
605
  };
439
606
  }
440
607
 
441
- 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;
442
610
  const initialFen = fenProp !== null && fenProp !== void 0 ? fenProp : EXPLORER_START_FEN;
443
- const [fen, setFen] = react.useState(initialFen);
444
- /** Board display FEN; may lead {@link fen} while a variation line is animating. */
445
- 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);
446
618
  const [position, setPosition] = react.useState(null);
447
- const [games, setGames] = react.useState(null);
619
+ const [games, setGames] = react.useState(() => initialCachedGames !== null && initialCachedGames !== void 0 ? initialCachedGames : null);
448
620
  /** Filter games to those that played this UCI from the current FEN (optional). */
449
621
  const [gamesMoveFilterUci, setGamesMoveFilterUci] = react.useState();
450
- const [minElo, setMinElo] = react.useState(defaultMinElo);
451
- const [maxElo, setMaxElo] = react.useState(defaultMaxElo);
452
- const [topOnly, setTopOnly] = react.useState(false);
622
+ const [sources, setSources] = react.useState([...ALL_GAME_SOURCES]);
453
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);
454
628
  const [error, setError] = react.useState(null);
455
- const [variationsTab, setVariationsTab] = react.useState("variations");
456
629
  const [selectedVariationKey, setSelectedVariationKey] = react.useState();
457
630
  const variationAnimationTimerRef = react.useRef(null);
458
631
  const isAnimatingVariationRef = react.useRef(false);
459
- 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);
460
634
  const cancelVariationAnimation = react.useCallback(() => {
461
635
  if (variationAnimationTimerRef.current !== null) {
462
636
  clearTimeout(variationAnimationTimerRef.current);
@@ -464,7 +638,17 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
464
638
  }
465
639
  isAnimatingVariationRef.current = false;
466
640
  }, []);
467
- 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]);
468
652
  const applyNavigation = react.useCallback((nextFen, clearMoveFilter = true) => {
469
653
  setFen(nextFen);
470
654
  setBoardFen(nextFen);
@@ -473,39 +657,72 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
473
657
  }
474
658
  onFenChange === null || onFenChange === void 0 ? void 0 : onFenChange(nextFen);
475
659
  }, [onFenChange]);
476
- const initialLineKey = react.useMemo(() => { var _a; return (_a = initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.join("|")) !== null && _a !== void 0 ? _a : ""; }, [initialLineSans]);
477
- 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);
478
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).
479
678
  react.useEffect(() => {
480
- if (lastAppliedLineKeyRef.current === initialLineKey)
481
- return;
482
- const currentLineKey = lineSans.join("|");
483
- if (currentLineKey === initialLineKey) {
484
- lastAppliedLineKeyRef.current = initialLineKey;
679
+ if (initialLineSans === undefined && onLineSansChange === undefined)
485
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
+ }
486
692
  }
487
693
  lastAppliedLineKeyRef.current = initialLineKey;
488
694
  cancelVariationAnimation();
489
- resetHistory(EXPLORER_START_FEN);
490
695
  setSelectedVariationKey(undefined);
491
696
  if (initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.length) {
492
697
  const result = applyLineSans(EXPLORER_START_FEN, initialLineSans);
493
698
  if (result) {
494
- pushEntries(result.entries);
699
+ replaceLineEntries(EXPLORER_START_FEN, result.entries);
495
700
  applyNavigation(result.fen, true);
496
701
  return;
497
702
  }
498
703
  }
704
+ replaceLineEntries(EXPLORER_START_FEN, []);
499
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
500
708
  }, [
501
709
  initialLineKey,
502
710
  initialLineSans,
503
- lineSans,
711
+ expectedLineFen,
504
712
  cancelVariationAnimation,
505
- resetHistory,
506
- pushEntries,
713
+ replaceLineEntries,
507
714
  applyNavigation,
508
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]);
509
726
  react.useEffect(() => {
510
727
  if (readyForLineSyncRef.current)
511
728
  return;
@@ -517,88 +734,171 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
517
734
  }
518
735
  }, [lineSans, initialLineSans]);
519
736
  react.useEffect(() => {
737
+ if (!onLineSansChange)
738
+ return;
520
739
  if (!readyForLineSyncRef.current)
521
740
  return;
522
- onLineSansChange === null || onLineSansChange === void 0 ? void 0 : onLineSansChange(lineSans);
741
+ onLineSansChange(lineSans);
523
742
  }, [lineSans, onLineSansChange]);
524
743
  react.useEffect(() => {
525
- if (fenProp === undefined || fenProp === fen)
744
+ if (fenProp === undefined)
745
+ return;
746
+ if (fenProp === queryFenRef.current)
526
747
  return;
527
748
  cancelVariationAnimation();
528
749
  resetHistory(fenProp);
529
750
  applyNavigation(fenProp, true);
530
- }, [fenProp, fen, resetHistory, applyNavigation, cancelVariationAnimation]);
751
+ }, [fenProp, cancelVariationAnimation, resetHistory, applyNavigation]);
531
752
  react.useEffect(() => {
532
753
  let cancelled = false;
533
- setLoading(true);
534
- 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
+ }
535
767
  (() => __awaiter(this, void 0, void 0, function* () {
536
768
  try {
537
- const [pos, gameList] = yield Promise.all([
538
- fetchPosition(fen),
539
- fetchPositionGames({
540
- fen,
541
- minElo,
542
- maxElo,
543
- uci: gamesMoveFilterUci,
544
- topOnly,
545
- }),
546
- ]);
547
- 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)
548
774
  return;
549
- setPosition(pos);
550
- setGames(gameList);
551
- if (!pos) {
552
- setError("No explorer data for this position yet");
775
+ if (pos) {
776
+ positionsByFenRef.current.set(positionCacheKey(requestedFen), pos);
553
777
  }
778
+ setPosition(pos);
779
+ setLoadedPositionFen(requestedFen);
780
+ setLoadedPositionFilterKey(positionCacheKey(requestedFen));
554
781
  }
555
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) {
556
856
  if (!cancelled) {
557
- setError(e instanceof Error ? e.message : "Failed to load explorer data");
857
+ setGames(null);
558
858
  }
559
859
  }
560
860
  finally {
561
861
  if (!cancelled)
562
- setLoading(false);
862
+ setGamesLoading(false);
563
863
  }
564
864
  }))();
565
865
  return () => {
566
866
  cancelled = true;
567
867
  };
568
868
  }, [
569
- fen,
570
- minElo,
571
- maxElo,
869
+ queryFen,
870
+ positionReady,
871
+ isStartPosition,
572
872
  gamesMoveFilterUci,
573
- topOnly,
574
- fetchPosition,
873
+ sources,
575
874
  fetchPositionGames,
875
+ gamesCacheKey,
576
876
  ]);
577
877
  const handleMoveSelect = react.useCallback((move) => {
578
878
  if (isAnimatingVariationRef.current) {
579
879
  cancelVariationAnimation();
580
- setBoardFen(fen);
880
+ setBoardFen(queryFen);
581
881
  setSelectedVariationKey(undefined);
582
882
  }
583
- const nextFen = fenAfterUci(fen, move.uci);
883
+ const nextFen = fenAfterUci(queryFen, move.uci);
584
884
  if (!nextFen)
585
885
  return;
586
886
  pushEntry(nextFen, move.san);
587
887
  setSelectedVariationKey(undefined);
588
888
  applyNavigation(nextFen, true);
589
- }, [fen, pushEntry, applyNavigation, cancelVariationAnimation]);
889
+ }, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
590
890
  const handleLineSelect = react.useCallback((line) => {
591
891
  if (isAnimatingVariationRef.current)
592
892
  return;
593
893
  cancelVariationAnimation();
594
- let currentFen = fen;
894
+ let lineFen = queryFen;
595
895
  const entries = [];
596
896
  for (let i = 0; i < line.uciPath.length; i += 1) {
597
- const nextFen = fenAfterUci(currentFen, line.uciPath[i]);
897
+ const nextFen = fenAfterUci(lineFen, line.uciPath[i]);
598
898
  if (!nextFen)
599
899
  return;
600
900
  entries.push({ fen: nextFen, lastSan: line.moves[i].san });
601
- currentFen = nextFen;
901
+ lineFen = nextFen;
602
902
  }
603
903
  if (entries.length === 0)
604
904
  return;
@@ -622,107 +922,162 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
622
922
  }
623
923
  };
624
924
  tick();
625
- }, [fen, pushEntries, applyNavigation, cancelVariationAnimation]);
925
+ }, [queryFen, pushEntries, applyNavigation, cancelVariationAnimation]);
626
926
  const handlePieceDrop = react.useCallback((sourceSquare, targetSquare, piece) => {
627
927
  if (isAnimatingVariationRef.current) {
628
928
  cancelVariationAnimation();
629
- setBoardFen(fen);
929
+ setBoardFen(queryFen);
630
930
  setSelectedVariationKey(undefined);
631
931
  }
632
- const result = applyBoardMove(fen, sourceSquare, targetSquare, piece);
932
+ const result = applyBoardMove(queryFen, sourceSquare, targetSquare, piece);
633
933
  if (!result)
634
934
  return false;
635
935
  pushEntry(result.fen, result.san);
636
936
  setSelectedVariationKey(undefined);
637
937
  applyNavigation(result.fen, true);
638
938
  return true;
639
- }, [fen, pushEntry, applyNavigation, cancelVariationAnimation]);
939
+ }, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
640
940
  const handleBack = react.useCallback(() => {
641
941
  if (isAnimatingVariationRef.current) {
642
942
  cancelVariationAnimation();
643
- setBoardFen(fen);
943
+ setBoardFen(queryFen);
644
944
  setSelectedVariationKey(undefined);
645
945
  return;
646
946
  }
647
947
  const entry = goBack();
648
948
  if (entry)
649
949
  applyNavigation(entry.fen, true);
650
- }, [fen, goBack, applyNavigation, cancelVariationAnimation]);
950
+ }, [queryFen, goBack, applyNavigation, cancelVariationAnimation]);
651
951
  const handleForward = react.useCallback(() => {
652
952
  if (isAnimatingVariationRef.current) {
653
953
  cancelVariationAnimation();
654
- setBoardFen(fen);
954
+ setBoardFen(queryFen);
655
955
  setSelectedVariationKey(undefined);
656
956
  return;
657
957
  }
658
958
  const entry = goForward();
659
959
  if (entry)
660
960
  applyNavigation(entry.fen, true);
661
- }, [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]);
662
984
  const lineLabel = react.useMemo(() => {
663
985
  if (lineSans.length > 0) {
664
- return lineSans.join(" ");
986
+ return formatNumberedLineSans(lineSans);
665
987
  }
666
- if (position) {
988
+ if (position && typeof position.totalGames === "number") {
667
989
  return `Starting position (${position.totalGames.toLocaleString()} games)`;
668
990
  }
669
991
  return "";
670
992
  }, [lineSans, position]);
671
993
  return {
672
- fen,
994
+ fen: queryFen,
673
995
  boardFen,
674
996
  position,
675
997
  games,
676
998
  gamesMoveFilterUci,
677
- minElo,
678
- maxElo,
679
- topOnly,
999
+ sources,
1000
+ lineSans,
680
1001
  loading,
1002
+ showPositionLoading,
1003
+ gamesLoading,
1004
+ positionReady,
1005
+ displayMoves,
681
1006
  error,
682
1007
  lineLabel,
683
1008
  canGoBack,
684
1009
  canGoForward,
685
- variationsTab,
686
1010
  forwardSans,
687
1011
  selectedVariationKey,
688
- setMinElo,
689
- setMaxElo,
690
- setTopOnly,
691
- setVariationsTab,
1012
+ setSources,
692
1013
  setGamesMoveFilterUci,
693
1014
  handleMoveSelect,
694
1015
  handleLineSelect,
695
1016
  handlePieceDrop,
696
1017
  handleBack,
697
1018
  handleForward,
1019
+ handleFirst,
1020
+ handleLast,
698
1021
  };
699
1022
  }
700
1023
 
701
- function useVariationLines({ fen, tab, minElo, maxElo, fetchPositionVariations, lineCount = 8, lineDepth = 4, enabled = true, }) {
702
- const [lines, setLines] = react.useState([]);
703
- 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]);
704
1052
  react.useEffect(() => {
705
- if (!enabled || tab === "endgames") {
1053
+ if (!enabled) {
706
1054
  setLines([]);
707
1055
  setLoading(false);
708
1056
  return;
709
1057
  }
710
1058
  let cancelled = false;
711
- setLoading(true);
712
- (() => __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* () {
713
1068
  var _a;
714
1069
  try {
715
1070
  const result = yield fetchPositionVariations({
716
1071
  fen,
717
- mode: tab,
718
- minElo,
719
- maxElo,
1072
+ mode: "variations",
720
1073
  lineCount,
721
1074
  depth: lineDepth,
722
1075
  });
723
1076
  if (cancelled)
724
1077
  return;
725
- 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);
726
1081
  }
727
1082
  catch (_b) {
728
1083
  if (!cancelled) {
@@ -738,21 +1093,11 @@ function useVariationLines({ fen, tab, minElo, maxElo, fetchPositionVariations,
738
1093
  return () => {
739
1094
  cancelled = true;
740
1095
  };
741
- }, [
742
- enabled,
743
- fen,
744
- tab,
745
- minElo,
746
- maxElo,
747
- fetchPositionVariations,
748
- lineCount,
749
- lineDepth,
750
- ]);
751
- return { lines, loading };
1096
+ }, [enabled, fen, fetchPositionVariations, lineCount, lineDepth, cacheKey]);
1097
+ return { lines, loading: enabled ? loading : false };
752
1098
  }
753
1099
 
754
- 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, }) => {
755
- 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, }) => {
756
1101
  const referenceData = usePositionReferenceData({
757
1102
  fenProp,
758
1103
  onFenChange,
@@ -760,57 +1105,96 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
760
1105
  onLineSansChange,
761
1106
  fetchPosition,
762
1107
  fetchPositionGames,
763
- defaultMinElo,
764
- 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,
765
1119
  });
766
- 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;
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]);
767
1138
  const { lines: variationLines, loading: variationLinesLoading } = useVariationLines({
768
1139
  fen,
769
- tab: variationsTab,
770
- minElo,
771
- maxElo,
772
1140
  fetchPositionVariations,
773
- enabled: Boolean(position),
1141
+ enabled: variationsEnabled && (positionReady || isStartPosition),
774
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
+ ]);
775
1167
  const outerStyle = {
776
1168
  width: "100%",
777
1169
  height: fillHeight ? "100%" : "auto",
778
1170
  minHeight: layoutMinHeight !== null && layoutMinHeight !== void 0 ? layoutMinHeight : DEFAULT_REFERENCE_LAYOUT.minHeight,
779
- overflow: "hidden",
1171
+ overflow: fillHeight ? "hidden" : "visible",
780
1172
  boxSizing: "border-box",
781
1173
  };
782
- 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({
783
- canGoBack,
784
- canGoForward,
785
- onBack: handleBack,
786
- onForward: handleForward,
787
- })] }));
788
- const referencePanel = (jsxRuntime.jsx(DefaultReferencePanel, { theme: theme, status: renderStatus({ error, loading }), moveStats: renderMoveStats({
789
- 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,
790
1180
  onMoveSelect: handleMoveSelect,
791
1181
  }), variationsStrip: renderVariationsStrip({
792
1182
  theme,
793
- tab: variationsTab,
794
- onTabChange: setVariationsTab,
795
1183
  lines: variationLines,
796
- loading: variationLinesLoading,
1184
+ loading: !variationsEnabled || variationLinesLoading,
797
1185
  selectedLineKey: selectedVariationKey,
798
1186
  forwardSans,
799
1187
  onLineSelect: handleLineSelect,
800
1188
  }), gamesPanel: renderGamesPanel({
801
- 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,
802
1191
  lineLabel,
803
- minElo,
804
- maxElo,
805
- defaultMinElo,
806
- defaultMaxElo,
807
- topOnly,
808
- onMinEloChange: setMinElo,
809
- onMaxEloChange: setMaxElo,
810
- onTopOnlyChange: setTopOnly,
1192
+ lineSans,
1193
+ sources,
1194
+ onSourcesChange: setSources,
811
1195
  onGameSelect,
812
1196
  }) }));
813
- 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 }) }) }));
814
1198
  };
815
1199
 
816
1200
  /** Reference explorer with library default layout and renderers (ChessBase-style grid). */
@@ -982,7 +1366,7 @@ const GameReplayTrainer = ({ gameId, startFen, fetchGame, onExit, theme = "dark"
982
1366
  !error &&
983
1367
  !complete &&
984
1368
  feedback === "incorrect" &&
985
- 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" }))] }) }));
986
1370
  };
987
1371
 
988
1372
  const mockPosition = {
@@ -1018,14 +1402,9 @@ const mockPosition = {
1018
1402
  avgElo: 2435,
1019
1403
  },
1020
1404
  ],
1021
- sampleGameIds: ["abc123", "def456"],
1022
1405
  };
1023
1406
  const mockGames = {
1024
1407
  positionKey: "mock-start",
1025
- fen: EXPLORER_START_FEN,
1026
- minElo: 2200,
1027
- maxElo: 2800,
1028
- topOnly: false,
1029
1408
  games: [
1030
1409
  {
1031
1410
  gameId: "abc123",
@@ -1039,6 +1418,7 @@ const mockGames = {
1039
1418
  nextSan: "e4",
1040
1419
  nextUci: "e2e4",
1041
1420
  avgElo: 2843,
1421
+ source: "lichess",
1042
1422
  },
1043
1423
  {
1044
1424
  gameId: "def456",
@@ -1052,10 +1432,11 @@ const mockGames = {
1052
1432
  nextSan: "d4",
1053
1433
  nextUci: "d2d4",
1054
1434
  avgElo: 2778,
1435
+ source: "twic",
1055
1436
  },
1056
1437
  ],
1057
1438
  };
1058
- function mockFetchPosition(fen) {
1439
+ function mockFetchPosition(params) {
1059
1440
  return __awaiter(this, void 0, void 0, function* () {
1060
1441
  return mockPosition;
1061
1442
  });
@@ -1089,7 +1470,11 @@ function mockVariationLines(params, position) {
1089
1470
  }
1090
1471
  function mockFetchPositionVariations(params) {
1091
1472
  return __awaiter(this, void 0, void 0, function* () {
1092
- const position = yield mockFetchPosition(params.fen);
1473
+ const position = yield mockFetchPosition({
1474
+ fen: params.fen,
1475
+ minElo: params.minElo,
1476
+ maxElo: params.maxElo,
1477
+ });
1093
1478
  if (!position)
1094
1479
  return null;
1095
1480
  return mockVariationLines(params, position);
@@ -1097,18 +1482,37 @@ function mockFetchPositionVariations(params) {
1097
1482
  }
1098
1483
  function mockFetchPositionGames(params) {
1099
1484
  return __awaiter(this, void 0, void 0, function* () {
1100
- const filtered = params.uci
1485
+ var _a, _b, _c;
1486
+ let filtered = params.uci
1101
1487
  ? mockGames.games.filter((g) => g.nextUci === params.uci)
1102
1488
  : mockGames.games;
1103
- 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 });
1489
+ if ((_a = params.sources) === null || _a === void 0 ? void 0 : _a.length) {
1490
+ filtered = filtered.filter((game) => params.sources.includes(game.source));
1491
+ }
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
+ };
1104
1504
  });
1105
1505
  }
1106
1506
 
1507
+ exports.ALL_GAME_SOURCES = ALL_GAME_SOURCES;
1107
1508
  exports.DEFAULT_REFERENCE_LAYOUT = DEFAULT_REFERENCE_LAYOUT;
1108
1509
  exports.DefaultBoardNav = DefaultBoardNav;
1109
1510
  exports.DefaultReferenceLayout = DefaultReferenceLayout;
1110
1511
  exports.DefaultReferencePanel = DefaultReferencePanel;
1111
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;
1112
1516
  exports.EXPLORER_START_FEN = EXPLORER_START_FEN;
1113
1517
  exports.ExplorerPlaceholder = ExplorerPlaceholder;
1114
1518
  exports.GameReplayTrainer = GameReplayTrainer;
@@ -1124,10 +1528,19 @@ exports.defaultRenderVariationsStrip = defaultRenderVariationsStrip;
1124
1528
  exports.fenAfterUci = fenAfterUci;
1125
1529
  exports.fenAtPly = fenAtPly;
1126
1530
  exports.findPlyIndexForFen = findPlyIndexForFen;
1531
+ exports.formatNumberedLineSans = formatNumberedLineSans;
1532
+ exports.gamesSessionKey = gamesSessionKey;
1127
1533
  exports.isVariationLineActive = isVariationLineActive;
1128
1534
  exports.mockFetchPosition = mockFetchPosition;
1129
1535
  exports.mockFetchPositionGames = mockFetchPositionGames;
1130
1536
  exports.mockFetchPositionVariations = mockFetchPositionVariations;
1131
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;
1132
1544
  exports.useGameReplayTraining = useGameReplayTraining;
1545
+ exports.variationsSessionKey = variationsSessionKey;
1133
1546
  exports.whiteScorePercent = whiteScorePercent;