@astronox/core 0.5.8 → 0.5.10
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.
- package/package.json +1 -1
- package/src/client/scroll.ts +14 -1
- package/src/components/data-display/IconList/IconList.astro +20 -2
- package/src/components/data-display/MetricTile/MetricTileContent.astro +6 -1
- package/src/components/feedback/Tooltip/Tooltip.css +9 -1
- package/src/components/feedback/Tooltip/tooltipController.ts +12 -9
- package/src/components/media/EmbedFrame/EmbedFrame.astro +9 -0
- package/src/components/media/EmbedFrame/EmbedFrame.css +5 -0
- package/src/components/media/EmbedFrame/embedFrameController.ts +84 -1
- package/src/display/contracts.ts +1 -0
- package/src/patterns/CircuitPuzzle/CircuitPuzzle.astro +25 -2
- package/src/patterns/CircuitPuzzle/CircuitPuzzle.css +24 -0
- package/src/patterns/CircuitPuzzle/circuitPuzzleController.ts +49 -0
- package/src/patterns/LightMazePuzzle/LightMazePuzzle.astro +1 -0
- package/src/patterns/LightMazePuzzle/LightMazePuzzle.css +9 -3
- package/src/patterns/LightMazePuzzle/lightMazePuzzleController.ts +43 -56
- package/src/patterns/MagneticFieldPuzzle/MagneticFieldPuzzle.astro +65 -3
- package/src/patterns/MagneticFieldPuzzle/MagneticFieldPuzzle.css +69 -0
- package/src/patterns/MagneticFieldPuzzle/contracts.ts +4 -0
- package/src/patterns/MagneticFieldPuzzle/magneticFieldPuzzleController.ts +50 -64
- package/src/patterns/PuzzleGrid/PuzzleGrid.astro +2 -0
- package/src/patterns/PuzzleGrid/keyboard.ts +220 -0
- package/src/patterns/PuzzleGrid/movement.css +4 -0
- package/src/patterns/PuzzleGrid/movement.ts +67 -3
- package/src/patterns/PuzzleGrid/selectionDismissal.ts +2 -2
- package/src/patterns/SlidingPuzzle/SlidingPuzzle.astro +9 -1
- package/src/patterns/SlidingPuzzle/SlidingPuzzle.css +13 -7
- package/src/patterns/SlidingPuzzle/slidingPuzzleController.ts +71 -74
- package/src/patterns/Table/TableCell.astro +1 -1
- package/src/patterns/Table/TableCellValue.astro +6 -1
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
animatePieceMovement,
|
|
3
3
|
cancelDragFrame,
|
|
4
|
+
clearPuzzleMovementPreview,
|
|
4
5
|
findDirectionalDropDestination,
|
|
6
|
+
getPuzzleMoveTarget,
|
|
5
7
|
hasLeftOriginCell,
|
|
6
8
|
renderPathStream,
|
|
7
9
|
scheduleDragFrame,
|
|
8
10
|
type PuzzleDrag,
|
|
9
11
|
} from '../PuzzleGrid/movement';
|
|
10
12
|
import { initPuzzleSelectionDismissal } from '../PuzzleGrid/selectionDismissal';
|
|
13
|
+
import {
|
|
14
|
+
initPuzzleKeyboard,
|
|
15
|
+
type PuzzleKeyboard,
|
|
16
|
+
} from '../PuzzleGrid/keyboard';
|
|
11
17
|
import { presentLightMazeCell } from './presentation';
|
|
12
18
|
import {
|
|
13
19
|
setPuzzleGridCellState,
|
|
@@ -48,15 +54,10 @@ type Instance = {
|
|
|
48
54
|
clickTimer: number | null;
|
|
49
55
|
controller: AbortController;
|
|
50
56
|
dismiss: () => void;
|
|
57
|
+
keyboard?: PuzzleKeyboard;
|
|
51
58
|
};
|
|
52
59
|
|
|
53
60
|
const instances = new WeakMap<HTMLElement, Instance>();
|
|
54
|
-
const arrows: Record<string, LightMazeDirection> = {
|
|
55
|
-
ArrowUp: 'north',
|
|
56
|
-
ArrowRight: 'east',
|
|
57
|
-
ArrowDown: 'south',
|
|
58
|
-
ArrowLeft: 'west',
|
|
59
|
-
};
|
|
60
61
|
|
|
61
62
|
export function initLightMazePuzzle(root: HTMLElement): void {
|
|
62
63
|
if (instances.has(root)) return;
|
|
@@ -92,15 +93,14 @@ export function initLightMazePuzzle(root: HTMLElement): void {
|
|
|
92
93
|
instances.set(root, instance);
|
|
93
94
|
instance.dismiss = initPuzzleSelectionDismissal(root, {
|
|
94
95
|
dismiss: () => selectPiece(root, null),
|
|
95
|
-
isSelectionTarget: (target) => {
|
|
96
|
+
isSelectionTarget: (target, event) => {
|
|
96
97
|
const panel = active(instance);
|
|
97
98
|
return (
|
|
98
99
|
panel.element.dataset.puzzleStatus !== 'valid' &&
|
|
99
100
|
panel.element.contains(target) &&
|
|
100
101
|
Boolean(
|
|
101
|
-
target.closest(
|
|
102
|
-
|
|
103
|
-
),
|
|
102
|
+
target.closest('[data-light-maze-piece]') ||
|
|
103
|
+
getPuzzleMoveTarget(panel.element, event),
|
|
104
104
|
)
|
|
105
105
|
);
|
|
106
106
|
},
|
|
@@ -128,50 +128,14 @@ export function initLightMazePuzzle(root: HTMLElement): void {
|
|
|
128
128
|
selectPiece(root, instance.selected === id ? null : id);
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (destination && !destination.disabled) {
|
|
135
|
-
void move(
|
|
136
|
-
root,
|
|
137
|
-
destination.dataset.destinationPiece as LightMazePiece,
|
|
138
|
-
destination.dataset.destinationDirection as LightMazeDirection,
|
|
139
|
-
);
|
|
131
|
+
const movement = getPuzzleMoveTarget(active(instance).element, event);
|
|
132
|
+
if (movement) {
|
|
133
|
+
void move(root, movement.pieceId as LightMazePiece, movement.direction);
|
|
140
134
|
} else selectPiece(root, null);
|
|
141
135
|
},
|
|
142
136
|
options,
|
|
143
137
|
);
|
|
144
138
|
|
|
145
|
-
root.addEventListener(
|
|
146
|
-
'keydown',
|
|
147
|
-
(event) => {
|
|
148
|
-
if (instance.motion) {
|
|
149
|
-
if (arrows[event.key] || ['Enter', ' ', 'Escape'].includes(event.key))
|
|
150
|
-
event.preventDefault();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (event.altKey || event.ctrlKey || event.metaKey || instance.drag)
|
|
154
|
-
return;
|
|
155
|
-
if (event.key === 'Escape') {
|
|
156
|
-
selectPiece(root, null);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
const direction = arrows[event.key];
|
|
160
|
-
if (!direction) return;
|
|
161
|
-
const focused =
|
|
162
|
-
event.target instanceof Element
|
|
163
|
-
? event.target.closest<HTMLElement>('[data-light-maze-piece]')
|
|
164
|
-
: null;
|
|
165
|
-
const piece =
|
|
166
|
-
(focused?.dataset.lightMazePiece as LightMazePiece | undefined) ??
|
|
167
|
-
instance.selected;
|
|
168
|
-
if (!piece) return;
|
|
169
|
-
event.preventDefault();
|
|
170
|
-
void move(root, piece, direction);
|
|
171
|
-
},
|
|
172
|
-
options,
|
|
173
|
-
);
|
|
174
|
-
|
|
175
139
|
root.addEventListener(
|
|
176
140
|
'pointerdown',
|
|
177
141
|
(event) => {
|
|
@@ -264,6 +228,30 @@ export function initLightMazePuzzle(root: HTMLElement): void {
|
|
|
264
228
|
options,
|
|
265
229
|
);
|
|
266
230
|
render(root);
|
|
231
|
+
instance.keyboard = initPuzzleKeyboard(root, {
|
|
232
|
+
getPanel: () => active(instance).element,
|
|
233
|
+
getSelected: () =>
|
|
234
|
+
instance.selected
|
|
235
|
+
? active(instance).element.querySelector<HTMLButtonElement>(
|
|
236
|
+
`[data-light-maze-piece="${instance.selected}"]`,
|
|
237
|
+
)
|
|
238
|
+
: null,
|
|
239
|
+
select: (piece) => {
|
|
240
|
+
if (!piece) cancelDrag(instance);
|
|
241
|
+
selectPiece(
|
|
242
|
+
root,
|
|
243
|
+
(piece?.dataset.lightMazePiece as LightMazePiece | undefined) ?? null,
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
move: (piece, direction) => {
|
|
247
|
+
void move(
|
|
248
|
+
root,
|
|
249
|
+
piece.dataset.lightMazePiece as LightMazePiece,
|
|
250
|
+
direction,
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
isLocked: () => instance.motion !== null || instance.drag !== null,
|
|
254
|
+
});
|
|
267
255
|
}
|
|
268
256
|
|
|
269
257
|
export function selectLightMazePuzzle(root: HTMLElement, index: number): void {
|
|
@@ -302,6 +290,7 @@ export function resetLightMazePuzzle(root: HTMLElement): void {
|
|
|
302
290
|
piece.removeAttribute('data-piece-moved');
|
|
303
291
|
}
|
|
304
292
|
render(root);
|
|
293
|
+
instance.keyboard?.reset();
|
|
305
294
|
emitChange(root, panel);
|
|
306
295
|
}
|
|
307
296
|
|
|
@@ -339,6 +328,7 @@ export function destroyLightMazePuzzle(root: HTMLElement): void {
|
|
|
339
328
|
cancelMotion(instance);
|
|
340
329
|
instance.controller.abort();
|
|
341
330
|
instance.dismiss();
|
|
331
|
+
instance.keyboard?.destroy();
|
|
342
332
|
cancelDrag(instance);
|
|
343
333
|
clearClickTimer(instance);
|
|
344
334
|
for (const panel of instance.panels) {
|
|
@@ -360,7 +350,7 @@ function selectPiece(root: HTMLElement, piece: LightMazePiece | null): void {
|
|
|
360
350
|
const instance = instances.get(root);
|
|
361
351
|
if (
|
|
362
352
|
!instance ||
|
|
363
|
-
instance.motion ||
|
|
353
|
+
(instance.motion && piece !== null) ||
|
|
364
354
|
active(instance).element.dataset.puzzleStatus === 'valid'
|
|
365
355
|
)
|
|
366
356
|
return;
|
|
@@ -570,8 +560,8 @@ function renderSelection(root: HTMLElement): void {
|
|
|
570
560
|
if (movement.piece !== instance.selected) continue;
|
|
571
561
|
const destination = findCell(panel, movement.to);
|
|
572
562
|
if (!destination) continue;
|
|
573
|
-
renderPathStream(panel.board, movement);
|
|
574
|
-
destination.tabIndex =
|
|
563
|
+
renderPathStream(panel.board, movement, movement.piece);
|
|
564
|
+
destination.tabIndex = -1;
|
|
575
565
|
destination.setAttribute('data-light-maze-destination', '');
|
|
576
566
|
destination.setAttribute('data-puzzle-cell-interactive', '');
|
|
577
567
|
destination.dataset.destinationPiece = movement.piece;
|
|
@@ -587,10 +577,7 @@ function renderSelection(root: HTMLElement): void {
|
|
|
587
577
|
}
|
|
588
578
|
|
|
589
579
|
function clearPreviews(panel: Panel): void {
|
|
590
|
-
|
|
591
|
-
'[data-puzzle-path-stream]',
|
|
592
|
-
))
|
|
593
|
-
stream.remove();
|
|
580
|
+
clearPuzzleMovementPreview(panel.element);
|
|
594
581
|
for (const cell of panel.element.querySelectorAll<HTMLButtonElement>(
|
|
595
582
|
'[data-light-maze-destination]',
|
|
596
583
|
)) {
|
|
@@ -4,14 +4,17 @@ import MinusIcon from '../../components/icons/MinusIcon.astro';
|
|
|
4
4
|
import PlusIcon from '../../components/icons/PlusIcon.astro';
|
|
5
5
|
import SlashIcon from '../../components/icons/SlashIcon.astro';
|
|
6
6
|
import PuzzleGrid from '../PuzzleGrid/PuzzleGrid.astro';
|
|
7
|
+
import Media from '../../primitives/media/Media/Media.astro';
|
|
7
8
|
import type {
|
|
8
9
|
MagneticFieldPuzzleLabels,
|
|
9
10
|
MagneticFieldPuzzleModel,
|
|
11
|
+
MagneticFieldPuzzleMedia,
|
|
10
12
|
} from './contracts';
|
|
11
13
|
|
|
12
14
|
type Props = HTMLAttributes<'section'> & {
|
|
13
15
|
label: string;
|
|
14
16
|
labels: MagneticFieldPuzzleLabels;
|
|
17
|
+
media?: MagneticFieldPuzzleMedia;
|
|
15
18
|
puzzles: readonly MagneticFieldPuzzleModel[];
|
|
16
19
|
};
|
|
17
20
|
|
|
@@ -19,9 +22,16 @@ const {
|
|
|
19
22
|
class: className,
|
|
20
23
|
label,
|
|
21
24
|
labels,
|
|
25
|
+
media,
|
|
22
26
|
puzzles,
|
|
23
27
|
...attrs
|
|
24
28
|
}: Props = Astro.props;
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
media &&
|
|
32
|
+
puzzles.some((puzzle) => puzzle.pieces.some((piece) => !media[piece.id]))
|
|
33
|
+
)
|
|
34
|
+
throw new Error('Magnetic field puzzle media must cover every piece.');
|
|
25
35
|
---
|
|
26
36
|
|
|
27
37
|
<section
|
|
@@ -46,7 +56,53 @@ const {
|
|
|
46
56
|
data-puzzle-id={puzzle.id}
|
|
47
57
|
data-puzzle-status="incomplete"
|
|
48
58
|
hidden={puzzleIndex !== 0}
|
|
59
|
+
style={
|
|
60
|
+
media
|
|
61
|
+
? `--piece-selection-filter: url(#magnetic-outline-${encodeURIComponent(puzzle.id)}); --piece-hover-filter: url(#magnetic-hover-${encodeURIComponent(puzzle.id)});`
|
|
62
|
+
: undefined
|
|
63
|
+
}
|
|
49
64
|
>
|
|
65
|
+
{media && (
|
|
66
|
+
<svg
|
|
67
|
+
class="magnetic-field-puzzle__filters"
|
|
68
|
+
aria-hidden="true"
|
|
69
|
+
focusable="false"
|
|
70
|
+
>
|
|
71
|
+
<defs>
|
|
72
|
+
<filter
|
|
73
|
+
id={`magnetic-hover-${encodeURIComponent(puzzle.id)}`}
|
|
74
|
+
color-interpolation-filters="sRGB"
|
|
75
|
+
>
|
|
76
|
+
<feFlood
|
|
77
|
+
class="magnetic-field-puzzle__hover-color"
|
|
78
|
+
flood-opacity="0.1"
|
|
79
|
+
/>
|
|
80
|
+
<feComposite operator="in" in2="SourceAlpha" />
|
|
81
|
+
<feMerge>
|
|
82
|
+
<feMergeNode in="SourceGraphic" />
|
|
83
|
+
<feMergeNode />
|
|
84
|
+
</feMerge>
|
|
85
|
+
</filter>
|
|
86
|
+
<filter
|
|
87
|
+
id={`magnetic-outline-${encodeURIComponent(puzzle.id)}`}
|
|
88
|
+
x="-25%"
|
|
89
|
+
y="-25%"
|
|
90
|
+
width="150%"
|
|
91
|
+
height="150%"
|
|
92
|
+
color-interpolation-filters="sRGB"
|
|
93
|
+
>
|
|
94
|
+
<feMorphology in="SourceAlpha" operator="dilate" radius="2" />
|
|
95
|
+
<feGaussianBlur stdDeviation="0.5" result="outline" />
|
|
96
|
+
<feFlood class="magnetic-field-puzzle__ring-color" />
|
|
97
|
+
<feComposite operator="in" in2="outline" />
|
|
98
|
+
<feMerge>
|
|
99
|
+
<feMergeNode />
|
|
100
|
+
<feMergeNode in="SourceGraphic" />
|
|
101
|
+
</feMerge>
|
|
102
|
+
</filter>
|
|
103
|
+
</defs>
|
|
104
|
+
</svg>
|
|
105
|
+
)}
|
|
50
106
|
<PuzzleGrid
|
|
51
107
|
class="magnetic-field-puzzle__board"
|
|
52
108
|
label={puzzle.label}
|
|
@@ -158,6 +214,8 @@ const {
|
|
|
158
214
|
aria-label={`${piece.label} — ${labels.polarities[piece.polarity]}`}
|
|
159
215
|
aria-pressed="false"
|
|
160
216
|
data-field-piece={piece.id}
|
|
217
|
+
data-puzzle-control
|
|
218
|
+
data-field-piece-media={media ? '' : undefined}
|
|
161
219
|
data-piece-label={piece.label}
|
|
162
220
|
data-piece-short-label={piece.shortLabel}
|
|
163
221
|
data-piece-row={piece.row}
|
|
@@ -175,9 +233,13 @@ const {
|
|
|
175
233
|
class="magnetic-field-puzzle__piece-core"
|
|
176
234
|
aria-hidden="true"
|
|
177
235
|
>
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
|
|
236
|
+
{media ? (
|
|
237
|
+
<Media media={media[piece.id]!} size="fill" />
|
|
238
|
+
) : (
|
|
239
|
+
<span class="magnetic-field-puzzle__piece-iris">
|
|
240
|
+
<span class="magnetic-field-puzzle__piece-pupil" />
|
|
241
|
+
</span>
|
|
242
|
+
)}
|
|
181
243
|
<span class="magnetic-field-puzzle__piece-identity-region">
|
|
182
244
|
<span
|
|
183
245
|
class="magnetic-field-puzzle__identity"
|
|
@@ -19,6 +19,21 @@
|
|
|
19
19
|
display: none;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
.magnetic-field-puzzle__filters {
|
|
23
|
+
position: absolute;
|
|
24
|
+
inline-size: 0;
|
|
25
|
+
block-size: 0;
|
|
26
|
+
pointer-events: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.magnetic-field-puzzle__ring-color {
|
|
30
|
+
flood-color: var(--nox-source-tone-info);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.magnetic-field-puzzle__hover-color {
|
|
34
|
+
flood-color: var(--nox-tone-color);
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
.magnetic-field-puzzle__board {
|
|
23
38
|
isolation: isolate;
|
|
24
39
|
touch-action: manipulation;
|
|
@@ -287,6 +302,60 @@
|
|
|
287
302
|
);
|
|
288
303
|
}
|
|
289
304
|
|
|
305
|
+
.magnetic-field-puzzle__piece[data-field-piece-media] {
|
|
306
|
+
border: var(--nox-border-width) solid transparent;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.magnetic-field-puzzle__piece[data-field-piece-media]
|
|
310
|
+
.magnetic-field-puzzle__piece-core {
|
|
311
|
+
inline-size: 84%;
|
|
312
|
+
border: 0;
|
|
313
|
+
border-radius: 0;
|
|
314
|
+
background: none;
|
|
315
|
+
box-shadow: none;
|
|
316
|
+
container-type: inline-size;
|
|
317
|
+
pointer-events: none;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.magnetic-field-puzzle__piece[data-field-piece-media]
|
|
321
|
+
.magnetic-field-puzzle__piece-identity-region {
|
|
322
|
+
inset-block: 68% 14%;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.magnetic-field-puzzle__piece[data-field-piece-media]
|
|
326
|
+
.magnetic-field-puzzle__identity {
|
|
327
|
+
font-size: clamp(0.625rem, 22cqi, var(--nox-font-size-md));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.magnetic-field-puzzle__piece[data-field-piece-media]:is(
|
|
331
|
+
[data-piece-selected],
|
|
332
|
+
:focus-visible
|
|
333
|
+
)
|
|
334
|
+
.magnetic-field-puzzle__piece-core {
|
|
335
|
+
filter: var(--piece-selection-filter);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
@media (hover: hover) {
|
|
339
|
+
.magnetic-field-puzzle__piece[data-field-piece-media]:hover
|
|
340
|
+
.magnetic-field-puzzle__piece-core {
|
|
341
|
+
filter: var(--piece-hover-filter);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.magnetic-field-puzzle__piece[data-field-piece-media]:is(
|
|
345
|
+
[data-piece-selected],
|
|
346
|
+
:focus-visible
|
|
347
|
+
):hover
|
|
348
|
+
.magnetic-field-puzzle__piece-core {
|
|
349
|
+
filter: var(--piece-hover-filter) var(--piece-selection-filter);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@media (width < 34rem) {
|
|
354
|
+
.magnetic-field-puzzle__piece[data-field-piece-media] {
|
|
355
|
+
margin: 2px;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
290
359
|
@media (prefers-reduced-motion: no-preference) {
|
|
291
360
|
.magnetic-field-puzzle__piece[data-piece-moved] {
|
|
292
361
|
animation: magnetic-field-slide 260ms var(--nox-motion-easing-standard);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { DisplayMedia } from '../../display/contracts';
|
|
2
|
+
|
|
3
|
+
export type MagneticFieldPuzzleMedia = Readonly<Record<string, DisplayMedia>>;
|
|
4
|
+
|
|
1
5
|
export type MagneticFieldDirection = 'north' | 'east' | 'south' | 'west';
|
|
2
6
|
export type MagneticFieldPolarity = 'north' | 'south';
|
|
3
7
|
export type MagneticFieldPupilPosition = 'upper-end' | 'center' | 'lower-start';
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
animatePieceMovement,
|
|
3
3
|
cancelDragFrame,
|
|
4
|
+
clearPuzzleMovementPreview,
|
|
4
5
|
findDirectionalDropDestination,
|
|
6
|
+
getPuzzleMoveTarget,
|
|
5
7
|
hasLeftOriginCell,
|
|
6
8
|
renderPathStream,
|
|
7
9
|
scheduleDragFrame,
|
|
@@ -12,6 +14,10 @@ import {
|
|
|
12
14
|
setPuzzleGridState,
|
|
13
15
|
} from '../PuzzleGrid/state';
|
|
14
16
|
import { initPuzzleSelectionDismissal } from '../PuzzleGrid/selectionDismissal';
|
|
17
|
+
import {
|
|
18
|
+
initPuzzleKeyboard,
|
|
19
|
+
type PuzzleKeyboard,
|
|
20
|
+
} from '../PuzzleGrid/keyboard';
|
|
15
21
|
import type {
|
|
16
22
|
MagneticFieldDirection,
|
|
17
23
|
MagneticFieldPolarity,
|
|
@@ -33,6 +39,7 @@ type MagneticFieldInstance = {
|
|
|
33
39
|
selectedPieceId: string | null;
|
|
34
40
|
drag: PuzzleDrag | null;
|
|
35
41
|
ignoreNextClick: boolean;
|
|
42
|
+
keyboard?: PuzzleKeyboard;
|
|
36
43
|
};
|
|
37
44
|
|
|
38
45
|
const dragThreshold = 12;
|
|
@@ -48,19 +55,14 @@ export function initMagneticFieldPuzzle(root: HTMLElement): void {
|
|
|
48
55
|
});
|
|
49
56
|
initPuzzleSelectionDismissal(root, {
|
|
50
57
|
dismiss: () => selectPiece(root, null),
|
|
51
|
-
isSelectionTarget: (target) => {
|
|
58
|
+
isSelectionTarget: (target, event) => {
|
|
52
59
|
const panel = getActivePanel(root);
|
|
53
60
|
if (!panel || panel.dataset.puzzleStatus === 'valid') return false;
|
|
54
61
|
|
|
55
62
|
const piece = target.closest<HTMLElement>('[data-field-piece]');
|
|
56
63
|
if (piece && panel.contains(piece)) return true;
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
'[data-field-destination]',
|
|
60
|
-
);
|
|
61
|
-
return Boolean(
|
|
62
|
-
destination && panel.contains(destination) && !destination.disabled,
|
|
63
|
-
);
|
|
65
|
+
return Boolean(getPuzzleMoveTarget(panel, event));
|
|
64
66
|
},
|
|
65
67
|
});
|
|
66
68
|
|
|
@@ -79,14 +81,9 @@ export function initMagneticFieldPuzzle(root: HTMLElement): void {
|
|
|
79
81
|
return;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (destination && !destination.disabled) {
|
|
86
|
-
const pieceId = destination.dataset.destinationPiece;
|
|
87
|
-
const direction = destination.dataset.destinationDirection as
|
|
88
|
-
MagneticFieldDirection | undefined;
|
|
89
|
-
if (pieceId && direction) movePiece(root, pieceId, direction);
|
|
84
|
+
const movement = getPuzzleMoveTarget(getActivePanel(root), event);
|
|
85
|
+
if (movement) {
|
|
86
|
+
movePiece(root, movement.pieceId, movement.direction);
|
|
90
87
|
return;
|
|
91
88
|
}
|
|
92
89
|
|
|
@@ -102,12 +99,11 @@ export function initMagneticFieldPuzzle(root: HTMLElement): void {
|
|
|
102
99
|
const pieceId = piece?.dataset.fieldPiece;
|
|
103
100
|
if (!piece || !pieceId || !instance) return;
|
|
104
101
|
const panel = getActivePanel(root);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
: null;
|
|
102
|
+
if (!panel || panel.dataset.puzzleStatus === 'valid') return;
|
|
103
|
+
const originCell = findCell(panel, {
|
|
104
|
+
row: Number(piece.dataset.pieceRow),
|
|
105
|
+
column: Number(piece.dataset.pieceColumn),
|
|
106
|
+
});
|
|
111
107
|
if (!originCell) return;
|
|
112
108
|
const pieceBounds = piece.getBoundingClientRect();
|
|
113
109
|
|
|
@@ -155,24 +151,20 @@ export function initMagneticFieldPuzzle(root: HTMLElement): void {
|
|
|
155
151
|
finishDrag(root, event, false);
|
|
156
152
|
});
|
|
157
153
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
piece
|
|
171
|
-
|
|
172
|
-
undefined;
|
|
173
|
-
if (!pieceId) return;
|
|
174
|
-
event.preventDefault();
|
|
175
|
-
movePiece(root, pieceId, direction);
|
|
154
|
+
const instance = instances.get(root)!;
|
|
155
|
+
instance.keyboard = initPuzzleKeyboard(root, {
|
|
156
|
+
getPanel: () => getActivePanel(root),
|
|
157
|
+
getSelected: () =>
|
|
158
|
+
getActivePanel(root)?.querySelector<HTMLButtonElement>(
|
|
159
|
+
'[data-field-piece][data-piece-selected]',
|
|
160
|
+
) ?? null,
|
|
161
|
+
select: (piece) => {
|
|
162
|
+
if (!piece) cancelDrag(instance);
|
|
163
|
+
selectPiece(root, piece?.dataset.fieldPiece ?? null);
|
|
164
|
+
},
|
|
165
|
+
move: (piece, direction) =>
|
|
166
|
+
movePiece(root, piece.dataset.fieldPiece!, direction),
|
|
167
|
+
isLocked: () => instance.drag !== null,
|
|
176
168
|
});
|
|
177
169
|
}
|
|
178
170
|
|
|
@@ -286,11 +278,11 @@ function updateDestinations(root: HTMLElement): void {
|
|
|
286
278
|
for (const movement of getMagneticFieldMoves(puzzle, state, pieceId)) {
|
|
287
279
|
const directionLabel = readDirectionLabel(labels, movement.direction);
|
|
288
280
|
const board = panel.querySelector<HTMLElement>('[data-field-board]');
|
|
289
|
-
if (board) renderPathStream(board, movement);
|
|
281
|
+
if (board) renderPathStream(board, movement, pieceId);
|
|
290
282
|
|
|
291
283
|
const destination = findCell(panel, movement.to);
|
|
292
284
|
if (!destination) continue;
|
|
293
|
-
destination.
|
|
285
|
+
destination.tabIndex = -1;
|
|
294
286
|
destination.setAttribute('data-field-destination', '');
|
|
295
287
|
destination.setAttribute('data-puzzle-cell-interactive', '');
|
|
296
288
|
destination.dataset.destinationPiece = pieceId;
|
|
@@ -350,13 +342,15 @@ function initializeActivePuzzle(root: HTMLElement): void {
|
|
|
350
342
|
const initial = createMagneticFieldInitialState(puzzle);
|
|
351
343
|
|
|
352
344
|
instance.selectedPieceId = null;
|
|
353
|
-
instance
|
|
345
|
+
cancelDrag(instance);
|
|
354
346
|
instance.ignoreNextClick = false;
|
|
355
347
|
panel.dataset.puzzleStatus = 'incomplete';
|
|
356
348
|
const grid = panel.querySelector<HTMLElement>('[data-puzzle-grid]');
|
|
357
349
|
if (grid) setPuzzleGridState(grid, null);
|
|
358
350
|
renderState(panel, puzzle, initial);
|
|
351
|
+
syncPieceSelection(panel, null);
|
|
359
352
|
hideMovePreviews(panel);
|
|
353
|
+
instance.keyboard?.reset();
|
|
360
354
|
emitChange(root, puzzle.id, initial);
|
|
361
355
|
}
|
|
362
356
|
|
|
@@ -458,14 +452,7 @@ function finishDrag(
|
|
|
458
452
|
? findDirectionalDropDestination(drag, deltaX, deltaY)
|
|
459
453
|
: null;
|
|
460
454
|
|
|
461
|
-
|
|
462
|
-
drag.element.removeAttribute('data-piece-dragging');
|
|
463
|
-
drag.element.style.removeProperty('--piece-drag-x');
|
|
464
|
-
drag.element.style.removeProperty('--piece-drag-y');
|
|
465
|
-
if (drag.element.hasPointerCapture(event.pointerId)) {
|
|
466
|
-
drag.element.releasePointerCapture(event.pointerId);
|
|
467
|
-
}
|
|
468
|
-
instance.drag = null;
|
|
455
|
+
cancelDrag(instance);
|
|
469
456
|
|
|
470
457
|
if (commit && drag.moved) {
|
|
471
458
|
instance.ignoreNextClick = true;
|
|
@@ -492,12 +479,20 @@ function getDropDestinations(root: HTMLElement): HTMLButtonElement[] {
|
|
|
492
479
|
];
|
|
493
480
|
}
|
|
494
481
|
|
|
482
|
+
function cancelDrag(instance: MagneticFieldInstance): void {
|
|
483
|
+
const drag = instance.drag;
|
|
484
|
+
if (!drag) return;
|
|
485
|
+
instance.drag = null;
|
|
486
|
+
cancelDragFrame(drag);
|
|
487
|
+
drag.element.removeAttribute('data-piece-dragging');
|
|
488
|
+
drag.element.style.removeProperty('--piece-drag-x');
|
|
489
|
+
drag.element.style.removeProperty('--piece-drag-y');
|
|
490
|
+
if (drag.element.hasPointerCapture(drag.pointerId))
|
|
491
|
+
drag.element.releasePointerCapture(drag.pointerId);
|
|
492
|
+
}
|
|
493
|
+
|
|
495
494
|
function hideMovePreviews(panel: HTMLElement): void {
|
|
496
|
-
|
|
497
|
-
'[data-puzzle-path-stream]',
|
|
498
|
-
)) {
|
|
499
|
-
stream.remove();
|
|
500
|
-
}
|
|
495
|
+
clearPuzzleMovementPreview(panel);
|
|
501
496
|
|
|
502
497
|
for (const destination of panel.querySelectorAll<HTMLButtonElement>(
|
|
503
498
|
'[data-field-destination]',
|
|
@@ -657,12 +652,3 @@ function formatTemplate(
|
|
|
657
652
|
function capitalize(value: string): string {
|
|
658
653
|
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
659
654
|
}
|
|
660
|
-
|
|
661
|
-
const arrowDirections: Readonly<
|
|
662
|
-
Partial<Record<string, MagneticFieldDirection>>
|
|
663
|
-
> = {
|
|
664
|
-
arrowup: 'north',
|
|
665
|
-
arrowright: 'east',
|
|
666
|
-
arrowdown: 'south',
|
|
667
|
-
arrowleft: 'west',
|
|
668
|
-
};
|
|
@@ -16,6 +16,8 @@ const { class: className, label, rows, columns, ...attrs }: Props = Astro.props;
|
|
|
16
16
|
role="group"
|
|
17
17
|
aria-label={label}
|
|
18
18
|
data-puzzle-grid
|
|
19
|
+
data-puzzle-rows={rows}
|
|
20
|
+
data-puzzle-columns={columns}
|
|
19
21
|
style={`--puzzle-columns: ${columns}; --puzzle-rows: ${rows};`}
|
|
20
22
|
>
|
|
21
23
|
<slot />
|