@hkdigital/lib-core 0.5.45 → 0.5.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,12 +7,20 @@
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
13
+ * - onEnter hooks with abort/complete handlers for animations
12
14
  *
13
15
  * Basic usage:
14
16
  * ```javascript
15
- * const machine = cityState.getOrCreatePageMachine('intro', IntroPageMachine);
17
+ * const machine = new PageMachine({
18
+ * startPath: '/intro/start',
19
+ * routeMap: {
20
+ * [STATE_START]: '/intro/start',
21
+ * [STATE_PROFILE]: '/intro/profile'
22
+ * }
23
+ * });
16
24
  *
17
25
  * // Sync machine state with URL changes
18
26
  * $effect(() => {
@@ -20,54 +28,77 @@
20
28
  * });
21
29
  * ```
22
30
  *
23
- * With data properties (for business logic):
31
+ * With onEnter hooks (for animations):
24
32
  * ```javascript
25
- * // Initialize with server data
26
- * const initialData = {
27
- * HAS_STRONG_PROFILE: false,
28
- * PROFILE_COMPLETED: false,
29
- * MATCHED_SECTOR: null
30
- * };
31
- * const machine = new CircuitPageMachine(initialState, routeMap, initialData);
33
+ * const machine = new PageMachine({
34
+ * startPath: '/game/animate',
35
+ * routeMap: {
36
+ * [STATE_ANIMATE]: '/game/animate',
37
+ * [STATE_PLAY]: '/game/play'
38
+ * },
39
+ * onEnterHooks: {
40
+ * [STATE_ANIMATE]: (done) => {
41
+ * const animation = playAnimation(1000);
42
+ * animation.finished.then(() => done(STATE_PLAY));
32
43
  *
33
- * // Read data
34
- * if (machine.getData('HAS_STRONG_PROFILE')) {
35
- * // Show advanced content
36
- * }
44
+ * return {
45
+ * abort: () => animation.cancel(),
46
+ * complete: () => animation.finish()
47
+ * };
48
+ * }
49
+ * }
50
+ * });
37
51
  *
38
- * // Update data (triggers reactivity)
39
- * machine.setData('HAS_STRONG_PROFILE', true);
52
+ * // Fast-forward animation
53
+ * machine.completeTransitions();
40
54
  *
41
- * // Check visited states
42
- * if (machine.hasVisited(STATE_PROFILE)) {
43
- * // User has seen profile page before
44
- * }
55
+ * // Cancel animation
56
+ * machine.abortTransitions();
45
57
  * ```
46
58
  */
47
59
  export default class PageMachine {
48
60
  /**
49
61
  * Constructor
50
62
  *
51
- * @param {string} initialState - Initial state name
52
- * @param {Record<string, string>} routeMap - Map of states to route paths
53
- * @param {Record<string, any>} [initialData={}] - Initial data properties (from server)
63
+ * @param {Object} config - Configuration object
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
54
72
  *
55
73
  * @example
56
74
  * ```javascript
57
- * const routeMap = {
58
- * [STATE_MATCH]: '/city/intro/match',
59
- * [STATE_CIRCUIT]: '/city/intro/racecircuit'
60
- * };
61
- *
62
- * const initialData = {
63
- * INTRO_COMPLETED: false,
64
- * PROFILE_SCORE: 0
65
- * };
66
- *
67
- * const machine = new CityIntroPageMachine(STATE_START, routeMap, initialData);
75
+ * const machine = new PageMachine({
76
+ * startPath: '/intro/start',
77
+ * routeMap: {
78
+ * [STATE_START]: '/intro/start',
79
+ * [STATE_ANIMATE]: '/intro/animate'
80
+ * },
81
+ * initialData: {
82
+ * INTRO_COMPLETED: false
83
+ * },
84
+ * onEnterHooks: {
85
+ * [STATE_ANIMATE]: (done) => {
86
+ * setTimeout(() => done(STATE_START), 1000);
87
+ * return {
88
+ * abort: () => clearTimeout(...),
89
+ * complete: () => done(STATE_START)
90
+ * };
91
+ * }
92
+ * }
93
+ * });
68
94
  * ```
69
95
  */
70
- constructor(initialState: string, routeMap?: Record<string, string>, initialData?: Record<string, any>);
96
+ constructor({ startPath, routeMap, initialData, onEnterHooks }: {
97
+ startPath: string;
98
+ routeMap?: Record<string, string> | undefined;
99
+ initialData?: Record<string, any> | undefined;
100
+ onEnterHooks?: Record<string, Function> | undefined;
101
+ });
71
102
  /**
72
103
  * Synchronize machine state with URL path
73
104
  *
@@ -81,10 +112,11 @@ export default class PageMachine {
81
112
  syncFromPath(currentPath: string): boolean;
82
113
  /**
83
114
  * Set the current state directly
115
+ * Handles onEnter hooks and auto-transitions
84
116
  *
85
117
  * @param {string} newState - Target state
86
118
  */
87
- setState(newState: string): void;
119
+ setState(newState: string): Promise<void>;
88
120
  /**
89
121
  * Get route path for a given state
90
122
  *
@@ -191,5 +223,103 @@ export default class PageMachine {
191
223
  * Useful for testing or resetting experience
192
224
  */
193
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;
276
+ /**
277
+ * Abort current state's transitions
278
+ * Cancels animations/operations immediately (incomplete state)
279
+ *
280
+ * @example
281
+ * ```javascript
282
+ * // User clicks "Cancel" button
283
+ * machine.abortTransitions();
284
+ * ```
285
+ */
286
+ abortTransitions(): void;
287
+ /**
288
+ * Complete current state's transitions immediately
289
+ * Fast-forwards animations/operations to completion (complete state)
290
+ *
291
+ * @example
292
+ * ```javascript
293
+ * // User clicks "Skip" or "Next" button
294
+ * machine.completeTransitions();
295
+ * ```
296
+ */
297
+ completeTransitions(): void;
298
+ /**
299
+ * Check if current state has transitions that can be completed
300
+ *
301
+ * @returns {boolean} True if completeTransitions() can be called
302
+ *
303
+ * @example
304
+ * ```svelte
305
+ * {#if machine.canCompleteTransitions}
306
+ * <button onclick={() => machine.completeTransitions()}>Skip</button>
307
+ * {/if}
308
+ * ```
309
+ */
310
+ get canCompleteTransitions(): boolean;
311
+ /**
312
+ * Check if current state has transitions that can be aborted
313
+ *
314
+ * @returns {boolean} True if abortTransitions() can be called
315
+ *
316
+ * @example
317
+ * ```svelte
318
+ * {#if machine.canAbortTransitions}
319
+ * <button onclick={() => machine.abortTransitions()}>Cancel</button>
320
+ * {/if}
321
+ * ```
322
+ */
323
+ get canAbortTransitions(): boolean;
194
324
  #private;
195
325
  }