@hkdigital/lib-core 0.5.46 → 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 +64 -9
- package/dist/state/machines/page-machine/PageMachine.svelte.js +109 -11
- 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 +1 -0
- 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/package.json +1 -1
- package/dist/state/context/RouteStateContext.svelte.d.ts +0 -44
- package/dist/state/context/RouteStateContext.svelte.js +0 -119
|
@@ -1,68 +1,72 @@
|
|
|
1
|
-
#
|
|
1
|
+
# State Context Utilities
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Helper functions for managing Svelte context in route-level state containers.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## defineStateContext
|
|
6
6
|
|
|
7
|
+
Creates context helper functions for a state container class.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
import { defineStateContext } from '@hkdigital/lib-core/state/context.js';
|
|
11
|
+
|
|
12
|
+
class PuzzleState {
|
|
13
|
+
#pageMachine;
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
this.#pageMachine = new PuzzlePageMachine();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get pageMachine() {
|
|
20
|
+
return this.#pageMachine;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Export helper functions
|
|
25
|
+
export const [createOrGetPuzzleState, createPuzzleState, getPuzzleState] =
|
|
26
|
+
defineStateContext(PuzzleState);
|
|
7
27
|
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
│ - IMPORTANT: Syncs URL with pageMachine.syncFromPath() │
|
|
21
|
-
│ - Optional: Calls validateAndRedirect() for protection │
|
|
22
|
-
└────────────────┬────────────────────────────────────────┘
|
|
23
|
-
│
|
|
24
|
-
│ Context available to children
|
|
25
|
-
│
|
|
26
|
-
┌────────┴─────────┬────────────────┐
|
|
27
|
-
▼ ▼ ▼
|
|
28
|
-
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
29
|
-
│ +page │ │ +page │ │ Component│
|
|
30
|
-
│ .svelte │ │ .svelte │ │ │
|
|
31
|
-
└──────────┘ └──────────┘ └──────────┘
|
|
32
|
-
Gets state via getPuzzleState()
|
|
28
|
+
|
|
29
|
+
## Helper Functions
|
|
30
|
+
|
|
31
|
+
The `defineStateContext` helper creates three functions:
|
|
32
|
+
|
|
33
|
+
### createOrGetPuzzleState()
|
|
34
|
+
|
|
35
|
+
Get existing instance or create new one. Use in `+layout.svelte`.
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
// routes/puzzle/+layout.svelte
|
|
39
|
+
const state = createOrGetPuzzleState();
|
|
33
40
|
```
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
### createPuzzleState()
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
2. **Apply enforceStartPath** - Control navigation flow (users must visit
|
|
39
|
-
start path before accessing subroutes)
|
|
40
|
-
3. **Provide validateAndRedirect** - Route protection via layout
|
|
44
|
+
Force create new instance (discards existing).
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
```javascript
|
|
47
|
+
const state = createPuzzleState();
|
|
48
|
+
```
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
- Persist state across navigation within the same route group
|
|
46
|
-
- Lifecycle methods for setup/teardown (preload, reset)
|
|
50
|
+
### getPuzzleState()
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
Get existing instance. Throws error if not found. Use in pages/components.
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
```javascript
|
|
55
|
+
// routes/puzzle/level1/+page.svelte
|
|
56
|
+
const state = getPuzzleState();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Complete Example
|
|
51
60
|
|
|
52
61
|
```javascript
|
|
53
62
|
// routes/puzzle/puzzle.state.svelte.js
|
|
54
63
|
import { defineStateContext } from '@hkdigital/lib-core/state/context.js';
|
|
55
|
-
import { RouteStateContext } from '$lib/state/context.js';
|
|
56
64
|
import PuzzlePageMachine from './puzzle.machine.svelte.js';
|
|
57
65
|
|
|
58
|
-
export class PuzzleState
|
|
66
|
+
export class PuzzleState {
|
|
59
67
|
#pageMachine;
|
|
60
68
|
|
|
61
69
|
constructor() {
|
|
62
|
-
super({
|
|
63
|
-
startPath: '/puzzle',
|
|
64
|
-
enforceStartPath: true // Optional: enforce route protection
|
|
65
|
-
});
|
|
66
70
|
this.#pageMachine = new PuzzlePageMachine();
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -81,34 +85,26 @@ export class PuzzleState extends RouteStateContext {
|
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
reset() {
|
|
84
|
-
|
|
88
|
+
this.#pageMachine = new PuzzlePageMachine();
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
// Export helper functions
|
|
89
92
|
export const [createOrGetPuzzleState, createPuzzleState, getPuzzleState] =
|
|
90
93
|
defineStateContext(PuzzleState);
|
|
91
94
|
```
|
|
92
95
|
|
|
93
|
-
### 2. Provide context in layout
|
|
94
|
-
|
|
95
96
|
```svelte
|
|
96
97
|
<!-- routes/puzzle/+layout.svelte -->
|
|
97
98
|
<script>
|
|
98
99
|
import { page } from '$app/stores';
|
|
99
100
|
import { createOrGetPuzzleState } from './puzzle.state.svelte.js';
|
|
100
101
|
|
|
101
|
-
// Create or get existing state container
|
|
102
102
|
const puzzleState = createOrGetPuzzleState();
|
|
103
|
+
const pageMachine = puzzleState.pageMachine;
|
|
103
104
|
|
|
104
105
|
// IMPORTANT: Sync URL with PageMachine state
|
|
105
106
|
$effect(() => {
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// Optional: Enforce start path (redirect if user skips intro)
|
|
110
|
-
$effect(() => {
|
|
111
|
-
puzzleState.validateAndRedirect($page.url.pathname);
|
|
107
|
+
pageMachine.syncFromPath($page.url.pathname);
|
|
112
108
|
});
|
|
113
109
|
|
|
114
110
|
// Optional: Preload assets
|
|
@@ -120,8 +116,6 @@ export const [createOrGetPuzzleState, createPuzzleState, getPuzzleState] =
|
|
|
120
116
|
<slot />
|
|
121
117
|
```
|
|
122
118
|
|
|
123
|
-
### 3. Consume context in pages
|
|
124
|
-
|
|
125
119
|
```svelte
|
|
126
120
|
<!-- routes/puzzle/level1/+page.svelte -->
|
|
127
121
|
<script>
|
|
@@ -134,81 +128,12 @@ export const [createOrGetPuzzleState, createPuzzleState, getPuzzleState] =
|
|
|
134
128
|
<div>Current state: {pageMachine.current}</div>
|
|
135
129
|
```
|
|
136
130
|
|
|
137
|
-
##
|
|
138
|
-
|
|
139
|
-
The `defineStateContext` helper creates three functions:
|
|
140
|
-
|
|
141
|
-
```javascript
|
|
142
|
-
// Get existing or create new (use in layout)
|
|
143
|
-
const state = createOrGetPuzzleState();
|
|
144
|
-
|
|
145
|
-
// Force create new instance
|
|
146
|
-
const state = createPuzzleState();
|
|
147
|
-
|
|
148
|
-
// Get existing (throws if not found, use in pages/components)
|
|
149
|
-
const state = getPuzzleState();
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## Constructor Options
|
|
153
|
-
|
|
154
|
-
```javascript
|
|
155
|
-
constructor({ startPath, enforceStartPath })
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
- `startPath` **(required)** - The start path for this route
|
|
159
|
-
(e.g., `/puzzle`)
|
|
160
|
-
- `enforceStartPath` **(optional, default: false)** - If true, users must
|
|
161
|
-
visit the start path before accessing subroutes
|
|
162
|
-
|
|
163
|
-
## validateAndRedirect Method
|
|
164
|
-
|
|
165
|
-
Automatically redirects users if they try to access subroutes before visiting
|
|
166
|
-
the start path.
|
|
167
|
-
|
|
168
|
-
**How it works:**
|
|
169
|
-
- If `enforceStartPath: true` is set in constructor
|
|
170
|
-
- User tries to access a subroute (e.g., `/puzzle/level2`)
|
|
171
|
-
- But hasn't visited the start path yet (`/puzzle`)
|
|
172
|
-
- → Automatically redirects to start path
|
|
173
|
-
|
|
174
|
-
**Example use case:** Puzzle game where users must see the intro before
|
|
175
|
-
accessing puzzle levels.
|
|
176
|
-
|
|
177
|
-
```javascript
|
|
178
|
-
// In state constructor
|
|
179
|
-
export class PuzzleState extends RouteStateContext {
|
|
180
|
-
constructor() {
|
|
181
|
-
super({
|
|
182
|
-
startPath: '/puzzle',
|
|
183
|
-
enforceStartPath: true
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
```svelte
|
|
190
|
-
<!-- In +layout.svelte -->
|
|
191
|
-
<script>
|
|
192
|
-
import { page } from '$app/stores';
|
|
193
|
-
|
|
194
|
-
const puzzleState = createOrGetPuzzleState();
|
|
195
|
-
|
|
196
|
-
// Enforce route protection
|
|
197
|
-
$effect(() => {
|
|
198
|
-
puzzleState.validateAndRedirect($page.url.pathname);
|
|
199
|
-
});
|
|
200
|
-
</script>
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
**Result:** If user navigates directly to `/puzzle/level2`, they'll be
|
|
204
|
-
redirected to `/puzzle` first. After visiting `/puzzle`, they can freely
|
|
205
|
-
navigate to any subroute.
|
|
131
|
+
## Key Features
|
|
206
132
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
```
|
|
133
|
+
- Share state between layout and pages without prop drilling
|
|
134
|
+
- Persist state across navigation within the same route group
|
|
135
|
+
- Lifecycle methods for setup/teardown (preload, reset)
|
|
136
|
+
- PageMachine integration for route/state management
|
|
212
137
|
|
|
213
138
|
## Separation of Concerns
|
|
214
139
|
|
|
@@ -222,5 +147,6 @@ puzzleState.validateAndRedirect($page.url.pathname, '/puzzle/welcome');
|
|
|
222
147
|
**PageMachine** = Page/view state ONLY (SINGLE responsibility)
|
|
223
148
|
- Current page state
|
|
224
149
|
- Route mapping
|
|
150
|
+
- Start path management
|
|
225
151
|
- Visited states
|
|
226
152
|
- Computed properties for state checks
|
package/dist/state/context.d.ts
CHANGED
package/dist/state/context.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Features:
|
|
9
9
|
* - State-to-route mapping and sync
|
|
10
|
+
* - Start path management
|
|
10
11
|
* - Data properties for business/domain state
|
|
11
12
|
* - Visited states tracking
|
|
12
13
|
* - onEnter hooks with abort/complete handlers for animations
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
* Basic usage:
|
|
15
16
|
* ```javascript
|
|
16
17
|
* const machine = new PageMachine({
|
|
17
|
-
*
|
|
18
|
+
* startPath: '/intro/start',
|
|
18
19
|
* routeMap: {
|
|
19
20
|
* [STATE_START]: '/intro/start',
|
|
20
21
|
* [STATE_PROFILE]: '/intro/profile'
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
* With onEnter hooks (for animations):
|
|
31
32
|
* ```javascript
|
|
32
33
|
* const machine = new PageMachine({
|
|
33
|
-
*
|
|
34
|
+
* startPath: '/game/animate',
|
|
34
35
|
* routeMap: {
|
|
35
36
|
* [STATE_ANIMATE]: '/game/animate',
|
|
36
37
|
* [STATE_PLAY]: '/game/play'
|
|
@@ -60,15 +61,19 @@ export default class PageMachine {
|
|
|
60
61
|
* Constructor
|
|
61
62
|
*
|
|
62
63
|
* @param {Object} config - Configuration object
|
|
63
|
-
* @param {string} config.
|
|
64
|
-
*
|
|
65
|
-
* @param {Record<string,
|
|
66
|
-
*
|
|
64
|
+
* @param {string} config.startPath
|
|
65
|
+
* Start path for this route group (e.g., '/game/play')
|
|
66
|
+
* @param {Record<string, string>} [config.routeMap={}]
|
|
67
|
+
* Map of states to route paths
|
|
68
|
+
* @param {Record<string, any>} [config.initialData={}]
|
|
69
|
+
* Initial data properties (from server)
|
|
70
|
+
* @param {Record<string, Function>} [config.onEnterHooks={}]
|
|
71
|
+
* Map of states to onEnter hook functions
|
|
67
72
|
*
|
|
68
73
|
* @example
|
|
69
74
|
* ```javascript
|
|
70
75
|
* const machine = new PageMachine({
|
|
71
|
-
*
|
|
76
|
+
* startPath: '/intro/start',
|
|
72
77
|
* routeMap: {
|
|
73
78
|
* [STATE_START]: '/intro/start',
|
|
74
79
|
* [STATE_ANIMATE]: '/intro/animate'
|
|
@@ -88,8 +93,8 @@ export default class PageMachine {
|
|
|
88
93
|
* });
|
|
89
94
|
* ```
|
|
90
95
|
*/
|
|
91
|
-
constructor({
|
|
92
|
-
|
|
96
|
+
constructor({ startPath, routeMap, initialData, onEnterHooks }: {
|
|
97
|
+
startPath: string;
|
|
93
98
|
routeMap?: Record<string, string> | undefined;
|
|
94
99
|
initialData?: Record<string, any> | undefined;
|
|
95
100
|
onEnterHooks?: Record<string, Function> | undefined;
|
|
@@ -218,6 +223,56 @@ export default class PageMachine {
|
|
|
218
223
|
* Useful for testing or resetting experience
|
|
219
224
|
*/
|
|
220
225
|
resetVisitedStates(): void;
|
|
226
|
+
/**
|
|
227
|
+
* Get the start path
|
|
228
|
+
*
|
|
229
|
+
* @returns {string} Start path
|
|
230
|
+
*/
|
|
231
|
+
get startPath(): string;
|
|
232
|
+
/**
|
|
233
|
+
* Get the start state
|
|
234
|
+
*
|
|
235
|
+
* @returns {string} Start state name
|
|
236
|
+
*/
|
|
237
|
+
get startState(): string;
|
|
238
|
+
/**
|
|
239
|
+
* Check if the supplied path matches the start path
|
|
240
|
+
*
|
|
241
|
+
* @param {string} path - Path to check
|
|
242
|
+
*
|
|
243
|
+
* @returns {boolean} True if path matches start path
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```javascript
|
|
247
|
+
* if (machine.isStartPath('/game/play')) {
|
|
248
|
+
* // User is on the start page
|
|
249
|
+
* }
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
isStartPath(path: string): boolean;
|
|
253
|
+
/**
|
|
254
|
+
* Check if currently on the start state
|
|
255
|
+
*
|
|
256
|
+
* @returns {boolean} True if current state is the start state
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```javascript
|
|
260
|
+
* if (machine.isOnStartState) {
|
|
261
|
+
* // Show onboarding
|
|
262
|
+
* }
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
get isOnStartState(): boolean;
|
|
266
|
+
/**
|
|
267
|
+
* Navigate to the start path
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```javascript
|
|
271
|
+
* // Redirect user to start
|
|
272
|
+
* machine.redirectToStartPath();
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
redirectToStartPath(): void;
|
|
221
276
|
/**
|
|
222
277
|
* Abort current state's transitions
|
|
223
278
|
* Cancels animations/operations immediately (incomplete state)
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Features:
|
|
9
9
|
* - State-to-route mapping and sync
|
|
10
|
+
* - Start path management
|
|
10
11
|
* - Data properties for business/domain state
|
|
11
12
|
* - Visited states tracking
|
|
12
13
|
* - onEnter hooks with abort/complete handlers for animations
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
* Basic usage:
|
|
15
16
|
* ```javascript
|
|
16
17
|
* const machine = new PageMachine({
|
|
17
|
-
*
|
|
18
|
+
* startPath: '/intro/start',
|
|
18
19
|
* routeMap: {
|
|
19
20
|
* [STATE_START]: '/intro/start',
|
|
20
21
|
* [STATE_PROFILE]: '/intro/profile'
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
* With onEnter hooks (for animations):
|
|
31
32
|
* ```javascript
|
|
32
33
|
* const machine = new PageMachine({
|
|
33
|
-
*
|
|
34
|
+
* startPath: '/game/animate',
|
|
34
35
|
* routeMap: {
|
|
35
36
|
* [STATE_ANIMATE]: '/game/animate',
|
|
36
37
|
* [STATE_PLAY]: '/game/play'
|
|
@@ -63,6 +64,18 @@ export default class PageMachine {
|
|
|
63
64
|
// @ts-ignore
|
|
64
65
|
#current = $state();
|
|
65
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Start path for this page machine
|
|
69
|
+
* @type {string}
|
|
70
|
+
*/
|
|
71
|
+
#startPath = '';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Initial/start state (derived from startPath)
|
|
75
|
+
* @type {string}
|
|
76
|
+
*/
|
|
77
|
+
#startState = '';
|
|
78
|
+
|
|
66
79
|
/**
|
|
67
80
|
* Map of states to route paths
|
|
68
81
|
* @type {Record<string, string>}
|
|
@@ -124,15 +137,19 @@ export default class PageMachine {
|
|
|
124
137
|
* Constructor
|
|
125
138
|
*
|
|
126
139
|
* @param {Object} config - Configuration object
|
|
127
|
-
* @param {string} config.
|
|
128
|
-
*
|
|
129
|
-
* @param {Record<string,
|
|
130
|
-
*
|
|
140
|
+
* @param {string} config.startPath
|
|
141
|
+
* Start path for this route group (e.g., '/game/play')
|
|
142
|
+
* @param {Record<string, string>} [config.routeMap={}]
|
|
143
|
+
* Map of states to route paths
|
|
144
|
+
* @param {Record<string, any>} [config.initialData={}]
|
|
145
|
+
* Initial data properties (from server)
|
|
146
|
+
* @param {Record<string, Function>} [config.onEnterHooks={}]
|
|
147
|
+
* Map of states to onEnter hook functions
|
|
131
148
|
*
|
|
132
149
|
* @example
|
|
133
150
|
* ```javascript
|
|
134
151
|
* const machine = new PageMachine({
|
|
135
|
-
*
|
|
152
|
+
* startPath: '/intro/start',
|
|
136
153
|
* routeMap: {
|
|
137
154
|
* [STATE_START]: '/intro/start',
|
|
138
155
|
* [STATE_ANIMATE]: '/intro/animate'
|
|
@@ -152,12 +169,12 @@ export default class PageMachine {
|
|
|
152
169
|
* });
|
|
153
170
|
* ```
|
|
154
171
|
*/
|
|
155
|
-
constructor({
|
|
156
|
-
if (!
|
|
157
|
-
throw new Error('PageMachine requires
|
|
172
|
+
constructor({ startPath, routeMap = {}, initialData = {}, onEnterHooks = {} }) {
|
|
173
|
+
if (!startPath) {
|
|
174
|
+
throw new Error('PageMachine requires startPath parameter');
|
|
158
175
|
}
|
|
159
176
|
|
|
160
|
-
this.#
|
|
177
|
+
this.#startPath = startPath;
|
|
161
178
|
this.#routeMap = routeMap;
|
|
162
179
|
this.#data = initialData;
|
|
163
180
|
this.#onEnterHooks = this.#normalizeOnEnterHooks(onEnterHooks);
|
|
@@ -167,6 +184,17 @@ export default class PageMachine {
|
|
|
167
184
|
this.#pathToStateMap[path] = state;
|
|
168
185
|
}
|
|
169
186
|
|
|
187
|
+
// Derive initial state from startPath
|
|
188
|
+
const initialState = this.#pathToStateMap[startPath];
|
|
189
|
+
if (!initialState) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`PageMachine: startPath "${startPath}" not found in routeMap`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.#startState = initialState;
|
|
196
|
+
this.#current = initialState;
|
|
197
|
+
|
|
170
198
|
// Mark initial state as visited
|
|
171
199
|
this.#visitedStates.add(initialState);
|
|
172
200
|
}
|
|
@@ -461,6 +489,76 @@ export default class PageMachine {
|
|
|
461
489
|
this.#revision++;
|
|
462
490
|
}
|
|
463
491
|
|
|
492
|
+
/* ===== Start Path Methods ===== */
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Get the start path
|
|
496
|
+
*
|
|
497
|
+
* @returns {string} Start path
|
|
498
|
+
*/
|
|
499
|
+
get startPath() {
|
|
500
|
+
return this.#startPath;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Get the start state
|
|
505
|
+
*
|
|
506
|
+
* @returns {string} Start state name
|
|
507
|
+
*/
|
|
508
|
+
get startState() {
|
|
509
|
+
return this.#startState;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Check if the supplied path matches the start path
|
|
514
|
+
*
|
|
515
|
+
* @param {string} path - Path to check
|
|
516
|
+
*
|
|
517
|
+
* @returns {boolean} True if path matches start path
|
|
518
|
+
*
|
|
519
|
+
* @example
|
|
520
|
+
* ```javascript
|
|
521
|
+
* if (machine.isStartPath('/game/play')) {
|
|
522
|
+
* // User is on the start page
|
|
523
|
+
* }
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
isStartPath(path) {
|
|
527
|
+
return path === this.#startPath;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Check if currently on the start state
|
|
532
|
+
*
|
|
533
|
+
* @returns {boolean} True if current state is the start state
|
|
534
|
+
*
|
|
535
|
+
* @example
|
|
536
|
+
* ```javascript
|
|
537
|
+
* if (machine.isOnStartState) {
|
|
538
|
+
* // Show onboarding
|
|
539
|
+
* }
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
542
|
+
get isOnStartState() {
|
|
543
|
+
return this.#current === this.#startState;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Navigate to the start path
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* ```javascript
|
|
551
|
+
* // Redirect user to start
|
|
552
|
+
* machine.redirectToStartPath();
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
redirectToStartPath() {
|
|
556
|
+
// Import dynamically to avoid circular dependencies
|
|
557
|
+
import('../../../util/sveltekit.js').then(({ switchToPage }) => {
|
|
558
|
+
switchToPage(this.#startPath);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
464
562
|
/* ===== Transition Control Methods ===== */
|
|
465
563
|
|
|
466
564
|
/**
|
|
@@ -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)
|
|
@@ -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/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
|
-
}
|