@daltonr/pathwrite-svelte 0.5.0
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/LICENSE +22 -0
- package/README.md +468 -0
- package/dist/PathShell.svelte +181 -0
- package/dist/PathShell.svelte.d.ts +24 -0
- package/dist/PathShell.svelte.d.ts.map +1 -0
- package/dist/index.css +272 -0
- package/dist/index.svelte.d.ts +130 -0
- package/dist/index.svelte.d.ts.map +1 -0
- package/dist/index.svelte.js +141 -0
- package/package.json +62 -0
- package/src/PathShell.svelte +181 -0
- package/src/index.svelte.ts +254 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Richard Dalton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# @daltonr/pathwrite-svelte
|
|
2
|
+
|
|
3
|
+
Svelte 5 adapter for [@daltonr/pathwrite-core](../core) — reactive stores with lifecycle hooks, guards, and optional `<PathShell>` default UI.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @daltonr/pathwrite-svelte
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> **Requires Svelte 5.** This adapter uses runes (`$props`, `$derived`, `$state`) and snippets (`{#snippet}`, `{@render}`).
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Option 1: Use `usePath()` with custom UI
|
|
16
|
+
|
|
17
|
+
```svelte
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
import { onMount } from 'svelte';
|
|
20
|
+
import { usePath } from '@daltonr/pathwrite-svelte';
|
|
21
|
+
|
|
22
|
+
const { snapshot, start, next, previous, setData } = usePath();
|
|
23
|
+
|
|
24
|
+
const myPath = {
|
|
25
|
+
id: 'signup',
|
|
26
|
+
steps: [
|
|
27
|
+
{ id: 'details', title: 'Your Details' },
|
|
28
|
+
{ id: 'review', title: 'Review' }
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
onMount(() => {
|
|
33
|
+
start(myPath, { name: '', email: '' });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
let snap = $derived($snapshot);
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
{#if snap}
|
|
40
|
+
<h2>{snap.steps[snap.stepIndex].title}</h2>
|
|
41
|
+
|
|
42
|
+
{#if snap.stepId === 'details'}
|
|
43
|
+
<input
|
|
44
|
+
type="text"
|
|
45
|
+
value={snap.data.name || ''}
|
|
46
|
+
oninput={(e) => setData('name', e.currentTarget.value)}
|
|
47
|
+
placeholder="Name"
|
|
48
|
+
/>
|
|
49
|
+
<input
|
|
50
|
+
type="email"
|
|
51
|
+
value={snap.data.email || ''}
|
|
52
|
+
oninput={(e) => setData('email', e.currentTarget.value)}
|
|
53
|
+
placeholder="Email"
|
|
54
|
+
/>
|
|
55
|
+
{:else if snap.stepId === 'review'}
|
|
56
|
+
<p>Name: {snap.data.name}</p>
|
|
57
|
+
<p>Email: {snap.data.email}</p>
|
|
58
|
+
{/if}
|
|
59
|
+
|
|
60
|
+
<button onclick={previous} disabled={snap.isFirstStep || snap.isNavigating}>
|
|
61
|
+
Previous
|
|
62
|
+
</button>
|
|
63
|
+
<button onclick={next} disabled={!snap.canMoveNext || snap.isNavigating}>
|
|
64
|
+
{snap.isLastStep ? 'Complete' : 'Next'}
|
|
65
|
+
</button>
|
|
66
|
+
{/if}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Option 2: Use `<PathShell>` with snippets
|
|
70
|
+
|
|
71
|
+
```svelte
|
|
72
|
+
<script lang="ts">
|
|
73
|
+
import { PathShell } from '@daltonr/pathwrite-svelte';
|
|
74
|
+
import '@daltonr/pathwrite-svelte/styles.css';
|
|
75
|
+
|
|
76
|
+
import DetailsForm from './DetailsForm.svelte';
|
|
77
|
+
import ReviewPanel from './ReviewPanel.svelte';
|
|
78
|
+
|
|
79
|
+
const signupPath = {
|
|
80
|
+
id: 'signup',
|
|
81
|
+
steps: [
|
|
82
|
+
{ id: 'details', title: 'Your Details' },
|
|
83
|
+
{ id: 'review', title: 'Review' }
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function handleComplete(data) {
|
|
88
|
+
console.log('Completed!', data);
|
|
89
|
+
}
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<PathShell
|
|
93
|
+
path={signupPath}
|
|
94
|
+
initialData={{ name: '', email: '' }}
|
|
95
|
+
oncomplete={handleComplete}
|
|
96
|
+
>
|
|
97
|
+
{#snippet details()}
|
|
98
|
+
<DetailsForm />
|
|
99
|
+
{/snippet}
|
|
100
|
+
{#snippet review()}
|
|
101
|
+
<ReviewPanel />
|
|
102
|
+
{/snippet}
|
|
103
|
+
</PathShell>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Each step is a **Svelte 5 snippet** whose name matches the step ID. PathShell collects them automatically and renders the active one.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Simple vs Persisted
|
|
111
|
+
|
|
112
|
+
PathShell supports two modes. Pick the one that fits your use case:
|
|
113
|
+
|
|
114
|
+
### Simple — PathShell manages the engine
|
|
115
|
+
|
|
116
|
+
Pass `path` and `initialData`. PathShell creates and starts the engine for you:
|
|
117
|
+
|
|
118
|
+
```svelte
|
|
119
|
+
<PathShell
|
|
120
|
+
path={signupPath}
|
|
121
|
+
initialData={{ name: '' }}
|
|
122
|
+
oncomplete={handleComplete}
|
|
123
|
+
>
|
|
124
|
+
{#snippet details()}
|
|
125
|
+
<DetailsForm />
|
|
126
|
+
{/snippet}
|
|
127
|
+
</PathShell>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Use this when:** you don't need persistence, restoration, or custom observers. Quick prototypes, simple forms, one-off wizards.
|
|
131
|
+
|
|
132
|
+
### Persisted — you create the engine
|
|
133
|
+
|
|
134
|
+
Create the engine yourself with `restoreOrStart()` and pass it via `engine`. PathShell subscribes to it but does not start or own it:
|
|
135
|
+
|
|
136
|
+
```svelte
|
|
137
|
+
<script>
|
|
138
|
+
import { HttpStore, restoreOrStart, httpPersistence } from '@daltonr/pathwrite-store-http';
|
|
139
|
+
|
|
140
|
+
let engine = $state(null);
|
|
141
|
+
|
|
142
|
+
onMount(async () => {
|
|
143
|
+
const result = await restoreOrStart({
|
|
144
|
+
store: new HttpStore({ baseUrl: '/api/wizard' }),
|
|
145
|
+
key: 'user:onboarding',
|
|
146
|
+
path: signupPath,
|
|
147
|
+
initialData: { name: '' },
|
|
148
|
+
observers: [
|
|
149
|
+
httpPersistence({ store, key: 'user:onboarding', strategy: 'onNext' })
|
|
150
|
+
]
|
|
151
|
+
});
|
|
152
|
+
engine = result.engine;
|
|
153
|
+
});
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
{#if engine}
|
|
157
|
+
<PathShell {engine} oncomplete={handleComplete}>
|
|
158
|
+
{#snippet details()}
|
|
159
|
+
<DetailsForm />
|
|
160
|
+
{/snippet}
|
|
161
|
+
</PathShell>
|
|
162
|
+
{/if}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Use this when:** you need auto-persistence, restore-on-reload, or custom observers. Production apps, checkout flows, anything where losing progress matters.
|
|
166
|
+
|
|
167
|
+
> **Don't pass both.** `path` and `engine` are mutually exclusive. If you pass `engine`, PathShell will not call `start()` — it assumes the engine is already running.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## ⚠️ Step Components Must Use `getPathContext()`
|
|
172
|
+
|
|
173
|
+
Step components rendered inside `<PathShell>` access the path engine via `getPathContext()` — **not** Svelte's raw `getContext()`.
|
|
174
|
+
|
|
175
|
+
```svelte
|
|
176
|
+
<script lang="ts">
|
|
177
|
+
// ✅ Correct — always use getPathContext()
|
|
178
|
+
import { getPathContext } from '@daltonr/pathwrite-svelte';
|
|
179
|
+
const { snapshot, setData } = getPathContext();
|
|
180
|
+
|
|
181
|
+
// ❌ Wrong — this will silently return undefined
|
|
182
|
+
// import { getContext } from 'svelte';
|
|
183
|
+
// const { snapshot, setData } = getContext('pathContext');
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
{#if $snapshot}
|
|
187
|
+
<input
|
|
188
|
+
value={$snapshot.data.name || ''}
|
|
189
|
+
oninput={(e) => setData('name', e.currentTarget.value)}
|
|
190
|
+
/>
|
|
191
|
+
{/if}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`getPathContext()` uses a `Symbol` key internally and throws a clear error if called outside a `<PathShell>`. Using `getContext()` with a string key will fail silently.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## API
|
|
199
|
+
|
|
200
|
+
### `usePath<TData>(options?)`
|
|
201
|
+
|
|
202
|
+
Create a Pathwrite engine with Svelte store bindings. Returns reactive stores and action methods.
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
const {
|
|
206
|
+
snapshot, // Readable<PathSnapshot | null>
|
|
207
|
+
start, // (path, initialData?) => Promise<void>
|
|
208
|
+
next, // () => Promise<void>
|
|
209
|
+
previous, // () => Promise<void>
|
|
210
|
+
cancel, // () => Promise<void>
|
|
211
|
+
setData, // (key, value) => Promise<void>
|
|
212
|
+
// ... more actions
|
|
213
|
+
} = usePath();
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### Options
|
|
217
|
+
|
|
218
|
+
| Option | Type | Description |
|
|
219
|
+
|--------|------|-------------|
|
|
220
|
+
| `engine` | `PathEngine` | External engine (e.g., from `restoreOrStart()`) |
|
|
221
|
+
| `onEvent` | `(event) => void` | Called for every engine event |
|
|
222
|
+
|
|
223
|
+
#### Returns
|
|
224
|
+
|
|
225
|
+
| Property | Type | Description |
|
|
226
|
+
|----------|------|-------------|
|
|
227
|
+
| `snapshot` | `Readable<PathSnapshot \| null>` | Current path state (reactive store) |
|
|
228
|
+
| `start` | `function` | Start a path |
|
|
229
|
+
| `startSubPath` | `function` | Launch a sub-path |
|
|
230
|
+
| `next` | `function` | Go to next step |
|
|
231
|
+
| `previous` | `function` | Go to previous step |
|
|
232
|
+
| `cancel` | `function` | Cancel the path |
|
|
233
|
+
| `goToStep` | `function` | Jump to step by ID |
|
|
234
|
+
| `goToStepChecked` | `function` | Jump with guard checks |
|
|
235
|
+
| `setData` | `function` | Update data |
|
|
236
|
+
| `restart` | `function` | Restart the path |
|
|
237
|
+
|
|
238
|
+
### `<PathShell>`
|
|
239
|
+
|
|
240
|
+
Default UI shell with progress indicator and navigation buttons.
|
|
241
|
+
|
|
242
|
+
#### Props
|
|
243
|
+
|
|
244
|
+
| Prop | Type | Default | Description |
|
|
245
|
+
|------|------|---------|-------------|
|
|
246
|
+
| `path` | `PathDefinition` | — | Path definition (for self-managed engine) |
|
|
247
|
+
| `engine` | `PathEngine` | — | External engine (for persistence — see below) |
|
|
248
|
+
| `initialData` | `PathData` | `{}` | Initial data |
|
|
249
|
+
| `autoStart` | `boolean` | `true` | Auto-start on mount |
|
|
250
|
+
| `backLabel` | `string` | `"Previous"` | Previous button label |
|
|
251
|
+
| `nextLabel` | `string` | `"Next"` | Next button label |
|
|
252
|
+
| `completeLabel` | `string` | `"Complete"` | Complete button label |
|
|
253
|
+
| `cancelLabel` | `string` | `"Cancel"` | Cancel button label |
|
|
254
|
+
| `hideCancel` | `boolean` | `false` | Hide cancel button |
|
|
255
|
+
| `hideProgress` | `boolean` | `false` | Hide progress indicator |
|
|
256
|
+
|
|
257
|
+
> **`path` vs `engine`:** Pass `path` for simple wizards where PathShell manages the engine. Pass `engine` when you create the engine yourself (e.g., via `restoreOrStart()` for persistence). These are mutually exclusive — don't pass both.
|
|
258
|
+
|
|
259
|
+
#### Callbacks
|
|
260
|
+
|
|
261
|
+
| Callback | Type | Description |
|
|
262
|
+
|----------|------|-------------|
|
|
263
|
+
| `oncomplete` | `(data) => void` | Called when path completes |
|
|
264
|
+
| `oncancel` | `(data) => void` | Called when path is cancelled |
|
|
265
|
+
| `onevent` | `(event) => void` | Called for every event |
|
|
266
|
+
|
|
267
|
+
#### Snippets
|
|
268
|
+
|
|
269
|
+
Step content is provided as Svelte 5 snippets. The snippet name must match the step ID:
|
|
270
|
+
|
|
271
|
+
```svelte
|
|
272
|
+
<PathShell path={myPath}>
|
|
273
|
+
{#snippet details()}
|
|
274
|
+
<DetailsStep />
|
|
275
|
+
{/snippet}
|
|
276
|
+
{#snippet review()}
|
|
277
|
+
<ReviewStep />
|
|
278
|
+
{/snippet}
|
|
279
|
+
</PathShell>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
You can also override the header and footer:
|
|
283
|
+
|
|
284
|
+
```svelte
|
|
285
|
+
<PathShell path={myPath}>
|
|
286
|
+
{#snippet header(snap)}
|
|
287
|
+
<h2>Step {snap.stepIndex + 1} of {snap.stepCount}</h2>
|
|
288
|
+
{/snippet}
|
|
289
|
+
|
|
290
|
+
{#snippet details()}
|
|
291
|
+
<DetailsStep />
|
|
292
|
+
{/snippet}
|
|
293
|
+
|
|
294
|
+
{#snippet footer(snap, actions)}
|
|
295
|
+
<button onclick={actions.previous} disabled={snap.isFirstStep}>
|
|
296
|
+
← Back
|
|
297
|
+
</button>
|
|
298
|
+
<button onclick={actions.next} disabled={!snap.canMoveNext}>
|
|
299
|
+
{snap.isLastStep ? 'Finish' : 'Continue →'}
|
|
300
|
+
</button>
|
|
301
|
+
{/snippet}
|
|
302
|
+
</PathShell>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### `getPathContext<TData>()`
|
|
306
|
+
|
|
307
|
+
Get the path context from a parent `<PathShell>`. Use this inside step components.
|
|
308
|
+
|
|
309
|
+
```svelte
|
|
310
|
+
<script lang="ts">
|
|
311
|
+
import { getPathContext } from '@daltonr/pathwrite-svelte';
|
|
312
|
+
|
|
313
|
+
const { snapshot, next, setData } = getPathContext();
|
|
314
|
+
</script>
|
|
315
|
+
|
|
316
|
+
{#if $snapshot}
|
|
317
|
+
<input
|
|
318
|
+
value={$snapshot.data.name || ''}
|
|
319
|
+
oninput={(e) => setData('name', e.currentTarget.value)}
|
|
320
|
+
/>
|
|
321
|
+
<button onclick={next}>Next</button>
|
|
322
|
+
{/if}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### `bindData(snapshot, setData, key)`
|
|
326
|
+
|
|
327
|
+
Helper to create a two-way binding store.
|
|
328
|
+
|
|
329
|
+
```svelte
|
|
330
|
+
<script lang="ts">
|
|
331
|
+
import { usePath, bindData } from '@daltonr/pathwrite-svelte';
|
|
332
|
+
|
|
333
|
+
const { snapshot, setData } = usePath();
|
|
334
|
+
const name = bindData(snapshot, setData, 'name');
|
|
335
|
+
</script>
|
|
336
|
+
|
|
337
|
+
<input bind:value={$name} />
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## PathSnapshot
|
|
341
|
+
|
|
342
|
+
The `snapshot` store contains the current path state:
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
interface PathSnapshot {
|
|
346
|
+
pathId: string;
|
|
347
|
+
stepId: string;
|
|
348
|
+
stepIndex: number;
|
|
349
|
+
stepCount: number;
|
|
350
|
+
data: PathData;
|
|
351
|
+
nestingLevel: number;
|
|
352
|
+
isFirstStep: boolean;
|
|
353
|
+
isLastStep: boolean;
|
|
354
|
+
canMoveNext: boolean;
|
|
355
|
+
canMovePrevious: boolean;
|
|
356
|
+
isNavigating: boolean;
|
|
357
|
+
progress: number; // 0-1
|
|
358
|
+
steps: Array<{ id: string; title?: string; status: 'completed' | 'current' | 'upcoming' }>;
|
|
359
|
+
validationMessages: string[];
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Access it via the store: `$snapshot`
|
|
364
|
+
|
|
365
|
+
## Guards and Hooks
|
|
366
|
+
|
|
367
|
+
Define validation and lifecycle logic in your path definition:
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
const myPath = {
|
|
371
|
+
id: 'signup',
|
|
372
|
+
steps: [
|
|
373
|
+
{
|
|
374
|
+
id: 'details',
|
|
375
|
+
canMoveNext: (ctx) => ctx.data.name && ctx.data.email,
|
|
376
|
+
validationMessages: (ctx) => {
|
|
377
|
+
const errors = [];
|
|
378
|
+
if (!ctx.data.name) errors.push('Name is required');
|
|
379
|
+
if (!ctx.data.email) errors.push('Email is required');
|
|
380
|
+
return errors;
|
|
381
|
+
},
|
|
382
|
+
onEnter: (ctx) => {
|
|
383
|
+
console.log('Entered details step');
|
|
384
|
+
},
|
|
385
|
+
onLeave: (ctx) => {
|
|
386
|
+
console.log('Leaving details step');
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
{ id: 'review' }
|
|
390
|
+
]
|
|
391
|
+
};
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Persistence
|
|
395
|
+
|
|
396
|
+
Use with [@daltonr/pathwrite-store-http](../store-http) for automatic state persistence:
|
|
397
|
+
|
|
398
|
+
```svelte
|
|
399
|
+
<script lang="ts">
|
|
400
|
+
import { onMount } from 'svelte';
|
|
401
|
+
import { PathShell } from '@daltonr/pathwrite-svelte';
|
|
402
|
+
import { HttpStore, restoreOrStart, httpPersistence } from '@daltonr/pathwrite-store-http';
|
|
403
|
+
import DetailsForm from './DetailsForm.svelte';
|
|
404
|
+
import ReviewPanel from './ReviewPanel.svelte';
|
|
405
|
+
|
|
406
|
+
const store = new HttpStore({ baseUrl: '/api/wizard' });
|
|
407
|
+
const key = 'user:123:signup';
|
|
408
|
+
|
|
409
|
+
let engine = $state(null);
|
|
410
|
+
let restored = $state(false);
|
|
411
|
+
|
|
412
|
+
onMount(async () => {
|
|
413
|
+
const result = await restoreOrStart({
|
|
414
|
+
store,
|
|
415
|
+
key,
|
|
416
|
+
path: signupPath,
|
|
417
|
+
initialData: { name: '', email: '' },
|
|
418
|
+
observers: [
|
|
419
|
+
httpPersistence({ store, key, strategy: 'onNext' })
|
|
420
|
+
]
|
|
421
|
+
});
|
|
422
|
+
engine = result.engine;
|
|
423
|
+
restored = result.restored;
|
|
424
|
+
});
|
|
425
|
+
</script>
|
|
426
|
+
|
|
427
|
+
{#if engine}
|
|
428
|
+
<PathShell {engine} oncomplete={(data) => console.log('Done!', data)}>
|
|
429
|
+
{#snippet details()}
|
|
430
|
+
<DetailsForm />
|
|
431
|
+
{/snippet}
|
|
432
|
+
{#snippet review()}
|
|
433
|
+
<ReviewPanel />
|
|
434
|
+
{/snippet}
|
|
435
|
+
</PathShell>
|
|
436
|
+
{/if}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## TypeScript
|
|
440
|
+
|
|
441
|
+
Type your path data for full type safety:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
interface SignupData {
|
|
445
|
+
name: string;
|
|
446
|
+
email: string;
|
|
447
|
+
age: number;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const { snapshot, setData } = usePath<SignupData>();
|
|
451
|
+
|
|
452
|
+
// ✅ Type-checked
|
|
453
|
+
setData('name', 'John');
|
|
454
|
+
|
|
455
|
+
// ❌ Type error
|
|
456
|
+
setData('invalid', 'value');
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## License
|
|
460
|
+
|
|
461
|
+
MIT
|
|
462
|
+
|
|
463
|
+
## See Also
|
|
464
|
+
|
|
465
|
+
- [@daltonr/pathwrite-core](../core) - Core engine
|
|
466
|
+
- [@daltonr/pathwrite-store-http](../store-http) - HTTP persistence
|
|
467
|
+
- [Documentation](../../docs/guides/DEVELOPER_GUIDE.md)
|
|
468
|
+
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { usePath, setPathContext } from './index.svelte.js';
|
|
4
|
+
import type { PathDefinition, PathData, PathEngine, PathSnapshot } from './index.svelte.js';
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
path?: PathDefinition<any>;
|
|
9
|
+
engine?: PathEngine;
|
|
10
|
+
initialData?: PathData;
|
|
11
|
+
autoStart?: boolean;
|
|
12
|
+
backLabel?: string;
|
|
13
|
+
nextLabel?: string;
|
|
14
|
+
completeLabel?: string;
|
|
15
|
+
cancelLabel?: string;
|
|
16
|
+
hideCancel?: boolean;
|
|
17
|
+
hideProgress?: boolean;
|
|
18
|
+
// Callback props replace event dispatching in Svelte 5
|
|
19
|
+
oncomplete?: (data: PathData) => void;
|
|
20
|
+
oncancel?: (data: PathData) => void;
|
|
21
|
+
onevent?: (event: any) => void;
|
|
22
|
+
// Optional override snippets for header and footer
|
|
23
|
+
header?: Snippet<[PathSnapshot<any>]>;
|
|
24
|
+
footer?: Snippet<[PathSnapshot<any>, object]>;
|
|
25
|
+
// All other props treated as step snippets keyed by step ID
|
|
26
|
+
[key: string]: Snippet | any;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
path,
|
|
31
|
+
engine: engineProp,
|
|
32
|
+
initialData = {},
|
|
33
|
+
autoStart = true,
|
|
34
|
+
backLabel = 'Previous',
|
|
35
|
+
nextLabel = 'Next',
|
|
36
|
+
completeLabel = 'Complete',
|
|
37
|
+
cancelLabel = 'Cancel',
|
|
38
|
+
hideCancel = false,
|
|
39
|
+
hideProgress = false,
|
|
40
|
+
oncomplete,
|
|
41
|
+
oncancel,
|
|
42
|
+
onevent,
|
|
43
|
+
header,
|
|
44
|
+
footer,
|
|
45
|
+
...stepSnippets
|
|
46
|
+
}: Props = $props();
|
|
47
|
+
|
|
48
|
+
// Initialize path engine
|
|
49
|
+
const pathReturn = usePath({
|
|
50
|
+
engine: engineProp,
|
|
51
|
+
onEvent: (event) => {
|
|
52
|
+
onevent?.(event);
|
|
53
|
+
if (event.type === 'completed') oncomplete?.(event.data);
|
|
54
|
+
if (event.type === 'cancelled') oncancel?.(event.data);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const { start, next, previous, cancel, goToStep, goToStepChecked, setData, restart } = pathReturn;
|
|
59
|
+
|
|
60
|
+
// Provide context for child step components
|
|
61
|
+
setPathContext({
|
|
62
|
+
get snapshot() { return pathReturn.snapshot; },
|
|
63
|
+
next,
|
|
64
|
+
previous,
|
|
65
|
+
cancel,
|
|
66
|
+
goToStep,
|
|
67
|
+
goToStepChecked,
|
|
68
|
+
setData,
|
|
69
|
+
restart: () => restart(path, initialData)
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Auto-start the path when no external engine is provided
|
|
73
|
+
let started = false;
|
|
74
|
+
onMount(() => {
|
|
75
|
+
if (autoStart && !started && !engineProp) {
|
|
76
|
+
started = true;
|
|
77
|
+
start(path, initialData);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
let snap = $derived(pathReturn.snapshot);
|
|
82
|
+
let actions = $derived({ next, previous, cancel, goToStep, goToStepChecked, setData, restart: () => restart(path, initialData) });
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<div class="pw-shell">
|
|
86
|
+
{#if !snap}
|
|
87
|
+
<div class="pw-shell__empty">
|
|
88
|
+
<p>No active path.</p>
|
|
89
|
+
{#if !autoStart}
|
|
90
|
+
<button type="button" class="pw-shell__start-btn" onclick={() => start(path, initialData)}>
|
|
91
|
+
Start
|
|
92
|
+
</button>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
{:else}
|
|
96
|
+
<!-- Header: progress indicator (overridable via header snippet) -->
|
|
97
|
+
{#if !hideProgress}
|
|
98
|
+
{#if header}
|
|
99
|
+
{@render header(snap)}
|
|
100
|
+
{:else}
|
|
101
|
+
<div class="pw-shell__header">
|
|
102
|
+
<div class="pw-shell__steps">
|
|
103
|
+
{#each snap.steps as step, i}
|
|
104
|
+
<div class="pw-shell__step pw-shell__step--{step.status}">
|
|
105
|
+
<span class="pw-shell__step-dot">
|
|
106
|
+
{step.status === 'completed' ? '✓' : i + 1}
|
|
107
|
+
</span>
|
|
108
|
+
<span class="pw-shell__step-label">{step.title ?? step.id}</span>
|
|
109
|
+
</div>
|
|
110
|
+
{/each}
|
|
111
|
+
</div>
|
|
112
|
+
<div class="pw-shell__track">
|
|
113
|
+
<div class="pw-shell__track-fill" style="width: {snap.progress * 100}%"></div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
{/if}
|
|
117
|
+
{/if}
|
|
118
|
+
|
|
119
|
+
<!-- Body: current step rendered via named snippet -->
|
|
120
|
+
<div class="pw-shell__body">
|
|
121
|
+
{#if stepSnippets[snap.stepId]}
|
|
122
|
+
{@render stepSnippets[snap.stepId]()}
|
|
123
|
+
{:else}
|
|
124
|
+
<p>No content for step "{snap.stepId}"</p>
|
|
125
|
+
{/if}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Validation messages -->
|
|
129
|
+
{#if snap.validationMessages.length > 0}
|
|
130
|
+
<ul class="pw-shell__validation">
|
|
131
|
+
{#each snap.validationMessages as msg}
|
|
132
|
+
<li class="pw-shell__validation-item">{msg}</li>
|
|
133
|
+
{/each}
|
|
134
|
+
</ul>
|
|
135
|
+
{/if}
|
|
136
|
+
|
|
137
|
+
<!-- Footer: navigation buttons (overridable via footer snippet) -->
|
|
138
|
+
{#if footer}
|
|
139
|
+
{@render footer(snap, actions)}
|
|
140
|
+
{:else}
|
|
141
|
+
<div class="pw-shell__footer">
|
|
142
|
+
<div class="pw-shell__footer-left">
|
|
143
|
+
{#if !snap.isFirstStep}
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
class="pw-shell__btn pw-shell__btn--back"
|
|
147
|
+
disabled={snap.isNavigating || !snap.canMovePrevious}
|
|
148
|
+
onclick={previous}
|
|
149
|
+
>
|
|
150
|
+
{backLabel}
|
|
151
|
+
</button>
|
|
152
|
+
{/if}
|
|
153
|
+
</div>
|
|
154
|
+
<div class="pw-shell__footer-right">
|
|
155
|
+
{#if !hideCancel}
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
class="pw-shell__btn pw-shell__btn--cancel"
|
|
159
|
+
disabled={snap.isNavigating}
|
|
160
|
+
onclick={cancel}
|
|
161
|
+
>
|
|
162
|
+
{cancelLabel}
|
|
163
|
+
</button>
|
|
164
|
+
{/if}
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
class="pw-shell__btn pw-shell__btn--next"
|
|
168
|
+
disabled={snap.isNavigating || !snap.canMoveNext}
|
|
169
|
+
onclick={next}
|
|
170
|
+
>
|
|
171
|
+
{snap.isLastStep ? completeLabel : nextLabel}
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
{/if}
|
|
176
|
+
{/if}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<style>
|
|
180
|
+
/* Component-level styles inherited from shell.css */
|
|
181
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PathDefinition, PathData, PathEngine, PathSnapshot } from './index.svelte.js';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
interface Props {
|
|
4
|
+
path?: PathDefinition<any>;
|
|
5
|
+
engine?: PathEngine;
|
|
6
|
+
initialData?: PathData;
|
|
7
|
+
autoStart?: boolean;
|
|
8
|
+
backLabel?: string;
|
|
9
|
+
nextLabel?: string;
|
|
10
|
+
completeLabel?: string;
|
|
11
|
+
cancelLabel?: string;
|
|
12
|
+
hideCancel?: boolean;
|
|
13
|
+
hideProgress?: boolean;
|
|
14
|
+
oncomplete?: (data: PathData) => void;
|
|
15
|
+
oncancel?: (data: PathData) => void;
|
|
16
|
+
onevent?: (event: any) => void;
|
|
17
|
+
header?: Snippet<[PathSnapshot<any>]>;
|
|
18
|
+
footer?: Snippet<[PathSnapshot<any>, object]>;
|
|
19
|
+
[key: string]: Snippet | any;
|
|
20
|
+
}
|
|
21
|
+
declare const PathShell: import("svelte").Component<Props, {}, "">;
|
|
22
|
+
type PathShell = ReturnType<typeof PathShell>;
|
|
23
|
+
export default PathShell;
|
|
24
|
+
//# sourceMappingURL=PathShell.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PathShell.svelte.d.ts","sourceRoot":"","sources":["../src/PathShell.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGpC,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC;IAC3B,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IAE/B,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAE9C,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,GAAG,CAAC;CAC9B;AAmJH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|