@daltonr/pathwrite-svelte 0.10.0 → 0.11.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/README.md +81 -470
- package/dist/PathShell.svelte +31 -6
- package/dist/PathShell.svelte.d.ts +4 -0
- package/dist/PathShell.svelte.d.ts.map +1 -1
- package/dist/index.svelte.d.ts +9 -0
- package/dist/index.svelte.d.ts.map +1 -1
- package/dist/index.svelte.js +12 -1
- package/package.json +2 -2
- package/src/PathShell.svelte +31 -6
- package/src/index.svelte.ts +16 -1
package/README.md
CHANGED
|
@@ -1,547 +1,158 @@
|
|
|
1
1
|
# @daltonr/pathwrite-svelte
|
|
2
2
|
|
|
3
|
-
Svelte 5 adapter for
|
|
3
|
+
Svelte 5 adapter for `@daltonr/pathwrite-core` — runes-based reactive state with an optional `<PathShell>` UI component.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @daltonr/pathwrite-svelte
|
|
8
|
+
npm install @daltonr/pathwrite-core @daltonr/pathwrite-svelte
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Peer dependencies: Svelte 5+.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
> Uses Svelte 5 runes (`$state`, `$derived`, `$props`) and snippets (`{#snippet}`, `{@render}`). Not compatible with Svelte 4.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## Quick start
|
|
16
16
|
|
|
17
17
|
```svelte
|
|
18
|
+
<!-- JobApplicationFlow.svelte -->
|
|
18
19
|
<script lang="ts">
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
```
|
|
20
|
+
import { PathShell } from "@daltonr/pathwrite-svelte";
|
|
21
|
+
import "@daltonr/pathwrite-svelte/styles.css";
|
|
22
|
+
import { applicationPath } from "./application-path";
|
|
23
|
+
import DetailsStep from "./DetailsStep.svelte";
|
|
24
|
+
import CoverNoteStep from "./CoverNoteStep.svelte";
|
|
68
25
|
|
|
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
26
|
function handleComplete(data) {
|
|
88
|
-
console.log(
|
|
27
|
+
console.log("Submitted:", data);
|
|
89
28
|
}
|
|
90
29
|
</script>
|
|
91
30
|
|
|
92
31
|
<PathShell
|
|
93
|
-
path={
|
|
94
|
-
initialData={{ name:
|
|
32
|
+
path={applicationPath}
|
|
33
|
+
initialData={{ name: "", email: "", coverNote: "" }}
|
|
95
34
|
oncomplete={handleComplete}
|
|
96
35
|
>
|
|
97
36
|
{#snippet details()}
|
|
98
|
-
<
|
|
99
|
-
{/snippet}
|
|
100
|
-
{#snippet review()}
|
|
101
|
-
<ReviewPanel />
|
|
37
|
+
<DetailsStep />
|
|
102
38
|
{/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
|
-
> **⚠️ Important: Snippet Names Must Match Step IDs**
|
|
109
|
-
>
|
|
110
|
-
> When passing step content to `<PathShell>`, each snippet's name **must exactly match** the corresponding step's `id`:
|
|
111
|
-
>
|
|
112
|
-
> ```typescript
|
|
113
|
-
> const myPath = {
|
|
114
|
-
> id: 'signup',
|
|
115
|
-
> steps: [
|
|
116
|
-
> { id: 'details' }, // ← Step ID
|
|
117
|
-
> { id: 'review' } // ← Step ID
|
|
118
|
-
> ]
|
|
119
|
-
> };
|
|
120
|
-
> ```
|
|
121
|
-
>
|
|
122
|
-
> ```svelte
|
|
123
|
-
> <PathShell path={myPath}>
|
|
124
|
-
> {#snippet details()} <!-- ✅ Matches "details" step -->
|
|
125
|
-
> <DetailsForm />
|
|
126
|
-
> {/snippet}
|
|
127
|
-
> {#snippet review()} <!-- ✅ Matches "review" step -->
|
|
128
|
-
> <ReviewPanel />
|
|
129
|
-
> {/snippet}
|
|
130
|
-
> {#snippet foo()} <!-- ❌ No step with id "foo" -->
|
|
131
|
-
> <FooPanel />
|
|
132
|
-
> {/snippet}
|
|
133
|
-
> </PathShell>
|
|
134
|
-
> ```
|
|
135
|
-
>
|
|
136
|
-
> If a snippet name doesn't match any step ID, PathShell will render:
|
|
137
|
-
> **`No content for step "foo"`**
|
|
138
|
-
>
|
|
139
|
-
> **💡 Tip:** Use your IDE's "Go to Definition" on the step ID in your path definition, then copy-paste the exact string when creating the snippet. This ensures perfect matching and avoids typos.
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
## Simple vs Persisted
|
|
144
|
-
|
|
145
|
-
PathShell supports two modes. Pick the one that fits your use case:
|
|
146
|
-
|
|
147
|
-
### Simple — PathShell manages the engine
|
|
148
|
-
|
|
149
|
-
Pass `path` and `initialData`. PathShell creates and starts the engine for you:
|
|
150
39
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
initialData={{ name: '' }}
|
|
155
|
-
oncomplete={handleComplete}
|
|
156
|
-
>
|
|
157
|
-
{#snippet details()}
|
|
158
|
-
<DetailsForm />
|
|
40
|
+
<!-- Step ID is "cover-note"; PathShell resolves the camelCase snippet automatically -->
|
|
41
|
+
{#snippet coverNote()}
|
|
42
|
+
<CoverNoteStep />
|
|
159
43
|
{/snippet}
|
|
160
44
|
</PathShell>
|
|
161
45
|
```
|
|
162
46
|
|
|
163
|
-
**Use this when:** you don't need persistence, restoration, or custom observers. Quick prototypes, simple forms, one-off wizards.
|
|
164
|
-
|
|
165
|
-
### Persisted — you create the engine
|
|
166
|
-
|
|
167
|
-
Create the engine yourself with `restoreOrStart()` and pass it via `engine`. PathShell subscribes to it but does not start or own it:
|
|
168
|
-
|
|
169
|
-
```svelte
|
|
170
|
-
<script>
|
|
171
|
-
import { HttpStore, restoreOrStart, persistence } from '@daltonr/pathwrite-store';
|
|
172
|
-
|
|
173
|
-
let engine = $state(null);
|
|
174
|
-
|
|
175
|
-
onMount(async () => {
|
|
176
|
-
const result = await restoreOrStart({
|
|
177
|
-
store: new HttpStore({ baseUrl: '/api/wizard' }),
|
|
178
|
-
key: 'user:onboarding',
|
|
179
|
-
path: signupPath,
|
|
180
|
-
initialData: { name: '' },
|
|
181
|
-
observers: [
|
|
182
|
-
persistence({ store, key: 'user:onboarding', strategy: 'onNext' })
|
|
183
|
-
]
|
|
184
|
-
});
|
|
185
|
-
engine = result.engine;
|
|
186
|
-
});
|
|
187
|
-
</script>
|
|
188
|
-
|
|
189
|
-
{#if engine}
|
|
190
|
-
<PathShell {engine} oncomplete={handleComplete}>
|
|
191
|
-
{#snippet details()}
|
|
192
|
-
<DetailsForm />
|
|
193
|
-
{/snippet}
|
|
194
|
-
</PathShell>
|
|
195
|
-
{/if}
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
**Use this when:** you need auto-persistence, restore-on-reload, or custom observers. Production apps, checkout flows, anything where losing progress matters.
|
|
199
|
-
|
|
200
|
-
> **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.
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
## ⚠️ Step Components Must Use `getPathContext()`
|
|
205
|
-
|
|
206
|
-
Step components rendered inside `<PathShell>` access the path engine via `getPathContext()` — **not** Svelte's raw `getContext()`.
|
|
207
|
-
|
|
208
47
|
```svelte
|
|
48
|
+
<!-- DetailsStep.svelte — step component uses usePathContext -->
|
|
209
49
|
<script lang="ts">
|
|
210
|
-
|
|
211
|
-
import { getPathContext } from '@daltonr/pathwrite-svelte';
|
|
212
|
-
const { snapshot, setData } = getPathContext();
|
|
50
|
+
import { usePathContext } from "@daltonr/pathwrite-svelte";
|
|
213
51
|
|
|
214
|
-
|
|
215
|
-
// import { getContext } from 'svelte';
|
|
216
|
-
// const { snapshot, setData } = getContext('pathContext');
|
|
52
|
+
const ctx = usePathContext();
|
|
217
53
|
</script>
|
|
218
54
|
|
|
219
|
-
{#if
|
|
55
|
+
{#if ctx.snapshot}
|
|
220
56
|
<input
|
|
221
|
-
value={
|
|
222
|
-
oninput={(e) => setData(
|
|
57
|
+
value={ctx.snapshot.data.name ?? ""}
|
|
58
|
+
oninput={(e) => ctx.setData("name", e.currentTarget.value)}
|
|
59
|
+
placeholder="Name"
|
|
223
60
|
/>
|
|
61
|
+
<button onclick={ctx.next}>Next</button>
|
|
224
62
|
{/if}
|
|
225
63
|
```
|
|
226
64
|
|
|
227
|
-
|
|
65
|
+
## usePath
|
|
228
66
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
## API
|
|
67
|
+
`usePath<TData>(options?)` creates an isolated path engine instance with runes-based reactive state. The engine is unsubscribed automatically when the component is destroyed.
|
|
232
68
|
|
|
233
|
-
|
|
69
|
+
> Do not destructure `snapshot` — it is a reactive getter backed by `$state`. Destructuring captures the value once and loses reactivity. Access it as `path.snapshot` throughout the template.
|
|
234
70
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
```
|
|
71
|
+
| Return value | Type | Description |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `snapshot` | `PathSnapshot \| null` | Reactive getter. `null` when no path is active. |
|
|
74
|
+
| `start(definition, data?)` | `Promise<void>` | Start or restart a path. |
|
|
75
|
+
| `restart(definition, data?)` | `Promise<void>` | Tear down any active path and start fresh. |
|
|
76
|
+
| `next()` | `Promise<void>` | Advance one step. Completes on the last step. |
|
|
77
|
+
| `previous()` | `Promise<void>` | Go back one step. No-op on the first step of a top-level path. |
|
|
78
|
+
| `cancel()` | `Promise<void>` | Cancel the active path (or sub-path). |
|
|
79
|
+
| `goToStep(stepId)` | `Promise<void>` | Jump to a step by ID. Calls `onLeave`/`onEnter`; bypasses guards. |
|
|
80
|
+
| `goToStepChecked(stepId)` | `Promise<void>` | Jump to a step by ID, checking the current step's guard first. |
|
|
81
|
+
| `setData(key, value)` | `Promise<void>` | Update a single data field. Type-safe when `TData` is specified. |
|
|
82
|
+
| `startSubPath(definition, data?, meta?)` | `Promise<void>` | Push a sub-path. `meta` is returned to `onSubPathComplete`/`onSubPathCancel`. |
|
|
248
83
|
|
|
249
|
-
|
|
84
|
+
**Options:**
|
|
250
85
|
|
|
251
86
|
| Option | Type | Description |
|
|
252
|
-
|
|
253
|
-
| `engine` | `PathEngine` |
|
|
254
|
-
| `onEvent` | `(event) => void` | Called for every engine event |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `engine` | `PathEngine` | Externally-managed engine (e.g. from `restoreOrStart()`). `usePath` subscribes to it; the caller owns the lifecycle. |
|
|
89
|
+
| `onEvent` | `(event: PathEvent) => void` | Called for every engine event. |
|
|
255
90
|
|
|
256
|
-
|
|
91
|
+
## PathShell props
|
|
257
92
|
|
|
258
|
-
|
|
259
|
-
|----------|------|-------------|
|
|
260
|
-
| `snapshot` | `Readable<PathSnapshot \| null>` | Current path state (reactive store) |
|
|
261
|
-
| `start` | `function` | Start a path |
|
|
262
|
-
| `startSubPath` | `function` | Launch a sub-path |
|
|
263
|
-
| `next` | `function` | Go to next step |
|
|
264
|
-
| `previous` | `function` | Go to previous step |
|
|
265
|
-
| `cancel` | `function` | Cancel the path |
|
|
266
|
-
| `goToStep` | `function` | Jump to step by ID |
|
|
267
|
-
| `goToStepChecked` | `function` | Jump with guard checks |
|
|
268
|
-
| `setData` | `function` | Update data |
|
|
269
|
-
| `restart` | `function` | Restart the path |
|
|
270
|
-
|
|
271
|
-
### `<PathShell>`
|
|
272
|
-
|
|
273
|
-
Default UI shell with progress indicator and navigation buttons.
|
|
274
|
-
|
|
275
|
-
#### Props
|
|
93
|
+
Step content is supplied as Svelte 5 snippets whose names match each step's `id`. For hyphenated step IDs (e.g. `"cover-letter"`), pass the snippet as the camelCase prop (`coverLetter={...}`) — PathShell resolves it automatically. A `console.warn` fires in development if no snippet is found under either the exact ID or the camelCase form.
|
|
276
94
|
|
|
277
95
|
| Prop | Type | Default | Description |
|
|
278
|
-
|
|
279
|
-
| `path` | `PathDefinition` | — | Path
|
|
280
|
-
| `engine` | `PathEngine` | — |
|
|
281
|
-
| `initialData` | `PathData` | `{}` | Initial data |
|
|
282
|
-
| `autoStart` | `boolean` | `true` |
|
|
283
|
-
| `
|
|
284
|
-
| `
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
| `
|
|
288
|
-
| `
|
|
289
|
-
| `
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
| Callback | Type | Description |
|
|
296
|
-
|----------|------|-------------|
|
|
297
|
-
| `oncomplete` | `(data) => void` | Called when path completes |
|
|
298
|
-
| `oncancel` | `(data) => void` | Called when path is cancelled |
|
|
299
|
-
| `onevent` | `(event) => void` | Called for every event |
|
|
300
|
-
|
|
301
|
-
#### Snippets
|
|
302
|
-
|
|
303
|
-
Step content is provided as Svelte 5 snippets. The snippet name must match the step ID:
|
|
304
|
-
|
|
305
|
-
```svelte
|
|
306
|
-
<PathShell path={myPath}>
|
|
307
|
-
{#snippet details()}
|
|
308
|
-
<DetailsStep />
|
|
309
|
-
{/snippet}
|
|
310
|
-
{#snippet review()}
|
|
311
|
-
<ReviewStep />
|
|
312
|
-
{/snippet}
|
|
313
|
-
</PathShell>
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
You can also override the header and footer:
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| `path` | `PathDefinition` | — | Path to run. Mutually exclusive with `engine`. |
|
|
98
|
+
| `engine` | `PathEngine` | — | Externally-managed engine (e.g. from `restoreOrStart()`). Mutually exclusive with `path`. |
|
|
99
|
+
| `initialData` | `PathData` | `{}` | Initial data passed to `engine.start()`. |
|
|
100
|
+
| `autoStart` | `boolean` | `true` | Start on mount. Ignored when `engine` is provided. |
|
|
101
|
+
| `footerLayout` | `"wizard" \| "form" \| "auto"` | `"auto"` | `"wizard"`: Back on left, Cancel+Submit on right. `"form"`: Cancel on left, Submit on right, no Back. `"auto"` picks `"form"` for single-step paths. |
|
|
102
|
+
| `hideProgress` | `boolean` | `false` | Hide the progress indicator. Also hidden automatically for single-step paths. |
|
|
103
|
+
| `backLabel` | `string` | `"Previous"` | Previous button label. |
|
|
104
|
+
| `nextLabel` | `string` | `"Next"` | Next button label. |
|
|
105
|
+
| `completeLabel` | `string` | `"Complete"` | Complete button label (last step). |
|
|
106
|
+
| `cancelLabel` | `string` | `"Cancel"` | Cancel button label. |
|
|
107
|
+
| `hideCancel` | `boolean` | `false` | Hide the Cancel button. |
|
|
108
|
+
| `oncomplete` | `(data: PathData) => void` | — | Called when the path finishes naturally. |
|
|
109
|
+
| `oncancel` | `(data: PathData) => void` | — | Called when the path is cancelled. |
|
|
110
|
+
| `onevent` | `(event: PathEvent) => void` | — | Called for every engine event. |
|
|
111
|
+
|
|
112
|
+
You can also replace the built-in header and footer with custom snippets:
|
|
317
113
|
|
|
318
114
|
```svelte
|
|
319
115
|
<PathShell path={myPath}>
|
|
320
116
|
{#snippet header(snap)}
|
|
321
|
-
<
|
|
117
|
+
<p>Step {snap.stepIndex + 1} of {snap.stepCount}</p>
|
|
322
118
|
{/snippet}
|
|
323
119
|
|
|
324
|
-
{#snippet details()}
|
|
325
|
-
<DetailsStep />
|
|
326
|
-
{/snippet}
|
|
120
|
+
{#snippet details()}<DetailsStep />{/snippet}
|
|
327
121
|
|
|
328
122
|
{#snippet footer(snap, actions)}
|
|
329
|
-
<button onclick={actions.previous} disabled={snap.isFirstStep}>
|
|
330
|
-
← Back
|
|
331
|
-
</button>
|
|
123
|
+
<button onclick={actions.previous} disabled={snap.isFirstStep}>Back</button>
|
|
332
124
|
<button onclick={actions.next} disabled={!snap.canMoveNext}>
|
|
333
|
-
{snap.isLastStep ?
|
|
125
|
+
{snap.isLastStep ? "Submit" : "Continue"}
|
|
334
126
|
</button>
|
|
335
127
|
{/snippet}
|
|
336
128
|
</PathShell>
|
|
337
129
|
```
|
|
338
130
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
There are two ways to reset `<PathShell>` to step 1.
|
|
342
|
-
|
|
343
|
-
**Option 1 — Toggle mount** (simplest, always correct)
|
|
344
|
-
|
|
345
|
-
Toggle a `$state` rune to destroy and recreate the shell:
|
|
346
|
-
|
|
347
|
-
```svelte
|
|
348
|
-
<script>
|
|
349
|
-
let isActive = $state(true);
|
|
350
|
-
</script>
|
|
351
|
-
|
|
352
|
-
{#if isActive}
|
|
353
|
-
<PathShell path={myPath} oncomplete={() => (isActive = false)}>
|
|
354
|
-
{#snippet details()}<DetailsStep />{/snippet}
|
|
355
|
-
</PathShell>
|
|
356
|
-
{:else}
|
|
357
|
-
<button onclick={() => (isActive = true)}>Try Again</button>
|
|
358
|
-
{/if}
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
**Option 2 — Call `restart()` on the shell ref** (in-place, no unmount)
|
|
362
|
-
|
|
363
|
-
Use `bind:this` to get a reference to the shell instance, then call `restart()`:
|
|
364
|
-
|
|
365
|
-
```svelte
|
|
366
|
-
<script>
|
|
367
|
-
let shellRef;
|
|
368
|
-
</script>
|
|
369
|
-
|
|
370
|
-
<PathShell bind:this={shellRef} path={myPath} oncomplete={onDone}>
|
|
371
|
-
{#snippet details()}<DetailsStep />{/snippet}
|
|
372
|
-
</PathShell>
|
|
373
|
-
|
|
374
|
-
<button onclick={() => shellRef.restart()}>Try Again</button>
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
`restart()` resets the path engine to step 1 with the original `initialData` without unmounting the component. Use this when you need to keep the shell mounted — for example, to preserve scroll position or drive a CSS transition.
|
|
378
|
-
|
|
379
|
-
### `getPathContext<TData>()`
|
|
131
|
+
## usePathContext
|
|
380
132
|
|
|
381
|
-
|
|
133
|
+
`usePathContext<TData>()` is the preferred way for step components rendered inside `<PathShell>` to access the path engine. `<PathShell>` calls `setContext()` internally with a private `Symbol` key; `usePathContext()` calls the matching `getContext()` and returns the same interface as `usePath`. It throws a clear error if called outside a `<PathShell>` — do not use Svelte's raw `getContext()` directly, as the key is a private `Symbol` and will silently return `undefined`.
|
|
382
134
|
|
|
383
135
|
```svelte
|
|
384
136
|
<script lang="ts">
|
|
385
|
-
import {
|
|
386
|
-
|
|
387
|
-
const
|
|
137
|
+
import { usePathContext } from "@daltonr/pathwrite-svelte";
|
|
138
|
+
|
|
139
|
+
const ctx = usePathContext<ApplicationData>();
|
|
388
140
|
</script>
|
|
389
141
|
|
|
390
|
-
{#if
|
|
142
|
+
{#if ctx.snapshot}
|
|
391
143
|
<input
|
|
392
|
-
value={
|
|
393
|
-
oninput={(e) => setData(
|
|
144
|
+
value={ctx.snapshot.data.name ?? ""}
|
|
145
|
+
oninput={(e) => ctx.setData("name", e.currentTarget.value)}
|
|
394
146
|
/>
|
|
395
|
-
<button onclick={next}>Next</button>
|
|
396
147
|
{/if}
|
|
397
148
|
```
|
|
398
149
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
Helper to create a two-way binding store.
|
|
402
|
-
|
|
403
|
-
```svelte
|
|
404
|
-
<script lang="ts">
|
|
405
|
-
import { usePath, bindData } from '@daltonr/pathwrite-svelte';
|
|
406
|
-
|
|
407
|
-
const { snapshot, setData } = usePath();
|
|
408
|
-
const name = bindData(snapshot, setData, 'name');
|
|
409
|
-
</script>
|
|
410
|
-
|
|
411
|
-
<input bind:value={$name} />
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
## PathSnapshot
|
|
415
|
-
|
|
416
|
-
The `snapshot` store contains the current path state:
|
|
417
|
-
|
|
418
|
-
```typescript
|
|
419
|
-
interface PathSnapshot {
|
|
420
|
-
pathId: string;
|
|
421
|
-
stepId: string;
|
|
422
|
-
stepIndex: number;
|
|
423
|
-
stepCount: number;
|
|
424
|
-
data: PathData;
|
|
425
|
-
nestingLevel: number;
|
|
426
|
-
isFirstStep: boolean;
|
|
427
|
-
isLastStep: boolean;
|
|
428
|
-
canMoveNext: boolean;
|
|
429
|
-
canMovePrevious: boolean;
|
|
430
|
-
isNavigating: boolean;
|
|
431
|
-
progress: number; // 0-1
|
|
432
|
-
steps: Array<{ id: string; title?: string; status: 'completed' | 'current' | 'upcoming' }>;
|
|
433
|
-
validationMessages: string[];
|
|
434
|
-
}
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
Access it via the store: `$snapshot`
|
|
438
|
-
|
|
439
|
-
## Guards and Hooks
|
|
440
|
-
|
|
441
|
-
Define validation and lifecycle logic in your path definition:
|
|
442
|
-
|
|
443
|
-
```typescript
|
|
444
|
-
const myPath = {
|
|
445
|
-
id: 'signup',
|
|
446
|
-
steps: [
|
|
447
|
-
{
|
|
448
|
-
id: 'details',
|
|
449
|
-
canMoveNext: (ctx) => ctx.data.name && ctx.data.email,
|
|
450
|
-
validationMessages: (ctx) => {
|
|
451
|
-
const errors = [];
|
|
452
|
-
if (!ctx.data.name) errors.push('Name is required');
|
|
453
|
-
if (!ctx.data.email) errors.push('Email is required');
|
|
454
|
-
return errors;
|
|
455
|
-
},
|
|
456
|
-
onEnter: (ctx) => {
|
|
457
|
-
console.log('Entered details step');
|
|
458
|
-
},
|
|
459
|
-
onLeave: (ctx) => {
|
|
460
|
-
console.log('Leaving details step');
|
|
461
|
-
}
|
|
462
|
-
},
|
|
463
|
-
{ id: 'review' }
|
|
464
|
-
]
|
|
465
|
-
};
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
## Persistence
|
|
469
|
-
|
|
470
|
-
Use with [@daltonr/pathwrite-store](../store) for automatic state persistence:
|
|
471
|
-
|
|
472
|
-
```svelte
|
|
473
|
-
<script lang="ts">
|
|
474
|
-
import { onMount } from 'svelte';
|
|
475
|
-
import { PathShell } from '@daltonr/pathwrite-svelte';
|
|
476
|
-
import { HttpStore, restoreOrStart, persistence } from '@daltonr/pathwrite-store';
|
|
477
|
-
import DetailsForm from './DetailsForm.svelte';
|
|
478
|
-
import ReviewPanel from './ReviewPanel.svelte';
|
|
479
|
-
|
|
480
|
-
const store = new HttpStore({ baseUrl: '/api/wizard' });
|
|
481
|
-
const key = 'user:123:signup';
|
|
482
|
-
|
|
483
|
-
let engine = $state(null);
|
|
484
|
-
let restored = $state(false);
|
|
485
|
-
|
|
486
|
-
onMount(async () => {
|
|
487
|
-
const result = await restoreOrStart({
|
|
488
|
-
store,
|
|
489
|
-
key,
|
|
490
|
-
path: signupPath,
|
|
491
|
-
initialData: { name: '', email: '' },
|
|
492
|
-
observers: [
|
|
493
|
-
persistence({ store, key, strategy: 'onNext' })
|
|
494
|
-
]
|
|
495
|
-
});
|
|
496
|
-
engine = result.engine;
|
|
497
|
-
restored = result.restored;
|
|
498
|
-
});
|
|
499
|
-
</script>
|
|
500
|
-
|
|
501
|
-
{#if engine}
|
|
502
|
-
<PathShell {engine} oncomplete={(data) => console.log('Done!', data)}>
|
|
503
|
-
{#snippet details()}
|
|
504
|
-
<DetailsForm />
|
|
505
|
-
{/snippet}
|
|
506
|
-
{#snippet review()}
|
|
507
|
-
<ReviewPanel />
|
|
508
|
-
{/snippet}
|
|
509
|
-
</PathShell>
|
|
510
|
-
{/if}
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
## TypeScript
|
|
514
|
-
|
|
515
|
-
Type your path data for full type safety:
|
|
516
|
-
|
|
517
|
-
```typescript
|
|
518
|
-
interface SignupData {
|
|
519
|
-
name: string;
|
|
520
|
-
email: string;
|
|
521
|
-
age: number;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const { snapshot, setData } = usePath<SignupData>();
|
|
525
|
-
|
|
526
|
-
// ✅ Type-checked
|
|
527
|
-
setData('name', 'John');
|
|
528
|
-
|
|
529
|
-
// ❌ Type error
|
|
530
|
-
setData('invalid', 'value');
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
## License
|
|
534
|
-
|
|
535
|
-
MIT — © 2026 Devjoy Ltd.
|
|
536
|
-
|
|
537
|
-
## See Also
|
|
538
|
-
|
|
539
|
-
- [@daltonr/pathwrite-core](../core) - Core engine
|
|
540
|
-
- [@daltonr/pathwrite-store](../store) - HTTP persistence
|
|
541
|
-
- [Documentation](../../docs/guides/DEVELOPER_GUIDE.md)
|
|
150
|
+
## Further reading
|
|
542
151
|
|
|
152
|
+
- [Svelte getting started guide](../../docs/getting-started/frameworks/svelte.md)
|
|
153
|
+
- [Navigation & guards](../../docs/guides/navigation.md)
|
|
154
|
+
- [Full documentation](../../docs/README.md)
|
|
543
155
|
|
|
544
156
|
---
|
|
545
157
|
|
|
546
|
-
© 2026 Devjoy Ltd.
|
|
547
|
-
|
|
158
|
+
MIT — © 2026 Devjoy Ltd.
|
package/dist/PathShell.svelte
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
|
-
import { usePath, setPathContext, formatFieldKey, errorPhaseMessage } from './index.svelte.js';
|
|
3
|
+
import { usePath, setPathContext, formatFieldKey, errorPhaseMessage, stepIdToCamelCase } from './index.svelte.js';
|
|
4
4
|
import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout } from './index.svelte.js';
|
|
5
5
|
import type { Snippet, Component } from 'svelte';
|
|
6
6
|
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
cancelLabel?: string;
|
|
18
18
|
hideCancel?: boolean;
|
|
19
19
|
hideProgress?: boolean;
|
|
20
|
+
/** If true, hide the footer (navigation buttons). The error panel is still shown on async failure regardless of this prop. */
|
|
21
|
+
hideFooter?: boolean;
|
|
22
|
+
/** When true, calls `validate()` on the engine so all steps show inline errors simultaneously. Useful when this shell is nested inside a step of an outer shell: bind to the outer snapshot's `hasAttemptedNext`. */
|
|
23
|
+
validateWhen?: boolean;
|
|
20
24
|
/**
|
|
21
25
|
* Footer layout mode:
|
|
22
26
|
* - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
|
|
@@ -67,6 +71,8 @@
|
|
|
67
71
|
cancelLabel = 'Cancel',
|
|
68
72
|
hideCancel = false,
|
|
69
73
|
hideProgress = false,
|
|
74
|
+
hideFooter = false,
|
|
75
|
+
validateWhen = false,
|
|
70
76
|
footerLayout = 'auto',
|
|
71
77
|
validationDisplay = 'summary',
|
|
72
78
|
progressLayout = 'merged',
|
|
@@ -115,6 +121,18 @@
|
|
|
115
121
|
}
|
|
116
122
|
});
|
|
117
123
|
|
|
124
|
+
$effect(() => {
|
|
125
|
+
if (validateWhen) pathReturn.validate();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
function warnMissingStep(stepId: string): void {
|
|
129
|
+
const camel = stepIdToCamelCase(stepId);
|
|
130
|
+
const hint = camel !== stepId
|
|
131
|
+
? ` No snippet found for "${stepId}" or its camelCase form "${camel}". If your step ID contains hyphens, pass the snippet as a camelCase prop: ${camel}={YourComponent}.`
|
|
132
|
+
: ` No snippet found for "${stepId}".`;
|
|
133
|
+
console.warn(`[PathShell]${hint}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
118
136
|
let snap = $derived(pathReturn.snapshot);
|
|
119
137
|
let actions = $derived({ next, previous, cancel, goToStep, goToStepChecked, setData, restart: () => restartFn(path, initialData), retry, suspend });
|
|
120
138
|
|
|
@@ -194,7 +212,10 @@
|
|
|
194
212
|
|
|
195
213
|
<!-- Body: current step rendered via named snippet.
|
|
196
214
|
Prefer formId (inner step id of a StepChoice) so consumers can
|
|
197
|
-
register snippets by inner step ids directly.
|
|
215
|
+
register snippets by inner step ids directly.
|
|
216
|
+
Hyphenated step IDs (e.g. "cover-letter") are normalised to camelCase
|
|
217
|
+
("coverLetter") as a fallback, since Svelte props must be valid JS
|
|
218
|
+
identifiers. -->
|
|
198
219
|
<div class="pw-shell__body">
|
|
199
220
|
{#if snap.formId && stepSnippets[snap.formId]}
|
|
200
221
|
{@const StepComponent = stepSnippets[snap.formId]}
|
|
@@ -202,13 +223,17 @@
|
|
|
202
223
|
{:else if stepSnippets[snap.stepId]}
|
|
203
224
|
{@const StepComponent = stepSnippets[snap.stepId]}
|
|
204
225
|
<StepComponent />
|
|
226
|
+
{:else if stepSnippets[stepIdToCamelCase(snap.formId ?? snap.stepId)]}
|
|
227
|
+
{@const StepComponent = stepSnippets[stepIdToCamelCase(snap.formId ?? snap.stepId)]}
|
|
228
|
+
<StepComponent />
|
|
205
229
|
{:else}
|
|
230
|
+
{warnMissingStep(snap.stepId)}
|
|
206
231
|
<p>No content for step "{snap.stepId}"</p>
|
|
207
232
|
{/if}
|
|
208
233
|
</div>
|
|
209
234
|
|
|
210
235
|
<!-- Validation messages — suppressed when validationDisplay="inline" -->
|
|
211
|
-
{#if validationDisplay !== 'inline' && snap.hasAttemptedNext && Object.keys(snap.fieldErrors).length > 0}
|
|
236
|
+
{#if validationDisplay !== 'inline' && (snap.hasAttemptedNext || snap.hasValidated) && Object.keys(snap.fieldErrors).length > 0}
|
|
212
237
|
<ul class="pw-shell__validation">
|
|
213
238
|
{#each Object.entries(snap.fieldErrors) as [key, msg]}
|
|
214
239
|
<li class="pw-shell__validation-item">
|
|
@@ -230,7 +255,7 @@
|
|
|
230
255
|
{/if}
|
|
231
256
|
|
|
232
257
|
<!-- Blocking error — guard returned { allowed: false, reason } -->
|
|
233
|
-
{#if validationDisplay !== 'inline' && snap.hasAttemptedNext && snap.blockingError}
|
|
258
|
+
{#if validationDisplay !== 'inline' && (snap.hasAttemptedNext || snap.hasValidated) && snap.blockingError}
|
|
234
259
|
<p class="pw-shell__blocking-error">{snap.blockingError}</p>
|
|
235
260
|
{/if}
|
|
236
261
|
|
|
@@ -258,9 +283,9 @@
|
|
|
258
283
|
</div>
|
|
259
284
|
</div>
|
|
260
285
|
<!-- Footer: navigation buttons (overridable via footer snippet) -->
|
|
261
|
-
{:else if footer}
|
|
286
|
+
{:else if !hideFooter && footer}
|
|
262
287
|
{@render footer(snap, actions)}
|
|
263
|
-
{:else}
|
|
288
|
+
{:else if !hideFooter}
|
|
264
289
|
<div class="pw-shell__footer">
|
|
265
290
|
<div class="pw-shell__footer-left">
|
|
266
291
|
{#if resolvedFooterLayout === 'form' && !hideCancel}
|
|
@@ -12,6 +12,10 @@ interface Props {
|
|
|
12
12
|
cancelLabel?: string;
|
|
13
13
|
hideCancel?: boolean;
|
|
14
14
|
hideProgress?: boolean;
|
|
15
|
+
/** If true, hide the footer (navigation buttons). The error panel is still shown on async failure regardless of this prop. */
|
|
16
|
+
hideFooter?: boolean;
|
|
17
|
+
/** When true, calls `validate()` on the engine so all steps show inline errors simultaneously. Useful when this shell is nested inside a step of an outer shell: bind to the outer snapshot's `hasAttemptedNext`. */
|
|
18
|
+
validateWhen?: boolean;
|
|
15
19
|
/**
|
|
16
20
|
* Footer layout mode:
|
|
17
21
|
* - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
|
|
@@ -1 +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,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC5G,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAI/C,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,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IAC1C;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;IAClD;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,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,SAAS,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;CACrC;
|
|
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,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC5G,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAI/C,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,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,8HAA8H;IAC9H,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qNAAqN;IACrN,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IAC1C;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;IAClD;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,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,SAAS,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;CACrC;AAqQH,QAAA,MAAM,SAAS;mBAhKQ,QAAQ,IAAI,CAAC;MAgKmB,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
package/dist/index.svelte.d.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
52
52
|
retry: () => Promise<void>;
|
|
53
53
|
/** Pauses the path with intent to return. Emits `suspended`. All state is preserved. */
|
|
54
54
|
suspend: () => Promise<void>;
|
|
55
|
+
/** Trigger inline validation on all steps without navigating. Sets `snapshot.hasValidated`. */
|
|
56
|
+
validate: () => void;
|
|
55
57
|
}
|
|
56
58
|
/**
|
|
57
59
|
* Create a Pathwrite engine with Svelte 5 runes-based reactivity.
|
|
@@ -178,5 +180,12 @@ export declare function bindData<TData extends PathData, K extends string & keyo
|
|
|
178
180
|
readonly value: TData[K];
|
|
179
181
|
set: (value: TData[K]) => void;
|
|
180
182
|
};
|
|
183
|
+
/**
|
|
184
|
+
* Converts a hyphenated step ID to camelCase.
|
|
185
|
+
* Used internally by PathShell to resolve step snippets when a step ID contains
|
|
186
|
+
* hyphens (e.g. "cover-letter" → "coverLetter"), since Svelte prop names must
|
|
187
|
+
* be valid JavaScript identifiers.
|
|
188
|
+
*/
|
|
189
|
+
export declare function stepIdToCamelCase(id: string): string;
|
|
181
190
|
export { default as PathShell } from "./PathShell.svelte";
|
|
182
191
|
//# sourceMappingURL=index.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.svelte.d.ts","sourceRoot":"","sources":["../src/index.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EACd,UAAU,EACV,SAAS,EACT,YAAY,EACb,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,YAAY,EACV,QAAQ,EACR,WAAW,EACX,cAAc,EACd,UAAU,EACV,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,eAAe,EACf,cAAc,EACd,YAAY,EACZ,mBAAmB,EACpB,MAAM,yBAAyB,CAAC;AAMjC,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,mFAAmF;IACnF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,aAAa,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ;IAC9D;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAC9C,iCAAiC;IACjC,KAAK,EAAE,CAAC,IAAI,EAAE,cAAc,CAAC,GAAG,CAAC,EAAE,WAAW,CAAC,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,6MAA6M;IAC7M,YAAY,EAAE,CAAC,IAAI,EAAE,cAAc,CAAC,GAAG,CAAC,EAAE,WAAW,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnH,6DAA6D;IAC7D,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,qJAAqJ;IACrJ,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,4CAA4C;IAC5C,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,iGAAiG;IACjG,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,oLAAoL;IACpL,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,gKAAgK;IAChK,OAAO,EAAE,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF,qHAAqH;IACrH,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B;;;;OAIG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,0IAA0I;IAC1I,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,wFAAwF;IACxF,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.svelte.d.ts","sourceRoot":"","sources":["../src/index.svelte.ts"],"names":[],"mappings":"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"}
|
package/dist/index.svelte.js
CHANGED
|
@@ -87,6 +87,7 @@ export function usePath(options) {
|
|
|
87
87
|
const restart = () => engine.restart();
|
|
88
88
|
const retry = () => engine.retry();
|
|
89
89
|
const suspend = () => engine.suspend();
|
|
90
|
+
const validate = () => engine.validate();
|
|
90
91
|
return {
|
|
91
92
|
get snapshot() { return _snapshot; },
|
|
92
93
|
start,
|
|
@@ -100,7 +101,8 @@ export function usePath(options) {
|
|
|
100
101
|
resetStep,
|
|
101
102
|
restart,
|
|
102
103
|
retry,
|
|
103
|
-
suspend
|
|
104
|
+
suspend,
|
|
105
|
+
validate
|
|
104
106
|
};
|
|
105
107
|
}
|
|
106
108
|
// ---------------------------------------------------------------------------
|
|
@@ -175,5 +177,14 @@ export function bindData(getSnapshot, setData, key) {
|
|
|
175
177
|
}
|
|
176
178
|
};
|
|
177
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Converts a hyphenated step ID to camelCase.
|
|
182
|
+
* Used internally by PathShell to resolve step snippets when a step ID contains
|
|
183
|
+
* hyphens (e.g. "cover-letter" → "coverLetter"), since Svelte prop names must
|
|
184
|
+
* be valid JavaScript identifiers.
|
|
185
|
+
*/
|
|
186
|
+
export function stepIdToCamelCase(id) {
|
|
187
|
+
return id.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
188
|
+
}
|
|
178
189
|
// Export PathShell component
|
|
179
190
|
export { default as PathShell } from "./PathShell.svelte";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daltonr/pathwrite-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Svelte 5 adapter for @daltonr/pathwrite-core — runes-based reactive bindings and optional PathShell component.",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"svelte": ">=5.0.0"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@daltonr/pathwrite-core": "^0.
|
|
55
|
+
"@daltonr/pathwrite-core": "^0.11.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@sveltejs/package": "^2.5.7",
|
package/src/PathShell.svelte
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
|
-
import { usePath, setPathContext, formatFieldKey, errorPhaseMessage } from './index.svelte.js';
|
|
3
|
+
import { usePath, setPathContext, formatFieldKey, errorPhaseMessage, stepIdToCamelCase } from './index.svelte.js';
|
|
4
4
|
import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout } from './index.svelte.js';
|
|
5
5
|
import type { Snippet, Component } from 'svelte';
|
|
6
6
|
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
cancelLabel?: string;
|
|
18
18
|
hideCancel?: boolean;
|
|
19
19
|
hideProgress?: boolean;
|
|
20
|
+
/** If true, hide the footer (navigation buttons). The error panel is still shown on async failure regardless of this prop. */
|
|
21
|
+
hideFooter?: boolean;
|
|
22
|
+
/** When true, calls `validate()` on the engine so all steps show inline errors simultaneously. Useful when this shell is nested inside a step of an outer shell: bind to the outer snapshot's `hasAttemptedNext`. */
|
|
23
|
+
validateWhen?: boolean;
|
|
20
24
|
/**
|
|
21
25
|
* Footer layout mode:
|
|
22
26
|
* - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
|
|
@@ -67,6 +71,8 @@
|
|
|
67
71
|
cancelLabel = 'Cancel',
|
|
68
72
|
hideCancel = false,
|
|
69
73
|
hideProgress = false,
|
|
74
|
+
hideFooter = false,
|
|
75
|
+
validateWhen = false,
|
|
70
76
|
footerLayout = 'auto',
|
|
71
77
|
validationDisplay = 'summary',
|
|
72
78
|
progressLayout = 'merged',
|
|
@@ -115,6 +121,18 @@
|
|
|
115
121
|
}
|
|
116
122
|
});
|
|
117
123
|
|
|
124
|
+
$effect(() => {
|
|
125
|
+
if (validateWhen) pathReturn.validate();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
function warnMissingStep(stepId: string): void {
|
|
129
|
+
const camel = stepIdToCamelCase(stepId);
|
|
130
|
+
const hint = camel !== stepId
|
|
131
|
+
? ` No snippet found for "${stepId}" or its camelCase form "${camel}". If your step ID contains hyphens, pass the snippet as a camelCase prop: ${camel}={YourComponent}.`
|
|
132
|
+
: ` No snippet found for "${stepId}".`;
|
|
133
|
+
console.warn(`[PathShell]${hint}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
118
136
|
let snap = $derived(pathReturn.snapshot);
|
|
119
137
|
let actions = $derived({ next, previous, cancel, goToStep, goToStepChecked, setData, restart: () => restartFn(path, initialData), retry, suspend });
|
|
120
138
|
|
|
@@ -194,7 +212,10 @@
|
|
|
194
212
|
|
|
195
213
|
<!-- Body: current step rendered via named snippet.
|
|
196
214
|
Prefer formId (inner step id of a StepChoice) so consumers can
|
|
197
|
-
register snippets by inner step ids directly.
|
|
215
|
+
register snippets by inner step ids directly.
|
|
216
|
+
Hyphenated step IDs (e.g. "cover-letter") are normalised to camelCase
|
|
217
|
+
("coverLetter") as a fallback, since Svelte props must be valid JS
|
|
218
|
+
identifiers. -->
|
|
198
219
|
<div class="pw-shell__body">
|
|
199
220
|
{#if snap.formId && stepSnippets[snap.formId]}
|
|
200
221
|
{@const StepComponent = stepSnippets[snap.formId]}
|
|
@@ -202,13 +223,17 @@
|
|
|
202
223
|
{:else if stepSnippets[snap.stepId]}
|
|
203
224
|
{@const StepComponent = stepSnippets[snap.stepId]}
|
|
204
225
|
<StepComponent />
|
|
226
|
+
{:else if stepSnippets[stepIdToCamelCase(snap.formId ?? snap.stepId)]}
|
|
227
|
+
{@const StepComponent = stepSnippets[stepIdToCamelCase(snap.formId ?? snap.stepId)]}
|
|
228
|
+
<StepComponent />
|
|
205
229
|
{:else}
|
|
230
|
+
{warnMissingStep(snap.stepId)}
|
|
206
231
|
<p>No content for step "{snap.stepId}"</p>
|
|
207
232
|
{/if}
|
|
208
233
|
</div>
|
|
209
234
|
|
|
210
235
|
<!-- Validation messages — suppressed when validationDisplay="inline" -->
|
|
211
|
-
{#if validationDisplay !== 'inline' && snap.hasAttemptedNext && Object.keys(snap.fieldErrors).length > 0}
|
|
236
|
+
{#if validationDisplay !== 'inline' && (snap.hasAttemptedNext || snap.hasValidated) && Object.keys(snap.fieldErrors).length > 0}
|
|
212
237
|
<ul class="pw-shell__validation">
|
|
213
238
|
{#each Object.entries(snap.fieldErrors) as [key, msg]}
|
|
214
239
|
<li class="pw-shell__validation-item">
|
|
@@ -230,7 +255,7 @@
|
|
|
230
255
|
{/if}
|
|
231
256
|
|
|
232
257
|
<!-- Blocking error — guard returned { allowed: false, reason } -->
|
|
233
|
-
{#if validationDisplay !== 'inline' && snap.hasAttemptedNext && snap.blockingError}
|
|
258
|
+
{#if validationDisplay !== 'inline' && (snap.hasAttemptedNext || snap.hasValidated) && snap.blockingError}
|
|
234
259
|
<p class="pw-shell__blocking-error">{snap.blockingError}</p>
|
|
235
260
|
{/if}
|
|
236
261
|
|
|
@@ -258,9 +283,9 @@
|
|
|
258
283
|
</div>
|
|
259
284
|
</div>
|
|
260
285
|
<!-- Footer: navigation buttons (overridable via footer snippet) -->
|
|
261
|
-
{:else if footer}
|
|
286
|
+
{:else if !hideFooter && footer}
|
|
262
287
|
{@render footer(snap, actions)}
|
|
263
|
-
{:else}
|
|
288
|
+
{:else if !hideFooter}
|
|
264
289
|
<div class="pw-shell__footer">
|
|
265
290
|
<div class="pw-shell__footer-left">
|
|
266
291
|
{#if resolvedFooterLayout === 'form' && !hideCancel}
|
package/src/index.svelte.ts
CHANGED
|
@@ -80,6 +80,8 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
80
80
|
retry: () => Promise<void>;
|
|
81
81
|
/** Pauses the path with intent to return. Emits `suspended`. All state is preserved. */
|
|
82
82
|
suspend: () => Promise<void>;
|
|
83
|
+
/** Trigger inline validation on all steps without navigating. Sets `snapshot.hasValidated`. */
|
|
84
|
+
validate: () => void;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
// ---------------------------------------------------------------------------
|
|
@@ -188,6 +190,8 @@ export function usePath<TData extends PathData = PathData>(
|
|
|
188
190
|
const retry = (): Promise<void> => engine.retry();
|
|
189
191
|
const suspend = (): Promise<void> => engine.suspend();
|
|
190
192
|
|
|
193
|
+
const validate = (): void => engine.validate();
|
|
194
|
+
|
|
191
195
|
return {
|
|
192
196
|
get snapshot() { return _snapshot; },
|
|
193
197
|
start,
|
|
@@ -201,7 +205,8 @@ export function usePath<TData extends PathData = PathData>(
|
|
|
201
205
|
resetStep,
|
|
202
206
|
restart,
|
|
203
207
|
retry,
|
|
204
|
-
suspend
|
|
208
|
+
suspend,
|
|
209
|
+
validate
|
|
205
210
|
};
|
|
206
211
|
}
|
|
207
212
|
|
|
@@ -310,6 +315,16 @@ export function bindData<TData extends PathData, K extends string & keyof TData>
|
|
|
310
315
|
};
|
|
311
316
|
}
|
|
312
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Converts a hyphenated step ID to camelCase.
|
|
320
|
+
* Used internally by PathShell to resolve step snippets when a step ID contains
|
|
321
|
+
* hyphens (e.g. "cover-letter" → "coverLetter"), since Svelte prop names must
|
|
322
|
+
* be valid JavaScript identifiers.
|
|
323
|
+
*/
|
|
324
|
+
export function stepIdToCamelCase(id: string): string {
|
|
325
|
+
return id.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
326
|
+
}
|
|
327
|
+
|
|
313
328
|
// Export PathShell component
|
|
314
329
|
export { default as PathShell } from "./PathShell.svelte";
|
|
315
330
|
|