@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.
@@ -1,68 +1,72 @@
1
- # RouteStateContext
1
+ # State Context Utilities
2
2
 
3
- Base class for route-level state containers.
3
+ Helper functions for managing Svelte context in route-level state containers.
4
4
 
5
- ## How it connects
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
- PuzzleState (extends RouteStateContext) │
10
- │ - Container for route-level concerns │
11
- - Contains PageMachine instance │
12
- │ - Optional: services, preload, reset, etc. │
13
- └────────────────┬────────────────────────────────────────┘
14
-
15
- Provided via Svelte context
16
-
17
- ┌────────────────▼────────────────────────────────────────┐
18
- +layout.svelte
19
- - Creates state with createOrGetPuzzleState()
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
- ## Main Purposes
42
+ ### createPuzzleState()
36
43
 
37
- 1. **State container** - Hold route-level concerns in one place
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
- ## Key Features
46
+ ```javascript
47
+ const state = createPuzzleState();
48
+ ```
43
49
 
44
- - Share state between layout and pages without prop drilling
45
- - Persist state across navigation within the same route group
46
- - Lifecycle methods for setup/teardown (preload, reset)
50
+ ### getPuzzleState()
47
51
 
48
- ## Basic Usage
52
+ Get existing instance. Throws error if not found. Use in pages/components.
49
53
 
50
- ### 1. Create state container class
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 extends RouteStateContext {
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
- // Reset state when needed
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
- puzzleState.pageMachine.syncFromPath($page.url.pathname);
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
- ## Context Helpers
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
- **Custom redirect URL:**
208
- ```javascript
209
- // Redirect to a different URL instead of startPath
210
- puzzleState.validateAndRedirect($page.url.pathname, '/puzzle/welcome');
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
@@ -1,2 +1 @@
1
- export { default as RouteStateContext } from "./context/RouteStateContext.svelte.js";
2
1
  export { defineStateContext, DEFAULT_CONTEXT_KEY } from "./context/util.js";
@@ -1,2 +1 @@
1
- export { default as RouteStateContext } from './context/RouteStateContext.svelte.js';
2
1
  export { defineStateContext, DEFAULT_CONTEXT_KEY } from './context/util.js';
@@ -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
- * initialState: STATE_START,
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
- * initialState: STATE_ANIMATE,
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.initialState - Initial state name
64
- * @param {Record<string, string>} [config.routeMap={}] - Map of states to route paths
65
- * @param {Record<string, any>} [config.initialData={}] - Initial data properties (from server)
66
- * @param {Record<string, Function>} [config.onEnterHooks={}] - Map of states to onEnter hook functions
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
- * initialState: STATE_START,
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({ initialState, routeMap, initialData, onEnterHooks }: {
92
- initialState: string;
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
- * initialState: STATE_START,
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
- * initialState: STATE_ANIMATE,
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.initialState - Initial state name
128
- * @param {Record<string, string>} [config.routeMap={}] - Map of states to route paths
129
- * @param {Record<string, any>} [config.initialData={}] - Initial data properties (from server)
130
- * @param {Record<string, Function>} [config.onEnterHooks={}] - Map of states to onEnter hook functions
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
- * initialState: STATE_START,
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({ initialState, routeMap = {}, initialData = {}, onEnterHooks = {} }) {
156
- if (!initialState) {
157
- throw new Error('PageMachine requires initialState parameter');
172
+ constructor({ startPath, routeMap = {}, initialData = {}, onEnterHooks = {} }) {
173
+ if (!startPath) {
174
+ throw new Error('PageMachine requires startPath parameter');
158
175
  }
159
176
 
160
- this.#current = initialState;
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
- MyFlowPageMachine (extends PageMachine)
9
+ PuzzleState (extends PageMachine)
10
10
  │ - Maps states to URL routes │
11
11
  │ - Tracks current state and visited states │
12
- │ - Provides computed properties (inIntro, inStep1, etc.)
13
- └────────────────┬────────────────────────────────────────┘
14
-
15
- Contained in state
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
- pageMachine.syncFromPath($page.url.pathname); │
24
+ puzzleState.syncFromPath($page.url.pathname); │
29
25
  │ }); │
30
26
  └────────────────┬────────────────────────────────────────┘
31
27
 
32
- │ Pages use machine state
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: pageMachine.current, pageMachine.inIntro, etc.
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. **Sync with browser navigation** - Keep state in sync with URL
48
- 4. **Track visited states** - Know which pages user has seen
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. Create a page machine class
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
- // my-flow.machine.svelte.js
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
- export const STATE_INTRO = 'intro';
59
- export const STATE_STEP1 = 'step1';
60
- export const STATE_STEP2 = 'step2';
61
-
62
- export default class MyFlowPageMachine extends PageMachine {
63
- constructor(initialData = {}) {
64
- const routeMap = {
65
- [STATE_INTRO]: '/my-flow/intro',
66
- [STATE_STEP1]: '/my-flow/step1',
67
- [STATE_STEP2]: '/my-flow/step2'
68
- };
69
-
70
- super(STATE_INTRO, routeMap, initialData);
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 inStep1() {
79
- return this.current === STATE_STEP1;
108
+ get inTutorial() {
109
+ return this.current === STATE_TUTORIAL;
80
110
  }
81
- }
82
- ```
83
111
 
84
- ### 2. Use in state container
112
+ get inLevel1() {
113
+ return this.current === STATE_LEVEL1;
114
+ }
85
115
 
86
- ```javascript
87
- // my-flow.state.svelte.js
88
- import { RouteStateContext } from '$lib/state/context.js';
89
- import MyFlowPageMachine from './my-flow.machine.svelte.js';
116
+ get inLevel2() {
117
+ return this.current === STATE_LEVEL2;
118
+ }
90
119
 
91
- export class MyFlowState extends RouteStateContext {
92
- #pageMachine;
120
+ get isComplete() {
121
+ return this.current === STATE_COMPLETE;
122
+ }
93
123
 
94
- constructor() {
95
- super();
96
- this.#pageMachine = new MyFlowPageMachine();
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
- get pageMachine() {
100
- return this.#pageMachine;
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 { getMyFlowState } from '../my-flow.state.svelte.js';
174
+ import { createOrGetPuzzleState } from '../puzzle.state.svelte.js';
175
+
176
+ const puzzleState = createOrGetPuzzleState();
113
177
 
114
- const flowState = getMyFlowState();
115
- const pageMachine = flowState.pageMachine;
178
+ // Sync state with URL changes
179
+ $effect(() => {
180
+ puzzleState.syncFromPath($page.url.pathname);
181
+ });
116
182
 
117
- // Sync machine with URL changes
183
+ // Optional: Enforce that users must visit intro before playing
118
184
  $effect(() => {
119
- pageMachine.syncFromPath($page.url.pathname);
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 pageMachine.inIntro}
124
- <IntroView />
125
- {:else if pageMachine.inStep1}
126
- <Step1View />
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
- machine.syncFromPath(currentPath)
221
+ puzzleState.syncFromPath(currentPath)
135
222
 
136
223
  // Get current state
137
- machine.current
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
- machine.getPathForState(stateName)
234
+ puzzleState.getPathForState(stateName)
141
235
 
142
- // Data properties (for business logic)
143
- machine.setData('KEY', value)
144
- machine.getData('KEY')
236
+ // Persistent data properties
237
+ puzzleState.setData(KEY_NAME, value)
238
+ puzzleState.getData(KEY_NAME)
145
239
 
146
240
  // Visited states tracking
147
- machine.hasVisited(stateName)
148
- machine.getVisitedStates()
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
- - Data properties are for business logic, not UI state
302
+ - Use constants for data keys (e.g., `KEY_TUTORIAL_SEEN = 'tutorial-seen'`)
303
+ - Separate persistent data (PageMachine) from reactive game state (GameLogic)
@@ -461,6 +461,7 @@
461
461
 
462
462
  {#if gameHeight}
463
463
  <div
464
+ data-about="game-box-centering"
464
465
  class:center
465
466
  style:height={center ? `${iosWindowHeight ?? windowHeight}px` : undefined}
466
467
  >
@@ -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,7 @@
1
+ /**
2
+ * Navigate to an internal path
3
+ * - Replaces the current url
4
+ *
5
+ * @param {string} path - Pathname to navigate to
6
+ */
7
+ export function switchToPage(path: string): void;
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-core",
3
- "version": "0.5.46",
3
+ "version": "0.5.47",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"
@@ -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
- }