@connectorvol/chess-widgets 1.1.0 → 1.2.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.
@@ -1,210 +1,216 @@
1
1
  <script lang="ts">
2
- import { untrack } from "svelte";
2
+ import {
3
+ Chessboard,
4
+ CHESSBOARD_THEMES,
5
+ createBoardApi,
6
+ type ChessboardTheme,
7
+ } from "@connectorvol/chessboard";
3
8
 
4
- import {
5
- Chessboard,
6
- CHESSBOARD_THEMES,
7
- createBoardApi,
8
- type ChessboardTheme,
9
- } from "@connectorvol/chessboard";
9
+ import { DEFAULT_EDITABLE_BOARD_SETTINGS } from "../constants/editable-board-settings.js";
10
+ import EditFen from "../position-editor/EditFen.svelte";
11
+ import EditMove from "../position-editor/EditMove.svelte";
12
+ import EditPanel from "../position-editor/EditPanel.svelte";
13
+ import { Fen } from "../position-editor/fen.svelte.js";
14
+ import { Popover } from "bits-ui";
15
+ import { buttonVariants } from "../button-variants.js";
16
+ import { cn } from "../utils.js";
17
+ import { Color } from "@connectorvol/shared";
18
+ import type { TChessboardAppearanceSettings } from "./types.js";
10
19
 
11
- import { DEFAULT_EDITABLE_BOARD_SETTINGS } from "../constants/editable-board-settings.js";
12
- import EditFen from "../position-editor/EditFen.svelte";
13
- import EditMove from "../position-editor/EditMove.svelte";
14
- import EditPanel from "../position-editor/EditPanel.svelte";
15
- import { Fen } from "../position-editor/fen.svelte.js";
16
- import { Popover } from "bits-ui";
17
- import { buttonVariants } from "../button-variants.js";
18
- import { cn } from "../utils.js";
19
- import { Color } from "@connectorvol/shared";
20
- import type { TChessboardAppearanceSettings } from "./types.js";
21
-
22
- interface Props {
23
- initialFen: string;
24
- onNext: (fen: string) => void;
25
- boardTheme?: ChessboardTheme;
26
- boardAppearanceSettings?: TChessboardAppearanceSettings;
27
- }
20
+ interface Props {
21
+ initialFen: string;
22
+ onNext: (fen: string) => void;
23
+ boardTheme?: ChessboardTheme;
24
+ boardAppearanceSettings?: TChessboardAppearanceSettings;
25
+ }
28
26
 
29
- const props: Props = $props();
27
+ const props: Props = $props();
30
28
 
31
- /** Настройки доски мастера задачи: фиксированный размер, без ручного resize. */
32
- const puzzleStepBoardSettings = $derived({
33
- ...DEFAULT_EDITABLE_BOARD_SETTINGS,
34
- ...(props.boardAppearanceSettings ?? {}),
35
- boardSize: 29,
36
- isResizable: false,
37
- editSettings: DEFAULT_EDITABLE_BOARD_SETTINGS.editSettings,
38
- });
29
+ /** Настройки доски мастера задачи: фиксированный размер, без ручного resize. */
30
+ const puzzleStepBoardSettings = $derived({
31
+ ...DEFAULT_EDITABLE_BOARD_SETTINGS,
32
+ ...(props.boardAppearanceSettings ?? {}),
33
+ boardSize: 29,
34
+ isResizable: false,
35
+ editSettings: DEFAULT_EDITABLE_BOARD_SETTINGS.editSettings,
36
+ });
39
37
 
40
- let chessboard = $derived(
41
- createBoardApi({
42
- fen: (() => props.initialFen)(),
43
- settings: puzzleStepBoardSettings,
44
- theme: props.boardTheme ?? CHESSBOARD_THEMES.blue,
45
- }),
46
- );
38
+ let chessboard = $derived(
39
+ createBoardApi({
40
+ fen: (() => props.initialFen)(),
41
+ settings: puzzleStepBoardSettings,
42
+ theme: props.boardTheme ?? CHESSBOARD_THEMES.blue,
43
+ }),
44
+ );
47
45
 
48
- /** Один объект Fen на доску: `$derived(new Fen(...))` пересоздавал бы класс при смене orientation и сбрасывал бы «Установить ход». */
49
- const fen = $derived(new Fen(chessboard));
46
+ /** Один объект Fen на доску: создаём сразу с нужной начальной строкой. */
47
+ const fen = $derived.by(() => {
48
+ const f = new Fen(chessboard);
49
+ try {
50
+ f.fullFen = props.initialFen;
51
+ } catch {
52
+ /* некорректная строка FEN — оставляем текущее состояние Fen */
53
+ }
54
+ return f;
55
+ });
50
56
 
51
- /**
52
- * Представляет подстановку полей FEN (ход, рокировка, счётчики) из строки родителя при смене стартовой позиции.
53
- */
54
- $effect(() => {
55
- const src = props.initialFen;
56
- untrack(() => {
57
- try {
58
- fen.fullFen = src;
59
- } catch {
60
- /* некорректная строка FEN — оставляем текущее состояние Fen */
61
- }
57
+ $effect(() => {
58
+ const sideToMove = fen.move;
59
+ const targetOrientation =
60
+ sideToMove === "w" ? Color.WHITE : Color.BLACK;
61
+ if (chessboard.orientation !== targetOrientation) {
62
+ chessboard.orientation = targetOrientation;
63
+ }
62
64
  });
63
- });
64
65
 
65
- $effect(() => {
66
- const sideToMove = fen.move;
67
- const targetOrientation = sideToMove === "w" ? Color.WHITE : Color.BLACK;
68
- if (chessboard.orientation !== targetOrientation) {
69
- chessboard.orientation = targetOrientation;
66
+ function handleNext() {
67
+ props.onNext(fen.fullFen);
70
68
  }
71
- });
72
-
73
- function handleNext() {
74
- props.onNext(fen.fullFen);
75
- }
76
69
  </script>
77
70
 
78
71
  <div class="flex flex-col gap-3 pt-2 pb-0">
79
- <div class="flex w-full flex-wrap items-center gap-2">
80
- <h2 class="text-xl font-semibold">Шаг 1: Создание позиции</h2>
81
- <Popover.Root>
82
- <Popover.Trigger type="button">
83
- {#snippet child({ props })}
84
- <button
85
- {...props}
86
- type="button"
87
- class={cn(
88
- buttonVariants({ variant: "outline", size: "icon-sm" }),
89
- "size-7 shrink-0 rounded-full text-sm font-semibold",
90
- )}
91
- aria-label="Справка: создание стартовой позиции"
92
- >
93
- ?
94
- </button>
95
- {/snippet}
96
- </Popover.Trigger>
97
- <Popover.Portal>
98
- <Popover.Content
99
- side="bottom"
100
- align="start"
101
- sideOffset={8}
102
- class={cn(
103
- "bg-popover text-popover-foreground border-border z-50 max-h-[min(70vh,32rem)] w-[min(calc(100vw-2rem),28rem)] overflow-y-auto rounded-lg border p-4 text-sm shadow-md outline-none",
104
- "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
105
- "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
106
- )}
107
- >
108
- <p class="text-muted-foreground leading-relaxed">
109
- Установите начальную позицию, с которой ученик будет искать решение.
110
- В поле «Установить ход» доска поворачивается так, что ходящая
111
- сторона оказывается у нижнего края, а палитры фигур меняются
112
- местами.
113
- </p>
114
- </Popover.Content>
115
- </Popover.Portal>
116
- </Popover.Root>
117
- </div>
72
+ <div class="flex w-full flex-wrap items-center gap-2">
73
+ <h2 class="text-xl font-semibold">Шаг 1: Создание позиции</h2>
74
+ <Popover.Root>
75
+ <Popover.Trigger type="button">
76
+ {#snippet child({ props })}
77
+ <button
78
+ {...props}
79
+ type="button"
80
+ class={cn(
81
+ buttonVariants({
82
+ variant: "outline",
83
+ size: "icon-sm",
84
+ }),
85
+ "size-7 shrink-0 rounded-full text-sm font-semibold",
86
+ )}
87
+ aria-label="Справка: создание стартовой позиции"
88
+ >
89
+ ?
90
+ </button>
91
+ {/snippet}
92
+ </Popover.Trigger>
93
+ <Popover.Portal>
94
+ <Popover.Content
95
+ side="bottom"
96
+ align="start"
97
+ sideOffset={8}
98
+ class={cn(
99
+ "bg-popover text-popover-foreground border-border z-50 max-h-[min(70vh,32rem)] w-[min(calc(100vw-2rem),28rem)] overflow-y-auto rounded-lg border p-4 text-sm shadow-md outline-none",
100
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
101
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
102
+ )}
103
+ >
104
+ <p class="text-muted-foreground leading-relaxed">
105
+ Установите начальную позицию, с которой ученик будет
106
+ искать решение. В поле «Установить ход» доска
107
+ поворачивается так, что ходящая сторона оказывается у
108
+ нижнего края, а палитры фигур меняются местами.
109
+ </p>
110
+ </Popover.Content>
111
+ </Popover.Portal>
112
+ </Popover.Root>
113
+ </div>
118
114
 
119
- <div class="flex flex-col md:flex-row gap-3">
120
- <div class="flex w-full max-w-full flex-col justify-center gap-2">
121
- {#snippet boardRow()}
122
- <div
123
- class="flex w-full max-w-full flex-col gap-3 md:flex-row md:items-start items-center"
124
- >
125
- <!-- Явная ширина: при родителе с w-fit/min-content BoardContainer даёт min(100%, Nrem), и 100% может схлопнуться до нуля. -->
126
- <div
127
- class="shrink-0 max-w-full"
128
- style="width: {puzzleStepBoardSettings.boardSize}rem; max-width: 100%;"
129
- >
130
- <Chessboard facade={chessboard} />
131
- </div>
132
- <div
133
- class="hidden min-w-0 flex-1 flex-col gap-3 md:flex md:min-w-[280px]"
134
- >
135
- <EditFen {fen} api={chessboard} constrainToBoardWidth={false} />
136
- <div class="space-y-2">
137
- <label
138
- for="puzzle-start-fullmove-md"
139
- class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
140
- >
141
- Номер хода в записи партии
142
- </label>
143
- <input
144
- id="puzzle-start-fullmove-md"
145
- type="number"
146
- min="1"
147
- class={cn(
148
- "border-input bg-background ring-offset-background shadow-xs flex h-9 w-full rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] md:text-sm",
149
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
150
- )}
151
- value={fen.fullmove}
152
- oninput={(e) => {
153
- const n = Number.parseInt(e.currentTarget.value, 10);
154
- if (!Number.isFinite(n) || n < 1) return;
155
- fen.fullmove = n;
156
- }}
157
- />
115
+ <div class="flex flex-col md:flex-row gap-3">
116
+ <div class="flex w-full max-w-full flex-col justify-center gap-2">
117
+ {#snippet boardRow()}
118
+ <div
119
+ class="flex w-full max-w-full flex-col gap-3 md:flex-row md:items-start items-center"
120
+ >
121
+ <!-- Явная ширина: при родителе с w-fit/min-content BoardContainer даёт min(100%, Nrem), и 100% может схлопнуться до нуля. -->
122
+ <div
123
+ class="shrink-0 max-w-full"
124
+ style="width: {puzzleStepBoardSettings.boardSize}rem; max-width: 100%;"
125
+ >
126
+ <Chessboard facade={chessboard} />
127
+ </div>
128
+ <div
129
+ class="hidden min-w-0 flex-1 flex-col gap-3 md:flex md:min-w-[280px]"
130
+ >
131
+ <EditFen
132
+ {fen}
133
+ api={chessboard}
134
+ constrainToBoardWidth={false}
135
+ />
136
+ <div class="space-y-2">
137
+ <label
138
+ for="puzzle-start-fullmove-md"
139
+ class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
140
+ >
141
+ Номер хода в записи партии
142
+ </label>
143
+ <input
144
+ id="puzzle-start-fullmove-md"
145
+ type="number"
146
+ min="1"
147
+ class={cn(
148
+ "border-input bg-background ring-offset-background shadow-xs flex h-9 w-full rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] md:text-sm",
149
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
150
+ )}
151
+ value={fen.fullmove}
152
+ oninput={(e) => {
153
+ const n = Number.parseInt(
154
+ e.currentTarget.value,
155
+ 10,
156
+ );
157
+ if (!Number.isFinite(n) || n < 1) return;
158
+ fen.fullmove = n;
159
+ }}
160
+ />
161
+ </div>
162
+ <EditMove {fen} api={chessboard} />
163
+ </div>
164
+ </div>
165
+ {/snippet}
166
+ <!-- Без {#if} по fen.move: иначе при смене стороны размонтируется boardRow/EditMove и bind у select ломается до следующего взаимодействия. -->
167
+ <div class={fen.move === "w" ? "order-1" : "order-3"}>
168
+ <EditPanel api={chessboard} color="b" />
169
+ </div>
170
+ <div class="order-2">{@render boardRow()}</div>
171
+ <div class={fen.move === "w" ? "order-3" : "order-1"}>
172
+ <EditPanel api={chessboard} color="w" />
173
+ </div>
174
+ <div class="md:hidden flex w-full flex-col gap-3">
175
+ <EditFen {fen} api={chessboard} constrainToBoardWidth={false} />
176
+ <div class="space-y-2">
177
+ <label
178
+ for="puzzle-start-fullmove-mobile"
179
+ class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
180
+ >
181
+ Номер хода в записи партии
182
+ </label>
183
+ <input
184
+ id="puzzle-start-fullmove-mobile"
185
+ type="number"
186
+ min="1"
187
+ class={cn(
188
+ "border-input bg-background ring-offset-background shadow-xs flex h-9 w-full rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] md:text-sm",
189
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
190
+ )}
191
+ value={fen.fullmove}
192
+ oninput={(e) => {
193
+ const n = Number.parseInt(
194
+ e.currentTarget.value,
195
+ 10,
196
+ );
197
+ if (!Number.isFinite(n) || n < 1) return;
198
+ fen.fullmove = n;
199
+ }}
200
+ />
201
+ </div>
202
+ <EditMove {fen} api={chessboard} />
158
203
  </div>
159
- <EditMove {fen} api={chessboard} />
160
- </div>
161
- </div>
162
- {/snippet}
163
- <!-- Без {#if} по fen.move: иначе при смене стороны размонтируется boardRow/EditMove и bind у select ломается до следующего взаимодействия. -->
164
- <div class={fen.move === "w" ? "order-1" : "order-3"}>
165
- <EditPanel api={chessboard} color="b" />
166
- </div>
167
- <div class="order-2">{@render boardRow()}</div>
168
- <div class={fen.move === "w" ? "order-3" : "order-1"}>
169
- <EditPanel api={chessboard} color="w" />
170
- </div>
171
- <div class="md:hidden flex w-full flex-col gap-3">
172
- <EditFen {fen} api={chessboard} constrainToBoardWidth={false} />
173
- <div class="space-y-2">
174
- <label
175
- for="puzzle-start-fullmove-mobile"
176
- class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
177
- >
178
- Номер хода в записи партии
179
- </label>
180
- <input
181
- id="puzzle-start-fullmove-mobile"
182
- type="number"
183
- min="1"
184
- class={cn(
185
- "border-input bg-background ring-offset-background shadow-xs flex h-9 w-full rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] md:text-sm",
186
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
187
- )}
188
- value={fen.fullmove}
189
- oninput={(e) => {
190
- const n = Number.parseInt(e.currentTarget.value, 10);
191
- if (!Number.isFinite(n) || n < 1) return;
192
- fen.fullmove = n;
193
- }}
194
- />
195
204
  </div>
196
- <EditMove {fen} api={chessboard} />
197
- </div>
198
205
  </div>
199
- </div>
200
206
 
201
- <div class="flex justify-end">
202
- <button
203
- type="button"
204
- class={cn(buttonVariants({ variant: "default" }))}
205
- onclick={handleNext}
206
- >
207
- Далее
208
- </button>
209
- </div>
207
+ <div class="flex justify-end">
208
+ <button
209
+ type="button"
210
+ class={cn(buttonVariants({ variant: "default" }))}
211
+ onclick={handleNext}
212
+ >
213
+ Далее
214
+ </button>
215
+ </div>
210
216
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@connectorvol/chess-widgets",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -37,21 +37,25 @@
37
37
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
38
38
  "format": "oxfmt --write . && prettier --write \"**/*.svelte\"",
39
39
  "prettier": "prettier --write \"**/*.svelte\"",
40
- "test": ""
40
+ "test": "",
41
+ "test:e2e": "node node_modules/@playwright/test/cli.js test",
42
+ "test:e2e:ui": "node node_modules/@playwright/test/cli.js test --ui"
41
43
  },
42
44
  "dependencies": {
43
- "@connectorvol/chessboard": "4.0.0",
44
- "@connectorvol/chessops": "3.0.0",
45
- "@connectorvol/shared": "4.0.0",
46
- "@connectorvol/tree": "4.0.0",
45
+ "@connectorvol/chessboard": "4.1.0",
46
+ "@connectorvol/chessops": "3.1.0",
47
+ "@connectorvol/shared": "4.1.0",
48
+ "@connectorvol/tree": "4.1.0",
49
+ "bits-ui": "2.16.4",
47
50
  "clsx": "^2.1.1",
51
+ "svelte-toolbelt": "^0.10.6",
48
52
  "tailwind-merge": "3.3.1",
49
53
  "tailwind-variants": "3.1.1"
50
54
  },
51
55
  "devDependencies": {
52
56
  "@ianvs/prettier-plugin-sort-imports": "4.5.1",
57
+ "@playwright/test": "1.54.1",
53
58
  "@lucide/svelte": "^0.553.0",
54
- "bits-ui": "2.16.4",
55
59
  "@sveltejs/adapter-static": "3.0.10",
56
60
  "@sveltejs/kit": "2.48.0",
57
61
  "@sveltejs/package": "2.4.0",
@@ -71,7 +75,6 @@
71
75
  },
72
76
  "peerDependencies": {
73
77
  "@lucide/svelte": "^0.553.0",
74
- "bits-ui": "2.16.4",
75
78
  "svelte": "^5.53.12"
76
79
  }
77
80
  }