react-chess-explorer 0.0.1 → 0.0.2

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.
@@ -1,4 +1,4 @@
1
- import type { PositionGameRowApiDto } from "../types";
1
+ import type { GameSource, PositionGameRowApiDto } from "../types";
2
2
  export type PositionGamesPanelProps = {
3
3
  games: PositionGameRowApiDto[];
4
4
  lineLabel: string;
@@ -7,9 +7,11 @@ export type PositionGamesPanelProps = {
7
7
  defaultMinElo: number;
8
8
  defaultMaxElo: number;
9
9
  topOnly: boolean;
10
+ sources: GameSource[];
10
11
  onMinEloChange: (value: number) => void;
11
12
  onMaxEloChange: (value: number) => void;
12
13
  onTopOnlyChange: (value: boolean) => void;
14
+ onSourcesChange: (sources: GameSource[]) => void;
13
15
  onGameSelect?: (game: PositionGameRowApiDto) => void;
14
16
  };
15
- export declare const PositionGamesPanel: ({ games, lineLabel, minElo, maxElo, defaultMinElo, defaultMaxElo, topOnly, onMinEloChange, onMaxEloChange, onTopOnlyChange, onGameSelect, }: PositionGamesPanelProps) => import("react").JSX.Element;
17
+ export declare const PositionGamesPanel: ({ games, lineLabel, minElo, maxElo, defaultMinElo, defaultMaxElo, topOnly, sources, onMinEloChange, onMaxEloChange, onTopOnlyChange, onSourcesChange, onGameSelect, }: PositionGamesPanelProps) => import("react").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import type { FetchPositionGamesParams, FetchPositionVariationsParams, PositionApiDto, PositionGameRowApiDto, PositionGamesApiDto, PositionMoveApiDto, PositionVariationLineApiDto, PositionVariationsApiDto } from "../types";
2
+ import type { FetchPositionGamesParams, FetchPositionVariationsParams, GameSource, PositionApiDto, PositionGameRowApiDto, PositionGamesApiDto, PositionMoveApiDto, PositionVariationLineApiDto, PositionVariationsApiDto } from "../types";
3
3
  import type { VariationsTab } from "../variationLines";
4
4
  export type ReferenceLayoutRenderProps = {
5
5
  theme: "light" | "dark";
@@ -33,9 +33,11 @@ export type GamesPanelRenderProps = {
33
33
  defaultMinElo: number;
34
34
  defaultMaxElo: number;
35
35
  topOnly: boolean;
36
+ sources: GameSource[];
36
37
  onMinEloChange: (value: number) => void;
37
38
  onMaxEloChange: (value: number) => void;
38
39
  onTopOnlyChange: (value: boolean) => void;
40
+ onSourcesChange: (sources: GameSource[]) => void;
39
41
  /** Start replay training for a database game from the current board FEN. */
40
42
  onGameSelect?: (game: PositionGameRowApiDto) => void;
41
43
  };
@@ -1,4 +1,4 @@
1
- import type { FetchPositionGamesParams, PositionApiDto, PositionGamesApiDto, PositionMoveApiDto } from "../types";
1
+ import { type FetchPositionGamesParams, type GameSource, type PositionApiDto, type PositionGamesApiDto, type PositionMoveApiDto } from "../types";
2
2
  import type { PositionVariationLineApiDto } from "../types";
3
3
  import type { VariationsTab } from "../variationLines";
4
4
  export type UsePositionReferenceDataOptions = {
@@ -20,6 +20,7 @@ export declare function usePositionReferenceData({ fenProp, onFenChange, initial
20
20
  minElo: number;
21
21
  maxElo: number;
22
22
  topOnly: boolean;
23
+ sources: GameSource[];
23
24
  loading: boolean;
24
25
  error: string | null;
25
26
  lineLabel: string;
@@ -31,6 +32,7 @@ export declare function usePositionReferenceData({ fenProp, onFenChange, initial
31
32
  setMinElo: import("react").Dispatch<import("react").SetStateAction<number>>;
32
33
  setMaxElo: import("react").Dispatch<import("react").SetStateAction<number>>;
33
34
  setTopOnly: import("react").Dispatch<import("react").SetStateAction<boolean>>;
35
+ setSources: import("react").Dispatch<import("react").SetStateAction<GameSource[]>>;
34
36
  setVariationsTab: import("react").Dispatch<import("react").SetStateAction<VariationsTab>>;
35
37
  setGamesMoveFilterUci: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
36
38
  handleMoveSelect: (move: PositionMoveApiDto) => void;
@@ -8,7 +8,8 @@ export { DEFAULT_REFERENCE_LAYOUT, DefaultReferenceLayout, DefaultReferencePanel
8
8
  export type { ReferenceLayoutConfig } from "./referenceLayout";
9
9
  export { GameReplayTrainer, type GameReplayTrainerProps, } from "./GameReplayTrainer";
10
10
  export { useGameReplayTraining } from "./hooks/useGameReplayTraining";
11
- export type { PositionApiDto, PositionGamesApiDto, PositionGameRowApiDto, PositionMoveApiDto, PositionVariationLineApiDto, PositionVariationsApiDto, FetchPositionGamesParams, FetchPositionVariationsParams, ExplorerGameReplayApiDto, } from "./types";
11
+ export type { GameSource, PositionApiDto, PositionGamesApiDto, PositionGameRowApiDto, PositionMoveApiDto, PositionVariationLineApiDto, PositionVariationsApiDto, FetchPositionGamesParams, FetchPositionVariationsParams, ExplorerGameReplayApiDto, } from "./types";
12
+ export { ALL_GAME_SOURCES } from "./types";
12
13
  export { mockFetchPosition, mockFetchPositionGames, mockFetchPositionVariations, } from "./mocks";
13
14
  export { applyLineSans, fenAfterUci, normalizeFen, whiteScorePercent, } from "./positionUtils";
14
15
  export { isVariationLineActive } from "./variationLines";
@@ -1,3 +1,5 @@
1
+ export type GameSource = "lichess" | "twic";
2
+ export declare const ALL_GAME_SOURCES: GameSource[];
1
3
  /** Align with endchess-backend GET /positions */
2
4
  export type PositionMoveApiDto = {
3
5
  san: string;
@@ -13,7 +15,6 @@ export type PositionApiDto = {
13
15
  fen: string;
14
16
  totalGames: number;
15
17
  moves: PositionMoveApiDto[];
16
- sampleGameIds: string[];
17
18
  };
18
19
  /** Align with endchess-backend GET /positions/games */
19
20
  export type PositionGameRowApiDto = {
@@ -31,6 +32,7 @@ export type PositionGameRowApiDto = {
31
32
  nextSan: string;
32
33
  nextUci: string;
33
34
  avgElo: number;
35
+ source: GameSource;
34
36
  };
35
37
  export type PositionGamesApiDto = {
36
38
  positionKey: string;
@@ -48,6 +50,7 @@ export type FetchPositionGamesParams = {
48
50
  uci?: string;
49
51
  topOnly: boolean;
50
52
  limit?: number;
53
+ sources?: GameSource[];
51
54
  };
52
55
  /** Align with endchess-backend GET /positions/variations */
53
56
  export type PositionVariationLineApiDto = {
@@ -91,6 +94,7 @@ export type ExplorerGameReplayApiDto = {
91
94
  event?: string;
92
95
  timeControl?: string;
93
96
  timeClass?: string;
97
+ source: GameSource;
94
98
  movesUci: string[];
95
99
  movesSan: string[];
96
100
  };
package/dist/index.esm.js CHANGED
@@ -284,12 +284,24 @@ const linkStyle = {
284
284
  textUnderlineOffset: 2,
285
285
  };
286
286
  const LichessGameLink = ({ url, name }) => (jsx("a", { href: url, target: "_blank", rel: "noopener noreferrer", style: linkStyle, children: name }));
287
- const GamesTable = ({ games, onGameSelect }) => (jsxs("table", { style: tableStyle, children: [jsx("thead", { children: jsxs("tr", { children: [jsx("th", { style: thStyle, children: "White" }), jsx("th", { style: thStyle, children: "Elo" }), jsx("th", { style: thStyle, children: "Black" }), jsx("th", { style: thStyle, children: "Elo" }), jsx("th", { style: thStyle, children: "Result" }), onGameSelect && jsx("th", { style: thStyle })] }) }), jsx("tbody", { children: games.length === 0 ? (jsx("tr", { children: jsx("td", { colSpan: onGameSelect ? 6 : 5, style: Object.assign(Object.assign({}, tdStyle), { opacity: 0.7, fontStyle: "italic" }), children: "No games match this position and filter. Widen the Elo range or turn off Top games." }) })) : (games.map((g) => (jsxs("tr", { children: [jsx("td", { style: tdStyle, children: jsx(LichessGameLink, { url: g.url, name: g.white }) }), jsx("td", { style: tdStyle, children: g.whiteElo }), jsx("td", { style: tdStyle, children: jsx(LichessGameLink, { url: g.url, name: g.black }) }), jsx("td", { style: tdStyle, children: g.blackElo }), jsx("td", { style: tdStyle, children: g.result }), onGameSelect && (jsx("td", { style: tdStyle, children: jsx("button", { type: "button", onClick: () => onGameSelect(g), children: "Train" }) }))] }, g.gameId)))) })] }));
287
+ const GamesTable = ({ games, onGameSelect }) => (jsxs("table", { style: tableStyle, children: [jsx("thead", { children: jsxs("tr", { children: [jsx("th", { style: thStyle, children: "White" }), jsx("th", { style: thStyle, children: "Elo" }), jsx("th", { style: thStyle, children: "Black" }), jsx("th", { style: thStyle, children: "Elo" }), jsx("th", { style: thStyle, children: "Result" }), jsx("th", { style: thStyle, children: "Source" }), onGameSelect && jsx("th", { style: thStyle })] }) }), jsx("tbody", { children: games.length === 0 ? (jsx("tr", { children: jsx("td", { colSpan: onGameSelect ? 7 : 6, style: Object.assign(Object.assign({}, tdStyle), { opacity: 0.7, fontStyle: "italic" }), children: "No games match this position and filter. Widen the Elo range or turn off Top games." }) })) : (games.map((g) => (jsxs("tr", { children: [jsx("td", { style: tdStyle, children: jsx(LichessGameLink, { url: g.url, name: g.white }) }), jsx("td", { style: tdStyle, children: g.whiteElo }), jsx("td", { style: tdStyle, children: jsx(LichessGameLink, { url: g.url, name: g.black }) }), jsx("td", { style: tdStyle, children: g.blackElo }), jsx("td", { style: tdStyle, children: g.result }), jsx("td", { style: tdStyle, children: g.source === "twic" ? "Master" : "Lichess" }), onGameSelect && (jsx("td", { style: tdStyle, children: jsx("button", { type: "button", onClick: () => onGameSelect(g), children: "Train" }) }))] }, g.gameId)))) })] }));
288
288
 
289
289
  const mainLineTitleStyle = {
290
290
  fontWeight: 600,
291
291
  };
292
- const PositionGamesPanel = ({ games, lineLabel, minElo, maxElo, defaultMinElo, defaultMaxElo, topOnly, onMinEloChange, onMaxEloChange, onTopOnlyChange, onGameSelect, }) => (jsxs("div", { style: gamesSectionStyle, children: [jsx("div", { style: gamesHeaderStyle, children: lineLabel ? (jsxs(Fragment, { children: [jsx("span", { style: mainLineTitleStyle, children: "Main line: " }), lineLabel, jsxs("span", { style: { opacity: 0.75 }, children: [" (", games.length, " games)"] })] })) : (jsxs("span", { children: ["Games ", jsxs("span", { style: { opacity: 0.75 }, children: ["(", games.length, ")"] })] })) }), jsx("div", { style: gamesScrollStyle, children: jsx(GamesTable, { games: games, onGameSelect: onGameSelect }) }), jsxs("div", { style: gamesToolbarStyle, children: [jsx(EloRangeFilter, { minElo: minElo, maxElo: maxElo, defaultMinElo: defaultMinElo, defaultMaxElo: defaultMaxElo, onMinEloChange: onMinEloChange, onMaxEloChange: onMaxEloChange }), jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [jsx("input", { type: "checkbox", checked: topOnly, onChange: (e) => onTopOnlyChange(e.target.checked) }), "Top games"] })] })] }));
292
+ const PositionGamesPanel = ({ games, lineLabel, minElo, maxElo, defaultMinElo, defaultMaxElo, topOnly, sources, onMinEloChange, onMaxEloChange, onTopOnlyChange, onSourcesChange, onGameSelect, }) => {
293
+ const toggleSource = (source) => {
294
+ if (sources.includes(source)) {
295
+ if (sources.length === 1) {
296
+ return;
297
+ }
298
+ onSourcesChange(sources.filter((value) => value !== source));
299
+ return;
300
+ }
301
+ onSourcesChange([...sources, source]);
302
+ };
303
+ return (jsxs("div", { style: gamesSectionStyle, children: [jsx("div", { style: gamesHeaderStyle, children: lineLabel ? (jsxs(Fragment, { children: [jsx("span", { style: mainLineTitleStyle, children: "Main line: " }), lineLabel, jsxs("span", { style: { opacity: 0.75 }, children: [" (", games.length, " games)"] })] })) : (jsxs("span", { children: ["Games ", jsxs("span", { style: { opacity: 0.75 }, children: ["(", games.length, ")"] })] })) }), jsx("div", { style: gamesScrollStyle, children: jsx(GamesTable, { games: games, onGameSelect: onGameSelect }) }), jsxs("div", { style: gamesToolbarStyle, children: [jsx(EloRangeFilter, { minElo: minElo, maxElo: maxElo, defaultMinElo: defaultMinElo, defaultMaxElo: defaultMaxElo, onMinEloChange: onMinEloChange, onMaxEloChange: onMaxEloChange }), jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [jsx("input", { type: "checkbox", checked: topOnly, onChange: (e) => onTopOnlyChange(e.target.checked) }), "Top games"] }), jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [jsx("input", { type: "checkbox", checked: sources.includes("lichess"), onChange: () => toggleSource("lichess") }), "Lichess"] }), jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [jsx("input", { type: "checkbox", checked: sources.includes("twic"), onChange: () => toggleSource("twic") }), "Master"] })] })] }));
304
+ };
293
305
 
294
306
  const DefaultReferenceLayout = ({ board, referencePanel, }) => (jsxs("div", { style: referenceShellStyle, children: [jsx("div", { style: boardColumnStyle, children: board }), referencePanel] }));
295
307
  const defaultRenderLayout = (props) => (jsx(DefaultReferenceLayout, Object.assign({}, props)));
@@ -367,6 +379,8 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
367
379
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
368
380
  };
369
381
 
382
+ const ALL_GAME_SOURCES = ["lichess", "twic"];
383
+
370
384
  function usePositionHistory(initialFen) {
371
385
  const [history, setHistory] = useState([
372
386
  { fen: initialFen },
@@ -448,6 +462,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
448
462
  const [minElo, setMinElo] = useState(defaultMinElo);
449
463
  const [maxElo, setMaxElo] = useState(defaultMaxElo);
450
464
  const [topOnly, setTopOnly] = useState(false);
465
+ const [sources, setSources] = useState([...ALL_GAME_SOURCES]);
451
466
  const [loading, setLoading] = useState(false);
452
467
  const [error, setError] = useState(null);
453
468
  const [variationsTab, setVariationsTab] = useState("variations");
@@ -540,6 +555,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
540
555
  maxElo,
541
556
  uci: gamesMoveFilterUci,
542
557
  topOnly,
558
+ sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
543
559
  }),
544
560
  ]);
545
561
  if (cancelled)
@@ -569,6 +585,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
569
585
  maxElo,
570
586
  gamesMoveFilterUci,
571
587
  topOnly,
588
+ sources,
572
589
  fetchPosition,
573
590
  fetchPositionGames,
574
591
  ]);
@@ -675,6 +692,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
675
692
  minElo,
676
693
  maxElo,
677
694
  topOnly,
695
+ sources,
678
696
  loading,
679
697
  error,
680
698
  lineLabel,
@@ -686,6 +704,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
686
704
  setMinElo,
687
705
  setMaxElo,
688
706
  setTopOnly,
707
+ setSources,
689
708
  setVariationsTab,
690
709
  setGamesMoveFilterUci,
691
710
  handleMoveSelect,
@@ -761,7 +780,7 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
761
780
  defaultMinElo,
762
781
  defaultMaxElo,
763
782
  });
764
- const { fen, boardFen, position, games, minElo, maxElo, topOnly, loading, error, lineLabel, canGoBack, canGoForward, variationsTab, forwardSans, selectedVariationKey, setMinElo, setMaxElo, setTopOnly, setVariationsTab, handleMoveSelect, handleLineSelect, handlePieceDrop, handleBack, handleForward, } = referenceData;
783
+ const { fen, boardFen, position, games, minElo, maxElo, topOnly, sources, loading, error, lineLabel, canGoBack, canGoForward, variationsTab, forwardSans, selectedVariationKey, setMinElo, setMaxElo, setTopOnly, setSources, setVariationsTab, handleMoveSelect, handleLineSelect, handlePieceDrop, handleBack, handleForward, } = referenceData;
765
784
  const { lines: variationLines, loading: variationLinesLoading } = useVariationLines({
766
785
  fen,
767
786
  tab: variationsTab,
@@ -803,9 +822,11 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
803
822
  defaultMinElo,
804
823
  defaultMaxElo,
805
824
  topOnly,
825
+ sources,
806
826
  onMinEloChange: setMinElo,
807
827
  onMaxEloChange: setMaxElo,
808
828
  onTopOnlyChange: setTopOnly,
829
+ onSourcesChange: setSources,
809
830
  onGameSelect,
810
831
  }) }));
811
832
  return (jsx(ThemeProvider, { theme: theme, children: jsx("div", { style: outerStyle, children: renderLayout({ theme, board, referencePanel }) }) }));
@@ -1016,7 +1037,6 @@ const mockPosition = {
1016
1037
  avgElo: 2435,
1017
1038
  },
1018
1039
  ],
1019
- sampleGameIds: ["abc123", "def456"],
1020
1040
  };
1021
1041
  const mockGames = {
1022
1042
  positionKey: "mock-start",
@@ -1037,6 +1057,7 @@ const mockGames = {
1037
1057
  nextSan: "e4",
1038
1058
  nextUci: "e2e4",
1039
1059
  avgElo: 2843,
1060
+ source: "lichess",
1040
1061
  },
1041
1062
  {
1042
1063
  gameId: "def456",
@@ -1050,6 +1071,7 @@ const mockGames = {
1050
1071
  nextSan: "d4",
1051
1072
  nextUci: "d2d4",
1052
1073
  avgElo: 2778,
1074
+ source: "twic",
1053
1075
  },
1054
1076
  ],
1055
1077
  };
@@ -1095,11 +1117,15 @@ function mockFetchPositionVariations(params) {
1095
1117
  }
1096
1118
  function mockFetchPositionGames(params) {
1097
1119
  return __awaiter(this, void 0, void 0, function* () {
1098
- const filtered = params.uci
1120
+ var _a;
1121
+ let filtered = params.uci
1099
1122
  ? mockGames.games.filter((g) => g.nextUci === params.uci)
1100
1123
  : mockGames.games;
1124
+ if ((_a = params.sources) === null || _a === void 0 ? void 0 : _a.length) {
1125
+ filtered = filtered.filter((game) => params.sources.includes(game.source));
1126
+ }
1101
1127
  return Object.assign(Object.assign({}, mockGames), { fen: params.fen, minElo: params.minElo, maxElo: params.maxElo, uci: params.uci, topOnly: params.topOnly, games: params.topOnly ? filtered.filter((g) => g.avgElo >= 2500) : filtered });
1102
1128
  });
1103
1129
  }
1104
1130
 
1105
- export { DEFAULT_REFERENCE_LAYOUT, DefaultBoardNav, DefaultReferenceLayout, DefaultReferencePanel, DefaultVariationsStrip, EXPLORER_START_FEN, ExplorerPlaceholder, GameReplayTrainer, PositionReferenceExplorer, PositionReferenceExplorerCore, applyLineSans, defaultRenderBoardNav, defaultRenderGamesPanel, defaultRenderLayout, defaultRenderMoveStats, defaultRenderStatus, defaultRenderVariationsStrip, fenAfterUci, fenAtPly, findPlyIndexForFen, isVariationLineActive, mockFetchPosition, mockFetchPositionGames, mockFetchPositionVariations, normalizeFen, useGameReplayTraining, whiteScorePercent };
1131
+ export { ALL_GAME_SOURCES, DEFAULT_REFERENCE_LAYOUT, DefaultBoardNav, DefaultReferenceLayout, DefaultReferencePanel, DefaultVariationsStrip, EXPLORER_START_FEN, ExplorerPlaceholder, GameReplayTrainer, PositionReferenceExplorer, PositionReferenceExplorerCore, applyLineSans, defaultRenderBoardNav, defaultRenderGamesPanel, defaultRenderLayout, defaultRenderMoveStats, defaultRenderStatus, defaultRenderVariationsStrip, fenAfterUci, fenAtPly, findPlyIndexForFen, isVariationLineActive, mockFetchPosition, mockFetchPositionGames, mockFetchPositionVariations, normalizeFen, useGameReplayTraining, whiteScorePercent };
package/dist/index.js CHANGED
@@ -286,12 +286,24 @@ const linkStyle = {
286
286
  textUnderlineOffset: 2,
287
287
  };
288
288
  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)))) })] }));
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)))) })] }));
290
290
 
291
291
  const mainLineTitleStyle = {
292
292
  fontWeight: 600,
293
293
  };
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"] })] })] }));
294
+ const PositionGamesPanel = ({ games, lineLabel, minElo, maxElo, defaultMinElo, defaultMaxElo, topOnly, sources, onMinEloChange, onMaxEloChange, onTopOnlyChange, onSourcesChange, onGameSelect, }) => {
295
+ const toggleSource = (source) => {
296
+ if (sources.includes(source)) {
297
+ if (sources.length === 1) {
298
+ return;
299
+ }
300
+ onSourcesChange(sources.filter((value) => value !== source));
301
+ return;
302
+ }
303
+ onSourcesChange([...sources, source]);
304
+ };
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"] })] })] }));
306
+ };
295
307
 
296
308
  const DefaultReferenceLayout = ({ board, referencePanel, }) => (jsxRuntime.jsxs("div", { style: referenceShellStyle, children: [jsxRuntime.jsx("div", { style: boardColumnStyle, children: board }), referencePanel] }));
297
309
  const defaultRenderLayout = (props) => (jsxRuntime.jsx(DefaultReferenceLayout, Object.assign({}, props)));
@@ -369,6 +381,8 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
369
381
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
370
382
  };
371
383
 
384
+ const ALL_GAME_SOURCES = ["lichess", "twic"];
385
+
372
386
  function usePositionHistory(initialFen) {
373
387
  const [history, setHistory] = react.useState([
374
388
  { fen: initialFen },
@@ -450,6 +464,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
450
464
  const [minElo, setMinElo] = react.useState(defaultMinElo);
451
465
  const [maxElo, setMaxElo] = react.useState(defaultMaxElo);
452
466
  const [topOnly, setTopOnly] = react.useState(false);
467
+ const [sources, setSources] = react.useState([...ALL_GAME_SOURCES]);
453
468
  const [loading, setLoading] = react.useState(false);
454
469
  const [error, setError] = react.useState(null);
455
470
  const [variationsTab, setVariationsTab] = react.useState("variations");
@@ -542,6 +557,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
542
557
  maxElo,
543
558
  uci: gamesMoveFilterUci,
544
559
  topOnly,
560
+ sources: sources.length < ALL_GAME_SOURCES.length ? sources : undefined,
545
561
  }),
546
562
  ]);
547
563
  if (cancelled)
@@ -571,6 +587,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
571
587
  maxElo,
572
588
  gamesMoveFilterUci,
573
589
  topOnly,
590
+ sources,
574
591
  fetchPosition,
575
592
  fetchPositionGames,
576
593
  ]);
@@ -677,6 +694,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
677
694
  minElo,
678
695
  maxElo,
679
696
  topOnly,
697
+ sources,
680
698
  loading,
681
699
  error,
682
700
  lineLabel,
@@ -688,6 +706,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
688
706
  setMinElo,
689
707
  setMaxElo,
690
708
  setTopOnly,
709
+ setSources,
691
710
  setVariationsTab,
692
711
  setGamesMoveFilterUci,
693
712
  handleMoveSelect,
@@ -763,7 +782,7 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
763
782
  defaultMinElo,
764
783
  defaultMaxElo,
765
784
  });
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;
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;
767
786
  const { lines: variationLines, loading: variationLinesLoading } = useVariationLines({
768
787
  fen,
769
788
  tab: variationsTab,
@@ -805,9 +824,11 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
805
824
  defaultMinElo,
806
825
  defaultMaxElo,
807
826
  topOnly,
827
+ sources,
808
828
  onMinEloChange: setMinElo,
809
829
  onMaxEloChange: setMaxElo,
810
830
  onTopOnlyChange: setTopOnly,
831
+ onSourcesChange: setSources,
811
832
  onGameSelect,
812
833
  }) }));
813
834
  return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, children: jsxRuntime.jsx("div", { style: outerStyle, children: renderLayout({ theme, board, referencePanel }) }) }));
@@ -1018,7 +1039,6 @@ const mockPosition = {
1018
1039
  avgElo: 2435,
1019
1040
  },
1020
1041
  ],
1021
- sampleGameIds: ["abc123", "def456"],
1022
1042
  };
1023
1043
  const mockGames = {
1024
1044
  positionKey: "mock-start",
@@ -1039,6 +1059,7 @@ const mockGames = {
1039
1059
  nextSan: "e4",
1040
1060
  nextUci: "e2e4",
1041
1061
  avgElo: 2843,
1062
+ source: "lichess",
1042
1063
  },
1043
1064
  {
1044
1065
  gameId: "def456",
@@ -1052,6 +1073,7 @@ const mockGames = {
1052
1073
  nextSan: "d4",
1053
1074
  nextUci: "d2d4",
1054
1075
  avgElo: 2778,
1076
+ source: "twic",
1055
1077
  },
1056
1078
  ],
1057
1079
  };
@@ -1097,13 +1119,18 @@ function mockFetchPositionVariations(params) {
1097
1119
  }
1098
1120
  function mockFetchPositionGames(params) {
1099
1121
  return __awaiter(this, void 0, void 0, function* () {
1100
- const filtered = params.uci
1122
+ var _a;
1123
+ let filtered = params.uci
1101
1124
  ? mockGames.games.filter((g) => g.nextUci === params.uci)
1102
1125
  : mockGames.games;
1126
+ if ((_a = params.sources) === null || _a === void 0 ? void 0 : _a.length) {
1127
+ filtered = filtered.filter((game) => params.sources.includes(game.source));
1128
+ }
1103
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 });
1104
1130
  });
1105
1131
  }
1106
1132
 
1133
+ exports.ALL_GAME_SOURCES = ALL_GAME_SOURCES;
1107
1134
  exports.DEFAULT_REFERENCE_LAYOUT = DEFAULT_REFERENCE_LAYOUT;
1108
1135
  exports.DefaultBoardNav = DefaultBoardNav;
1109
1136
  exports.DefaultReferenceLayout = DefaultReferenceLayout;
@@ -0,0 +1,10 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { PositionReferenceExplorer } from "../features/explorer/PositionReferenceExplorer";
3
+ declare const meta: Meta<typeof PositionReferenceExplorer>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof PositionReferenceExplorer>;
6
+ /** 100 sample games after 1.e4 e5 2.Nf3 Nc6 — Open Game / Italian–Spanish tabiya. */
7
+ export declare const Nc6100Games: Story;
8
+ export declare const LightTheme: Story;
9
+ export declare const StartingPosition: Story;
10
+ export declare const WithReplayFetch: Story;
@@ -0,0 +1,13 @@
1
+ import type { FetchPositionGamesParams, FetchPositionVariationsParams, PositionGamesApiDto, PositionVariationsApiDto } from "../../features/explorer/types";
2
+ import { nc6SampleGames, NC6_TABIYA_FEN } from "./nc6SampleGames";
3
+ export declare function nc6FetchPosition(fen: string): Promise<import("../..").PositionApiDto | null>;
4
+ export declare function nc6FetchPositionGames(params: FetchPositionGamesParams): Promise<PositionGamesApiDto>;
5
+ export declare function nc6FetchPositionVariations(params: FetchPositionVariationsParams): Promise<PositionVariationsApiDto | null>;
6
+ export declare function nc6FetchGame(gameId: string): Promise<(import("../..").ExplorerGameReplayApiDto & {
7
+ avgElo: number;
8
+ occurrences: Map<string, {
9
+ nextSan: string;
10
+ nextUci: string;
11
+ }>;
12
+ }) | null>;
13
+ export { NC6_TABIYA_FEN, nc6SampleGames };
@@ -0,0 +1,39 @@
1
+ import type { ExplorerGameReplayApiDto, PositionApiDto, PositionGameRowApiDto, PositionMoveApiDto } from "../../features/explorer/types";
2
+ export declare const NC6_OPENING_SANS: readonly ["e4", "e5", "Nf3", "Nc6"];
3
+ export declare const NC6_GAME_COUNT = 100;
4
+ export declare const NC6_GAME_ID_PREFIX = "nc6-sb-";
5
+ type MoveBucket = {
6
+ san: string;
7
+ uci: string;
8
+ games: number;
9
+ whiteWins: number;
10
+ draws: number;
11
+ blackWins: number;
12
+ eloSum: number;
13
+ };
14
+ type PositionBucket = {
15
+ fen: string;
16
+ totalGames: number;
17
+ moves: Map<string, MoveBucket>;
18
+ gameRows: PositionGameRowApiDto[];
19
+ };
20
+ type BuiltGame = ExplorerGameReplayApiDto & {
21
+ avgElo: number;
22
+ occurrences: Map<string, {
23
+ nextSan: string;
24
+ nextUci: string;
25
+ }>;
26
+ };
27
+ export declare const NC6_TABIYA_FEN: string;
28
+ export declare const nc6SampleGames: BuiltGame[];
29
+ export declare const nc6PositionBuckets: Map<string, PositionBucket>;
30
+ export declare function nc6PositionForFen(fen: string): PositionApiDto | null;
31
+ export declare function nc6GamesForPosition(fen: string, options: {
32
+ minElo: number;
33
+ maxElo: number;
34
+ uci?: string;
35
+ topOnly: boolean;
36
+ sources?: ("lichess" | "twic")[];
37
+ }): PositionGameRowApiDto[];
38
+ export declare function nc6ScorePercentForMove(move: PositionMoveApiDto): number | null;
39
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-chess-explorer",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "React components for browsing and replaying chess games (depends on react-chess-core only)",
5
5
  "license": "MIT",
6
6
  "author": "Robert Blackwell",
@@ -16,7 +16,9 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "build": "rollup -c",
19
- "prepublishOnly": "npm run build"
19
+ "prepublishOnly": "npm run build",
20
+ "storybook": "storybook dev -p 6008",
21
+ "build-storybook": "storybook build"
20
22
  },
21
23
  "dependencies": {
22
24
  "rollup": "^4.22.2",
@@ -30,16 +32,28 @@
30
32
  "react-chessboard": "^4.7.1"
31
33
  },
32
34
  "devDependencies": {
35
+ "@chromatic-com/storybook": "^1.9.0",
33
36
  "@rollup/plugin-commonjs": "^26.0.1",
34
37
  "@rollup/plugin-node-resolve": "^15.2.3",
35
38
  "@rollup/plugin-typescript": "^12.3.0",
39
+ "@storybook/addon-essentials": "^8.2.9",
40
+ "@storybook/addon-interactions": "^8.2.9",
41
+ "@storybook/addon-links": "^8.2.9",
42
+ "@storybook/addon-onboarding": "^8.2.9",
43
+ "@storybook/blocks": "^8.2.9",
44
+ "@storybook/preset-typescript": "^3.0.0",
45
+ "@storybook/react": "^8.2.9",
46
+ "@storybook/react-vite": "^8.2.9",
47
+ "@storybook/test": "^8.2.9",
36
48
  "@types/react": "^18.3.12",
37
49
  "@types/react-dom": "^18.3.1",
50
+ "@vitejs/plugin-react": "^4.3.1",
38
51
  "chess.js": "^1.0.0-beta.8",
39
52
  "react": "^18.3.1",
40
53
  "react-chess-core": "^0.1.0",
41
54
  "react-chessboard": "^4.7.1",
42
55
  "react-dom": "^18.3.1",
56
+ "storybook": "^8.2.9",
43
57
  "tslib": "^2.8.1"
44
58
  }
45
59
  }