@hkdigital/lib-core 0.5.45 → 0.5.47
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/dist/state/context/README.md +58 -132
- package/dist/state/context.d.ts +0 -1
- package/dist/state/context.js +0 -1
- package/dist/state/machines/page-machine/PageMachine.svelte.d.ts +165 -35
- package/dist/state/machines/page-machine/PageMachine.svelte.js +336 -38
- package/dist/state/machines/page-machine/README.md +209 -63
- package/dist/ui/components/game-box/GameBox.svelte +1 -0
- package/dist/ui/components/game-box/ScaledContainer.svelte +2 -1
- package/dist/util/sveltekit/navigation.d.ts +7 -0
- package/dist/util/sveltekit/navigation.js +12 -0
- package/dist/util/sveltekit.d.ts +1 -0
- package/dist/util/sveltekit.js +1 -0
- package/dist/util/time/index.js +19 -8
- package/package.json +1 -1
- package/dist/state/context/RouteStateContext.svelte.d.ts +0 -44
- package/dist/state/context/RouteStateContext.svelte.js +0 -119
|
@@ -6,17 +6,13 @@ State machine for managing page view states with URL route mapping.
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
┌─────────────────────────────────────────────────────────┐
|
|
9
|
-
│
|
|
9
|
+
│ PuzzleState (extends PageMachine) │
|
|
10
10
|
│ - Maps states to URL routes │
|
|
11
11
|
│ - Tracks current state and visited states │
|
|
12
|
-
│ -
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
│
|
|
17
|
-
┌────────────────▼────────────────────────────────────────┐
|
|
18
|
-
│ MyFlowState (extends RouteStateContext) │
|
|
19
|
-
│ get pageMachine() { return this.#pageMachine; } │
|
|
12
|
+
│ - Manages start path and navigation │
|
|
13
|
+
│ - Provides computed properties (inIntro, inLevel1, etc.)│
|
|
14
|
+
│ - Contains GameLogic for reactive game state │
|
|
15
|
+
│ - Optional: services, preload, reset, etc. │
|
|
20
16
|
└────────────────┬────────────────────────────────────────┘
|
|
21
17
|
│
|
|
22
18
|
│ Context provided to layout
|
|
@@ -25,11 +21,11 @@ State machine for managing page view states with URL route mapping.
|
|
|
25
21
|
│ +layout.svelte │
|
|
26
22
|
│ IMPORTANT: Must sync URL with state: │
|
|
27
23
|
│ $effect(() => { │
|
|
28
|
-
│
|
|
24
|
+
│ puzzleState.syncFromPath($page.url.pathname); │
|
|
29
25
|
│ }); │
|
|
30
26
|
└────────────────┬────────────────────────────────────────┘
|
|
31
27
|
│
|
|
32
|
-
│ Pages use
|
|
28
|
+
│ Pages use state directly
|
|
33
29
|
│
|
|
34
30
|
┌────────┴─────────┬────────────────┐
|
|
35
31
|
▼ ▼ ▼
|
|
@@ -37,37 +33,71 @@ State machine for managing page view states with URL route mapping.
|
|
|
37
33
|
│ +page │ │ +page │ │ Component│
|
|
38
34
|
│ │ │ │ │ │
|
|
39
35
|
└──────────┘ └──────────┘ └──────────┘
|
|
40
|
-
Access via:
|
|
36
|
+
Access via: puzzleState.current, puzzleState.inIntro, etc.
|
|
41
37
|
```
|
|
42
38
|
|
|
43
39
|
## Main Purposes
|
|
44
40
|
|
|
45
41
|
1. **Track current view/step** - Which page is active
|
|
46
42
|
2. **Map states to URL paths** - Connect state names to routes
|
|
47
|
-
3. **
|
|
48
|
-
4. **
|
|
43
|
+
3. **Manage start path** - Define and navigate to the entry point
|
|
44
|
+
4. **Sync with browser navigation** - Keep state in sync with URL
|
|
45
|
+
5. **Track visited states** - Know which pages user has seen
|
|
49
46
|
|
|
50
47
|
## Basic Usage
|
|
51
48
|
|
|
52
|
-
### 1.
|
|
49
|
+
### 1. Define state constants
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
// puzzle.constants.js
|
|
53
|
+
export const STATE_INTRO = 'intro';
|
|
54
|
+
export const STATE_TUTORIAL = 'tutorial';
|
|
55
|
+
export const STATE_LEVEL1 = 'level1';
|
|
56
|
+
export const STATE_LEVEL2 = 'level2';
|
|
57
|
+
export const STATE_COMPLETE = 'complete';
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Create state class (extends PageMachine)
|
|
53
61
|
|
|
54
62
|
```javascript
|
|
55
|
-
//
|
|
63
|
+
// puzzle.state.svelte.js
|
|
64
|
+
import { defineStateContext } from '@hkdigital/lib-core/state/context.js';
|
|
56
65
|
import PageMachine from '$lib/state/machines/PageMachine.svelte.js';
|
|
66
|
+
import PuzzleGameLogic from './puzzle.game-logic.svelte.js';
|
|
67
|
+
import {
|
|
68
|
+
STATE_INTRO,
|
|
69
|
+
STATE_TUTORIAL,
|
|
70
|
+
STATE_LEVEL1,
|
|
71
|
+
STATE_LEVEL2,
|
|
72
|
+
STATE_COMPLETE
|
|
73
|
+
} from './puzzle.constants.js';
|
|
74
|
+
|
|
75
|
+
// Data keys for persistent data
|
|
76
|
+
const KEY_TUTORIAL_SEEN = 'tutorial-seen';
|
|
77
|
+
const KEY_HIGHEST_LEVEL = 'highest-level';
|
|
78
|
+
const KEY_DIFFICULTY = 'difficulty';
|
|
79
|
+
|
|
80
|
+
export class PuzzleState extends PageMachine {
|
|
81
|
+
#gameLogic;
|
|
57
82
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
constructor() {
|
|
84
|
+
// Call PageMachine constructor with route config
|
|
85
|
+
super({
|
|
86
|
+
startPath: '/puzzle/intro',
|
|
87
|
+
routeMap: {
|
|
88
|
+
[STATE_INTRO]: '/puzzle/intro',
|
|
89
|
+
[STATE_TUTORIAL]: '/puzzle/tutorial',
|
|
90
|
+
[STATE_LEVEL1]: '/puzzle/level1',
|
|
91
|
+
[STATE_LEVEL2]: '/puzzle/level2',
|
|
92
|
+
[STATE_COMPLETE]: '/puzzle/complete'
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.#gameLogic = new PuzzleGameLogic();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get gameLogic() {
|
|
100
|
+
return this.#gameLogic;
|
|
71
101
|
}
|
|
72
102
|
|
|
73
103
|
// Computed properties for convenience
|
|
@@ -75,31 +105,63 @@ export default class MyFlowPageMachine extends PageMachine {
|
|
|
75
105
|
return this.current === STATE_INTRO;
|
|
76
106
|
}
|
|
77
107
|
|
|
78
|
-
get
|
|
79
|
-
return this.current ===
|
|
108
|
+
get inTutorial() {
|
|
109
|
+
return this.current === STATE_TUTORIAL;
|
|
80
110
|
}
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
111
|
|
|
84
|
-
|
|
112
|
+
get inLevel1() {
|
|
113
|
+
return this.current === STATE_LEVEL1;
|
|
114
|
+
}
|
|
85
115
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
import MyFlowPageMachine from './my-flow.machine.svelte.js';
|
|
116
|
+
get inLevel2() {
|
|
117
|
+
return this.current === STATE_LEVEL2;
|
|
118
|
+
}
|
|
90
119
|
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
get isComplete() {
|
|
121
|
+
return this.current === STATE_COMPLETE;
|
|
122
|
+
}
|
|
93
123
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this
|
|
124
|
+
// Persistent settings/progress (use getData/setData)
|
|
125
|
+
get hasSeenTutorial() {
|
|
126
|
+
return this.getData(KEY_TUTORIAL_SEEN) || false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
markTutorialComplete() {
|
|
130
|
+
this.setData(KEY_TUTORIAL_SEEN, true);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get highestLevel() {
|
|
134
|
+
return this.getData(KEY_HIGHEST_LEVEL) || 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
updateHighestLevel(level) {
|
|
138
|
+
const current = this.highestLevel;
|
|
139
|
+
if (level > current) {
|
|
140
|
+
this.setData(KEY_HIGHEST_LEVEL, level);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get difficulty() {
|
|
145
|
+
return this.getData(KEY_DIFFICULTY) || 'normal';
|
|
97
146
|
}
|
|
98
147
|
|
|
99
|
-
|
|
100
|
-
|
|
148
|
+
setDifficulty(level) {
|
|
149
|
+
this.setData(KEY_DIFFICULTY, level);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Optional: Lifecycle methods
|
|
153
|
+
preload(onProgress) {
|
|
154
|
+
return loadPuzzleAssets(onProgress);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
reset() {
|
|
158
|
+
this.#gameLogic = new PuzzleGameLogic();
|
|
101
159
|
}
|
|
102
160
|
}
|
|
161
|
+
|
|
162
|
+
// Export context helpers
|
|
163
|
+
export const [createOrGetPuzzleState, createPuzzleState, getPuzzleState] =
|
|
164
|
+
defineStateContext(PuzzleState);
|
|
103
165
|
```
|
|
104
166
|
|
|
105
167
|
### 3. Sync with route in +layout.svelte component
|
|
@@ -109,43 +171,126 @@ export class MyFlowState extends RouteStateContext {
|
|
|
109
171
|
```svelte
|
|
110
172
|
<script>
|
|
111
173
|
import { page } from '$app/stores';
|
|
112
|
-
import {
|
|
174
|
+
import { createOrGetPuzzleState } from '../puzzle.state.svelte.js';
|
|
175
|
+
|
|
176
|
+
const puzzleState = createOrGetPuzzleState();
|
|
113
177
|
|
|
114
|
-
|
|
115
|
-
|
|
178
|
+
// Sync state with URL changes
|
|
179
|
+
$effect(() => {
|
|
180
|
+
puzzleState.syncFromPath($page.url.pathname);
|
|
181
|
+
});
|
|
116
182
|
|
|
117
|
-
//
|
|
183
|
+
// Optional: Enforce that users must visit intro before playing
|
|
118
184
|
$effect(() => {
|
|
119
|
-
|
|
185
|
+
const currentPath = $page.url.pathname;
|
|
186
|
+
|
|
187
|
+
if (!puzzleState.isStartPath(currentPath) &&
|
|
188
|
+
currentPath.startsWith('/puzzle') &&
|
|
189
|
+
!puzzleState.hasVisited('intro')) {
|
|
190
|
+
puzzleState.redirectToStartPath();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Optional: Preload assets
|
|
195
|
+
puzzleState.preload((progress) => {
|
|
196
|
+
console.log('Loading:', progress);
|
|
120
197
|
});
|
|
121
198
|
</script>
|
|
122
199
|
|
|
123
|
-
{#if
|
|
124
|
-
<IntroView />
|
|
125
|
-
{:else if
|
|
126
|
-
<
|
|
200
|
+
{#if puzzleState.inIntro}
|
|
201
|
+
<IntroView onComplete={() => puzzleState.markTutorialComplete()} />
|
|
202
|
+
{:else if puzzleState.inTutorial}
|
|
203
|
+
<TutorialView />
|
|
204
|
+
{:else if puzzleState.inLevel1}
|
|
205
|
+
<Level1View gameLogic={puzzleState.gameLogic} />
|
|
206
|
+
{:else if puzzleState.inLevel2}
|
|
207
|
+
<Level2View gameLogic={puzzleState.gameLogic} />
|
|
208
|
+
{:else if puzzleState.isComplete}
|
|
209
|
+
<CompleteView
|
|
210
|
+
score={puzzleState.gameLogic.score}
|
|
211
|
+
highestLevel={puzzleState.highestLevel} />
|
|
127
212
|
{/if}
|
|
128
213
|
```
|
|
129
214
|
|
|
130
215
|
## Key Methods
|
|
131
216
|
|
|
217
|
+
All PageMachine methods are directly available on your state class:
|
|
218
|
+
|
|
132
219
|
```javascript
|
|
133
220
|
// Sync with URL path
|
|
134
|
-
|
|
221
|
+
puzzleState.syncFromPath(currentPath)
|
|
135
222
|
|
|
136
223
|
// Get current state
|
|
137
|
-
|
|
224
|
+
puzzleState.current
|
|
225
|
+
|
|
226
|
+
// Start path management
|
|
227
|
+
puzzleState.startPath // Get start path
|
|
228
|
+
puzzleState.startState // Get start state
|
|
229
|
+
puzzleState.isStartPath(path) // Check if path is start path
|
|
230
|
+
puzzleState.isOnStartState // Check if on start state
|
|
231
|
+
puzzleState.redirectToStartPath() // Navigate to start path
|
|
138
232
|
|
|
139
233
|
// Get route for state
|
|
140
|
-
|
|
234
|
+
puzzleState.getPathForState(stateName)
|
|
141
235
|
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
236
|
+
// Persistent data properties
|
|
237
|
+
puzzleState.setData(KEY_NAME, value)
|
|
238
|
+
puzzleState.getData(KEY_NAME)
|
|
145
239
|
|
|
146
240
|
// Visited states tracking
|
|
147
|
-
|
|
148
|
-
|
|
241
|
+
puzzleState.hasVisited(stateName)
|
|
242
|
+
puzzleState.getVisitedStates()
|
|
243
|
+
|
|
244
|
+
// Custom computed properties (from your class)
|
|
245
|
+
puzzleState.inIntro
|
|
246
|
+
puzzleState.inLevel1
|
|
247
|
+
puzzleState.hasSeenTutorial
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Data Storage Guidelines
|
|
251
|
+
|
|
252
|
+
### When to use `getData/setData` (PageMachine)
|
|
253
|
+
|
|
254
|
+
Use PageMachine's data properties for **persistent settings and progress**:
|
|
255
|
+
|
|
256
|
+
- ✅ Tutorial completion flags
|
|
257
|
+
- ✅ User preferences (difficulty, language, sound)
|
|
258
|
+
- ✅ Progress tracking (highest level reached)
|
|
259
|
+
- ✅ Settings that survive page navigation
|
|
260
|
+
- ✅ Data that might be saved to server
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
const KEY_TUTORIAL_SEEN = 'tutorial-seen';
|
|
264
|
+
const KEY_DIFFICULTY = 'difficulty';
|
|
265
|
+
const KEY_HIGHEST_LEVEL = 'highest-level';
|
|
266
|
+
|
|
267
|
+
// Persistent data
|
|
268
|
+
pageMachine.setData(KEY_TUTORIAL_SEEN, true);
|
|
269
|
+
pageMachine.setData(KEY_DIFFICULTY, 'hard');
|
|
270
|
+
pageMachine.setData(KEY_HIGHEST_LEVEL, 5);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### When to use GameLogic with `$state`
|
|
274
|
+
|
|
275
|
+
Use a separate GameLogic class with `$state` fields for **reactive game state**:
|
|
276
|
+
|
|
277
|
+
- ✅ Live scores, lives, health
|
|
278
|
+
- ✅ Current player, selected items
|
|
279
|
+
- ✅ Complex objects (cards, inventory, enemies)
|
|
280
|
+
- ✅ Temporary turn/round state
|
|
281
|
+
- ✅ Reactive UI state that changes frequently
|
|
282
|
+
|
|
283
|
+
```javascript
|
|
284
|
+
// puzzle.game-logic.svelte.js
|
|
285
|
+
export class PuzzleGameLogic {
|
|
286
|
+
#score = $state(0);
|
|
287
|
+
#lives = $state(3);
|
|
288
|
+
#selectedCards = $state([]);
|
|
289
|
+
#currentPuzzle = $state(null);
|
|
290
|
+
|
|
291
|
+
get score() { return this.#score; }
|
|
292
|
+
incrementScore(points) { this.#score += points; }
|
|
293
|
+
}
|
|
149
294
|
```
|
|
150
295
|
|
|
151
296
|
## Important Notes
|
|
@@ -154,4 +299,5 @@ machine.getVisitedStates()
|
|
|
154
299
|
- States map 1:1 with routes
|
|
155
300
|
- Use state constants instead of magic strings
|
|
156
301
|
- Always sync in `$effect` watching `$page.url.pathname`
|
|
157
|
-
-
|
|
302
|
+
- Use constants for data keys (e.g., `KEY_TUTORIAL_SEEN = 'tutorial-seen'`)
|
|
303
|
+
- Separate persistent data (PageMachine) from reactive game state (GameLogic)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import {
|
|
2
|
+
import { enableContainerScaling } from '../../../design/index.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Wrapper component that applies container scaling to its children
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
|
|
61
61
|
{#if snippet && snippetParams}
|
|
62
62
|
<div
|
|
63
|
+
data-component="scaled-container"
|
|
63
64
|
bind:this={container}
|
|
64
65
|
class:hidden
|
|
65
66
|
style:width="{width}px"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { goto } from '$app/navigation';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Navigate to an internal path
|
|
5
|
+
* - Replaces the current url
|
|
6
|
+
*
|
|
7
|
+
* @param {string} path - Pathname to navigate to
|
|
8
|
+
*/
|
|
9
|
+
export function switchToPage( path ) {
|
|
10
|
+
// eslint-disable-next-line svelte/no-navigation-without-resolve
|
|
11
|
+
goto( path, { replaceState: true } );
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./sveltekit/navigation.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sveltekit/navigation.js';
|
package/dist/util/time/index.js
CHANGED
|
@@ -185,12 +185,16 @@ export function getWeekNumber(dateOrTimestamp) {
|
|
|
185
185
|
//
|
|
186
186
|
// Create a copy of this date object
|
|
187
187
|
//
|
|
188
|
-
const target = new Date(
|
|
188
|
+
const target = new Date(Date.UTC(
|
|
189
|
+
date.getUTCFullYear(),
|
|
190
|
+
date.getUTCMonth(),
|
|
191
|
+
date.getUTCDate()
|
|
192
|
+
));
|
|
189
193
|
|
|
190
194
|
//
|
|
191
195
|
// ISO week date weeks start on Monday, so correct the day number
|
|
192
196
|
//
|
|
193
|
-
const dayNumber = (date.
|
|
197
|
+
const dayNumber = (date.getUTCDay() + 6) % 7;
|
|
194
198
|
|
|
195
199
|
//
|
|
196
200
|
// ISO 8601 states that week 1 is the week with the first Thursday
|
|
@@ -198,22 +202,29 @@ export function getWeekNumber(dateOrTimestamp) {
|
|
|
198
202
|
//
|
|
199
203
|
// Set the target date to the Thursday in the target week
|
|
200
204
|
//
|
|
201
|
-
target.
|
|
205
|
+
target.setUTCDate(target.getUTCDate() - dayNumber + 3);
|
|
202
206
|
|
|
203
207
|
//
|
|
204
208
|
// Store the millisecond value of the target date
|
|
205
209
|
//
|
|
206
210
|
const firstThursday = target.valueOf();
|
|
207
211
|
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
212
|
+
//
|
|
213
|
+
// Get the year of the Thursday in the target week
|
|
214
|
+
// (This is important for dates near year boundaries)
|
|
215
|
+
//
|
|
216
|
+
const yearOfThursday = target.getUTCFullYear();
|
|
217
|
+
|
|
218
|
+
// Set the target to the first Thursday of that year
|
|
219
|
+
// First, set the target to January 1st of that year
|
|
220
|
+
target.setUTCFullYear(yearOfThursday);
|
|
221
|
+
target.setUTCMonth(0, 1);
|
|
211
222
|
|
|
212
223
|
//
|
|
213
224
|
// Not a Thursday? Correct the date to the next Thursday
|
|
214
225
|
//
|
|
215
|
-
if (target.
|
|
216
|
-
target.
|
|
226
|
+
if (target.getUTCDay() !== 4) {
|
|
227
|
+
target.setUTCMonth(0, 1 + ((4 - target.getUTCDay() + 7) % 7));
|
|
217
228
|
}
|
|
218
229
|
|
|
219
230
|
//
|
package/package.json
CHANGED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Base class for route state containers
|
|
3
|
-
*
|
|
4
|
-
* Main purposes:
|
|
5
|
-
* - Container for route-level concerns (PageMachine, services, engines)
|
|
6
|
-
* - Apply enforceStartPath to control navigation flow
|
|
7
|
-
* - Provide validateAndRedirect for route protection
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```javascript
|
|
11
|
-
* export class PuzzleState extends RouteStateContext {
|
|
12
|
-
* constructor() {
|
|
13
|
-
* super({
|
|
14
|
-
* startPath: '/puzzle',
|
|
15
|
-
* enforceStartPath: true
|
|
16
|
-
* });
|
|
17
|
-
* }
|
|
18
|
-
*
|
|
19
|
-
* preload(onProgress) {
|
|
20
|
-
* return loadAudioAndVideoScenes(onProgress);
|
|
21
|
-
* }
|
|
22
|
-
* }
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
export default class RouteStateContext {
|
|
26
|
-
/**
|
|
27
|
-
* @param {Object} options - Configuration options
|
|
28
|
-
* @param {string} options.startPath - Start path for this route (required)
|
|
29
|
-
* @param {boolean} [options.enforceStartPath=false] - If true, redirect to start path before allowing subroutes
|
|
30
|
-
*/
|
|
31
|
-
constructor({ startPath, enforceStartPath }: {
|
|
32
|
-
startPath: string;
|
|
33
|
-
enforceStartPath?: boolean | undefined;
|
|
34
|
-
});
|
|
35
|
-
/**
|
|
36
|
-
* Validate current path and redirect if needed
|
|
37
|
-
* Call this in a $effect in the layout
|
|
38
|
-
*
|
|
39
|
-
* @param {string} currentPath - Current URL pathname
|
|
40
|
-
* @param {string} [redirectUrl] - Optional redirect URL (defaults to startPath)
|
|
41
|
-
*/
|
|
42
|
-
validateAndRedirect(currentPath: string, redirectUrl?: string): void;
|
|
43
|
-
#private;
|
|
44
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { goto } from '$app/navigation';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Base class for route state containers
|
|
5
|
-
*
|
|
6
|
-
* Main purposes:
|
|
7
|
-
* - Container for route-level concerns (PageMachine, services, engines)
|
|
8
|
-
* - Apply enforceStartPath to control navigation flow
|
|
9
|
-
* - Provide validateAndRedirect for route protection
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```javascript
|
|
13
|
-
* export class PuzzleState extends RouteStateContext {
|
|
14
|
-
* constructor() {
|
|
15
|
-
* super({
|
|
16
|
-
* startPath: '/puzzle',
|
|
17
|
-
* enforceStartPath: true
|
|
18
|
-
* });
|
|
19
|
-
* }
|
|
20
|
-
*
|
|
21
|
-
* preload(onProgress) {
|
|
22
|
-
* return loadAudioAndVideoScenes(onProgress);
|
|
23
|
-
* }
|
|
24
|
-
* }
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export default class RouteStateContext {
|
|
28
|
-
/**
|
|
29
|
-
* Start path for this route
|
|
30
|
-
* @type {string}
|
|
31
|
-
*/
|
|
32
|
-
#startPath;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Whether to enforce that users visit start path before subroutes
|
|
36
|
-
* @type {boolean}
|
|
37
|
-
*/
|
|
38
|
-
#enforceStartPath = false;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Track which paths have been visited during this session
|
|
42
|
-
* Used for enforceStartPath validation
|
|
43
|
-
* @type {Set<string>}
|
|
44
|
-
*/
|
|
45
|
-
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
46
|
-
#visitedPaths = new Set();
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @param {Object} options - Configuration options
|
|
50
|
-
* @param {string} options.startPath - Start path for this route (required)
|
|
51
|
-
* @param {boolean} [options.enforceStartPath=false] - If true, redirect to start path before allowing subroutes
|
|
52
|
-
*/
|
|
53
|
-
constructor({ startPath, enforceStartPath = false }) {
|
|
54
|
-
if (!startPath) {
|
|
55
|
-
throw new Error(
|
|
56
|
-
'RouteStateContext requires startPath parameter'
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
this.#startPath = startPath;
|
|
61
|
-
this.#enforceStartPath = enforceStartPath;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Determine if current path needs redirection
|
|
66
|
-
* Private method - enforces sequential access to subroutes
|
|
67
|
-
*
|
|
68
|
-
* @param {string} currentPath - Current URL pathname
|
|
69
|
-
*
|
|
70
|
-
* @returns {string|null}
|
|
71
|
-
* Path to redirect to, or null if no redirect needed
|
|
72
|
-
*/
|
|
73
|
-
#determineRedirect(currentPath) {
|
|
74
|
-
// No enforcement configured
|
|
75
|
-
if (!this.#enforceStartPath) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Currently on the start path - mark as visited and allow
|
|
80
|
-
if (currentPath === this.#startPath) {
|
|
81
|
-
this.#visitedPaths.add(currentPath);
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// On a subroute - check if start path was visited
|
|
86
|
-
if (currentPath.startsWith(this.#startPath + '/')) {
|
|
87
|
-
// Allow if user has visited the start path
|
|
88
|
-
if (this.#visitedPaths.has(this.#startPath)) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
// Redirect to start path if not visited yet
|
|
92
|
-
return this.#startPath;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Path is valid (not a subroute)
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Validate current path and redirect if needed
|
|
101
|
-
* Call this in a $effect in the layout
|
|
102
|
-
*
|
|
103
|
-
* @param {string} currentPath - Current URL pathname
|
|
104
|
-
* @param {string} [redirectUrl] - Optional redirect URL (defaults to startPath)
|
|
105
|
-
*/
|
|
106
|
-
validateAndRedirect(currentPath, redirectUrl = null) {
|
|
107
|
-
const redirectPath = this.#determineRedirect(currentPath);
|
|
108
|
-
|
|
109
|
-
if (redirectPath && redirectPath !== currentPath) {
|
|
110
|
-
const targetPath = redirectUrl || redirectPath;
|
|
111
|
-
|
|
112
|
-
console.debug(
|
|
113
|
-
`[${this.constructor.name}] Redirecting: ${currentPath} → ${targetPath}`
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
goto(targetPath, { replaceState: true });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|