@hkdigital/lib-core 0.5.56 → 0.5.58
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/logging/internal/logger/Logger.js +1 -1
- package/dist/state/machines/page-machine/PageMachine.svelte.d.ts +76 -122
- package/dist/state/machines/page-machine/PageMachine.svelte.js +97 -394
- package/dist/state/machines/page-machine/PageMachine.svelte.js__ +708 -0
- package/dist/state/machines/page-machine/README.md +102 -66
- package/package.json +1 -1
|
@@ -1,99 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Route-aware data manager for page groups
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Does NOT enforce
|
|
6
|
-
* (because users can navigate to any URL via browser).
|
|
4
|
+
* Tracks navigation within a route group and manages business/domain data.
|
|
5
|
+
* Does NOT enforce transitions - allows free navigation via browser.
|
|
7
6
|
*
|
|
8
7
|
* Features:
|
|
9
|
-
* -
|
|
8
|
+
* - Current route tracking and synchronization
|
|
10
9
|
* - Start path management
|
|
11
10
|
* - Data properties for business/domain state
|
|
12
|
-
* - Visited
|
|
13
|
-
* - onEnter hooks with abort/complete handlers for animations
|
|
11
|
+
* - Visited routes tracking
|
|
14
12
|
*
|
|
15
13
|
* Basic usage:
|
|
16
14
|
* ```javascript
|
|
17
15
|
* const machine = new PageMachine({
|
|
18
16
|
* startPath: '/intro/start',
|
|
19
|
-
*
|
|
20
|
-
* [STATE_START]: '/intro/start',
|
|
21
|
-
* [STATE_PROFILE]: '/intro/profile'
|
|
22
|
-
* }
|
|
17
|
+
* routes: ['/intro/start', '/intro/profile', '/intro/complete']
|
|
23
18
|
* });
|
|
24
19
|
*
|
|
25
|
-
* // Sync machine
|
|
20
|
+
* // Sync machine with URL changes
|
|
26
21
|
* $effect(() => {
|
|
27
22
|
* machine.syncFromPath($page.url.pathname);
|
|
28
23
|
* });
|
|
24
|
+
*
|
|
25
|
+
* // Check current route
|
|
26
|
+
* if (machine.current === '/intro/profile') {
|
|
27
|
+
* // Do something
|
|
28
|
+
* }
|
|
29
29
|
* ```
|
|
30
30
|
*
|
|
31
|
-
*
|
|
31
|
+
* Animations and page-specific logic should use $effect in pages:
|
|
32
32
|
* ```javascript
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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));
|
|
33
|
+
* // In +page.svelte
|
|
34
|
+
* const animations = new PageAnimations();
|
|
43
35
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* };
|
|
48
|
-
* }
|
|
36
|
+
* $effect(() => {
|
|
37
|
+
* if (someCondition) {
|
|
38
|
+
* animations.start();
|
|
49
39
|
* }
|
|
50
40
|
* });
|
|
51
|
-
*
|
|
52
|
-
* // Fast-forward animation
|
|
53
|
-
* machine.completeTransitions();
|
|
54
|
-
*
|
|
55
|
-
* // Cancel animation
|
|
56
|
-
* machine.abortTransitions();
|
|
57
41
|
* ```
|
|
58
42
|
*/
|
|
59
|
-
import { switchToPage } from '../../../util/sveltekit.js';
|
|
60
|
-
|
|
61
43
|
export default class PageMachine {
|
|
62
44
|
/**
|
|
63
|
-
* Logger instance
|
|
45
|
+
* Logger instance
|
|
64
46
|
* @type {import('../../../logging/client.js').Logger}
|
|
65
47
|
*/
|
|
66
48
|
logger;
|
|
49
|
+
|
|
67
50
|
/**
|
|
68
|
-
* Current
|
|
51
|
+
* Current route path
|
|
69
52
|
* @type {string}
|
|
70
53
|
*/
|
|
71
54
|
// @ts-ignore
|
|
72
55
|
#current = $state();
|
|
73
56
|
|
|
74
57
|
/**
|
|
75
|
-
* Start path for this
|
|
58
|
+
* Start path for this route group
|
|
76
59
|
* @type {string}
|
|
77
60
|
*/
|
|
78
61
|
#startPath = '';
|
|
79
62
|
|
|
80
63
|
/**
|
|
81
|
-
*
|
|
82
|
-
* @type {string}
|
|
83
|
-
*/
|
|
84
|
-
#startState = '';
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Map of states to route paths
|
|
88
|
-
* @type {Record<string, string>}
|
|
64
|
+
* Optional list of valid routes for this group
|
|
65
|
+
* @type {string[]}
|
|
89
66
|
*/
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Reverse map of route paths to states
|
|
94
|
-
* @type {Record<string, string>}
|
|
95
|
-
*/
|
|
96
|
-
#pathToStateMap = {};
|
|
67
|
+
#routes = [];
|
|
97
68
|
|
|
98
69
|
/**
|
|
99
70
|
* Data properties for business/domain state
|
|
@@ -103,12 +74,12 @@ export default class PageMachine {
|
|
|
103
74
|
#data = $state({});
|
|
104
75
|
|
|
105
76
|
/**
|
|
106
|
-
* Track which
|
|
77
|
+
* Track which routes have been visited during this session
|
|
107
78
|
* Useful for showing first-time hints/tips
|
|
108
79
|
* @type {Set<string>}
|
|
109
80
|
*/
|
|
110
81
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
111
|
-
#
|
|
82
|
+
#visitedRoutes = new Set();
|
|
112
83
|
|
|
113
84
|
/**
|
|
114
85
|
* Revision counter for triggering reactivity
|
|
@@ -116,150 +87,68 @@ export default class PageMachine {
|
|
|
116
87
|
*/
|
|
117
88
|
#revision = $state(0);
|
|
118
89
|
|
|
119
|
-
/**
|
|
120
|
-
* Map of state names to onEnter hook configurations
|
|
121
|
-
* @type {Record<string, {onEnter: Function}>}
|
|
122
|
-
*/
|
|
123
|
-
#onEnterHooks = {};
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Current state's onEnter handler (abort/complete functions)
|
|
127
|
-
* @type {{abort?: Function, complete?: Function} | null}
|
|
128
|
-
*/
|
|
129
|
-
#currentOnEnterHandler = null;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Current state's done callback
|
|
133
|
-
* @type {Function | null}
|
|
134
|
-
*/
|
|
135
|
-
#currentOnEnterDone = null;
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Flag to prevent concurrent state transitions
|
|
139
|
-
* @type {boolean}
|
|
140
|
-
*/
|
|
141
|
-
#isTransitioning = false;
|
|
142
|
-
|
|
143
90
|
/**
|
|
144
91
|
* Constructor
|
|
145
92
|
*
|
|
146
93
|
* @param {Object} config - Configuration object
|
|
147
94
|
* @param {string} config.startPath
|
|
148
95
|
* Start path for this route group (e.g., '/game/play')
|
|
149
|
-
* @param {
|
|
150
|
-
*
|
|
96
|
+
* @param {string[]} [config.routes=[]]
|
|
97
|
+
* Optional list of valid routes (for validation/dev tools)
|
|
151
98
|
* @param {Record<string, any>} [config.initialData={}]
|
|
152
99
|
* Initial data properties (from server)
|
|
153
|
-
* @param {Record<string, Function>} [config.onEnterHooks={}]
|
|
154
|
-
* Map of states to onEnter hook functions
|
|
155
100
|
* @param {import('../../../logging/client.js').Logger} [config.logger]
|
|
156
|
-
* Logger instance (optional
|
|
101
|
+
* Logger instance (optional)
|
|
157
102
|
*
|
|
158
103
|
* @example
|
|
159
104
|
* ```javascript
|
|
160
105
|
* const machine = new PageMachine({
|
|
161
106
|
* startPath: '/intro/start',
|
|
162
|
-
*
|
|
163
|
-
* [STATE_START]: '/intro/start',
|
|
164
|
-
* [STATE_ANIMATE]: '/intro/animate'
|
|
165
|
-
* },
|
|
107
|
+
* routes: ['/intro/start', '/intro/profile'],
|
|
166
108
|
* initialData: {
|
|
167
109
|
* INTRO_COMPLETED: false
|
|
168
|
-
* },
|
|
169
|
-
* onEnterHooks: {
|
|
170
|
-
* [STATE_ANIMATE]: (done) => {
|
|
171
|
-
* setTimeout(() => done(STATE_START), 1000);
|
|
172
|
-
* return {
|
|
173
|
-
* abort: () => clearTimeout(...),
|
|
174
|
-
* complete: () => done(STATE_START)
|
|
175
|
-
* };
|
|
176
|
-
* }
|
|
177
110
|
* }
|
|
178
111
|
* });
|
|
179
112
|
* ```
|
|
180
113
|
*/
|
|
181
|
-
constructor({
|
|
182
|
-
startPath,
|
|
183
|
-
routeMap = {},
|
|
184
|
-
initialData = {},
|
|
185
|
-
onEnterHooks = {},
|
|
186
|
-
logger = null
|
|
187
|
-
}) {
|
|
114
|
+
constructor({ startPath, routes = [], initialData = {}, logger = null }) {
|
|
188
115
|
if (!startPath) {
|
|
189
116
|
throw new Error('PageMachine requires startPath parameter');
|
|
190
117
|
}
|
|
191
118
|
|
|
192
119
|
this.logger = logger;
|
|
193
120
|
this.#startPath = startPath;
|
|
194
|
-
this.#
|
|
121
|
+
this.#routes = routes;
|
|
195
122
|
this.#data = initialData;
|
|
196
|
-
this.#
|
|
197
|
-
|
|
198
|
-
// Build reverse map (path -> state)
|
|
199
|
-
for (const [state, path] of Object.entries(routeMap)) {
|
|
200
|
-
this.#pathToStateMap[path] = state;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Derive initial state from startPath
|
|
204
|
-
const initialState = this.#pathToStateMap[startPath];
|
|
205
|
-
if (!initialState) {
|
|
206
|
-
throw new Error(
|
|
207
|
-
`PageMachine: startPath "${startPath}" not found in routeMap`
|
|
208
|
-
);
|
|
209
|
-
}
|
|
123
|
+
this.#current = startPath;
|
|
210
124
|
|
|
211
|
-
|
|
212
|
-
this.#
|
|
213
|
-
|
|
214
|
-
// Mark initial state as visited
|
|
215
|
-
this.#visitedStates.add(initialState);
|
|
125
|
+
// Mark start path as visited
|
|
126
|
+
this.#visitedRoutes.add(startPath);
|
|
216
127
|
}
|
|
217
128
|
|
|
218
129
|
/**
|
|
219
|
-
*
|
|
220
|
-
* Converts function to {onEnter: function} object
|
|
130
|
+
* Synchronize machine with URL path
|
|
221
131
|
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*/
|
|
225
|
-
#normalizeOnEnterHooks(hooks) {
|
|
226
|
-
/** @type {Record<string, {onEnter: Function} */
|
|
227
|
-
const normalized = {};
|
|
228
|
-
|
|
229
|
-
for (const [state, hook] of Object.entries(hooks)) {
|
|
230
|
-
if (typeof hook === 'function') {
|
|
231
|
-
// Simple function -> wrap in object
|
|
232
|
-
normalized[state] = { onEnter: hook };
|
|
233
|
-
} else if (hook?.onEnter) {
|
|
234
|
-
// Already an object with onEnter
|
|
235
|
-
normalized[state] = hook;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return normalized;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Synchronize machine state with URL path
|
|
244
|
-
*
|
|
245
|
-
* Call this in a $effect that watches $page.url.pathname
|
|
246
|
-
* Automatically tracks visited states and triggers onEnter hooks
|
|
132
|
+
* Call this in a $effect that watches $page.url.pathname.
|
|
133
|
+
* Automatically tracks visited routes.
|
|
247
134
|
*
|
|
248
135
|
* @param {string} currentPath - Current URL pathname
|
|
249
136
|
*
|
|
250
|
-
* @returns {boolean} True if
|
|
137
|
+
* @returns {boolean} True if route was changed
|
|
251
138
|
*/
|
|
252
139
|
syncFromPath(currentPath) {
|
|
253
|
-
|
|
140
|
+
// Find matching route
|
|
141
|
+
const matchedRoute = this.#findMatchingRoute(currentPath);
|
|
142
|
+
|
|
143
|
+
if (matchedRoute && matchedRoute !== this.#current) {
|
|
144
|
+
this.logger?.debug(`syncFromPath: ${currentPath} → ${matchedRoute}`);
|
|
254
145
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
this.
|
|
258
|
-
|
|
259
|
-
);
|
|
146
|
+
const oldRoute = this.#current;
|
|
147
|
+
this.#current = matchedRoute;
|
|
148
|
+
this.#visitedRoutes.add(matchedRoute);
|
|
149
|
+
this.#revision++;
|
|
260
150
|
|
|
261
|
-
|
|
262
|
-
this.#setState(targetState);
|
|
151
|
+
this.logger?.debug(`Route changed: ${oldRoute} → ${matchedRoute}`);
|
|
263
152
|
|
|
264
153
|
return true;
|
|
265
154
|
}
|
|
@@ -268,154 +157,50 @@ export default class PageMachine {
|
|
|
268
157
|
}
|
|
269
158
|
|
|
270
159
|
/**
|
|
271
|
-
*
|
|
272
|
-
* Handles onEnter hooks and auto-transitions
|
|
273
|
-
*
|
|
274
|
-
* Note: This is private to enforce URL-first navigation.
|
|
275
|
-
* To change state, navigate to the URL using switchToPage()
|
|
276
|
-
* and let syncFromPath() update the state.
|
|
277
|
-
*
|
|
278
|
-
* @param {string} newState - Target state
|
|
279
|
-
*/
|
|
280
|
-
async #setState(newState) {
|
|
281
|
-
if (newState === this.#current || this.#isTransitioning) {
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Abort previous state's onEnter handler
|
|
286
|
-
if (this.#currentOnEnterHandler?.abort) {
|
|
287
|
-
this.#currentOnEnterHandler.abort();
|
|
288
|
-
}
|
|
289
|
-
this.#currentOnEnterHandler = null;
|
|
290
|
-
this.#currentOnEnterDone = null;
|
|
291
|
-
|
|
292
|
-
const oldState = this.#current;
|
|
293
|
-
this.#isTransitioning = true;
|
|
294
|
-
this.#current = newState;
|
|
295
|
-
this.#visitedStates.add(newState);
|
|
296
|
-
|
|
297
|
-
// Log state transition
|
|
298
|
-
this.logger?.debug(`setState: ${oldState} → ${newState}`);
|
|
299
|
-
|
|
300
|
-
// Check if this state has an onEnter hook
|
|
301
|
-
const hookConfig = this.#onEnterHooks[newState];
|
|
302
|
-
if (hookConfig?.onEnter) {
|
|
303
|
-
// Create done callback for auto-transition
|
|
304
|
-
let doneCalled = false;
|
|
305
|
-
|
|
306
|
-
const done = (/** @type {string} */ nextState) => {
|
|
307
|
-
if (!doneCalled && nextState && nextState !== newState) {
|
|
308
|
-
doneCalled = true;
|
|
309
|
-
this.#isTransitioning = false;
|
|
310
|
-
this.#setState(nextState);
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
this.#currentOnEnterDone = done;
|
|
315
|
-
|
|
316
|
-
// Call the onEnter hook
|
|
317
|
-
try {
|
|
318
|
-
const handler = hookConfig.onEnter(done);
|
|
319
|
-
|
|
320
|
-
// Store abort/complete handlers if provided
|
|
321
|
-
if (handler && typeof handler === 'object') {
|
|
322
|
-
if (handler.abort || handler.complete) {
|
|
323
|
-
this.#currentOnEnterHandler = {
|
|
324
|
-
abort: handler.abort,
|
|
325
|
-
complete: handler.complete
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// If hook returned a promise, await it
|
|
331
|
-
if (handler?.then) {
|
|
332
|
-
await handler;
|
|
333
|
-
}
|
|
334
|
-
} catch (error) {
|
|
335
|
-
const logger = this.logger ?? console;
|
|
336
|
-
logger.error(`Error in onEnter hook for state ${newState}:`, error);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
this.#isTransitioning = false;
|
|
341
|
-
this.#revision++;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Get state name from URL path
|
|
160
|
+
* Find matching route from path
|
|
346
161
|
*
|
|
347
162
|
* @param {string} path - URL pathname
|
|
348
163
|
*
|
|
349
|
-
* @returns {string|null}
|
|
164
|
+
* @returns {string|null} Matched route or null
|
|
350
165
|
*/
|
|
351
|
-
#
|
|
352
|
-
//
|
|
353
|
-
if (this.#
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
// Try partial match (path includes route)
|
|
358
|
-
for (const [routePath, state] of Object.entries(this.#pathToStateMap)) {
|
|
359
|
-
if (path.includes(routePath)) {
|
|
360
|
-
return state;
|
|
166
|
+
#findMatchingRoute(path) {
|
|
167
|
+
// If routes list provided, try to match against it
|
|
168
|
+
if (this.#routes.length > 0) {
|
|
169
|
+
// Try exact match first
|
|
170
|
+
if (this.#routes.includes(path)) {
|
|
171
|
+
return path;
|
|
361
172
|
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return null;
|
|
365
|
-
}
|
|
366
173
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
*/
|
|
374
|
-
getPathForState(state) {
|
|
375
|
-
const path = this.#routeMap[state];
|
|
174
|
+
// Try partial match (path starts with route)
|
|
175
|
+
for (const route of this.#routes) {
|
|
176
|
+
if (path.startsWith(route)) {
|
|
177
|
+
return route;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
376
180
|
|
|
377
|
-
|
|
378
|
-
throw new Error(`No path found for state [${state}]`);
|
|
181
|
+
return null;
|
|
379
182
|
}
|
|
380
183
|
|
|
184
|
+
// No routes list - accept any path
|
|
381
185
|
return path;
|
|
382
186
|
}
|
|
383
187
|
|
|
384
188
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* @param {string} state - State name
|
|
388
|
-
*/
|
|
389
|
-
navigateToState(state) {
|
|
390
|
-
const path = this.getPathForState(state);
|
|
391
|
-
switchToPage(path);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Get route path for current state
|
|
189
|
+
* Get current route
|
|
396
190
|
*
|
|
397
|
-
* @returns {string
|
|
398
|
-
*/
|
|
399
|
-
getCurrentPath() {
|
|
400
|
-
return this.getPathForState(this.#current);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Get current state
|
|
405
|
-
*
|
|
406
|
-
* @returns {string} Current state name
|
|
191
|
+
* @returns {string} Current route path
|
|
407
192
|
*/
|
|
408
193
|
get current() {
|
|
409
194
|
return this.#current;
|
|
410
195
|
}
|
|
411
196
|
|
|
412
197
|
/**
|
|
413
|
-
* Get the
|
|
198
|
+
* Get the routes list
|
|
414
199
|
*
|
|
415
|
-
* @returns {
|
|
200
|
+
* @returns {string[]} Copy of routes list
|
|
416
201
|
*/
|
|
417
|
-
get
|
|
418
|
-
return
|
|
202
|
+
get routes() {
|
|
203
|
+
return [...this.#routes];
|
|
419
204
|
}
|
|
420
205
|
|
|
421
206
|
/* ===== Data Properties (Business/Domain State) ===== */
|
|
@@ -476,7 +261,8 @@ export default class PageMachine {
|
|
|
476
261
|
/**
|
|
477
262
|
* Update multiple data properties at once
|
|
478
263
|
*
|
|
479
|
-
* @param {Record<string, any>} dataUpdates
|
|
264
|
+
* @param {Record<string, any>} dataUpdates
|
|
265
|
+
* Object with key-value pairs
|
|
480
266
|
*
|
|
481
267
|
* @example
|
|
482
268
|
* ```javascript
|
|
@@ -494,32 +280,32 @@ export default class PageMachine {
|
|
|
494
280
|
this.#revision++;
|
|
495
281
|
}
|
|
496
282
|
|
|
497
|
-
/* ===== Visited
|
|
283
|
+
/* ===== Visited Routes Tracking ===== */
|
|
498
284
|
|
|
499
285
|
/**
|
|
500
|
-
* Check if a
|
|
286
|
+
* Check if a route has been visited
|
|
501
287
|
*
|
|
502
|
-
* @param {string}
|
|
288
|
+
* @param {string} route - Route path to check
|
|
503
289
|
*
|
|
504
|
-
* @returns {boolean} True if the
|
|
290
|
+
* @returns {boolean} True if the route has been visited
|
|
505
291
|
*
|
|
506
292
|
* @example
|
|
507
293
|
* ```javascript
|
|
508
|
-
* if (machine.hasVisited(
|
|
294
|
+
* if (machine.hasVisited('/intro/profile')) {
|
|
509
295
|
* // User has seen profile page, skip intro
|
|
510
296
|
* }
|
|
511
297
|
* ```
|
|
512
298
|
*/
|
|
513
|
-
hasVisited(
|
|
299
|
+
hasVisited(route) {
|
|
514
300
|
// Access revision to ensure reactivity
|
|
515
301
|
this.#revision;
|
|
516
|
-
return this.#
|
|
302
|
+
return this.#visitedRoutes.has(route);
|
|
517
303
|
}
|
|
518
304
|
|
|
519
305
|
/**
|
|
520
|
-
* Check if the start
|
|
306
|
+
* Check if the start route has been visited
|
|
521
307
|
*
|
|
522
|
-
* @returns {boolean} True if the start
|
|
308
|
+
* @returns {boolean} True if the start route has been visited
|
|
523
309
|
*
|
|
524
310
|
* @example
|
|
525
311
|
* ```javascript
|
|
@@ -529,27 +315,27 @@ export default class PageMachine {
|
|
|
529
315
|
* ```
|
|
530
316
|
*/
|
|
531
317
|
get hasVisitedStart() {
|
|
532
|
-
return this.hasVisited(this.#
|
|
318
|
+
return this.hasVisited(this.#startPath);
|
|
533
319
|
}
|
|
534
320
|
|
|
535
321
|
/**
|
|
536
|
-
* Get all visited
|
|
322
|
+
* Get all visited routes
|
|
537
323
|
*
|
|
538
|
-
* @returns {string[]} Array of visited
|
|
324
|
+
* @returns {string[]} Array of visited route paths
|
|
539
325
|
*/
|
|
540
|
-
|
|
326
|
+
getVisitedRoutes() {
|
|
541
327
|
// Access revision to ensure reactivity
|
|
542
328
|
this.#revision;
|
|
543
|
-
return Array.from(this.#
|
|
329
|
+
return Array.from(this.#visitedRoutes);
|
|
544
330
|
}
|
|
545
331
|
|
|
546
332
|
/**
|
|
547
|
-
* Reset visited
|
|
333
|
+
* Reset visited routes tracking
|
|
548
334
|
* Useful for testing or resetting experience
|
|
549
335
|
*/
|
|
550
|
-
|
|
551
|
-
this.#
|
|
552
|
-
this.#
|
|
336
|
+
resetVisitedRoutes() {
|
|
337
|
+
this.#visitedRoutes.clear();
|
|
338
|
+
this.#visitedRoutes.add(this.#current);
|
|
553
339
|
this.#revision++;
|
|
554
340
|
}
|
|
555
341
|
|
|
@@ -564,15 +350,6 @@ export default class PageMachine {
|
|
|
564
350
|
return this.#startPath;
|
|
565
351
|
}
|
|
566
352
|
|
|
567
|
-
/**
|
|
568
|
-
* Get the start state
|
|
569
|
-
*
|
|
570
|
-
* @returns {string} Start state name
|
|
571
|
-
*/
|
|
572
|
-
get startState() {
|
|
573
|
-
return this.#startState;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
353
|
/**
|
|
577
354
|
* Check if the supplied path matches the start path
|
|
578
355
|
*
|
|
@@ -592,19 +369,19 @@ export default class PageMachine {
|
|
|
592
369
|
}
|
|
593
370
|
|
|
594
371
|
/**
|
|
595
|
-
* Check if currently on the start
|
|
372
|
+
* Check if currently on the start path
|
|
596
373
|
*
|
|
597
|
-
* @returns {boolean} True if current
|
|
374
|
+
* @returns {boolean} True if current route is the start path
|
|
598
375
|
*
|
|
599
376
|
* @example
|
|
600
377
|
* ```javascript
|
|
601
|
-
* if (machine.
|
|
378
|
+
* if (machine.isOnStartPath) {
|
|
602
379
|
* // Show onboarding
|
|
603
380
|
* }
|
|
604
381
|
* ```
|
|
605
382
|
*/
|
|
606
|
-
get
|
|
607
|
-
return this.#current === this.#
|
|
383
|
+
get isOnStartPath() {
|
|
384
|
+
return this.#current === this.#startPath;
|
|
608
385
|
}
|
|
609
386
|
|
|
610
387
|
/**
|
|
@@ -622,78 +399,4 @@ export default class PageMachine {
|
|
|
622
399
|
switchToPage(this.#startPath);
|
|
623
400
|
});
|
|
624
401
|
}
|
|
625
|
-
|
|
626
|
-
/* ===== Transition Control Methods ===== */
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* Abort current state's transitions
|
|
630
|
-
* Cancels animations/operations immediately (incomplete state)
|
|
631
|
-
*
|
|
632
|
-
* @example
|
|
633
|
-
* ```javascript
|
|
634
|
-
* // User clicks "Cancel" button
|
|
635
|
-
* machine.abortTransitions();
|
|
636
|
-
* ```
|
|
637
|
-
*/
|
|
638
|
-
abortTransitions() {
|
|
639
|
-
if (this.#currentOnEnterHandler?.abort) {
|
|
640
|
-
this.#currentOnEnterHandler.abort();
|
|
641
|
-
this.#currentOnEnterHandler = null;
|
|
642
|
-
this.#currentOnEnterDone = null;
|
|
643
|
-
this.#revision++;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Complete current state's transitions immediately
|
|
649
|
-
* Fast-forwards animations/operations to completion (complete state)
|
|
650
|
-
*
|
|
651
|
-
* @example
|
|
652
|
-
* ```javascript
|
|
653
|
-
* // User clicks "Skip" or "Next" button
|
|
654
|
-
* machine.completeTransitions();
|
|
655
|
-
* ```
|
|
656
|
-
*/
|
|
657
|
-
completeTransitions() {
|
|
658
|
-
if (this.#currentOnEnterHandler?.complete) {
|
|
659
|
-
this.#currentOnEnterHandler.complete();
|
|
660
|
-
this.#currentOnEnterHandler = null;
|
|
661
|
-
this.#currentOnEnterDone = null;
|
|
662
|
-
this.#revision++;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
/**
|
|
667
|
-
* Check if current state has transitions that can be completed
|
|
668
|
-
*
|
|
669
|
-
* @returns {boolean} True if completeTransitions() can be called
|
|
670
|
-
*
|
|
671
|
-
* @example
|
|
672
|
-
* ```svelte
|
|
673
|
-
* {#if machine.canCompleteTransitions}
|
|
674
|
-
* <button onclick={() => machine.completeTransitions()}>Skip</button>
|
|
675
|
-
* {/if}
|
|
676
|
-
* ```
|
|
677
|
-
*/
|
|
678
|
-
get canCompleteTransitions() {
|
|
679
|
-
this.#revision; // Ensure reactivity
|
|
680
|
-
return !!this.#currentOnEnterHandler?.complete;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* Check if current state has transitions that can be aborted
|
|
685
|
-
*
|
|
686
|
-
* @returns {boolean} True if abortTransitions() can be called
|
|
687
|
-
*
|
|
688
|
-
* @example
|
|
689
|
-
* ```svelte
|
|
690
|
-
* {#if machine.canAbortTransitions}
|
|
691
|
-
* <button onclick={() => machine.abortTransitions()}>Cancel</button>
|
|
692
|
-
* {/if}
|
|
693
|
-
* ```
|
|
694
|
-
*/
|
|
695
|
-
get canAbortTransitions() {
|
|
696
|
-
this.#revision; // Ensure reactivity
|
|
697
|
-
return !!this.#currentOnEnterHandler?.abort;
|
|
698
|
-
}
|
|
699
402
|
}
|