@connectorvol/chess-widgets 1.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.
Files changed (35) hide show
  1. package/dist/button-variants.d.ts +73 -0
  2. package/dist/button-variants.js +31 -0
  3. package/dist/constants/editable-board-settings.d.ts +26 -0
  4. package/dist/constants/editable-board-settings.js +27 -0
  5. package/dist/index.d.ts +12 -0
  6. package/dist/index.js +8 -0
  7. package/dist/position-editor/EditFen.svelte +164 -0
  8. package/dist/position-editor/EditFen.svelte.d.ts +16 -0
  9. package/dist/position-editor/EditMove.svelte +180 -0
  10. package/dist/position-editor/EditMove.svelte.d.ts +9 -0
  11. package/dist/position-editor/EditPanel.svelte +164 -0
  12. package/dist/position-editor/EditPanel.svelte.d.ts +8 -0
  13. package/dist/position-editor/fen.svelte.d.ts +26 -0
  14. package/dist/position-editor/fen.svelte.js +177 -0
  15. package/dist/puzzle/puzzleCreatedPayload.d.ts +17 -0
  16. package/dist/puzzle/puzzleCreatedPayload.js +24 -0
  17. package/dist/puzzle/puzzleData.d.ts +32 -0
  18. package/dist/puzzle/puzzleData.js +51 -0
  19. package/dist/puzzle/puzzleSolverForkAnnotations.d.ts +14 -0
  20. package/dist/puzzle/puzzleSolverForkAnnotations.js +94 -0
  21. package/dist/puzzle/puzzleStepPreviewSolver.d.ts +39 -0
  22. package/dist/puzzle/puzzleStepPreviewSolver.js +87 -0
  23. package/dist/puzzle-creation/PuzzleCreationWizard.svelte +247 -0
  24. package/dist/puzzle-creation/PuzzleCreationWizard.svelte.d.ts +5 -0
  25. package/dist/puzzle-creation/StepMoves.svelte +225 -0
  26. package/dist/puzzle-creation/StepMoves.svelte.d.ts +15 -0
  27. package/dist/puzzle-creation/StepPosition.svelte +210 -0
  28. package/dist/puzzle-creation/StepPosition.svelte.d.ts +11 -0
  29. package/dist/puzzle-creation/StepPreview.svelte +589 -0
  30. package/dist/puzzle-creation/StepPreview.svelte.d.ts +23 -0
  31. package/dist/puzzle-creation/types.d.ts +27 -0
  32. package/dist/puzzle-creation/types.js +1 -0
  33. package/dist/utils.d.ts +15 -0
  34. package/dist/utils.js +8 -0
  35. package/package.json +76 -0
@@ -0,0 +1,87 @@
1
+ import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
2
+ import { Color, PUZZLE_BRANCH_WRONG_NAG_ID } from "@connectorvol/shared";
3
+ /**
4
+ * Представляет извлечение стороны, имеющей ход, из полной строки FEN.
5
+ */
6
+ export function puzzlePreviewSideToMoveFromFen(fullFen) {
7
+ const token = fullFen.trim().split(/\s+/)[1];
8
+ return token === "b" ? Color.BLACK : Color.WHITE;
9
+ }
10
+ /**
11
+ * Представляет получение узла дерева по пути индексов детей от корня партии.
12
+ */
13
+ export function puzzlePreviewNodeAtPath(rootMoves, path) {
14
+ let node = rootMoves;
15
+ for (const idx of path) {
16
+ node = node.children[idx];
17
+ }
18
+ return node;
19
+ }
20
+ /**
21
+ * Представляет поиск индекса ребёнка, чей SAN даёт ту же результирующую позицию, что и сыгранный SAN.
22
+ */
23
+ export function puzzlePreviewFindChildIndexForPlayedSan(cursorFen, children, playedSan) {
24
+ const playedProbe = new PgnOps(cursorFen, "chess");
25
+ try {
26
+ playedProbe.makeSanMove(playedSan);
27
+ }
28
+ catch {
29
+ return -1;
30
+ }
31
+ const playedResultFen = playedProbe.fen();
32
+ for (let i = 0; i < children.length; i++) {
33
+ const childProbe = new PgnOps(cursorFen, "chess");
34
+ try {
35
+ childProbe.makeSanMove(children[i].data.san);
36
+ if (childProbe.fen() === playedResultFen) {
37
+ return i;
38
+ }
39
+ }
40
+ catch {
41
+ continue;
42
+ }
43
+ }
44
+ return -1;
45
+ }
46
+ /**
47
+ * Представляет классификацию хода решателя на развилке (или на единственном продолжении).
48
+ */
49
+ export function puzzlePreviewClassifySolverMove(cursorNode, cursorFen, playedSan, solverColor) {
50
+ const children = cursorNode.children;
51
+ const idx = puzzlePreviewFindChildIndexForPlayedSan(cursorFen, children, playedSan);
52
+ if (idx < 0) {
53
+ return { kind: "no_matching_variant" };
54
+ }
55
+ const child = children[idx];
56
+ const mover = puzzlePreviewSideToMoveFromFen(cursorFen);
57
+ const isSolverFork = children.length > 1 && mover === solverColor;
58
+ if (isSolverFork) {
59
+ const nags = child.data.nags ?? [];
60
+ if (nags.includes(PUZZLE_BRANCH_WRONG_NAG_ID)) {
61
+ return { kind: "wrong_marked_variant", wrongBranchRoot: child };
62
+ }
63
+ }
64
+ return { kind: "correct", childIndex: idx };
65
+ }
66
+ /**
67
+ * Представляет проверку: линия закончилась ключевым ходом решателя (ход соперника, лист дерева).
68
+ */
69
+ export function puzzlePreviewIsSolvedPosition(previewChess, leafNode, solverColor) {
70
+ return (leafNode.children.length === 0 &&
71
+ puzzlePreviewSideToMoveFromFen(previewChess.fen()) !== solverColor);
72
+ }
73
+ /**
74
+ * Представляет рекурсивное клонирование поддерева варианта под текущим узлом студенческого дерева (новые id обеспечивает ChessTree.addNodeToCurrent).
75
+ */
76
+ export function puzzlePreviewDupSubtreeUnderStudentCursor(studentTree, sourceNode) {
77
+ studentTree.addNodeToCurrent({
78
+ id: "",
79
+ children: [],
80
+ data: { ...sourceNode.data },
81
+ });
82
+ const anchor = studentTree.currentNode;
83
+ for (const ch of sourceNode.children) {
84
+ puzzlePreviewDupSubtreeUnderStudentCursor(studentTree, ch);
85
+ studentTree.currentNode = anchor;
86
+ }
87
+ }
@@ -0,0 +1,247 @@
1
+ <script lang="ts" module>
2
+ export type { TPuzzleCreationWizardProps } from "./types.js";
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ import { untrack } from "svelte";
7
+
8
+ import StepMoves from "./StepMoves.svelte";
9
+ import StepPosition from "./StepPosition.svelte";
10
+ import StepPreview from "./StepPreview.svelte";
11
+ import { DEFAULT_EDITABLE_BOARD_SETTINGS } from "../constants/editable-board-settings.js";
12
+ import type { TPuzzleCreationWizardProps } from "./types.js";
13
+ import {
14
+ createEmptyTreeFromFen,
15
+ createInitialPuzzleData,
16
+ type PuzzleData,
17
+ } from "../puzzle/puzzleData.js";
18
+ import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
19
+ import { ChessTree, createPgnFromTree } from "@connectorvol/tree";
20
+ import {
21
+ CHESSBOARD_THEMES,
22
+ createBoardApi,
23
+ DEFAULT_BOARD_SETTINGS,
24
+ Draggable,
25
+ type IChessBoardActions,
26
+ } from "@connectorvol/chessboard";
27
+ import { calculatePly, Color, Square } from "@connectorvol/shared";
28
+
29
+ const {
30
+ onPuzzleCreated,
31
+ fen: fenProp,
32
+ boardTheme,
33
+ boardAppearanceSettings,
34
+ }: TPuzzleCreationWizardProps = $props();
35
+
36
+ /**
37
+ * Представляет нормализацию входного FEN из пропса (пустая строка трактуется как отсутствие значения).
38
+ */
39
+ function wizardSeedFen(prop: string | undefined): string | undefined {
40
+ const t = prop?.trim();
41
+ return t ? t : undefined;
42
+ }
43
+
44
+ /**
45
+ * Представляет извлечение признака стороны, имеющей ход, из полной строки FEN.
46
+ */
47
+ function sideToMoveFromFullFen(fullFen: string): Color {
48
+ const token = fullFen.trim().split(/\s+/)[1];
49
+ return token === "b" ? Color.BLACK : Color.WHITE;
50
+ }
51
+
52
+ let step = $state<1 | 2 | 3>(1);
53
+ let puzzleData = $derived<PuzzleData>(
54
+ createInitialPuzzleData(wizardSeedFen(fenProp)),
55
+ );
56
+
57
+ /** Возвращает PGN дерева решения для превью на шаге 3. */
58
+ let solutionPgnForPreview = $state("");
59
+
60
+ function handleMovesBack() {
61
+ step = 1;
62
+ }
63
+
64
+ function handleMovesNext(moves: string[]) {
65
+ puzzleData.moves = moves;
66
+ solutionPgnForPreview = createPgnFromTree(tree.rootNode);
67
+ step = 3;
68
+ }
69
+
70
+ function handlePreviewBack() {
71
+ step = 2;
72
+ }
73
+
74
+ let chess = $derived(new PgnOps(puzzleData.initialFen, "chess"));
75
+ let tree = $derived(
76
+ new ChessTree(createEmptyTreeFromFen(puzzleData.initialFen)),
77
+ );
78
+ const addMoveToTree = (san: string) => {
79
+ const move = chess.makeSanMove(san);
80
+ if (!move) return;
81
+ const fen = chess.fen();
82
+ const { halfMoves, fullMoves } = calculatePly(fen);
83
+ tree.addNodeToCurrent({
84
+ id: "",
85
+ children: [],
86
+ data: {
87
+ fen,
88
+ san,
89
+ ply: halfMoves,
90
+ fullMoves,
91
+ },
92
+ });
93
+ return { move, fen, turn: chess.turn() };
94
+ };
95
+
96
+ const actions: IChessBoardActions = {
97
+ game: {
98
+ possibleMovesOnSquare: (square: Square) => chess.moves(square),
99
+ beforePieceMoveSan(san: string) {
100
+ const result = addMoveToTree(san);
101
+ return result;
102
+ },
103
+ afterPieceMoveSan: () => {},
104
+ beforePieceMove: (from, to, promotion) => {
105
+ const san = chess.getSanForMove({ from, to, promotion });
106
+ chess.makeMove({ from, to, promotion });
107
+ const fen = chess.fen();
108
+ const { halfMoves, fullMoves } = calculatePly(fen);
109
+ tree.addNodeToCurrent({
110
+ id: "",
111
+ children: [],
112
+ data: {
113
+ fen,
114
+ san,
115
+ ply: halfMoves,
116
+ fullMoves,
117
+ },
118
+ });
119
+ return fen;
120
+ },
121
+ afterPieceMove: () => {
122
+ const side = chess.turn();
123
+ chessboard.draggable =
124
+ side === Color.WHITE ? Draggable.WHITE : Draggable.BLACK;
125
+ },
126
+ },
127
+ };
128
+
129
+ /**
130
+ * Представляет сборку доски для шага построения линии: позиция и ориентация по стороне хода в FEN.
131
+ */
132
+ function createMainChessboard(fullFen: string) {
133
+ const side = sideToMoveFromFullFen(fullFen);
134
+ const {
135
+ boardSize: _boardSize,
136
+ orientation: _orientation,
137
+ draggable: _draggable,
138
+ ...appearanceSettings
139
+ } = (boardAppearanceSettings ?? {}) as Record<string, unknown> & {
140
+ boardSize?: unknown;
141
+ orientation?: unknown;
142
+ draggable?: unknown;
143
+ };
144
+ return createBoardApi({
145
+ fen: fullFen,
146
+ settings: {
147
+ ...DEFAULT_BOARD_SETTINGS,
148
+ ...(appearanceSettings as typeof DEFAULT_BOARD_SETTINGS),
149
+ boardSize: DEFAULT_EDITABLE_BOARD_SETTINGS.boardSize,
150
+ isResizable: false,
151
+ orientation: side,
152
+ draggable: side === Color.WHITE ? Draggable.WHITE : Draggable.BLACK,
153
+ },
154
+ actions,
155
+ theme: boardTheme ?? CHESSBOARD_THEMES.blue,
156
+ });
157
+ }
158
+
159
+ let chessboard = $derived(createMainChessboard(puzzleData.initialFen));
160
+
161
+ /**
162
+ * Представляет сброс мастера при смене входного FEN извне (шаг 1 и черновик ходов обнуляются).
163
+ */
164
+ $effect(() => {
165
+ const seed = wizardSeedFen(fenProp);
166
+ const next = createInitialPuzzleData(seed);
167
+ untrack(() => {
168
+ puzzleData = next;
169
+ chess = new PgnOps(next.initialFen, "chess");
170
+ tree = new ChessTree(createEmptyTreeFromFen(next.initialFen));
171
+ chessboard = createMainChessboard(next.initialFen);
172
+ step = 1;
173
+ solutionPgnForPreview = "";
174
+ });
175
+ });
176
+
177
+ function handlePositionNext(fen: string) {
178
+ puzzleData.initialFen = fen;
179
+ chess = new PgnOps(fen, "chess");
180
+ tree = new ChessTree(createEmptyTreeFromFen(fen));
181
+ chessboard = createMainChessboard(fen);
182
+ step = 2;
183
+ }
184
+ </script>
185
+
186
+ <div class="mx-4 lg:container lg:mx-auto">
187
+ <div class="flex flex-col gap-4 pt-4 pb-0">
188
+ <div class="flex items-center gap-2">
189
+ <span
190
+ class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium"
191
+ class:bg-primary={step >= 1}
192
+ class:bg-muted={step < 1}
193
+ class:text-primary-foreground={step >= 1}
194
+ >
195
+ 1
196
+ </span>
197
+ <span class="text-sm">Позиция</span>
198
+ <span class="h-px flex-1 bg-border"></span>
199
+ <span
200
+ class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium"
201
+ class:bg-primary={step >= 2}
202
+ class:bg-muted={step < 2}
203
+ class:text-primary-foreground={step >= 2}
204
+ >
205
+ 2
206
+ </span>
207
+ <span class="text-sm">Ходы</span>
208
+ <span class="h-px flex-1 bg-border"></span>
209
+ <span
210
+ class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium"
211
+ class:bg-primary={step >= 3}
212
+ class:bg-muted={step < 3}
213
+ class:text-primary-foreground={step >= 3}
214
+ >
215
+ 3
216
+ </span>
217
+ <span class="text-sm">Превью</span>
218
+ </div>
219
+
220
+ {#if step === 1}
221
+ <StepPosition
222
+ initialFen={puzzleData.initialFen}
223
+ onNext={handlePositionNext}
224
+ {boardTheme}
225
+ boardAppearanceSettings={boardAppearanceSettings}
226
+ />
227
+ {:else if step === 2}
228
+ <StepMoves
229
+ {puzzleData}
230
+ {chess}
231
+ {tree}
232
+ {chessboard}
233
+ onBack={handleMovesBack}
234
+ onNext={handleMovesNext}
235
+ />
236
+ {:else}
237
+ <StepPreview
238
+ {puzzleData}
239
+ solutionPgn={solutionPgnForPreview}
240
+ onBack={handlePreviewBack}
241
+ {onPuzzleCreated}
242
+ {boardTheme}
243
+ boardAppearanceSettings={boardAppearanceSettings}
244
+ />
245
+ {/if}
246
+ </div>
247
+ </div>
@@ -0,0 +1,5 @@
1
+ export type { TPuzzleCreationWizardProps } from "./types.js";
2
+ import type { TPuzzleCreationWizardProps } from "./types.js";
3
+ declare const PuzzleCreationWizard: import("svelte").Component<TPuzzleCreationWizardProps, {}, "">;
4
+ type PuzzleCreationWizard = ReturnType<typeof PuzzleCreationWizard>;
5
+ export default PuzzleCreationWizard;
@@ -0,0 +1,225 @@
1
+ <script lang="ts">
2
+ import type { BoardApi } from "@connectorvol/chessboard";
3
+
4
+ import { Chessboard, Draggable } from "@connectorvol/chessboard";
5
+ import { ChessTree, TreeViewer } from "@connectorvol/tree";
6
+ import { Color, DEFAULT_PIECE_SET } from "@connectorvol/shared";
7
+ import { Popover } from "bits-ui";
8
+ import { buttonVariants } from "../button-variants.js";
9
+ import { cn } from "../utils.js";
10
+
11
+ import type { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
12
+ import {
13
+ getMainLineFromTree,
14
+ type PuzzleData,
15
+ } from "../puzzle/puzzleData.js";
16
+ import { validatePuzzleSolverForkAnnotations } from "../puzzle/puzzleSolverForkAnnotations.js";
17
+
18
+ interface Props {
19
+ puzzleData: PuzzleData;
20
+ onBack: () => void;
21
+ onNext: (moves: string[]) => void;
22
+ chess: PgnOps;
23
+ tree: ChessTree;
24
+ chessboard: BoardApi;
25
+ }
26
+
27
+ const { puzzleData, onBack, onNext, chess, tree, chessboard }: Props =
28
+ $props();
29
+
30
+ const initialTurn = $derived(
31
+ puzzleData.initialFen.trim().split(/\s+/)[1] === "b"
32
+ ? Color.BLACK
33
+ : Color.WHITE,
34
+ );
35
+
36
+ const mainLine = $derived(getMainLineFromTree(tree.rootNode));
37
+
38
+ function setChessFen(fen: string) {
39
+ chess.setFen(fen);
40
+ }
41
+
42
+ function setChessboardFen(_animationTime?: number) {
43
+ chessboard.fen = chess.fen();
44
+ const side = chess.turn();
45
+ chessboard.draggable =
46
+ side === Color.WHITE ? Draggable.WHITE : Draggable.BLACK;
47
+ }
48
+
49
+ function onSelectNode() {
50
+ setChessFen(tree.currentNode.data.fen);
51
+ setChessboardFen();
52
+ }
53
+
54
+ function onDeleteVariant() {
55
+ setChessFen(tree.currentNode.data.fen);
56
+ setChessboardFen();
57
+ }
58
+
59
+ const canGoNext = $derived(mainLine.length >= 1 && mainLine.length % 2 === 1);
60
+
61
+ const solverForkAnnotations = $derived(
62
+ validatePuzzleSolverForkAnnotations(tree, initialTurn),
63
+ );
64
+
65
+ const canProceedToNextStep = $derived(canGoNext && solverForkAnnotations.ok);
66
+
67
+ /** Текст предупреждения по шагу для кнопки «!» (если пусто — предупреждений нет). */
68
+ const stepIssueMessage = $derived.by(() => {
69
+ if (!canGoNext && mainLine.length > 0) {
70
+ return `Последний ход должна сделать сторона, которая начинала. Добавьте ещё один ход за ${initialTurn === Color.WHITE ? "белых" : "чёрных"}.`;
71
+ }
72
+ if (canGoNext && !solverForkAnnotations.ok) {
73
+ return solverForkAnnotations.reason;
74
+ }
75
+ return null;
76
+ });
77
+
78
+ /** Высота блока с шахматной доской (px) для выравнивания панели TreeViewer по высоте доски на lg. */
79
+ let chessboardBlockHeight = $state(0);
80
+
81
+ function handleNext() {
82
+ if (canProceedToNextStep) {
83
+ onNext(mainLine);
84
+ }
85
+ }
86
+ </script>
87
+
88
+ <div class="flex flex-col gap-3 pt-2 pb-0">
89
+ <div class="flex flex-wrap items-center gap-2">
90
+ <h2 class="text-xl font-semibold">Шаг 2: Построение линии решения</h2>
91
+ <Popover.Root>
92
+ <Popover.Trigger type="button">
93
+ {#snippet child({ props })}
94
+ <button
95
+ {...props}
96
+ type="button"
97
+ class={cn(
98
+ buttonVariants({ variant: "outline", size: "icon-sm" }),
99
+ "size-7 shrink-0 rounded-full text-sm font-semibold",
100
+ )}
101
+ aria-label="Справка: построение линии решения"
102
+ >
103
+ ?
104
+ </button>
105
+ {/snippet}
106
+ </Popover.Trigger>
107
+ <Popover.Portal>
108
+ <Popover.Content
109
+ side="bottom"
110
+ align="start"
111
+ sideOffset={8}
112
+ class={cn(
113
+ "bg-popover text-popover-foreground border-border z-50 max-h-[min(70vh,32rem)] w-[min(calc(100vw-2rem),28rem)] overflow-y-auto rounded-lg border p-4 text-sm shadow-md outline-none",
114
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
115
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
116
+ )}
117
+ >
118
+ <p class="text-muted-foreground leading-relaxed">
119
+ Делайте ходы за обе стороны. Последний ход должна сделать сторона,
120
+ которая начинала игру (ключевой ход). В дереве можно выбирать ходы,
121
+ добавлять комментарии и символы оценки в режиме правки. Если от
122
+ позиции решателя есть несколько вариантов хода, у каждого обязательно
123
+ отметьте ✓ или ✗ в блоке «Задача (развилка)» под деревом (эти метки
124
+ только для ходов из развилки, не для единственного продолжения).
125
+ После ✓ главный вариант нужно довести до позиции, где последним
126
+ походил решатель. Если несколько ответов соперника ведут из одной
127
+ позиции, каждая такая линия должна заканчиваться ходом решателя (это не
128
+ требуется внутри продолжения после хода решателя с меткой ✗
129
+ «неверное решение»). Ниже по этой линии метки ✓ и ✗ не ставятся.
130
+ </p>
131
+ </Popover.Content>
132
+ </Popover.Portal>
133
+ </Popover.Root>
134
+ {#if stepIssueMessage}
135
+ <Popover.Root>
136
+ <Popover.Trigger type="button">
137
+ {#snippet child({ props })}
138
+ <button
139
+ {...props}
140
+ type="button"
141
+ class={cn(
142
+ buttonVariants({ variant: "outline", size: "icon-sm" }),
143
+ "size-7 shrink-0 rounded-full border-amber-600/60 text-sm font-semibold text-amber-600 dark:border-amber-400/60 dark:text-amber-400",
144
+ )}
145
+ aria-label="Предупреждение по условиям шага"
146
+ >
147
+ !
148
+ </button>
149
+ {/snippet}
150
+ </Popover.Trigger>
151
+ <Popover.Portal>
152
+ <Popover.Content
153
+ side="bottom"
154
+ align="start"
155
+ sideOffset={8}
156
+ class={cn(
157
+ "bg-popover text-popover-foreground border-border z-50 max-h-[min(70vh,32rem)] w-[min(calc(100vw-2rem),28rem)] overflow-y-auto rounded-lg border p-4 text-sm shadow-md outline-none",
158
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
159
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
160
+ )}
161
+ >
162
+ <p
163
+ class="leading-relaxed text-amber-700 dark:text-amber-400"
164
+ >
165
+ {stepIssueMessage}
166
+ </p>
167
+ </Popover.Content>
168
+ </Popover.Portal>
169
+ </Popover.Root>
170
+ {/if}
171
+ </div>
172
+
173
+ <div
174
+ class="relative flex min-h-0 flex-col justify-center lg:flex-row lg:items-start lg:gap-4"
175
+ >
176
+ <div class="flex w-full max-w-[38rem] shrink-0 flex-col gap-2">
177
+ <div class="w-full shrink-0" bind:clientHeight={chessboardBlockHeight}>
178
+ <Chessboard facade={chessboard} />
179
+ </div>
180
+ </div>
181
+
182
+ <div class="flex min-h-0 w-full flex-1 flex-col gap-2 lg:min-w-0">
183
+ <div
184
+ class={cn(
185
+ "flex min-w-0 flex-col overflow-hidden rounded-md border border-border",
186
+ chessboardBlockHeight > 0
187
+ ? "min-h-0 shrink-0"
188
+ : "min-h-[12rem] max-h-[min(65vh,36rem)]",
189
+ )}
190
+ style:height={chessboardBlockHeight > 0
191
+ ? `${chessboardBlockHeight}px`
192
+ : undefined}
193
+ >
194
+ <TreeViewer
195
+ chessTree={tree}
196
+ {onSelectNode}
197
+ {onDeleteVariant}
198
+ {setChessFen}
199
+ {setChessboardFen}
200
+ pieceSet={DEFAULT_PIECE_SET}
201
+ className="min-h-0 flex-1 border-x-0 border-t-0"
202
+ editMode={true}
203
+ />
204
+ </div>
205
+ </div>
206
+ </div>
207
+
208
+ <div class="flex justify-between">
209
+ <button
210
+ type="button"
211
+ class={cn(buttonVariants({ variant: "outline" }))}
212
+ onclick={onBack}
213
+ >
214
+ Назад
215
+ </button>
216
+ <button
217
+ type="button"
218
+ class={cn(buttonVariants({ variant: "default" }))}
219
+ onclick={handleNext}
220
+ disabled={!canProceedToNextStep}
221
+ >
222
+ Далее
223
+ </button>
224
+ </div>
225
+ </div>
@@ -0,0 +1,15 @@
1
+ import type { BoardApi } from "@connectorvol/chessboard";
2
+ import { ChessTree } from "@connectorvol/tree";
3
+ import type { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
4
+ import { type PuzzleData } from "../puzzle/puzzleData.js";
5
+ interface Props {
6
+ puzzleData: PuzzleData;
7
+ onBack: () => void;
8
+ onNext: (moves: string[]) => void;
9
+ chess: PgnOps;
10
+ tree: ChessTree;
11
+ chessboard: BoardApi;
12
+ }
13
+ declare const StepMoves: import("svelte").Component<Props, {}, "">;
14
+ type StepMoves = ReturnType<typeof StepMoves>;
15
+ export default StepMoves;