@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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
TPuzzleOpeningTagOption,
|
|
4
|
+
TPuzzleTagOption,
|
|
5
|
+
} from "@connectorvol/shared";
|
|
6
|
+
|
|
7
|
+
import StepTags from "./StepTags.svelte";
|
|
8
|
+
import { getWizardContext } from "@connectorvol/shared";
|
|
9
|
+
import type { TPuzzleWizardState } from "./puzzleWizardState.js";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Возвращает каталог дебютов для выбора одного тега. */
|
|
13
|
+
openingTags: readonly TPuzzleOpeningTagOption[];
|
|
14
|
+
/** Возвращает каталог тактических приёмов. */
|
|
15
|
+
tacticTags: readonly TPuzzleTagOption[];
|
|
16
|
+
/** Возвращает максимальное число выбираемых тактических тегов. */
|
|
17
|
+
tacticTagsMax: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { openingTags, tacticTags, tacticTagsMax }: Props = $props();
|
|
21
|
+
|
|
22
|
+
const w = getWizardContext<TPuzzleWizardState>();
|
|
23
|
+
let tags = $state(w.state.puzzleTags);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<StepTags
|
|
27
|
+
bind:tags
|
|
28
|
+
{openingTags}
|
|
29
|
+
{tacticTags}
|
|
30
|
+
{tacticTagsMax}
|
|
31
|
+
onBack={w.back}
|
|
32
|
+
onDone={(tags) => {
|
|
33
|
+
w.state.puzzleTags = tags;
|
|
34
|
+
w.done();
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TPuzzleOpeningTagOption, TPuzzleTagOption } from "@connectorvol/shared";
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Возвращает каталог дебютов для выбора одного тега. */
|
|
4
|
+
openingTags: readonly TPuzzleOpeningTagOption[];
|
|
5
|
+
/** Возвращает каталог тактических приёмов. */
|
|
6
|
+
tacticTags: readonly TPuzzleTagOption[];
|
|
7
|
+
/** Возвращает максимальное число выбираемых тактических тегов. */
|
|
8
|
+
tacticTagsMax: number;
|
|
9
|
+
}
|
|
10
|
+
declare const PuzzleWizardTagsStep: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type PuzzleWizardTagsStep = ReturnType<typeof PuzzleWizardTagsStep>;
|
|
12
|
+
export default PuzzleWizardTagsStep;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
BoardApi,
|
|
4
|
+
TChessBoardDesignSettings,
|
|
5
|
+
} from "@connectorvol/chessboard";
|
|
3
6
|
|
|
4
7
|
import { Draggable } from "@connectorvol/chessboard";
|
|
5
8
|
import type { ChessTree } from "@connectorvol/tree";
|
|
@@ -17,6 +20,8 @@
|
|
|
17
20
|
type PuzzleData,
|
|
18
21
|
} from "../puzzle/puzzleData.js";
|
|
19
22
|
import { validatePuzzleSolverForkAnnotations } from "../puzzle/puzzleSolverForkAnnotations.js";
|
|
23
|
+
import { syncPuzzleBranchNagsOnTree } from "../puzzle/syncPuzzleBranchNags.js";
|
|
24
|
+
import { untrack } from "svelte";
|
|
20
25
|
|
|
21
26
|
interface Props {
|
|
22
27
|
puzzleData: PuzzleData;
|
|
@@ -25,10 +30,18 @@
|
|
|
25
30
|
chess: PgnOps;
|
|
26
31
|
tree: ChessTree;
|
|
27
32
|
chessboard: BoardApi;
|
|
33
|
+
chessboardDesign: TChessBoardDesignSettings;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
const {
|
|
31
|
-
|
|
36
|
+
const {
|
|
37
|
+
puzzleData,
|
|
38
|
+
onBack,
|
|
39
|
+
onNext,
|
|
40
|
+
chess,
|
|
41
|
+
tree,
|
|
42
|
+
chessboard,
|
|
43
|
+
chessboardDesign,
|
|
44
|
+
}: Props = $props();
|
|
32
45
|
|
|
33
46
|
const initialTurn = $derived(
|
|
34
47
|
puzzleData.initialFen.trim().split(/\s+/)[1] === "b"
|
|
@@ -42,7 +55,7 @@
|
|
|
42
55
|
chess.setFen(fen);
|
|
43
56
|
}
|
|
44
57
|
|
|
45
|
-
function setChessboardFen(_animationTime
|
|
58
|
+
function setChessboardFen(_animationTime: number | undefined) {
|
|
46
59
|
chessboard.fen = chess.fen();
|
|
47
60
|
const side = chess.turn();
|
|
48
61
|
chessboard.draggable =
|
|
@@ -51,12 +64,12 @@
|
|
|
51
64
|
|
|
52
65
|
function onSelectNode() {
|
|
53
66
|
setChessFen(tree.currentNode.data.fen);
|
|
54
|
-
setChessboardFen();
|
|
67
|
+
setChessboardFen(0);
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
function onDeleteVariant() {
|
|
58
71
|
setChessFen(tree.currentNode.data.fen);
|
|
59
|
-
setChessboardFen();
|
|
72
|
+
setChessboardFen(0);
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
const canGoNext = $derived(mainLine.length >= 1 && mainLine.length % 2 === 1);
|
|
@@ -67,6 +80,14 @@
|
|
|
67
80
|
|
|
68
81
|
const canProceedToNextStep = $derived(canGoNext && solverForkAnnotations.ok);
|
|
69
82
|
|
|
83
|
+
/** Представляет автоматическую разметку ✓/✗ на развилках решателя после каждой правки дерева. */
|
|
84
|
+
$effect(() => {
|
|
85
|
+
void tree.mutationVersion;
|
|
86
|
+
untrack(() => {
|
|
87
|
+
syncPuzzleBranchNagsOnTree(tree, initialTurn);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
70
91
|
/** Текст предупреждения по шагу для кнопки «!» (если пусто — предупреждений нет). */
|
|
71
92
|
const stepIssueMessage = $derived.by(() => {
|
|
72
93
|
if (!canGoNext && mainLine.length > 0) {
|
|
@@ -118,15 +139,14 @@
|
|
|
118
139
|
<p class="text-muted-foreground leading-relaxed">
|
|
119
140
|
Делайте ходы за обе стороны. Последний ход должна сделать сторона,
|
|
120
141
|
которая начинала игру (ключевой ход). В дереве можно выбирать ходы,
|
|
121
|
-
добавлять комментарии и символы оценки в режиме правки.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
После ✓ главный вариант нужно довести
|
|
126
|
-
походил решатель. Если несколько ответов
|
|
127
|
-
позиции, каждая такая линия должна
|
|
128
|
-
|
|
129
|
-
«неверное решение»). Ниже по этой линии метки ✓ и ✗ не ставятся.
|
|
142
|
+
добавлять комментарии и символы оценки в режиме правки. Ходы главной
|
|
143
|
+
линии автоматически получают ✓ (верный ход), неверные ходы решателя
|
|
144
|
+
в вариациях — ✗; ходы соперника без этих меток. Если от позиции
|
|
145
|
+
решателя есть несколько вариантов хода, главный вариант должен быть
|
|
146
|
+
верным, остальные — неверными. После ✓ главный вариант нужно довести
|
|
147
|
+
до позиции, где последним походил решатель. Если несколько ответов
|
|
148
|
+
соперника ведут из одной позиции, каждая такая линия должна
|
|
149
|
+
заканчиваться ходом решателя.
|
|
130
150
|
</p>
|
|
131
151
|
</Popover.Content>
|
|
132
152
|
</Popover.Portal>
|
|
@@ -159,9 +179,7 @@
|
|
|
159
179
|
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
160
180
|
)}
|
|
161
181
|
>
|
|
162
|
-
<p
|
|
163
|
-
class="leading-relaxed text-amber-700 dark:text-amber-400"
|
|
164
|
-
>
|
|
182
|
+
<p class="leading-relaxed text-amber-700 dark:text-amber-400">
|
|
165
183
|
{stepIssueMessage}
|
|
166
184
|
</p>
|
|
167
185
|
</Popover.Content>
|
|
@@ -172,11 +190,13 @@
|
|
|
172
190
|
|
|
173
191
|
<PuzzleBoardTreeViewerPane
|
|
174
192
|
{chessboard}
|
|
193
|
+
{chessboardDesign}
|
|
175
194
|
chessTree={tree}
|
|
176
195
|
{onSelectNode}
|
|
177
196
|
{onDeleteVariant}
|
|
178
197
|
{setChessFen}
|
|
179
198
|
{setChessboardFen}
|
|
199
|
+
showPuzzleBranchNags={true}
|
|
180
200
|
/>
|
|
181
201
|
|
|
182
202
|
<div class="flex justify-between">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BoardApi } from "@connectorvol/chessboard";
|
|
1
|
+
import type { BoardApi, TChessBoardDesignSettings } from "@connectorvol/chessboard";
|
|
2
2
|
import type { ChessTree } from "@connectorvol/tree";
|
|
3
3
|
import type { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
|
|
4
4
|
import { type PuzzleData } from "../puzzle/puzzleData.js";
|
|
@@ -9,6 +9,7 @@ interface Props {
|
|
|
9
9
|
chess: PgnOps;
|
|
10
10
|
tree: ChessTree;
|
|
11
11
|
chessboard: BoardApi;
|
|
12
|
+
chessboardDesign: TChessBoardDesignSettings;
|
|
12
13
|
}
|
|
13
14
|
declare const StepMoves: import("svelte").Component<Props, {}, "">;
|
|
14
15
|
type StepMoves = ReturnType<typeof StepMoves>;
|
|
@@ -28,18 +28,24 @@
|
|
|
28
28
|
|
|
29
29
|
/** Настройки доски мастера задачи: фиксированный размер, без ручного resize. */
|
|
30
30
|
const puzzleStepBoardSettings = $derived({
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
play: {
|
|
32
|
+
...DEFAULT_EDITABLE_BOARD_SETTINGS.play,
|
|
33
|
+
...(props.boardAppearanceSettings?.play ?? {}),
|
|
34
|
+
boardSize: 29,
|
|
35
|
+
isResizable: false,
|
|
36
|
+
editSettings: DEFAULT_EDITABLE_BOARD_SETTINGS.play.editSettings,
|
|
37
|
+
},
|
|
38
|
+
design: {
|
|
39
|
+
...DEFAULT_EDITABLE_BOARD_SETTINGS.design,
|
|
40
|
+
...(props.boardAppearanceSettings?.design ?? {}),
|
|
41
|
+
theme: props.boardTheme ?? CHESSBOARD_THEMES.blue,
|
|
42
|
+
},
|
|
36
43
|
});
|
|
37
44
|
|
|
38
45
|
let chessboard = $derived(
|
|
39
46
|
createBoardApi({
|
|
40
47
|
fen: (() => props.initialFen)(),
|
|
41
|
-
|
|
42
|
-
theme: props.boardTheme ?? CHESSBOARD_THEMES.blue,
|
|
48
|
+
playSettings: puzzleStepBoardSettings.play,
|
|
43
49
|
}),
|
|
44
50
|
);
|
|
45
51
|
|
|
@@ -121,9 +127,9 @@
|
|
|
121
127
|
<!-- Явная ширина: при родителе с w-fit/min-content BoardContainer даёт min(100%, Nrem), и 100% может схлопнуться до нуля. -->
|
|
122
128
|
<div
|
|
123
129
|
class="shrink-0 max-w-full"
|
|
124
|
-
style="width: {puzzleStepBoardSettings.boardSize}rem; max-width: 100%;"
|
|
130
|
+
style="width: {puzzleStepBoardSettings.play.boardSize}rem; max-width: 100%;"
|
|
125
131
|
>
|
|
126
|
-
<Chessboard facade={chessboard} />
|
|
132
|
+
<Chessboard facade={chessboard} design={puzzleStepBoardSettings.design} />
|
|
127
133
|
</div>
|
|
128
134
|
<div
|
|
129
135
|
class="hidden min-w-0 flex-1 flex-col gap-3 md:flex md:min-w-[280px]"
|
|
@@ -27,10 +27,12 @@
|
|
|
27
27
|
* Возвращает колбэк сохранения задачи с FEN и строкой ходов (кнопка «Готово» показывается после решения превью).
|
|
28
28
|
*/
|
|
29
29
|
onPuzzleCreated?: (payload: TPuzzleCreatedPayload) => void;
|
|
30
|
+
/** Возвращает переход на шаг выбора тегов вместо немедленного завершения. */
|
|
31
|
+
onNext?: () => void;
|
|
30
32
|
/** Возвращает тему оформления шахматной доски (по умолчанию `CHESSBOARD_THEMES.blue`). */
|
|
31
33
|
boardTheme?: ChessboardTheme;
|
|
32
34
|
/** Возвращает дополнительные визуальные настройки шахматной доски (кроме `boardSize`, `orientation`, `draggable`). */
|
|
33
|
-
boardAppearanceSettings
|
|
35
|
+
boardAppearanceSettings?: TChessboardAppearanceSettings;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
const props: Props = $props();
|
|
@@ -150,16 +152,27 @@
|
|
|
150
152
|
>
|
|
151
153
|
Назад
|
|
152
154
|
</button>
|
|
153
|
-
{#if
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
props.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
155
|
+
{#if solved}
|
|
156
|
+
{#if props.onNext}
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
class={cn(buttonVariants({ variant: "default" }))}
|
|
160
|
+
onclick={props.onNext}
|
|
161
|
+
disabled={!props.solutionPgn.trim()}
|
|
162
|
+
>
|
|
163
|
+
Далее
|
|
164
|
+
</button>
|
|
165
|
+
{:else if props.onPuzzleCreated}
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
class={cn(buttonVariants({ variant: "default" }))}
|
|
169
|
+
onclick={() =>
|
|
170
|
+
props.onPuzzleCreated?.(puzzlePartsFromFullPgn(props.solutionPgn))}
|
|
171
|
+
disabled={!props.solutionPgn.trim()}
|
|
172
|
+
>
|
|
173
|
+
Готово
|
|
174
|
+
</button>
|
|
175
|
+
{/if}
|
|
163
176
|
{/if}
|
|
164
177
|
</div>
|
|
165
178
|
</div>
|
|
@@ -13,10 +13,12 @@ interface Props {
|
|
|
13
13
|
* Возвращает колбэк сохранения задачи с FEN и строкой ходов (кнопка «Готово» показывается после решения превью).
|
|
14
14
|
*/
|
|
15
15
|
onPuzzleCreated?: (payload: TPuzzleCreatedPayload) => void;
|
|
16
|
+
/** Возвращает переход на шаг выбора тегов вместо немедленного завершения. */
|
|
17
|
+
onNext?: () => void;
|
|
16
18
|
/** Возвращает тему оформления шахматной доски (по умолчанию `CHESSBOARD_THEMES.blue`). */
|
|
17
19
|
boardTheme?: ChessboardTheme;
|
|
18
20
|
/** Возвращает дополнительные визуальные настройки шахматной доски (кроме `boardSize`, `orientation`, `draggable`). */
|
|
19
|
-
boardAppearanceSettings
|
|
21
|
+
boardAppearanceSettings?: TChessboardAppearanceSettings;
|
|
20
22
|
}
|
|
21
23
|
declare const StepPreview: import("svelte").Component<Props, {}, "">;
|
|
22
24
|
type StepPreview = ReturnType<typeof StepPreview>;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Popover } from "bits-ui";
|
|
3
|
+
|
|
4
|
+
import { buttonVariants } from "../button-variants.js";
|
|
5
|
+
import { cn } from "../utils.js";
|
|
6
|
+
import type { TPuzzleTags } from "../puzzle/puzzleCreatedPayload.js";
|
|
7
|
+
import type {
|
|
8
|
+
TPuzzleOpeningTagOption,
|
|
9
|
+
TPuzzleTagOption,
|
|
10
|
+
} from "@connectorvol/shared";
|
|
11
|
+
import OpeningTagHoverPreview from "./OpeningTagHoverPreview.svelte";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** Возвращает текущие выбранные теги (двухсторонняя привязка). */
|
|
15
|
+
tags: TPuzzleTags;
|
|
16
|
+
/** Возвращает каталог дебютов для выбора одного тега. */
|
|
17
|
+
openingTags: readonly TPuzzleOpeningTagOption[];
|
|
18
|
+
/** Возвращает каталог тактических приёмов для выбора до `tacticTagsMax` тегов. */
|
|
19
|
+
tacticTags: readonly TPuzzleTagOption[];
|
|
20
|
+
/** Возвращает максимальное число выбираемых тактических тегов. */
|
|
21
|
+
tacticTagsMax: number;
|
|
22
|
+
/** Возвращает переход назад на шаг превью. */
|
|
23
|
+
onBack: () => void;
|
|
24
|
+
/** Возвращает завершение мастера с выбранными тегами. */
|
|
25
|
+
onDone: (tags: TPuzzleTags) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type TOpeningPreviewState = {
|
|
29
|
+
tag: TPuzzleOpeningTagOption;
|
|
30
|
+
anchor: DOMRect;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
tags = $bindable(),
|
|
35
|
+
openingTags,
|
|
36
|
+
tacticTags,
|
|
37
|
+
tacticTagsMax,
|
|
38
|
+
onBack,
|
|
39
|
+
onDone,
|
|
40
|
+
}: Props = $props();
|
|
41
|
+
|
|
42
|
+
let openingFilter = $state("");
|
|
43
|
+
let tacticFilter = $state("");
|
|
44
|
+
let hoveredOpening = $state<TOpeningPreviewState | null>(null);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Представляет показ превью дебюта при наведении на тег.
|
|
48
|
+
*/
|
|
49
|
+
function handleOpeningPointerEnter(
|
|
50
|
+
event: { currentTarget: EventTarget | null },
|
|
51
|
+
tag: TPuzzleOpeningTagOption,
|
|
52
|
+
) {
|
|
53
|
+
const anchor = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
54
|
+
hoveredOpening = { tag, anchor };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Представляет скрытие превью дебюта.
|
|
59
|
+
*/
|
|
60
|
+
function handleOpeningPointerLeave() {
|
|
61
|
+
hoveredOpening = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const filteredOpenings = $derived(
|
|
65
|
+
openingTags.filter((tag) =>
|
|
66
|
+
tag.label.toLowerCase().includes(openingFilter.trim().toLowerCase()),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const filteredTactics = $derived(
|
|
71
|
+
tacticTags.filter((tag) =>
|
|
72
|
+
tag.label.toLowerCase().includes(tacticFilter.trim().toLowerCase()),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const tacticsLimitReached = $derived(
|
|
77
|
+
tags.tactics.length >= tacticTagsMax,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Представляет переключение выбора дебютного тега (только один).
|
|
82
|
+
*/
|
|
83
|
+
function toggleOpening(id: string) {
|
|
84
|
+
tags = {
|
|
85
|
+
...tags,
|
|
86
|
+
opening: tags.opening === id ? undefined : id,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Представляет переключение тактического тега с ограничением по количеству.
|
|
92
|
+
*/
|
|
93
|
+
function toggleTactic(id: string) {
|
|
94
|
+
const selected = tags.tactics.includes(id);
|
|
95
|
+
if (selected) {
|
|
96
|
+
tags = {
|
|
97
|
+
...tags,
|
|
98
|
+
tactics: tags.tactics.filter((t) => t !== id),
|
|
99
|
+
};
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (tags.tactics.length >= tacticTagsMax) return;
|
|
103
|
+
tags = {
|
|
104
|
+
...tags,
|
|
105
|
+
tactics: [...tags.tactics, id],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
{#if hoveredOpening}
|
|
111
|
+
{#key hoveredOpening.tag.id}
|
|
112
|
+
<OpeningTagHoverPreview
|
|
113
|
+
fen={hoveredOpening.tag.fen}
|
|
114
|
+
label={hoveredOpening.tag.label}
|
|
115
|
+
anchor={hoveredOpening.anchor}
|
|
116
|
+
/>
|
|
117
|
+
{/key}
|
|
118
|
+
{/if}
|
|
119
|
+
|
|
120
|
+
<div class="flex flex-col gap-4 pt-2 pb-0">
|
|
121
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
122
|
+
<h2 class="text-xl font-semibold">Шаг 4: Теги задачи</h2>
|
|
123
|
+
<Popover.Root>
|
|
124
|
+
<Popover.Trigger type="button">
|
|
125
|
+
{#snippet child({ props })}
|
|
126
|
+
<button
|
|
127
|
+
{...props}
|
|
128
|
+
type="button"
|
|
129
|
+
class={cn(
|
|
130
|
+
buttonVariants({ variant: "outline", size: "icon-sm" }),
|
|
131
|
+
"size-7 shrink-0 rounded-full text-sm font-semibold",
|
|
132
|
+
)}
|
|
133
|
+
aria-label="Справка: теги задачи"
|
|
134
|
+
>
|
|
135
|
+
?
|
|
136
|
+
</button>
|
|
137
|
+
{/snippet}
|
|
138
|
+
</Popover.Trigger>
|
|
139
|
+
<Popover.Portal>
|
|
140
|
+
<Popover.Content
|
|
141
|
+
side="bottom"
|
|
142
|
+
align="start"
|
|
143
|
+
sideOffset={8}
|
|
144
|
+
class={cn(
|
|
145
|
+
"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",
|
|
146
|
+
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
147
|
+
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
<div class="space-y-3 text-muted-foreground leading-relaxed">
|
|
151
|
+
<p>
|
|
152
|
+
Теги помогают классифицировать задачу. Шаг опционален: можно
|
|
153
|
+
завершить мастер без выбора тегов.
|
|
154
|
+
</p>
|
|
155
|
+
<p>
|
|
156
|
+
Дебют — один тег из списка популярных дебютов. Приёмы — до трёх
|
|
157
|
+
тактических мотивов из каталога.
|
|
158
|
+
</p>
|
|
159
|
+
</div>
|
|
160
|
+
</Popover.Content>
|
|
161
|
+
</Popover.Portal>
|
|
162
|
+
</Popover.Root>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<section class="space-y-3">
|
|
166
|
+
<div class="flex flex-wrap items-end justify-between gap-2">
|
|
167
|
+
<div>
|
|
168
|
+
<h3 class="text-base font-medium">Дебют</h3>
|
|
169
|
+
<p class="text-muted-foreground text-sm">Можно выбрать не более одного</p>
|
|
170
|
+
</div>
|
|
171
|
+
<input
|
|
172
|
+
type="search"
|
|
173
|
+
class="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:ring-ring h-9 w-full max-w-xs rounded-md border px-3 text-sm outline-none focus-visible:ring-2"
|
|
174
|
+
placeholder="Поиск дебюта…"
|
|
175
|
+
bind:value={openingFilter}
|
|
176
|
+
aria-label="Поиск дебюта"
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
<div
|
|
180
|
+
class="border-border flex max-h-48 flex-wrap gap-2 overflow-y-auto rounded-lg border p-3"
|
|
181
|
+
role="radiogroup"
|
|
182
|
+
aria-label="Дебют"
|
|
183
|
+
>
|
|
184
|
+
{#each filteredOpenings as tag (tag.id)}
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
role="radio"
|
|
188
|
+
aria-checked={tags.opening === tag.id}
|
|
189
|
+
class={cn(
|
|
190
|
+
"rounded-full border px-3 py-1.5 text-sm transition-colors",
|
|
191
|
+
tags.opening === tag.id
|
|
192
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
193
|
+
: "border-border bg-background hover:bg-muted",
|
|
194
|
+
)}
|
|
195
|
+
onpointerenter={(event) => handleOpeningPointerEnter(event, tag)}
|
|
196
|
+
onpointerleave={handleOpeningPointerLeave}
|
|
197
|
+
onfocus={(event) => handleOpeningPointerEnter(event, tag)}
|
|
198
|
+
onblur={handleOpeningPointerLeave}
|
|
199
|
+
onclick={() => toggleOpening(tag.id)}
|
|
200
|
+
>
|
|
201
|
+
{tag.label}
|
|
202
|
+
</button>
|
|
203
|
+
{:else}
|
|
204
|
+
<p class="text-muted-foreground text-sm">Ничего не найдено</p>
|
|
205
|
+
{/each}
|
|
206
|
+
</div>
|
|
207
|
+
</section>
|
|
208
|
+
|
|
209
|
+
<section class="space-y-3">
|
|
210
|
+
<div class="flex flex-wrap items-end justify-between gap-2">
|
|
211
|
+
<div>
|
|
212
|
+
<h3 class="text-base font-medium">Приёмы</h3>
|
|
213
|
+
<p class="text-muted-foreground text-sm">
|
|
214
|
+
Выбрано {tags.tactics.length} из {tacticTagsMax}
|
|
215
|
+
</p>
|
|
216
|
+
</div>
|
|
217
|
+
<input
|
|
218
|
+
type="search"
|
|
219
|
+
class="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:ring-ring h-9 w-full max-w-xs rounded-md border px-3 text-sm outline-none focus-visible:ring-2"
|
|
220
|
+
placeholder="Поиск приёма…"
|
|
221
|
+
bind:value={tacticFilter}
|
|
222
|
+
aria-label="Поиск приёма"
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
<div
|
|
226
|
+
class="border-border flex max-h-56 flex-wrap gap-2 overflow-y-auto rounded-lg border p-3"
|
|
227
|
+
role="group"
|
|
228
|
+
aria-label="Тактические приёмы"
|
|
229
|
+
>
|
|
230
|
+
{#each filteredTactics as tag (tag.id)}
|
|
231
|
+
{@const selected = tags.tactics.includes(tag.id)}
|
|
232
|
+
{@const disabled = !selected && tacticsLimitReached}
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
aria-pressed={selected}
|
|
236
|
+
{disabled}
|
|
237
|
+
class={cn(
|
|
238
|
+
"rounded-full border px-3 py-1.5 text-sm transition-colors",
|
|
239
|
+
selected
|
|
240
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
241
|
+
: "border-border bg-background hover:bg-muted",
|
|
242
|
+
disabled && "cursor-not-allowed opacity-50 hover:bg-background",
|
|
243
|
+
)}
|
|
244
|
+
onclick={() => toggleTactic(tag.id)}
|
|
245
|
+
>
|
|
246
|
+
{tag.label}
|
|
247
|
+
</button>
|
|
248
|
+
{:else}
|
|
249
|
+
<p class="text-muted-foreground text-sm">Ничего не найдено</p>
|
|
250
|
+
{/each}
|
|
251
|
+
</div>
|
|
252
|
+
</section>
|
|
253
|
+
|
|
254
|
+
<div class="flex justify-between gap-2">
|
|
255
|
+
<button
|
|
256
|
+
type="button"
|
|
257
|
+
class={cn(buttonVariants({ variant: "outline" }))}
|
|
258
|
+
onclick={onBack}
|
|
259
|
+
>
|
|
260
|
+
Назад
|
|
261
|
+
</button>
|
|
262
|
+
<button
|
|
263
|
+
type="button"
|
|
264
|
+
class={cn(buttonVariants({ variant: "default" }))}
|
|
265
|
+
onclick={() => onDone(tags)}
|
|
266
|
+
>
|
|
267
|
+
Готово
|
|
268
|
+
</button>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TPuzzleTags } from "../puzzle/puzzleCreatedPayload.js";
|
|
2
|
+
import type { TPuzzleOpeningTagOption, TPuzzleTagOption } from "@connectorvol/shared";
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Возвращает текущие выбранные теги (двухсторонняя привязка). */
|
|
5
|
+
tags: TPuzzleTags;
|
|
6
|
+
/** Возвращает каталог дебютов для выбора одного тега. */
|
|
7
|
+
openingTags: readonly TPuzzleOpeningTagOption[];
|
|
8
|
+
/** Возвращает каталог тактических приёмов для выбора до `tacticTagsMax` тегов. */
|
|
9
|
+
tacticTags: readonly TPuzzleTagOption[];
|
|
10
|
+
/** Возвращает максимальное число выбираемых тактических тегов. */
|
|
11
|
+
tacticTagsMax: number;
|
|
12
|
+
/** Возвращает переход назад на шаг превью. */
|
|
13
|
+
onBack: () => void;
|
|
14
|
+
/** Возвращает завершение мастера с выбранными тегами. */
|
|
15
|
+
onDone: (tags: TPuzzleTags) => void;
|
|
16
|
+
}
|
|
17
|
+
declare const StepTags: import("svelte").Component<Props, {}, "tags">;
|
|
18
|
+
type StepTags = ReturnType<typeof StepTags>;
|
|
19
|
+
export default StepTags;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TChessBoardDesignSettings, TChessBoardPlaySettings } from "@connectorvol/chessboard";
|
|
2
|
+
import type { TChessboardAppearanceSettings } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Представляет визуальные настройки доски для шагов мастера задачи (построение линии и превью).
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildPuzzleWizardBoardSettings(boardAppearanceSettings?: TChessboardAppearanceSettings): {
|
|
7
|
+
play: TChessBoardPlaySettings;
|
|
8
|
+
design: TChessBoardDesignSettings;
|
|
9
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DEFAULT_BOARD_APPEARANCE_SETTINGS } from "../constants/default-board-appearance-settings.js";
|
|
2
|
+
import { DEFAULT_EDITABLE_BOARD_SETTINGS } from "../constants/editable-board-settings.js";
|
|
3
|
+
/**
|
|
4
|
+
* Представляет визуальные настройки доски для шагов мастера задачи (построение линии и превью).
|
|
5
|
+
*/
|
|
6
|
+
export function buildPuzzleWizardBoardSettings(boardAppearanceSettings) {
|
|
7
|
+
return {
|
|
8
|
+
play: {
|
|
9
|
+
...DEFAULT_BOARD_APPEARANCE_SETTINGS.play,
|
|
10
|
+
...boardAppearanceSettings?.play,
|
|
11
|
+
boardSize: DEFAULT_EDITABLE_BOARD_SETTINGS.play.boardSize,
|
|
12
|
+
isResizable: false,
|
|
13
|
+
},
|
|
14
|
+
design: {
|
|
15
|
+
...DEFAULT_BOARD_APPEARANCE_SETTINGS.design,
|
|
16
|
+
...boardAppearanceSettings?.design,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChessboardTheme } from "@connectorvol/chessboard";
|
|
1
|
+
import type { BoardApi, ChessboardTheme, TChessBoardDesignSettings } from "@connectorvol/chessboard";
|
|
2
2
|
import { type IChessBoardActions } from "@connectorvol/chessboard";
|
|
3
3
|
import { Color } from "@connectorvol/shared";
|
|
4
4
|
import type { TChessboardAppearanceSettings } from "./types.js";
|
|
@@ -6,11 +6,18 @@ import type { TChessboardAppearanceSettings } from "./types.js";
|
|
|
6
6
|
* Представляет определение стороны хода по полному FEN.
|
|
7
7
|
*/
|
|
8
8
|
export declare function sideToMoveFromFullFen(fullFen: string): Color;
|
|
9
|
+
interface ICreatePuzzleLineEditingBoardResult {
|
|
10
|
+
api: BoardApi;
|
|
11
|
+
design: TChessBoardDesignSettings;
|
|
12
|
+
}
|
|
9
13
|
/**
|
|
10
14
|
* Представляет создание API доски для интерактивного построения линии (как в мастере задач):
|
|
11
15
|
* ориентация и перетаскивание по стороне хода, фиксированный размер доски.
|
|
16
|
+
* Возвращает пару `{ api, design }` — `design` следует передать в `<Chessboard design={...} />`,
|
|
17
|
+
* чтобы доска разделяла снимок с превью/настройками через контекст.
|
|
12
18
|
*/
|
|
13
19
|
export declare function createPuzzleLineEditingBoardApi(fullFen: string, actions: IChessBoardActions, opts: {
|
|
14
20
|
boardTheme?: ChessboardTheme;
|
|
15
|
-
boardAppearanceSettings
|
|
16
|
-
}):
|
|
21
|
+
boardAppearanceSettings?: TChessboardAppearanceSettings;
|
|
22
|
+
}): ICreatePuzzleLineEditingBoardResult;
|
|
23
|
+
export {};
|