@astronox/core 0.5.6 → 0.5.8

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.6",
3
+ "version": "0.5.8",
4
4
  "description": "Reusable Astro components, patterns, styles, and infrastructure for AstroNox sites.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -0,0 +1,210 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types';
3
+ import Media from '../../primitives/media/Media/Media.astro';
4
+ import PuzzleGrid from '../PuzzleGrid/PuzzleGrid.astro';
5
+ import { presentLightMazeCell } from './presentation';
6
+ import type {
7
+ LightMazePuzzleLabels,
8
+ LightMazePuzzleMedia,
9
+ LightMazePuzzleModel,
10
+ } from './contracts';
11
+ import {
12
+ createLightMazePuzzleState,
13
+ evaluateLightMazeMechanism,
14
+ } from './simulation';
15
+
16
+ type Props = HTMLAttributes<'section'> & {
17
+ label: string;
18
+ labels: LightMazePuzzleLabels;
19
+ media: LightMazePuzzleMedia;
20
+ puzzles: readonly LightMazePuzzleModel[];
21
+ };
22
+
23
+ const {
24
+ class: className,
25
+ label,
26
+ labels,
27
+ media,
28
+ puzzles,
29
+ ...attrs
30
+ } = Astro.props;
31
+
32
+ if (puzzles.some((puzzle) => puzzle.activators?.length) && !media.mechanism)
33
+ throw new Error('Light maze mechanisms require display media.');
34
+ ---
35
+
36
+ <section
37
+ {...attrs}
38
+ class:list={['light-maze-puzzle', className]}
39
+ aria-label={label}
40
+ data-light-maze-puzzle
41
+ >
42
+ {
43
+ puzzles.map((puzzle, index) => {
44
+ const initial = createLightMazePuzzleState(puzzle);
45
+ const mechanism = evaluateLightMazeMechanism(puzzle, initial);
46
+ return (
47
+ <div
48
+ class="light-maze-puzzle__instance"
49
+ data-puzzle-id={puzzle.id}
50
+ data-puzzle-status="incomplete"
51
+ data-light-maze-model={JSON.stringify(puzzle)}
52
+ data-light-maze-labels={JSON.stringify(labels)}
53
+ hidden={index !== 0}
54
+ style={`--piece-selection-filter: url(#light-maze-outline-${encodeURIComponent(puzzle.id)}); --piece-hover-filter: url(#light-maze-hover-${encodeURIComponent(puzzle.id)});`}
55
+ >
56
+ <svg
57
+ class="light-maze-puzzle__filters"
58
+ aria-hidden="true"
59
+ focusable="false"
60
+ >
61
+ <defs>
62
+ <filter
63
+ id={`light-maze-hover-${encodeURIComponent(puzzle.id)}`}
64
+ color-interpolation-filters="sRGB"
65
+ >
66
+ <feFlood
67
+ class="light-maze-puzzle__hover-color"
68
+ flood-opacity="0.1"
69
+ />
70
+ <feComposite operator="in" in2="SourceAlpha" />
71
+ <feMerge>
72
+ <feMergeNode in="SourceGraphic" />
73
+ <feMergeNode />
74
+ </feMerge>
75
+ </filter>
76
+ <filter
77
+ id={`light-maze-outline-${encodeURIComponent(puzzle.id)}`}
78
+ x="-25%"
79
+ y="-25%"
80
+ width="150%"
81
+ height="150%"
82
+ color-interpolation-filters="sRGB"
83
+ >
84
+ <feMorphology in="SourceAlpha" operator="dilate" radius="2" />
85
+ <feGaussianBlur stdDeviation="0.5" result="outline" />
86
+ <feFlood class="light-maze-puzzle__ring-color" />
87
+ <feComposite operator="in" in2="outline" />
88
+ <feMerge>
89
+ <feMergeNode />
90
+ <feMergeNode in="SourceGraphic" />
91
+ </feMerge>
92
+ </filter>
93
+ </defs>
94
+ </svg>
95
+ <PuzzleGrid
96
+ class="light-maze-puzzle__board"
97
+ label={puzzle.label}
98
+ rows={puzzle.rows}
99
+ columns={puzzle.columns}
100
+ >
101
+ {Array.from(
102
+ { length: puzzle.rows * puzzle.columns },
103
+ (_, cellIndex) => {
104
+ const row = Math.floor(cellIndex / puzzle.columns);
105
+ const column = cellIndex % puzzle.columns;
106
+ const {
107
+ disabled: blocked,
108
+ target,
109
+ lit,
110
+ activator,
111
+ consumed,
112
+ removableWall,
113
+ releasedWall,
114
+ label: cellLabel,
115
+ } = presentLightMazeCell(puzzle, mechanism, labels, {
116
+ row,
117
+ column,
118
+ });
119
+ return (
120
+ <button
121
+ type="button"
122
+ class="puzzle-grid__cell light-maze-puzzle__cell"
123
+ disabled={blocked}
124
+ tabindex={-1}
125
+ aria-label={cellLabel}
126
+ data-puzzle-cell
127
+ data-puzzle-cell-disabled={blocked ? '' : undefined}
128
+ data-light-maze-cell
129
+ data-light-maze-lit={lit ? '' : undefined}
130
+ data-light-maze-target={target ? '' : undefined}
131
+ data-light-maze-wall={removableWall ? '' : undefined}
132
+ data-light-maze-wall-released={
133
+ releasedWall ? '' : undefined
134
+ }
135
+ data-light-maze-activator={activator ? '' : undefined}
136
+ data-light-maze-activator-consumed={
137
+ consumed ? '' : undefined
138
+ }
139
+ data-cell-row={row}
140
+ data-cell-column={column}
141
+ data-cell-label={cellLabel}
142
+ style={`--cell-row: ${row + 1}; --cell-column: ${column + 1};`}
143
+ >
144
+ {activator && media.mechanism && (
145
+ <span
146
+ class="light-maze-puzzle__activator"
147
+ aria-hidden="true"
148
+ >
149
+ <span data-activator-inactive>
150
+ <Media
151
+ media={media.mechanism.activatorInactive}
152
+ size="fill"
153
+ />
154
+ </span>
155
+ <span data-activator-active>
156
+ <Media
157
+ media={media.mechanism.activatorActive}
158
+ size="fill"
159
+ />
160
+ </span>
161
+ </span>
162
+ )}
163
+ {removableWall && (
164
+ <span
165
+ class="light-maze-puzzle__wall-fill"
166
+ data-wall-fill
167
+ aria-hidden="true"
168
+ >
169
+ <svg
170
+ class="light-maze-puzzle__wall-cracks"
171
+ viewBox="0 0 100 100"
172
+ focusable="false"
173
+ >
174
+ <path d="M29 0L34 17L27 29L40 39L37 56M27 29L15 34L9 46" />
175
+ <path d="M100 31L82 37L78 49L63 55L58 69M78 49L85 59L83 70" />
176
+ <path d="M24 100L32 84L29 75L43 67M32 84L47 89" />
177
+ </svg>
178
+ </span>
179
+ )}
180
+ </button>
181
+ );
182
+ },
183
+ )}
184
+ {(['walker', 'light'] as const).map((piece) => (
185
+ <button
186
+ type="button"
187
+ class="light-maze-puzzle__piece"
188
+ aria-label={labels[piece]}
189
+ aria-pressed="false"
190
+ data-light-maze-piece={piece}
191
+ style={`--piece-row: ${initial[piece].row + 1}; --piece-column: ${initial[piece].column + 1};`}
192
+ >
193
+ <span class="light-maze-puzzle__piece-media" aria-hidden="true">
194
+ <Media media={media[piece]} size="fill" />
195
+ </span>
196
+ </button>
197
+ ))}
198
+ </PuzzleGrid>
199
+ </div>
200
+ );
201
+ })
202
+ }
203
+ </section>
204
+
205
+ <script>
206
+ import { initLightMazePuzzle } from './lightMazePuzzleController';
207
+ for (const root of document.querySelectorAll('[data-light-maze-puzzle]')) {
208
+ if (root instanceof HTMLElement) initLightMazePuzzle(root);
209
+ }
210
+ </script>
@@ -0,0 +1,189 @@
1
+ .light-maze-puzzle,
2
+ .light-maze-puzzle__instance {
3
+ display: grid;
4
+ min-inline-size: 0;
5
+ }
6
+
7
+ .light-maze-puzzle__instance[hidden] {
8
+ display: none;
9
+ }
10
+
11
+ .light-maze-puzzle__filters {
12
+ position: absolute;
13
+ inline-size: 0;
14
+ block-size: 0;
15
+ pointer-events: none;
16
+ }
17
+
18
+ .light-maze-puzzle__ring-color {
19
+ flood-color: var(--nox-source-tone-info);
20
+ }
21
+ .light-maze-puzzle__hover-color {
22
+ flood-color: var(--nox-tone-color);
23
+ }
24
+
25
+ .light-maze-puzzle__board {
26
+ isolation: isolate;
27
+ touch-action: manipulation;
28
+ }
29
+
30
+ .light-maze-puzzle__cell {
31
+ appearance: none;
32
+ }
33
+
34
+ .light-maze-puzzle__activator {
35
+ position: absolute;
36
+ inset: 18%;
37
+ display: grid;
38
+ pointer-events: none;
39
+ }
40
+
41
+ .light-maze-puzzle__activator > span {
42
+ display: grid;
43
+ grid-area: 1 / 1;
44
+ }
45
+
46
+ .light-maze-puzzle__cell:not([data-light-maze-activator-consumed])
47
+ [data-activator-active],
48
+ .light-maze-puzzle__cell[data-light-maze-activator-consumed]
49
+ [data-activator-inactive] {
50
+ display: none;
51
+ }
52
+
53
+ .light-maze-puzzle__wall-fill {
54
+ position: absolute;
55
+ inset: 0;
56
+ display: grid;
57
+ border-radius: inherit;
58
+ overflow: hidden;
59
+ background: var(--nox-surface-strong);
60
+ pointer-events: none;
61
+ }
62
+
63
+ .light-maze-puzzle__wall-cracks {
64
+ position: absolute;
65
+ inset: 0;
66
+ inline-size: 100%;
67
+ block-size: 100%;
68
+ fill: none;
69
+ stroke: var(--nox-text-muted);
70
+ stroke-width: 1.5;
71
+ stroke-linecap: round;
72
+ stroke-linejoin: round;
73
+ }
74
+
75
+ .light-maze-puzzle__cell[data-light-maze-wall-released]:not(
76
+ [data-wall-removing]
77
+ )
78
+ .light-maze-puzzle__wall-fill {
79
+ display: none;
80
+ }
81
+
82
+ .light-maze-puzzle__cell[data-light-maze-lit]:not([data-puzzle-cell-disabled]) {
83
+ --puzzle-cell-base-background: color-mix(
84
+ in srgb,
85
+ var(--nox-source-tone-warning) 12%,
86
+ var(--nox-surface)
87
+ );
88
+ }
89
+
90
+ .light-maze-puzzle__cell[data-light-maze-target] {
91
+ --puzzle-cell-base-border: var(--nox-source-tone-success);
92
+ border-style: dashed;
93
+ }
94
+
95
+ .light-maze-puzzle__cell[data-light-maze-destination] {
96
+ --puzzle-cell-ring-width: var(--nox-focus-ring-width);
97
+ --puzzle-cell-ring-color: var(--nox-source-tone-info);
98
+ z-index: 3;
99
+ overflow: visible;
100
+ }
101
+
102
+ .light-maze-puzzle__piece {
103
+ position: relative;
104
+ z-index: 4;
105
+ display: grid;
106
+ grid-row: var(--piece-row);
107
+ grid-column: var(--piece-column);
108
+ min-inline-size: 0;
109
+ min-block-size: 0;
110
+ margin: var(--nox-space-1);
111
+ padding: 0;
112
+ place-items: center;
113
+ border: 0;
114
+ border-radius: var(--nox-radius-sm);
115
+ background: transparent;
116
+ cursor: grab;
117
+ touch-action: none;
118
+ user-select: none;
119
+ }
120
+
121
+ .light-maze-puzzle__piece-media {
122
+ display: grid;
123
+ inline-size: 80%;
124
+ aspect-ratio: 1;
125
+ pointer-events: none;
126
+ }
127
+
128
+ .light-maze-puzzle__piece:focus-visible {
129
+ outline: none;
130
+ }
131
+ .light-maze-puzzle__piece:is([data-piece-selected], :focus-visible)
132
+ .light-maze-puzzle__piece-media {
133
+ filter: var(--piece-selection-filter);
134
+ }
135
+
136
+ @media (hover: hover) {
137
+ .light-maze-puzzle__piece:hover .light-maze-puzzle__piece-media {
138
+ filter: var(--piece-hover-filter);
139
+ }
140
+ .light-maze-puzzle__piece:is([data-piece-selected], :focus-visible):hover
141
+ .light-maze-puzzle__piece-media {
142
+ filter: var(--piece-hover-filter) var(--piece-selection-filter);
143
+ }
144
+ }
145
+
146
+ .light-maze-puzzle__piece[data-piece-dragging] {
147
+ z-index: 6;
148
+ cursor: grabbing;
149
+ }
150
+ .light-maze-puzzle__piece[data-piece-dragging] .light-maze-puzzle__piece-media {
151
+ transform: translate3d(var(--piece-drag-x, 0px), var(--piece-drag-y, 0px), 0)
152
+ scale(1.04);
153
+ will-change: transform;
154
+ }
155
+ .light-maze-puzzle__instance[data-puzzle-status='valid']
156
+ .light-maze-puzzle__piece {
157
+ cursor: default;
158
+ }
159
+
160
+ .light-maze-puzzle__instance[data-light-maze-transitioning]
161
+ .light-maze-puzzle__piece {
162
+ cursor: default;
163
+ }
164
+
165
+ .light-maze-puzzle__instance[data-light-maze-transitioning]
166
+ .light-maze-puzzle__piece
167
+ .light-maze-puzzle__piece-media {
168
+ filter: none;
169
+ }
170
+
171
+ .light-maze-puzzle__instance[data-light-maze-transitioning]
172
+ .light-maze-puzzle__cell[data-puzzle-cell] {
173
+ --puzzle-cell-ring-width: 0px;
174
+ }
175
+
176
+ @media (prefers-reduced-motion: no-preference) {
177
+ .light-maze-puzzle__piece[data-piece-moved] {
178
+ animation: light-maze-puzzle-move 260ms var(--nox-motion-easing-standard);
179
+ }
180
+ }
181
+ @keyframes light-maze-puzzle-move {
182
+ from {
183
+ transform: translate3d(
184
+ var(--piece-move-inline-distance),
185
+ var(--piece-move-block-distance),
186
+ 0
187
+ );
188
+ }
189
+ }
@@ -0,0 +1,75 @@
1
+ import type { DisplayMedia } from '../../display/contracts';
2
+
3
+ export type LightMazePosition = { row: number; column: number };
4
+ export type LightMazeDirection = 'north' | 'east' | 'south' | 'west';
5
+ export type LightMazePiece = 'walker' | 'light';
6
+ export type LightMazePuzzleStatus = 'incomplete' | 'valid';
7
+
8
+ export type LightMazePuzzleModel = {
9
+ id: string;
10
+ label: string;
11
+ rows: number;
12
+ columns: number;
13
+ start: LightMazePosition;
14
+ light: LightMazePosition;
15
+ target: LightMazePosition;
16
+ blockedCells: readonly LightMazePosition[];
17
+ activators?: readonly {
18
+ position: LightMazePosition;
19
+ walls: readonly LightMazePosition[];
20
+ }[];
21
+ };
22
+
23
+ export type LightMazeMechanism = {
24
+ illuminatedCells: readonly LightMazePosition[];
25
+ blockedCells: readonly LightMazePosition[];
26
+ activatedCells: readonly LightMazePosition[];
27
+ };
28
+
29
+ export type LightMazePuzzleState = {
30
+ walker: LightMazePosition;
31
+ light: LightMazePosition;
32
+ activated: readonly number[];
33
+ };
34
+
35
+ export type LightMazeAction = {
36
+ piece: LightMazePiece;
37
+ direction: LightMazeDirection;
38
+ };
39
+
40
+ export type LightMazeMovement = LightMazeAction & {
41
+ from: LightMazePosition;
42
+ to: LightMazePosition;
43
+ stoppedByWalker: boolean;
44
+ state: LightMazePuzzleState;
45
+ };
46
+
47
+ export type LightMazePuzzleLabels = {
48
+ walker: string;
49
+ light: string;
50
+ target: string;
51
+ blockedCell: string;
52
+ litCell: string;
53
+ darkCell: string;
54
+ directions: Readonly<Record<LightMazeDirection, string>>;
55
+ moveTemplate: string;
56
+ mechanism?: {
57
+ activatorAvailable: string;
58
+ activatorConsumed: string;
59
+ removableWall: string;
60
+ releasedWall: string;
61
+ };
62
+ };
63
+
64
+ export type LightMazePuzzleMedia = Record<LightMazePiece, DisplayMedia> & {
65
+ mechanism?: {
66
+ activatorInactive: DisplayMedia;
67
+ activatorActive: DisplayMedia;
68
+ };
69
+ };
70
+
71
+ export type LightMazePuzzleChangeDetail = {
72
+ kind: 'light-maze';
73
+ puzzleId: string;
74
+ state: LightMazePuzzleState;
75
+ };