@connectorvol/chess-widgets 8.0.1 → 9.0.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.
- 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 +74 -70
- package/dist/game-analyzer/gameAnalyzer.svelte.js +8 -6
- package/dist/game-analyzer/types.d.ts +11 -3
- 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 +104 -32
- package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte.d.ts +16 -2
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte +192 -202
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte.d.ts +47 -3
- package/dist/puzzle-creation/PuzzlePgnBoardTreeEditor.svelte +485 -74
- package/dist/puzzle-creation/PuzzleWizardTagsStep.svelte +36 -0
- package/dist/puzzle-creation/PuzzleWizardTagsStep.svelte.d.ts +12 -0
- package/dist/puzzle-creation/StepMoves.svelte +38 -18
- package/dist/puzzle-creation/StepMoves.svelte.d.ts +2 -1
- package/dist/puzzle-creation/StepPosition.svelte +15 -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 +37 -7
- package/package.json +20 -17
|
@@ -23,31 +23,15 @@ function solverJustPlayedLastOnLine(fullFen, solverColor) {
|
|
|
23
23
|
return sideToMoveFromFullFen(fullFen) !== solverColor;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* Представляет проверку линии решения задачи:
|
|
27
|
-
*
|
|
28
|
-
* решателя и окончание вариантов на развилке соперника ходом решателя вне поддерева
|
|
29
|
-
* после хода решателя с меткой «неверное решение».
|
|
26
|
+
* Представляет проверку линии решения задачи: разметка развилок со стороны решателя
|
|
27
|
+
* и окончание вариантов на развилке соперника ходом решателя.
|
|
30
28
|
*/
|
|
31
29
|
export function validatePuzzleSolverForkAnnotations(tree, solverColor) {
|
|
32
|
-
function walk(node,
|
|
33
|
-
const puzzleForkNags = node.data.nags ?? [];
|
|
34
|
-
const hasPuzzleForkMarker = puzzleForkNags.includes(PUZZLE_BRANCH_CORRECT_NAG_ID) ||
|
|
35
|
-
puzzleForkNags.includes(PUZZLE_BRANCH_WRONG_NAG_ID);
|
|
36
|
-
if (hasPuzzleForkMarker && (!parent || parent.children.length <= 1)) {
|
|
37
|
-
return {
|
|
38
|
-
ok: false,
|
|
39
|
-
reason: "Метки верного или неверного хода задачи (✓ / ✗) можно ставить только на ход из развилки: у родительской позиции должно быть несколько вариантов хода.",
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
if (ancestorsContainWrongMarkedSolver && hasPuzzleForkMarker) {
|
|
43
|
-
return {
|
|
44
|
-
ok: false,
|
|
45
|
-
reason: "Внутри продолжения после хода решателя с меткой ✗ «неверное решение» не ставьте метки ✓ и ✗ на последующих развилках.",
|
|
46
|
-
};
|
|
47
|
-
}
|
|
30
|
+
function walk(node, ancestorsContainWrongMarkedSolver) {
|
|
48
31
|
if (node.children.length > 1) {
|
|
49
32
|
const mover = sideToMoveFromFullFen(node.data.fen);
|
|
50
33
|
if (mover === solverColor && !ancestorsContainWrongMarkedSolver) {
|
|
34
|
+
let hasAnyCorrect = false;
|
|
51
35
|
for (const child of node.children) {
|
|
52
36
|
const nags = child.data.nags ?? [];
|
|
53
37
|
const markedCorrect = nags.includes(PUZZLE_BRANCH_CORRECT_NAG_ID);
|
|
@@ -59,6 +43,7 @@ export function validatePuzzleSolverForkAnnotations(tree, solverColor) {
|
|
|
59
43
|
};
|
|
60
44
|
}
|
|
61
45
|
if (markedCorrect) {
|
|
46
|
+
hasAnyCorrect = true;
|
|
62
47
|
const leaf = tailAlongFirstVariation(child);
|
|
63
48
|
if (!solverJustPlayedLastOnLine(leaf.data.fen, solverColor)) {
|
|
64
49
|
return {
|
|
@@ -68,6 +53,12 @@ export function validatePuzzleSolverForkAnnotations(tree, solverColor) {
|
|
|
68
53
|
}
|
|
69
54
|
}
|
|
70
55
|
}
|
|
56
|
+
if (!hasAnyCorrect) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
reason: "На каждой развилке со стороны решателя хотя бы один вариант должен быть отмечен как верный ход (✓).",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
71
62
|
}
|
|
72
63
|
else if (mover !== solverColor && !ancestorsContainWrongMarkedSolver) {
|
|
73
64
|
for (const child of node.children) {
|
|
@@ -84,11 +75,11 @@ export function validatePuzzleSolverForkAnnotations(tree, solverColor) {
|
|
|
84
75
|
const wrongMarkedAmongAncestorsForChildren = ancestorsContainWrongMarkedSolver ||
|
|
85
76
|
(node.data.nags ?? []).includes(PUZZLE_BRANCH_WRONG_NAG_ID);
|
|
86
77
|
for (const child of node.children) {
|
|
87
|
-
const sub = walk(child,
|
|
78
|
+
const sub = walk(child, wrongMarkedAmongAncestorsForChildren);
|
|
88
79
|
if (!sub.ok)
|
|
89
80
|
return sub;
|
|
90
81
|
}
|
|
91
82
|
return { ok: true };
|
|
92
83
|
}
|
|
93
|
-
return walk(tree.rootNode.moves,
|
|
84
|
+
return walk(tree.rootNode.moves, false);
|
|
94
85
|
}
|
|
@@ -28,7 +28,19 @@ export declare function puzzlePreviewFindChildIndexForPlayedSan(cursorFen: strin
|
|
|
28
28
|
/**
|
|
29
29
|
* Представляет классификацию хода решателя на развилке (или на единственном продолжении).
|
|
30
30
|
*/
|
|
31
|
-
export declare function puzzlePreviewClassifySolverMove(cursorNode: ChessTreeNode, cursorFen: string, playedSan: string,
|
|
31
|
+
export declare function puzzlePreviewClassifySolverMove(cursorNode: ChessTreeNode, cursorFen: string, playedSan: string, _solverColor: Color): TSolverMoveOutcome;
|
|
32
|
+
/**
|
|
33
|
+
* Представляет сбор данных узла для произвольного неверного хода решателя (нет в PGN).
|
|
34
|
+
*/
|
|
35
|
+
export declare function puzzlePreviewBuildAdHocWrongMoveData(cursorFen: string, san: string): ChessTreeNode["data"] | null;
|
|
36
|
+
/**
|
|
37
|
+
* Представляет добавление отсутствующего в PGN хода как новой ветки дерева решения с NAG ✗.
|
|
38
|
+
*/
|
|
39
|
+
export declare function puzzlePreviewAddAdHocWrongMoveToSolutionTree(solutionTree: ChessTree, cursorPath: number[], cursorFen: string, san: string): ChessTreeNode | null;
|
|
40
|
+
/**
|
|
41
|
+
* Представляет пересчёт пути индексов после перестановки детей на развилках.
|
|
42
|
+
*/
|
|
43
|
+
export declare function puzzlePreviewRemapPathAfterReorder(rootMoves: ChessTreeNode, path: number[]): number[];
|
|
32
44
|
/**
|
|
33
45
|
* Представляет проверку: линия закончилась ключевым ходом решателя (ход соперника, лист дерева).
|
|
34
46
|
*/
|
|
@@ -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,10 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
+
import { MediaQuery } from "svelte/reactivity";
|
|
3
4
|
|
|
4
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
BoardApi,
|
|
7
|
+
TChessBoardDesignSettings,
|
|
8
|
+
} from "@connectorvol/chessboard";
|
|
5
9
|
import { Chessboard } from "@connectorvol/chessboard";
|
|
6
|
-
import type { ChessTree } from "@connectorvol/tree";
|
|
7
|
-
import { TreeViewer } from "@connectorvol/tree";
|
|
10
|
+
import type { ChessTree, TTreeViewerTheme } from "@connectorvol/tree";
|
|
11
|
+
import { TREE_VIEWER_LIGHT_THEME, TreeViewer } from "@connectorvol/tree";
|
|
8
12
|
|
|
9
13
|
import { cn } from "../utils.js";
|
|
10
14
|
|
|
@@ -16,6 +20,10 @@
|
|
|
16
20
|
* Возвращает API шахматной доски для компонента `Chessboard`.
|
|
17
21
|
*/
|
|
18
22
|
chessboard: BoardApi;
|
|
23
|
+
/**
|
|
24
|
+
* Возвращает снимок дизайна, передаваемый в `<Chessboard design={...} />`.
|
|
25
|
+
*/
|
|
26
|
+
chessboardDesign: TChessBoardDesignSettings;
|
|
19
27
|
/**
|
|
20
28
|
* Возвращает экземпляр дерева ходов для `TreeViewer`.
|
|
21
29
|
*/
|
|
@@ -44,6 +52,16 @@
|
|
|
44
52
|
* Возвращает признак возможности выбора узлов в дереве.
|
|
45
53
|
*/
|
|
46
54
|
selectable?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Возвращает true, если в режиме правки показывать группу NAG «Задача (развилка)»
|
|
57
|
+
* рядом с кнопками undo/redo на ходах из развилки.
|
|
58
|
+
*/
|
|
59
|
+
showPuzzleBranchNags?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Возвращает тему `TreeViewer` (фон, границы, текст). Если не задана —
|
|
62
|
+
* используется `TREE_VIEWER_LIGHT_THEME`.
|
|
63
|
+
*/
|
|
64
|
+
theme?: TTreeViewerTheme;
|
|
47
65
|
/**
|
|
48
66
|
* Возвращает дополнительные классы Tailwind для корневого контейнера.
|
|
49
67
|
*/
|
|
@@ -56,6 +74,7 @@
|
|
|
56
74
|
|
|
57
75
|
const {
|
|
58
76
|
chessboard,
|
|
77
|
+
chessboardDesign,
|
|
59
78
|
chessTree,
|
|
60
79
|
onSelectNode,
|
|
61
80
|
onDeleteVariant,
|
|
@@ -63,50 +82,103 @@
|
|
|
63
82
|
setChessboardFen,
|
|
64
83
|
editMode = true,
|
|
65
84
|
selectable = true,
|
|
85
|
+
showPuzzleBranchNags = false,
|
|
86
|
+
theme = TREE_VIEWER_LIGHT_THEME,
|
|
66
87
|
class: className,
|
|
67
88
|
belowChessboard,
|
|
68
89
|
}: Props = $props();
|
|
69
90
|
|
|
70
|
-
|
|
71
|
-
|
|
91
|
+
/** ponytail: совпадает с Tailwind `lg` (1024px), как в разметке панели. */
|
|
92
|
+
const isMobile = new MediaQuery("(max-width: 1023px)");
|
|
93
|
+
|
|
94
|
+
const boardSize = $derived(chessboard.getBoard().settingsService.boardSize);
|
|
95
|
+
const useAutoBoardLayout = $derived(isMobile.current || boardSize === "auto");
|
|
96
|
+
|
|
97
|
+
const boardColumnStyle = $derived(
|
|
98
|
+
!useAutoBoardLayout && typeof boardSize === "number"
|
|
99
|
+
? `flex: 0 0 min(100%, ${boardSize}rem);`
|
|
100
|
+
: undefined,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
let savedDesktopBoardSettings = $state<{
|
|
104
|
+
boardSize: number | "auto";
|
|
105
|
+
isResizable: boolean;
|
|
106
|
+
} | null>(null);
|
|
107
|
+
|
|
108
|
+
$effect(() => {
|
|
109
|
+
const mobile = isMobile.current;
|
|
110
|
+
const settings = chessboard.getBoard().settingsService;
|
|
111
|
+
|
|
112
|
+
if (mobile) {
|
|
113
|
+
if (savedDesktopBoardSettings === null) {
|
|
114
|
+
savedDesktopBoardSettings = {
|
|
115
|
+
boardSize: settings.boardSize,
|
|
116
|
+
isResizable: chessboard.isResizable,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
chessboard.isResizable = false;
|
|
120
|
+
settings.boardSize = "auto";
|
|
121
|
+
} else if (savedDesktopBoardSettings !== null) {
|
|
122
|
+
chessboard.isResizable = savedDesktopBoardSettings.isResizable;
|
|
123
|
+
settings.boardSize = savedDesktopBoardSettings.boardSize;
|
|
124
|
+
savedDesktopBoardSettings = null;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
let boardHeight = $state(0);
|
|
129
|
+
|
|
130
|
+
const treeHeightLocked = $derived(boardHeight > 0);
|
|
131
|
+
|
|
132
|
+
const treeColumnStyle = $derived(
|
|
133
|
+
treeHeightLocked ? `--pane-board-height: ${boardHeight}px` : undefined,
|
|
72
134
|
);
|
|
73
135
|
</script>
|
|
74
136
|
|
|
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
|
-
>
|
|
137
|
+
<div class={cn("flex w-full flex-col gap-2", className)}>
|
|
81
138
|
<div
|
|
82
|
-
class="
|
|
139
|
+
class="relative flex min-h-0 flex-col gap-4 lg:flex-row lg:items-stretch"
|
|
83
140
|
>
|
|
84
141
|
<div
|
|
85
142
|
class={cn(
|
|
86
|
-
"flex
|
|
87
|
-
|
|
143
|
+
"order-1 flex w-full shrink-0 flex-col lg:order-1 lg:min-h-0",
|
|
144
|
+
useAutoBoardLayout && "lg:max-w-[min(100%,38rem)]",
|
|
88
145
|
)}
|
|
146
|
+
style={boardColumnStyle}
|
|
89
147
|
>
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
pieceSet={chessboard.chessSet}
|
|
97
|
-
className="min-h-0 flex-1 border-x-0 border-t-0"
|
|
98
|
-
{editMode}
|
|
99
|
-
{selectable}
|
|
100
|
-
/>
|
|
148
|
+
<div
|
|
149
|
+
class="aspect-square w-full shrink-0"
|
|
150
|
+
bind:clientHeight={boardHeight}
|
|
151
|
+
>
|
|
152
|
+
<Chessboard facade={chessboard} design={chessboardDesign} />
|
|
153
|
+
</div>
|
|
101
154
|
</div>
|
|
102
|
-
</div>
|
|
103
155
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
156
|
+
<div
|
|
157
|
+
class={cn(
|
|
158
|
+
"order-2 flex min-h-48 min-w-0 flex-1 flex-col lg:order-2 lg:min-w-[20rem] lg:shrink-0",
|
|
159
|
+
treeHeightLocked &&
|
|
160
|
+
"lg:h-[var(--pane-board-height)] lg:max-h-[var(--pane-board-height)] lg:min-h-0 lg:overflow-hidden",
|
|
161
|
+
)}
|
|
162
|
+
style={treeColumnStyle}
|
|
163
|
+
>
|
|
164
|
+
<div
|
|
165
|
+
class="flex min-h-0 flex-1 flex-col overflow-hidden border border-border"
|
|
166
|
+
>
|
|
167
|
+
<TreeViewer
|
|
168
|
+
{chessTree}
|
|
169
|
+
{onSelectNode}
|
|
170
|
+
{onDeleteVariant}
|
|
171
|
+
{setChessFen}
|
|
172
|
+
{setChessboardFen}
|
|
173
|
+
className="min-h-0 flex-1 rounded-none border-0"
|
|
174
|
+
{editMode}
|
|
175
|
+
{selectable}
|
|
176
|
+
{showPuzzleBranchNags}
|
|
177
|
+
{theme}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
109
180
|
</div>
|
|
110
|
-
{@render belowChessboard?.()}
|
|
111
181
|
</div>
|
|
182
|
+
|
|
183
|
+
{@render belowChessboard?.()}
|
|
112
184
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from "svelte";
|
|
2
|
-
import type { BoardApi } from "@connectorvol/chessboard";
|
|
3
|
-
import type { ChessTree } from "@connectorvol/tree";
|
|
2
|
+
import type { BoardApi, TChessBoardDesignSettings } from "@connectorvol/chessboard";
|
|
3
|
+
import type { ChessTree, TTreeViewerTheme } from "@connectorvol/tree";
|
|
4
4
|
/**
|
|
5
5
|
* Представляет свойства панели разметки «доска и просмотр дерева партии».
|
|
6
6
|
*/
|
|
@@ -9,6 +9,10 @@ interface Props {
|
|
|
9
9
|
* Возвращает API шахматной доски для компонента `Chessboard`.
|
|
10
10
|
*/
|
|
11
11
|
chessboard: BoardApi;
|
|
12
|
+
/**
|
|
13
|
+
* Возвращает снимок дизайна, передаваемый в `<Chessboard design={...} />`.
|
|
14
|
+
*/
|
|
15
|
+
chessboardDesign: TChessBoardDesignSettings;
|
|
12
16
|
/**
|
|
13
17
|
* Возвращает экземпляр дерева ходов для `TreeViewer`.
|
|
14
18
|
*/
|
|
@@ -37,6 +41,16 @@ interface Props {
|
|
|
37
41
|
* Возвращает признак возможности выбора узлов в дереве.
|
|
38
42
|
*/
|
|
39
43
|
selectable?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Возвращает true, если в режиме правки показывать группу NAG «Задача (развилка)»
|
|
46
|
+
* рядом с кнопками undo/redo на ходах из развилки.
|
|
47
|
+
*/
|
|
48
|
+
showPuzzleBranchNags?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Возвращает тему `TreeViewer` (фон, границы, текст). Если не задана —
|
|
51
|
+
* используется `TREE_VIEWER_LIGHT_THEME`.
|
|
52
|
+
*/
|
|
53
|
+
theme?: TTreeViewerTheme;
|
|
40
54
|
/**
|
|
41
55
|
* Возвращает дополнительные классы Tailwind для корневого контейнера.
|
|
42
56
|
*/
|