@connectorvol/chess-widgets 8.0.0 → 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.
Files changed (47) hide show
  1. package/dist/constants/default-board-appearance-settings.d.ts +9 -0
  2. package/dist/constants/default-board-appearance-settings.js +30 -0
  3. package/dist/constants/editable-board-settings.d.ts +3 -21
  4. package/dist/constants/editable-board-settings.js +24 -19
  5. package/dist/game-analyzer/GameAnalyzer.svelte +38 -19
  6. package/dist/game-analyzer/gameAnalyzer.svelte.js +9 -7
  7. package/dist/game-analyzer/types.d.ts +10 -2
  8. package/dist/index.d.ts +9 -1
  9. package/dist/index.js +6 -0
  10. package/dist/position-editor/EditPanel.svelte +9 -6
  11. package/dist/puzzle/puzzleCreatedPayload.d.ts +17 -0
  12. package/dist/puzzle/puzzleData.d.ts +4 -0
  13. package/dist/puzzle/puzzleData.js +10 -0
  14. package/dist/puzzle/puzzlePreviewConstants.d.ts +4 -0
  15. package/dist/puzzle/puzzlePreviewConstants.js +4 -0
  16. package/dist/puzzle/puzzlePreviewPathNags.d.ts +6 -0
  17. package/dist/puzzle/puzzlePreviewPathNags.js +35 -0
  18. package/dist/puzzle/puzzleSolverForkAnnotations.d.ts +2 -4
  19. package/dist/puzzle/puzzleSolverForkAnnotations.js +13 -22
  20. package/dist/puzzle/puzzleStepPreviewSolver.d.ts +13 -1
  21. package/dist/puzzle/puzzleStepPreviewSolver.js +69 -9
  22. package/dist/puzzle/syncPuzzleBranchNags.d.ts +14 -0
  23. package/dist/puzzle/syncPuzzleBranchNags.js +125 -0
  24. package/dist/puzzle-creation/OpeningTagHoverPreview.svelte +81 -0
  25. package/dist/puzzle-creation/OpeningTagHoverPreview.svelte.d.ts +11 -0
  26. package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte +104 -32
  27. package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte.d.ts +16 -2
  28. package/dist/puzzle-creation/PuzzleCreationWizard.svelte +192 -202
  29. package/dist/puzzle-creation/PuzzleCreationWizard.svelte.d.ts +47 -3
  30. package/dist/puzzle-creation/PuzzlePgnBoardTreeEditor.svelte +485 -76
  31. package/dist/puzzle-creation/PuzzleWizardTagsStep.svelte +36 -0
  32. package/dist/puzzle-creation/PuzzleWizardTagsStep.svelte.d.ts +12 -0
  33. package/dist/puzzle-creation/StepMoves.svelte +38 -18
  34. package/dist/puzzle-creation/StepMoves.svelte.d.ts +2 -1
  35. package/dist/puzzle-creation/StepPosition.svelte +15 -9
  36. package/dist/puzzle-creation/StepPreview.svelte +23 -10
  37. package/dist/puzzle-creation/StepPreview.svelte.d.ts +2 -0
  38. package/dist/puzzle-creation/StepTags.svelte +270 -0
  39. package/dist/puzzle-creation/StepTags.svelte.d.ts +19 -0
  40. package/dist/puzzle-creation/buildPuzzleWizardBoardSettings.d.ts +9 -0
  41. package/dist/puzzle-creation/buildPuzzleWizardBoardSettings.js +19 -0
  42. package/dist/puzzle-creation/createPuzzleLineEditingBoard.d.ts +10 -3
  43. package/dist/puzzle-creation/createPuzzleLineEditingBoard.js +15 -11
  44. package/dist/puzzle-creation/puzzleWizardState.d.ts +35 -0
  45. package/dist/puzzle-creation/puzzleWizardState.js +37 -0
  46. package/dist/puzzle-creation/types.d.ts +35 -5
  47. 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 { BoardApi } from "@connectorvol/chessboard";
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 { puzzleData, onBack, onNext, chess, tree, chessboard }: Props =
31
- $props();
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?: number) {
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
- ...DEFAULT_EDITABLE_BOARD_SETTINGS,
32
- ...(props.boardAppearanceSettings ?? {}),
33
- boardSize: 29,
34
- isResizable: false,
35
- editSettings: DEFAULT_EDITABLE_BOARD_SETTINGS.editSettings,
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
- settings: puzzleStepBoardSettings,
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,6 +27,8 @@
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`). */
@@ -150,16 +152,27 @@
150
152
  >
151
153
  Назад
152
154
  </button>
153
- {#if props.onPuzzleCreated && solved}
154
- <button
155
- type="button"
156
- class={cn(buttonVariants({ variant: "default" }))}
157
- onclick={() =>
158
- props.onPuzzleCreated?.(puzzlePartsFromFullPgn(props.solutionPgn))}
159
- disabled={!props.solutionPgn.trim()}
160
- >
161
- Готово
162
- </button>
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,6 +13,8 @@ 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`). */
@@ -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
- export declare function createPuzzleLineEditingBoardApi(fullFen: string, actions: IChessBoardActions, opts?: {
19
+ export declare function createPuzzleLineEditingBoardApi(fullFen: string, actions: IChessBoardActions, opts: {
14
20
  boardTheme?: ChessboardTheme;
15
21
  boardAppearanceSettings?: TChessboardAppearanceSettings;
16
- }): import("@connectorvol/chessboard").BoardApi;
22
+ }): ICreatePuzzleLineEditingBoardResult;
23
+ export {};