@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.
@@ -0,0 +1,55 @@
1
+ import type {
2
+ LightMazeMechanism,
3
+ LightMazePosition,
4
+ LightMazePuzzleLabels,
5
+ LightMazePuzzleModel,
6
+ } from './contracts';
7
+
8
+ export function presentLightMazeCell(
9
+ puzzle: LightMazePuzzleModel,
10
+ mechanism: LightMazeMechanism,
11
+ labels: LightMazePuzzleLabels,
12
+ position: LightMazePosition,
13
+ ) {
14
+ const matches = (cell: LightMazePosition): boolean =>
15
+ cell.row === position.row && cell.column === position.column;
16
+ const activator =
17
+ puzzle.activators?.some((entry) => matches(entry.position)) ?? false;
18
+ const consumed = mechanism.activatedCells.some(matches);
19
+ const disabled = mechanism.blockedCells.some(matches);
20
+ const removableWall =
21
+ puzzle.activators?.some((entry) => entry.walls.some(matches)) ?? false;
22
+ const releasedWall = removableWall && !disabled;
23
+ const target = matches(puzzle.target);
24
+ const lit = mechanism.illuminatedCells.some(matches);
25
+ if ((activator || removableWall) && !labels.mechanism)
26
+ throw new Error('Light maze mechanisms require accessible labels.');
27
+ let activationLabel = '';
28
+ if (activator) {
29
+ activationLabel = consumed
30
+ ? labels.mechanism!.activatorConsumed
31
+ : labels.mechanism!.activatorAvailable;
32
+ }
33
+ const label = disabled
34
+ ? removableWall
35
+ ? labels.mechanism!.removableWall
36
+ : labels.blockedCell
37
+ : [
38
+ target ? labels.target : '',
39
+ activationLabel,
40
+ releasedWall ? labels.mechanism!.releasedWall : '',
41
+ lit ? labels.litCell : labels.darkCell,
42
+ ]
43
+ .filter(Boolean)
44
+ .join(' — ');
45
+ return {
46
+ disabled,
47
+ target,
48
+ lit,
49
+ activator,
50
+ consumed,
51
+ removableWall,
52
+ releasedWall,
53
+ label,
54
+ };
55
+ }
@@ -0,0 +1,164 @@
1
+ import type {
2
+ LightMazeAction,
3
+ LightMazeDirection,
4
+ LightMazeMovement,
5
+ LightMazeMechanism,
6
+ LightMazePosition,
7
+ LightMazePuzzleModel,
8
+ LightMazePuzzleState,
9
+ } from './contracts';
10
+
11
+ const deltas = {
12
+ north: [-1, 0],
13
+ east: [0, 1],
14
+ south: [1, 0],
15
+ west: [0, -1],
16
+ } as const;
17
+
18
+ function same(a: LightMazePosition, b: LightMazePosition): boolean {
19
+ return a.row === b.row && a.column === b.column;
20
+ }
21
+
22
+ function free(
23
+ puzzle: LightMazePuzzleModel,
24
+ position: LightMazePosition,
25
+ blockedCells: readonly LightMazePosition[],
26
+ ): boolean {
27
+ return (
28
+ position.row >= 0 &&
29
+ position.row < puzzle.rows &&
30
+ position.column >= 0 &&
31
+ position.column < puzzle.columns &&
32
+ !blockedCells.some((cell) => same(cell, position))
33
+ );
34
+ }
35
+
36
+ export function createLightMazePuzzleState(
37
+ puzzle: LightMazePuzzleModel,
38
+ ): LightMazePuzzleState {
39
+ return {
40
+ walker: { ...puzzle.start },
41
+ light: { ...puzzle.light },
42
+ activated: [],
43
+ };
44
+ }
45
+
46
+ function blockedCells(
47
+ puzzle: LightMazePuzzleModel,
48
+ state: LightMazePuzzleState,
49
+ ): LightMazePosition[] {
50
+ return [
51
+ ...puzzle.blockedCells,
52
+ ...(puzzle.activators ?? []).flatMap((activator, index) =>
53
+ state.activated.includes(index) ? [] : activator.walls,
54
+ ),
55
+ ];
56
+ }
57
+
58
+ export function isLightMazePuzzleComplete(
59
+ puzzle: LightMazePuzzleModel,
60
+ state: LightMazePuzzleState,
61
+ ): boolean {
62
+ return same(state.walker, puzzle.target);
63
+ }
64
+
65
+ export function getLightMazeIllumination(
66
+ puzzle: LightMazePuzzleModel,
67
+ state: LightMazePuzzleState,
68
+ ): LightMazePosition[] {
69
+ const cells = [{ ...state.light }];
70
+ const blocked = blockedCells(puzzle, state);
71
+ for (const [dr, dc] of Object.values(deltas)) {
72
+ for (let distance = 1; distance <= 2; distance += 1) {
73
+ const cell = {
74
+ row: state.light.row + dr * distance,
75
+ column: state.light.column + dc * distance,
76
+ };
77
+ if (!free(puzzle, cell, blocked)) break;
78
+ cells.push(cell);
79
+ }
80
+ }
81
+ return cells;
82
+ }
83
+
84
+ export function evaluateLightMazeMechanism(
85
+ puzzle: LightMazePuzzleModel,
86
+ state: LightMazePuzzleState,
87
+ ): LightMazeMechanism {
88
+ const illuminatedCells = getLightMazeIllumination(puzzle, state);
89
+ return {
90
+ illuminatedCells,
91
+ blockedCells: blockedCells(puzzle, state),
92
+ activatedCells: (puzzle.activators ?? []).flatMap((activator, index) =>
93
+ state.activated.includes(index) ? [activator.position] : [],
94
+ ),
95
+ };
96
+ }
97
+
98
+ export function simulateLightMazeMove(
99
+ puzzle: LightMazePuzzleModel,
100
+ state: LightMazePuzzleState,
101
+ action: LightMazeAction,
102
+ ): LightMazeMovement | null {
103
+ if (isLightMazePuzzleComplete(puzzle, state)) return null;
104
+ const mechanism = evaluateLightMazeMechanism(puzzle, state);
105
+ const traversable = (position: LightMazePosition): boolean =>
106
+ free(puzzle, position, mechanism.blockedCells);
107
+ const [dr, dc] = deltas[action.direction];
108
+ const from = state[action.piece];
109
+ let to = { ...from };
110
+ let stoppedByWalker = false;
111
+ const crossed: LightMazePosition[] = [];
112
+ if (action.piece === 'walker') {
113
+ to = { row: from.row + dr, column: from.column + dc };
114
+ if (
115
+ !traversable(to) ||
116
+ same(to, state.light) ||
117
+ puzzle.activators?.some((activator) => same(activator.position, to)) ||
118
+ !mechanism.illuminatedCells.some((cell) => same(cell, to))
119
+ )
120
+ return null;
121
+ } else {
122
+ while (true) {
123
+ const next = { row: to.row + dr, column: to.column + dc };
124
+ if (!traversable(next)) break;
125
+ if (same(next, state.walker)) {
126
+ stoppedByWalker = true;
127
+ break;
128
+ }
129
+ to = next;
130
+ crossed.push(next);
131
+ }
132
+ if (same(from, to)) return null;
133
+ }
134
+ const activated = (puzzle.activators ?? []).flatMap((activator, index) =>
135
+ state.activated.includes(index) ||
136
+ crossed.some((position) => same(activator.position, position))
137
+ ? [index]
138
+ : [],
139
+ );
140
+ return {
141
+ ...action,
142
+ from: { ...from },
143
+ to,
144
+ stoppedByWalker,
145
+ state: {
146
+ walker: { ...state.walker },
147
+ light: { ...state.light },
148
+ [action.piece]: to,
149
+ activated,
150
+ },
151
+ };
152
+ }
153
+
154
+ export function getLightMazeMoves(
155
+ puzzle: LightMazePuzzleModel,
156
+ state: LightMazePuzzleState,
157
+ ): LightMazeMovement[] {
158
+ return (['walker', 'light'] as const).flatMap((piece) =>
159
+ (Object.keys(deltas) as LightMazeDirection[]).flatMap((direction) => {
160
+ const move = simulateLightMazeMove(puzzle, state, { piece, direction });
161
+ return move ? [move] : [];
162
+ }),
163
+ );
164
+ }
@@ -98,12 +98,16 @@ const {
98
98
  );
99
99
  const target =
100
100
  puzzle.target.row === row && puzzle.target.column === column;
101
- const hole = puzzle.holes.some(
101
+ const hole = puzzle.holes.find(
102
102
  ({ position }) =>
103
103
  position.row === row && position.column === column,
104
104
  );
105
+ const exit = puzzle.holes.find(
106
+ ({ exit }) => exit.row === row && exit.column === column,
107
+ );
108
+ const transferTone = (hole ?? exit)?.tone ?? 'neutral';
105
109
  const fragile =
106
- hole ||
110
+ Boolean(hole) ||
107
111
  puzzle.fragileCells.some(
108
112
  (cell) => cell.row === row && cell.column === column,
109
113
  );
@@ -130,7 +134,7 @@ const {
130
134
  data-cell-row={row}
131
135
  data-cell-column={column}
132
136
  data-cell-label={cellLabel}
133
- style={`--cell-row: ${row + 1}; --cell-column: ${column + 1};`}
137
+ style={`--cell-row: ${row + 1}; --cell-column: ${column + 1}; --sliding-transfer-color: var(--nox-source-tone-${transferTone});`}
134
138
  >
135
139
  {fragile && (
136
140
  <svg
@@ -32,6 +32,12 @@
32
32
  appearance: none;
33
33
  }
34
34
 
35
+ .sliding-puzzle__cell[data-sliding-exit-discovered] {
36
+ --puzzle-cell-base-border: var(--sliding-transfer-color);
37
+
38
+ border-style: dashed;
39
+ }
40
+
35
41
  .sliding-puzzle__cell[data-sliding-target] {
36
42
  --puzzle-cell-base-border: var(--nox-source-tone-success);
37
43
 
@@ -41,7 +47,7 @@
41
47
  .sliding-puzzle__cell[data-sliding-fragile]:not([data-sliding-collapsed]):not(
42
48
  [data-sliding-hole-open]
43
49
  ) {
44
- --puzzle-cell-base-border: var(--nox-source-tone-info);
50
+ --puzzle-cell-base-border: var(--nox-source-tone-neutral);
45
51
 
46
52
  border-style: dashed;
47
53
  }
@@ -52,20 +58,20 @@
52
58
  inline-size: 100%;
53
59
  block-size: 100%;
54
60
  fill: none;
55
- stroke: var(--nox-source-tone-info);
61
+ stroke: var(--nox-source-tone-neutral);
56
62
  stroke-width: 3;
57
63
  pointer-events: none;
58
64
  }
59
65
 
60
66
  .sliding-puzzle__cell[data-sliding-collapsed] .sliding-puzzle__crack {
61
- stroke: var(--nox-text-disabled);
67
+ display: none;
62
68
  }
63
69
 
64
70
  .sliding-puzzle__hole {
65
71
  display: none;
66
72
  inline-size: 72%;
67
73
  aspect-ratio: 1;
68
- border: calc(var(--nox-border-width) * 2) solid var(--nox-source-tone-info);
74
+ border: calc(var(--nox-border-width) * 2) solid var(--sliding-transfer-color);
69
75
  border-radius: var(--nox-radius-pill);
70
76
  background: color-mix(
71
77
  in srgb,
@@ -128,8 +134,10 @@
128
134
  outline: none;
129
135
  }
130
136
 
131
- .sliding-puzzle__piece[data-piece-selected] .sliding-puzzle__piece-media,
132
- .sliding-puzzle__piece:focus-visible .sliding-puzzle__piece-media {
137
+ .sliding-puzzle__piece:is([data-piece-selected], :focus-visible):not(
138
+ [data-piece-transferring]
139
+ )
140
+ .sliding-puzzle__piece-media {
133
141
  filter: var(--piece-selection-filter);
134
142
  }
135
143
 
@@ -138,9 +146,10 @@
138
146
  filter: var(--piece-hover-filter);
139
147
  }
140
148
 
141
- .sliding-puzzle__piece[data-piece-selected]:hover
142
- .sliding-puzzle__piece-media,
143
- .sliding-puzzle__piece:focus-visible:hover .sliding-puzzle__piece-media {
149
+ .sliding-puzzle__piece:is([data-piece-selected], :focus-visible):hover:not(
150
+ [data-piece-transferring]
151
+ )
152
+ .sliding-puzzle__piece-media {
144
153
  filter: var(--piece-hover-filter) var(--piece-selection-filter);
145
154
  }
146
155
  }
@@ -1,4 +1,5 @@
1
1
  import type { DisplayMedia } from '../../display/contracts';
2
+ import type { Tone } from '../../theme/contracts';
2
3
 
3
4
  export type SlidingDirection = 'north' | 'east' | 'south' | 'west';
4
5
  export type SlidingPosition = { row: number; column: number };
@@ -15,7 +16,11 @@ export type SlidingPuzzleModel = {
15
16
  fragileCells: readonly SlidingPosition[];
16
17
  // Entry direction selects the adjacent landing cell around each exit.
17
18
  // A transfer ends the move; its landing never triggers another tile.
18
- holes: readonly { position: SlidingPosition; exit: SlidingPosition }[];
19
+ holes: readonly {
20
+ position: SlidingPosition;
21
+ exit: SlidingPosition;
22
+ tone?: Tone;
23
+ }[];
19
24
  };
20
25
 
21
26
  export type SlidingPuzzleState = {
@@ -353,9 +353,6 @@ async function move(
353
353
  instance.selected =
354
354
  retainSelection && !isSlidingPuzzleComplete(puzzle, movement.state);
355
355
  render(root, false, movement.slideTo);
356
- // Ordinary slides expose the next moves immediately, like the magnetic
357
- // puzzle. A transfer keeps the current preview until the piece reappears,
358
- // so outgoing paths do not disclose its hidden landing position early.
359
356
  if (!movement.teleported || !instance.selected) previews(root);
360
357
  try {
361
358
  if (
@@ -371,14 +368,22 @@ async function move(
371
368
  )
372
369
  return;
373
370
  if (movement.teleported) {
374
- if (!(await animateTransfer(piece, 'fall', motion.signal))) return;
375
- render(root, false);
376
- previews(root);
377
- if (!(await animateTransfer(piece, 'arrive', motion.signal))) return;
371
+ clearPreviews(panel);
372
+ if (
373
+ !(await animateHoleTransfer(
374
+ root,
375
+ piece,
376
+ puzzle,
377
+ movement,
378
+ motion.signal,
379
+ ))
380
+ )
381
+ return;
378
382
  }
379
383
  if (motion.signal.aborted) return;
380
384
  instance.motion = null;
381
385
  render(root);
386
+ if (movement.teleported) previews(root);
382
387
  emitChange(root);
383
388
  } finally {
384
389
  motion.signal.removeEventListener('abort', clearBusy);
@@ -387,6 +392,51 @@ async function move(
387
392
  }
388
393
  }
389
394
 
395
+ async function animateHoleTransfer(
396
+ root: HTMLElement,
397
+ piece: HTMLButtonElement,
398
+ puzzle: SlidingPuzzleModel,
399
+ movement: SlidingMovement,
400
+ signal: AbortSignal,
401
+ ): Promise<boolean> {
402
+ if (signal.aborted) return false;
403
+ const clear = (): void => piece.removeAttribute('data-piece-transferring');
404
+ piece.setAttribute('data-piece-transferring', '');
405
+ signal.addEventListener('abort', clear, { once: true });
406
+ try {
407
+ if (!(await animateTransferPhase(piece, 'fall', signal))) return false;
408
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches)
409
+ return true;
410
+ const hole = puzzle.holes.find(
411
+ ({ position }) =>
412
+ position.row === movement.slideTo.row &&
413
+ position.column === movement.slideTo.column,
414
+ )!;
415
+ render(root, false, hole.exit);
416
+ const panel = activePanel(root);
417
+ if (panel) renderExitMarkers(panel, movement.state);
418
+ if (!(await animateTransferPhase(piece, 'arrive', signal))) return false;
419
+ if (!(await animateLanding(piece, signal))) return false;
420
+ const exitBounds = piece.getBoundingClientRect();
421
+ render(root, false);
422
+ const landingBounds = piece.getBoundingClientRect();
423
+ return await animatePieceTransition(
424
+ piece,
425
+ [
426
+ {
427
+ transform: `translate3d(${exitBounds.left - landingBounds.left}px, ${exitBounds.top - landingBounds.top}px, 0)`,
428
+ },
429
+ { transform: 'translate3d(0, 0, 0)' },
430
+ ],
431
+ 120,
432
+ signal,
433
+ );
434
+ } finally {
435
+ signal.removeEventListener('abort', clear);
436
+ clear();
437
+ }
438
+ }
439
+
390
440
  function animateSlide(
391
441
  panel: HTMLElement,
392
442
  piece: HTMLButtonElement,
@@ -398,8 +448,6 @@ function animateSlide(
398
448
  ): Promise<boolean> {
399
449
  const finalBounds = piece.getBoundingClientRect();
400
450
  animatePieceMovement(piece, bounds);
401
- // Follow the actual slide so each tile changes as the piece enters it,
402
- // including the tile where it stops. No delayed collapse on departure.
403
451
  const horizontal = movement.from.column !== movement.slideTo.column;
404
452
  const start = horizontal ? bounds.left : bounds.top;
405
453
  const travel = (horizontal ? finalBounds.left : finalBounds.top) - start;
@@ -473,24 +521,57 @@ function animateSlide(
473
521
  });
474
522
  }
475
523
 
476
- function animateTransfer(
524
+ function animateTransferPhase(
477
525
  piece: HTMLButtonElement,
478
526
  phase: 'fall' | 'arrive',
479
527
  signal: AbortSignal,
480
528
  ): Promise<boolean> {
481
- if (signal.aborted) return Promise.resolve(false);
482
- if (window.matchMedia('(prefers-reduced-motion: reduce)').matches)
483
- return Promise.resolve(true);
484
529
  const hidden = { opacity: 0, transform: 'scale(0.15)' };
485
530
  const visible = { opacity: 1, transform: 'scale(1)' };
486
- const animation = piece.animate(
531
+ return animatePieceTransition(
532
+ piece,
487
533
  phase === 'fall' ? [visible, hidden] : [hidden, visible],
488
- {
489
- duration: 160,
490
- easing: 'ease-out',
491
- fill: 'forwards',
492
- },
534
+ 160,
535
+ signal,
536
+ phase === 'fall' ? 'ease-out' : 'ease-in',
537
+ );
538
+ }
539
+
540
+ function animateLanding(
541
+ piece: HTMLButtonElement,
542
+ signal: AbortSignal,
543
+ ): Promise<boolean> {
544
+ return animatePieceTransition(
545
+ piece,
546
+ [
547
+ { transform: 'scale(1)', transformOrigin: '50% 90%' },
548
+ {
549
+ transform: 'scale(1.04, 0.94)',
550
+ transformOrigin: '50% 90%',
551
+ offset: 0.35,
552
+ },
553
+ { transform: 'scale(1)', transformOrigin: '50% 90%' },
554
+ ],
555
+ 150,
556
+ signal,
493
557
  );
558
+ }
559
+
560
+ function animatePieceTransition(
561
+ piece: HTMLButtonElement,
562
+ keyframes: Keyframe[],
563
+ duration: number,
564
+ signal: AbortSignal,
565
+ easing: 'ease-out' | 'ease-in' = 'ease-out',
566
+ ): Promise<boolean> {
567
+ if (signal.aborted) return Promise.resolve(false);
568
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches)
569
+ return Promise.resolve(true);
570
+ const animation = piece.animate(keyframes, {
571
+ duration,
572
+ easing,
573
+ fill: 'forwards',
574
+ });
494
575
  const cancel = (): void => animation.cancel();
495
576
  signal.addEventListener('abort', cancel, { once: true });
496
577
  return animation.finished
@@ -523,7 +604,10 @@ function render(
523
604
  '--piece-column',
524
605
  String((position ?? instance.state.position).column + 1),
525
606
  );
526
- if (updateTerrain) renderTerrain(panel, instance.state);
607
+ if (updateTerrain) {
608
+ renderTerrain(panel, instance.state);
609
+ renderExitMarkers(panel, instance.state);
610
+ }
527
611
  const target = panel.querySelector<HTMLElement>('[data-sliding-target]');
528
612
  if (target && updateTerrain)
529
613
  setPuzzleGridCellState(
@@ -532,11 +616,38 @@ function render(
532
616
  );
533
617
  }
534
618
 
619
+ function renderExitMarkers(
620
+ panel: HTMLElement,
621
+ state: SlidingPuzzleState,
622
+ ): void {
623
+ const holes = model(panel).holes;
624
+ const discoveredExits = holes
625
+ .filter((hole) =>
626
+ state.openHoles.some(
627
+ (position) =>
628
+ position.row === hole.position.row &&
629
+ position.column === hole.position.column,
630
+ ),
631
+ )
632
+ .map((hole) => hole.exit);
633
+ for (const { exit } of holes) {
634
+ const discovered = discoveredExits.some(
635
+ (position) =>
636
+ position.row === exit.row && position.column === exit.column,
637
+ );
638
+ cell(panel, exit)?.toggleAttribute(
639
+ 'data-sliding-exit-discovered',
640
+ discovered,
641
+ );
642
+ }
643
+ }
644
+
535
645
  function renderTerrain(panel: HTMLElement, state: SlidingPuzzleState): void {
646
+ const puzzle = model(panel);
536
647
  const labels = JSON.parse(
537
648
  panel.dataset.slidingLabels ?? '{}',
538
649
  ) as SlidingPuzzleLabels;
539
- for (const fragile of model(panel).fragileCells) {
650
+ for (const fragile of puzzle.fragileCells) {
540
651
  const element = cell(panel, fragile);
541
652
  if (!element) continue;
542
653
  const collapsed = state.collapsedCells.some(
@@ -552,7 +663,7 @@ function renderTerrain(panel: HTMLElement, state: SlidingPuzzleState): void {
552
663
  if (!element.hasAttribute('data-sliding-destination'))
553
664
  element.setAttribute('aria-label', element.dataset.cellLabel);
554
665
  }
555
- for (const hole of model(panel).holes) {
666
+ for (const hole of puzzle.holes) {
556
667
  const element = cell(panel, hole.position);
557
668
  if (!element) continue;
558
669
  const open = state.openHoles.some(
@@ -580,19 +691,7 @@ function previews(root: HTMLElement): void {
580
691
  const labels = JSON.parse(
581
692
  panel.dataset.slidingLabels ?? '{}',
582
693
  ) as SlidingPuzzleLabels;
583
- // Before discovery, preview ordinary ice. Do not expose the hidden hole
584
- // through path lengths, destination markers or accessible move labels.
585
- const puzzle = model(panel);
586
- const previewPuzzle = {
587
- ...puzzle,
588
- holes: puzzle.holes.filter((hole) =>
589
- instance.state.openHoles.some(
590
- (position) =>
591
- position.row === hole.position.row &&
592
- position.column === hole.position.column,
593
- ),
594
- ),
595
- };
694
+ const previewPuzzle = { ...model(panel), holes: [] };
596
695
  for (const movement of getSlidingMoves(previewPuzzle, instance.state)) {
597
696
  if (board) renderPathStream(board, { ...movement, to: movement.slideTo });
598
697
  const destination = cell(panel, movement.slideTo);
@@ -11,6 +11,10 @@ export { default as ModalClose } from './Modal/ModalClose.astro';
11
11
  export { default as ModalOpen } from './Modal/ModalOpen.astro';
12
12
  export { default as PuzzleGrid } from './PuzzleGrid/PuzzleGrid.astro';
13
13
  export { default as SlidingPuzzle } from './SlidingPuzzle/SlidingPuzzle.astro';
14
+ export { default as LightMazePuzzle } from './LightMazePuzzle/LightMazePuzzle.astro';
15
+ export * from './LightMazePuzzle/contracts';
16
+ export * from './LightMazePuzzle/simulation';
17
+ export * from './LightMazePuzzle/lightMazePuzzleController';
14
18
  export * from './SlidingPuzzle/contracts';
15
19
  export * from './SlidingPuzzle/simulation';
16
20
  export * from './SlidingPuzzle/slidingPuzzleController';
@@ -65,6 +65,7 @@
65
65
  @import '../patterns/PuzzleGrid/PuzzleGrid.css' layer(patterns);
66
66
  @import '../patterns/PuzzleGrid/movement.css' layer(patterns);
67
67
  @import '../patterns/SlidingPuzzle/SlidingPuzzle.css' layer(patterns);
68
+ @import '../patterns/LightMazePuzzle/LightMazePuzzle.css' layer(patterns);
68
69
  @import '../patterns/CircuitPuzzle/CircuitPuzzle.css' layer(patterns);
69
70
  @import '../patterns/Combobox/Combobox.css' layer(patterns);
70
71
  @import '../patterns/Dropdown/Dropdown.css' layer(patterns);