@daltonr/pathwrite-svelte 0.11.0 → 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;
@@ -22,12 +28,13 @@
22
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`. */
23
29
  validateWhen?: boolean;
24
30
  /**
25
- * Footer layout mode:
31
+ * Shell layout mode:
26
32
  * - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
27
- * - "wizard": Back button on left, Cancel and Submit together on right.
28
- * - "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.
29
36
  */
30
- footerLayout?: "wizard" | "form" | "auto";
37
+ layout?: "wizard" | "form" | "auto" | "tabs";
31
38
  /**
32
39
  * Controls whether the shell renders its auto-generated field-error summary box.
33
40
  * - `"summary"` (default): Shell renders the labeled error list below the step body.
@@ -55,6 +62,8 @@
55
62
  // Optional override snippets for header and footer
56
63
  header?: Snippet<[PathSnapshot<any>]>;
57
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>]>;
58
67
  // All other props treated as step components keyed by step ID
59
68
  [key: string]: Component<any> | any;
60
69
  }
@@ -63,6 +72,7 @@
63
72
  path,
64
73
  engine: engineProp,
65
74
  initialData = {},
75
+ restoreKey = undefined,
66
76
  autoStart = true,
67
77
  backLabel = 'Previous',
68
78
  nextLabel = 'Next',
@@ -73,7 +83,7 @@
73
83
  hideProgress = false,
74
84
  hideFooter = false,
75
85
  validateWhen = false,
76
- footerLayout = 'auto',
86
+ layout = 'auto',
77
87
  validationDisplay = 'summary',
78
88
  progressLayout = 'merged',
79
89
  services = null,
@@ -82,9 +92,14 @@
82
92
  onevent,
83
93
  header,
84
94
  footer,
95
+ completion,
85
96
  ...stepSnippets
86
97
  }: Props = $props();
87
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
+
88
103
  // Initialize path engine
89
104
  const pathReturn = usePath({
90
105
  get engine() { return engineProp; },
@@ -92,6 +107,11 @@
92
107
  onevent?.(event);
93
108
  if (event.type === 'completed') oncomplete?.(event.data);
94
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
+ }
95
115
  }
96
116
  });
97
117
 
@@ -112,12 +132,38 @@
112
132
  get services() { return services; },
113
133
  });
114
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
+
115
149
  // Auto-start the path when no external engine is provided
116
150
  let started = false;
117
151
  onMount(() => {
118
152
  if (autoStart && !started && !engineProp) {
119
153
  started = true;
120
- 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
+ }
121
167
  }
122
168
  });
123
169
 
@@ -136,11 +182,14 @@
136
182
  let snap = $derived(pathReturn.snapshot);
137
183
  let actions = $derived({ next, previous, cancel, goToStep, goToStepChecked, setData, restart: () => restartFn(path, initialData), retry, suspend });
138
184
 
185
+ let effectiveHideProgress = $derived(hideProgress || layout === 'tabs');
186
+ let effectiveHideFooter = $derived(hideFooter || layout === 'tabs');
187
+
139
188
  // Auto-detect footer layout: single-step top-level paths use "form", everything else uses "wizard"
140
189
  let resolvedFooterLayout = $derived(
141
- footerLayout === 'auto' && snap
190
+ (layout === 'auto' || layout === 'tabs') && snap
142
191
  ? (snap.stepCount === 1 && snap.nestingLevel === 0 ? 'form' : 'wizard')
143
- : (footerLayout === 'auto' ? 'wizard' : footerLayout)
192
+ : (layout === 'auto' || layout === 'tabs' ? 'wizard' : layout)
144
193
  );
145
194
 
146
195
  /**
@@ -167,9 +216,38 @@
167
216
  </button>
168
217
  {/if}
169
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>
170
248
  {:else}
171
249
  <!-- Root progress: persistent top-level bar visible during sub-paths -->
172
- {#if !hideProgress && snap.rootProgress && progressLayout !== 'activeOnly'}
250
+ {#if !effectiveHideProgress && snap.rootProgress && progressLayout !== 'activeOnly'}
173
251
  <div class="pw-shell__root-progress">
174
252
  <div class="pw-shell__steps">
175
253
  {#each snap.rootProgress.steps as step, i}
@@ -188,7 +266,7 @@
188
266
  {/if}
189
267
 
190
268
  <!-- Header: progress indicator (overridable via header snippet) -->
191
- {#if !hideProgress && progressLayout !== 'rootOnly'}
269
+ {#if !effectiveHideProgress && progressLayout !== 'rootOnly'}
192
270
  {#if header}
193
271
  {@render header(snap)}
194
272
  {:else if snap.stepCount > 1 || snap.nestingLevel > 0}
@@ -283,9 +361,9 @@
283
361
  </div>
284
362
  </div>
285
363
  <!-- Footer: navigation buttons (overridable via footer snippet) -->
286
- {:else if !hideFooter && footer}
364
+ {:else if !effectiveHideFooter && footer}
287
365
  {@render footer(snap, actions)}
288
- {:else if !hideFooter}
366
+ {:else if !effectiveHideFooter}
289
367
  <div class="pw-shell__footer">
290
368
  <div class="pw-shell__footer-left">
291
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;
@@ -17,12 +23,13 @@ interface Props {
17
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`. */
18
24
  validateWhen?: boolean;
19
25
  /**
20
- * Footer layout mode:
26
+ * Shell layout mode:
21
27
  * - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
22
- * - "wizard": Back button on left, Cancel and Submit together on right.
23
- * - "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.
24
31
  */
25
- footerLayout?: "wizard" | "form" | "auto";
32
+ layout?: "wizard" | "form" | "auto" | "tabs";
26
33
  /**
27
34
  * Controls whether the shell renders its auto-generated field-error summary box.
28
35
  * - `"summary"` (default): Shell renders the labeled error list below the step body.
@@ -48,6 +55,8 @@ interface Props {
48
55
  onevent?: (event: any) => void;
49
56
  header?: Snippet<[PathSnapshot<any>]>;
50
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>]>;
51
60
  [key: string]: Component<any> | any;
52
61
  }
53
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,8HAA8H;IAC9H,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qNAAqN;IACrN,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IAC1C;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;IAClD;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IAE/B,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAE9C,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;CACrC;AAqQH,QAAA,MAAM,SAAS;mBAhKQ,QAAQ,IAAI,CAAC;MAgKmB,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
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. */
@@ -115,8 +119,12 @@ export interface PathContext<TData extends PathData = PathData, TServices = unkn
115
119
  next: () => Promise<void>;
116
120
  previous: () => Promise<void>;
117
121
  cancel: () => Promise<void>;
118
- goToStep: (stepId: string) => Promise<void>;
119
- 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>;
120
128
  setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
121
129
  resetStep: () => Promise<void>;
122
130
  restart: () => Promise<void>;
@@ -156,6 +164,13 @@ export declare function usePathContext<TData extends PathData = PathData, TServi
156
164
  * Used by PathShell component.
157
165
  */
158
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;
159
174
  /**
160
175
  * Create a two-way binding helper for form inputs.
161
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,8 +80,8 @@ 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();
@@ -144,6 +144,15 @@ export function usePathContext() {
144
144
  export function setPathContext(ctx) {
145
145
  setContext(PATH_CONTEXT_KEY, ctx);
146
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
+ }
147
156
  // ---------------------------------------------------------------------------
148
157
  // Helper for binding form inputs
149
158
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daltonr/pathwrite-svelte",
3
- "version": "0.11.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.11.0"
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;
@@ -22,12 +28,13 @@
22
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`. */
23
29
  validateWhen?: boolean;
24
30
  /**
25
- * Footer layout mode:
31
+ * Shell layout mode:
26
32
  * - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
27
- * - "wizard": Back button on left, Cancel and Submit together on right.
28
- * - "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.
29
36
  */
30
- footerLayout?: "wizard" | "form" | "auto";
37
+ layout?: "wizard" | "form" | "auto" | "tabs";
31
38
  /**
32
39
  * Controls whether the shell renders its auto-generated field-error summary box.
33
40
  * - `"summary"` (default): Shell renders the labeled error list below the step body.
@@ -55,6 +62,8 @@
55
62
  // Optional override snippets for header and footer
56
63
  header?: Snippet<[PathSnapshot<any>]>;
57
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>]>;
58
67
  // All other props treated as step components keyed by step ID
59
68
  [key: string]: Component<any> | any;
60
69
  }
@@ -63,6 +72,7 @@
63
72
  path,
64
73
  engine: engineProp,
65
74
  initialData = {},
75
+ restoreKey = undefined,
66
76
  autoStart = true,
67
77
  backLabel = 'Previous',
68
78
  nextLabel = 'Next',
@@ -73,7 +83,7 @@
73
83
  hideProgress = false,
74
84
  hideFooter = false,
75
85
  validateWhen = false,
76
- footerLayout = 'auto',
86
+ layout = 'auto',
77
87
  validationDisplay = 'summary',
78
88
  progressLayout = 'merged',
79
89
  services = null,
@@ -82,9 +92,14 @@
82
92
  onevent,
83
93
  header,
84
94
  footer,
95
+ completion,
85
96
  ...stepSnippets
86
97
  }: Props = $props();
87
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
+
88
103
  // Initialize path engine
89
104
  const pathReturn = usePath({
90
105
  get engine() { return engineProp; },
@@ -92,6 +107,11 @@
92
107
  onevent?.(event);
93
108
  if (event.type === 'completed') oncomplete?.(event.data);
94
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
+ }
95
115
  }
96
116
  });
97
117
 
@@ -112,12 +132,38 @@
112
132
  get services() { return services; },
113
133
  });
114
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
+
115
149
  // Auto-start the path when no external engine is provided
116
150
  let started = false;
117
151
  onMount(() => {
118
152
  if (autoStart && !started && !engineProp) {
119
153
  started = true;
120
- 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
+ }
121
167
  }
122
168
  });
123
169
 
@@ -136,11 +182,14 @@
136
182
  let snap = $derived(pathReturn.snapshot);
137
183
  let actions = $derived({ next, previous, cancel, goToStep, goToStepChecked, setData, restart: () => restartFn(path, initialData), retry, suspend });
138
184
 
185
+ let effectiveHideProgress = $derived(hideProgress || layout === 'tabs');
186
+ let effectiveHideFooter = $derived(hideFooter || layout === 'tabs');
187
+
139
188
  // Auto-detect footer layout: single-step top-level paths use "form", everything else uses "wizard"
140
189
  let resolvedFooterLayout = $derived(
141
- footerLayout === 'auto' && snap
190
+ (layout === 'auto' || layout === 'tabs') && snap
142
191
  ? (snap.stepCount === 1 && snap.nestingLevel === 0 ? 'form' : 'wizard')
143
- : (footerLayout === 'auto' ? 'wizard' : footerLayout)
192
+ : (layout === 'auto' || layout === 'tabs' ? 'wizard' : layout)
144
193
  );
145
194
 
146
195
  /**
@@ -167,9 +216,38 @@
167
216
  </button>
168
217
  {/if}
169
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>
170
248
  {:else}
171
249
  <!-- Root progress: persistent top-level bar visible during sub-paths -->
172
- {#if !hideProgress && snap.rootProgress && progressLayout !== 'activeOnly'}
250
+ {#if !effectiveHideProgress && snap.rootProgress && progressLayout !== 'activeOnly'}
173
251
  <div class="pw-shell__root-progress">
174
252
  <div class="pw-shell__steps">
175
253
  {#each snap.rootProgress.steps as step, i}
@@ -188,7 +266,7 @@
188
266
  {/if}
189
267
 
190
268
  <!-- Header: progress indicator (overridable via header snippet) -->
191
- {#if !hideProgress && progressLayout !== 'rootOnly'}
269
+ {#if !effectiveHideProgress && progressLayout !== 'rootOnly'}
192
270
  {#if header}
193
271
  {@render header(snap)}
194
272
  {:else if snap.stepCount > 1 || snap.nestingLevel > 0}
@@ -283,9 +361,9 @@
283
361
  </div>
284
362
  </div>
285
363
  <!-- Footer: navigation buttons (overridable via footer snippet) -->
286
- {:else if !hideFooter && footer}
364
+ {:else if !effectiveHideFooter && footer}
287
365
  {@render footer(snap, actions)}
288
- {:else if !hideFooter}
366
+ {:else if !effectiveHideFooter}
289
367
  <div class="pw-shell__footer">
290
368
  <div class="pw-shell__footer-left">
291
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. */
@@ -157,7 +157,7 @@ export function usePath<TData extends PathData = PathData>(
157
157
  if (event.type === "stateChanged" || event.type === "resumed") {
158
158
  _snapshot = event.snapshot as PathSnapshot<TData>;
159
159
  } else if (event.type === "completed" || event.type === "cancelled") {
160
- _snapshot = null;
160
+ _snapshot = engine.snapshot() as PathSnapshot<TData> | null;
161
161
  }
162
162
  options?.onEvent?.(event);
163
163
  });
@@ -178,8 +178,8 @@ export function usePath<TData extends PathData = PathData>(
178
178
  const previous = (): Promise<void> => engine.previous();
179
179
  const cancel = (): Promise<void> => engine.cancel();
180
180
 
181
- const goToStep = (stepId: string): Promise<void> => engine.goToStep(stepId);
182
- 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);
183
183
 
184
184
  const setData = (<K extends string & keyof TData>(key: K, value: TData[K]): Promise<void> =>
185
185
  engine.setData(key, value as unknown)) as UsePathReturn<TData>["setData"];
@@ -221,8 +221,8 @@ export interface PathContext<TData extends PathData = PathData, TServices = unkn
221
221
  next: () => Promise<void>;
222
222
  previous: () => Promise<void>;
223
223
  cancel: () => Promise<void>;
224
- goToStep: (stepId: string) => Promise<void>;
225
- goToStepChecked: (stepId: string) => Promise<void>;
224
+ goToStep: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
225
+ goToStepChecked: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
226
226
  setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
227
227
  resetStep: () => Promise<void>;
228
228
  restart: () => Promise<void>;
@@ -276,6 +276,16 @@ export function setPathContext<TData extends PathData = PathData, TServices = un
276
276
  setContext(PATH_CONTEXT_KEY, ctx);
277
277
  }
278
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
+
279
289
  // ---------------------------------------------------------------------------
280
290
  // Helper for binding form inputs
281
291
  // ---------------------------------------------------------------------------