@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.
- package/README.md +13 -0
- package/package.json +1 -1
- package/src/components/content/AnnotatedText/AnnotatedText.astro +38 -0
- package/src/components/content/AnnotatedText/AnnotatedText.css +19 -0
- package/src/components/content/AnnotatedText/MarkedText.astro +11 -0
- package/src/components/content/InlineDefinition/InlineDefinition.astro +11 -2
- package/src/components/data-display/TextPair/TextPair.astro +13 -2
- package/src/components/feedback/Tooltip/Tooltip.astro +18 -3
- package/src/components/feedback/Tooltip/Tooltip.css +9 -0
- package/src/components/index.ts +1 -0
- package/src/display/annotatedText.ts +14 -0
- package/src/display/contracts.ts +4 -3
- package/src/display/index.ts +1 -0
- package/src/patterns/MagneticFieldPuzzle/MagneticFieldPuzzle.css +0 -79
- package/src/patterns/MagneticFieldPuzzle/magneticFieldPuzzleController.ts +14 -148
- package/src/patterns/PuzzleGrid/movement.css +80 -0
- package/src/patterns/PuzzleGrid/movement.ts +149 -0
- package/src/patterns/PuzzleGrid/selectionDismissal.ts +4 -2
- package/src/patterns/SlidingPuzzle/SlidingPuzzle.astro +181 -0
- package/src/patterns/SlidingPuzzle/SlidingPuzzle.css +177 -0
- package/src/patterns/SlidingPuzzle/contracts.ts +57 -0
- package/src/patterns/SlidingPuzzle/simulation.ts +106 -0
- package/src/patterns/SlidingPuzzle/slidingPuzzleController.ts +703 -0
- package/src/patterns/Table/TableBody.astro +7 -1
- package/src/patterns/Table/TableCell.astro +3 -1
- package/src/patterns/Table/TableCellValue.astro +10 -1
- package/src/patterns/index.ts +4 -0
- package/src/primitives/text/TextBlock/TextBlock.astro +54 -4
- 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
|
@@ -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
|
-
{
|
|
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
|
-
{
|
|
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);
|
package/src/components/index.ts
CHANGED
|
@@ -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
|
+
})[];
|
package/src/display/contracts.ts
CHANGED
|
@@ -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;
|
package/src/display/index.ts
CHANGED
|
@@ -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:
|
|
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(
|
|
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
|
-
|
|
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-
|
|
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
|
+
}
|