@astronox/core 0.5.4 → 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.
Files changed (29) hide show
  1. package/README.md +13 -0
  2. package/package.json +1 -1
  3. package/src/components/content/AnnotatedText/AnnotatedText.astro +38 -0
  4. package/src/components/content/AnnotatedText/AnnotatedText.css +19 -0
  5. package/src/components/content/AnnotatedText/MarkedText.astro +11 -0
  6. package/src/components/content/InlineDefinition/InlineDefinition.astro +11 -2
  7. package/src/components/data-display/TextPair/TextPair.astro +13 -2
  8. package/src/components/feedback/Tooltip/Tooltip.astro +18 -3
  9. package/src/components/feedback/Tooltip/Tooltip.css +9 -0
  10. package/src/components/index.ts +1 -0
  11. package/src/display/annotatedText.ts +14 -0
  12. package/src/display/contracts.ts +4 -3
  13. package/src/display/index.ts +1 -0
  14. package/src/patterns/MagneticFieldPuzzle/MagneticFieldPuzzle.css +0 -79
  15. package/src/patterns/MagneticFieldPuzzle/magneticFieldPuzzleController.ts +14 -148
  16. package/src/patterns/PuzzleGrid/movement.css +80 -0
  17. package/src/patterns/PuzzleGrid/movement.ts +149 -0
  18. package/src/patterns/PuzzleGrid/selectionDismissal.ts +4 -2
  19. package/src/patterns/SlidingPuzzle/SlidingPuzzle.astro +181 -0
  20. package/src/patterns/SlidingPuzzle/SlidingPuzzle.css +177 -0
  21. package/src/patterns/SlidingPuzzle/contracts.ts +57 -0
  22. package/src/patterns/SlidingPuzzle/simulation.ts +106 -0
  23. package/src/patterns/SlidingPuzzle/slidingPuzzleController.ts +703 -0
  24. package/src/patterns/Table/TableBody.astro +7 -1
  25. package/src/patterns/Table/TableCell.astro +3 -1
  26. package/src/patterns/Table/TableCellValue.astro +10 -1
  27. package/src/patterns/index.ts +4 -0
  28. package/src/primitives/text/TextBlock/TextBlock.astro +54 -4
  29. package/src/styles/style.css +4 -0
package/README.md CHANGED
@@ -48,6 +48,19 @@ Use the remaining entry points for layouts, patterns, primitives, display
48
48
  contracts, configuration, theme, client behavior, data validation, and SEO.
49
49
  Deep imports into the physical `src` tree are not part of the public API.
50
50
 
51
+ `AnnotatedText` renders `DisplayAnnotatedText` spans with optional emphasis and
52
+ explanatory tooltips. `Tooltip` accepts either its existing string content or a
53
+ `DisplayTooltip` with a title and non-interactive text spans. `TextBlock`,
54
+ `InlineDefinition` and table descriptions also accept annotated text; table
55
+ tooltip bodies never create nested interactions. These contracts contain no
56
+ markup parser or product-specific vocabulary.
57
+
58
+ Annotated text inherits its tone and whitespace behavior from its context;
59
+ `data-tone` can override the tone at its public root. Empty or whitespace-only
60
+ segments never create tooltip buttons. `TextBlock preserveBreaks` applies the
61
+ same paragraph boundaries to plain and annotated text, even when a separator
62
+ crosses annotation boundaries, while retaining each segment's annotations.
63
+
51
64
  Locale catalogs, localized routing, preference persistence, localized metadata,
52
65
  and responsive media presentation are available through the `i18n`, `routing`,
53
66
  `seo`, and `display` entry points. Consuming sites provide their own locale
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astronox/core",
3
- "version": "0.5.4",
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": [
@@ -0,0 +1,38 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types';
3
+ import type { DisplayAnnotatedText } from '../../../display/annotatedText';
4
+ import type { Tone } from '../../../theme/contracts';
5
+ import Tooltip from '../../feedback/Tooltip/Tooltip.astro';
6
+ import MarkedText from './MarkedText.astro';
7
+
8
+ type Props = Omit<HTMLAttributes<'span'>, 'content'> & {
9
+ 'data-tone'?: Tone;
10
+ content: DisplayAnnotatedText;
11
+ interactive?: boolean;
12
+ };
13
+ const { content, interactive = true, class: className, ...attrs } = Astro.props;
14
+ ---
15
+
16
+ <span
17
+ {...attrs}
18
+ class:list={['annotated-text', className]}
19
+ data-widget="annotated-text"
20
+ >
21
+ {
22
+ content.map((span) =>
23
+ span.tooltip && interactive && span.text.trim().length > 0 ? (
24
+ <Tooltip content={span.tooltip}>
25
+ <button
26
+ type="button"
27
+ class="annotated-text__term"
28
+ data-tooltip-trigger
29
+ >
30
+ {span.text}
31
+ </button>
32
+ </Tooltip>
33
+ ) : (
34
+ <MarkedText spans={[span]} />
35
+ ),
36
+ )
37
+ }
38
+ </span>
@@ -0,0 +1,19 @@
1
+ .annotated-text {
2
+ white-space: inherit;
3
+ }
4
+
5
+ .annotated-text__term {
6
+ display: inline;
7
+ margin: 0;
8
+ padding: 0;
9
+ border: 0;
10
+ background: none;
11
+ color: var(--nox-tone-text);
12
+ font: inherit;
13
+ font-weight: var(--nox-font-weight-strong);
14
+ text-align: inherit;
15
+ white-space: inherit;
16
+ text-decoration: underline dotted;
17
+ text-underline-offset: 0.2em;
18
+ cursor: help;
19
+ }
@@ -0,0 +1,11 @@
1
+ ---
2
+ import type { DisplayTextSpan } from '../../../display/annotatedText';
3
+ type Props = { spans: readonly DisplayTextSpan[] };
4
+ const { spans } = Astro.props;
5
+ ---
6
+
7
+ {
8
+ spans.map((span) =>
9
+ span.emphasized ? <strong>{span.text}</strong> : span.text,
10
+ )
11
+ }
@@ -1,11 +1,13 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types';
3
3
  import type { Tone } from '../../../theme/contracts';
4
+ import type { DisplayAnnotatedText } from '../../../display/annotatedText';
5
+ import AnnotatedText from '../AnnotatedText/AnnotatedText.astro';
4
6
 
5
7
  type Props = HTMLAttributes<'p'> & {
6
8
  'data-tone'?: Tone;
7
9
  term: string;
8
- text?: string;
10
+ text?: string | DisplayAnnotatedText;
9
11
  };
10
12
 
11
13
  const {
@@ -26,6 +28,13 @@ const hasText = text !== undefined && text.length > 0;
26
28
  >
27
29
  <strong class="inline-definition__term">{term}</strong>
28
30
  {hasText ? <span class="inline-definition__separator">:</span> : null}
29
- {hasText ? <span class="inline-definition__text"> {text}</span> : null}
31
+ {
32
+ hasText && text !== undefined ? (
33
+ <span class="inline-definition__text">
34
+ {' '}
35
+ {typeof text === 'string' ? text : <AnnotatedText content={text} />}
36
+ </span>
37
+ ) : null
38
+ }
30
39
  {!hasText ? <slot /> : null}
31
40
  </p>
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types';
3
+ import type { DisplayAnnotatedText } from '../../../display/annotatedText';
4
+ import AnnotatedText from '../../content/AnnotatedText/AnnotatedText.astro';
3
5
  import type {
4
6
  DisplayTextValue,
5
7
  TextColor,
@@ -13,7 +15,8 @@ type Props = HTMLAttributes<'span'> & {
13
15
  primaryColor?: TextColor;
14
16
  primarySize?: TextSize;
15
17
  primaryWeight?: TextWeight;
16
- secondary?: DisplayTextValue;
18
+ secondary?: DisplayTextValue | DisplayAnnotatedText;
19
+ interactiveSecondary?: boolean;
17
20
  secondaryWeight?: TextWeight;
18
21
  layout?: TextPairLayout;
19
22
  truncate?: boolean;
@@ -27,6 +30,7 @@ const {
27
30
  primarySize = 'default',
28
31
  primaryWeight = 'strong',
29
32
  secondary,
33
+ interactiveSecondary = true,
30
34
  secondaryWeight = 'medium',
31
35
  truncate = false,
32
36
  ...attrs
@@ -56,7 +60,14 @@ const {
56
60
  data-part="text-pair-secondary"
57
61
  data-text-weight={secondaryWeight}
58
62
  >
59
- {secondary}
63
+ {typeof secondary === 'object' ? (
64
+ <AnnotatedText
65
+ content={secondary}
66
+ interactive={interactiveSecondary}
67
+ />
68
+ ) : (
69
+ secondary
70
+ )}
60
71
  </span>
61
72
  ) : null
62
73
  }
@@ -1,11 +1,13 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types';
3
3
  import type { Tone } from '../../../theme/contracts';
4
+ import type { DisplayTooltip } from '../../../display/annotatedText';
5
+ import MarkedText from '../../content/AnnotatedText/MarkedText.astro';
4
6
 
5
- type Props = HTMLAttributes<'span'> & {
7
+ type Props = Omit<HTMLAttributes<'span'>, 'content'> & {
6
8
  activation?: 'interactive' | 'hover';
7
9
  alignTo?: 'boundary' | 'trigger';
8
- content: string;
10
+ content: string | DisplayTooltip;
9
11
  tone?: Tone;
10
12
  triggerScope?: 'ancestor' | 'content';
11
13
  };
@@ -40,7 +42,20 @@ const resolvedTone = tone ?? dataTone;
40
42
  popover="manual"
41
43
  role="tooltip"
42
44
  >
43
- {content}
45
+ {
46
+ typeof content === 'string' ? (
47
+ content
48
+ ) : (
49
+ <>
50
+ {content.title ? (
51
+ <strong class="tooltip__title">{content.title}</strong>
52
+ ) : null}
53
+ <span class="tooltip__text">
54
+ <MarkedText spans={content.content} />
55
+ </span>
56
+ </>
57
+ )
58
+ }
44
59
  </span>
45
60
  </nox-tooltip>
46
61
 
@@ -50,6 +50,15 @@
50
50
  display: none;
51
51
  }
52
52
 
53
+ .tooltip__title {
54
+ display: block;
55
+ margin-block-end: var(--nox-space-2);
56
+ }
57
+
58
+ .tooltip__text {
59
+ white-space: pre-line;
60
+ }
61
+
53
62
  @media (width < 30rem) {
54
63
  .tooltip__panel {
55
64
  --tooltip-viewport-gap: var(--nox-space-3);
@@ -11,6 +11,7 @@ export { default as CardHeader } from './content/CardHeader/CardHeader.astro';
11
11
  export { default as CardLink } from './content/CardLink/CardLink.astro';
12
12
  export { default as CodeBlock } from './content/CodeBlock/CodeBlock.astro';
13
13
  export { default as InlineDefinition } from './content/InlineDefinition/InlineDefinition.astro';
14
+ export { default as AnnotatedText } from './content/AnnotatedText/AnnotatedText.astro';
14
15
  export { default as MarkdownContent } from './content/MarkdownContent/MarkdownContent.astro';
15
16
  export { default as BadgeList } from './data-display/BadgeList/BadgeList.astro';
16
17
  export { default as IconList } from './data-display/IconList/IconList.astro';
@@ -0,0 +1,14 @@
1
+ /** Content-only spans; tooltip bodies deliberately cannot contain interactions. */
2
+ export type DisplayTextSpan = {
3
+ text: string;
4
+ emphasized?: boolean;
5
+ };
6
+
7
+ export type DisplayTooltip = {
8
+ title?: string;
9
+ content: readonly DisplayTextSpan[];
10
+ };
11
+
12
+ export type DisplayAnnotatedText = readonly (DisplayTextSpan & {
13
+ tooltip?: DisplayTooltip;
14
+ })[];
@@ -1,5 +1,6 @@
1
1
  import type { AstroComponent } from '../components/widgetTypes';
2
2
  import type { Tone } from '../theme/contracts';
3
+ import type { DisplayAnnotatedText, DisplayTooltip } from './annotatedText';
3
4
 
4
5
  export type DisplayTextValue = string | number;
5
6
 
@@ -217,12 +218,12 @@ export type TableAction = TableActionBase &
217
218
  );
218
219
 
219
220
  export type TableCell = {
220
- tooltip?: string;
221
+ tooltip?: string | DisplayTooltip;
221
222
  } & (
222
223
  | {
223
224
  kind: 'text';
224
225
  value: DisplayTextValue;
225
- description?: string;
226
+ description?: string | DisplayAnnotatedText;
226
227
  layout?: TextPairLayout;
227
228
  primaryColor?: TextColor;
228
229
  primarySize?: TextSize;
@@ -233,7 +234,7 @@ export type TableCell = {
233
234
  kind: 'link';
234
235
  label: string;
235
236
  href: string;
236
- description?: string;
237
+ description?: string | DisplayAnnotatedText;
237
238
  layout?: TextPairLayout;
238
239
  primaryColor?: TextColor;
239
240
  primarySize?: TextSize;
@@ -1,3 +1,4 @@
1
1
  export * from './contracts';
2
+ export * from './annotatedText';
2
3
  export * from './responsiveMedia';
3
4
  export * from './tableProgressive';
@@ -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
+ }