@connectorvol/chess-widgets 8.0.1 → 9.0.1

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 (48) hide show
  1. package/dist/(components)/BoardSettingsTrigger.svelte +10 -0
  2. package/dist/(components)/BoardSettingsTrigger.svelte.d.ts +18 -0
  3. package/dist/constants/default-board-appearance-settings.d.ts +9 -0
  4. package/dist/constants/default-board-appearance-settings.js +30 -0
  5. package/dist/constants/editable-board-settings.d.ts +3 -21
  6. package/dist/constants/editable-board-settings.js +24 -19
  7. package/dist/game-analyzer/GameAnalyzer.svelte +28 -10
  8. package/dist/game-analyzer/gameAnalyzer.svelte.js +6 -9
  9. package/dist/game-analyzer/types.d.ts +13 -8
  10. package/dist/index.d.ts +9 -1
  11. package/dist/index.js +6 -0
  12. package/dist/position-editor/EditPanel.svelte +9 -6
  13. package/dist/puzzle/puzzleCreatedPayload.d.ts +17 -0
  14. package/dist/puzzle/puzzleData.d.ts +4 -0
  15. package/dist/puzzle/puzzleData.js +10 -0
  16. package/dist/puzzle/puzzlePreviewConstants.d.ts +4 -0
  17. package/dist/puzzle/puzzlePreviewConstants.js +4 -0
  18. package/dist/puzzle/puzzlePreviewPathNags.d.ts +6 -0
  19. package/dist/puzzle/puzzlePreviewPathNags.js +35 -0
  20. package/dist/puzzle/puzzleSolverForkAnnotations.d.ts +2 -4
  21. package/dist/puzzle/puzzleSolverForkAnnotations.js +13 -22
  22. package/dist/puzzle/puzzleStepPreviewSolver.d.ts +13 -1
  23. package/dist/puzzle/puzzleStepPreviewSolver.js +69 -9
  24. package/dist/puzzle/syncPuzzleBranchNags.d.ts +14 -0
  25. package/dist/puzzle/syncPuzzleBranchNags.js +125 -0
  26. package/dist/puzzle-creation/OpeningTagHoverPreview.svelte +81 -0
  27. package/dist/puzzle-creation/OpeningTagHoverPreview.svelte.d.ts +11 -0
  28. package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte +198 -98
  29. package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte.d.ts +20 -2
  30. package/dist/puzzle-creation/PuzzleCreationWizard.svelte +100 -106
  31. package/dist/puzzle-creation/PuzzleCreationWizard.svelte.d.ts +47 -3
  32. package/dist/puzzle-creation/PuzzlePgnBoardTreeEditor.svelte +945 -498
  33. package/dist/puzzle-creation/PuzzleWizardTagsStep.svelte +36 -0
  34. package/dist/puzzle-creation/PuzzleWizardTagsStep.svelte.d.ts +12 -0
  35. package/dist/puzzle-creation/StepMoves.svelte +210 -188
  36. package/dist/puzzle-creation/StepPosition.svelte +35 -9
  37. package/dist/puzzle-creation/StepPreview.svelte +24 -11
  38. package/dist/puzzle-creation/StepPreview.svelte.d.ts +3 -1
  39. package/dist/puzzle-creation/StepTags.svelte +270 -0
  40. package/dist/puzzle-creation/StepTags.svelte.d.ts +19 -0
  41. package/dist/puzzle-creation/buildPuzzleWizardBoardSettings.d.ts +9 -0
  42. package/dist/puzzle-creation/buildPuzzleWizardBoardSettings.js +19 -0
  43. package/dist/puzzle-creation/createPuzzleLineEditingBoard.d.ts +10 -3
  44. package/dist/puzzle-creation/createPuzzleLineEditingBoard.js +14 -9
  45. package/dist/puzzle-creation/puzzleWizardState.d.ts +35 -0
  46. package/dist/puzzle-creation/puzzleWizardState.js +37 -0
  47. package/dist/puzzle-creation/types.d.ts +36 -10
  48. package/package.json +22 -17
@@ -1,5 +1,5 @@
1
1
  import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
2
- import { Color, PUZZLE_BRANCH_WRONG_NAG_ID } from "@connectorvol/shared";
2
+ import { Color, PUZZLE_BRANCH_WRONG_NAG_ID, calculatePly } from "@connectorvol/shared";
3
3
  /**
4
4
  * Представляет извлечение стороны, имеющей ход, из полной строки FEN.
5
5
  */
@@ -49,23 +49,83 @@ export function puzzlePreviewFindChildIndexForPlayedSan(cursorFen, children, pla
49
49
  /**
50
50
  * Представляет классификацию хода решателя на развилке (или на единственном продолжении).
51
51
  */
52
- export function puzzlePreviewClassifySolverMove(cursorNode, cursorFen, playedSan, solverColor) {
52
+ export function puzzlePreviewClassifySolverMove(cursorNode, cursorFen, playedSan, _solverColor) {
53
53
  const children = cursorNode.children;
54
54
  const idx = puzzlePreviewFindChildIndexForPlayedSan(cursorFen, children, playedSan);
55
55
  if (idx < 0) {
56
56
  return { kind: "no_matching_variant" };
57
57
  }
58
58
  const child = children[idx];
59
- const mover = puzzlePreviewSideToMoveFromFen(cursorFen);
60
- const isSolverFork = children.length > 1 && mover === solverColor;
61
- if (isSolverFork) {
62
- const nags = child.data.nags ?? [];
63
- if (nags.includes(PUZZLE_BRANCH_WRONG_NAG_ID)) {
64
- return { kind: "wrong_marked_variant", wrongBranchRoot: child };
65
- }
59
+ const nags = child.data.nags ?? [];
60
+ if (nags.includes(PUZZLE_BRANCH_WRONG_NAG_ID)) {
61
+ return { kind: "wrong_marked_variant", wrongBranchRoot: child };
66
62
  }
67
63
  return { kind: "correct", childIndex: idx };
68
64
  }
65
+ /**
66
+ * Представляет сбор данных узла для произвольного неверного хода решателя (нет в PGN).
67
+ */
68
+ export function puzzlePreviewBuildAdHocWrongMoveData(cursorFen, san) {
69
+ const probe = new PgnOps(cursorFen, "chess");
70
+ try {
71
+ const move = probe.makeSanMove(san);
72
+ const fen = probe.fen();
73
+ const { halfMoves, fullMoves } = calculatePly(fen);
74
+ return {
75
+ fen,
76
+ san,
77
+ ply: halfMoves,
78
+ fullMoves,
79
+ lastMove: move,
80
+ nags: [PUZZLE_BRANCH_WRONG_NAG_ID],
81
+ };
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /**
88
+ * Представляет добавление отсутствующего в PGN хода как новой ветки дерева решения с NAG ✗.
89
+ */
90
+ export function puzzlePreviewAddAdHocWrongMoveToSolutionTree(solutionTree, cursorPath, cursorFen, san) {
91
+ const data = puzzlePreviewBuildAdHocWrongMoveData(cursorFen, san);
92
+ if (!data)
93
+ return null;
94
+ const cursorNode = puzzlePreviewNodeAtPath(solutionTree.rootNode.moves, cursorPath);
95
+ if (!cursorNode)
96
+ return null;
97
+ solutionTree.currentNode = cursorNode;
98
+ solutionTree.addNodeToCurrent({
99
+ id: "",
100
+ children: [],
101
+ data,
102
+ });
103
+ return solutionTree.currentNode;
104
+ }
105
+ /**
106
+ * Представляет пересчёт пути индексов после перестановки детей на развилках.
107
+ */
108
+ export function puzzlePreviewRemapPathAfterReorder(rootMoves, path) {
109
+ const nodeIds = [];
110
+ let node = rootMoves;
111
+ for (const idx of path) {
112
+ const child = node.children[idx];
113
+ if (!child)
114
+ return path;
115
+ nodeIds.push(child.id);
116
+ node = child;
117
+ }
118
+ node = rootMoves;
119
+ const remapped = [];
120
+ for (const id of nodeIds) {
121
+ const idx = node.children.findIndex((c) => c.id === id);
122
+ if (idx < 0)
123
+ return path;
124
+ remapped.push(idx);
125
+ node = node.children[idx];
126
+ }
127
+ return remapped;
128
+ }
69
129
  /**
70
130
  * Представляет проверку: линия закончилась ключевым ходом решателя (ход соперника, лист дерева).
71
131
  */
@@ -0,0 +1,14 @@
1
+ import type { ChessTree, ChessTreeNode } from "@connectorvol/tree";
2
+ import { Color } from "@connectorvol/shared";
3
+ /**
4
+ * Представляет нормализацию дерева задачи: NAG ✓/✗ и порядок веток (верный ход — главная линия).
5
+ */
6
+ export declare function normalizePuzzleTreeMainLine(root: ChessTreeNode, solverColor: Color): boolean;
7
+ /**
8
+ * Представляет синхронизацию NAG ✓/✗ по дереву решения (устаревшее имя, вызывает нормализацию).
9
+ */
10
+ export declare function syncPuzzleBranchNags(root: ChessTreeNode, solverColor: Color): boolean;
11
+ /**
12
+ * Представляет нормализацию главной линии во всём дереве задачи и инкремент версии мутации при изменениях.
13
+ */
14
+ export declare function syncPuzzleBranchNagsOnTree(tree: ChessTree, solverColor: Color): void;
@@ -0,0 +1,125 @@
1
+ import { Color, PUZZLE_BRANCH_CORRECT_NAG_ID, PUZZLE_BRANCH_WRONG_NAG_ID, isPuzzleBranchNag, } from "@connectorvol/shared";
2
+ /**
3
+ * Представляет извлечение стороны, имеющей ход, из полной строки FEN.
4
+ */
5
+ function sideToMoveFromFullFen(fullFen) {
6
+ const token = fullFen.trim().split(/\s+/)[1];
7
+ return token === "b" ? Color.BLACK : Color.WHITE;
8
+ }
9
+ /**
10
+ * Представляет удаление NAG «верно / неверно» из списка аннотаций узла.
11
+ */
12
+ function stripPuzzleBranchNags(nags) {
13
+ return (nags ?? []).filter((n) => !isPuzzleBranchNag(n));
14
+ }
15
+ /**
16
+ * Представляет проверку: на узле стоит NAG верного хода задачи.
17
+ */
18
+ function childHasCorrectNag(child) {
19
+ return (child.data.nags ?? []).includes(PUZZLE_BRANCH_CORRECT_NAG_ID);
20
+ }
21
+ /**
22
+ * Представляет проверку: на узле стоит NAG неверного хода задачи.
23
+ */
24
+ function childHasWrongNag(child) {
25
+ return (child.data.nags ?? []).includes(PUZZLE_BRANCH_WRONG_NAG_ID);
26
+ }
27
+ /**
28
+ * Представляет выбор ребёнка для канонической главной линии на развилке.
29
+ */
30
+ function pickMainLineChild(parent, solverColor) {
31
+ const { children } = parent;
32
+ if (children.length === 0)
33
+ return null;
34
+ if (children.length === 1)
35
+ return children[0];
36
+ const mover = sideToMoveFromFullFen(parent.data.fen);
37
+ if (mover === solverColor) {
38
+ const correct = children.find(childHasCorrectNag);
39
+ if (correct)
40
+ return correct;
41
+ const notWrong = children.find((c) => !childHasWrongNag(c));
42
+ if (notWrong)
43
+ return notWrong;
44
+ }
45
+ else {
46
+ const notWrong = children.find((c) => !childHasWrongNag(c));
47
+ if (notWrong)
48
+ return notWrong;
49
+ }
50
+ return children[0];
51
+ }
52
+ /**
53
+ * Представляет удаление NAG ✓/✗ у ходов, которые не являются ходом решателя.
54
+ */
55
+ function cleanupInvalidPuzzleBranchNags(root, solverColor) {
56
+ let changed = false;
57
+ function walk(parent) {
58
+ const mover = sideToMoveFromFullFen(parent.data.fen);
59
+ for (const child of parent.children) {
60
+ const isSolverFork = mover === solverColor;
61
+ if (!isSolverFork) {
62
+ const before = child.data.nags ?? [];
63
+ if (before.some(isPuzzleBranchNag)) {
64
+ const next = stripPuzzleBranchNags(before);
65
+ child.data.nags = next.length > 0 ? next : undefined;
66
+ changed = true;
67
+ }
68
+ }
69
+ walk(child);
70
+ }
71
+ }
72
+ walk(root);
73
+ return changed;
74
+ }
75
+ /**
76
+ * Представляет поднятие канонической главной ветки в `children[0]` на всех развилках.
77
+ */
78
+ function reorderMainLineChildrenFirst(root, solverColor) {
79
+ let changed = false;
80
+ function walk(node) {
81
+ for (const child of node.children) {
82
+ walk(child);
83
+ }
84
+ if (node.children.length <= 1)
85
+ return;
86
+ const mainChild = pickMainLineChild(node, solverColor);
87
+ if (!mainChild)
88
+ return;
89
+ const idx = node.children.findIndex((c) => c.id === mainChild.id);
90
+ if (idx <= 0)
91
+ return;
92
+ const [moved] = node.children.splice(idx, 1);
93
+ node.children.unshift(moved);
94
+ changed = true;
95
+ }
96
+ walk(root);
97
+ return changed;
98
+ }
99
+ /**
100
+ * Представляет нормализацию дерева задачи: NAG ✓/✗ и порядок веток (верный ход — главная линия).
101
+ */
102
+ export function normalizePuzzleTreeMainLine(root, solverColor) {
103
+ let changed = false;
104
+ if (cleanupInvalidPuzzleBranchNags(root, solverColor)) {
105
+ changed = true;
106
+ }
107
+ if (reorderMainLineChildrenFirst(root, solverColor)) {
108
+ changed = true;
109
+ }
110
+ return changed;
111
+ }
112
+ /**
113
+ * Представляет синхронизацию NAG ✓/✗ по дереву решения (устаревшее имя, вызывает нормализацию).
114
+ */
115
+ export function syncPuzzleBranchNags(root, solverColor) {
116
+ return normalizePuzzleTreeMainLine(root, solverColor);
117
+ }
118
+ /**
119
+ * Представляет нормализацию главной линии во всём дереве задачи и инкремент версии мутации при изменениях.
120
+ */
121
+ export function syncPuzzleBranchNagsOnTree(tree, solverColor) {
122
+ if (normalizePuzzleTreeMainLine(tree.rootNode.moves, solverColor)) {
123
+ tree.mutationVersion++;
124
+ }
125
+ }
@@ -0,0 +1,81 @@
1
+ <script lang="ts">
2
+ import {
3
+ Chessboard,
4
+ CHESSBOARD_THEMES,
5
+ createBoardApi,
6
+ Draggable,
7
+ } from "@connectorvol/chessboard";
8
+
9
+ import { DEFAULT_BOARD_APPEARANCE_SETTINGS } from "../constants/default-board-appearance-settings.js";
10
+
11
+ interface Props {
12
+ /** Возвращает FEN позиции для превью. */
13
+ fen: string;
14
+ /** Возвращает подпись дебютного тега. */
15
+ label: string;
16
+ /** Возвращает якорь кнопки тега для позиционирования popover. */
17
+ anchor: DOMRect;
18
+ }
19
+
20
+ let { fen, label, anchor }: Props = $props();
21
+
22
+ const previewBoardSizePx = 220;
23
+ const previewGapPx = 8;
24
+ const viewportMarginPx = 12;
25
+
26
+ const previewDesign = {
27
+ ...DEFAULT_BOARD_APPEARANCE_SETTINGS.design,
28
+ showBoardCoordinates: false,
29
+ showSquareCoordinates: false,
30
+ markerConfig: null,
31
+ showPossibleMoveIndicators: false,
32
+ theme: CHESSBOARD_THEMES.blue,
33
+ };
34
+
35
+ const previewBoardApi = $derived.by(() =>
36
+ createBoardApi({
37
+ fen,
38
+ playSettings: {
39
+ ...DEFAULT_BOARD_APPEARANCE_SETTINGS.play,
40
+ boardSize: previewBoardSizePx * 10,
41
+ isResizable: false,
42
+ draggable: Draggable.NONE,
43
+ allowPreMove: false,
44
+ showSquareBadges: false,
45
+ animationTime: 0,
46
+ },
47
+ }),
48
+ );
49
+
50
+ const previewStyle = $derived.by(() => {
51
+ const belowTop = anchor.bottom + previewGapPx;
52
+ const aboveTop = anchor.top - previewBoardSizePx - previewGapPx;
53
+ const fitsBelow =
54
+ belowTop + previewBoardSizePx <= window.innerHeight - viewportMarginPx;
55
+ const top = fitsBelow
56
+ ? belowTop
57
+ : Math.max(viewportMarginPx, aboveTop);
58
+
59
+ const centeredLeft =
60
+ anchor.left + anchor.width / 2 - previewBoardSizePx / 2;
61
+ const maxLeft = Math.max(
62
+ viewportMarginPx,
63
+ window.innerWidth - previewBoardSizePx - viewportMarginPx,
64
+ );
65
+ const left = Math.min(
66
+ Math.max(centeredLeft, viewportMarginPx),
67
+ maxLeft,
68
+ );
69
+
70
+ return `left:${left}px;top:${top}px;width:${previewBoardSizePx}px;`;
71
+ });
72
+ </script>
73
+
74
+ <div
75
+ class="border-border pointer-events-none fixed z-50 overflow-hidden rounded-md border bg-white shadow-lg [&_*]:pointer-events-none"
76
+ style={previewStyle}
77
+ role="tooltip"
78
+ aria-label="Превью дебюта: {label}"
79
+ >
80
+ <Chessboard facade={previewBoardApi} design={previewDesign} />
81
+ </div>
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ /** Возвращает FEN позиции для превью. */
3
+ fen: string;
4
+ /** Возвращает подпись дебютного тега. */
5
+ label: string;
6
+ /** Возвращает якорь кнопки тега для позиционирования popover. */
7
+ anchor: DOMRect;
8
+ }
9
+ declare const OpeningTagHoverPreview: import("svelte").Component<Props, {}, "">;
10
+ type OpeningTagHoverPreview = ReturnType<typeof OpeningTagHoverPreview>;
11
+ export default OpeningTagHoverPreview;
@@ -1,112 +1,212 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from "svelte";
2
+ import type { Snippet } from "svelte";
3
+ import { MediaQuery } from "svelte/reactivity";
4
+ import { mode } from "mode-watcher";
3
5
 
4
- import type { BoardApi } from "@connectorvol/chessboard";
5
- import { Chessboard } from "@connectorvol/chessboard";
6
- import type { ChessTree } from "@connectorvol/tree";
7
- import { TreeViewer } from "@connectorvol/tree";
6
+ import type {
7
+ BoardApi,
8
+ TChessBoardDesignSettings,
9
+ } from "@connectorvol/chessboard";
10
+ import {
11
+ Chessboard,
12
+ getBoardDesignSettingsContext,
13
+ } from "@connectorvol/chessboard";
14
+ import type { ChessTree, TTreeViewerTheme } from "@connectorvol/tree";
15
+ import {
16
+ TREE_VIEWER_DARK_THEME,
17
+ TREE_VIEWER_LIGHT_THEME,
18
+ TreeViewer,
19
+ } from "@connectorvol/tree";
8
20
 
9
- import { cn } from "../utils.js";
21
+ import { cn } from "../utils.js";
10
22
 
11
- /**
12
- * Представляет свойства панели разметки «доска и просмотр дерева партии».
13
- */
14
- interface Props {
15
23
  /**
16
- * Возвращает API шахматной доски для компонента `Chessboard`.
24
+ * Представляет свойства панели разметки «доска и просмотр дерева партии».
17
25
  */
18
- chessboard: BoardApi;
19
- /**
20
- * Возвращает экземпляр дерева ходов для `TreeViewer`.
21
- */
22
- chessTree: ChessTree;
23
- /**
24
- * Возвращает обработчик выбора узла в дереве.
25
- */
26
- onSelectNode: () => void;
27
- /**
28
- * Возвращает обработчик после удаления варианта.
29
- */
30
- onDeleteVariant: () => void;
31
- /**
32
- * Возвращает функцию установки FEN в движке партии.
33
- */
34
- setChessFen: (fen: string) => void;
35
- /**
36
- * Возвращает функцию синхронизации FEN и UI доски.
37
- */
38
- setChessboardFen: (animationTime?: number) => void;
39
- /**
40
- * Возвращает признак режима правки дерева (`TreeViewer.editMode`).
41
- */
42
- editMode?: boolean;
43
- /**
44
- * Возвращает признак возможности выбора узлов в дереве.
45
- */
46
- selectable?: boolean;
47
- /**
48
- * Возвращает дополнительные классы Tailwind для корневого контейнера.
49
- */
50
- class?: string;
51
- /**
52
- * Возвращает фрагмент разметки под шахматной доской (например сообщение об ошибке в превью).
53
- */
54
- belowChessboard?: Snippet;
55
- }
56
-
57
- const {
58
- chessboard,
59
- chessTree,
60
- onSelectNode,
61
- onDeleteVariant,
62
- setChessFen,
63
- setChessboardFen,
64
- editMode = true,
65
- selectable = true,
66
- class: className,
67
- belowChessboard,
68
- }: Props = $props();
69
-
70
- const hasBoardCoordinatesWithoutBorder = $derived(
71
- chessboard.showBoardCoordinates && !chessboard.border,
72
- );
26
+ interface Props {
27
+ /**
28
+ * Возвращает API шахматной доски для компонента `Chessboard`.
29
+ */
30
+ chessboard: BoardApi;
31
+ /**
32
+ * Возвращает снимок дизайна, передаваемый в `<Chessboard design={...} />`.
33
+ * Опционально: если не передан, доска читает дизайн из контекста
34
+ * (`setBoardDesignSettingsContext`), что позволяет реактивно подхватывать
35
+ * изменения из `<BoardSettings />`.
36
+ */
37
+ chessboardDesign?: TChessBoardDesignSettings;
38
+ /**
39
+ * Возвращает экземпляр дерева ходов для `TreeViewer`.
40
+ */
41
+ chessTree: ChessTree;
42
+ /**
43
+ * Возвращает обработчик выбора узла в дереве.
44
+ */
45
+ onSelectNode: () => void;
46
+ /**
47
+ * Возвращает обработчик после удаления варианта.
48
+ */
49
+ onDeleteVariant: () => void;
50
+ /**
51
+ * Возвращает функцию установки FEN в движке партии.
52
+ */
53
+ setChessFen: (fen: string) => void;
54
+ /**
55
+ * Возвращает функцию синхронизации FEN и UI доски.
56
+ */
57
+ setChessboardFen: (animationTime?: number) => void;
58
+ /**
59
+ * Возвращает признак режима правки дерева (`TreeViewer.editMode`).
60
+ */
61
+ editMode?: boolean;
62
+ /**
63
+ * Возвращает признак возможности выбора узлов в дереве.
64
+ */
65
+ selectable?: boolean;
66
+ /**
67
+ * Возвращает true, если в режиме правки показывать группу NAG «Задача (развилка)»
68
+ * рядом с кнопками undo/redo на ходах из развилки.
69
+ */
70
+ showPuzzleBranchNags?: boolean;
71
+ /**
72
+ * Возвращает тему `TreeViewer` (фон, границы, текст). По умолчанию
73
+ * подстраивается под `mode-watcher` (dark → `TREE_VIEWER_DARK_THEME`,
74
+ * иначе `TREE_VIEWER_LIGHT_THEME`).
75
+ */
76
+ theme?: TTreeViewerTheme;
77
+ /**
78
+ * Возвращает дополнительные классы Tailwind для корневого контейнера.
79
+ */
80
+ class?: string;
81
+ /**
82
+ * Возвращает фрагмент разметки под шахматной доской (например сообщение об ошибке в превью).
83
+ */
84
+ belowChessboard?: Snippet;
85
+ }
86
+
87
+ const {
88
+ chessboard,
89
+ chessboardDesign,
90
+ chessTree,
91
+ onSelectNode,
92
+ onDeleteVariant,
93
+ setChessFen,
94
+ setChessboardFen,
95
+ editMode = true,
96
+ selectable = true,
97
+ showPuzzleBranchNags = false,
98
+ theme,
99
+ class: className,
100
+ belowChessboard,
101
+ }: Props = $props();
102
+
103
+ // ponytail: автотема по mode-watcher вместо жёсткого светлого дефолта,
104
+ // как в chess-tree/src/routes/game/+page.svelte. Явный проп `theme` побеждает.
105
+ const resolvedTheme = $derived<TTreeViewerTheme>(
106
+ theme ??
107
+ (mode.current === "dark"
108
+ ? TREE_VIEWER_DARK_THEME
109
+ : TREE_VIEWER_LIGHT_THEME),
110
+ );
111
+
112
+ /** ponytail: совпадает с Tailwind `lg` (1024px), как в разметке панели. */
113
+ const isMobile = new MediaQuery("(max-width: 1023px)");
114
+
115
+ const boardSize = $derived(chessboard.getBoard().settingsService.boardSize);
116
+ const useAutoBoardLayout = $derived(
117
+ isMobile.current || boardSize === "auto",
118
+ );
119
+
120
+ const boardColumnStyle = $derived(
121
+ !useAutoBoardLayout && typeof boardSize === "number"
122
+ ? `flex: 0 0 min(100%, ${boardSize}rem);`
123
+ : undefined,
124
+ );
125
+
126
+ let savedDesktopBoardSettings = $state<{
127
+ boardSize: number | "auto";
128
+ isResizable: boolean;
129
+ } | null>(null);
130
+
131
+ $effect(() => {
132
+ const mobile = isMobile.current;
133
+ const settings = chessboard.getBoard().settingsService;
134
+
135
+ if (mobile) {
136
+ if (savedDesktopBoardSettings === null) {
137
+ savedDesktopBoardSettings = {
138
+ boardSize: settings.boardSize,
139
+ isResizable: chessboard.isResizable,
140
+ };
141
+ }
142
+ chessboard.isResizable = false;
143
+ settings.boardSize = "auto";
144
+ } else if (savedDesktopBoardSettings !== null) {
145
+ chessboard.isResizable = savedDesktopBoardSettings.isResizable;
146
+ settings.boardSize = savedDesktopBoardSettings.boardSize;
147
+ savedDesktopBoardSettings = null;
148
+ }
149
+ });
150
+
151
+ let boardHeight = $state(0);
152
+
153
+ const treeHeightLocked = $derived(boardHeight > 0);
154
+
155
+ const treeColumnStyle = $derived(
156
+ treeHeightLocked ? `--pane-board-height: ${boardHeight}px` : undefined,
157
+ );
73
158
  </script>
74
159
 
75
- <div
76
- class={cn(
77
- "relative flex min-h-0 flex-col justify-start lg:flex-row lg:items-stretch lg:justify-start lg:gap-4",
78
- className,
79
- )}
80
- >
81
- <div
82
- class="order-2 flex min-h-48 w-full flex-1 flex-col gap-2 lg:order-2 lg:min-h-0 lg:min-w-0"
83
- >
160
+ <div class={cn("flex w-full flex-col gap-2", className)}>
84
161
  <div
85
- class={cn(
86
- "flex min-h-28 min-w-0 flex-1 flex-col overflow-hidden rounded-md border border-border lg:min-h-0",
87
- hasBoardCoordinatesWithoutBorder && "mb-4",
88
- )}
162
+ class="relative flex min-h-0 flex-col gap-4 lg:flex-row lg:items-stretch"
89
163
  >
90
- <TreeViewer
91
- chessTree={chessTree}
92
- {onSelectNode}
93
- {onDeleteVariant}
94
- {setChessFen}
95
- {setChessboardFen}
96
- pieceSet={chessboard.chessSet}
97
- className="min-h-0 flex-1 border-x-0 border-t-0"
98
- {editMode}
99
- {selectable}
100
- />
101
- </div>
102
- </div>
164
+ <div
165
+ class={cn(
166
+ "order-1 flex w-full shrink-0 flex-col lg:order-1 lg:min-h-0",
167
+ useAutoBoardLayout && "lg:max-w-[min(100%,38rem)]",
168
+ )}
169
+ style={boardColumnStyle}
170
+ >
171
+ <div
172
+ class="aspect-square w-full shrink-0"
173
+ bind:clientHeight={boardHeight}
174
+ >
175
+ <Chessboard
176
+ facade={chessboard}
177
+ design={chessboardDesign ??
178
+ getBoardDesignSettingsContext() ??
179
+ undefined}
180
+ />
181
+ </div>
182
+ </div>
103
183
 
104
- <div
105
- class="order-1 flex w-full max-w-[38rem] shrink-0 flex-col gap-2 lg:order-1 lg:min-h-0"
106
- >
107
- <div class="w-full shrink-0">
108
- <Chessboard facade={chessboard} />
184
+ <div
185
+ class={cn(
186
+ "order-2 flex min-h-48 min-w-0 flex-1 flex-col lg:order-2 lg:min-w-[20rem] lg:shrink-0",
187
+ treeHeightLocked &&
188
+ "lg:h-[var(--pane-board-height)] lg:max-h-[var(--pane-board-height)] lg:min-h-0 lg:overflow-hidden",
189
+ )}
190
+ style={treeColumnStyle}
191
+ >
192
+ <div
193
+ class="flex min-h-0 flex-1 flex-col overflow-hidden border border-border"
194
+ >
195
+ <TreeViewer
196
+ {chessTree}
197
+ {onSelectNode}
198
+ {onDeleteVariant}
199
+ {setChessFen}
200
+ {setChessboardFen}
201
+ className="min-h-0 flex-1 rounded-none border-0 bg-[var(--tree-viewer-bg)]"
202
+ {editMode}
203
+ {selectable}
204
+ {showPuzzleBranchNags}
205
+ theme={resolvedTheme}
206
+ />
207
+ </div>
208
+ </div>
109
209
  </div>
210
+
110
211
  {@render belowChessboard?.()}
111
- </div>
112
212
  </div>