@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,257 +1,316 @@
1
1
  <script lang="ts">
2
2
  import type { ChessTreeNode } from "../(models)/chessTreeNode.js";
3
3
 
4
- import ArrowUpIcon from "@lucide/svelte/icons/arrow-up";
5
- import CheckCheckIcon from "@lucide/svelte/icons/check-check";
6
- import FlagIcon from "@lucide/svelte/icons/flag";
7
- import Trash2Icon from "@lucide/svelte/icons/trash-2";
8
4
  import { parseComment } from "@connectorvol/chessops/pgn";
9
- import * as ContextMenu from "../components/ui/context-menu/index.js";
10
5
 
6
+ import { createPreviewHover } from "../(utils)/createPreviewHover.js";
11
7
  import { getGameContext } from "../(utils)/context.js";
12
- import { transformNag } from "../(utils)/transformNag.js";
13
8
  import Move from "./Move.svelte";
14
- import MoveWithIcon from "./MoveWithIcon.svelte";
9
+ import MoveComment from "./MoveComment.svelte";
10
+ import MoveSanWithMenu from "./MoveSanWithMenu.svelte";
11
+ import VariationGroup from "./VariationGroup.svelte";
15
12
  import type { PieceSet } from "@connectorvol/shared";
16
- import { ACTIVE_MOVE_CLASS } from "../(utils)/scrollToActiveMove.js";
17
13
 
18
14
  const HIGHEST_LEVEL = 1;
19
15
 
20
- interface Props {
16
+ type TMoveProps = {
17
+ /** Возвращает узел хода в дереве партии. */
21
18
  chessNode: ChessTreeNode;
19
+ /** Возвращает глубину вложенности относительно корня отображения. */
22
20
  depth: number;
21
+ /** Возвращает родительский узел или null для корня. */
23
22
  parentNode: ChessTreeNode | null;
24
- needSpace: boolean;
25
- needRenderParentChildrens?: boolean;
23
+ /** Возвращает true, если нужна пустая ячейка хода (сетка верхнего уровня). */
24
+ shouldReserveEmptyMoveCell: boolean;
25
+ /** Возвращает true, если нужно отрисовать соседние варианты у развилки. */
26
+ shouldRenderSiblingVariations?: boolean;
27
+ /** Возвращает набор фигур для иконок SAN. */
26
28
  pieceSet: PieceSet;
27
- }
29
+ };
28
30
 
29
31
  const {
30
32
  chessNode,
31
33
  depth,
32
34
  parentNode,
33
- needSpace,
34
- needRenderParentChildrens = true,
35
+ shouldReserveEmptyMoveCell,
36
+ shouldRenderSiblingVariations = true,
35
37
  pieceSet,
36
- }: Props = $props();
38
+ }: TMoveProps = $props();
37
39
 
38
- const { chessTree, onSelectNode, onDeleteVariant, selectable } = getGameContext();
40
+ /** Контекст игры без деструктуризации — иначе selectable и chessTree «замораживаются» при первом рендере TreeViewer. */
41
+ const game = getGameContext();
39
42
 
40
- function onmousedown() {
41
- if (!selectable || !node) return;
42
- chessTree.currentNode = chessNode;
43
+ /** Представляет обработчик выбора текущего узла по `mousedown`. */
44
+ function handleMoveMouseDown() {
45
+ if (!game.selectable || !moveNode) return;
46
+ game.chessTree.currentNode = chessNode;
43
47
 
44
- onSelectNode();
48
+ game.onSelectNode();
49
+ game.onChessNodeSelected?.(game.chessTree.currentNode);
45
50
  }
46
51
 
47
- const node = $derived(chessNode);
48
- const isTheSameNode = $derived(chessNode.id === chessTree.currentNode.id);
52
+ const moveNode = $derived(chessNode);
53
+ const previewHover = createPreviewHover({
54
+ setPreviewFen: game.setPreviewFen,
55
+ getFen: () => moveNode.data.fen,
56
+ getLastMove: () => moveNode.data.lastMove ?? null,
57
+ getDelayMs: () => game.previewHoverDelayMs,
58
+ });
59
+ const isCurrentMove = $derived(chessNode.id === game.chessTree.currentNode.id);
60
+ /** Представляет текст первого PGN-комментария узла после разбора `comments[0]`, синхронно с правкой строки под деревом. */
49
61
  const firstCommentParsed = $derived(
50
- node.data.parsedFirstComment ?? (node.data.comments?.[0] ? parseComment(node.data.comments[0]) : null)
62
+ moveNode.data.comments?.[0] !== undefined
63
+ ? parseComment(moveNode.data.comments[0])
64
+ : null,
51
65
  );
52
66
  const firstCommentText = $derived(firstCommentParsed?.text);
53
67
  const childMoves = $derived(chessNode.children);
54
- const isForcedNodeId = $derived(chessNode.id === chessTree.forcedNodeId);
68
+ const isForcedLineStart = $derived(chessNode.id === game.chessTree.forcedNodeId);
69
+
70
+ /** Представляет признак активного drill-down «варианты отдельно». */
71
+ const variationDrillDownActive = $derived(
72
+ game.chessTree.isVariationDrillDownActive(),
73
+ );
74
+
75
+ /**
76
+ * Представляет признак: развилка на этом уровне совпадает с узлом, для которого
77
+ * `ChessTree` рассчитывает акцент направляющих при текущем выборе.
78
+ */
79
+ const isActiveForkForSelection = $derived(
80
+ shouldRenderSiblingVariations &&
81
+ parentNode !== null &&
82
+ (parentNode.children?.length ?? 0) > 1 &&
83
+ game.chessTree.currentGuideHighlightForkParent?.id === parentNode.id,
84
+ );
55
85
 
56
- const spaceBlockClass = "p-8";
86
+ const spaceBlockClass = "px-3 py-2 !bg-gray-50/95";
57
87
  const spaceInlineBlockClass = "p-2";
58
88
 
59
- const isHighestLevel = depth === HIGHEST_LEVEL;
60
- </script>
89
+ const isHighestLevel = $derived(depth === HIGHEST_LEVEL);
61
90
 
62
- {#snippet sanMove()}
63
- {@const selectedOnHighestLevel = isTheSameNode && isHighestLevel}
64
- <!-- svelte-ignore a11y_no_static_element_interactions -->
65
- <ContextMenu.Root>
66
- <ContextMenu.Trigger
67
- class={{
68
- "cursor-pointer ": selectable,
69
- "cursor-default": !selectable,
70
- [`${spaceInlineBlockClass} text-center`]: isHighestLevel,
71
- inline: !isHighestLevel,
72
- [` outline-primary z-10 -outline-offset-2 outline-3 ${ACTIVE_MOVE_CLASS}`]:
73
- selectedOnHighestLevel,
74
- "outline-none": !selectedOnHighestLevel,
75
- }}
76
- {onmousedown}
77
- >
78
- {#if !isHighestLevel && node.data.fullMoves && node.data.ply % 2 === 1}
79
- {node.data.fullMoves}.
80
- {/if}
81
- {#if !isHighestLevel && node.data.fullMoves && node.data.ply % 2 === 0 && parentNode?.children?.[0]?.id !== chessNode.id}
82
- {node.data.fullMoves - 1}...
83
- {/if}<span
84
- class={{
85
- " outline-primary outline-2": isTheSameNode && !isHighestLevel,
86
- }}
87
- >
88
- <MoveWithIcon
89
- san={node.data.san}
90
- ply={node.data.ply}
91
- {pieceSet}
92
- />{node.data.nags?.map(transformNag).join(" ")}
93
- </span>
94
- </ContextMenu.Trigger>
95
- <ContextMenu.Content class="outline-none">
96
- {#if !isHighestLevel}
97
- <ContextMenu.Item
98
- class="gap-4"
99
- onclick={() => chessTree.makeMainVariant(chessNode)}
100
- >Сделать вариант главной линией <ContextMenu.Shortcut
101
- ><CheckCheckIcon /></ContextMenu.Shortcut
102
- ></ContextMenu.Item
103
- >
104
- {#if depth > HIGHEST_LEVEL + 1}
105
- <ContextMenu.Item
106
- class="gap-4"
107
- onclick={() => chessTree.upPriority(chessNode)}
108
- >Поднять приоритет варианта <ContextMenu.Shortcut
109
- ><ArrowUpIcon /></ContextMenu.Shortcut
110
- ></ContextMenu.Item
111
- >
112
- {/if}
113
- {:else}
114
- <ContextMenu.Item
115
- class="gap-4"
116
- onclick={() => (chessTree.forcedNodeId = chessNode.parentId ?? null)}
117
- >Завершить главную линию <ContextMenu.Shortcut
118
- ><FlagIcon /></ContextMenu.Shortcut
119
- ></ContextMenu.Item
120
- >
121
- {/if}
122
- <ContextMenu.Separator />
123
- <ContextMenu.Item
124
- class="gap-4 "
125
- onclick={() => {
126
- chessTree.deleteVariant(chessNode);
127
- // Обновляем состояние игры после удаления
128
- onDeleteVariant();
129
- }}
130
- >Удалить вариант <ContextMenu.Shortcut
131
- ><Trash2Icon /></ContextMenu.Shortcut
132
- ></ContextMenu.Item
133
- >
134
- </ContextMenu.Content>
135
- </ContextMenu.Root>
136
- {/snippet}
91
+ /**
92
+ * Представляет признак: в верхней сетке после белого полухода показывается пустая ячейка чёрных
93
+ * без ноды (партия оборвалась на ходе белых) — нижнюю границу рисуем только под двумя первыми колонками.
94
+ */
95
+ const addsEmptyMainLineBlackCell = $derived(
96
+ isHighestLevel &&
97
+ moveNode.data.ply % 2 === 1 &&
98
+ !isForcedLineStart &&
99
+ (parentNode === null || parentNode.children.length <= 1) &&
100
+ !firstCommentText &&
101
+ (childMoves?.length ?? 0) === 0,
102
+ );
103
+
104
+ /**
105
+ * Представляет признак: верхняя строка — первый полуход чёрных от корня партии (нотация «1... SAN»).
106
+ */
107
+ const isLeadingBlackHalfMoveFromRoot = $derived(
108
+ isHighestLevel &&
109
+ parentNode !== null &&
110
+ parentNode.data.ply === 0 &&
111
+ moveNode.data.ply % 2 === 0,
112
+ );
113
+
114
+ /** Представляет true, если в контекстном меню показывать «Поднять приоритет». */
115
+ const showUpPriorityItem = $derived(depth > HIGHEST_LEVEL + 1);
116
+
117
+ /**
118
+ * Представляет признак: SAN главного варианта (children[0]) переносится в блок списка веток,
119
+ * чтобы не дублировать его перед группой (например «8. g3» только среди g3 / f3 / a3).
120
+ */
121
+ const shouldRenderSanInsideVariationGroup = $derived(
122
+ !isHighestLevel &&
123
+ shouldRenderSiblingVariations &&
124
+ parentNode !== null &&
125
+ parentNode.children.length > 1 &&
126
+ parentNode.children[0]?.id === chessNode.id,
127
+ );
128
+
129
+ /**
130
+ * Представляет обработчик клика по зоне линий ветвления, открывающий режим «варианты отдельно».
131
+ */
132
+ function onVariationLinesHit(e: MouseEvent) {
133
+ e.stopPropagation();
134
+ if (parentNode === null) return;
135
+ const selectFork = !game.chessTree.isCurrentNodeWithinForkSubtree(parentNode);
136
+ game.chessTree.openVariationDrillDown(parentNode, { selectFork });
137
+ game.onSelectNode();
138
+ game.onChessNodeSelected?.(game.chessTree.currentNode);
139
+ }
140
+ </script>
137
141
 
138
142
  {#snippet emptyMove()}
139
143
  <div class="flex items-center justify-center text-center">...</div>
140
144
  {/snippet}
141
145
 
146
+ {#snippet emptyMainLineBlackCell()}
147
+ <div
148
+ class="tree-viewer-tail-no-node-cell flex min-w-0 items-center justify-center border-b-0 p-2 text-center"
149
+ ></div>
150
+ {/snippet}
151
+
142
152
  {#snippet fullMovesCounter(minus: number = 0)}
143
153
  <span
144
154
  class={{
145
155
  "flex items-center justify-center text-center": isHighestLevel,
146
156
  inline: !isHighestLevel,
157
+ "border-b border-gray-300": addsEmptyMainLineBlackCell && isHighestLevel,
158
+ "tree-viewer-overflow-tail-border":
159
+ addsEmptyMainLineBlackCell && isHighestLevel,
147
160
  }}
148
161
  >
149
- {node.data.fullMoves - minus}{!isHighestLevel && node.data.ply % 2 === 1
162
+ {moveNode.data.fullMoves - minus}{!isHighestLevel &&
163
+ moveNode.data.ply % 2 === 1
150
164
  ? "..."
151
- : !isHighestLevel && node.data.ply % 2 === 0
165
+ : !isHighestLevel && moveNode.data.ply % 2 === 0
152
166
  ? "..."
153
167
  : ""}
154
168
  </span>
155
169
  {/snippet}
156
170
 
157
- {#snippet comment(text: string)}
158
- <div
159
- class={{
160
- "select-text": true,
161
- "col-span-3 flex flex-col": isHighestLevel,
162
- inline: !isHighestLevel,
163
- [spaceBlockClass]: isHighestLevel,
164
- }}
165
- >
166
- {#each text.split("\n") as line, index}
167
- {#if index > 0}<br />{/if}{line}
168
- {/each}
169
- </div>
171
+ {#snippet variationMoveRow(child: ChessTreeNode)}
172
+ <Move
173
+ {pieceSet}
174
+ chessNode={child}
175
+ shouldReserveEmptyMoveCell={parentNode !== null &&
176
+ parentNode.children.length > 1}
177
+ depth={depth + 1}
178
+ shouldRenderSiblingVariations={false}
179
+ {parentNode}
180
+ />
170
181
  {/snippet}
171
182
 
172
183
  {#if isHighestLevel}
173
- {#if node.data.ply % 2 === 1}
184
+ {#if moveNode.data.ply % 2 === 1}
174
185
  {@render fullMovesCounter()}
175
186
  {/if}
176
187
 
177
- {#if node.parentId}
178
- {@render sanMove()}
188
+ {#if variationDrillDownActive && moveNode.data.ply % 2 === 0 && game.chessTree.drillForkParent !== null && game.chessTree.drillForkParent.children[0]?.id === chessNode.id && moveNode.parentId}
189
+ <span
190
+ class={{
191
+ "flex items-center justify-center text-center": isHighestLevel,
192
+ }}
193
+ >
194
+ {moveNode.data.fullMoves}
195
+ </span>
196
+ {@render emptyMove()}
179
197
  {/if}
180
- {#if (isForcedNodeId && node.data.ply % 2 === 1) || (parentNode?.children.length && parentNode?.children.length > 1 && node.data.ply % 2 === 1) || (firstCommentText && node.data.ply % 2 === 1)}
198
+
199
+ {#if isLeadingBlackHalfMoveFromRoot}
200
+ <span
201
+ class={{
202
+ "flex items-center justify-center text-center": isHighestLevel,
203
+ }}
204
+ >
205
+ {Math.max(1, moveNode.data.fullMoves - 1)}
206
+ </span>
181
207
  {@render emptyMove()}
182
208
  {/if}
183
209
 
210
+ {#if moveNode.parentId}
211
+ <MoveSanWithMenu
212
+ {chessNode}
213
+ {parentNode}
214
+ {pieceSet}
215
+ selectable={game.selectable}
216
+ {isHighestLevel}
217
+ {isCurrentMove}
218
+ previewHover={{
219
+ onmouseenter: previewHover.onmouseenter,
220
+ onmouseleave: previewHover.onmouseleave,
221
+ }}
222
+ onMoveMouseDown={handleMoveMouseDown}
223
+ {spaceInlineBlockClass}
224
+ nags={moveNode.data.nags}
225
+ chessTree={game.chessTree}
226
+ {showUpPriorityItem}
227
+ onDeleteVariant={game.onDeleteVariant}
228
+ mainRowBottomEdge={addsEmptyMainLineBlackCell}
229
+ />
230
+ {/if}
231
+ {#if isHighestLevel && moveNode.data.ply % 2 === 1}
232
+ {#if isForcedLineStart || (parentNode != null && parentNode.children.length > 1) || !!firstCommentText}
233
+ {@render emptyMove()}
234
+ {:else if (childMoves?.length ?? 0) === 0}
235
+ {@render emptyMainLineBlackCell()}
236
+ {/if}
237
+ {/if}
238
+
184
239
  {#if firstCommentText}
185
- {#if node.data.ply % 2 === 1}
186
- {@render comment(firstCommentText)}
187
- {#if !needSpace && childMoves?.[0] && !isForcedNodeId}
240
+ {#if moveNode.data.ply % 2 === 1}
241
+ <MoveComment text={firstCommentText} {isHighestLevel} {spaceBlockClass} />
242
+ {#if !shouldReserveEmptyMoveCell && childMoves?.[0] && !isForcedLineStart}
188
243
  {@render fullMovesCounter()}
189
244
  {@render emptyMove()}
190
245
  {/if}
191
246
  {/if}
192
247
 
193
- {#if node.data.ply % 2 === 0}
194
- {@render comment(firstCommentText)}
248
+ {#if moveNode.data.ply % 2 === 0}
249
+ <MoveComment text={firstCommentText} {isHighestLevel} {spaceBlockClass} />
195
250
  {/if}
196
251
  {/if}
197
252
  {:else}
198
- {#if node.data.ply % 2 === 0 && chessTree.forcedNodeId === node.parentId && parentNode?.children[0].id === node.id}
253
+ {#if moveNode.data.ply % 2 === 0 && game.chessTree.forcedNodeId === moveNode.parentId && parentNode?.children[0].id === moveNode.id}
199
254
  {@render fullMovesCounter(1)}
200
255
  {/if}
201
256
 
202
- {@render sanMove()}
257
+ {#if !shouldRenderSanInsideVariationGroup}
258
+ <MoveSanWithMenu
259
+ {chessNode}
260
+ {parentNode}
261
+ {pieceSet}
262
+ selectable={game.selectable}
263
+ {isHighestLevel}
264
+ {isCurrentMove}
265
+ previewHover={{
266
+ onmouseenter: previewHover.onmouseenter,
267
+ onmouseleave: previewHover.onmouseleave,
268
+ }}
269
+ onMoveMouseDown={handleMoveMouseDown}
270
+ {spaceInlineBlockClass}
271
+ nags={moveNode.data.nags}
272
+ chessTree={game.chessTree}
273
+ {showUpPriorityItem}
274
+ onDeleteVariant={game.onDeleteVariant}
275
+ />
276
+ {/if}
203
277
  {#if firstCommentText}
204
- {#if node.data.ply % 2 === 1}
205
- {@render comment(firstCommentText)}
206
- {#if !needSpace && childMoves?.[0]}
278
+ {#if moveNode.data.ply % 2 === 1}
279
+ <MoveComment text={firstCommentText} {isHighestLevel} {spaceBlockClass} />
280
+ {#if !shouldReserveEmptyMoveCell && childMoves?.[0] && !shouldRenderSanInsideVariationGroup}
207
281
  {@render fullMovesCounter()}
208
282
  {/if}
209
283
  {/if}
210
284
 
211
- {#if node.data.ply % 2 === 0}
212
- {@render comment(firstCommentText)}
285
+ {#if moveNode.data.ply % 2 === 0}
286
+ <MoveComment text={firstCommentText} {isHighestLevel} {spaceBlockClass} />
213
287
  {/if}
214
288
  {/if}
215
289
  {/if}
216
290
 
217
- {#if parentNode && parentNode?.children.length > 1 && needRenderParentChildrens}
218
- <div class={{ "col-span-3": isHighestLevel, inline: !isHighestLevel }}>
219
- {#if !isHighestLevel}
220
- <span>(</span>
221
- {/if}
222
- {#if parentNode?.children.length > 1}
223
- {#each parentNode?.children as child, index (index)}
224
- {#if index >= 1}
225
- <div
226
- class={{
227
- inline: !isHighestLevel,
228
- [spaceBlockClass]: isHighestLevel,
229
- "tracking-tight": true,
230
- }}
231
- >
232
- <Move
233
- {pieceSet}
234
- chessNode={child}
235
- needSpace={parentNode?.children.length > 1}
236
- depth={depth + 1}
237
- needRenderParentChildrens={false}
238
- {parentNode}
239
- ></Move>
240
- </div>
241
- {/if}
242
- {/each}
243
- {/if}
244
- {#if !isHighestLevel}
245
- <span>)</span>
246
- {/if}
247
- </div>
291
+ {#if parentNode && parentNode?.children.length > 1 && shouldRenderSiblingVariations}
292
+ {@const showVariationDrillExpand = !game.chessTree.isNodeOnMainLine(chessNode)}
293
+ <VariationGroup
294
+ {parentNode}
295
+ {isHighestLevel}
296
+ {isActiveForkForSelection}
297
+ {showVariationDrillExpand}
298
+ {onVariationLinesHit}
299
+ {variationMoveRow}
300
+ isBranchOnCurrentPath={(n) => game.chessTree.isCurrentOnPathFromNode(n)}
301
+ />
248
302
  {/if}
249
303
 
250
- {#if isForcedNodeId}
251
- <div class={{ "col-span-3": isHighestLevel, inline: !isHighestLevel }}>
304
+ {#if isForcedLineStart}
305
+ <div
306
+ class={{
307
+ "col-span-3 col-start-1 w-full min-w-0": isHighestLevel,
308
+ "block w-full min-w-0 basis-full shrink-0": !isHighestLevel,
309
+ }}
310
+ >
252
311
  <div
253
312
  class={{
254
- inline: !isHighestLevel,
313
+ "block w-full min-w-0": !isHighestLevel,
255
314
  [spaceBlockClass]: isHighestLevel,
256
315
  "tracking-tight": true,
257
316
  }}
@@ -259,25 +318,35 @@
259
318
  <Move
260
319
  {pieceSet}
261
320
  chessNode={chessNode.children[0]}
262
- needSpace={chessNode.children.length > 1}
321
+ shouldReserveEmptyMoveCell={chessNode.children.length > 1}
263
322
  depth={depth + 1}
264
- needRenderParentChildrens={true}
323
+ shouldRenderSiblingVariations={true}
265
324
  parentNode={chessNode}
266
325
  ></Move>
267
326
  </div>
268
327
  </div>
269
328
  {/if}
270
329
 
271
- {#if needSpace && node.data.ply % 2 === 1 && isHighestLevel && !isForcedNodeId}
330
+ {#if shouldReserveEmptyMoveCell && moveNode.data.ply % 2 === 1 && isHighestLevel && !isForcedLineStart && childMoves?.[0]}
272
331
  {@render fullMovesCounter()}
273
332
  {@render emptyMove()}
274
333
  {/if}
275
- {#if childMoves?.[0]}
276
- {#if !isForcedNodeId}
334
+ {#if variationDrillDownActive && isHighestLevel && moveNode.data.ply % 2 === 1 && !shouldReserveEmptyMoveCell && childMoves?.[0] && !isForcedLineStart && game.chessTree.drillForkParent !== null && game.chessTree.drillForkParent.children[0]?.id === chessNode.id}
335
+ <span
336
+ class={{
337
+ "flex items-center justify-center text-center": isHighestLevel,
338
+ }}
339
+ >
340
+ {moveNode.data.fullMoves}
341
+ </span>
342
+ {@render emptyMove()}
343
+ {/if}
344
+ {#if childMoves?.[0] && !shouldRenderSanInsideVariationGroup}
345
+ {#if !isForcedLineStart}
277
346
  <Move
278
347
  {pieceSet}
279
348
  chessNode={chessNode.children[0]}
280
- needSpace={childMoves?.length > 1}
349
+ shouldReserveEmptyMoveCell={childMoves?.length > 1}
281
350
  {depth}
282
351
  parentNode={chessNode}
283
352
  ></Move>
@@ -1,14 +1,20 @@
1
1
  import type { ChessTreeNode } from "../(models)/chessTreeNode.js";
2
2
  import Move from "./Move.svelte";
3
3
  import type { PieceSet } from "@connectorvol/shared";
4
- interface Props {
4
+ type TMoveProps = {
5
+ /** Возвращает узел хода в дереве партии. */
5
6
  chessNode: ChessTreeNode;
7
+ /** Возвращает глубину вложенности относительно корня отображения. */
6
8
  depth: number;
9
+ /** Возвращает родительский узел или null для корня. */
7
10
  parentNode: ChessTreeNode | null;
8
- needSpace: boolean;
9
- needRenderParentChildrens?: boolean;
11
+ /** Возвращает true, если нужна пустая ячейка хода (сетка верхнего уровня). */
12
+ shouldReserveEmptyMoveCell: boolean;
13
+ /** Возвращает true, если нужно отрисовать соседние варианты у развилки. */
14
+ shouldRenderSiblingVariations?: boolean;
15
+ /** Возвращает набор фигур для иконок SAN. */
10
16
  pieceSet: PieceSet;
11
- }
12
- declare const Move: import("svelte").Component<Props, {}, "">;
17
+ };
18
+ declare const Move: import("svelte").Component<TMoveProps, {}, "">;
13
19
  type Move = ReturnType<typeof Move>;
14
20
  export default Move;
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ type TMoveCommentProps = {
3
+ /** Возвращает текст комментария (возможны переводы строк). */
4
+ text: string;
5
+ /** Возвращает true, если строка в верхнем уровне сетки дерева. */
6
+ isHighestLevel: boolean;
7
+ /** Возвращает класс фона/отступов для уровня «верхняя строка». */
8
+ spaceBlockClass: string;
9
+ };
10
+
11
+ const {
12
+ text,
13
+ isHighestLevel,
14
+ spaceBlockClass,
15
+ }: TMoveCommentProps = $props();
16
+
17
+ /** Представляет строки комментария после разбиения по `\n`. */
18
+ const commentLines = $derived(text.split("\n"));
19
+ </script>
20
+
21
+ <div
22
+ class={{
23
+ "select-text": true,
24
+ "col-span-3 flex flex-col": isHighestLevel,
25
+ inline: !isHighestLevel,
26
+ [spaceBlockClass]: isHighestLevel,
27
+ }}
28
+ >
29
+ {#each commentLines as line, index (index)}
30
+ {#if index > 0}<br />{/if}{line}
31
+ {/each}
32
+ </div>
@@ -0,0 +1,11 @@
1
+ type TMoveCommentProps = {
2
+ /** Возвращает текст комментария (возможны переводы строк). */
3
+ text: string;
4
+ /** Возвращает true, если строка в верхнем уровне сетки дерева. */
5
+ isHighestLevel: boolean;
6
+ /** Возвращает класс фона/отступов для уровня «верхняя строка». */
7
+ spaceBlockClass: string;
8
+ };
9
+ declare const MoveComment: import("svelte").Component<TMoveCommentProps, {}, "">;
10
+ type MoveComment = ReturnType<typeof MoveComment>;
11
+ export default MoveComment;