@hkdigital/lib-core 0.5.43 → 0.5.45
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 +226 -0
- package/dist/state/context/RouteStateContext.svelte.d.ts +44 -0
- package/dist/state/context/RouteStateContext.svelte.js +119 -0
- package/dist/state/context.d.ts +2 -1
- package/dist/state/context.js +2 -1
- package/dist/state/machines/page-machine/PageMachine.svelte.d.ts +195 -0
- package/dist/state/machines/page-machine/PageMachine.svelte.js +337 -0
- package/dist/state/machines/page-machine/README.md +157 -0
- package/dist/state/machines.d.ts +1 -0
- package/dist/state/machines.js +2 -0
- package/dist/ui/primitives/inputs/text-input/TextInput.svelte.d.ts +1 -1
- package/dist/valibot/parsers.d.ts +1 -0
- package/dist/valibot/parsers.js +2 -3
- package/package.json +1 -1
- /package/dist/state/context/{state-context.d.ts → util.d.ts} +0 -0
- /package/dist/state/context/{state-context.js → util.js} +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# RouteStateContext
|
|
2
|
+
|
|
3
|
+
Base class for route-level state containers.
|
|
4
|
+
|
|
5
|
+
## How it connects
|
|
6
|
+
|
|
7
|
+
```
|
|
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()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Main Purposes
|
|
36
|
+
|
|
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
|
|
41
|
+
|
|
42
|
+
## Key Features
|
|
43
|
+
|
|
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)
|
|
47
|
+
|
|
48
|
+
## Basic Usage
|
|
49
|
+
|
|
50
|
+
### 1. Create state container class
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
// routes/puzzle/puzzle.state.svelte.js
|
|
54
|
+
import { defineStateContext } from '@hkdigital/lib-core/state/context.js';
|
|
55
|
+
import { RouteStateContext } from '$lib/state/context.js';
|
|
56
|
+
import PuzzlePageMachine from './puzzle.machine.svelte.js';
|
|
57
|
+
|
|
58
|
+
export class PuzzleState extends RouteStateContext {
|
|
59
|
+
#pageMachine;
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
super({
|
|
63
|
+
startPath: '/puzzle',
|
|
64
|
+
enforceStartPath: true // Optional: enforce route protection
|
|
65
|
+
});
|
|
66
|
+
this.#pageMachine = new PuzzlePageMachine();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get pageMachine() {
|
|
70
|
+
return this.#pageMachine;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Optional: Service accessors
|
|
74
|
+
getPuzzleService() {
|
|
75
|
+
return getPuzzleService();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Optional: Lifecycle methods
|
|
79
|
+
preload(onProgress) {
|
|
80
|
+
return loadPuzzleAssets(onProgress);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
reset() {
|
|
84
|
+
// Reset state when needed
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Export helper functions
|
|
89
|
+
export const [createOrGetPuzzleState, createPuzzleState, getPuzzleState] =
|
|
90
|
+
defineStateContext(PuzzleState);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 2. Provide context in layout
|
|
94
|
+
|
|
95
|
+
```svelte
|
|
96
|
+
<!-- routes/puzzle/+layout.svelte -->
|
|
97
|
+
<script>
|
|
98
|
+
import { page } from '$app/stores';
|
|
99
|
+
import { createOrGetPuzzleState } from './puzzle.state.svelte.js';
|
|
100
|
+
|
|
101
|
+
// Create or get existing state container
|
|
102
|
+
const puzzleState = createOrGetPuzzleState();
|
|
103
|
+
|
|
104
|
+
// IMPORTANT: Sync URL with PageMachine state
|
|
105
|
+
$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);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Optional: Preload assets
|
|
115
|
+
puzzleState.preload((progress) => {
|
|
116
|
+
console.log('Loading:', progress);
|
|
117
|
+
});
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<slot />
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 3. Consume context in pages
|
|
124
|
+
|
|
125
|
+
```svelte
|
|
126
|
+
<!-- routes/puzzle/level1/+page.svelte -->
|
|
127
|
+
<script>
|
|
128
|
+
import { getPuzzleState } from '../puzzle.state.svelte.js';
|
|
129
|
+
|
|
130
|
+
const puzzleState = getPuzzleState();
|
|
131
|
+
const pageMachine = puzzleState.pageMachine;
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<div>Current state: {pageMachine.current}</div>
|
|
135
|
+
```
|
|
136
|
+
|
|
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.
|
|
206
|
+
|
|
207
|
+
**Custom redirect URL:**
|
|
208
|
+
```javascript
|
|
209
|
+
// Redirect to a different URL instead of startPath
|
|
210
|
+
puzzleState.validateAndRedirect($page.url.pathname, '/puzzle/welcome');
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Separation of Concerns
|
|
214
|
+
|
|
215
|
+
**State Container** = Route-level concerns (MULTIPLE responsibilities)
|
|
216
|
+
- PageMachine instance
|
|
217
|
+
- Game engines
|
|
218
|
+
- Service accessors
|
|
219
|
+
- Media preloading
|
|
220
|
+
- Any other route-level concern
|
|
221
|
+
|
|
222
|
+
**PageMachine** = Page/view state ONLY (SINGLE responsibility)
|
|
223
|
+
- Current page state
|
|
224
|
+
- Route mapping
|
|
225
|
+
- Visited states
|
|
226
|
+
- Computed properties for state checks
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
}
|
package/dist/state/context.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { default as RouteStateContext } from "./context/RouteStateContext.svelte.js";
|
|
2
|
+
export { defineStateContext, DEFAULT_CONTEXT_KEY } from "./context/util.js";
|
package/dist/state/context.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { default as RouteStateContext } from './context/RouteStateContext.svelte.js';
|
|
2
|
+
export { defineStateContext, DEFAULT_CONTEXT_KEY } from './context/util.js';
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for page state machines with URL route mapping
|
|
3
|
+
*
|
|
4
|
+
* Simple state tracker that maps states to URL routes.
|
|
5
|
+
* Does NOT enforce FSM transitions - allows free navigation
|
|
6
|
+
* (because users can navigate to any URL via browser).
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - State-to-route mapping and sync
|
|
10
|
+
* - Data properties for business/domain state
|
|
11
|
+
* - Visited states tracking
|
|
12
|
+
*
|
|
13
|
+
* Basic usage:
|
|
14
|
+
* ```javascript
|
|
15
|
+
* const machine = cityState.getOrCreatePageMachine('intro', IntroPageMachine);
|
|
16
|
+
*
|
|
17
|
+
* // Sync machine state with URL changes
|
|
18
|
+
* $effect(() => {
|
|
19
|
+
* machine.syncFromPath($page.url.pathname);
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* With data properties (for business logic):
|
|
24
|
+
* ```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);
|
|
32
|
+
*
|
|
33
|
+
* // Read data
|
|
34
|
+
* if (machine.getData('HAS_STRONG_PROFILE')) {
|
|
35
|
+
* // Show advanced content
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* // Update data (triggers reactivity)
|
|
39
|
+
* machine.setData('HAS_STRONG_PROFILE', true);
|
|
40
|
+
*
|
|
41
|
+
* // Check visited states
|
|
42
|
+
* if (machine.hasVisited(STATE_PROFILE)) {
|
|
43
|
+
* // User has seen profile page before
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export default class PageMachine {
|
|
48
|
+
/**
|
|
49
|
+
* Constructor
|
|
50
|
+
*
|
|
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)
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```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);
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
constructor(initialState: string, routeMap?: Record<string, string>, initialData?: Record<string, any>);
|
|
71
|
+
/**
|
|
72
|
+
* Synchronize machine state with URL path
|
|
73
|
+
*
|
|
74
|
+
* Call this in a $effect that watches $page.url.pathname
|
|
75
|
+
* Automatically tracks visited states
|
|
76
|
+
*
|
|
77
|
+
* @param {string} currentPath - Current URL pathname
|
|
78
|
+
*
|
|
79
|
+
* @returns {boolean} True if state was changed
|
|
80
|
+
*/
|
|
81
|
+
syncFromPath(currentPath: string): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Set the current state directly
|
|
84
|
+
*
|
|
85
|
+
* @param {string} newState - Target state
|
|
86
|
+
*/
|
|
87
|
+
setState(newState: string): void;
|
|
88
|
+
/**
|
|
89
|
+
* Get route path for a given state
|
|
90
|
+
*
|
|
91
|
+
* @param {string} state - State name
|
|
92
|
+
*
|
|
93
|
+
* @returns {string|null} Route path or null if no mapping
|
|
94
|
+
*/
|
|
95
|
+
getPathForState(state: string): string | null;
|
|
96
|
+
/**
|
|
97
|
+
* Get route path for current state
|
|
98
|
+
*
|
|
99
|
+
* @returns {string|null} Route path or null if no mapping
|
|
100
|
+
*/
|
|
101
|
+
getCurrentPath(): string | null;
|
|
102
|
+
/**
|
|
103
|
+
* Get current state
|
|
104
|
+
*
|
|
105
|
+
* @returns {string} Current state name
|
|
106
|
+
*/
|
|
107
|
+
get current(): string;
|
|
108
|
+
/**
|
|
109
|
+
* Get the route map
|
|
110
|
+
*
|
|
111
|
+
* @returns {Record<string, string>} Copy of route map
|
|
112
|
+
*/
|
|
113
|
+
get routeMap(): Record<string, string>;
|
|
114
|
+
/**
|
|
115
|
+
* Set a data property value
|
|
116
|
+
*
|
|
117
|
+
* @param {string} key - Property key
|
|
118
|
+
* @param {any} value - Property value
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```javascript
|
|
122
|
+
* machine.setData('HAS_STRONG_PROFILE', true);
|
|
123
|
+
* machine.setData('PROFILE_SCORE', 85);
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
setData(key: string, value: any): void;
|
|
127
|
+
/**
|
|
128
|
+
* Get a data property value
|
|
129
|
+
*
|
|
130
|
+
* @param {string} key - Property key
|
|
131
|
+
*
|
|
132
|
+
* @returns {any} Property value or undefined
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```javascript
|
|
136
|
+
* const hasProfile = machine.getData('HAS_STRONG_PROFILE');
|
|
137
|
+
* const score = machine.getData('PROFILE_SCORE');
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
getData(key: string): any;
|
|
141
|
+
/**
|
|
142
|
+
* Get all data properties
|
|
143
|
+
*
|
|
144
|
+
* @returns {Record<string, any>} Copy of all data
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```javascript
|
|
148
|
+
* const allData = machine.getAllData();
|
|
149
|
+
* await playerService.saveData(allData);
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
getAllData(): Record<string, any>;
|
|
153
|
+
/**
|
|
154
|
+
* Update multiple data properties at once
|
|
155
|
+
*
|
|
156
|
+
* @param {Record<string, any>} dataUpdates - Object with key-value pairs
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```javascript
|
|
160
|
+
* machine.updateData({
|
|
161
|
+
* HAS_STRONG_PROFILE: true,
|
|
162
|
+
* PROFILE_SCORE: 85,
|
|
163
|
+
* MATCHED_SECTOR: 'technology'
|
|
164
|
+
* });
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
updateData(dataUpdates: Record<string, any>): void;
|
|
168
|
+
/**
|
|
169
|
+
* Check if a state has been visited
|
|
170
|
+
*
|
|
171
|
+
* @param {string} state - State name to check
|
|
172
|
+
*
|
|
173
|
+
* @returns {boolean} True if the state has been visited
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```javascript
|
|
177
|
+
* if (machine.hasVisited(STATE_PROFILE)) {
|
|
178
|
+
* // User has seen profile page, skip intro
|
|
179
|
+
* }
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
hasVisited(state: string): boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Get all visited states
|
|
185
|
+
*
|
|
186
|
+
* @returns {string[]} Array of visited state names
|
|
187
|
+
*/
|
|
188
|
+
getVisitedStates(): string[];
|
|
189
|
+
/**
|
|
190
|
+
* Reset visited states tracking
|
|
191
|
+
* Useful for testing or resetting experience
|
|
192
|
+
*/
|
|
193
|
+
resetVisitedStates(): void;
|
|
194
|
+
#private;
|
|
195
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for page state machines with URL route mapping
|
|
3
|
+
*
|
|
4
|
+
* Simple state tracker that maps states to URL routes.
|
|
5
|
+
* Does NOT enforce FSM transitions - allows free navigation
|
|
6
|
+
* (because users can navigate to any URL via browser).
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - State-to-route mapping and sync
|
|
10
|
+
* - Data properties for business/domain state
|
|
11
|
+
* - Visited states tracking
|
|
12
|
+
*
|
|
13
|
+
* Basic usage:
|
|
14
|
+
* ```javascript
|
|
15
|
+
* const machine = cityState.getOrCreatePageMachine('intro', IntroPageMachine);
|
|
16
|
+
*
|
|
17
|
+
* // Sync machine state with URL changes
|
|
18
|
+
* $effect(() => {
|
|
19
|
+
* machine.syncFromPath($page.url.pathname);
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* With data properties (for business logic):
|
|
24
|
+
* ```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);
|
|
32
|
+
*
|
|
33
|
+
* // Read data
|
|
34
|
+
* if (machine.getData('HAS_STRONG_PROFILE')) {
|
|
35
|
+
* // Show advanced content
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* // Update data (triggers reactivity)
|
|
39
|
+
* machine.setData('HAS_STRONG_PROFILE', true);
|
|
40
|
+
*
|
|
41
|
+
* // Check visited states
|
|
42
|
+
* if (machine.hasVisited(STATE_PROFILE)) {
|
|
43
|
+
* // User has seen profile page before
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export default class PageMachine {
|
|
48
|
+
/**
|
|
49
|
+
* Current state
|
|
50
|
+
* @type {string}
|
|
51
|
+
*/
|
|
52
|
+
// @ts-ignore
|
|
53
|
+
#current = $state();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Map of states to route paths
|
|
57
|
+
* @type {Record<string, string>}
|
|
58
|
+
*/
|
|
59
|
+
#routeMap = {};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reverse map of route paths to states
|
|
63
|
+
* @type {Record<string, string>}
|
|
64
|
+
*/
|
|
65
|
+
#pathToStateMap = {};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Data properties for business/domain state
|
|
69
|
+
* Can be initialized from server and synced back
|
|
70
|
+
* @type {Record<string, any>}
|
|
71
|
+
*/
|
|
72
|
+
#data = $state({});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Track which states have been visited during this session
|
|
76
|
+
* Useful for showing first-time hints/tips
|
|
77
|
+
* @type {Set<string>}
|
|
78
|
+
*/
|
|
79
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
80
|
+
#visitedStates = new Set();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Revision counter for triggering reactivity
|
|
84
|
+
* @type {number}
|
|
85
|
+
*/
|
|
86
|
+
#revision = $state(0);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Constructor
|
|
90
|
+
*
|
|
91
|
+
* @param {string} initialState - Initial state name
|
|
92
|
+
* @param {Record<string, string>} routeMap - Map of states to route paths
|
|
93
|
+
* @param {Record<string, any>} [initialData={}] - Initial data properties (from server)
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```javascript
|
|
97
|
+
* const routeMap = {
|
|
98
|
+
* [STATE_MATCH]: '/city/intro/match',
|
|
99
|
+
* [STATE_CIRCUIT]: '/city/intro/racecircuit'
|
|
100
|
+
* };
|
|
101
|
+
*
|
|
102
|
+
* const initialData = {
|
|
103
|
+
* INTRO_COMPLETED: false,
|
|
104
|
+
* PROFILE_SCORE: 0
|
|
105
|
+
* };
|
|
106
|
+
*
|
|
107
|
+
* const machine = new CityIntroPageMachine(STATE_START, routeMap, initialData);
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
constructor(initialState, routeMap = {}, initialData = {}) {
|
|
111
|
+
this.#current = initialState;
|
|
112
|
+
this.#routeMap = routeMap;
|
|
113
|
+
this.#data = initialData;
|
|
114
|
+
|
|
115
|
+
// Build reverse map (path -> state)
|
|
116
|
+
for (const [state, path] of Object.entries(routeMap)) {
|
|
117
|
+
this.#pathToStateMap[path] = state;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Mark initial state as visited
|
|
121
|
+
this.#visitedStates.add(initialState);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Synchronize machine state with URL path
|
|
126
|
+
*
|
|
127
|
+
* Call this in a $effect that watches $page.url.pathname
|
|
128
|
+
* Automatically tracks visited states
|
|
129
|
+
*
|
|
130
|
+
* @param {string} currentPath - Current URL pathname
|
|
131
|
+
*
|
|
132
|
+
* @returns {boolean} True if state was changed
|
|
133
|
+
*/
|
|
134
|
+
syncFromPath(currentPath) {
|
|
135
|
+
const targetState = this.#getStateFromPath(currentPath);
|
|
136
|
+
|
|
137
|
+
if (targetState && targetState !== this.#current) {
|
|
138
|
+
this.#current = targetState;
|
|
139
|
+
this.#visitedStates.add(targetState);
|
|
140
|
+
this.#revision++;
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Set the current state directly
|
|
149
|
+
*
|
|
150
|
+
* @param {string} newState - Target state
|
|
151
|
+
*/
|
|
152
|
+
setState(newState) {
|
|
153
|
+
if (newState !== this.#current) {
|
|
154
|
+
this.#current = newState;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get state name from URL path
|
|
160
|
+
*
|
|
161
|
+
* @param {string} path - URL pathname
|
|
162
|
+
*
|
|
163
|
+
* @returns {string|null} State name or null
|
|
164
|
+
*/
|
|
165
|
+
#getStateFromPath(path) {
|
|
166
|
+
// Try exact match first
|
|
167
|
+
if (this.#pathToStateMap[path]) {
|
|
168
|
+
return this.#pathToStateMap[path];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Try partial match (path includes route)
|
|
172
|
+
for (const [routePath, state] of Object.entries(this.#pathToStateMap)) {
|
|
173
|
+
if (path.includes(routePath)) {
|
|
174
|
+
return state;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get route path for a given state
|
|
183
|
+
*
|
|
184
|
+
* @param {string} state - State name
|
|
185
|
+
*
|
|
186
|
+
* @returns {string|null} Route path or null if no mapping
|
|
187
|
+
*/
|
|
188
|
+
getPathForState(state) {
|
|
189
|
+
return this.#routeMap[state] || null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get route path for current state
|
|
194
|
+
*
|
|
195
|
+
* @returns {string|null} Route path or null if no mapping
|
|
196
|
+
*/
|
|
197
|
+
getCurrentPath() {
|
|
198
|
+
return this.getPathForState(this.#current);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get current state
|
|
203
|
+
*
|
|
204
|
+
* @returns {string} Current state name
|
|
205
|
+
*/
|
|
206
|
+
get current() {
|
|
207
|
+
return this.#current;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get the route map
|
|
212
|
+
*
|
|
213
|
+
* @returns {Record<string, string>} Copy of route map
|
|
214
|
+
*/
|
|
215
|
+
get routeMap() {
|
|
216
|
+
return { ...this.#routeMap };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* ===== Data Properties (Business/Domain State) ===== */
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Set a data property value
|
|
223
|
+
*
|
|
224
|
+
* @param {string} key - Property key
|
|
225
|
+
* @param {any} value - Property value
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```javascript
|
|
229
|
+
* machine.setData('HAS_STRONG_PROFILE', true);
|
|
230
|
+
* machine.setData('PROFILE_SCORE', 85);
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
setData(key, value) {
|
|
234
|
+
this.#data[key] = value;
|
|
235
|
+
this.#revision++;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get a data property value
|
|
240
|
+
*
|
|
241
|
+
* @param {string} key - Property key
|
|
242
|
+
*
|
|
243
|
+
* @returns {any} Property value or undefined
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```javascript
|
|
247
|
+
* const hasProfile = machine.getData('HAS_STRONG_PROFILE');
|
|
248
|
+
* const score = machine.getData('PROFILE_SCORE');
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
getData(key) {
|
|
252
|
+
// Access revision to ensure reactivity
|
|
253
|
+
this.#revision;
|
|
254
|
+
return this.#data[key];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get all data properties
|
|
259
|
+
*
|
|
260
|
+
* @returns {Record<string, any>} Copy of all data
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```javascript
|
|
264
|
+
* const allData = machine.getAllData();
|
|
265
|
+
* await playerService.saveData(allData);
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
getAllData() {
|
|
269
|
+
// Access revision to ensure reactivity
|
|
270
|
+
this.#revision;
|
|
271
|
+
return { ...this.#data };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Update multiple data properties at once
|
|
276
|
+
*
|
|
277
|
+
* @param {Record<string, any>} dataUpdates - Object with key-value pairs
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```javascript
|
|
281
|
+
* machine.updateData({
|
|
282
|
+
* HAS_STRONG_PROFILE: true,
|
|
283
|
+
* PROFILE_SCORE: 85,
|
|
284
|
+
* MATCHED_SECTOR: 'technology'
|
|
285
|
+
* });
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
updateData(dataUpdates) {
|
|
289
|
+
for (const [key, value] of Object.entries(dataUpdates)) {
|
|
290
|
+
this.#data[key] = value;
|
|
291
|
+
}
|
|
292
|
+
this.#revision++;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* ===== Visited States Tracking ===== */
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Check if a state has been visited
|
|
299
|
+
*
|
|
300
|
+
* @param {string} state - State name to check
|
|
301
|
+
*
|
|
302
|
+
* @returns {boolean} True if the state has been visited
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* ```javascript
|
|
306
|
+
* if (machine.hasVisited(STATE_PROFILE)) {
|
|
307
|
+
* // User has seen profile page, skip intro
|
|
308
|
+
* }
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
hasVisited(state) {
|
|
312
|
+
// Access revision to ensure reactivity
|
|
313
|
+
this.#revision;
|
|
314
|
+
return this.#visitedStates.has(state);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get all visited states
|
|
319
|
+
*
|
|
320
|
+
* @returns {string[]} Array of visited state names
|
|
321
|
+
*/
|
|
322
|
+
getVisitedStates() {
|
|
323
|
+
// Access revision to ensure reactivity
|
|
324
|
+
this.#revision;
|
|
325
|
+
return Array.from(this.#visitedStates);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Reset visited states tracking
|
|
330
|
+
* Useful for testing or resetting experience
|
|
331
|
+
*/
|
|
332
|
+
resetVisitedStates() {
|
|
333
|
+
this.#visitedStates.clear();
|
|
334
|
+
this.#visitedStates.add(this.#current);
|
|
335
|
+
this.#revision++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# PageMachine
|
|
2
|
+
|
|
3
|
+
State machine for managing page view states with URL route mapping.
|
|
4
|
+
|
|
5
|
+
## How it connects
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────────────────┐
|
|
9
|
+
│ MyFlowPageMachine (extends PageMachine) │
|
|
10
|
+
│ - Maps states to URL routes │
|
|
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; } │
|
|
20
|
+
└────────────────┬────────────────────────────────────────┘
|
|
21
|
+
│
|
|
22
|
+
│ Context provided to layout
|
|
23
|
+
│
|
|
24
|
+
┌────────────────▼────────────────────────────────────────┐
|
|
25
|
+
│ +layout.svelte │
|
|
26
|
+
│ IMPORTANT: Must sync URL with state: │
|
|
27
|
+
│ $effect(() => { │
|
|
28
|
+
│ pageMachine.syncFromPath($page.url.pathname); │
|
|
29
|
+
│ }); │
|
|
30
|
+
└────────────────┬────────────────────────────────────────┘
|
|
31
|
+
│
|
|
32
|
+
│ Pages use machine state
|
|
33
|
+
│
|
|
34
|
+
┌────────┴─────────┬────────────────┐
|
|
35
|
+
▼ ▼ ▼
|
|
36
|
+
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
37
|
+
│ +page │ │ +page │ │ Component│
|
|
38
|
+
│ │ │ │ │ │
|
|
39
|
+
└──────────┘ └──────────┘ └──────────┘
|
|
40
|
+
Access via: pageMachine.current, pageMachine.inIntro, etc.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Main Purposes
|
|
44
|
+
|
|
45
|
+
1. **Track current view/step** - Which page is active
|
|
46
|
+
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
|
|
49
|
+
|
|
50
|
+
## Basic Usage
|
|
51
|
+
|
|
52
|
+
### 1. Create a page machine class
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
// my-flow.machine.svelte.js
|
|
56
|
+
import PageMachine from '$lib/state/machines/PageMachine.svelte.js';
|
|
57
|
+
|
|
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);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Computed properties for convenience
|
|
74
|
+
get inIntro() {
|
|
75
|
+
return this.current === STATE_INTRO;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get inStep1() {
|
|
79
|
+
return this.current === STATE_STEP1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2. Use in state container
|
|
85
|
+
|
|
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';
|
|
90
|
+
|
|
91
|
+
export class MyFlowState extends RouteStateContext {
|
|
92
|
+
#pageMachine;
|
|
93
|
+
|
|
94
|
+
constructor() {
|
|
95
|
+
super();
|
|
96
|
+
this.#pageMachine = new MyFlowPageMachine();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get pageMachine() {
|
|
100
|
+
return this.#pageMachine;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 3. Sync with route in +layout.svelte component
|
|
106
|
+
|
|
107
|
+
**This is IMPORTANT for url path to be connected to the page machine**
|
|
108
|
+
|
|
109
|
+
```svelte
|
|
110
|
+
<script>
|
|
111
|
+
import { page } from '$app/stores';
|
|
112
|
+
import { getMyFlowState } from '../my-flow.state.svelte.js';
|
|
113
|
+
|
|
114
|
+
const flowState = getMyFlowState();
|
|
115
|
+
const pageMachine = flowState.pageMachine;
|
|
116
|
+
|
|
117
|
+
// Sync machine with URL changes
|
|
118
|
+
$effect(() => {
|
|
119
|
+
pageMachine.syncFromPath($page.url.pathname);
|
|
120
|
+
});
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
{#if pageMachine.inIntro}
|
|
124
|
+
<IntroView />
|
|
125
|
+
{:else if pageMachine.inStep1}
|
|
126
|
+
<Step1View />
|
|
127
|
+
{/if}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Key Methods
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
// Sync with URL path
|
|
134
|
+
machine.syncFromPath(currentPath)
|
|
135
|
+
|
|
136
|
+
// Get current state
|
|
137
|
+
machine.current
|
|
138
|
+
|
|
139
|
+
// Get route for state
|
|
140
|
+
machine.getPathForState(stateName)
|
|
141
|
+
|
|
142
|
+
// Data properties (for business logic)
|
|
143
|
+
machine.setData('KEY', value)
|
|
144
|
+
machine.getData('KEY')
|
|
145
|
+
|
|
146
|
+
// Visited states tracking
|
|
147
|
+
machine.hasVisited(stateName)
|
|
148
|
+
machine.getVisitedStates()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Important Notes
|
|
152
|
+
|
|
153
|
+
- Not a finite state machine - allows free navigation
|
|
154
|
+
- States map 1:1 with routes
|
|
155
|
+
- Use state constants instead of magic strings
|
|
156
|
+
- Always sync in `$effect` watching `$page.url.pathname`
|
|
157
|
+
- Data properties are for business logic, not UI state
|
package/dist/state/machines.d.ts
CHANGED
package/dist/state/machines.js
CHANGED
|
@@ -12,7 +12,7 @@ type TextInput = {
|
|
|
12
12
|
iconClasses?: string | undefined;
|
|
13
13
|
initialValue?: string | undefined;
|
|
14
14
|
value?: string | undefined;
|
|
15
|
-
type?: "number" | "
|
|
15
|
+
type?: "number" | "email" | "url" | "text" | undefined;
|
|
16
16
|
pattern?: string | undefined;
|
|
17
17
|
required?: boolean | undefined;
|
|
18
18
|
title?: string | undefined;
|
package/dist/valibot/parsers.js
CHANGED
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|