@hkdigital/lib-core 0.5.46 → 0.5.48

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,23 @@ 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
72
+ * @param {string} [config.name='PageMachine']
73
+ * Name for logger identification
74
+ * @param {import('../../../logging/typedef.js').LogLevel} [config.logLevel]
75
+ * Log level (defaults to INFO for state transitions)
67
76
  *
68
77
  * @example
69
78
  * ```javascript
70
79
  * const machine = new PageMachine({
71
- * initialState: STATE_START,
80
+ * startPath: '/intro/start',
72
81
  * routeMap: {
73
82
  * [STATE_START]: '/intro/start',
74
83
  * [STATE_ANIMATE]: '/intro/animate'
@@ -88,12 +97,19 @@ export default class PageMachine {
88
97
  * });
89
98
  * ```
90
99
  */
91
- constructor({ initialState, routeMap, initialData, onEnterHooks }: {
92
- initialState: string;
100
+ constructor({ startPath, routeMap, initialData, onEnterHooks, name, logLevel }: {
101
+ startPath: string;
93
102
  routeMap?: Record<string, string> | undefined;
94
103
  initialData?: Record<string, any> | undefined;
95
104
  onEnterHooks?: Record<string, Function> | undefined;
105
+ name?: string | undefined;
106
+ logLevel?: import("../../../logging/typedef.js").LogLevel | undefined;
96
107
  });
108
+ /**
109
+ * Logger instance for state machine
110
+ * @type {Logger}
111
+ */
112
+ logger: Logger;
97
113
  /**
98
114
  * Synchronize machine state with URL path
99
115
  *
@@ -218,6 +234,56 @@ export default class PageMachine {
218
234
  * Useful for testing or resetting experience
219
235
  */
220
236
  resetVisitedStates(): void;
237
+ /**
238
+ * Get the start path
239
+ *
240
+ * @returns {string} Start path
241
+ */
242
+ get startPath(): string;
243
+ /**
244
+ * Get the start state
245
+ *
246
+ * @returns {string} Start state name
247
+ */
248
+ get startState(): string;
249
+ /**
250
+ * Check if the supplied path matches the start path
251
+ *
252
+ * @param {string} path - Path to check
253
+ *
254
+ * @returns {boolean} True if path matches start path
255
+ *
256
+ * @example
257
+ * ```javascript
258
+ * if (machine.isStartPath('/game/play')) {
259
+ * // User is on the start page
260
+ * }
261
+ * ```
262
+ */
263
+ isStartPath(path: string): boolean;
264
+ /**
265
+ * Check if currently on the start state
266
+ *
267
+ * @returns {boolean} True if current state is the start state
268
+ *
269
+ * @example
270
+ * ```javascript
271
+ * if (machine.isOnStartState) {
272
+ * // Show onboarding
273
+ * }
274
+ * ```
275
+ */
276
+ get isOnStartState(): boolean;
277
+ /**
278
+ * Navigate to the start path
279
+ *
280
+ * @example
281
+ * ```javascript
282
+ * // Redirect user to start
283
+ * machine.redirectToStartPath();
284
+ * ```
285
+ */
286
+ redirectToStartPath(): void;
221
287
  /**
222
288
  * Abort current state's transitions
223
289
  * Cancels animations/operations immediately (incomplete state)
@@ -268,3 +334,4 @@ export default class PageMachine {
268
334
  get canAbortTransitions(): boolean;
269
335
  #private;
270
336
  }
337
+ import { Logger } from '../../../logging/common.js';
@@ -1,3 +1,5 @@
1
+ import { Logger, INFO } from '../../../logging/common.js';
2
+
1
3
  /**
2
4
  * Base class for page state machines with URL route mapping
3
5
  *
@@ -7,6 +9,7 @@
7
9
  *
8
10
  * Features:
9
11
  * - State-to-route mapping and sync
12
+ * - Start path management
10
13
  * - Data properties for business/domain state
11
14
  * - Visited states tracking
12
15
  * - onEnter hooks with abort/complete handlers for animations
@@ -14,7 +17,7 @@
14
17
  * Basic usage:
15
18
  * ```javascript
16
19
  * const machine = new PageMachine({
17
- * initialState: STATE_START,
20
+ * startPath: '/intro/start',
18
21
  * routeMap: {
19
22
  * [STATE_START]: '/intro/start',
20
23
  * [STATE_PROFILE]: '/intro/profile'
@@ -30,7 +33,7 @@
30
33
  * With onEnter hooks (for animations):
31
34
  * ```javascript
32
35
  * const machine = new PageMachine({
33
- * initialState: STATE_ANIMATE,
36
+ * startPath: '/game/animate',
34
37
  * routeMap: {
35
38
  * [STATE_ANIMATE]: '/game/animate',
36
39
  * [STATE_PLAY]: '/game/play'
@@ -56,6 +59,11 @@
56
59
  * ```
57
60
  */
58
61
  export default class PageMachine {
62
+ /**
63
+ * Logger instance for state machine
64
+ * @type {Logger}
65
+ */
66
+ logger;
59
67
  /**
60
68
  * Current state
61
69
  * @type {string}
@@ -63,6 +71,18 @@ export default class PageMachine {
63
71
  // @ts-ignore
64
72
  #current = $state();
65
73
 
74
+ /**
75
+ * Start path for this page machine
76
+ * @type {string}
77
+ */
78
+ #startPath = '';
79
+
80
+ /**
81
+ * Initial/start state (derived from startPath)
82
+ * @type {string}
83
+ */
84
+ #startState = '';
85
+
66
86
  /**
67
87
  * Map of states to route paths
68
88
  * @type {Record<string, string>}
@@ -124,15 +144,23 @@ export default class PageMachine {
124
144
  * Constructor
125
145
  *
126
146
  * @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
147
+ * @param {string} config.startPath
148
+ * Start path for this route group (e.g., '/game/play')
149
+ * @param {Record<string, string>} [config.routeMap={}]
150
+ * Map of states to route paths
151
+ * @param {Record<string, any>} [config.initialData={}]
152
+ * Initial data properties (from server)
153
+ * @param {Record<string, Function>} [config.onEnterHooks={}]
154
+ * Map of states to onEnter hook functions
155
+ * @param {string} [config.name='PageMachine']
156
+ * Name for logger identification
157
+ * @param {import('../../../logging/typedef.js').LogLevel} [config.logLevel]
158
+ * Log level (defaults to INFO for state transitions)
131
159
  *
132
160
  * @example
133
161
  * ```javascript
134
162
  * const machine = new PageMachine({
135
- * initialState: STATE_START,
163
+ * startPath: '/intro/start',
136
164
  * routeMap: {
137
165
  * [STATE_START]: '/intro/start',
138
166
  * [STATE_ANIMATE]: '/intro/animate'
@@ -152,12 +180,20 @@ export default class PageMachine {
152
180
  * });
153
181
  * ```
154
182
  */
155
- constructor({ initialState, routeMap = {}, initialData = {}, onEnterHooks = {} }) {
156
- if (!initialState) {
157
- throw new Error('PageMachine requires initialState parameter');
183
+ constructor({
184
+ startPath,
185
+ routeMap = {},
186
+ initialData = {},
187
+ onEnterHooks = {},
188
+ name = 'PageMachine',
189
+ logLevel = INFO
190
+ }) {
191
+ if (!startPath) {
192
+ throw new Error('PageMachine requires startPath parameter');
158
193
  }
159
194
 
160
- this.#current = initialState;
195
+ this.logger = new Logger(name, logLevel);
196
+ this.#startPath = startPath;
161
197
  this.#routeMap = routeMap;
162
198
  this.#data = initialData;
163
199
  this.#onEnterHooks = this.#normalizeOnEnterHooks(onEnterHooks);
@@ -167,6 +203,17 @@ export default class PageMachine {
167
203
  this.#pathToStateMap[path] = state;
168
204
  }
169
205
 
206
+ // Derive initial state from startPath
207
+ const initialState = this.#pathToStateMap[startPath];
208
+ if (!initialState) {
209
+ throw new Error(
210
+ `PageMachine: startPath "${startPath}" not found in routeMap`
211
+ );
212
+ }
213
+
214
+ this.#startState = initialState;
215
+ this.#current = initialState;
216
+
170
217
  // Mark initial state as visited
171
218
  this.#visitedStates.add(initialState);
172
219
  }
@@ -235,10 +282,14 @@ export default class PageMachine {
235
282
  this.#currentOnEnterHandler = null;
236
283
  this.#currentOnEnterDone = null;
237
284
 
285
+ const oldState = this.#current;
238
286
  this.#isTransitioning = true;
239
287
  this.#current = newState;
240
288
  this.#visitedStates.add(newState);
241
289
 
290
+ // Log state transition
291
+ this.logger.debug(`${oldState} → ${newState}`);
292
+
242
293
  // Check if this state has an onEnter hook
243
294
  const hookConfig = this.#onEnterHooks[newState];
244
295
  if (hookConfig?.onEnter) {
@@ -461,6 +512,76 @@ export default class PageMachine {
461
512
  this.#revision++;
462
513
  }
463
514
 
515
+ /* ===== Start Path Methods ===== */
516
+
517
+ /**
518
+ * Get the start path
519
+ *
520
+ * @returns {string} Start path
521
+ */
522
+ get startPath() {
523
+ return this.#startPath;
524
+ }
525
+
526
+ /**
527
+ * Get the start state
528
+ *
529
+ * @returns {string} Start state name
530
+ */
531
+ get startState() {
532
+ return this.#startState;
533
+ }
534
+
535
+ /**
536
+ * Check if the supplied path matches the start path
537
+ *
538
+ * @param {string} path - Path to check
539
+ *
540
+ * @returns {boolean} True if path matches start path
541
+ *
542
+ * @example
543
+ * ```javascript
544
+ * if (machine.isStartPath('/game/play')) {
545
+ * // User is on the start page
546
+ * }
547
+ * ```
548
+ */
549
+ isStartPath(path) {
550
+ return path === this.#startPath;
551
+ }
552
+
553
+ /**
554
+ * Check if currently on the start state
555
+ *
556
+ * @returns {boolean} True if current state is the start state
557
+ *
558
+ * @example
559
+ * ```javascript
560
+ * if (machine.isOnStartState) {
561
+ * // Show onboarding
562
+ * }
563
+ * ```
564
+ */
565
+ get isOnStartState() {
566
+ return this.#current === this.#startState;
567
+ }
568
+
569
+ /**
570
+ * Navigate to the start path
571
+ *
572
+ * @example
573
+ * ```javascript
574
+ * // Redirect user to start
575
+ * machine.redirectToStartPath();
576
+ * ```
577
+ */
578
+ redirectToStartPath() {
579
+ // Import dynamically to avoid circular dependencies
580
+ import('../../../util/sveltekit.js').then(({ switchToPage }) => {
581
+ switchToPage(this.#startPath);
582
+ });
583
+ }
584
+
464
585
  /* ===== Transition Control Methods ===== */
465
586
 
466
587
  /**
@@ -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.48",
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
- }