@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.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/src/client/scroll.ts +14 -1
  3. package/src/components/data-display/IconList/IconList.astro +20 -2
  4. package/src/components/data-display/MetricTile/MetricTileContent.astro +6 -1
  5. package/src/components/feedback/Tooltip/Tooltip.css +9 -1
  6. package/src/components/feedback/Tooltip/tooltipController.ts +12 -9
  7. package/src/components/media/EmbedFrame/EmbedFrame.astro +9 -0
  8. package/src/components/media/EmbedFrame/EmbedFrame.css +5 -0
  9. package/src/components/media/EmbedFrame/embedFrameController.ts +84 -1
  10. package/src/display/contracts.ts +1 -0
  11. package/src/patterns/CircuitPuzzle/CircuitPuzzle.astro +3 -0
  12. package/src/patterns/CircuitPuzzle/circuitPuzzleController.ts +49 -0
  13. package/src/patterns/LightMazePuzzle/LightMazePuzzle.astro +1 -0
  14. package/src/patterns/LightMazePuzzle/lightMazePuzzleController.ts +43 -56
  15. package/src/patterns/MagneticFieldPuzzle/MagneticFieldPuzzle.astro +1 -0
  16. package/src/patterns/MagneticFieldPuzzle/magneticFieldPuzzleController.ts +50 -64
  17. package/src/patterns/PuzzleGrid/PuzzleGrid.astro +2 -0
  18. package/src/patterns/PuzzleGrid/keyboard.ts +220 -0
  19. package/src/patterns/PuzzleGrid/movement.css +4 -0
  20. package/src/patterns/PuzzleGrid/movement.ts +67 -3
  21. package/src/patterns/PuzzleGrid/selectionDismissal.ts +2 -2
  22. package/src/patterns/SlidingPuzzle/SlidingPuzzle.astro +1 -0
  23. package/src/patterns/SlidingPuzzle/slidingPuzzleController.ts +71 -74
  24. package/src/patterns/Table/TableCell.astro +1 -1
  25. 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: AbortController | null;
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], [data-sliding-destination]'),
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)) return;
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 destination = event.target.closest<HTMLButtonElement>(
95
- '[data-sliding-destination]',
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 motion = new AbortController();
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
- motion.signal.addEventListener('abort', clearBusy, { once: true });
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
- if (
359
- !(await animateSlide(
360
- panel,
361
- piece,
362
- instance,
363
- movement,
364
- previousState,
365
- bounds,
366
- motion.signal,
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 (motion.signal.aborted) return;
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
- motion.signal.removeEventListener('abort', clearBusy);
386
+ signal.removeEventListener('abort', clearBusy);
390
387
  if (instance.motion === motion) instance.motion = null;
391
- if (!motion.signal.aborted) clearBusy();
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) renderPathStream(board, { ...movement, to: movement.slideTo });
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.removeAttribute('tabindex');
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
- for (const stream of panel.querySelectorAll('[data-puzzle-path-stream]'))
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
 
@@ -31,7 +31,7 @@ const rootAttributes = cell
31
31
  tooltip ? (
32
32
  <Tooltip
33
33
  activation="hover"
34
- alignTo="boundary"
34
+ alignTo="trigger"
35
35
  content={tooltip}
36
36
  triggerScope="ancestor"
37
37
  >
@@ -81,7 +81,12 @@ const formatValue = (value: DisplayTextValue): string => String(value);
81
81
 
82
82
  {
83
83
  cell.kind === 'icons' ? (
84
- <IconList items={cell.items} label={cell.label} size={cell.size} />
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