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