@daltonr/pathwrite-svelte 0.7.0 → 0.9.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
@@ -168,7 +168,7 @@ Create the engine yourself with `restoreOrStart()` and pass it via `engine`. Pat
168
168
 
169
169
  ```svelte
170
170
  <script>
171
- import { HttpStore, restoreOrStart, httpPersistence } from '@daltonr/pathwrite-store-http';
171
+ import { HttpStore, restoreOrStart, persistence } from '@daltonr/pathwrite-store';
172
172
 
173
173
  let engine = $state(null);
174
174
 
@@ -179,7 +179,7 @@ Create the engine yourself with `restoreOrStart()` and pass it via `engine`. Pat
179
179
  path: signupPath,
180
180
  initialData: { name: '' },
181
181
  observers: [
182
- httpPersistence({ store, key: 'user:onboarding', strategy: 'onNext' })
182
+ persistence({ store, key: 'user:onboarding', strategy: 'onNext' })
183
183
  ]
184
184
  });
185
185
  engine = result.engine;
@@ -467,13 +467,13 @@ const myPath = {
467
467
 
468
468
  ## Persistence
469
469
 
470
- Use with [@daltonr/pathwrite-store-http](../store-http) for automatic state persistence:
470
+ Use with [@daltonr/pathwrite-store](../store) for automatic state persistence:
471
471
 
472
472
  ```svelte
473
473
  <script lang="ts">
474
474
  import { onMount } from 'svelte';
475
475
  import { PathShell } from '@daltonr/pathwrite-svelte';
476
- import { HttpStore, restoreOrStart, httpPersistence } from '@daltonr/pathwrite-store-http';
476
+ import { HttpStore, restoreOrStart, persistence } from '@daltonr/pathwrite-store';
477
477
  import DetailsForm from './DetailsForm.svelte';
478
478
  import ReviewPanel from './ReviewPanel.svelte';
479
479
 
@@ -490,7 +490,7 @@ Use with [@daltonr/pathwrite-store-http](../store-http) for automatic state pers
490
490
  path: signupPath,
491
491
  initialData: { name: '', email: '' },
492
492
  observers: [
493
- httpPersistence({ store, key, strategy: 'onNext' })
493
+ persistence({ store, key, strategy: 'onNext' })
494
494
  ]
495
495
  });
496
496
  engine = result.engine;
@@ -537,7 +537,7 @@ MIT — © 2026 Devjoy Ltd.
537
537
  ## See Also
538
538
 
539
539
  - [@daltonr/pathwrite-core](../core) - Core engine
540
- - [@daltonr/pathwrite-store-http](../store-http) - HTTP persistence
540
+ - [@daltonr/pathwrite-store](../store) - HTTP persistence
541
541
  - [Documentation](../../docs/guides/DEVELOPER_GUIDE.md)
542
542
 
543
543
 
@@ -2,7 +2,7 @@
2
2
  import { onMount } from 'svelte';
3
3
  import { usePath, setPathContext } from './index.svelte.js';
4
4
  import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout } from './index.svelte.js';
5
- import type { Snippet } from 'svelte';
5
+ import type { Snippet, Component } from 'svelte';
6
6
 
7
7
  /** Converts a camelCase or lowercase field key to a display label. */
8
8
  function formatFieldKey(key: string): string {
@@ -49,8 +49,8 @@
49
49
  // Optional override snippets for header and footer
50
50
  header?: Snippet<[PathSnapshot<any>]>;
51
51
  footer?: Snippet<[PathSnapshot<any>, object]>;
52
- // All other props treated as step snippets keyed by step ID
53
- [key: string]: Snippet | any;
52
+ // All other props treated as step components keyed by step ID
53
+ [key: string]: Component<any> | any;
54
54
  }
55
55
 
56
56
  let {
@@ -65,7 +65,7 @@
65
65
  hideCancel = false,
66
66
  hideProgress = false,
67
67
  footerLayout = 'auto',
68
- validationDisplay = 'inline',
68
+ validationDisplay = 'summary',
69
69
  progressLayout = 'merged',
70
70
  oncomplete,
71
71
  oncancel,
@@ -185,19 +185,23 @@
185
185
  {/if}
186
186
  {/if}
187
187
 
188
- <!-- Body: current step rendered via named snippet -->
188
+ <!-- Body: current step rendered via named snippet.
189
+ Prefer formId (inner step id of a StepChoice) so consumers can
190
+ register snippets by inner step ids directly. -->
189
191
  <div class="pw-shell__body">
190
- {#if stepSnippets[snap.stepId]}
191
- {@render stepSnippets[snap.stepId]()}
192
+ {#if snap.formId && stepSnippets[snap.formId]}
193
+ <svelte:component this={stepSnippets[snap.formId]} />
194
+ {:else if stepSnippets[snap.stepId]}
195
+ <svelte:component this={stepSnippets[snap.stepId]} />
192
196
  {:else}
193
197
  <p>No content for step "{snap.stepId}"</p>
194
198
  {/if}
195
199
  </div>
196
200
 
197
201
  <!-- Validation messages — suppressed when validationDisplay="inline" -->
198
- {#if validationDisplay !== 'inline' && snap.hasAttemptedNext && Object.keys(snap.fieldMessages).length > 0}
202
+ {#if validationDisplay !== 'inline' && snap.hasAttemptedNext && Object.keys(snap.fieldErrors).length > 0}
199
203
  <ul class="pw-shell__validation">
200
- {#each Object.entries(snap.fieldMessages) as [key, msg]}
204
+ {#each Object.entries(snap.fieldErrors) as [key, msg]}
201
205
  <li class="pw-shell__validation-item">
202
206
  {#if key !== '_'}<span class="pw-shell__validation-label">{formatFieldKey(key)}</span>{/if}{msg}
203
207
  </li>
@@ -205,6 +209,17 @@
205
209
  </ul>
206
210
  {/if}
207
211
 
212
+ <!-- Warning messages — non-blocking, shown immediately (no hasAttemptedNext gate) -->
213
+ {#if validationDisplay !== 'inline' && Object.keys(snap.fieldWarnings).length > 0}
214
+ <ul class="pw-shell__warnings">
215
+ {#each Object.entries(snap.fieldWarnings) as [key, msg]}
216
+ <li class="pw-shell__warnings-item">
217
+ {#if key !== '_'}<span class="pw-shell__warnings-label">{formatFieldKey(key)}</span>{/if}{msg}
218
+ </li>
219
+ {/each}
220
+ </ul>
221
+ {/if}
222
+
208
223
  <!-- Footer: navigation buttons (overridable via footer snippet) -->
209
224
  {#if footer}
210
225
  {@render footer(snap, actions)}
@@ -1,5 +1,5 @@
1
1
  import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout } from './index.svelte.js';
2
- import type { Snippet } from 'svelte';
2
+ import type { Snippet, Component } from 'svelte';
3
3
  interface Props {
4
4
  path?: PathDefinition<any>;
5
5
  engine?: PathEngine;
@@ -38,9 +38,9 @@ interface Props {
38
38
  onevent?: (event: any) => void;
39
39
  header?: Snippet<[PathSnapshot<any>]>;
40
40
  footer?: Snippet<[PathSnapshot<any>, object]>;
41
- [key: string]: Snippet | any;
41
+ [key: string]: Component<any> | any;
42
42
  }
43
- declare const PathShell: import("svelte").Component<Props, {
43
+ declare const PathShell: Component<Props, {
44
44
  restart: () => Promise<void>;
45
45
  }, "">;
46
46
  type PathShell = ReturnType<typeof PathShell>;
@@ -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,MAAM,QAAQ,CAAC;AAGpC,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC;IAC3B,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;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;IAEhC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IAE/B,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAE9C,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,GAAG,CAAC;CAC9B;AA4MH,QAAA,MAAM,SAAS;mBArHQ,QAAQ,IAAI,CAAC;MAqHmB,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;AAG/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,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;IAEhC,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;AAyNH,QAAA,MAAM,SAAS;mBAlIQ,QAAQ,IAAI,CAAC;MAkImB,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
package/dist/index.css CHANGED
@@ -246,6 +246,9 @@
246
246
  --pw-color-error: #dc2626;
247
247
  --pw-color-error-bg: #fef2f2;
248
248
  --pw-color-error-border: #fecaca;
249
+ --pw-color-warning: #d97706;
250
+ --pw-color-warning-bg: #fffbeb;
251
+ --pw-color-warning-border: #fde68a;
249
252
  }
250
253
 
251
254
  .pw-shell__validation {
@@ -282,6 +285,43 @@
282
285
  content: ":";
283
286
  }
284
287
 
288
+ /* ------------------------------------------------------------------ */
289
+ /* Warning messages */
290
+ /* ------------------------------------------------------------------ */
291
+ .pw-shell__warnings {
292
+ list-style: none;
293
+ margin: 0;
294
+ padding: 12px 16px;
295
+ background: var(--pw-color-warning-bg);
296
+ border: 1px solid var(--pw-color-warning-border);
297
+ border-radius: var(--pw-shell-radius);
298
+ display: flex;
299
+ flex-direction: column;
300
+ gap: 4px;
301
+ }
302
+
303
+ .pw-shell__warnings-item {
304
+ font-size: 13px;
305
+ color: var(--pw-color-warning);
306
+ padding-left: 16px;
307
+ position: relative;
308
+ }
309
+
310
+ .pw-shell__warnings-item::before {
311
+ content: "•";
312
+ position: absolute;
313
+ left: 4px;
314
+ }
315
+
316
+ .pw-shell__warnings-label {
317
+ font-weight: 600;
318
+ margin-right: 3px;
319
+ }
320
+
321
+ .pw-shell__warnings-label::after {
322
+ content: ":";
323
+ }
324
+
285
325
  /* ------------------------------------------------------------------ */
286
326
  /* Footer — navigation buttons */
287
327
  /* ------------------------------------------------------------------ */
@@ -3,7 +3,7 @@ export type { PathData, FieldErrors, PathDefinition, PathEngine, PathEvent, Path
3
3
  export interface UsePathOptions {
4
4
  /**
5
5
  * An externally-managed `PathEngine` to subscribe to — for example, the engine
6
- * returned by `restoreOrStart()` from `@daltonr/pathwrite-store-http`.
6
+ * returned by `restoreOrStart()` from `@daltonr/pathwrite-store`.
7
7
  *
8
8
  * When provided:
9
9
  * - `usePath` will **not** create its own engine.
@@ -16,7 +16,12 @@ export interface UsePathOptions {
16
16
  onEvent?: (event: PathEvent) => void;
17
17
  }
18
18
  export interface UsePathReturn<TData extends PathData = PathData> {
19
- /** Current path snapshot, or `null` when no path is active. Reactive via `$state`. */
19
+ /**
20
+ * Current path snapshot, or `null` when no path is active. Reactive via `$state`.
21
+ *
22
+ * ⚠️ **Do not destructure.** `const { snapshot } = usePath()` captures the value
23
+ * once and loses reactivity. Always access as `path.snapshot`.
24
+ */
20
25
  readonly snapshot: PathSnapshot<TData> | null;
21
26
  /** Start (or restart) a path. */
22
27
  start: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
@@ -34,20 +39,49 @@ export interface UsePathReturn<TData extends PathData = PathData> {
34
39
  goToStepChecked: (stepId: string) => Promise<void>;
35
40
  /** 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. */
36
41
  setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
42
+ /** Reset the current step's data to what it was when the step was entered. Useful for "Clear" or "Reset" buttons. */
43
+ resetStep: () => Promise<void>;
37
44
  /**
38
45
  * Tear down any active path (without firing hooks) and immediately start the
39
46
  * given path fresh. Safe to call whether or not a path is currently active.
40
47
  * Use for "Start over" / retry flows without remounting the component.
41
48
  */
42
- restart: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
49
+ restart: () => Promise<void>;
43
50
  }
44
51
  /**
45
52
  * Create a Pathwrite engine with Svelte 5 runes-based reactivity.
46
53
  * Call this from inside a Svelte component to get a reactive snapshot.
47
54
  * Cleanup is automatic via onDestroy.
48
55
  *
49
- * **Note:** `snapshot` is a reactive getter — access it via the returned
50
- * object (e.g. `path.snapshot`). Destructuring `snapshot` will lose reactivity.
56
+ * ---
57
+ *
58
+ * ⚠️ **Do not destructure `snapshot`.**
59
+ *
60
+ * `snapshot` is a reactive getter. Destructuring it copies the value once
61
+ * and severs the reactive connection — your component will stop updating.
62
+ *
63
+ * ```svelte
64
+ * // ❌ Broken — snapshot is captured once and never updates
65
+ * const { snapshot, next } = usePath();
66
+ *
67
+ * // ✅ Correct — snapshot is read through the live object on every render
68
+ * const path = usePath();
69
+ * // use path.snapshot in your template
70
+ * ```
71
+ *
72
+ * Other properties (`next`, `previous`, `setData`, etc.) are plain functions
73
+ * and are safe to destructure.
74
+ *
75
+ * If you need a local variable that stays reactive, use `$derived`:
76
+ * ```svelte
77
+ * const path = usePath();
78
+ * const snapshot = $derived(path.snapshot);
79
+ * ```
80
+ *
81
+ * This is expected Svelte 5 behaviour — see the
82
+ * [Svelte $state docs](https://svelte.dev/docs/svelte/$state) for details.
83
+ *
84
+ * ---
51
85
  *
52
86
  * @example
53
87
  * ```svelte
@@ -70,13 +104,14 @@ export interface UsePathReturn<TData extends PathData = PathData> {
70
104
  */
71
105
  export declare function usePath<TData extends PathData = PathData>(options?: UsePathOptions): UsePathReturn<TData>;
72
106
  export interface PathContext<TData extends PathData = PathData> {
73
- readonly snapshot: PathSnapshot<TData> | null;
107
+ readonly snapshot: PathSnapshot<TData>;
74
108
  next: () => Promise<void>;
75
109
  previous: () => Promise<void>;
76
110
  cancel: () => Promise<void>;
77
111
  goToStep: (stepId: string) => Promise<void>;
78
112
  goToStepChecked: (stepId: string) => Promise<void>;
79
113
  setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
114
+ resetStep: () => Promise<void>;
80
115
  restart: () => Promise<void>;
81
116
  }
82
117
  /**
@@ -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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"}
@@ -8,8 +8,35 @@ import { PathEngine as PathEngineClass } from "@daltonr/pathwrite-core";
8
8
  * Call this from inside a Svelte component to get a reactive snapshot.
9
9
  * Cleanup is automatic via onDestroy.
10
10
  *
11
- * **Note:** `snapshot` is a reactive getter — access it via the returned
12
- * object (e.g. `path.snapshot`). Destructuring `snapshot` will lose reactivity.
11
+ * ---
12
+ *
13
+ * ⚠️ **Do not destructure `snapshot`.**
14
+ *
15
+ * `snapshot` is a reactive getter. Destructuring it copies the value once
16
+ * and severs the reactive connection — your component will stop updating.
17
+ *
18
+ * ```svelte
19
+ * // ❌ Broken — snapshot is captured once and never updates
20
+ * const { snapshot, next } = usePath();
21
+ *
22
+ * // ✅ Correct — snapshot is read through the live object on every render
23
+ * const path = usePath();
24
+ * // use path.snapshot in your template
25
+ * ```
26
+ *
27
+ * Other properties (`next`, `previous`, `setData`, etc.) are plain functions
28
+ * and are safe to destructure.
29
+ *
30
+ * If you need a local variable that stays reactive, use `$derived`:
31
+ * ```svelte
32
+ * const path = usePath();
33
+ * const snapshot = $derived(path.snapshot);
34
+ * ```
35
+ *
36
+ * This is expected Svelte 5 behaviour — see the
37
+ * [Svelte $state docs](https://svelte.dev/docs/svelte/$state) for details.
38
+ *
39
+ * ---
13
40
  *
14
41
  * @example
15
42
  * ```svelte
@@ -54,7 +81,8 @@ export function usePath(options) {
54
81
  const goToStep = (stepId) => engine.goToStep(stepId);
55
82
  const goToStepChecked = (stepId) => engine.goToStepChecked(stepId);
56
83
  const setData = ((key, value) => engine.setData(key, value));
57
- const restart = (path, initialData = {}) => engine.restart(path, initialData);
84
+ const resetStep = () => engine.resetStep();
85
+ const restart = () => engine.restart();
58
86
  return {
59
87
  get snapshot() { return _snapshot; },
60
88
  start,
@@ -65,6 +93,7 @@ export function usePath(options) {
65
93
  goToStep,
66
94
  goToStepChecked,
67
95
  setData,
96
+ resetStep,
68
97
  restart
69
98
  };
70
99
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daltonr/pathwrite-svelte",
3
- "version": "0.7.0",
3
+ "version": "0.9.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.7.0"
55
+ "@daltonr/pathwrite-core": "^0.9.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@sveltejs/package": "^2.5.7",
@@ -2,7 +2,7 @@
2
2
  import { onMount } from 'svelte';
3
3
  import { usePath, setPathContext } from './index.svelte.js';
4
4
  import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout } from './index.svelte.js';
5
- import type { Snippet } from 'svelte';
5
+ import type { Snippet, Component } from 'svelte';
6
6
 
7
7
  /** Converts a camelCase or lowercase field key to a display label. */
8
8
  function formatFieldKey(key: string): string {
@@ -49,8 +49,8 @@
49
49
  // Optional override snippets for header and footer
50
50
  header?: Snippet<[PathSnapshot<any>]>;
51
51
  footer?: Snippet<[PathSnapshot<any>, object]>;
52
- // All other props treated as step snippets keyed by step ID
53
- [key: string]: Snippet | any;
52
+ // All other props treated as step components keyed by step ID
53
+ [key: string]: Component<any> | any;
54
54
  }
55
55
 
56
56
  let {
@@ -65,7 +65,7 @@
65
65
  hideCancel = false,
66
66
  hideProgress = false,
67
67
  footerLayout = 'auto',
68
- validationDisplay = 'inline',
68
+ validationDisplay = 'summary',
69
69
  progressLayout = 'merged',
70
70
  oncomplete,
71
71
  oncancel,
@@ -185,19 +185,23 @@
185
185
  {/if}
186
186
  {/if}
187
187
 
188
- <!-- Body: current step rendered via named snippet -->
188
+ <!-- Body: current step rendered via named snippet.
189
+ Prefer formId (inner step id of a StepChoice) so consumers can
190
+ register snippets by inner step ids directly. -->
189
191
  <div class="pw-shell__body">
190
- {#if stepSnippets[snap.stepId]}
191
- {@render stepSnippets[snap.stepId]()}
192
+ {#if snap.formId && stepSnippets[snap.formId]}
193
+ <svelte:component this={stepSnippets[snap.formId]} />
194
+ {:else if stepSnippets[snap.stepId]}
195
+ <svelte:component this={stepSnippets[snap.stepId]} />
192
196
  {:else}
193
197
  <p>No content for step "{snap.stepId}"</p>
194
198
  {/if}
195
199
  </div>
196
200
 
197
201
  <!-- Validation messages — suppressed when validationDisplay="inline" -->
198
- {#if validationDisplay !== 'inline' && snap.hasAttemptedNext && Object.keys(snap.fieldMessages).length > 0}
202
+ {#if validationDisplay !== 'inline' && snap.hasAttemptedNext && Object.keys(snap.fieldErrors).length > 0}
199
203
  <ul class="pw-shell__validation">
200
- {#each Object.entries(snap.fieldMessages) as [key, msg]}
204
+ {#each Object.entries(snap.fieldErrors) as [key, msg]}
201
205
  <li class="pw-shell__validation-item">
202
206
  {#if key !== '_'}<span class="pw-shell__validation-label">{formatFieldKey(key)}</span>{/if}{msg}
203
207
  </li>
@@ -205,6 +209,17 @@
205
209
  </ul>
206
210
  {/if}
207
211
 
212
+ <!-- Warning messages — non-blocking, shown immediately (no hasAttemptedNext gate) -->
213
+ {#if validationDisplay !== 'inline' && Object.keys(snap.fieldWarnings).length > 0}
214
+ <ul class="pw-shell__warnings">
215
+ {#each Object.entries(snap.fieldWarnings) as [key, msg]}
216
+ <li class="pw-shell__warnings-item">
217
+ {#if key !== '_'}<span class="pw-shell__warnings-label">{formatFieldKey(key)}</span>{/if}{msg}
218
+ </li>
219
+ {/each}
220
+ </ul>
221
+ {/if}
222
+
208
223
  <!-- Footer: navigation buttons (overridable via footer snippet) -->
209
224
  {#if footer}
210
225
  {@render footer(snap, actions)}
@@ -30,7 +30,7 @@ export type {
30
30
  export interface UsePathOptions {
31
31
  /**
32
32
  * An externally-managed `PathEngine` to subscribe to — for example, the engine
33
- * returned by `restoreOrStart()` from `@daltonr/pathwrite-store-http`.
33
+ * returned by `restoreOrStart()` from `@daltonr/pathwrite-store`.
34
34
  *
35
35
  * When provided:
36
36
  * - `usePath` will **not** create its own engine.
@@ -44,7 +44,12 @@ export interface UsePathOptions {
44
44
  }
45
45
 
46
46
  export interface UsePathReturn<TData extends PathData = PathData> {
47
- /** Current path snapshot, or `null` when no path is active. Reactive via `$state`. */
47
+ /**
48
+ * Current path snapshot, or `null` when no path is active. Reactive via `$state`.
49
+ *
50
+ * ⚠️ **Do not destructure.** `const { snapshot } = usePath()` captures the value
51
+ * once and loses reactivity. Always access as `path.snapshot`.
52
+ */
48
53
  readonly snapshot: PathSnapshot<TData> | null;
49
54
  /** Start (or restart) a path. */
50
55
  start: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
@@ -62,12 +67,14 @@ export interface UsePathReturn<TData extends PathData = PathData> {
62
67
  goToStepChecked: (stepId: string) => Promise<void>;
63
68
  /** 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. */
64
69
  setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
70
+ /** Reset the current step's data to what it was when the step was entered. Useful for "Clear" or "Reset" buttons. */
71
+ resetStep: () => Promise<void>;
65
72
  /**
66
73
  * Tear down any active path (without firing hooks) and immediately start the
67
74
  * given path fresh. Safe to call whether or not a path is currently active.
68
75
  * Use for "Start over" / retry flows without remounting the component.
69
76
  */
70
- restart: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
77
+ restart: () => Promise<void>;
71
78
  }
72
79
 
73
80
  // ---------------------------------------------------------------------------
@@ -79,8 +86,35 @@ export interface UsePathReturn<TData extends PathData = PathData> {
79
86
  * Call this from inside a Svelte component to get a reactive snapshot.
80
87
  * Cleanup is automatic via onDestroy.
81
88
  *
82
- * **Note:** `snapshot` is a reactive getter — access it via the returned
83
- * object (e.g. `path.snapshot`). Destructuring `snapshot` will lose reactivity.
89
+ * ---
90
+ *
91
+ * ⚠️ **Do not destructure `snapshot`.**
92
+ *
93
+ * `snapshot` is a reactive getter. Destructuring it copies the value once
94
+ * and severs the reactive connection — your component will stop updating.
95
+ *
96
+ * ```svelte
97
+ * // ❌ Broken — snapshot is captured once and never updates
98
+ * const { snapshot, next } = usePath();
99
+ *
100
+ * // ✅ Correct — snapshot is read through the live object on every render
101
+ * const path = usePath();
102
+ * // use path.snapshot in your template
103
+ * ```
104
+ *
105
+ * Other properties (`next`, `previous`, `setData`, etc.) are plain functions
106
+ * and are safe to destructure.
107
+ *
108
+ * If you need a local variable that stays reactive, use `$derived`:
109
+ * ```svelte
110
+ * const path = usePath();
111
+ * const snapshot = $derived(path.snapshot);
112
+ * ```
113
+ *
114
+ * This is expected Svelte 5 behaviour — see the
115
+ * [Svelte $state docs](https://svelte.dev/docs/svelte/$state) for details.
116
+ *
117
+ * ---
84
118
  *
85
119
  * @example
86
120
  * ```svelte
@@ -143,8 +177,9 @@ export function usePath<TData extends PathData = PathData>(
143
177
  const setData = (<K extends string & keyof TData>(key: K, value: TData[K]): Promise<void> =>
144
178
  engine.setData(key, value as unknown)) as UsePathReturn<TData>["setData"];
145
179
 
146
- const restart = (path: PathDefinition<any>, initialData: PathData = {}): Promise<void> =>
147
- engine.restart(path, initialData);
180
+ const resetStep = (): Promise<void> => engine.resetStep();
181
+
182
+ const restart = (): Promise<void> => engine.restart();
148
183
 
149
184
  return {
150
185
  get snapshot() { return _snapshot; },
@@ -156,6 +191,7 @@ export function usePath<TData extends PathData = PathData>(
156
191
  goToStep,
157
192
  goToStepChecked,
158
193
  setData,
194
+ resetStep,
159
195
  restart
160
196
  };
161
197
  }
@@ -167,13 +203,14 @@ export function usePath<TData extends PathData = PathData>(
167
203
  const PATH_CONTEXT_KEY = Symbol("pathwrite-context");
168
204
 
169
205
  export interface PathContext<TData extends PathData = PathData> {
170
- readonly snapshot: PathSnapshot<TData> | null;
206
+ readonly snapshot: PathSnapshot<TData>;
171
207
  next: () => Promise<void>;
172
208
  previous: () => Promise<void>;
173
209
  cancel: () => Promise<void>;
174
210
  goToStep: (stepId: string) => Promise<void>;
175
211
  goToStepChecked: (stepId: string) => Promise<void>;
176
212
  setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
213
+ resetStep: () => Promise<void>;
177
214
  restart: () => Promise<void>;
178
215
  }
179
216