@connectorvol/tree 2.1.1 → 4.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/(classes)/chessTree.svelte.d.ts +83 -0
- package/dist/(classes)/chessTree.svelte.js +198 -0
- package/dist/(components)/DrillBreadcrumbs.svelte +135 -0
- package/dist/(components)/DrillBreadcrumbs.svelte.d.ts +20 -0
- package/dist/(components)/DrillForkSanLabel.svelte +26 -0
- package/dist/(components)/DrillForkSanLabel.svelte.d.ts +11 -0
- package/dist/(components)/DrillVariationList.svelte +65 -0
- package/dist/(components)/DrillVariationList.svelte.d.ts +15 -0
- package/dist/(components)/Move.svelte +247 -171
- package/dist/(components)/Move.svelte.d.ts +11 -5
- package/dist/(components)/MoveComment.svelte +32 -0
- package/dist/(components)/MoveComment.svelte.d.ts +11 -0
- package/dist/(components)/MoveContextMenu.svelte +73 -0
- package/dist/(components)/MoveContextMenu.svelte.d.ts +17 -0
- package/dist/(components)/MoveSanWithMenu.svelte +158 -0
- package/dist/(components)/MoveSanWithMenu.svelte.d.ts +42 -0
- package/dist/(components)/NagBadges.svelte +37 -0
- package/dist/(components)/NagBadges.svelte.d.ts +9 -0
- package/dist/(components)/TreeViewer.svelte +843 -42
- package/dist/(components)/TreeViewer.svelte.d.ts +29 -3
- package/dist/(components)/TreeViewerPanelManager.svelte +18 -155
- package/dist/(components)/VariantDropdownNavigator.svelte +173 -0
- package/dist/(components)/VariantDropdownNavigator.svelte.d.ts +16 -0
- package/dist/(components)/VariationGroup.svelte +100 -0
- package/dist/(components)/VariationGroup.svelte.d.ts +21 -0
- package/dist/(constants)/png.d.ts +1 -1
- package/dist/(constants)/png.js +2 -4
- package/dist/(models)/nagCatalog.d.ts +36 -0
- package/dist/(models)/nagCatalog.js +97 -0
- package/dist/(models)/pgnNodeCustomData.d.ts +3 -0
- package/dist/(utils)/context.d.ts +10 -3
- package/dist/(utils)/context.js +3 -14
- package/dist/(utils)/createPreviewHover.d.ts +19 -0
- package/dist/(utils)/createPreviewHover.js +37 -0
- package/dist/(utils)/nagBadgeStyle.d.ts +1 -0
- package/dist/(utils)/nagBadgeStyle.js +1 -0
- package/dist/(utils)/scrollToActiveMove.js +6 -5
- package/dist/(utils)/transformNag.d.ts +1 -1
- package/dist/(utils)/transformNag.js +1 -34
- package/dist/(utils)/transformPgnToChessNode.js +13 -0
- package/dist/(utils)/treeViewerPanelNavigation.svelte.d.ts +39 -0
- package/dist/(utils)/treeViewerPanelNavigation.svelte.js +257 -0
- package/dist/components/ui/button/button-variants.d.ts +65 -0
- package/dist/components/ui/button/button-variants.js +28 -0
- package/dist/components/ui/button/button.svelte +3 -43
- package/dist/components/ui/button/button.svelte.d.ts +1 -61
- package/dist/components/ui/button/index.d.ts +3 -2
- package/dist/components/ui/button/index.js +3 -4
- package/dist/components/ui/button-group/button-group-separator.svelte.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +4 -4
|
@@ -1,17 +1,43 @@
|
|
|
1
1
|
import type { ClassValue } from "svelte/elements";
|
|
2
2
|
import { ChessTree } from "../(classes)/chessTree.svelte.js";
|
|
3
|
-
import type {
|
|
4
|
-
type
|
|
3
|
+
import type { ChessTreeNode } from "../(models)/chessTreeNode.js";
|
|
4
|
+
import type { Move as TSharedMove, PieceSet } from "@connectorvol/shared";
|
|
5
|
+
type TPreviewOnHoverChessNodeSettings = {
|
|
6
|
+
/** Возвращает функцию для установки FEN превью при наведении (null скрывает); второй аргумент — подсветка последнего хода. */
|
|
7
|
+
setPreviewFen?: (fen: string | null, lastMove?: TSharedMove | null) => void;
|
|
8
|
+
/** Возвращает задержку (мс) до показа превью-позиции при наведении на ноду, либо `off` для отключения. */
|
|
9
|
+
delayMs?: number | "off";
|
|
10
|
+
};
|
|
11
|
+
type TTreeViewerProps = {
|
|
12
|
+
/** Возвращает дерево партии. */
|
|
5
13
|
chessTree: ChessTree;
|
|
14
|
+
/** Возвращает дополнительные классы корневого контейнера. */
|
|
6
15
|
className?: ClassValue;
|
|
16
|
+
/** Возвращает колбэк после выбора узла в дереве. */
|
|
7
17
|
onSelectNode: () => void;
|
|
18
|
+
/** Возвращает колбэк после удаления варианта. */
|
|
8
19
|
onDeleteVariant: () => void;
|
|
20
|
+
/** Возвращает колбэк после выбора узла (после `onSelectNode`), передаётся актуальный `currentNode`. */
|
|
21
|
+
onChessNodeSelected?: (node: ChessTreeNode) => void;
|
|
22
|
+
/** Возвращает функцию установки FEN основной партии. */
|
|
9
23
|
setChessFen: (fen: string) => void;
|
|
24
|
+
/** Возвращает функцию обновления доски. */
|
|
10
25
|
setChessboardFen: (animationTime?: number) => void;
|
|
26
|
+
/** Возвращает набор фигур для иконок SAN. */
|
|
11
27
|
pieceSet: PieceSet;
|
|
12
28
|
/** Возвращает true, если клик по ходу переключает текущий узел и доску. По умолчанию true. */
|
|
13
29
|
selectable?: boolean;
|
|
30
|
+
/** Возвращает настройки превью-доски при наведении на ноду. */
|
|
31
|
+
previewOnHoverChessNodeSettings?: TPreviewOnHoverChessNodeSettings;
|
|
32
|
+
/** Возвращает функцию для установки FEN превью при наведении (null скрывает); второй аргумент — подсветка последнего хода. */
|
|
33
|
+
setPreviewFen?: (fen: string | null, lastMove?: TSharedMove | null) => void;
|
|
34
|
+
/** Возвращает задержку (мс) до показа превью-позиции при наведении на ноду, либо `off` для отключения. */
|
|
35
|
+
previewHoverDelayMs?: number | "off";
|
|
36
|
+
/** Возвращает true, если под деревом показываются комментарий и выбор NAG для текущей ноды. */
|
|
37
|
+
editMode?: boolean;
|
|
38
|
+
/** Возвращает функцию для установки признака несохранённости дерева. */
|
|
39
|
+
onChangeDirty?: (setIsDirty: (value: boolean) => void) => void;
|
|
14
40
|
};
|
|
15
|
-
declare const TreeViewer: import("svelte").Component<
|
|
41
|
+
declare const TreeViewer: import("svelte").Component<TTreeViewerProps, {}, "">;
|
|
16
42
|
type TreeViewer = ReturnType<typeof TreeViewer>;
|
|
17
43
|
export default TreeViewer;
|
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
import * as Button from "../components/ui/button/index.js";
|
|
9
9
|
import * as ButtonGroup from "../components/ui/button-group/index.js";
|
|
10
10
|
|
|
11
|
-
import { scrollToActiveMoveIfNeeded } from "../(utils)/scrollToActiveMove.js";
|
|
12
|
-
import { scrollToStart } from "../(utils)/scrollToStart.js";
|
|
13
11
|
import type { Snippet } from "svelte";
|
|
14
12
|
import type { ChessTree } from "../(classes)/chessTree.svelte.js";
|
|
15
13
|
import type { ClassValue } from "clsx";
|
|
14
|
+
import { getOrCreateTreeViewerPanelNavigation } from "../(utils)/treeViewerPanelNavigation.svelte.js";
|
|
16
15
|
|
|
17
16
|
type Props = {
|
|
18
17
|
chessTree: ChessTree;
|
|
@@ -30,146 +29,12 @@
|
|
|
30
29
|
className,
|
|
31
30
|
}: Props = $props();
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
next: false,
|
|
37
|
-
start: false,
|
|
38
|
-
end: false,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Состояние автопроигрывания
|
|
42
|
-
let isPlaying = $state(false);
|
|
43
|
-
let playInterval: ReturnType<typeof setInterval> | null = $state(null);
|
|
44
|
-
|
|
45
|
-
const selectPreviousNode = () => {
|
|
46
|
-
const parentNode = chessTree.getCurrentNodeParent();
|
|
47
|
-
if (parentNode) {
|
|
48
|
-
chessTree.currentNode = parentNode;
|
|
49
|
-
setChessFen(parentNode.data.fen);
|
|
50
|
-
}
|
|
51
|
-
setChessboardFen();
|
|
52
|
-
setTimeout(() => scrollToActiveMoveIfNeeded(), 50);
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const selectNextNode = (animationTime: number = 0) => {
|
|
56
|
-
const childId = chessTree.currentNode?.children?.[0]?.id;
|
|
57
|
-
if (childId) {
|
|
58
|
-
const childNode = chessTree.currentNode.children[0];
|
|
59
|
-
chessTree.currentNode = childNode;
|
|
60
|
-
setChessFen(childNode.data.fen);
|
|
61
|
-
}
|
|
62
|
-
setChessboardFen(animationTime);
|
|
63
|
-
setTimeout(() => scrollToActiveMoveIfNeeded(), 50);
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Представляет функцию для перехода к началу главной линии (корневому узлу)
|
|
68
|
-
*/
|
|
69
|
-
const stepToStart = () => {
|
|
70
|
-
chessTree.currentNode = chessTree.rootNode.moves;
|
|
71
|
-
setChessFen(chessTree.rootNode.moves.data.fen);
|
|
72
|
-
setChessboardFen();
|
|
73
|
-
highlightButton("start");
|
|
74
|
-
setTimeout(() => scrollToStart(), 50);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Представляет функцию для перехода к концу главной линии
|
|
79
|
-
* Идет по первым дочерним узлам до тех пор, пока не достигнет листа
|
|
80
|
-
*/
|
|
81
|
-
const stepToEnd = () => {
|
|
82
|
-
let currentNode = chessTree.currentNode;
|
|
83
|
-
|
|
84
|
-
// Идем по первым дочерним узлам (главная линия) до конца
|
|
85
|
-
while (currentNode.children && currentNode.children.length > 0) {
|
|
86
|
-
currentNode = currentNode.children[0];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
chessTree.currentNode = currentNode;
|
|
90
|
-
setChessFen(currentNode.data.fen);
|
|
91
|
-
setChessboardFen();
|
|
92
|
-
highlightButton("end");
|
|
93
|
-
setTimeout(() => scrollToActiveMoveIfNeeded(), 50);
|
|
94
|
-
};
|
|
32
|
+
const panelNav = $derived.by(() =>
|
|
33
|
+
getOrCreateTreeViewerPanelNavigation(chessTree),
|
|
34
|
+
);
|
|
95
35
|
|
|
96
|
-
// Функция для подсветки кнопки на 1 секунду
|
|
97
|
-
const highlightButton = (button: "previous" | "next" | "start" | "end") => {
|
|
98
|
-
highlightedButtons[button] = true;
|
|
99
|
-
setTimeout(() => {
|
|
100
|
-
highlightedButtons[button] = false;
|
|
101
|
-
}, 100);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Представляет функцию для запуска автопроигрывания ходов
|
|
106
|
-
*/
|
|
107
|
-
const startAutoPlay = () => {
|
|
108
|
-
if (isPlaying || !chessTree.currentNode?.children?.length) return;
|
|
109
|
-
|
|
110
|
-
isPlaying = true;
|
|
111
|
-
playInterval = setInterval(() => {
|
|
112
|
-
// Проверяем, есть ли следующий ход
|
|
113
|
-
const hasNextNode = chessTree.currentNode?.children?.length > 0;
|
|
114
|
-
if (hasNextNode) {
|
|
115
|
-
selectNextNode(1000);
|
|
116
|
-
} else {
|
|
117
|
-
// Если достигли конца партии, останавливаем автопроигрывание
|
|
118
|
-
stopAutoPlay();
|
|
119
|
-
}
|
|
120
|
-
}, 1000);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Представляет функцию для остановки автопроигрывания ходов
|
|
125
|
-
*/
|
|
126
|
-
const stopAutoPlay = () => {
|
|
127
|
-
isPlaying = false;
|
|
128
|
-
if (playInterval !== null) {
|
|
129
|
-
clearInterval(playInterval);
|
|
130
|
-
playInterval = null;
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Представляет функцию-переключатель для автопроигрывания
|
|
136
|
-
*/
|
|
137
|
-
const toggleAutoPlay = () => {
|
|
138
|
-
if (isPlaying) {
|
|
139
|
-
stopAutoPlay();
|
|
140
|
-
} else {
|
|
141
|
-
startAutoPlay();
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const onKeyDown = (e: KeyboardEvent) => {
|
|
146
|
-
if (e.key === "ArrowUp") {
|
|
147
|
-
stepToStart();
|
|
148
|
-
e.preventDefault();
|
|
149
|
-
} else if (e.key === "ArrowDown") {
|
|
150
|
-
stepToEnd();
|
|
151
|
-
e.preventDefault();
|
|
152
|
-
} else if (e.key === "ArrowLeft") {
|
|
153
|
-
selectPreviousNode();
|
|
154
|
-
highlightButton("previous");
|
|
155
|
-
e.preventDefault();
|
|
156
|
-
} else if (e.key === "ArrowRight") {
|
|
157
|
-
selectNextNode();
|
|
158
|
-
highlightButton("next");
|
|
159
|
-
e.preventDefault();
|
|
160
|
-
} else if (e.key === " ") {
|
|
161
|
-
toggleAutoPlay();
|
|
162
|
-
e.preventDefault();
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
// Очистка интервала при размонтировании компонента
|
|
167
36
|
$effect(() => {
|
|
168
|
-
|
|
169
|
-
if (playInterval !== null) {
|
|
170
|
-
clearInterval(playInterval);
|
|
171
|
-
}
|
|
172
|
-
};
|
|
37
|
+
panelNav.bindCallbacks(setChessFen, setChessboardFen);
|
|
173
38
|
});
|
|
174
39
|
</script>
|
|
175
40
|
|
|
@@ -181,9 +46,9 @@
|
|
|
181
46
|
variant="outline"
|
|
182
47
|
size="icon"
|
|
183
48
|
aria-label="Previous Move"
|
|
184
|
-
onclick={selectPreviousNode}
|
|
49
|
+
onclick={() => panelNav.selectPreviousNode()}
|
|
185
50
|
class={{
|
|
186
|
-
"bg-primary text-white": highlightedButtons.previous,
|
|
51
|
+
"bg-primary text-white": panelNav.highlightedButtons.previous,
|
|
187
52
|
"cursor-pointer": true,
|
|
188
53
|
}}
|
|
189
54
|
>
|
|
@@ -193,23 +58,23 @@
|
|
|
193
58
|
variant="outline"
|
|
194
59
|
size="icon"
|
|
195
60
|
aria-label="Go to Start"
|
|
196
|
-
onclick={stepToStart}
|
|
61
|
+
onclick={() => panelNav.stepToStart()}
|
|
197
62
|
class={{
|
|
198
|
-
"bg-primary text-white": highlightedButtons.start,
|
|
63
|
+
"bg-primary text-white": panelNav.highlightedButtons.start,
|
|
199
64
|
}}
|
|
200
65
|
>
|
|
201
66
|
<ListStart />
|
|
202
67
|
</Button.Root>
|
|
203
68
|
<Button.Root
|
|
204
|
-
variant={isPlaying ? "default" : "outline"}
|
|
69
|
+
variant={panelNav.isPlaying ? "default" : "outline"}
|
|
205
70
|
size="icon"
|
|
206
|
-
aria-label={isPlaying ? "Stop Auto Play" : "Start Auto Play"}
|
|
207
|
-
onclick={toggleAutoPlay}
|
|
71
|
+
aria-label={panelNav.isPlaying ? "Stop Auto Play" : "Start Auto Play"}
|
|
72
|
+
onclick={() => panelNav.toggleAutoPlay()}
|
|
208
73
|
class={{
|
|
209
|
-
"bg-primary text-white": isPlaying,
|
|
74
|
+
"bg-primary text-white": panelNav.isPlaying,
|
|
210
75
|
}}
|
|
211
76
|
>
|
|
212
|
-
{#if isPlaying}
|
|
77
|
+
{#if panelNav.isPlaying}
|
|
213
78
|
<StopButtonIcon />
|
|
214
79
|
{:else}
|
|
215
80
|
<PlayButtonIcon />
|
|
@@ -219,9 +84,9 @@
|
|
|
219
84
|
variant="outline"
|
|
220
85
|
size="icon"
|
|
221
86
|
aria-label="Go to End"
|
|
222
|
-
onclick={stepToEnd}
|
|
87
|
+
onclick={() => panelNav.stepToEnd()}
|
|
223
88
|
class={{
|
|
224
|
-
"bg-primary text-white": highlightedButtons.end,
|
|
89
|
+
"bg-primary text-white": panelNav.highlightedButtons.end,
|
|
225
90
|
}}
|
|
226
91
|
>
|
|
227
92
|
<ListEnd />
|
|
@@ -230,9 +95,9 @@
|
|
|
230
95
|
variant="outline"
|
|
231
96
|
size="icon"
|
|
232
97
|
aria-label="Next Move"
|
|
233
|
-
onclick={() => selectNextNode()}
|
|
98
|
+
onclick={() => panelNav.selectNextNode()}
|
|
234
99
|
class={{
|
|
235
|
-
"bg-primary text-white": highlightedButtons.next,
|
|
100
|
+
"bg-primary text-white": panelNav.highlightedButtons.next,
|
|
236
101
|
}}
|
|
237
102
|
>
|
|
238
103
|
<ArrowRight />
|
|
@@ -242,5 +107,3 @@
|
|
|
242
107
|
</ButtonGroup.Root>
|
|
243
108
|
{@render extraActions?.()}
|
|
244
109
|
</div>
|
|
245
|
-
|
|
246
|
-
<svelte:window onkeydown={onKeyDown} />
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ChessTreeNode } from "../(models)/chessTreeNode.js";
|
|
3
|
+
import NagBadges from "./NagBadges.svelte";
|
|
4
|
+
type Props = {
|
|
5
|
+
variants: ChessTreeNode[];
|
|
6
|
+
onActivateVariant: (node: ChessTreeNode) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Представляет CSS-селектор элемента, относительно которого нужно позиционировать dropdown.
|
|
9
|
+
*/
|
|
10
|
+
anchorSelector?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const { variants, onActivateVariant, anchorSelector = "" }: Props = $props();
|
|
14
|
+
|
|
15
|
+
let requestedOpen = $state(false);
|
|
16
|
+
let activeIndex = $state(0);
|
|
17
|
+
let anchorRect = $state<DOMRect | null>(null);
|
|
18
|
+
|
|
19
|
+
const isOpen = $derived(requestedOpen && variants.length > 1);
|
|
20
|
+
|
|
21
|
+
const safeActiveIndex = $derived(
|
|
22
|
+
variants.length === 0
|
|
23
|
+
? 0
|
|
24
|
+
: Math.min(Math.max(activeIndex, 0), variants.length - 1),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function readAnchorRect(): void {
|
|
28
|
+
if (!anchorSelector) {
|
|
29
|
+
anchorRect = null;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const el = document.querySelector(anchorSelector);
|
|
33
|
+
anchorRect = el instanceof HTMLElement ? el.getBoundingClientRect() : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function close(): void {
|
|
37
|
+
requestedOpen = false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function forceClose(): void {
|
|
41
|
+
close();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function open(): void {
|
|
45
|
+
if (variants.length <= 1) return;
|
|
46
|
+
requestedOpen = true;
|
|
47
|
+
activeIndex = 0;
|
|
48
|
+
readAnchorRect();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function handleKeyDown(e: KeyboardEvent): boolean {
|
|
52
|
+
if (!isOpen) return false;
|
|
53
|
+
|
|
54
|
+
if (e.key === "Escape") {
|
|
55
|
+
close();
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (e.key === "ArrowDown") {
|
|
60
|
+
if (variants.length <= 1) return true;
|
|
61
|
+
activeIndex = Math.min(safeActiveIndex + 1, variants.length - 1);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (e.key === "ArrowUp") {
|
|
66
|
+
if (variants.length <= 1) return true;
|
|
67
|
+
activeIndex = Math.max(safeActiveIndex - 1, 0);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (e.key === "ArrowRight") {
|
|
72
|
+
const node = variants[safeActiveIndex];
|
|
73
|
+
if (!node) return true;
|
|
74
|
+
onActivateVariant(node);
|
|
75
|
+
close();
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function onWindowMouseDown(e: MouseEvent): void {
|
|
83
|
+
if (!isOpen) return;
|
|
84
|
+
const path = e.composedPath?.() ?? [];
|
|
85
|
+
const clickedInside = path.some(
|
|
86
|
+
(t) => t instanceof HTMLElement && t.dataset?.["variantDropdown"] === "1",
|
|
87
|
+
);
|
|
88
|
+
if (!clickedInside) close();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
$effect(() => {
|
|
92
|
+
if (!isOpen) return;
|
|
93
|
+
|
|
94
|
+
readAnchorRect();
|
|
95
|
+
|
|
96
|
+
const onScrollOrResize = () => readAnchorRect();
|
|
97
|
+
window.addEventListener("scroll", onScrollOrResize, true);
|
|
98
|
+
window.addEventListener("resize", onScrollOrResize, { passive: true });
|
|
99
|
+
return () => {
|
|
100
|
+
window.removeEventListener("scroll", onScrollOrResize, true);
|
|
101
|
+
window.removeEventListener("resize", onScrollOrResize);
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const position = $derived(
|
|
106
|
+
!isOpen || !anchorRect
|
|
107
|
+
? null
|
|
108
|
+
: (() => {
|
|
109
|
+
const gap = 8;
|
|
110
|
+
const minW = 224; // ~14rem
|
|
111
|
+
const viewportW = window.innerWidth;
|
|
112
|
+
const viewportH = window.innerHeight;
|
|
113
|
+
|
|
114
|
+
// Prefer to the right of anchor; fallback to left if overflowing.
|
|
115
|
+
let left = anchorRect.right + gap;
|
|
116
|
+
if (left + minW > viewportW - gap) {
|
|
117
|
+
left = Math.max(gap, anchorRect.left - gap - minW);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Align tops, but keep in viewport.
|
|
121
|
+
let top = anchorRect.top;
|
|
122
|
+
top = Math.min(Math.max(gap, top), viewportH - gap);
|
|
123
|
+
|
|
124
|
+
return { left, top };
|
|
125
|
+
})(),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
function getMovePrefix(node: ChessTreeNode): string {
|
|
129
|
+
const fm = node.data.fullMoves;
|
|
130
|
+
if (!fm) return "";
|
|
131
|
+
return node.data.ply % 2 === 1 ? `${fm}.` : `${fm - 1}...`;
|
|
132
|
+
}
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<svelte:window onmousedown={onWindowMouseDown} />
|
|
136
|
+
|
|
137
|
+
{#if isOpen}
|
|
138
|
+
<div
|
|
139
|
+
data-variant-dropdown="1"
|
|
140
|
+
class="z-50 min-w-[14rem] rounded-md border bg-white p-1 shadow-md outline-none"
|
|
141
|
+
role="menu"
|
|
142
|
+
aria-label="Варианты следующего хода"
|
|
143
|
+
style={position
|
|
144
|
+
? `position: fixed; left: ${position.left}px; top: ${position.top}px;`
|
|
145
|
+
: "position: fixed;"}
|
|
146
|
+
>
|
|
147
|
+
{#each variants as variant, index (variant.id)}
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
data-variant-dropdown="1"
|
|
151
|
+
class={{
|
|
152
|
+
"flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-left text-sm outline-none": true,
|
|
153
|
+
"bg-accent text-accent-foreground": index === safeActiveIndex,
|
|
154
|
+
"hover:bg-accent hover:text-accent-foreground": true,
|
|
155
|
+
}}
|
|
156
|
+
onclick={(e) => {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
e.stopPropagation();
|
|
159
|
+
onActivateVariant(variant);
|
|
160
|
+
close();
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
<span class="flex min-w-0 flex-wrap items-center gap-x-1 gap-y-0.5">
|
|
164
|
+
{#if getMovePrefix(variant)}
|
|
165
|
+
<span>{getMovePrefix(variant)}</span>
|
|
166
|
+
{/if}
|
|
167
|
+
<span>{variant.data.san}</span>
|
|
168
|
+
<NagBadges nags={variant.data.nags} class="!ml-0" />
|
|
169
|
+
</span>
|
|
170
|
+
</button>
|
|
171
|
+
{/each}
|
|
172
|
+
</div>
|
|
173
|
+
{/if}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ChessTreeNode } from "../(models)/chessTreeNode.js";
|
|
2
|
+
type Props = {
|
|
3
|
+
variants: ChessTreeNode[];
|
|
4
|
+
onActivateVariant: (node: ChessTreeNode) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Представляет CSS-селектор элемента, относительно которого нужно позиционировать dropdown.
|
|
7
|
+
*/
|
|
8
|
+
anchorSelector?: string;
|
|
9
|
+
};
|
|
10
|
+
declare const VariantDropdownNavigator: import("svelte").Component<Props, {
|
|
11
|
+
forceClose: () => void;
|
|
12
|
+
open: () => void;
|
|
13
|
+
handleKeyDown: (e: KeyboardEvent) => boolean;
|
|
14
|
+
}, "">;
|
|
15
|
+
type VariantDropdownNavigator = ReturnType<typeof VariantDropdownNavigator>;
|
|
16
|
+
export default VariantDropdownNavigator;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { ChessTreeNode } from "../(models)/chessTreeNode.js";
|
|
4
|
+
|
|
5
|
+
type TVariationGroupProps = {
|
|
6
|
+
/** Возвращает родительский узел-развилку. */
|
|
7
|
+
parentNode: ChessTreeNode;
|
|
8
|
+
/** Возвращает true, если блок в верхней строке сетки дерева. */
|
|
9
|
+
isHighestLevel: boolean;
|
|
10
|
+
/** Возвращает true, если вертикальная направляющая должна быть акцентной. */
|
|
11
|
+
isActiveForkForSelection: boolean;
|
|
12
|
+
/** Возвращает true, если доступен drill-down «варианты отдельно». */
|
|
13
|
+
showVariationDrillExpand: boolean;
|
|
14
|
+
/** Представляет обработчик клика по зоне открытия drill-down. */
|
|
15
|
+
onVariationLinesHit: (e: MouseEvent) => void;
|
|
16
|
+
/** Возвращает фрагмент разметки одного дочернего варианта. */
|
|
17
|
+
variationMoveRow: Snippet<[ChessTreeNode]>;
|
|
18
|
+
/** Возвращает true, если ветка текущего выбора проходит через узел (для «птички»). */
|
|
19
|
+
isBranchOnCurrentPath: (node: ChessTreeNode) => boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
parentNode,
|
|
24
|
+
isHighestLevel,
|
|
25
|
+
isActiveForkForSelection,
|
|
26
|
+
showVariationDrillExpand,
|
|
27
|
+
onVariationLinesHit,
|
|
28
|
+
variationMoveRow,
|
|
29
|
+
isBranchOnCurrentPath,
|
|
30
|
+
}: TVariationGroupProps = $props();
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div
|
|
34
|
+
class={{
|
|
35
|
+
"col-span-3 col-start-1 w-full min-w-0": isHighestLevel,
|
|
36
|
+
"block w-full min-w-0 basis-full shrink-0": !isHighestLevel,
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
{#if parentNode.children.length > 1}
|
|
40
|
+
<div
|
|
41
|
+
class={{
|
|
42
|
+
"relative min-w-0 w-full max-w-full bg-gray-50/95": true,
|
|
43
|
+
" rounded-md shadow-none transition-[border-color,box-shadow] duration-150 [&:has(>[data-line-hit]:hover)>.flex>div[data-line-stroke]]:border-gray-600 [&:has(>[data-line-hit]:hover)>.flex>ul>li>span[data-line-stroke]]:border-gray-600 [&:has(>[data-line-hit]:focus-visible)>.flex>div[data-line-stroke]]:border-gray-600 [&:has(>[data-line-hit]:focus-visible)>.flex>ul>li>span[data-line-stroke]]:border-gray-600 ":
|
|
44
|
+
showVariationDrillExpand,
|
|
45
|
+
"pr-2": !showVariationDrillExpand,
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<div class="flex min-w-0 w-full max-w-full gap-0 align-top">
|
|
49
|
+
<div
|
|
50
|
+
class={{
|
|
51
|
+
"w-2 shrink-0 self-stretch border-r-2 transition-[border-color] duration-150": true,
|
|
52
|
+
"border-gray-600": isActiveForkForSelection,
|
|
53
|
+
"border-gray-300": !isActiveForkForSelection,
|
|
54
|
+
"pointer-events-none": showVariationDrillExpand,
|
|
55
|
+
}}
|
|
56
|
+
data-line-stroke
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
></div>
|
|
59
|
+
<ul
|
|
60
|
+
class="m-0 flex min-w-0 flex-1 list-none flex-col gap-1.5 py-2 pl-0"
|
|
61
|
+
>
|
|
62
|
+
{#each parentNode.children as child, index (child.id)}
|
|
63
|
+
{#if !isHighestLevel || index >= 1}
|
|
64
|
+
{@const branchOnPath = isBranchOnCurrentPath(child)}
|
|
65
|
+
<li class="relative min-w-0 pl-3">
|
|
66
|
+
<span
|
|
67
|
+
class={{
|
|
68
|
+
"absolute left-0 top-[0.65em] w-3 border-t-2 transition-[border-color] duration-150": true,
|
|
69
|
+
"border-gray-600":
|
|
70
|
+
isActiveForkForSelection &&
|
|
71
|
+
branchOnPath,
|
|
72
|
+
"border-gray-300":
|
|
73
|
+
!isActiveForkForSelection ||
|
|
74
|
+
!branchOnPath,
|
|
75
|
+
"pointer-events-none":
|
|
76
|
+
showVariationDrillExpand,
|
|
77
|
+
}}
|
|
78
|
+
data-line-stroke
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
></span>
|
|
81
|
+
<div class="tracking-tight">
|
|
82
|
+
{@render variationMoveRow(child)}
|
|
83
|
+
</div>
|
|
84
|
+
</li>
|
|
85
|
+
{/if}
|
|
86
|
+
{/each}
|
|
87
|
+
</ul>
|
|
88
|
+
</div>
|
|
89
|
+
{#if showVariationDrillExpand}
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
class="absolute left-0 top-0 z-20 h-full w-5 max-w-full cursor-pointer border-0 bg-transparent p-0 focus:outline-none focus-visible:ring-0"
|
|
93
|
+
data-line-hit
|
|
94
|
+
aria-label="Открыть варианты отдельно"
|
|
95
|
+
onclick={onVariationLinesHit}
|
|
96
|
+
></button>
|
|
97
|
+
{/if}
|
|
98
|
+
</div>
|
|
99
|
+
{/if}
|
|
100
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { ChessTreeNode } from "../(models)/chessTreeNode.js";
|
|
3
|
+
type TVariationGroupProps = {
|
|
4
|
+
/** Возвращает родительский узел-развилку. */
|
|
5
|
+
parentNode: ChessTreeNode;
|
|
6
|
+
/** Возвращает true, если блок в верхней строке сетки дерева. */
|
|
7
|
+
isHighestLevel: boolean;
|
|
8
|
+
/** Возвращает true, если вертикальная направляющая должна быть акцентной. */
|
|
9
|
+
isActiveForkForSelection: boolean;
|
|
10
|
+
/** Возвращает true, если доступен drill-down «варианты отдельно». */
|
|
11
|
+
showVariationDrillExpand: boolean;
|
|
12
|
+
/** Представляет обработчик клика по зоне открытия drill-down. */
|
|
13
|
+
onVariationLinesHit: (e: MouseEvent) => void;
|
|
14
|
+
/** Возвращает фрагмент разметки одного дочернего варианта. */
|
|
15
|
+
variationMoveRow: Snippet<[ChessTreeNode]>;
|
|
16
|
+
/** Возвращает true, если ветка текущего выбора проходит через узел (для «птички»). */
|
|
17
|
+
isBranchOnCurrentPath: (node: ChessTreeNode) => boolean;
|
|
18
|
+
};
|
|
19
|
+
declare const VariationGroup: import("svelte").Component<TVariationGroupProps, {}, "">;
|
|
20
|
+
type VariationGroup = ReturnType<typeof VariationGroup>;
|
|
21
|
+
export default VariationGroup;
|