@connectorvol/chess-widgets 6.4.0 → 6.5.0

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.
@@ -5,7 +5,7 @@ import { Draggable, PieceInputMode } from "@connectorvol/chessboard";
5
5
  export declare const DEFAULT_EDITABLE_BOARD_SETTINGS: {
6
6
  autoQueenPromotion: false;
7
7
  isResizable: true;
8
- isDisplayCoordinate: boolean;
8
+ showBoardCoordinates: false;
9
9
  orientation: "w";
10
10
  boardSize: number;
11
11
  draggable: Draggable.BOTH;
@@ -6,7 +6,7 @@ import { Color } from "@connectorvol/shared";
6
6
  export const DEFAULT_EDITABLE_BOARD_SETTINGS = {
7
7
  autoQueenPromotion: false,
8
8
  isResizable: true,
9
- isDisplayCoordinate: false,
9
+ showBoardCoordinates: false,
10
10
  orientation: Color.WHITE,
11
11
  boardSize: 38,
12
12
  draggable: Draggable.BOTH,
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ import type { TGameAnalyzerProps } from "./types.js";
3
+
4
+ import { Chessboard, DEFAULT_BOARD_SETTINGS } from "@connectorvol/chessboard";
5
+ import { TreeViewer, TreeViewerPanelManager } from "@connectorvol/tree";
6
+ import { Game } from "./gameAnalyzer.svelte.js";
7
+ import { untrack } from "svelte";
8
+
9
+ let {
10
+ pgn,
11
+ onChangePGN,
12
+ boardTheme,
13
+ boardAppearanceSettings,
14
+ editMode = true,
15
+ class: className,
16
+ }: TGameAnalyzerProps = $props();
17
+
18
+
19
+ let game = $derived(new Game(
20
+ untrack(() => pgn),
21
+ {
22
+ ...DEFAULT_BOARD_SETTINGS,
23
+ ...boardAppearanceSettings,
24
+ boardSize: "auto",
25
+ },
26
+ "chess",
27
+ boardTheme,
28
+ ))
29
+
30
+ // $effect(() => {
31
+ // if (pgn !== internalPgn) {
32
+ // internalPgn = pgn;
33
+ // game.loadPgn(pgn);
34
+ // }
35
+ // });
36
+
37
+ function handleChangeDirty(_setIsDirty: (value: boolean) => void) {
38
+ console.log('handleChangeDirty')
39
+ const exported = game.exportPgn();
40
+ onChangePGN?.(exported);
41
+ }
42
+
43
+ </script>
44
+
45
+ <div class={["mx-4 lg:container lg:mx-auto gap-4", className]}>
46
+ <div class="flex justify-center relative lg:gap-4 flex-col lg:flex-row">
47
+ <div class="lg:w-2/5">
48
+ <Chessboard facade={game.chessboard} />
49
+ </div>
50
+
51
+
52
+ <div class="lg:w-3/5 flex lg:flex-col flex-col-reverse h-full">
53
+ <div class="h-full">
54
+ <TreeViewer
55
+ className="h-[calc(100dvh-8rem)]"
56
+ chessTree={game.chessTree}
57
+ onSelectNode={game.onSelectNode}
58
+ onDeleteVariant={game.onDeleteVariant}
59
+ setChessFen={game.setChessFen}
60
+ setChessboardFen={game.setChessboardFen}
61
+ pieceSet={game.chessboard.chessSet}
62
+ {editMode}
63
+ onChangeDirty={handleChangeDirty}
64
+ />
65
+ </div>
66
+
67
+ <TreeViewerPanelManager
68
+ className="flex w-full justify-center select-none col-span-1 my-4"
69
+ chessTree={game.chessTree}
70
+ setChessFen={game.setChessFen}
71
+ setChessboardFen={game.setChessboardFen}
72
+ />
73
+ </div>
74
+ </div>
75
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { TGameAnalyzerProps } from "./types.js";
2
+ declare const GameAnalyzer: import("svelte").Component<TGameAnalyzerProps, {}, "">;
3
+ type GameAnalyzer = ReturnType<typeof GameAnalyzer>;
4
+ export default GameAnalyzer;
@@ -0,0 +1,124 @@
1
+ import { Color, calculatePly, generateId } from "@connectorvol/shared";
2
+ import { CHESSBOARD_THEMES, createBoardApi, DEFAULT_BOARD_SETTINGS, Draggable, syncMoveEvaluationNagBadge, } from "@connectorvol/chessboard";
3
+ import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
4
+ import { transformPgnToChessNode, createPgnFromTree } from "@connectorvol/tree";
5
+ import { ChessTree } from "@connectorvol/tree";
6
+ import { saveNodeMarkers } from "./utils.js";
7
+ export class Game {
8
+ chess;
9
+ chessboard;
10
+ chessTree;
11
+ previousNode;
12
+ boardSettings;
13
+ variant;
14
+ boardTheme;
15
+ constructor(pgn, chessBoardSettings = DEFAULT_BOARD_SETTINGS, variant = "chess", boardTheme) {
16
+ this.boardSettings = chessBoardSettings;
17
+ this.variant = variant;
18
+ this.boardTheme = boardTheme ?? CHESSBOARD_THEMES.blue;
19
+ const { rootNode, initialFen: position } = transformPgnToChessNode(pgn);
20
+ this.chessTree = new ChessTree(rootNode);
21
+ this.chess = new PgnOps(position, variant);
22
+ this.chessboard = createBoardApi({
23
+ fen: this.chess.fen(),
24
+ settings: {
25
+ ...this.boardSettings,
26
+ draggable: this.chess.turn() === Color.WHITE ? Draggable.WHITE : Draggable.BLACK,
27
+ },
28
+ theme: this.boardTheme,
29
+ });
30
+ this.chessboard.setActions(this.buildActions());
31
+ this.previousNode = this.chessTree.currentNode;
32
+ $effect(() => {
33
+ const markerService = this.chessboard.getBoard().markerService;
34
+ void markerService.totalMarkerCount;
35
+ const changed = saveNodeMarkers(this.chessboard, this.chessTree.currentNode);
36
+ if (changed) {
37
+ this.chessTree.mutationVersion++;
38
+ }
39
+ });
40
+ }
41
+ buildActions() {
42
+ return {
43
+ game: {
44
+ possibleMovesOnSquare: (square) => this.chess.moves(square),
45
+ beforePieceMoveSan: (san) => {
46
+ const move = this.chess.makeSanMove(san);
47
+ const fen = this.chess.fen();
48
+ const { halfMoves, fullMoves } = calculatePly(fen);
49
+ saveNodeMarkers(this.chessboard, this.chessTree.currentNode);
50
+ this.chessTree.addNodeToCurrent({
51
+ id: generateId(),
52
+ children: [],
53
+ data: { fen, san, ply: halfMoves, fullMoves, lastMove: move },
54
+ });
55
+ return { move, fen, turn: this.chess.turn() };
56
+ },
57
+ afterPieceMoveSan: () => {
58
+ syncMoveEvaluationNagBadge(this.chessboard, this.chessTree.currentNode.data);
59
+ this.chessboard.setNodeMarkers(this.chessTree.currentNode.data.parsedFirstComment?.shapes);
60
+ },
61
+ beforePieceMove: (from, to, promotion) => {
62
+ const { san, ply, fullMoves, move: lastMove } = this.chess.makeMove({
63
+ from,
64
+ to,
65
+ promotion,
66
+ });
67
+ const fen = this.chess.fen();
68
+ saveNodeMarkers(this.chessboard, this.chessTree.currentNode);
69
+ this.chessTree.addNodeToCurrent({
70
+ id: generateId(),
71
+ children: [],
72
+ data: { fen, san, ply, fullMoves, lastMove },
73
+ });
74
+ return fen;
75
+ },
76
+ afterPieceMove: () => {
77
+ this.chessboard.draggable =
78
+ this.chess.turn() === Color.WHITE ? Draggable.WHITE : Draggable.BLACK;
79
+ syncMoveEvaluationNagBadge(this.chessboard, this.chessTree.currentNode.data);
80
+ this.chessboard.setNodeMarkers(this.chessTree.currentNode.data.parsedFirstComment?.shapes);
81
+ },
82
+ },
83
+ };
84
+ }
85
+ /** Перезагружает партию из нового PGN. */
86
+ loadPgn(pgn) {
87
+ const { rootNode, initialFen: position } = transformPgnToChessNode(pgn);
88
+ this.chessTree.replaceRootTree(rootNode);
89
+ this.chess = new PgnOps(position, this.variant);
90
+ this.chessboard.fen = this.chess.fen();
91
+ this.syncGameState();
92
+ }
93
+ /** Экспортирует текущее дерево в строку PGN. */
94
+ exportPgn() {
95
+ return createPgnFromTree(this.chessTree.rootNode);
96
+ }
97
+ onSelectNode = () => {
98
+ saveNodeMarkers(this.chessboard, this.previousNode);
99
+ this.chess.setFen(this.chessTree.currentNode.data.fen);
100
+ this.syncGameState();
101
+ };
102
+ onDeleteVariant = () => {
103
+ saveNodeMarkers(this.chessboard, this.previousNode);
104
+ this.chess.setFen(this.chessTree.currentNode.data.fen);
105
+ this.syncGameState();
106
+ };
107
+ setChessFen = (fen) => {
108
+ this.chess.setFen(fen);
109
+ };
110
+ setChessboardFen = (animationTime = 0) => {
111
+ saveNodeMarkers(this.chessboard, this.previousNode);
112
+ this.chessboard.animationTime = animationTime;
113
+ this.syncGameState();
114
+ };
115
+ syncGameState() {
116
+ this.chessboard.fen = this.chess.fen();
117
+ this.chessboard.addLastMove(this.chessTree.currentNode.data.lastMove ?? null);
118
+ syncMoveEvaluationNagBadge(this.chessboard, this.chessTree.currentNode.data);
119
+ this.chessboard.setNodeMarkers(this.chessTree.currentNode.data.parsedFirstComment?.shapes);
120
+ this.chessboard.draggable =
121
+ this.chess.turn() === Color.WHITE ? Draggable.WHITE : Draggable.BLACK;
122
+ this.previousNode = this.chessTree.currentNode;
123
+ }
124
+ }
@@ -0,0 +1,35 @@
1
+ import type { ChessboardTheme } from "@connectorvol/chessboard";
2
+ import type { ChessBoardSettings } from "@connectorvol/chessboard";
3
+ /**
4
+ * Представляет тип визуальных настроек шахматной доски для анализатора партии.
5
+ */
6
+ export type TChessboardAppearanceSettings = Omit<Partial<ChessBoardSettings>, "boardSize" | "orientation" | "draggable">;
7
+ /**
8
+ * Представляет свойства компонента анализатора партии.
9
+ */
10
+ export interface TGameAnalyzerProps {
11
+ /**
12
+ * Возвращает PGN партии.
13
+ */
14
+ pgn: string;
15
+ /**
16
+ * Возвращает колбэк при изменении дерева ходов (добавление/удаление ходов, комментариев, маркеров).
17
+ */
18
+ onChangePGN?: (pgn: string) => void;
19
+ /**
20
+ * Возвращает тему оформления шахматной доски (по умолчанию синяя).
21
+ */
22
+ boardTheme?: ChessboardTheme;
23
+ /**
24
+ * Возвращает дополнительные визуальные настройки шахматной доски (кроме `boardSize`, `orientation`, `draggable`).
25
+ */
26
+ boardAppearanceSettings?: TChessboardAppearanceSettings;
27
+ /**
28
+ * Возвращает признак режима правки дерева (по умолчанию `true`).
29
+ */
30
+ editMode?: boolean;
31
+ /**
32
+ * Возвращает дополнительные классы Tailwind для корневого контейнера.
33
+ */
34
+ class?: string;
35
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { BoardApi } from "@connectorvol/chessboard";
2
+ import type { ChessTreeNode } from "@connectorvol/tree";
3
+ /** Сохраняет текущие маркеры доски в комментарий узла дерева (в формате PGN [%cal/%csl]).
4
+ * Возвращает true, если комментарий был изменён. */
5
+ export declare function saveNodeMarkers(boardApi: BoardApi, node: ChessTreeNode): boolean;
@@ -0,0 +1,53 @@
1
+ import { parseComment, makeComment, } from "@connectorvol/chessops/pgn";
2
+ const HEX_TO_COMMENT_COLOR = {
3
+ "#ff0000": "red",
4
+ "#00ff00": "green",
5
+ "#008000": "green",
6
+ "#0000ff": "blue",
7
+ "#ffff00": "yellow",
8
+ };
9
+ function hexToCommentColor(hex) {
10
+ return HEX_TO_COMMENT_COLOR[hex.toLowerCase()] ?? "green";
11
+ }
12
+ function shapeKey(s) {
13
+ return `${s.color}:${s.from}:${s.to}`;
14
+ }
15
+ function shapesEqual(a, b) {
16
+ if (a.length !== b.length)
17
+ return false;
18
+ const setA = new Set(a.map(shapeKey));
19
+ return b.every((s) => setA.has(shapeKey(s)));
20
+ }
21
+ /** Сохраняет текущие маркеры доски в комментарий узла дерева (в формате PGN [%cal/%csl]).
22
+ * Возвращает true, если комментарий был изменён. */
23
+ export function saveNodeMarkers(boardApi, node) {
24
+ const shapes = boardApi.getNodeMarkers();
25
+ const rawFirst = node.data.comments?.[0];
26
+ const base = rawFirst ? parseComment(rawFirst) : { text: "", shapes: [] };
27
+ const newShapes = shapes.map((s) => ({
28
+ color: hexToCommentColor(s.color ?? "#ff0000"),
29
+ from: s.from,
30
+ to: s.to,
31
+ }));
32
+ if (shapesEqual(newShapes, base.shapes))
33
+ return false;
34
+ const newComment = makeComment({ ...base, shapes: newShapes });
35
+ if (newComment === rawFirst)
36
+ return false;
37
+ if (newComment === "") {
38
+ if (rawFirst === undefined)
39
+ return false;
40
+ const tail = node.data.comments?.slice(1);
41
+ if (tail?.length) {
42
+ node.data.comments = tail;
43
+ }
44
+ else {
45
+ node.data.comments = undefined;
46
+ }
47
+ node.data.parsedFirstComment = null;
48
+ return true;
49
+ }
50
+ node.data.comments = [newComment, ...(node.data.comments?.slice(1) ?? [])];
51
+ node.data.parsedFirstComment = parseComment(newComment);
52
+ return true;
53
+ }
package/dist/index.d.ts CHANGED
@@ -12,3 +12,5 @@ export { default as EditFen } from "./position-editor/EditFen.svelte";
12
12
  export { default as EditMove } from "./position-editor/EditMove.svelte";
13
13
  export { default as EditPanel } from "./position-editor/EditPanel.svelte";
14
14
  export { Fen, type Castling } from "./position-editor/fen.svelte.js";
15
+ export { default as GameAnalyzer } from "./game-analyzer/GameAnalyzer.svelte";
16
+ export type { TGameAnalyzerProps } from "./game-analyzer/types.js";
package/dist/index.js CHANGED
@@ -8,3 +8,4 @@ export { default as EditFen } from "./position-editor/EditFen.svelte";
8
8
  export { default as EditMove } from "./position-editor/EditMove.svelte";
9
9
  export { default as EditPanel } from "./position-editor/EditPanel.svelte";
10
10
  export { Fen } from "./position-editor/fen.svelte.js";
11
+ export { default as GameAnalyzer } from "./game-analyzer/GameAnalyzer.svelte";
@@ -5,7 +5,6 @@
5
5
  import { Chessboard } from "@connectorvol/chessboard";
6
6
  import type { ChessTree } from "@connectorvol/tree";
7
7
  import { TreeViewer } from "@connectorvol/tree";
8
- import { DEFAULT_PIECE_SET } from "@connectorvol/shared";
9
8
 
10
9
  import { cn } from "../utils.js";
11
10
 
@@ -67,6 +66,10 @@
67
66
  class: className,
68
67
  belowChessboard,
69
68
  }: Props = $props();
69
+
70
+ const hasBoardCoordinatesWithoutBorder = $derived(
71
+ chessboard.showBoardCoordinates && !chessboard.border,
72
+ );
70
73
  </script>
71
74
 
72
75
  <div
@@ -79,19 +82,22 @@
79
82
  class="order-2 flex min-h-48 w-full flex-1 flex-col gap-2 lg:order-2 lg:min-h-0 lg:min-w-0"
80
83
  >
81
84
  <div
82
- class="flex min-h-28 min-w-0 flex-1 flex-col overflow-hidden rounded-md border border-border lg:min-h-0"
85
+ class={cn(
86
+ "flex min-h-28 min-w-0 flex-1 flex-col overflow-hidden rounded-md border border-border lg:min-h-0",
87
+ hasBoardCoordinatesWithoutBorder && "mb-4",
88
+ )}
83
89
  >
84
90
  <TreeViewer
85
- chessTree={chessTree}
86
- {onSelectNode}
87
- {onDeleteVariant}
88
- {setChessFen}
89
- {setChessboardFen}
90
- pieceSet={DEFAULT_PIECE_SET}
91
- className="min-h-0 flex-1 border-x-0 border-t-0"
92
- {editMode}
93
- {selectable}
94
- />
91
+ chessTree={chessTree}
92
+ {onSelectNode}
93
+ {onDeleteVariant}
94
+ {setChessFen}
95
+ {setChessboardFen}
96
+ pieceSet={chessboard.chessSet}
97
+ className="min-h-0 flex-1 border-x-0 border-t-0"
98
+ {editMode}
99
+ {selectable}
100
+ />
95
101
  </div>
96
102
  </div>
97
103
 
@@ -41,7 +41,7 @@
41
41
  }
42
42
 
43
43
  let step = $state<1 | 2 | 3>(1);
44
- let puzzleData = $state<PuzzleData>(
44
+ let puzzleData = $derived<PuzzleData>(
45
45
  createInitialPuzzleData(wizardSeedFen(fenProp)),
46
46
  );
47
47
 
@@ -487,6 +487,7 @@
487
487
  chessboard.fen = previewChess.fen();
488
488
  chessboard.addLastMove(tree.currentNode.data.lastMove ?? null);
489
489
  syncMoveEvaluationNagBadge(chessboard, tree.currentNode.data);
490
+ chessboard.setNodeMarkers(tree.currentNode.data.parsedFirstComment?.shapes);
490
491
  syncBoardDragFromChessTurn();
491
492
  }
492
493
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@connectorvol/chess-widgets",
3
- "version": "6.4.0",
3
+ "version": "6.5.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"