@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 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
- | `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
+ | `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
- MIT — © 2026 Devjoy Ltd.
165
+ © 2026 Devjoy Ltd. MIT License.
@@ -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
- * Footer layout mode:
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
- footerLayout?: "wizard" | "form" | "auto";
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
- footerLayout = 'auto',
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
- start(path, initialData);
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
- footerLayout === 'auto' && snap
190
+ (layout === 'auto' || layout === 'tabs') && snap
132
191
  ? (snap.stepCount === 1 && snap.nestingLevel === 0 ? 'form' : 'wizard')
133
- : (footerLayout === 'auto' ? 'wizard' : footerLayout)
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 !hideProgress && snap.rootProgress && progressLayout !== 'activeOnly'}
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 !hideProgress && progressLayout !== 'rootOnly'}
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
- * Footer layout mode:
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
- footerLayout?: "wizard" | "form" | "auto";
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;;;;;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;AA+PH,QAAA,MAAM,SAAS;mBAhKQ,QAAQ,IAAI,CAAC;MAgKmB,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
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
  /* ------------------------------------------------------------------ */
@@ -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) => Promise<void>;
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) => Promise<void>;
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) => Promise<void>;
117
- goToStepChecked: (stepId: string) => Promise<void>;
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":"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"}
1
+ {"version":3,"file":"index.svelte.d.ts","sourceRoot":"","sources":["../src/index.svelte.ts"],"names":[],"mappings":"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"}
@@ -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 = null;
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.10.1",
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.10.1"
55
+ "@daltonr/pathwrite-core": "^0.12.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@sveltejs/package": "^2.5.7",
@@ -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
- * Footer layout mode:
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
- footerLayout?: "wizard" | "form" | "auto";
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
- footerLayout = 'auto',
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
- start(path, initialData);
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
- footerLayout === 'auto' && snap
190
+ (layout === 'auto' || layout === 'tabs') && snap
132
191
  ? (snap.stepCount === 1 && snap.nestingLevel === 0 ? 'form' : 'wizard')
133
- : (footerLayout === 'auto' ? 'wizard' : footerLayout)
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 !hideProgress && snap.rootProgress && progressLayout !== 'activeOnly'}
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 !hideProgress && progressLayout !== 'rootOnly'}
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}
@@ -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
  // ---------------------------------------------------------------------------