@astronox/core 0.5.9 → 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 +3 -0
- package/src/patterns/CircuitPuzzle/circuitPuzzleController.ts +49 -0
- package/src/patterns/LightMazePuzzle/LightMazePuzzle.astro +1 -0
- package/src/patterns/LightMazePuzzle/lightMazePuzzleController.ts +43 -56
- package/src/patterns/MagneticFieldPuzzle/MagneticFieldPuzzle.astro +1 -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 +1 -0
- package/src/patterns/SlidingPuzzle/slidingPuzzleController.ts +71 -74
- package/src/patterns/Table/TableCell.astro +1 -1
- package/src/patterns/Table/TableCellValue.astro +6 -1
|
@@ -3,10 +3,16 @@ import {
|
|
|
3
3
|
setPuzzleGridState,
|
|
4
4
|
} from '../PuzzleGrid/state';
|
|
5
5
|
import { initPuzzleSelectionDismissal } from '../PuzzleGrid/selectionDismissal';
|
|
6
|
+
import {
|
|
7
|
+
initPuzzleKeyboard,
|
|
8
|
+
type PuzzleKeyboard,
|
|
9
|
+
} from '../PuzzleGrid/keyboard';
|
|
6
10
|
import {
|
|
7
11
|
animatePieceMovement,
|
|
8
12
|
cancelDragFrame,
|
|
13
|
+
clearPuzzleMovementPreview,
|
|
9
14
|
findDirectionalDropDestination,
|
|
15
|
+
getPuzzleMoveTarget,
|
|
10
16
|
hasLeftOriginCell,
|
|
11
17
|
renderPathStream,
|
|
12
18
|
scheduleDragFrame,
|
|
@@ -35,19 +41,17 @@ type Instance = {
|
|
|
35
41
|
selected: boolean;
|
|
36
42
|
drag: PuzzleDrag | null;
|
|
37
43
|
terrainFrame: number | null;
|
|
38
|
-
motion:
|
|
44
|
+
motion: {
|
|
45
|
+
controller: AbortController;
|
|
46
|
+
blocking: boolean;
|
|
47
|
+
} | null;
|
|
39
48
|
ignoreNextClick: boolean;
|
|
40
49
|
abort: AbortController;
|
|
41
50
|
dismiss: () => void;
|
|
51
|
+
keyboard?: PuzzleKeyboard;
|
|
42
52
|
};
|
|
43
53
|
|
|
44
54
|
const instances = new WeakMap<HTMLElement, Instance>();
|
|
45
|
-
const directions: Readonly<Partial<Record<string, SlidingDirection>>> = {
|
|
46
|
-
ArrowUp: 'north',
|
|
47
|
-
ArrowRight: 'east',
|
|
48
|
-
ArrowDown: 'south',
|
|
49
|
-
ArrowLeft: 'west',
|
|
50
|
-
};
|
|
51
55
|
|
|
52
56
|
export function initSlidingPuzzle(root: HTMLElement): void {
|
|
53
57
|
if (instances.has(root)) return;
|
|
@@ -68,13 +72,14 @@ export function initSlidingPuzzle(root: HTMLElement): void {
|
|
|
68
72
|
const options = { signal: instance.abort.signal };
|
|
69
73
|
instance.dismiss = initPuzzleSelectionDismissal(root, {
|
|
70
74
|
dismiss: () => select(root, false),
|
|
71
|
-
isSelectionTarget: (target) => {
|
|
75
|
+
isSelectionTarget: (target, event) => {
|
|
72
76
|
const panel = activePanel(root);
|
|
73
77
|
return Boolean(
|
|
74
78
|
panel &&
|
|
75
79
|
panel.dataset.puzzleStatus !== 'valid' &&
|
|
76
80
|
panel.contains(target) &&
|
|
77
|
-
target.closest('[data-sliding-piece]
|
|
81
|
+
(target.closest('[data-sliding-piece]') ||
|
|
82
|
+
getPuzzleMoveTarget(panel, event)),
|
|
78
83
|
);
|
|
79
84
|
},
|
|
80
85
|
});
|
|
@@ -82,7 +87,8 @@ export function initSlidingPuzzle(root: HTMLElement): void {
|
|
|
82
87
|
root.addEventListener(
|
|
83
88
|
'click',
|
|
84
89
|
(event) => {
|
|
85
|
-
if (!(event.target instanceof Element))
|
|
90
|
+
if (!(event.target instanceof Element) || instance.motion?.blocking)
|
|
91
|
+
return;
|
|
86
92
|
if (instance.ignoreNextClick) {
|
|
87
93
|
instance.ignoreNextClick = false;
|
|
88
94
|
return;
|
|
@@ -91,37 +97,13 @@ export function initSlidingPuzzle(root: HTMLElement): void {
|
|
|
91
97
|
select(root, !instance.selected);
|
|
92
98
|
return;
|
|
93
99
|
}
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
const direction = destination?.dataset.destinationDirection as
|
|
98
|
-
SlidingDirection | undefined;
|
|
99
|
-
if (destination && !destination.disabled && direction)
|
|
100
|
-
void move(root, direction);
|
|
100
|
+
const movement = getPuzzleMoveTarget(activePanel(root), event);
|
|
101
|
+
if (movement) void move(root, movement.direction);
|
|
101
102
|
else select(root, false);
|
|
102
103
|
},
|
|
103
104
|
options,
|
|
104
105
|
);
|
|
105
106
|
|
|
106
|
-
root.addEventListener(
|
|
107
|
-
'keydown',
|
|
108
|
-
(event) => {
|
|
109
|
-
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
110
|
-
if (event.key === 'Escape') {
|
|
111
|
-
cancelDrag(instance);
|
|
112
|
-
select(root, false);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const direction = directions[event.key];
|
|
116
|
-
if (!direction || !(event.target instanceof Element)) return;
|
|
117
|
-
if (!instance.selected && !event.target.closest('[data-sliding-piece]'))
|
|
118
|
-
return;
|
|
119
|
-
event.preventDefault();
|
|
120
|
-
void move(root, direction);
|
|
121
|
-
},
|
|
122
|
-
options,
|
|
123
|
-
);
|
|
124
|
-
|
|
125
107
|
root.addEventListener(
|
|
126
108
|
'dragstart',
|
|
127
109
|
(event) => event.preventDefault(),
|
|
@@ -231,6 +213,24 @@ export function initSlidingPuzzle(root: HTMLElement): void {
|
|
|
231
213
|
() => cancelDrag(instance),
|
|
232
214
|
options,
|
|
233
215
|
);
|
|
216
|
+
instance.keyboard = initPuzzleKeyboard(root, {
|
|
217
|
+
getPanel: () => activePanel(root),
|
|
218
|
+
getSelected: () =>
|
|
219
|
+
instance.selected
|
|
220
|
+
? (activePanel(root)?.querySelector<HTMLButtonElement>(
|
|
221
|
+
'[data-sliding-piece]',
|
|
222
|
+
) ?? null)
|
|
223
|
+
: null,
|
|
224
|
+
select: (piece) => {
|
|
225
|
+
if (!piece) cancelDrag(instance);
|
|
226
|
+
select(root, piece !== null);
|
|
227
|
+
},
|
|
228
|
+
move: (_piece, direction) => {
|
|
229
|
+
void move(root, direction);
|
|
230
|
+
},
|
|
231
|
+
isLocked: () =>
|
|
232
|
+
instance.motion?.blocking === true || instance.drag !== null,
|
|
233
|
+
});
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
export function destroySlidingPuzzle(root: HTMLElement): void {
|
|
@@ -240,6 +240,7 @@ export function destroySlidingPuzzle(root: HTMLElement): void {
|
|
|
240
240
|
cancelMotion(instance);
|
|
241
241
|
cancelTerrainFrame(instance);
|
|
242
242
|
instance.dismiss();
|
|
243
|
+
instance.keyboard?.destroy();
|
|
243
244
|
instance.abort.abort();
|
|
244
245
|
instances.delete(root);
|
|
245
246
|
}
|
|
@@ -270,6 +271,7 @@ export function resetSlidingPuzzle(root: HTMLElement): void {
|
|
|
270
271
|
setSlidingPuzzleStatus(root, panel.dataset.puzzleId ?? '', 'incomplete');
|
|
271
272
|
render(root);
|
|
272
273
|
previews(root);
|
|
274
|
+
instance.keyboard?.reset();
|
|
273
275
|
emitChange(root);
|
|
274
276
|
}
|
|
275
277
|
|
|
@@ -311,7 +313,7 @@ function select(root: HTMLElement, selected: boolean): void {
|
|
|
311
313
|
if (
|
|
312
314
|
!instance ||
|
|
313
315
|
!panel ||
|
|
314
|
-
instance.motion ||
|
|
316
|
+
(instance.motion?.blocking && selected) ||
|
|
315
317
|
panel.dataset.puzzleStatus === 'valid'
|
|
316
318
|
)
|
|
317
319
|
return;
|
|
@@ -332,63 +334,58 @@ async function move(
|
|
|
332
334
|
!panel ||
|
|
333
335
|
!piece ||
|
|
334
336
|
instance.drag ||
|
|
335
|
-
instance.motion ||
|
|
336
|
-
isMoving(piece) ||
|
|
337
|
+
instance.motion?.blocking ||
|
|
337
338
|
panel.dataset.puzzleStatus === 'valid'
|
|
338
339
|
)
|
|
339
340
|
return;
|
|
340
341
|
const puzzle = model(panel);
|
|
341
342
|
const movement = simulateSlidingMove(puzzle, instance.state, direction);
|
|
342
343
|
if (!movement) return;
|
|
343
|
-
const
|
|
344
|
+
const bounds = piece.getBoundingClientRect();
|
|
345
|
+
cancelMotion(instance);
|
|
346
|
+
const complete = isSlidingPuzzleComplete(puzzle, movement.state);
|
|
347
|
+
const motion = {
|
|
348
|
+
controller: new AbortController(),
|
|
349
|
+
blocking: movement.teleported || complete,
|
|
350
|
+
};
|
|
351
|
+
const { signal } = motion.controller;
|
|
344
352
|
instance.motion = motion;
|
|
345
353
|
piece.setAttribute('aria-busy', 'true');
|
|
346
354
|
const clearBusy = (): void => piece.removeAttribute('aria-busy');
|
|
347
|
-
|
|
355
|
+
signal.addEventListener('abort', clearBusy, { once: true });
|
|
348
356
|
cancelTerrainFrame(instance);
|
|
349
357
|
const previousState = instance.state;
|
|
350
358
|
renderTerrain(panel, previousState);
|
|
351
|
-
const bounds = piece.getBoundingClientRect();
|
|
352
359
|
instance.state = movement.state;
|
|
353
|
-
instance.selected =
|
|
354
|
-
retainSelection && !isSlidingPuzzleComplete(puzzle, movement.state);
|
|
360
|
+
instance.selected = retainSelection && !complete;
|
|
355
361
|
render(root, false, movement.slideTo);
|
|
356
362
|
if (!movement.teleported || !instance.selected) previews(root);
|
|
357
363
|
try {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
)
|
|
369
|
-
return;
|
|
364
|
+
const animation = animateSlide(
|
|
365
|
+
panel,
|
|
366
|
+
piece,
|
|
367
|
+
instance,
|
|
368
|
+
movement,
|
|
369
|
+
previousState,
|
|
370
|
+
bounds,
|
|
371
|
+
signal,
|
|
372
|
+
);
|
|
373
|
+
if (!motion.blocking) emitChange(root);
|
|
374
|
+
if (!(await animation)) return;
|
|
370
375
|
if (movement.teleported) {
|
|
371
376
|
clearPreviews(panel);
|
|
372
|
-
if (
|
|
373
|
-
!(await animateHoleTransfer(
|
|
374
|
-
root,
|
|
375
|
-
piece,
|
|
376
|
-
puzzle,
|
|
377
|
-
movement,
|
|
378
|
-
motion.signal,
|
|
379
|
-
))
|
|
380
|
-
)
|
|
377
|
+
if (!(await animateHoleTransfer(root, piece, puzzle, movement, signal)))
|
|
381
378
|
return;
|
|
382
379
|
}
|
|
383
|
-
if (
|
|
380
|
+
if (signal.aborted) return;
|
|
384
381
|
instance.motion = null;
|
|
385
382
|
render(root);
|
|
386
383
|
if (movement.teleported) previews(root);
|
|
387
|
-
emitChange(root);
|
|
384
|
+
if (motion.blocking) emitChange(root);
|
|
388
385
|
} finally {
|
|
389
|
-
|
|
386
|
+
signal.removeEventListener('abort', clearBusy);
|
|
390
387
|
if (instance.motion === motion) instance.motion = null;
|
|
391
|
-
if (!
|
|
388
|
+
if (!signal.aborted) clearBusy();
|
|
392
389
|
}
|
|
393
390
|
}
|
|
394
391
|
|
|
@@ -693,10 +690,11 @@ function previews(root: HTMLElement): void {
|
|
|
693
690
|
) as SlidingPuzzleLabels;
|
|
694
691
|
const previewPuzzle = { ...model(panel), holes: [] };
|
|
695
692
|
for (const movement of getSlidingMoves(previewPuzzle, instance.state)) {
|
|
696
|
-
if (board)
|
|
693
|
+
if (board)
|
|
694
|
+
renderPathStream(board, { ...movement, to: movement.slideTo }, 'piece');
|
|
697
695
|
const destination = cell(panel, movement.slideTo);
|
|
698
696
|
if (!destination) continue;
|
|
699
|
-
destination.
|
|
697
|
+
destination.tabIndex = -1;
|
|
700
698
|
destination.setAttribute('data-sliding-destination', '');
|
|
701
699
|
destination.setAttribute('data-puzzle-cell-interactive', '');
|
|
702
700
|
destination.dataset.destinationPiece = 'piece';
|
|
@@ -711,8 +709,7 @@ function previews(root: HTMLElement): void {
|
|
|
711
709
|
}
|
|
712
710
|
|
|
713
711
|
function clearPreviews(panel: HTMLElement): void {
|
|
714
|
-
|
|
715
|
-
stream.remove();
|
|
712
|
+
clearPuzzleMovementPreview(panel);
|
|
716
713
|
for (const destination of panel.querySelectorAll<HTMLButtonElement>(
|
|
717
714
|
'[data-sliding-destination]',
|
|
718
715
|
)) {
|
|
@@ -746,7 +743,7 @@ function cancelTerrainFrame(instance: Instance): void {
|
|
|
746
743
|
}
|
|
747
744
|
|
|
748
745
|
function cancelMotion(instance: Instance): void {
|
|
749
|
-
instance.motion?.abort();
|
|
746
|
+
instance.motion?.controller.abort();
|
|
750
747
|
instance.motion = null;
|
|
751
748
|
}
|
|
752
749
|
|
|
@@ -81,7 +81,12 @@ const formatValue = (value: DisplayTextValue): string => String(value);
|
|
|
81
81
|
|
|
82
82
|
{
|
|
83
83
|
cell.kind === 'icons' ? (
|
|
84
|
-
<IconList
|
|
84
|
+
<IconList
|
|
85
|
+
items={cell.items}
|
|
86
|
+
label={cell.label}
|
|
87
|
+
size={cell.size}
|
|
88
|
+
tooltips={interactive}
|
|
89
|
+
/>
|
|
85
90
|
) : null
|
|
86
91
|
}
|
|
87
92
|
|