@connectorvol/tree 2.1.2 → 4.1.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 (52) hide show
  1. package/dist/(classes)/chessTree.svelte.d.ts +83 -0
  2. package/dist/(classes)/chessTree.svelte.js +198 -0
  3. package/dist/(components)/DrillBreadcrumbs.svelte +135 -0
  4. package/dist/(components)/DrillBreadcrumbs.svelte.d.ts +20 -0
  5. package/dist/(components)/DrillForkSanLabel.svelte +26 -0
  6. package/dist/(components)/DrillForkSanLabel.svelte.d.ts +11 -0
  7. package/dist/(components)/DrillVariationList.svelte +65 -0
  8. package/dist/(components)/DrillVariationList.svelte.d.ts +15 -0
  9. package/dist/(components)/Move.svelte +242 -173
  10. package/dist/(components)/Move.svelte.d.ts +11 -5
  11. package/dist/(components)/MoveComment.svelte +32 -0
  12. package/dist/(components)/MoveComment.svelte.d.ts +11 -0
  13. package/dist/(components)/MoveContextMenu.svelte +73 -0
  14. package/dist/(components)/MoveContextMenu.svelte.d.ts +17 -0
  15. package/dist/(components)/MoveSanWithMenu.svelte +158 -0
  16. package/dist/(components)/MoveSanWithMenu.svelte.d.ts +42 -0
  17. package/dist/(components)/NagBadges.svelte +37 -0
  18. package/dist/(components)/NagBadges.svelte.d.ts +9 -0
  19. package/dist/(components)/TreeViewer.svelte +861 -42
  20. package/dist/(components)/TreeViewer.svelte.d.ts +29 -3
  21. package/dist/(components)/TreeViewerPanelManager.svelte +18 -155
  22. package/dist/(components)/VariantDropdownNavigator.svelte +173 -0
  23. package/dist/(components)/VariantDropdownNavigator.svelte.d.ts +16 -0
  24. package/dist/(components)/VariationGroup.svelte +100 -0
  25. package/dist/(components)/VariationGroup.svelte.d.ts +21 -0
  26. package/dist/(constants)/png.d.ts +1 -1
  27. package/dist/(constants)/png.js +2 -4
  28. package/dist/(models)/nagCatalog.d.ts +36 -0
  29. package/dist/(models)/nagCatalog.js +97 -0
  30. package/dist/(models)/pgnNodeCustomData.d.ts +3 -0
  31. package/dist/(utils)/context.d.ts +10 -3
  32. package/dist/(utils)/context.js +3 -14
  33. package/dist/(utils)/createPreviewHover.d.ts +19 -0
  34. package/dist/(utils)/createPreviewHover.js +37 -0
  35. package/dist/(utils)/nagBadgeStyle.d.ts +1 -0
  36. package/dist/(utils)/nagBadgeStyle.js +1 -0
  37. package/dist/(utils)/scrollToActiveMove.js +6 -5
  38. package/dist/(utils)/transformNag.d.ts +1 -1
  39. package/dist/(utils)/transformNag.js +1 -34
  40. package/dist/(utils)/transformPgnToChessNode.js +13 -0
  41. package/dist/(utils)/treeViewerPanelNavigation.svelte.d.ts +39 -0
  42. package/dist/(utils)/treeViewerPanelNavigation.svelte.js +257 -0
  43. package/dist/components/ui/button/button-variants.d.ts +65 -0
  44. package/dist/components/ui/button/button-variants.js +28 -0
  45. package/dist/components/ui/button/button.svelte +3 -43
  46. package/dist/components/ui/button/button.svelte.d.ts +1 -61
  47. package/dist/components/ui/button/index.d.ts +3 -2
  48. package/dist/components/ui/button/index.js +3 -4
  49. package/dist/components/ui/button-group/button-group-separator.svelte.d.ts +1 -1
  50. package/dist/index.d.ts +2 -1
  51. package/dist/index.js +2 -1
  52. 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 { PieceSet } from "@connectorvol/shared";
4
- type Props = {
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<Props, {}, "">;
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
- let highlightedButtons = $state({
35
- previous: false,
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
- return () => {
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;