@connectorvol/chess-widgets 8.0.1 → 9.0.1
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.
- package/dist/(components)/BoardSettingsTrigger.svelte +10 -0
- package/dist/(components)/BoardSettingsTrigger.svelte.d.ts +18 -0
- package/dist/constants/default-board-appearance-settings.d.ts +9 -0
- package/dist/constants/default-board-appearance-settings.js +30 -0
- package/dist/constants/editable-board-settings.d.ts +3 -21
- package/dist/constants/editable-board-settings.js +24 -19
- package/dist/game-analyzer/GameAnalyzer.svelte +28 -10
- package/dist/game-analyzer/gameAnalyzer.svelte.js +6 -9
- package/dist/game-analyzer/types.d.ts +13 -8
- package/dist/index.d.ts +9 -1
- package/dist/index.js +6 -0
- package/dist/position-editor/EditPanel.svelte +9 -6
- package/dist/puzzle/puzzleCreatedPayload.d.ts +17 -0
- package/dist/puzzle/puzzleData.d.ts +4 -0
- package/dist/puzzle/puzzleData.js +10 -0
- package/dist/puzzle/puzzlePreviewConstants.d.ts +4 -0
- package/dist/puzzle/puzzlePreviewConstants.js +4 -0
- package/dist/puzzle/puzzlePreviewPathNags.d.ts +6 -0
- package/dist/puzzle/puzzlePreviewPathNags.js +35 -0
- package/dist/puzzle/puzzleSolverForkAnnotations.d.ts +2 -4
- package/dist/puzzle/puzzleSolverForkAnnotations.js +13 -22
- package/dist/puzzle/puzzleStepPreviewSolver.d.ts +13 -1
- package/dist/puzzle/puzzleStepPreviewSolver.js +69 -9
- package/dist/puzzle/syncPuzzleBranchNags.d.ts +14 -0
- package/dist/puzzle/syncPuzzleBranchNags.js +125 -0
- package/dist/puzzle-creation/OpeningTagHoverPreview.svelte +81 -0
- package/dist/puzzle-creation/OpeningTagHoverPreview.svelte.d.ts +11 -0
- package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte +198 -98
- package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte.d.ts +20 -2
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte +100 -106
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte.d.ts +47 -3
- package/dist/puzzle-creation/PuzzlePgnBoardTreeEditor.svelte +945 -498
- package/dist/puzzle-creation/PuzzleWizardTagsStep.svelte +36 -0
- package/dist/puzzle-creation/PuzzleWizardTagsStep.svelte.d.ts +12 -0
- package/dist/puzzle-creation/StepMoves.svelte +210 -188
- package/dist/puzzle-creation/StepPosition.svelte +35 -9
- package/dist/puzzle-creation/StepPreview.svelte +24 -11
- package/dist/puzzle-creation/StepPreview.svelte.d.ts +3 -1
- package/dist/puzzle-creation/StepTags.svelte +270 -0
- package/dist/puzzle-creation/StepTags.svelte.d.ts +19 -0
- package/dist/puzzle-creation/buildPuzzleWizardBoardSettings.d.ts +9 -0
- package/dist/puzzle-creation/buildPuzzleWizardBoardSettings.js +19 -0
- package/dist/puzzle-creation/createPuzzleLineEditingBoard.d.ts +10 -3
- package/dist/puzzle-creation/createPuzzleLineEditingBoard.js +14 -9
- package/dist/puzzle-creation/puzzleWizardState.d.ts +35 -0
- package/dist/puzzle-creation/puzzleWizardState.js +37 -0
- package/dist/puzzle-creation/types.d.ts +36 -10
- package/package.json +22 -17
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
|
|
2
|
-
import { Color, PUZZLE_BRANCH_WRONG_NAG_ID } from "@connectorvol/shared";
|
|
2
|
+
import { Color, PUZZLE_BRANCH_WRONG_NAG_ID, calculatePly } from "@connectorvol/shared";
|
|
3
3
|
/**
|
|
4
4
|
* Представляет извлечение стороны, имеющей ход, из полной строки FEN.
|
|
5
5
|
*/
|
|
@@ -49,23 +49,83 @@ export function puzzlePreviewFindChildIndexForPlayedSan(cursorFen, children, pla
|
|
|
49
49
|
/**
|
|
50
50
|
* Представляет классификацию хода решателя на развилке (или на единственном продолжении).
|
|
51
51
|
*/
|
|
52
|
-
export function puzzlePreviewClassifySolverMove(cursorNode, cursorFen, playedSan,
|
|
52
|
+
export function puzzlePreviewClassifySolverMove(cursorNode, cursorFen, playedSan, _solverColor) {
|
|
53
53
|
const children = cursorNode.children;
|
|
54
54
|
const idx = puzzlePreviewFindChildIndexForPlayedSan(cursorFen, children, playedSan);
|
|
55
55
|
if (idx < 0) {
|
|
56
56
|
return { kind: "no_matching_variant" };
|
|
57
57
|
}
|
|
58
58
|
const child = children[idx];
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const nags = child.data.nags ?? [];
|
|
63
|
-
if (nags.includes(PUZZLE_BRANCH_WRONG_NAG_ID)) {
|
|
64
|
-
return { kind: "wrong_marked_variant", wrongBranchRoot: child };
|
|
65
|
-
}
|
|
59
|
+
const nags = child.data.nags ?? [];
|
|
60
|
+
if (nags.includes(PUZZLE_BRANCH_WRONG_NAG_ID)) {
|
|
61
|
+
return { kind: "wrong_marked_variant", wrongBranchRoot: child };
|
|
66
62
|
}
|
|
67
63
|
return { kind: "correct", childIndex: idx };
|
|
68
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Представляет сбор данных узла для произвольного неверного хода решателя (нет в PGN).
|
|
67
|
+
*/
|
|
68
|
+
export function puzzlePreviewBuildAdHocWrongMoveData(cursorFen, san) {
|
|
69
|
+
const probe = new PgnOps(cursorFen, "chess");
|
|
70
|
+
try {
|
|
71
|
+
const move = probe.makeSanMove(san);
|
|
72
|
+
const fen = probe.fen();
|
|
73
|
+
const { halfMoves, fullMoves } = calculatePly(fen);
|
|
74
|
+
return {
|
|
75
|
+
fen,
|
|
76
|
+
san,
|
|
77
|
+
ply: halfMoves,
|
|
78
|
+
fullMoves,
|
|
79
|
+
lastMove: move,
|
|
80
|
+
nags: [PUZZLE_BRANCH_WRONG_NAG_ID],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Представляет добавление отсутствующего в PGN хода как новой ветки дерева решения с NAG ✗.
|
|
89
|
+
*/
|
|
90
|
+
export function puzzlePreviewAddAdHocWrongMoveToSolutionTree(solutionTree, cursorPath, cursorFen, san) {
|
|
91
|
+
const data = puzzlePreviewBuildAdHocWrongMoveData(cursorFen, san);
|
|
92
|
+
if (!data)
|
|
93
|
+
return null;
|
|
94
|
+
const cursorNode = puzzlePreviewNodeAtPath(solutionTree.rootNode.moves, cursorPath);
|
|
95
|
+
if (!cursorNode)
|
|
96
|
+
return null;
|
|
97
|
+
solutionTree.currentNode = cursorNode;
|
|
98
|
+
solutionTree.addNodeToCurrent({
|
|
99
|
+
id: "",
|
|
100
|
+
children: [],
|
|
101
|
+
data,
|
|
102
|
+
});
|
|
103
|
+
return solutionTree.currentNode;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Представляет пересчёт пути индексов после перестановки детей на развилках.
|
|
107
|
+
*/
|
|
108
|
+
export function puzzlePreviewRemapPathAfterReorder(rootMoves, path) {
|
|
109
|
+
const nodeIds = [];
|
|
110
|
+
let node = rootMoves;
|
|
111
|
+
for (const idx of path) {
|
|
112
|
+
const child = node.children[idx];
|
|
113
|
+
if (!child)
|
|
114
|
+
return path;
|
|
115
|
+
nodeIds.push(child.id);
|
|
116
|
+
node = child;
|
|
117
|
+
}
|
|
118
|
+
node = rootMoves;
|
|
119
|
+
const remapped = [];
|
|
120
|
+
for (const id of nodeIds) {
|
|
121
|
+
const idx = node.children.findIndex((c) => c.id === id);
|
|
122
|
+
if (idx < 0)
|
|
123
|
+
return path;
|
|
124
|
+
remapped.push(idx);
|
|
125
|
+
node = node.children[idx];
|
|
126
|
+
}
|
|
127
|
+
return remapped;
|
|
128
|
+
}
|
|
69
129
|
/**
|
|
70
130
|
* Представляет проверку: линия закончилась ключевым ходом решателя (ход соперника, лист дерева).
|
|
71
131
|
*/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChessTree, ChessTreeNode } from "@connectorvol/tree";
|
|
2
|
+
import { Color } from "@connectorvol/shared";
|
|
3
|
+
/**
|
|
4
|
+
* Представляет нормализацию дерева задачи: NAG ✓/✗ и порядок веток (верный ход — главная линия).
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizePuzzleTreeMainLine(root: ChessTreeNode, solverColor: Color): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Представляет синхронизацию NAG ✓/✗ по дереву решения (устаревшее имя, вызывает нормализацию).
|
|
9
|
+
*/
|
|
10
|
+
export declare function syncPuzzleBranchNags(root: ChessTreeNode, solverColor: Color): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Представляет нормализацию главной линии во всём дереве задачи и инкремент версии мутации при изменениях.
|
|
13
|
+
*/
|
|
14
|
+
export declare function syncPuzzleBranchNagsOnTree(tree: ChessTree, solverColor: Color): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Color, PUZZLE_BRANCH_CORRECT_NAG_ID, PUZZLE_BRANCH_WRONG_NAG_ID, isPuzzleBranchNag, } from "@connectorvol/shared";
|
|
2
|
+
/**
|
|
3
|
+
* Представляет извлечение стороны, имеющей ход, из полной строки FEN.
|
|
4
|
+
*/
|
|
5
|
+
function sideToMoveFromFullFen(fullFen) {
|
|
6
|
+
const token = fullFen.trim().split(/\s+/)[1];
|
|
7
|
+
return token === "b" ? Color.BLACK : Color.WHITE;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Представляет удаление NAG «верно / неверно» из списка аннотаций узла.
|
|
11
|
+
*/
|
|
12
|
+
function stripPuzzleBranchNags(nags) {
|
|
13
|
+
return (nags ?? []).filter((n) => !isPuzzleBranchNag(n));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Представляет проверку: на узле стоит NAG верного хода задачи.
|
|
17
|
+
*/
|
|
18
|
+
function childHasCorrectNag(child) {
|
|
19
|
+
return (child.data.nags ?? []).includes(PUZZLE_BRANCH_CORRECT_NAG_ID);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Представляет проверку: на узле стоит NAG неверного хода задачи.
|
|
23
|
+
*/
|
|
24
|
+
function childHasWrongNag(child) {
|
|
25
|
+
return (child.data.nags ?? []).includes(PUZZLE_BRANCH_WRONG_NAG_ID);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Представляет выбор ребёнка для канонической главной линии на развилке.
|
|
29
|
+
*/
|
|
30
|
+
function pickMainLineChild(parent, solverColor) {
|
|
31
|
+
const { children } = parent;
|
|
32
|
+
if (children.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
if (children.length === 1)
|
|
35
|
+
return children[0];
|
|
36
|
+
const mover = sideToMoveFromFullFen(parent.data.fen);
|
|
37
|
+
if (mover === solverColor) {
|
|
38
|
+
const correct = children.find(childHasCorrectNag);
|
|
39
|
+
if (correct)
|
|
40
|
+
return correct;
|
|
41
|
+
const notWrong = children.find((c) => !childHasWrongNag(c));
|
|
42
|
+
if (notWrong)
|
|
43
|
+
return notWrong;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const notWrong = children.find((c) => !childHasWrongNag(c));
|
|
47
|
+
if (notWrong)
|
|
48
|
+
return notWrong;
|
|
49
|
+
}
|
|
50
|
+
return children[0];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Представляет удаление NAG ✓/✗ у ходов, которые не являются ходом решателя.
|
|
54
|
+
*/
|
|
55
|
+
function cleanupInvalidPuzzleBranchNags(root, solverColor) {
|
|
56
|
+
let changed = false;
|
|
57
|
+
function walk(parent) {
|
|
58
|
+
const mover = sideToMoveFromFullFen(parent.data.fen);
|
|
59
|
+
for (const child of parent.children) {
|
|
60
|
+
const isSolverFork = mover === solverColor;
|
|
61
|
+
if (!isSolverFork) {
|
|
62
|
+
const before = child.data.nags ?? [];
|
|
63
|
+
if (before.some(isPuzzleBranchNag)) {
|
|
64
|
+
const next = stripPuzzleBranchNags(before);
|
|
65
|
+
child.data.nags = next.length > 0 ? next : undefined;
|
|
66
|
+
changed = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
walk(child);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
walk(root);
|
|
73
|
+
return changed;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Представляет поднятие канонической главной ветки в `children[0]` на всех развилках.
|
|
77
|
+
*/
|
|
78
|
+
function reorderMainLineChildrenFirst(root, solverColor) {
|
|
79
|
+
let changed = false;
|
|
80
|
+
function walk(node) {
|
|
81
|
+
for (const child of node.children) {
|
|
82
|
+
walk(child);
|
|
83
|
+
}
|
|
84
|
+
if (node.children.length <= 1)
|
|
85
|
+
return;
|
|
86
|
+
const mainChild = pickMainLineChild(node, solverColor);
|
|
87
|
+
if (!mainChild)
|
|
88
|
+
return;
|
|
89
|
+
const idx = node.children.findIndex((c) => c.id === mainChild.id);
|
|
90
|
+
if (idx <= 0)
|
|
91
|
+
return;
|
|
92
|
+
const [moved] = node.children.splice(idx, 1);
|
|
93
|
+
node.children.unshift(moved);
|
|
94
|
+
changed = true;
|
|
95
|
+
}
|
|
96
|
+
walk(root);
|
|
97
|
+
return changed;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Представляет нормализацию дерева задачи: NAG ✓/✗ и порядок веток (верный ход — главная линия).
|
|
101
|
+
*/
|
|
102
|
+
export function normalizePuzzleTreeMainLine(root, solverColor) {
|
|
103
|
+
let changed = false;
|
|
104
|
+
if (cleanupInvalidPuzzleBranchNags(root, solverColor)) {
|
|
105
|
+
changed = true;
|
|
106
|
+
}
|
|
107
|
+
if (reorderMainLineChildrenFirst(root, solverColor)) {
|
|
108
|
+
changed = true;
|
|
109
|
+
}
|
|
110
|
+
return changed;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Представляет синхронизацию NAG ✓/✗ по дереву решения (устаревшее имя, вызывает нормализацию).
|
|
114
|
+
*/
|
|
115
|
+
export function syncPuzzleBranchNags(root, solverColor) {
|
|
116
|
+
return normalizePuzzleTreeMainLine(root, solverColor);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Представляет нормализацию главной линии во всём дереве задачи и инкремент версии мутации при изменениях.
|
|
120
|
+
*/
|
|
121
|
+
export function syncPuzzleBranchNagsOnTree(tree, solverColor) {
|
|
122
|
+
if (normalizePuzzleTreeMainLine(tree.rootNode.moves, solverColor)) {
|
|
123
|
+
tree.mutationVersion++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Chessboard,
|
|
4
|
+
CHESSBOARD_THEMES,
|
|
5
|
+
createBoardApi,
|
|
6
|
+
Draggable,
|
|
7
|
+
} from "@connectorvol/chessboard";
|
|
8
|
+
|
|
9
|
+
import { DEFAULT_BOARD_APPEARANCE_SETTINGS } from "../constants/default-board-appearance-settings.js";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Возвращает FEN позиции для превью. */
|
|
13
|
+
fen: string;
|
|
14
|
+
/** Возвращает подпись дебютного тега. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Возвращает якорь кнопки тега для позиционирования popover. */
|
|
17
|
+
anchor: DOMRect;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { fen, label, anchor }: Props = $props();
|
|
21
|
+
|
|
22
|
+
const previewBoardSizePx = 220;
|
|
23
|
+
const previewGapPx = 8;
|
|
24
|
+
const viewportMarginPx = 12;
|
|
25
|
+
|
|
26
|
+
const previewDesign = {
|
|
27
|
+
...DEFAULT_BOARD_APPEARANCE_SETTINGS.design,
|
|
28
|
+
showBoardCoordinates: false,
|
|
29
|
+
showSquareCoordinates: false,
|
|
30
|
+
markerConfig: null,
|
|
31
|
+
showPossibleMoveIndicators: false,
|
|
32
|
+
theme: CHESSBOARD_THEMES.blue,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const previewBoardApi = $derived.by(() =>
|
|
36
|
+
createBoardApi({
|
|
37
|
+
fen,
|
|
38
|
+
playSettings: {
|
|
39
|
+
...DEFAULT_BOARD_APPEARANCE_SETTINGS.play,
|
|
40
|
+
boardSize: previewBoardSizePx * 10,
|
|
41
|
+
isResizable: false,
|
|
42
|
+
draggable: Draggable.NONE,
|
|
43
|
+
allowPreMove: false,
|
|
44
|
+
showSquareBadges: false,
|
|
45
|
+
animationTime: 0,
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const previewStyle = $derived.by(() => {
|
|
51
|
+
const belowTop = anchor.bottom + previewGapPx;
|
|
52
|
+
const aboveTop = anchor.top - previewBoardSizePx - previewGapPx;
|
|
53
|
+
const fitsBelow =
|
|
54
|
+
belowTop + previewBoardSizePx <= window.innerHeight - viewportMarginPx;
|
|
55
|
+
const top = fitsBelow
|
|
56
|
+
? belowTop
|
|
57
|
+
: Math.max(viewportMarginPx, aboveTop);
|
|
58
|
+
|
|
59
|
+
const centeredLeft =
|
|
60
|
+
anchor.left + anchor.width / 2 - previewBoardSizePx / 2;
|
|
61
|
+
const maxLeft = Math.max(
|
|
62
|
+
viewportMarginPx,
|
|
63
|
+
window.innerWidth - previewBoardSizePx - viewportMarginPx,
|
|
64
|
+
);
|
|
65
|
+
const left = Math.min(
|
|
66
|
+
Math.max(centeredLeft, viewportMarginPx),
|
|
67
|
+
maxLeft,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return `left:${left}px;top:${top}px;width:${previewBoardSizePx}px;`;
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<div
|
|
75
|
+
class="border-border pointer-events-none fixed z-50 overflow-hidden rounded-md border bg-white shadow-lg [&_*]:pointer-events-none"
|
|
76
|
+
style={previewStyle}
|
|
77
|
+
role="tooltip"
|
|
78
|
+
aria-label="Превью дебюта: {label}"
|
|
79
|
+
>
|
|
80
|
+
<Chessboard facade={previewBoardApi} design={previewDesign} />
|
|
81
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Возвращает FEN позиции для превью. */
|
|
3
|
+
fen: string;
|
|
4
|
+
/** Возвращает подпись дебютного тега. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Возвращает якорь кнопки тега для позиционирования popover. */
|
|
7
|
+
anchor: DOMRect;
|
|
8
|
+
}
|
|
9
|
+
declare const OpeningTagHoverPreview: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type OpeningTagHoverPreview = ReturnType<typeof OpeningTagHoverPreview>;
|
|
11
|
+
export default OpeningTagHoverPreview;
|
|
@@ -1,112 +1,212 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import { MediaQuery } from "svelte/reactivity";
|
|
4
|
+
import { mode } from "mode-watcher";
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import type {
|
|
7
|
+
BoardApi,
|
|
8
|
+
TChessBoardDesignSettings,
|
|
9
|
+
} from "@connectorvol/chessboard";
|
|
10
|
+
import {
|
|
11
|
+
Chessboard,
|
|
12
|
+
getBoardDesignSettingsContext,
|
|
13
|
+
} from "@connectorvol/chessboard";
|
|
14
|
+
import type { ChessTree, TTreeViewerTheme } from "@connectorvol/tree";
|
|
15
|
+
import {
|
|
16
|
+
TREE_VIEWER_DARK_THEME,
|
|
17
|
+
TREE_VIEWER_LIGHT_THEME,
|
|
18
|
+
TreeViewer,
|
|
19
|
+
} from "@connectorvol/tree";
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
import { cn } from "../utils.js";
|
|
10
22
|
|
|
11
|
-
/**
|
|
12
|
-
* Представляет свойства панели разметки «доска и просмотр дерева партии».
|
|
13
|
-
*/
|
|
14
|
-
interface Props {
|
|
15
23
|
/**
|
|
16
|
-
*
|
|
24
|
+
* Представляет свойства панели разметки «доска и просмотр дерева партии».
|
|
17
25
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
26
|
+
interface Props {
|
|
27
|
+
/**
|
|
28
|
+
* Возвращает API шахматной доски для компонента `Chessboard`.
|
|
29
|
+
*/
|
|
30
|
+
chessboard: BoardApi;
|
|
31
|
+
/**
|
|
32
|
+
* Возвращает снимок дизайна, передаваемый в `<Chessboard design={...} />`.
|
|
33
|
+
* Опционально: если не передан, доска читает дизайн из контекста
|
|
34
|
+
* (`setBoardDesignSettingsContext`), что позволяет реактивно подхватывать
|
|
35
|
+
* изменения из `<BoardSettings />`.
|
|
36
|
+
*/
|
|
37
|
+
chessboardDesign?: TChessBoardDesignSettings;
|
|
38
|
+
/**
|
|
39
|
+
* Возвращает экземпляр дерева ходов для `TreeViewer`.
|
|
40
|
+
*/
|
|
41
|
+
chessTree: ChessTree;
|
|
42
|
+
/**
|
|
43
|
+
* Возвращает обработчик выбора узла в дереве.
|
|
44
|
+
*/
|
|
45
|
+
onSelectNode: () => void;
|
|
46
|
+
/**
|
|
47
|
+
* Возвращает обработчик после удаления варианта.
|
|
48
|
+
*/
|
|
49
|
+
onDeleteVariant: () => void;
|
|
50
|
+
/**
|
|
51
|
+
* Возвращает функцию установки FEN в движке партии.
|
|
52
|
+
*/
|
|
53
|
+
setChessFen: (fen: string) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Возвращает функцию синхронизации FEN и UI доски.
|
|
56
|
+
*/
|
|
57
|
+
setChessboardFen: (animationTime?: number) => void;
|
|
58
|
+
/**
|
|
59
|
+
* Возвращает признак режима правки дерева (`TreeViewer.editMode`).
|
|
60
|
+
*/
|
|
61
|
+
editMode?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Возвращает признак возможности выбора узлов в дереве.
|
|
64
|
+
*/
|
|
65
|
+
selectable?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Возвращает true, если в режиме правки показывать группу NAG «Задача (развилка)»
|
|
68
|
+
* рядом с кнопками undo/redo на ходах из развилки.
|
|
69
|
+
*/
|
|
70
|
+
showPuzzleBranchNags?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Возвращает тему `TreeViewer` (фон, границы, текст). По умолчанию
|
|
73
|
+
* подстраивается под `mode-watcher` (dark → `TREE_VIEWER_DARK_THEME`,
|
|
74
|
+
* иначе `TREE_VIEWER_LIGHT_THEME`).
|
|
75
|
+
*/
|
|
76
|
+
theme?: TTreeViewerTheme;
|
|
77
|
+
/**
|
|
78
|
+
* Возвращает дополнительные классы Tailwind для корневого контейнера.
|
|
79
|
+
*/
|
|
80
|
+
class?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Возвращает фрагмент разметки под шахматной доской (например сообщение об ошибке в превью).
|
|
83
|
+
*/
|
|
84
|
+
belowChessboard?: Snippet;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const {
|
|
88
|
+
chessboard,
|
|
89
|
+
chessboardDesign,
|
|
90
|
+
chessTree,
|
|
91
|
+
onSelectNode,
|
|
92
|
+
onDeleteVariant,
|
|
93
|
+
setChessFen,
|
|
94
|
+
setChessboardFen,
|
|
95
|
+
editMode = true,
|
|
96
|
+
selectable = true,
|
|
97
|
+
showPuzzleBranchNags = false,
|
|
98
|
+
theme,
|
|
99
|
+
class: className,
|
|
100
|
+
belowChessboard,
|
|
101
|
+
}: Props = $props();
|
|
102
|
+
|
|
103
|
+
// ponytail: автотема по mode-watcher вместо жёсткого светлого дефолта,
|
|
104
|
+
// как в chess-tree/src/routes/game/+page.svelte. Явный проп `theme` побеждает.
|
|
105
|
+
const resolvedTheme = $derived<TTreeViewerTheme>(
|
|
106
|
+
theme ??
|
|
107
|
+
(mode.current === "dark"
|
|
108
|
+
? TREE_VIEWER_DARK_THEME
|
|
109
|
+
: TREE_VIEWER_LIGHT_THEME),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
/** ponytail: совпадает с Tailwind `lg` (1024px), как в разметке панели. */
|
|
113
|
+
const isMobile = new MediaQuery("(max-width: 1023px)");
|
|
114
|
+
|
|
115
|
+
const boardSize = $derived(chessboard.getBoard().settingsService.boardSize);
|
|
116
|
+
const useAutoBoardLayout = $derived(
|
|
117
|
+
isMobile.current || boardSize === "auto",
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const boardColumnStyle = $derived(
|
|
121
|
+
!useAutoBoardLayout && typeof boardSize === "number"
|
|
122
|
+
? `flex: 0 0 min(100%, ${boardSize}rem);`
|
|
123
|
+
: undefined,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
let savedDesktopBoardSettings = $state<{
|
|
127
|
+
boardSize: number | "auto";
|
|
128
|
+
isResizable: boolean;
|
|
129
|
+
} | null>(null);
|
|
130
|
+
|
|
131
|
+
$effect(() => {
|
|
132
|
+
const mobile = isMobile.current;
|
|
133
|
+
const settings = chessboard.getBoard().settingsService;
|
|
134
|
+
|
|
135
|
+
if (mobile) {
|
|
136
|
+
if (savedDesktopBoardSettings === null) {
|
|
137
|
+
savedDesktopBoardSettings = {
|
|
138
|
+
boardSize: settings.boardSize,
|
|
139
|
+
isResizable: chessboard.isResizable,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
chessboard.isResizable = false;
|
|
143
|
+
settings.boardSize = "auto";
|
|
144
|
+
} else if (savedDesktopBoardSettings !== null) {
|
|
145
|
+
chessboard.isResizable = savedDesktopBoardSettings.isResizable;
|
|
146
|
+
settings.boardSize = savedDesktopBoardSettings.boardSize;
|
|
147
|
+
savedDesktopBoardSettings = null;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
let boardHeight = $state(0);
|
|
152
|
+
|
|
153
|
+
const treeHeightLocked = $derived(boardHeight > 0);
|
|
154
|
+
|
|
155
|
+
const treeColumnStyle = $derived(
|
|
156
|
+
treeHeightLocked ? `--pane-board-height: ${boardHeight}px` : undefined,
|
|
157
|
+
);
|
|
73
158
|
</script>
|
|
74
159
|
|
|
75
|
-
<div
|
|
76
|
-
class={cn(
|
|
77
|
-
"relative flex min-h-0 flex-col justify-start lg:flex-row lg:items-stretch lg:justify-start lg:gap-4",
|
|
78
|
-
className,
|
|
79
|
-
)}
|
|
80
|
-
>
|
|
81
|
-
<div
|
|
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"
|
|
83
|
-
>
|
|
160
|
+
<div class={cn("flex w-full flex-col gap-2", className)}>
|
|
84
161
|
<div
|
|
85
|
-
|
|
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
|
-
)}
|
|
162
|
+
class="relative flex min-h-0 flex-col gap-4 lg:flex-row lg:items-stretch"
|
|
89
163
|
>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
164
|
+
<div
|
|
165
|
+
class={cn(
|
|
166
|
+
"order-1 flex w-full shrink-0 flex-col lg:order-1 lg:min-h-0",
|
|
167
|
+
useAutoBoardLayout && "lg:max-w-[min(100%,38rem)]",
|
|
168
|
+
)}
|
|
169
|
+
style={boardColumnStyle}
|
|
170
|
+
>
|
|
171
|
+
<div
|
|
172
|
+
class="aspect-square w-full shrink-0"
|
|
173
|
+
bind:clientHeight={boardHeight}
|
|
174
|
+
>
|
|
175
|
+
<Chessboard
|
|
176
|
+
facade={chessboard}
|
|
177
|
+
design={chessboardDesign ??
|
|
178
|
+
getBoardDesignSettingsContext() ??
|
|
179
|
+
undefined}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
103
183
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
184
|
+
<div
|
|
185
|
+
class={cn(
|
|
186
|
+
"order-2 flex min-h-48 min-w-0 flex-1 flex-col lg:order-2 lg:min-w-[20rem] lg:shrink-0",
|
|
187
|
+
treeHeightLocked &&
|
|
188
|
+
"lg:h-[var(--pane-board-height)] lg:max-h-[var(--pane-board-height)] lg:min-h-0 lg:overflow-hidden",
|
|
189
|
+
)}
|
|
190
|
+
style={treeColumnStyle}
|
|
191
|
+
>
|
|
192
|
+
<div
|
|
193
|
+
class="flex min-h-0 flex-1 flex-col overflow-hidden border border-border"
|
|
194
|
+
>
|
|
195
|
+
<TreeViewer
|
|
196
|
+
{chessTree}
|
|
197
|
+
{onSelectNode}
|
|
198
|
+
{onDeleteVariant}
|
|
199
|
+
{setChessFen}
|
|
200
|
+
{setChessboardFen}
|
|
201
|
+
className="min-h-0 flex-1 rounded-none border-0 bg-[var(--tree-viewer-bg)]"
|
|
202
|
+
{editMode}
|
|
203
|
+
{selectable}
|
|
204
|
+
{showPuzzleBranchNags}
|
|
205
|
+
theme={resolvedTheme}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
109
209
|
</div>
|
|
210
|
+
|
|
110
211
|
{@render belowChessboard?.()}
|
|
111
|
-
</div>
|
|
112
212
|
</div>
|