@astronox/core 0.5.5 → 0.5.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astronox/core",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "Reusable Astro components, patterns, styles, and infrastructure for AstroNox sites.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -148,57 +148,6 @@
148
148
  overflow: visible;
149
149
  }
150
150
 
151
- .magnetic-field-puzzle__path-stream {
152
- --field-path-rotation: 0deg;
153
-
154
- position: relative;
155
- z-index: 3;
156
- grid-row: var(--field-path-row);
157
- grid-column: var(--field-path-column);
158
- min-inline-size: 0;
159
- min-block-size: 0;
160
- overflow: visible;
161
- pointer-events: none;
162
- }
163
-
164
- .magnetic-field-puzzle__path-chevron {
165
- --field-path-chevron-opacity: 0.38;
166
-
167
- position: absolute;
168
- display: grid;
169
- inset-block-start: var(--field-path-static-block);
170
- inset-inline-start: var(--field-path-static-inline);
171
- inline-size: 1em;
172
- block-size: 1em;
173
- place-items: center;
174
- color: var(--nox-source-tone-success);
175
- font-size: var(--nox-font-size-xl);
176
- opacity: var(--field-path-chevron-opacity);
177
- pointer-events: none;
178
- transform: translate(-50%, -50%) rotate(var(--field-path-rotation));
179
- }
180
-
181
- .magnetic-field-puzzle__path-chevron::before {
182
- content: '';
183
- inline-size: 0.46em;
184
- block-size: 0.46em;
185
- border-block-start: calc(var(--nox-border-width) * 2) solid currentColor;
186
- border-inline-end: calc(var(--nox-border-width) * 2) solid currentColor;
187
- transform: rotate(45deg);
188
- }
189
-
190
- .magnetic-field-puzzle__path-stream[data-field-path-direction='north'] {
191
- --field-path-rotation: -90deg;
192
- }
193
-
194
- .magnetic-field-puzzle__path-stream[data-field-path-direction='south'] {
195
- --field-path-rotation: 90deg;
196
- }
197
-
198
- .magnetic-field-puzzle__path-stream[data-field-path-direction='west'] {
199
- --field-path-rotation: 180deg;
200
- }
201
-
202
151
  .magnetic-field-puzzle__piece {
203
152
  --piece-pupil-block-position: 35%;
204
153
  --piece-pupil-inline-position: 68%;
@@ -342,12 +291,6 @@
342
291
  .magnetic-field-puzzle__piece[data-piece-moved] {
343
292
  animation: magnetic-field-slide 260ms var(--nox-motion-easing-standard);
344
293
  }
345
-
346
- .magnetic-field-puzzle__path-chevron {
347
- animation: magnetic-field-path var(--field-path-duration) linear infinite
348
- both;
349
- animation-delay: calc(var(--field-path-chevron-index) * -700ms);
350
- }
351
294
  }
352
295
 
353
296
  @keyframes magnetic-field-slide {
@@ -359,25 +302,3 @@
359
302
  );
360
303
  }
361
304
  }
362
-
363
- @keyframes magnetic-field-path {
364
- 0% {
365
- inset-block-start: 50%;
366
- inset-inline-start: 50%;
367
- opacity: 0;
368
- transform: translate(-50%, -50%) rotate(var(--field-path-rotation))
369
- scale(0.9);
370
- }
371
-
372
- 12%,
373
- 82% {
374
- opacity: var(--field-path-chevron-opacity);
375
- }
376
-
377
- 100% {
378
- inset-block-start: var(--field-path-target-block);
379
- inset-inline-start: var(--field-path-target-inline);
380
- opacity: 0;
381
- transform: translate(-50%, -50%) rotate(var(--field-path-rotation)) scale(1);
382
- }
383
- }
@@ -1,3 +1,12 @@
1
+ import {
2
+ animatePieceMovement,
3
+ cancelDragFrame,
4
+ findDirectionalDropDestination,
5
+ hasLeftOriginCell,
6
+ renderPathStream,
7
+ scheduleDragFrame,
8
+ type PuzzleDrag,
9
+ } from '../PuzzleGrid/movement';
1
10
  import {
2
11
  setPuzzleGridCellState,
3
12
  setPuzzleGridState,
@@ -5,7 +14,6 @@ import {
5
14
  import { initPuzzleSelectionDismissal } from '../PuzzleGrid/selectionDismissal';
6
15
  import type {
7
16
  MagneticFieldDirection,
8
- MagneticFieldMovement,
9
17
  MagneticFieldPolarity,
10
18
  MagneticFieldPupilPosition,
11
19
  MagneticFieldPuzzleChangeDetail,
@@ -20,32 +28,14 @@ import {
20
28
  simulateMagneticFieldMove,
21
29
  } from './simulation';
22
30
 
23
- type MagneticFieldDrag = {
24
- pointerId: number;
25
- pieceId: string;
26
- startX: number;
27
- startY: number;
28
- currentX: number;
29
- currentY: number;
30
- frameId: number | null;
31
- element: HTMLButtonElement;
32
- originCellBounds: DOMRect;
33
- originCenterX: number;
34
- originCenterY: number;
35
- destinations: readonly HTMLButtonElement[] | null;
36
- moved: boolean;
37
- };
38
-
39
31
  type MagneticFieldInstance = {
40
32
  activeIndex: number;
41
33
  selectedPieceId: string | null;
42
- drag: MagneticFieldDrag | null;
34
+ drag: PuzzleDrag | null;
43
35
  ignoreNextClick: boolean;
44
36
  };
45
37
 
46
38
  const dragThreshold = 12;
47
- const pathChevronsPerCell = 3;
48
- const pathDurationPerCellMs = 2100;
49
39
  const instances = new WeakMap<HTMLElement, MagneticFieldInstance>();
50
40
 
51
41
  export function initMagneticFieldPuzzle(root: HTMLElement): void {
@@ -155,7 +145,7 @@ export function initMagneticFieldPuzzle(root: HTMLElement): void {
155
145
  drag.destinations ??= getDropDestinations(root);
156
146
  drag.currentX = event.clientX;
157
147
  drag.currentY = event.clientY;
158
- scheduleDragFrame(root, drag);
148
+ scheduleDragFrame(drag, () => instances.get(root)?.drag === drag);
159
149
  });
160
150
 
161
151
  root.addEventListener('pointerup', (event) => {
@@ -295,7 +285,8 @@ function updateDestinations(root: HTMLElement): void {
295
285
 
296
286
  for (const movement of getMagneticFieldMoves(puzzle, state, pieceId)) {
297
287
  const directionLabel = readDirectionLabel(labels, movement.direction);
298
- renderPathStream(panel, movement);
288
+ const board = panel.querySelector<HTMLElement>('[data-field-board]');
289
+ if (board) renderPathStream(board, movement);
299
290
 
300
291
  const destination = findCell(panel, movement.to);
301
292
  if (!destination) continue;
@@ -452,22 +443,6 @@ function renderState(
452
443
  }
453
444
  }
454
445
 
455
- function animatePieceMovement(
456
- element: HTMLButtonElement,
457
- initialBounds: DOMRect,
458
- ): void {
459
- const finalBounds = element.getBoundingClientRect();
460
- element.style.setProperty(
461
- '--piece-move-inline-distance',
462
- `${initialBounds.left - finalBounds.left}px`,
463
- );
464
- element.style.setProperty(
465
- '--piece-move-block-distance',
466
- `${initialBounds.top - finalBounds.top}px`,
467
- );
468
- element.setAttribute('data-piece-moved', '');
469
- }
470
-
471
446
  function finishDrag(
472
447
  root: HTMLElement,
473
448
  event: PointerEvent,
@@ -508,41 +483,6 @@ function finishDrag(
508
483
  if (drag.moved) selectPiece(root, null);
509
484
  }
510
485
 
511
- function hasLeftOriginCell(
512
- drag: MagneticFieldDrag,
513
- deltaX: number,
514
- deltaY: number,
515
- ): boolean {
516
- const centerX = drag.originCenterX + deltaX;
517
- const centerY = drag.originCenterY + deltaY;
518
- const { bottom, left, right, top } = drag.originCellBounds;
519
-
520
- return centerX < left || centerX > right || centerY < top || centerY > bottom;
521
- }
522
-
523
- function scheduleDragFrame(root: HTMLElement, drag: MagneticFieldDrag): void {
524
- if (drag.frameId !== null) return;
525
- drag.frameId = window.requestAnimationFrame(() => {
526
- drag.frameId = null;
527
- if (instances.get(root)?.drag !== drag) return;
528
-
529
- drag.element.style.setProperty(
530
- '--piece-drag-x',
531
- `${drag.currentX - drag.startX}px`,
532
- );
533
- drag.element.style.setProperty(
534
- '--piece-drag-y',
535
- `${drag.currentY - drag.startY}px`,
536
- );
537
- });
538
- }
539
-
540
- function cancelDragFrame(drag: MagneticFieldDrag): void {
541
- if (drag.frameId === null) return;
542
- window.cancelAnimationFrame(drag.frameId);
543
- drag.frameId = null;
544
- }
545
-
546
486
  function getDropDestinations(root: HTMLElement): HTMLButtonElement[] {
547
487
  const panel = getActivePanel(root);
548
488
  if (!panel) return [];
@@ -552,30 +492,9 @@ function getDropDestinations(root: HTMLElement): HTMLButtonElement[] {
552
492
  ];
553
493
  }
554
494
 
555
- function findDirectionalDropDestination(
556
- drag: MagneticFieldDrag,
557
- deltaX: number,
558
- deltaY: number,
559
- ): HTMLButtonElement | null {
560
- let direction: MagneticFieldDirection;
561
- if (Math.abs(deltaX) >= Math.abs(deltaY)) {
562
- direction = deltaX >= 0 ? 'east' : 'west';
563
- } else {
564
- direction = deltaY >= 0 ? 'south' : 'north';
565
- }
566
-
567
- return (
568
- (drag.destinations ?? []).find(
569
- (destination) =>
570
- destination.dataset.destinationDirection === direction &&
571
- destination.dataset.destinationPiece === drag.pieceId,
572
- ) ?? null
573
- );
574
- }
575
-
576
495
  function hideMovePreviews(panel: HTMLElement): void {
577
496
  for (const stream of panel.querySelectorAll<HTMLElement>(
578
- '[data-field-path-stream]',
497
+ '[data-puzzle-path-stream]',
579
498
  )) {
580
499
  stream.remove();
581
500
  }
@@ -597,59 +516,6 @@ function hideMovePreviews(panel: HTMLElement): void {
597
516
  }
598
517
  }
599
518
 
600
- function renderPathStream(
601
- panel: HTMLElement,
602
- movement: MagneticFieldMovement,
603
- ): void {
604
- const board = panel.querySelector<HTMLElement>('[data-field-board]');
605
- if (!board) return;
606
-
607
- const rowDistance = movement.to.row - movement.from.row;
608
- const columnDistance = movement.to.column - movement.from.column;
609
- const cellDistance = Math.abs(rowDistance) + Math.abs(columnDistance);
610
- const chevronCount = pathChevronsPerCell * cellDistance;
611
- const stream = document.createElement('span');
612
- stream.className = 'magnetic-field-puzzle__path-stream';
613
- stream.dataset.fieldPathStream = '';
614
- stream.dataset.fieldPathDirection = movement.direction;
615
- stream.setAttribute('aria-hidden', 'true');
616
- stream.style.setProperty('--field-path-row', String(movement.from.row + 1));
617
- stream.style.setProperty(
618
- '--field-path-column',
619
- String(movement.from.column + 1),
620
- );
621
- stream.style.setProperty(
622
- '--field-path-target-inline',
623
- `${50 + columnDistance * 100}%`,
624
- );
625
- stream.style.setProperty(
626
- '--field-path-target-block',
627
- `${50 + rowDistance * 100}%`,
628
- );
629
- stream.style.setProperty(
630
- '--field-path-duration',
631
- `${pathDurationPerCellMs * cellDistance}ms`,
632
- );
633
-
634
- for (let index = 0; index < chevronCount; index += 1) {
635
- const progress = (index + 0.5) / chevronCount;
636
- const chevron = document.createElement('span');
637
- chevron.className = 'magnetic-field-puzzle__path-chevron';
638
- chevron.style.setProperty('--field-path-chevron-index', String(index));
639
- chevron.style.setProperty(
640
- '--field-path-static-inline',
641
- `${50 + columnDistance * progress * 100}%`,
642
- );
643
- chevron.style.setProperty(
644
- '--field-path-static-block',
645
- `${50 + rowDistance * progress * 100}%`,
646
- );
647
- stream.append(chevron);
648
- }
649
-
650
- board.append(stream);
651
- }
652
-
653
519
  function findCell(
654
520
  panel: HTMLElement,
655
521
  position: { row: number; column: number },
@@ -0,0 +1,80 @@
1
+ .puzzle-grid__path-stream {
2
+ --puzzle-path-rotation: 0deg;
3
+
4
+ position: relative;
5
+ z-index: 3;
6
+ grid-row: var(--puzzle-path-row);
7
+ grid-column: var(--puzzle-path-column);
8
+ min-inline-size: 0;
9
+ min-block-size: 0;
10
+ overflow: visible;
11
+ pointer-events: none;
12
+ }
13
+
14
+ .puzzle-grid__path-chevron {
15
+ --puzzle-path-chevron-opacity: 0.38;
16
+
17
+ position: absolute;
18
+ display: grid;
19
+ inset-block-start: var(--puzzle-path-static-block);
20
+ inset-inline-start: var(--puzzle-path-static-inline);
21
+ inline-size: 1em;
22
+ block-size: 1em;
23
+ place-items: center;
24
+ color: var(--nox-source-tone-success);
25
+ font-size: var(--nox-font-size-xl);
26
+ opacity: var(--puzzle-path-chevron-opacity);
27
+ pointer-events: none;
28
+ transform: translate(-50%, -50%) rotate(var(--puzzle-path-rotation));
29
+ }
30
+
31
+ .puzzle-grid__path-chevron::before {
32
+ content: '';
33
+ inline-size: 0.46em;
34
+ block-size: 0.46em;
35
+ border-block-start: calc(var(--nox-border-width) * 2) solid currentColor;
36
+ border-inline-end: calc(var(--nox-border-width) * 2) solid currentColor;
37
+ transform: rotate(45deg);
38
+ }
39
+
40
+ .puzzle-grid__path-stream[data-puzzle-path-direction='north'] {
41
+ --puzzle-path-rotation: -90deg;
42
+ }
43
+
44
+ .puzzle-grid__path-stream[data-puzzle-path-direction='south'] {
45
+ --puzzle-path-rotation: 90deg;
46
+ }
47
+
48
+ .puzzle-grid__path-stream[data-puzzle-path-direction='west'] {
49
+ --puzzle-path-rotation: 180deg;
50
+ }
51
+
52
+ @media (prefers-reduced-motion: no-preference) {
53
+ .puzzle-grid__path-chevron {
54
+ animation: puzzle-grid-path var(--puzzle-path-duration) linear infinite both;
55
+ animation-delay: calc(var(--puzzle-path-chevron-index) * -700ms);
56
+ }
57
+ }
58
+
59
+ @keyframes puzzle-grid-path {
60
+ 0% {
61
+ inset-block-start: 50%;
62
+ inset-inline-start: 50%;
63
+ opacity: 0;
64
+ transform: translate(-50%, -50%) rotate(var(--puzzle-path-rotation))
65
+ scale(0.9);
66
+ }
67
+
68
+ 12%,
69
+ 82% {
70
+ opacity: var(--puzzle-path-chevron-opacity);
71
+ }
72
+
73
+ 100% {
74
+ inset-block-start: var(--puzzle-path-target-block);
75
+ inset-inline-start: var(--puzzle-path-target-inline);
76
+ opacity: 0;
77
+ transform: translate(-50%, -50%) rotate(var(--puzzle-path-rotation))
78
+ scale(1);
79
+ }
80
+ }
@@ -0,0 +1,149 @@
1
+ export type PuzzleDrag = {
2
+ pointerId: number;
3
+ pieceId: string;
4
+ startX: number;
5
+ startY: number;
6
+ currentX: number;
7
+ currentY: number;
8
+ frameId: number | null;
9
+ element: HTMLButtonElement;
10
+ originCellBounds: DOMRect;
11
+ originCenterX: number;
12
+ originCenterY: number;
13
+ destinations: readonly HTMLButtonElement[] | null;
14
+ moved: boolean;
15
+ };
16
+
17
+ type Direction = 'north' | 'east' | 'south' | 'west';
18
+
19
+ const pathChevronsPerCell = 3;
20
+ const pathDurationPerCellMs = 2100;
21
+
22
+ export function animatePieceMovement(
23
+ element: HTMLButtonElement,
24
+ initialBounds: DOMRect,
25
+ ): void {
26
+ const finalBounds = element.getBoundingClientRect();
27
+ element.style.setProperty(
28
+ '--piece-move-inline-distance',
29
+ `${initialBounds.left - finalBounds.left}px`,
30
+ );
31
+ element.style.setProperty(
32
+ '--piece-move-block-distance',
33
+ `${initialBounds.top - finalBounds.top}px`,
34
+ );
35
+ element.setAttribute('data-piece-moved', '');
36
+ }
37
+
38
+ export function hasLeftOriginCell(
39
+ drag: PuzzleDrag,
40
+ deltaX: number,
41
+ deltaY: number,
42
+ ): boolean {
43
+ const centerX = drag.originCenterX + deltaX;
44
+ const centerY = drag.originCenterY + deltaY;
45
+ const { bottom, left, right, top } = drag.originCellBounds;
46
+
47
+ return centerX < left || centerX > right || centerY < top || centerY > bottom;
48
+ }
49
+
50
+ export function scheduleDragFrame(
51
+ drag: PuzzleDrag,
52
+ isCurrent: () => boolean,
53
+ ): void {
54
+ if (drag.frameId !== null) return;
55
+ drag.frameId = window.requestAnimationFrame(() => {
56
+ drag.frameId = null;
57
+ if (!isCurrent()) return;
58
+
59
+ drag.element.style.setProperty(
60
+ '--piece-drag-x',
61
+ `${drag.currentX - drag.startX}px`,
62
+ );
63
+ drag.element.style.setProperty(
64
+ '--piece-drag-y',
65
+ `${drag.currentY - drag.startY}px`,
66
+ );
67
+ });
68
+ }
69
+
70
+ export function cancelDragFrame(drag: PuzzleDrag): void {
71
+ if (drag.frameId === null) return;
72
+ window.cancelAnimationFrame(drag.frameId);
73
+ drag.frameId = null;
74
+ }
75
+
76
+ export function findDirectionalDropDestination(
77
+ drag: PuzzleDrag,
78
+ deltaX: number,
79
+ deltaY: number,
80
+ ): HTMLButtonElement | null {
81
+ let direction: Direction;
82
+ if (Math.abs(deltaX) >= Math.abs(deltaY)) {
83
+ direction = deltaX >= 0 ? 'east' : 'west';
84
+ } else {
85
+ direction = deltaY >= 0 ? 'south' : 'north';
86
+ }
87
+
88
+ return (
89
+ (drag.destinations ?? []).find(
90
+ (destination) =>
91
+ destination.dataset.destinationDirection === direction &&
92
+ destination.dataset.destinationPiece === drag.pieceId,
93
+ ) ?? null
94
+ );
95
+ }
96
+
97
+ export function renderPathStream(
98
+ board: HTMLElement,
99
+ movement: {
100
+ from: { row: number; column: number };
101
+ to: { row: number; column: number };
102
+ direction: Direction;
103
+ },
104
+ ): void {
105
+ const rowDistance = movement.to.row - movement.from.row;
106
+ const columnDistance = movement.to.column - movement.from.column;
107
+ const cellDistance = Math.abs(rowDistance) + Math.abs(columnDistance);
108
+ const chevronCount = pathChevronsPerCell * cellDistance;
109
+ const stream = document.createElement('span');
110
+ stream.className = 'puzzle-grid__path-stream';
111
+ stream.dataset.puzzlePathStream = '';
112
+ stream.dataset.puzzlePathDirection = movement.direction;
113
+ stream.setAttribute('aria-hidden', 'true');
114
+ stream.style.setProperty('--puzzle-path-row', String(movement.from.row + 1));
115
+ stream.style.setProperty(
116
+ '--puzzle-path-column',
117
+ String(movement.from.column + 1),
118
+ );
119
+ stream.style.setProperty(
120
+ '--puzzle-path-target-inline',
121
+ `${50 + columnDistance * 100}%`,
122
+ );
123
+ stream.style.setProperty(
124
+ '--puzzle-path-target-block',
125
+ `${50 + rowDistance * 100}%`,
126
+ );
127
+ stream.style.setProperty(
128
+ '--puzzle-path-duration',
129
+ `${pathDurationPerCellMs * cellDistance}ms`,
130
+ );
131
+
132
+ for (let index = 0; index < chevronCount; index += 1) {
133
+ const progress = (index + 0.5) / chevronCount;
134
+ const chevron = document.createElement('span');
135
+ chevron.className = 'puzzle-grid__path-chevron';
136
+ chevron.style.setProperty('--puzzle-path-chevron-index', String(index));
137
+ chevron.style.setProperty(
138
+ '--puzzle-path-static-inline',
139
+ `${50 + columnDistance * progress * 100}%`,
140
+ );
141
+ chevron.style.setProperty(
142
+ '--puzzle-path-static-block',
143
+ `${50 + rowDistance * progress * 100}%`,
144
+ );
145
+ stream.append(chevron);
146
+ }
147
+
148
+ board.append(stream);
149
+ }
@@ -6,7 +6,8 @@ type PuzzleSelectionDismissalOptions = {
6
6
  export function initPuzzleSelectionDismissal(
7
7
  root: HTMLElement,
8
8
  { dismiss, isSelectionTarget }: PuzzleSelectionDismissalOptions,
9
- ): void {
9
+ ): () => void {
10
+ const controller = new AbortController();
10
11
  document.addEventListener(
11
12
  'pointerdown',
12
13
  (event) => {
@@ -22,6 +23,7 @@ export function initPuzzleSelectionDismissal(
22
23
  focused.blur();
23
24
  }
24
25
  },
25
- true,
26
+ { capture: true, signal: controller.signal },
26
27
  );
28
+ return () => controller.abort();
27
29
  }