@hkdigital/lib-core 0.5.45 → 0.5.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/state/context/README.md +58 -132
- package/dist/state/context.d.ts +0 -1
- package/dist/state/context.js +0 -1
- package/dist/state/machines/page-machine/PageMachine.svelte.d.ts +165 -35
- package/dist/state/machines/page-machine/PageMachine.svelte.js +336 -38
- package/dist/state/machines/page-machine/README.md +209 -63
- package/dist/ui/components/game-box/GameBox.svelte +1 -0
- package/dist/ui/components/game-box/ScaledContainer.svelte +2 -1
- package/dist/util/sveltekit/navigation.d.ts +7 -0
- package/dist/util/sveltekit/navigation.js +12 -0
- package/dist/util/sveltekit.d.ts +1 -0
- package/dist/util/sveltekit.js +1 -0
- package/dist/util/time/index.js +19 -8
- package/package.json +1 -1
- package/dist/state/context/RouteStateContext.svelte.d.ts +0 -44
- package/dist/state/context/RouteStateContext.svelte.js +0 -119
|
@@ -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 =
|
|
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,28 +28,32 @@
|
|
|
20
28
|
* });
|
|
21
29
|
* ```
|
|
22
30
|
*
|
|
23
|
-
* With
|
|
31
|
+
* With onEnter hooks (for animations):
|
|
24
32
|
* ```javascript
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
44
|
+
* return {
|
|
45
|
+
* abort: () => animation.cancel(),
|
|
46
|
+
* complete: () => animation.finish()
|
|
47
|
+
* };
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
* });
|
|
37
51
|
*
|
|
38
|
-
* //
|
|
39
|
-
* machine.
|
|
52
|
+
* // Fast-forward animation
|
|
53
|
+
* machine.completeTransitions();
|
|
40
54
|
*
|
|
41
|
-
* //
|
|
42
|
-
*
|
|
43
|
-
* // User has seen profile page before
|
|
44
|
-
* }
|
|
55
|
+
* // Cancel animation
|
|
56
|
+
* machine.abortTransitions();
|
|
45
57
|
* ```
|
|
46
58
|
*/
|
|
47
59
|
export default class PageMachine {
|
|
@@ -52,6 +64,18 @@ export default class PageMachine {
|
|
|
52
64
|
// @ts-ignore
|
|
53
65
|
#current = $state();
|
|
54
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
|
+
|
|
55
79
|
/**
|
|
56
80
|
* Map of states to route paths
|
|
57
81
|
* @type {Record<string, string>}
|
|
@@ -85,42 +109,119 @@ export default class PageMachine {
|
|
|
85
109
|
*/
|
|
86
110
|
#revision = $state(0);
|
|
87
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Map of state names to onEnter hook configurations
|
|
114
|
+
* @type {Record<string, {onEnter: Function}>}
|
|
115
|
+
*/
|
|
116
|
+
#onEnterHooks = {};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Current state's onEnter handler (abort/complete functions)
|
|
120
|
+
* @type {{abort?: Function, complete?: Function} | null}
|
|
121
|
+
*/
|
|
122
|
+
#currentOnEnterHandler = null;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Current state's done callback
|
|
126
|
+
* @type {Function | null}
|
|
127
|
+
*/
|
|
128
|
+
#currentOnEnterDone = null;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Flag to prevent concurrent state transitions
|
|
132
|
+
* @type {boolean}
|
|
133
|
+
*/
|
|
134
|
+
#isTransitioning = false;
|
|
135
|
+
|
|
88
136
|
/**
|
|
89
137
|
* Constructor
|
|
90
138
|
*
|
|
91
|
-
* @param {
|
|
92
|
-
* @param {
|
|
93
|
-
*
|
|
139
|
+
* @param {Object} config - Configuration object
|
|
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
|
|
94
148
|
*
|
|
95
149
|
* @example
|
|
96
150
|
* ```javascript
|
|
97
|
-
* const
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
151
|
+
* const machine = new PageMachine({
|
|
152
|
+
* startPath: '/intro/start',
|
|
153
|
+
* routeMap: {
|
|
154
|
+
* [STATE_START]: '/intro/start',
|
|
155
|
+
* [STATE_ANIMATE]: '/intro/animate'
|
|
156
|
+
* },
|
|
157
|
+
* initialData: {
|
|
158
|
+
* INTRO_COMPLETED: false
|
|
159
|
+
* },
|
|
160
|
+
* onEnterHooks: {
|
|
161
|
+
* [STATE_ANIMATE]: (done) => {
|
|
162
|
+
* setTimeout(() => done(STATE_START), 1000);
|
|
163
|
+
* return {
|
|
164
|
+
* abort: () => clearTimeout(...),
|
|
165
|
+
* complete: () => done(STATE_START)
|
|
166
|
+
* };
|
|
167
|
+
* }
|
|
168
|
+
* }
|
|
169
|
+
* });
|
|
108
170
|
* ```
|
|
109
171
|
*/
|
|
110
|
-
constructor(
|
|
111
|
-
|
|
172
|
+
constructor({ startPath, routeMap = {}, initialData = {}, onEnterHooks = {} }) {
|
|
173
|
+
if (!startPath) {
|
|
174
|
+
throw new Error('PageMachine requires startPath parameter');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.#startPath = startPath;
|
|
112
178
|
this.#routeMap = routeMap;
|
|
113
179
|
this.#data = initialData;
|
|
180
|
+
this.#onEnterHooks = this.#normalizeOnEnterHooks(onEnterHooks);
|
|
114
181
|
|
|
115
182
|
// Build reverse map (path -> state)
|
|
116
183
|
for (const [state, path] of Object.entries(routeMap)) {
|
|
117
184
|
this.#pathToStateMap[path] = state;
|
|
118
185
|
}
|
|
119
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
|
+
|
|
120
198
|
// Mark initial state as visited
|
|
121
199
|
this.#visitedStates.add(initialState);
|
|
122
200
|
}
|
|
123
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Normalize onEnterHooks to ensure consistent format
|
|
204
|
+
* Converts function to {onEnter: function} object
|
|
205
|
+
*
|
|
206
|
+
* @param {Record<string, Function|Object>} hooks - Raw hooks configuration
|
|
207
|
+
* @returns {Record<string, {onEnter: Function}>} Normalized hooks
|
|
208
|
+
*/
|
|
209
|
+
#normalizeOnEnterHooks(hooks) {
|
|
210
|
+
const normalized = {};
|
|
211
|
+
|
|
212
|
+
for (const [state, hook] of Object.entries(hooks)) {
|
|
213
|
+
if (typeof hook === 'function') {
|
|
214
|
+
// Simple function -> wrap in object
|
|
215
|
+
normalized[state] = { onEnter: hook };
|
|
216
|
+
} else if (hook && typeof hook === 'object' && hook.onEnter) {
|
|
217
|
+
// Already an object with onEnter
|
|
218
|
+
normalized[state] = hook;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return normalized;
|
|
223
|
+
}
|
|
224
|
+
|
|
124
225
|
/**
|
|
125
226
|
* Synchronize machine state with URL path
|
|
126
227
|
*
|
|
@@ -146,13 +247,66 @@ export default class PageMachine {
|
|
|
146
247
|
|
|
147
248
|
/**
|
|
148
249
|
* Set the current state directly
|
|
250
|
+
* Handles onEnter hooks and auto-transitions
|
|
149
251
|
*
|
|
150
252
|
* @param {string} newState - Target state
|
|
151
253
|
*/
|
|
152
|
-
setState(newState) {
|
|
153
|
-
if (newState
|
|
154
|
-
|
|
254
|
+
async setState(newState) {
|
|
255
|
+
if (newState === this.#current || this.#isTransitioning) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Abort previous state's onEnter handler
|
|
260
|
+
if (this.#currentOnEnterHandler?.abort) {
|
|
261
|
+
this.#currentOnEnterHandler.abort();
|
|
262
|
+
}
|
|
263
|
+
this.#currentOnEnterHandler = null;
|
|
264
|
+
this.#currentOnEnterDone = null;
|
|
265
|
+
|
|
266
|
+
this.#isTransitioning = true;
|
|
267
|
+
this.#current = newState;
|
|
268
|
+
this.#visitedStates.add(newState);
|
|
269
|
+
|
|
270
|
+
// Check if this state has an onEnter hook
|
|
271
|
+
const hookConfig = this.#onEnterHooks[newState];
|
|
272
|
+
if (hookConfig?.onEnter) {
|
|
273
|
+
// Create done callback for auto-transition
|
|
274
|
+
let doneCalled = false;
|
|
275
|
+
const done = (nextState) => {
|
|
276
|
+
if (!doneCalled && nextState && nextState !== newState) {
|
|
277
|
+
doneCalled = true;
|
|
278
|
+
this.#isTransitioning = false;
|
|
279
|
+
this.setState(nextState);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
this.#currentOnEnterDone = done;
|
|
284
|
+
|
|
285
|
+
// Call the onEnter hook
|
|
286
|
+
try {
|
|
287
|
+
const handler = hookConfig.onEnter(done);
|
|
288
|
+
|
|
289
|
+
// Store abort/complete handlers if provided
|
|
290
|
+
if (handler && typeof handler === 'object') {
|
|
291
|
+
if (handler.abort || handler.complete) {
|
|
292
|
+
this.#currentOnEnterHandler = {
|
|
293
|
+
abort: handler.abort,
|
|
294
|
+
complete: handler.complete
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// If hook returned a promise, await it
|
|
300
|
+
if (handler?.then) {
|
|
301
|
+
await handler;
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error(`Error in onEnter hook for state ${newState}:`, error);
|
|
305
|
+
}
|
|
155
306
|
}
|
|
307
|
+
|
|
308
|
+
this.#isTransitioning = false;
|
|
309
|
+
this.#revision++;
|
|
156
310
|
}
|
|
157
311
|
|
|
158
312
|
/**
|
|
@@ -334,4 +488,148 @@ export default class PageMachine {
|
|
|
334
488
|
this.#visitedStates.add(this.#current);
|
|
335
489
|
this.#revision++;
|
|
336
490
|
}
|
|
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
|
+
|
|
562
|
+
/* ===== Transition Control Methods ===== */
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Abort current state's transitions
|
|
566
|
+
* Cancels animations/operations immediately (incomplete state)
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```javascript
|
|
570
|
+
* // User clicks "Cancel" button
|
|
571
|
+
* machine.abortTransitions();
|
|
572
|
+
* ```
|
|
573
|
+
*/
|
|
574
|
+
abortTransitions() {
|
|
575
|
+
if (this.#currentOnEnterHandler?.abort) {
|
|
576
|
+
this.#currentOnEnterHandler.abort();
|
|
577
|
+
this.#currentOnEnterHandler = null;
|
|
578
|
+
this.#currentOnEnterDone = null;
|
|
579
|
+
this.#revision++;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Complete current state's transitions immediately
|
|
585
|
+
* Fast-forwards animations/operations to completion (complete state)
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* ```javascript
|
|
589
|
+
* // User clicks "Skip" or "Next" button
|
|
590
|
+
* machine.completeTransitions();
|
|
591
|
+
* ```
|
|
592
|
+
*/
|
|
593
|
+
completeTransitions() {
|
|
594
|
+
if (this.#currentOnEnterHandler?.complete) {
|
|
595
|
+
this.#currentOnEnterHandler.complete();
|
|
596
|
+
this.#currentOnEnterHandler = null;
|
|
597
|
+
this.#currentOnEnterDone = null;
|
|
598
|
+
this.#revision++;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Check if current state has transitions that can be completed
|
|
604
|
+
*
|
|
605
|
+
* @returns {boolean} True if completeTransitions() can be called
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```svelte
|
|
609
|
+
* {#if machine.canCompleteTransitions}
|
|
610
|
+
* <button onclick={() => machine.completeTransitions()}>Skip</button>
|
|
611
|
+
* {/if}
|
|
612
|
+
* ```
|
|
613
|
+
*/
|
|
614
|
+
get canCompleteTransitions() {
|
|
615
|
+
this.#revision; // Ensure reactivity
|
|
616
|
+
return !!this.#currentOnEnterHandler?.complete;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Check if current state has transitions that can be aborted
|
|
621
|
+
*
|
|
622
|
+
* @returns {boolean} True if abortTransitions() can be called
|
|
623
|
+
*
|
|
624
|
+
* @example
|
|
625
|
+
* ```svelte
|
|
626
|
+
* {#if machine.canAbortTransitions}
|
|
627
|
+
* <button onclick={() => machine.abortTransitions()}>Cancel</button>
|
|
628
|
+
* {/if}
|
|
629
|
+
* ```
|
|
630
|
+
*/
|
|
631
|
+
get canAbortTransitions() {
|
|
632
|
+
this.#revision; // Ensure reactivity
|
|
633
|
+
return !!this.#currentOnEnterHandler?.abort;
|
|
634
|
+
}
|
|
337
635
|
}
|