@daltonr/pathwrite-svelte 0.10.1 → 0.12.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 +10 -3
- package/dist/PathShell.svelte +103 -15
- package/dist/PathShell.svelte.d.ts +17 -4
- package/dist/PathShell.svelte.d.ts.map +1 -1
- package/dist/index.css +25 -0
- package/dist/index.svelte.d.ts +22 -5
- package/dist/index.svelte.d.ts.map +1 -1
- package/dist/index.svelte.js +15 -4
- package/package.json +2 -2
- package/src/PathShell.svelte +103 -15
- package/src/index.svelte.ts +24 -9
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Peer dependencies: Svelte 5+.
|
|
|
70
70
|
|
|
71
71
|
| Return value | Type | Description |
|
|
72
72
|
|---|---|---|
|
|
73
|
-
| `snapshot` | `PathSnapshot \| null` | Reactive getter. `null` when no path is active. |
|
|
73
|
+
| `snapshot` | `PathSnapshot \| null` | Reactive getter. `null` when no path is active or when `completionBehaviour: "dismiss"` is used. With the default `"stayOnFinal"`, a non-null snapshot with `status === "completed"` is returned after the path finishes. |
|
|
74
74
|
| `start(definition, data?)` | `Promise<void>` | Start or restart a path. |
|
|
75
75
|
| `restart(definition, data?)` | `Promise<void>` | Tear down any active path and start fresh. |
|
|
76
76
|
| `next()` | `Promise<void>` | Advance one step. Completes on the last step. |
|
|
@@ -80,6 +80,7 @@ Peer dependencies: Svelte 5+.
|
|
|
80
80
|
| `goToStepChecked(stepId)` | `Promise<void>` | Jump to a step by ID, checking the current step's guard first. |
|
|
81
81
|
| `setData(key, value)` | `Promise<void>` | Update a single data field. Type-safe when `TData` is specified. |
|
|
82
82
|
| `startSubPath(definition, data?, meta?)` | `Promise<void>` | Push a sub-path. `meta` is returned to `onSubPathComplete`/`onSubPathCancel`. |
|
|
83
|
+
| `validate()` | `void` | Set `snapshot.hasValidated` without navigating. Triggers all inline field errors simultaneously. Used to validate all tabs in a nested shell at once. |
|
|
83
84
|
|
|
84
85
|
**Options:**
|
|
85
86
|
|
|
@@ -98,16 +99,22 @@ Step content is supplied as Svelte 5 snippets whose names match each step's `id`
|
|
|
98
99
|
| `engine` | `PathEngine` | — | Externally-managed engine (e.g. from `restoreOrStart()`). Mutually exclusive with `path`. |
|
|
99
100
|
| `initialData` | `PathData` | `{}` | Initial data passed to `engine.start()`. |
|
|
100
101
|
| `autoStart` | `boolean` | `true` | Start on mount. Ignored when `engine` is provided. |
|
|
101
|
-
| `
|
|
102
|
+
| `layout` | `"wizard" \| "form" \| "auto" \| "tabs"` | `"auto"` | `"wizard"`: Back on left, Cancel+Submit on right. `"form"`: Cancel on left, Submit on right, no Back. `"tabs"`: No progress header or footer — for tabbed interfaces. `"auto"` picks `"form"` for single-step paths. |
|
|
102
103
|
| `hideProgress` | `boolean` | `false` | Hide the progress indicator. Also hidden automatically for single-step paths. |
|
|
103
104
|
| `backLabel` | `string` | `"Previous"` | Previous button label. |
|
|
104
105
|
| `nextLabel` | `string` | `"Next"` | Next button label. |
|
|
105
106
|
| `completeLabel` | `string` | `"Complete"` | Complete button label (last step). |
|
|
106
107
|
| `cancelLabel` | `string` | `"Cancel"` | Cancel button label. |
|
|
107
108
|
| `hideCancel` | `boolean` | `false` | Hide the Cancel button. |
|
|
109
|
+
| `validateWhen` | `boolean` | `false` | When it becomes `true`, calls `validate()` on the engine. Bind to the outer snapshot's `hasAttemptedNext` when this shell is nested inside a step of an outer shell. |
|
|
110
|
+
| `restoreKey` | `string` | — | When set, the shell automatically saves its full state (data + active step) into the nearest outer `PathShell`'s data under this key on every change, and restores from it on remount. No-op on a top-level shell. |
|
|
111
|
+
| `services` | `unknown` | `null` | Arbitrary services object available to step components via `usePathContext<TData, TServices>().services`. |
|
|
108
112
|
| `oncomplete` | `(data: PathData) => void` | — | Called when the path finishes naturally. |
|
|
109
113
|
| `oncancel` | `(data: PathData) => void` | — | Called when the path is cancelled. |
|
|
110
114
|
| `onevent` | `(event: PathEvent) => void` | — | Called for every engine event. |
|
|
115
|
+
| `completion` | `Snippet<[PathSnapshot<any>]>` | — | Custom snippet rendered when `snapshot.status === "completed"` (`completionBehaviour: "stayOnFinal"`). Receives the completed snapshot. If omitted, a default "All done." panel is shown. |
|
|
116
|
+
|
|
117
|
+
> **Note:** Svelte requires event/callback props to be lowercase. Unlike React/Vue/Angular, passing `onComplete`, `onCancel`, or `onEvent` (camelCase) will be silently ignored. PathShell emits a `console.warn` in development if it detects one of these common mistakes.
|
|
111
118
|
|
|
112
119
|
You can also replace the built-in header and footer with custom snippets:
|
|
113
120
|
|
|
@@ -155,4 +162,4 @@ You can also replace the built-in header and footer with custom snippets:
|
|
|
155
162
|
|
|
156
163
|
---
|
|
157
164
|
|
|
158
|
-
|
|
165
|
+
© 2026 Devjoy Ltd. MIT License.
|
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, stepIdToCamelCase } from './index.svelte.js';
|
|
3
|
+
import { usePath, setPathContext, getPathContextOrNull, 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
|
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
path?: PathDefinition<any>;
|
|
10
10
|
engine?: PathEngine;
|
|
11
11
|
initialData?: PathData;
|
|
12
|
+
/**
|
|
13
|
+
* When set, this shell automatically saves its state into the nearest outer `PathShell`'s
|
|
14
|
+
* data under this key on every change, and restores from that stored state on remount.
|
|
15
|
+
* No-op when used on a top-level shell with no outer `PathShell` ancestor.
|
|
16
|
+
*/
|
|
17
|
+
restoreKey?: string;
|
|
12
18
|
autoStart?: boolean;
|
|
13
19
|
backLabel?: string;
|
|
14
20
|
nextLabel?: string;
|
|
@@ -17,13 +23,18 @@
|
|
|
17
23
|
cancelLabel?: string;
|
|
18
24
|
hideCancel?: boolean;
|
|
19
25
|
hideProgress?: boolean;
|
|
26
|
+
/** If true, hide the footer (navigation buttons). The error panel is still shown on async failure regardless of this prop. */
|
|
27
|
+
hideFooter?: boolean;
|
|
28
|
+
/** 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`. */
|
|
29
|
+
validateWhen?: boolean;
|
|
20
30
|
/**
|
|
21
|
-
*
|
|
31
|
+
* Shell layout mode:
|
|
22
32
|
* - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
|
|
23
|
-
* - "wizard": Back button on left, Cancel and Submit together on right.
|
|
24
|
-
* - "form": Cancel on left, Submit alone on right. Back button never shown.
|
|
33
|
+
* - "wizard": Progress header + Back button on left, Cancel and Submit together on right.
|
|
34
|
+
* - "form": Progress header + Cancel on left, Submit alone on right. Back button never shown.
|
|
35
|
+
* - "tabs": No progress header, no footer. Use for tabbed interfaces with a custom tab bar inside the step body.
|
|
25
36
|
*/
|
|
26
|
-
|
|
37
|
+
layout?: "wizard" | "form" | "auto" | "tabs";
|
|
27
38
|
/**
|
|
28
39
|
* Controls whether the shell renders its auto-generated field-error summary box.
|
|
29
40
|
* - `"summary"` (default): Shell renders the labeled error list below the step body.
|
|
@@ -51,6 +62,8 @@
|
|
|
51
62
|
// Optional override snippets for header and footer
|
|
52
63
|
header?: Snippet<[PathSnapshot<any>]>;
|
|
53
64
|
footer?: Snippet<[PathSnapshot<any>, object]>;
|
|
65
|
+
/** Snippet rendered when `snapshot.status === "completed"`. Defaults to a simple "All done." panel with a restart button. */
|
|
66
|
+
completion?: Snippet<[PathSnapshot<any>]>;
|
|
54
67
|
// All other props treated as step components keyed by step ID
|
|
55
68
|
[key: string]: Component<any> | any;
|
|
56
69
|
}
|
|
@@ -59,6 +72,7 @@
|
|
|
59
72
|
path,
|
|
60
73
|
engine: engineProp,
|
|
61
74
|
initialData = {},
|
|
75
|
+
restoreKey = undefined,
|
|
62
76
|
autoStart = true,
|
|
63
77
|
backLabel = 'Previous',
|
|
64
78
|
nextLabel = 'Next',
|
|
@@ -67,7 +81,9 @@
|
|
|
67
81
|
cancelLabel = 'Cancel',
|
|
68
82
|
hideCancel = false,
|
|
69
83
|
hideProgress = false,
|
|
70
|
-
|
|
84
|
+
hideFooter = false,
|
|
85
|
+
validateWhen = false,
|
|
86
|
+
layout = 'auto',
|
|
71
87
|
validationDisplay = 'summary',
|
|
72
88
|
progressLayout = 'merged',
|
|
73
89
|
services = null,
|
|
@@ -76,9 +92,14 @@
|
|
|
76
92
|
onevent,
|
|
77
93
|
header,
|
|
78
94
|
footer,
|
|
95
|
+
completion,
|
|
79
96
|
...stepSnippets
|
|
80
97
|
}: Props = $props();
|
|
81
98
|
|
|
99
|
+
// Read outer PathShell context BEFORE setting our own — gives access to
|
|
100
|
+
// parent shell's snapshot and setData for restoreKey auto-wiring.
|
|
101
|
+
const outerCtx = getPathContextOrNull();
|
|
102
|
+
|
|
82
103
|
// Initialize path engine
|
|
83
104
|
const pathReturn = usePath({
|
|
84
105
|
get engine() { return engineProp; },
|
|
@@ -86,6 +107,11 @@
|
|
|
86
107
|
onevent?.(event);
|
|
87
108
|
if (event.type === 'completed') oncomplete?.(event.data);
|
|
88
109
|
if (event.type === 'cancelled') oncancel?.(event.data);
|
|
110
|
+
if (restoreKey && outerCtx && event.type === 'stateChanged') {
|
|
111
|
+
(outerCtx.setData as unknown as (key: string, value: unknown) => Promise<void>)(
|
|
112
|
+
restoreKey, event.snapshot
|
|
113
|
+
);
|
|
114
|
+
}
|
|
89
115
|
}
|
|
90
116
|
});
|
|
91
117
|
|
|
@@ -106,15 +132,45 @@
|
|
|
106
132
|
get services() { return services; },
|
|
107
133
|
});
|
|
108
134
|
|
|
135
|
+
// Dev-mode warning: camelCase callback props are silently ignored in Svelte.
|
|
136
|
+
// Warn if the user passed onComplete/onCancel/onEvent instead of the correct
|
|
137
|
+
// lowercase forms oncomplete/oncancel/onevent.
|
|
138
|
+
if (import.meta.env?.DEV !== false) {
|
|
139
|
+
const camelCallbacks = ['onComplete', 'onCancel', 'onEvent'] as const;
|
|
140
|
+
for (const name of camelCallbacks) {
|
|
141
|
+
if (name in stepSnippets) {
|
|
142
|
+
console.warn(
|
|
143
|
+
`[PathShell] "${name}" was passed but will be ignored. Svelte uses lowercase callback props — use "${name.toLowerCase()}" instead.`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
109
149
|
// Auto-start the path when no external engine is provided
|
|
110
150
|
let started = false;
|
|
111
151
|
onMount(() => {
|
|
112
152
|
if (autoStart && !started && !engineProp) {
|
|
113
153
|
started = true;
|
|
114
|
-
|
|
154
|
+
let startData: PathData = initialData ?? {};
|
|
155
|
+
let restoreStepId: string | undefined;
|
|
156
|
+
if (restoreKey && outerCtx) {
|
|
157
|
+
const stored = outerCtx.snapshot?.data[restoreKey] as PathSnapshot<any> | undefined;
|
|
158
|
+
if (stored != null && typeof stored === 'object' && 'stepId' in stored) {
|
|
159
|
+
startData = stored.data as PathData;
|
|
160
|
+
if (stored.stepIndex > 0) restoreStepId = stored.stepId as string;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const p = start(path, startData);
|
|
164
|
+
if (restoreStepId) {
|
|
165
|
+
p.then(() => goToStep(restoreStepId!));
|
|
166
|
+
}
|
|
115
167
|
}
|
|
116
168
|
});
|
|
117
169
|
|
|
170
|
+
$effect(() => {
|
|
171
|
+
if (validateWhen) pathReturn.validate();
|
|
172
|
+
});
|
|
173
|
+
|
|
118
174
|
function warnMissingStep(stepId: string): void {
|
|
119
175
|
const camel = stepIdToCamelCase(stepId);
|
|
120
176
|
const hint = camel !== stepId
|
|
@@ -126,11 +182,14 @@
|
|
|
126
182
|
let snap = $derived(pathReturn.snapshot);
|
|
127
183
|
let actions = $derived({ next, previous, cancel, goToStep, goToStepChecked, setData, restart: () => restartFn(path, initialData), retry, suspend });
|
|
128
184
|
|
|
185
|
+
let effectiveHideProgress = $derived(hideProgress || layout === 'tabs');
|
|
186
|
+
let effectiveHideFooter = $derived(hideFooter || layout === 'tabs');
|
|
187
|
+
|
|
129
188
|
// Auto-detect footer layout: single-step top-level paths use "form", everything else uses "wizard"
|
|
130
189
|
let resolvedFooterLayout = $derived(
|
|
131
|
-
|
|
190
|
+
(layout === 'auto' || layout === 'tabs') && snap
|
|
132
191
|
? (snap.stepCount === 1 && snap.nestingLevel === 0 ? 'form' : 'wizard')
|
|
133
|
-
: (
|
|
192
|
+
: (layout === 'auto' || layout === 'tabs' ? 'wizard' : layout)
|
|
134
193
|
);
|
|
135
194
|
|
|
136
195
|
/**
|
|
@@ -157,9 +216,38 @@
|
|
|
157
216
|
</button>
|
|
158
217
|
{/if}
|
|
159
218
|
</div>
|
|
219
|
+
{:else if snap.status === 'completed'}
|
|
220
|
+
<!-- Completion panel: shown after stayOnFinal completion -->
|
|
221
|
+
{#if !effectiveHideProgress && snap.stepCount > 1}
|
|
222
|
+
<div class="pw-shell__header">
|
|
223
|
+
<div class="pw-shell__steps">
|
|
224
|
+
{#each snap.steps as step, i}
|
|
225
|
+
<div class="pw-shell__step pw-shell__step--{step.status}">
|
|
226
|
+
<span class="pw-shell__step-dot">✓</span>
|
|
227
|
+
<span class="pw-shell__step-label">{step.title ?? step.id}</span>
|
|
228
|
+
</div>
|
|
229
|
+
{/each}
|
|
230
|
+
</div>
|
|
231
|
+
<div class="pw-shell__track">
|
|
232
|
+
<div class="pw-shell__track-fill" style="width: 100%"></div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
{/if}
|
|
236
|
+
<div class="pw-shell__body">
|
|
237
|
+
{#if completion}
|
|
238
|
+
{@render completion(snap)}
|
|
239
|
+
{:else}
|
|
240
|
+
<div class="pw-shell__completion">
|
|
241
|
+
<p class="pw-shell__completion-message">All done.</p>
|
|
242
|
+
<button type="button" class="pw-shell__completion-restart" onclick={() => restartFn(path, initialData)}>
|
|
243
|
+
Start over
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
{/if}
|
|
247
|
+
</div>
|
|
160
248
|
{:else}
|
|
161
249
|
<!-- Root progress: persistent top-level bar visible during sub-paths -->
|
|
162
|
-
{#if !
|
|
250
|
+
{#if !effectiveHideProgress && snap.rootProgress && progressLayout !== 'activeOnly'}
|
|
163
251
|
<div class="pw-shell__root-progress">
|
|
164
252
|
<div class="pw-shell__steps">
|
|
165
253
|
{#each snap.rootProgress.steps as step, i}
|
|
@@ -178,7 +266,7 @@
|
|
|
178
266
|
{/if}
|
|
179
267
|
|
|
180
268
|
<!-- Header: progress indicator (overridable via header snippet) -->
|
|
181
|
-
{#if !
|
|
269
|
+
{#if !effectiveHideProgress && progressLayout !== 'rootOnly'}
|
|
182
270
|
{#if header}
|
|
183
271
|
{@render header(snap)}
|
|
184
272
|
{:else if snap.stepCount > 1 || snap.nestingLevel > 0}
|
|
@@ -223,7 +311,7 @@
|
|
|
223
311
|
</div>
|
|
224
312
|
|
|
225
313
|
<!-- Validation messages — suppressed when validationDisplay="inline" -->
|
|
226
|
-
{#if validationDisplay !== 'inline' && snap.hasAttemptedNext && Object.keys(snap.fieldErrors).length > 0}
|
|
314
|
+
{#if validationDisplay !== 'inline' && (snap.hasAttemptedNext || snap.hasValidated) && Object.keys(snap.fieldErrors).length > 0}
|
|
227
315
|
<ul class="pw-shell__validation">
|
|
228
316
|
{#each Object.entries(snap.fieldErrors) as [key, msg]}
|
|
229
317
|
<li class="pw-shell__validation-item">
|
|
@@ -245,7 +333,7 @@
|
|
|
245
333
|
{/if}
|
|
246
334
|
|
|
247
335
|
<!-- Blocking error — guard returned { allowed: false, reason } -->
|
|
248
|
-
{#if validationDisplay !== 'inline' && snap.hasAttemptedNext && snap.blockingError}
|
|
336
|
+
{#if validationDisplay !== 'inline' && (snap.hasAttemptedNext || snap.hasValidated) && snap.blockingError}
|
|
249
337
|
<p class="pw-shell__blocking-error">{snap.blockingError}</p>
|
|
250
338
|
{/if}
|
|
251
339
|
|
|
@@ -273,9 +361,9 @@
|
|
|
273
361
|
</div>
|
|
274
362
|
</div>
|
|
275
363
|
<!-- Footer: navigation buttons (overridable via footer snippet) -->
|
|
276
|
-
{:else if footer}
|
|
364
|
+
{:else if !effectiveHideFooter && footer}
|
|
277
365
|
{@render footer(snap, actions)}
|
|
278
|
-
{:else}
|
|
366
|
+
{:else if !effectiveHideFooter}
|
|
279
367
|
<div class="pw-shell__footer">
|
|
280
368
|
<div class="pw-shell__footer-left">
|
|
281
369
|
{#if resolvedFooterLayout === 'form' && !hideCancel}
|
|
@@ -4,6 +4,12 @@ interface Props {
|
|
|
4
4
|
path?: PathDefinition<any>;
|
|
5
5
|
engine?: PathEngine;
|
|
6
6
|
initialData?: PathData;
|
|
7
|
+
/**
|
|
8
|
+
* When set, this shell automatically saves its state into the nearest outer `PathShell`'s
|
|
9
|
+
* data under this key on every change, and restores from that stored state on remount.
|
|
10
|
+
* No-op when used on a top-level shell with no outer `PathShell` ancestor.
|
|
11
|
+
*/
|
|
12
|
+
restoreKey?: string;
|
|
7
13
|
autoStart?: boolean;
|
|
8
14
|
backLabel?: string;
|
|
9
15
|
nextLabel?: string;
|
|
@@ -12,13 +18,18 @@ interface Props {
|
|
|
12
18
|
cancelLabel?: string;
|
|
13
19
|
hideCancel?: boolean;
|
|
14
20
|
hideProgress?: boolean;
|
|
21
|
+
/** If true, hide the footer (navigation buttons). The error panel is still shown on async failure regardless of this prop. */
|
|
22
|
+
hideFooter?: boolean;
|
|
23
|
+
/** 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`. */
|
|
24
|
+
validateWhen?: boolean;
|
|
15
25
|
/**
|
|
16
|
-
*
|
|
26
|
+
* Shell layout mode:
|
|
17
27
|
* - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
|
|
18
|
-
* - "wizard": Back button on left, Cancel and Submit together on right.
|
|
19
|
-
* - "form": Cancel on left, Submit alone on right. Back button never shown.
|
|
28
|
+
* - "wizard": Progress header + Back button on left, Cancel and Submit together on right.
|
|
29
|
+
* - "form": Progress header + Cancel on left, Submit alone on right. Back button never shown.
|
|
30
|
+
* - "tabs": No progress header, no footer. Use for tabbed interfaces with a custom tab bar inside the step body.
|
|
20
31
|
*/
|
|
21
|
-
|
|
32
|
+
layout?: "wizard" | "form" | "auto" | "tabs";
|
|
22
33
|
/**
|
|
23
34
|
* Controls whether the shell renders its auto-generated field-error summary box.
|
|
24
35
|
* - `"summary"` (default): Shell renders the labeled error list below the step body.
|
|
@@ -44,6 +55,8 @@ interface Props {
|
|
|
44
55
|
onevent?: (event: any) => void;
|
|
45
56
|
header?: Snippet<[PathSnapshot<any>]>;
|
|
46
57
|
footer?: Snippet<[PathSnapshot<any>, object]>;
|
|
58
|
+
/** Snippet rendered when `snapshot.status === "completed"`. Defaults to a simple "All done." panel with a restart button. */
|
|
59
|
+
completion?: Snippet<[PathSnapshot<any>]>;
|
|
47
60
|
[key: string]: Component<any> | any;
|
|
48
61
|
}
|
|
49
62
|
declare const PathShell: Component<Props, {
|
|
@@ -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
|
|
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;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,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;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAC7C;;;;;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;IAC9C,6HAA6H;IAC7H,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAE1C,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;CACrC;AA0UH,QAAA,MAAM,SAAS;mBA7LQ,QAAQ,IAAI,CAAC;MA6LmB,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
package/dist/index.css
CHANGED
|
@@ -77,6 +77,31 @@
|
|
|
77
77
|
font-size: 14px;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/* ------------------------------------------------------------------ */
|
|
81
|
+
/* Completion panel */
|
|
82
|
+
/* ------------------------------------------------------------------ */
|
|
83
|
+
.pw-shell__completion {
|
|
84
|
+
text-align: center;
|
|
85
|
+
padding: 40px 16px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.pw-shell__completion-message {
|
|
89
|
+
font-size: 18px;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
color: var(--pw-color-text);
|
|
92
|
+
margin: 0 0 20px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.pw-shell__completion-restart {
|
|
96
|
+
border: 1px solid var(--pw-color-btn-border);
|
|
97
|
+
background: var(--pw-color-btn-bg);
|
|
98
|
+
color: var(--pw-color-text);
|
|
99
|
+
padding: var(--pw-btn-padding);
|
|
100
|
+
border-radius: var(--pw-btn-radius);
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
font-size: 14px;
|
|
103
|
+
}
|
|
104
|
+
|
|
80
105
|
/* ------------------------------------------------------------------ */
|
|
81
106
|
/* Root progress — persistent top-level bar visible during sub-paths */
|
|
82
107
|
/* ------------------------------------------------------------------ */
|
package/dist/index.svelte.d.ts
CHANGED
|
@@ -34,10 +34,14 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
34
34
|
previous: () => Promise<void>;
|
|
35
35
|
/** Cancel the active path (or sub-path). */
|
|
36
36
|
cancel: () => Promise<void>;
|
|
37
|
-
/** Jump directly to a step by ID. Calls onLeave / onEnter but bypasses guards and shouldSkip. */
|
|
38
|
-
goToStep: (stepId: string
|
|
37
|
+
/** Jump directly to a step by ID. Calls onLeave / onEnter but bypasses guards and shouldSkip. Pass `{ validateOnLeave: true }` to mark the departing step as attempted before navigating. */
|
|
38
|
+
goToStep: (stepId: string, options?: {
|
|
39
|
+
validateOnLeave?: boolean;
|
|
40
|
+
}) => Promise<void>;
|
|
39
41
|
/** Jump directly to a step by ID, checking the current step's canMoveNext (forward) or canMovePrevious (backward) guard first. Navigation is blocked if the guard returns false. */
|
|
40
|
-
goToStepChecked: (stepId: string
|
|
42
|
+
goToStepChecked: (stepId: string, options?: {
|
|
43
|
+
validateOnLeave?: boolean;
|
|
44
|
+
}) => Promise<void>;
|
|
41
45
|
/** Update a single data value; triggers a re-render via stateChanged. When `TData` is specified, `key` and `value` are type-checked against your data shape. */
|
|
42
46
|
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
43
47
|
/** Reset the current step's data to what it was when the step was entered. Useful for "Clear" or "Reset" buttons. */
|
|
@@ -52,6 +56,8 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
52
56
|
retry: () => Promise<void>;
|
|
53
57
|
/** Pauses the path with intent to return. Emits `suspended`. All state is preserved. */
|
|
54
58
|
suspend: () => Promise<void>;
|
|
59
|
+
/** Trigger inline validation on all steps without navigating. Sets `snapshot.hasValidated`. */
|
|
60
|
+
validate: () => void;
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
57
63
|
* Create a Pathwrite engine with Svelte 5 runes-based reactivity.
|
|
@@ -113,8 +119,12 @@ export interface PathContext<TData extends PathData = PathData, TServices = unkn
|
|
|
113
119
|
next: () => Promise<void>;
|
|
114
120
|
previous: () => Promise<void>;
|
|
115
121
|
cancel: () => Promise<void>;
|
|
116
|
-
goToStep: (stepId: string
|
|
117
|
-
|
|
122
|
+
goToStep: (stepId: string, options?: {
|
|
123
|
+
validateOnLeave?: boolean;
|
|
124
|
+
}) => Promise<void>;
|
|
125
|
+
goToStepChecked: (stepId: string, options?: {
|
|
126
|
+
validateOnLeave?: boolean;
|
|
127
|
+
}) => Promise<void>;
|
|
118
128
|
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
119
129
|
resetStep: () => Promise<void>;
|
|
120
130
|
restart: () => Promise<void>;
|
|
@@ -154,6 +164,13 @@ export declare function usePathContext<TData extends PathData = PathData, TServi
|
|
|
154
164
|
* Used by PathShell component.
|
|
155
165
|
*/
|
|
156
166
|
export declare function setPathContext<TData extends PathData = PathData, TServices = unknown>(ctx: PathContext<TData, TServices>): void;
|
|
167
|
+
/**
|
|
168
|
+
* Internal: Get the PathContext from the nearest ancestor PathShell, or
|
|
169
|
+
* `undefined` if no PathShell is present. Used by PathShell itself to access
|
|
170
|
+
* the outer shell's context for `restoreKey` auto-wiring — must be called
|
|
171
|
+
* before `setPathContext()` so it reads the parent rather than self.
|
|
172
|
+
*/
|
|
173
|
+
export declare function getPathContextOrNull<TData extends PathData = PathData, TServices = unknown>(): PathContext<TData, TServices> | undefined;
|
|
157
174
|
/**
|
|
158
175
|
* Create a two-way binding helper for form inputs.
|
|
159
176
|
* Returns an object with a reactive `value` property.
|
|
@@ -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,
|
|
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
|
@@ -69,7 +69,7 @@ export function usePath(options) {
|
|
|
69
69
|
_snapshot = event.snapshot;
|
|
70
70
|
}
|
|
71
71
|
else if (event.type === "completed" || event.type === "cancelled") {
|
|
72
|
-
_snapshot =
|
|
72
|
+
_snapshot = engine.snapshot();
|
|
73
73
|
}
|
|
74
74
|
options?.onEvent?.(event);
|
|
75
75
|
});
|
|
@@ -80,13 +80,14 @@ export function usePath(options) {
|
|
|
80
80
|
const next = () => engine.next();
|
|
81
81
|
const previous = () => engine.previous();
|
|
82
82
|
const cancel = () => engine.cancel();
|
|
83
|
-
const goToStep = (stepId) => engine.goToStep(stepId);
|
|
84
|
-
const goToStepChecked = (stepId) => engine.goToStepChecked(stepId);
|
|
83
|
+
const goToStep = (stepId, options) => engine.goToStep(stepId, options);
|
|
84
|
+
const goToStepChecked = (stepId, options) => engine.goToStepChecked(stepId, options);
|
|
85
85
|
const setData = ((key, value) => engine.setData(key, value));
|
|
86
86
|
const resetStep = () => engine.resetStep();
|
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -142,6 +144,15 @@ export function usePathContext() {
|
|
|
142
144
|
export function setPathContext(ctx) {
|
|
143
145
|
setContext(PATH_CONTEXT_KEY, ctx);
|
|
144
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Internal: Get the PathContext from the nearest ancestor PathShell, or
|
|
149
|
+
* `undefined` if no PathShell is present. Used by PathShell itself to access
|
|
150
|
+
* the outer shell's context for `restoreKey` auto-wiring — must be called
|
|
151
|
+
* before `setPathContext()` so it reads the parent rather than self.
|
|
152
|
+
*/
|
|
153
|
+
export function getPathContextOrNull() {
|
|
154
|
+
return getContext(PATH_CONTEXT_KEY);
|
|
155
|
+
}
|
|
145
156
|
// ---------------------------------------------------------------------------
|
|
146
157
|
// Helper for binding form inputs
|
|
147
158
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daltonr/pathwrite-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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.12.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, stepIdToCamelCase } from './index.svelte.js';
|
|
3
|
+
import { usePath, setPathContext, getPathContextOrNull, 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
|
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
path?: PathDefinition<any>;
|
|
10
10
|
engine?: PathEngine;
|
|
11
11
|
initialData?: PathData;
|
|
12
|
+
/**
|
|
13
|
+
* When set, this shell automatically saves its state into the nearest outer `PathShell`'s
|
|
14
|
+
* data under this key on every change, and restores from that stored state on remount.
|
|
15
|
+
* No-op when used on a top-level shell with no outer `PathShell` ancestor.
|
|
16
|
+
*/
|
|
17
|
+
restoreKey?: string;
|
|
12
18
|
autoStart?: boolean;
|
|
13
19
|
backLabel?: string;
|
|
14
20
|
nextLabel?: string;
|
|
@@ -17,13 +23,18 @@
|
|
|
17
23
|
cancelLabel?: string;
|
|
18
24
|
hideCancel?: boolean;
|
|
19
25
|
hideProgress?: boolean;
|
|
26
|
+
/** If true, hide the footer (navigation buttons). The error panel is still shown on async failure regardless of this prop. */
|
|
27
|
+
hideFooter?: boolean;
|
|
28
|
+
/** 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`. */
|
|
29
|
+
validateWhen?: boolean;
|
|
20
30
|
/**
|
|
21
|
-
*
|
|
31
|
+
* Shell layout mode:
|
|
22
32
|
* - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
|
|
23
|
-
* - "wizard": Back button on left, Cancel and Submit together on right.
|
|
24
|
-
* - "form": Cancel on left, Submit alone on right. Back button never shown.
|
|
33
|
+
* - "wizard": Progress header + Back button on left, Cancel and Submit together on right.
|
|
34
|
+
* - "form": Progress header + Cancel on left, Submit alone on right. Back button never shown.
|
|
35
|
+
* - "tabs": No progress header, no footer. Use for tabbed interfaces with a custom tab bar inside the step body.
|
|
25
36
|
*/
|
|
26
|
-
|
|
37
|
+
layout?: "wizard" | "form" | "auto" | "tabs";
|
|
27
38
|
/**
|
|
28
39
|
* Controls whether the shell renders its auto-generated field-error summary box.
|
|
29
40
|
* - `"summary"` (default): Shell renders the labeled error list below the step body.
|
|
@@ -51,6 +62,8 @@
|
|
|
51
62
|
// Optional override snippets for header and footer
|
|
52
63
|
header?: Snippet<[PathSnapshot<any>]>;
|
|
53
64
|
footer?: Snippet<[PathSnapshot<any>, object]>;
|
|
65
|
+
/** Snippet rendered when `snapshot.status === "completed"`. Defaults to a simple "All done." panel with a restart button. */
|
|
66
|
+
completion?: Snippet<[PathSnapshot<any>]>;
|
|
54
67
|
// All other props treated as step components keyed by step ID
|
|
55
68
|
[key: string]: Component<any> | any;
|
|
56
69
|
}
|
|
@@ -59,6 +72,7 @@
|
|
|
59
72
|
path,
|
|
60
73
|
engine: engineProp,
|
|
61
74
|
initialData = {},
|
|
75
|
+
restoreKey = undefined,
|
|
62
76
|
autoStart = true,
|
|
63
77
|
backLabel = 'Previous',
|
|
64
78
|
nextLabel = 'Next',
|
|
@@ -67,7 +81,9 @@
|
|
|
67
81
|
cancelLabel = 'Cancel',
|
|
68
82
|
hideCancel = false,
|
|
69
83
|
hideProgress = false,
|
|
70
|
-
|
|
84
|
+
hideFooter = false,
|
|
85
|
+
validateWhen = false,
|
|
86
|
+
layout = 'auto',
|
|
71
87
|
validationDisplay = 'summary',
|
|
72
88
|
progressLayout = 'merged',
|
|
73
89
|
services = null,
|
|
@@ -76,9 +92,14 @@
|
|
|
76
92
|
onevent,
|
|
77
93
|
header,
|
|
78
94
|
footer,
|
|
95
|
+
completion,
|
|
79
96
|
...stepSnippets
|
|
80
97
|
}: Props = $props();
|
|
81
98
|
|
|
99
|
+
// Read outer PathShell context BEFORE setting our own — gives access to
|
|
100
|
+
// parent shell's snapshot and setData for restoreKey auto-wiring.
|
|
101
|
+
const outerCtx = getPathContextOrNull();
|
|
102
|
+
|
|
82
103
|
// Initialize path engine
|
|
83
104
|
const pathReturn = usePath({
|
|
84
105
|
get engine() { return engineProp; },
|
|
@@ -86,6 +107,11 @@
|
|
|
86
107
|
onevent?.(event);
|
|
87
108
|
if (event.type === 'completed') oncomplete?.(event.data);
|
|
88
109
|
if (event.type === 'cancelled') oncancel?.(event.data);
|
|
110
|
+
if (restoreKey && outerCtx && event.type === 'stateChanged') {
|
|
111
|
+
(outerCtx.setData as unknown as (key: string, value: unknown) => Promise<void>)(
|
|
112
|
+
restoreKey, event.snapshot
|
|
113
|
+
);
|
|
114
|
+
}
|
|
89
115
|
}
|
|
90
116
|
});
|
|
91
117
|
|
|
@@ -106,15 +132,45 @@
|
|
|
106
132
|
get services() { return services; },
|
|
107
133
|
});
|
|
108
134
|
|
|
135
|
+
// Dev-mode warning: camelCase callback props are silently ignored in Svelte.
|
|
136
|
+
// Warn if the user passed onComplete/onCancel/onEvent instead of the correct
|
|
137
|
+
// lowercase forms oncomplete/oncancel/onevent.
|
|
138
|
+
if (import.meta.env?.DEV !== false) {
|
|
139
|
+
const camelCallbacks = ['onComplete', 'onCancel', 'onEvent'] as const;
|
|
140
|
+
for (const name of camelCallbacks) {
|
|
141
|
+
if (name in stepSnippets) {
|
|
142
|
+
console.warn(
|
|
143
|
+
`[PathShell] "${name}" was passed but will be ignored. Svelte uses lowercase callback props — use "${name.toLowerCase()}" instead.`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
109
149
|
// Auto-start the path when no external engine is provided
|
|
110
150
|
let started = false;
|
|
111
151
|
onMount(() => {
|
|
112
152
|
if (autoStart && !started && !engineProp) {
|
|
113
153
|
started = true;
|
|
114
|
-
|
|
154
|
+
let startData: PathData = initialData ?? {};
|
|
155
|
+
let restoreStepId: string | undefined;
|
|
156
|
+
if (restoreKey && outerCtx) {
|
|
157
|
+
const stored = outerCtx.snapshot?.data[restoreKey] as PathSnapshot<any> | undefined;
|
|
158
|
+
if (stored != null && typeof stored === 'object' && 'stepId' in stored) {
|
|
159
|
+
startData = stored.data as PathData;
|
|
160
|
+
if (stored.stepIndex > 0) restoreStepId = stored.stepId as string;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const p = start(path, startData);
|
|
164
|
+
if (restoreStepId) {
|
|
165
|
+
p.then(() => goToStep(restoreStepId!));
|
|
166
|
+
}
|
|
115
167
|
}
|
|
116
168
|
});
|
|
117
169
|
|
|
170
|
+
$effect(() => {
|
|
171
|
+
if (validateWhen) pathReturn.validate();
|
|
172
|
+
});
|
|
173
|
+
|
|
118
174
|
function warnMissingStep(stepId: string): void {
|
|
119
175
|
const camel = stepIdToCamelCase(stepId);
|
|
120
176
|
const hint = camel !== stepId
|
|
@@ -126,11 +182,14 @@
|
|
|
126
182
|
let snap = $derived(pathReturn.snapshot);
|
|
127
183
|
let actions = $derived({ next, previous, cancel, goToStep, goToStepChecked, setData, restart: () => restartFn(path, initialData), retry, suspend });
|
|
128
184
|
|
|
185
|
+
let effectiveHideProgress = $derived(hideProgress || layout === 'tabs');
|
|
186
|
+
let effectiveHideFooter = $derived(hideFooter || layout === 'tabs');
|
|
187
|
+
|
|
129
188
|
// Auto-detect footer layout: single-step top-level paths use "form", everything else uses "wizard"
|
|
130
189
|
let resolvedFooterLayout = $derived(
|
|
131
|
-
|
|
190
|
+
(layout === 'auto' || layout === 'tabs') && snap
|
|
132
191
|
? (snap.stepCount === 1 && snap.nestingLevel === 0 ? 'form' : 'wizard')
|
|
133
|
-
: (
|
|
192
|
+
: (layout === 'auto' || layout === 'tabs' ? 'wizard' : layout)
|
|
134
193
|
);
|
|
135
194
|
|
|
136
195
|
/**
|
|
@@ -157,9 +216,38 @@
|
|
|
157
216
|
</button>
|
|
158
217
|
{/if}
|
|
159
218
|
</div>
|
|
219
|
+
{:else if snap.status === 'completed'}
|
|
220
|
+
<!-- Completion panel: shown after stayOnFinal completion -->
|
|
221
|
+
{#if !effectiveHideProgress && snap.stepCount > 1}
|
|
222
|
+
<div class="pw-shell__header">
|
|
223
|
+
<div class="pw-shell__steps">
|
|
224
|
+
{#each snap.steps as step, i}
|
|
225
|
+
<div class="pw-shell__step pw-shell__step--{step.status}">
|
|
226
|
+
<span class="pw-shell__step-dot">✓</span>
|
|
227
|
+
<span class="pw-shell__step-label">{step.title ?? step.id}</span>
|
|
228
|
+
</div>
|
|
229
|
+
{/each}
|
|
230
|
+
</div>
|
|
231
|
+
<div class="pw-shell__track">
|
|
232
|
+
<div class="pw-shell__track-fill" style="width: 100%"></div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
{/if}
|
|
236
|
+
<div class="pw-shell__body">
|
|
237
|
+
{#if completion}
|
|
238
|
+
{@render completion(snap)}
|
|
239
|
+
{:else}
|
|
240
|
+
<div class="pw-shell__completion">
|
|
241
|
+
<p class="pw-shell__completion-message">All done.</p>
|
|
242
|
+
<button type="button" class="pw-shell__completion-restart" onclick={() => restartFn(path, initialData)}>
|
|
243
|
+
Start over
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
{/if}
|
|
247
|
+
</div>
|
|
160
248
|
{:else}
|
|
161
249
|
<!-- Root progress: persistent top-level bar visible during sub-paths -->
|
|
162
|
-
{#if !
|
|
250
|
+
{#if !effectiveHideProgress && snap.rootProgress && progressLayout !== 'activeOnly'}
|
|
163
251
|
<div class="pw-shell__root-progress">
|
|
164
252
|
<div class="pw-shell__steps">
|
|
165
253
|
{#each snap.rootProgress.steps as step, i}
|
|
@@ -178,7 +266,7 @@
|
|
|
178
266
|
{/if}
|
|
179
267
|
|
|
180
268
|
<!-- Header: progress indicator (overridable via header snippet) -->
|
|
181
|
-
{#if !
|
|
269
|
+
{#if !effectiveHideProgress && progressLayout !== 'rootOnly'}
|
|
182
270
|
{#if header}
|
|
183
271
|
{@render header(snap)}
|
|
184
272
|
{:else if snap.stepCount > 1 || snap.nestingLevel > 0}
|
|
@@ -223,7 +311,7 @@
|
|
|
223
311
|
</div>
|
|
224
312
|
|
|
225
313
|
<!-- Validation messages — suppressed when validationDisplay="inline" -->
|
|
226
|
-
{#if validationDisplay !== 'inline' && snap.hasAttemptedNext && Object.keys(snap.fieldErrors).length > 0}
|
|
314
|
+
{#if validationDisplay !== 'inline' && (snap.hasAttemptedNext || snap.hasValidated) && Object.keys(snap.fieldErrors).length > 0}
|
|
227
315
|
<ul class="pw-shell__validation">
|
|
228
316
|
{#each Object.entries(snap.fieldErrors) as [key, msg]}
|
|
229
317
|
<li class="pw-shell__validation-item">
|
|
@@ -245,7 +333,7 @@
|
|
|
245
333
|
{/if}
|
|
246
334
|
|
|
247
335
|
<!-- Blocking error — guard returned { allowed: false, reason } -->
|
|
248
|
-
{#if validationDisplay !== 'inline' && snap.hasAttemptedNext && snap.blockingError}
|
|
336
|
+
{#if validationDisplay !== 'inline' && (snap.hasAttemptedNext || snap.hasValidated) && snap.blockingError}
|
|
249
337
|
<p class="pw-shell__blocking-error">{snap.blockingError}</p>
|
|
250
338
|
{/if}
|
|
251
339
|
|
|
@@ -273,9 +361,9 @@
|
|
|
273
361
|
</div>
|
|
274
362
|
</div>
|
|
275
363
|
<!-- Footer: navigation buttons (overridable via footer snippet) -->
|
|
276
|
-
{:else if footer}
|
|
364
|
+
{:else if !effectiveHideFooter && footer}
|
|
277
365
|
{@render footer(snap, actions)}
|
|
278
|
-
{:else}
|
|
366
|
+
{:else if !effectiveHideFooter}
|
|
279
367
|
<div class="pw-shell__footer">
|
|
280
368
|
<div class="pw-shell__footer-left">
|
|
281
369
|
{#if resolvedFooterLayout === 'form' && !hideCancel}
|
package/src/index.svelte.ts
CHANGED
|
@@ -62,10 +62,10 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
62
62
|
previous: () => Promise<void>;
|
|
63
63
|
/** Cancel the active path (or sub-path). */
|
|
64
64
|
cancel: () => Promise<void>;
|
|
65
|
-
/** Jump directly to a step by ID. Calls onLeave / onEnter but bypasses guards and shouldSkip. */
|
|
66
|
-
goToStep: (stepId: string) => Promise<void>;
|
|
65
|
+
/** Jump directly to a step by ID. Calls onLeave / onEnter but bypasses guards and shouldSkip. Pass `{ validateOnLeave: true }` to mark the departing step as attempted before navigating. */
|
|
66
|
+
goToStep: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
67
67
|
/** Jump directly to a step by ID, checking the current step's canMoveNext (forward) or canMovePrevious (backward) guard first. Navigation is blocked if the guard returns false. */
|
|
68
|
-
goToStepChecked: (stepId: string) => Promise<void>;
|
|
68
|
+
goToStepChecked: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
69
69
|
/** Update a single data value; triggers a re-render via stateChanged. When `TData` is specified, `key` and `value` are type-checked against your data shape. */
|
|
70
70
|
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
71
71
|
/** Reset the current step's data to what it was when the step was entered. Useful for "Clear" or "Reset" buttons. */
|
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -155,7 +157,7 @@ export function usePath<TData extends PathData = PathData>(
|
|
|
155
157
|
if (event.type === "stateChanged" || event.type === "resumed") {
|
|
156
158
|
_snapshot = event.snapshot as PathSnapshot<TData>;
|
|
157
159
|
} else if (event.type === "completed" || event.type === "cancelled") {
|
|
158
|
-
_snapshot = null;
|
|
160
|
+
_snapshot = engine.snapshot() as PathSnapshot<TData> | null;
|
|
159
161
|
}
|
|
160
162
|
options?.onEvent?.(event);
|
|
161
163
|
});
|
|
@@ -176,8 +178,8 @@ export function usePath<TData extends PathData = PathData>(
|
|
|
176
178
|
const previous = (): Promise<void> => engine.previous();
|
|
177
179
|
const cancel = (): Promise<void> => engine.cancel();
|
|
178
180
|
|
|
179
|
-
const goToStep = (stepId: string): Promise<void> => engine.goToStep(stepId);
|
|
180
|
-
const goToStepChecked = (stepId: string): Promise<void> => engine.goToStepChecked(stepId);
|
|
181
|
+
const goToStep = (stepId: string, options?: { validateOnLeave?: boolean }): Promise<void> => engine.goToStep(stepId, options);
|
|
182
|
+
const goToStepChecked = (stepId: string, options?: { validateOnLeave?: boolean }): Promise<void> => engine.goToStepChecked(stepId, options);
|
|
181
183
|
|
|
182
184
|
const setData = (<K extends string & keyof TData>(key: K, value: TData[K]): Promise<void> =>
|
|
183
185
|
engine.setData(key, value as unknown)) as UsePathReturn<TData>["setData"];
|
|
@@ -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
|
|
|
@@ -216,8 +221,8 @@ export interface PathContext<TData extends PathData = PathData, TServices = unkn
|
|
|
216
221
|
next: () => Promise<void>;
|
|
217
222
|
previous: () => Promise<void>;
|
|
218
223
|
cancel: () => Promise<void>;
|
|
219
|
-
goToStep: (stepId: string) => Promise<void>;
|
|
220
|
-
goToStepChecked: (stepId: string) => Promise<void>;
|
|
224
|
+
goToStep: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
225
|
+
goToStepChecked: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
221
226
|
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
222
227
|
resetStep: () => Promise<void>;
|
|
223
228
|
restart: () => Promise<void>;
|
|
@@ -271,6 +276,16 @@ export function setPathContext<TData extends PathData = PathData, TServices = un
|
|
|
271
276
|
setContext(PATH_CONTEXT_KEY, ctx);
|
|
272
277
|
}
|
|
273
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Internal: Get the PathContext from the nearest ancestor PathShell, or
|
|
281
|
+
* `undefined` if no PathShell is present. Used by PathShell itself to access
|
|
282
|
+
* the outer shell's context for `restoreKey` auto-wiring — must be called
|
|
283
|
+
* before `setPathContext()` so it reads the parent rather than self.
|
|
284
|
+
*/
|
|
285
|
+
export function getPathContextOrNull<TData extends PathData = PathData, TServices = unknown>(): PathContext<TData, TServices> | undefined {
|
|
286
|
+
return getContext<PathContext<TData, TServices>>(PATH_CONTEXT_KEY);
|
|
287
|
+
}
|
|
288
|
+
|
|
274
289
|
// ---------------------------------------------------------------------------
|
|
275
290
|
// Helper for binding form inputs
|
|
276
291
|
// ---------------------------------------------------------------------------
|