@daltonr/pathwrite-svelte 0.13.0 → 0.14.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
@@ -22,8 +22,9 @@ Peer dependencies: Svelte 5+.
22
22
  import { applicationPath } from "./application-path";
23
23
  import DetailsStep from "./DetailsStep.svelte";
24
24
  import CoverNoteStep from "./CoverNoteStep.svelte";
25
+ import type { PathData } from "@daltonr/pathwrite-core";
25
26
 
26
- function handleComplete(data) {
27
+ function handleComplete(data: PathData) {
27
28
  console.log("Submitted:", data);
28
29
  }
29
30
  </script>
@@ -32,16 +33,8 @@ Peer dependencies: Svelte 5+.
32
33
  path={applicationPath}
33
34
  initialData={{ name: "", email: "", coverNote: "" }}
34
35
  oncomplete={handleComplete}
35
- >
36
- {#snippet details()}
37
- <DetailsStep />
38
- {/snippet}
39
-
40
- <!-- Step ID is "cover-note"; PathShell resolves the camelCase snippet automatically -->
41
- {#snippet coverNote()}
42
- <CoverNoteStep />
43
- {/snippet}
44
- </PathShell>
36
+ steps={{ details: DetailsStep, "cover-note": CoverNoteStep }}
37
+ />
45
38
  ```
46
39
 
47
40
  ```svelte
@@ -94,7 +87,9 @@ Peer dependencies: Svelte 5+.
94
87
 
95
88
  ## PathShell props
96
89
 
97
- Step content is supplied as Svelte 5 snippets whose names match each step's `id`. For hyphenated step IDs (e.g. `"cover-letter"`), pass the snippet as the camelCase prop (`coverLetter={...}`) PathShell resolves it automatically. A `console.warn` fires in development if no snippet is found under either the exact ID or the camelCase form.
90
+ Step content is supplied through the `steps` prop: a record of Svelte components keyed by step `id` (for a StepChoice, by the inner step's id, which is what `snapshot.formId` reports). The shell renders the active step's component with no props — step components reach the path through `usePathContext()`. Hyphenated step IDs can be used as-is (`"cover-letter": CoverLetterStep`) or in camelCase (`coverLetter: CoverLetterStep`); PathShell checks both. A `console.warn` fires in development if no component is found under either key.
91
+
92
+ `Props` has no index signature, so a misspelled prop (`nextLable`, `onComplete`) is a type error rather than a silently ignored attribute.
98
93
 
99
94
  | Prop | Type | Default | Description |
100
95
  |---|---|---|---|
@@ -116,6 +111,7 @@ Step content is supplied as Svelte 5 snippets whose names match each step's `id`
116
111
  | `validateWhen` | `boolean` | `false` | When `true` (including already at mount), calls `validate()` on the engine so all steps show inline errors at once. Bind to the outer snapshot's `hasAttemptedNext` when this shell is nested inside a step of an outer shell. |
117
112
  | `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. The stored value also carries the inner engine's serialized state, so a remount restores in place: no `onEnter` / `onLeave` re-run, attempted / visited state kept. |
118
113
  | `services` | `unknown` | `null` | Arbitrary services object available to step components via `usePathContext<TData, TServices>().services`. |
114
+ | `steps` | `Record<string, Component>` | `{}` | Step components keyed by step id (see above). Each is rendered without props. |
119
115
  | `oncomplete` | `(data: PathData) => void` | — | Called when the path finishes naturally. |
120
116
  | `oncancel` | `(data: PathData) => void` | — | Called when the path is cancelled. |
121
117
  | `onevent` | `(event: PathEvent) => void` | — | Called for every engine event. |
@@ -130,13 +126,11 @@ The component instance also exposes `restart()` for `bind:this` refs, which rest
130
126
  You can also replace the built-in header and footer with custom snippets:
131
127
 
132
128
  ```svelte
133
- <PathShell path={myPath}>
129
+ <PathShell path={myPath} steps={{ details: DetailsStep }}>
134
130
  {#snippet header(snap)}
135
131
  <p>Step {snap.stepIndex + 1} of {snap.stepCount}</p>
136
132
  {/snippet}
137
133
 
138
- {#snippet details()}<DetailsStep />{/snippet}
139
-
140
134
  {#snippet footer(snap, actions)}
141
135
  <button onclick={actions.previous} disabled={snap.isFirstStep}>Back</button>
142
136
  <button onclick={actions.next} disabled={!snap.canMoveNext}>
@@ -170,7 +164,7 @@ You can also replace the built-in header and footer with custom snippets:
170
164
  | Export | Description |
171
165
  |---|---|
172
166
  | `bindData(getSnapshot, setData, key)` | Two-way binding helper for inputs. Returns an object with a reactive `value` getter (reads `getSnapshot()?.data[key]`) and a `set(value)` method that calls `setData(key, value)`. Example: `const name = bindData(() => path.snapshot, path.setData, "name")`, then `<input value={name.value} oninput={(e) => name.set(e.currentTarget.value)} />`. |
173
- | `stepIdToCamelCase(id)` | Converts a hyphenated step ID to camelCase (`"cover-letter"` → `"coverLetter"`) — the conversion `<PathShell>` uses to resolve snippets for hyphenated step IDs. |
167
+ | `stepIdToCamelCase(id)` | Converts a hyphenated step ID to camelCase (`"cover-letter"` → `"coverLetter"`) — the fallback key `<PathShell>` tries in its `steps` record for hyphenated step IDs. |
174
168
  | `setPathContext(ctx)` | Sets the `PathContext` that `usePathContext()` reads, under the adapter's private `Symbol` key. Used internally by `<PathShell>`; only needed when building your own shell component. |
175
169
  | `getPathContextOrNull()` | Reads the nearest ancestor `PathContext`, or `undefined` when there is none. Used internally by `<PathShell>` to reach the outer shell for `restoreKey`; call it before `setPathContext()` so it reads the parent rather than self. |
176
170
  | `formatFieldKey`, `errorPhaseMessage` | Re-exported from `@daltonr/pathwrite-core` for building custom summaries and error panels. |
@@ -1,14 +1,28 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
3
- import { usePath, setPathContext, getPathContextOrNull, formatFieldKey, errorPhaseMessage, stepIdToCamelCase } from './index.svelte.js';
4
- import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout, PathShellActions } from './index.svelte.js';
5
- import { PathEngine as PathEngineClass } from '@daltonr/pathwrite-core';
6
- import type { SerializedPathState } from '@daltonr/pathwrite-core';
7
- import type { Snippet, Component } from 'svelte';
8
-
2
+ import { onMount } from "svelte";
3
+ import {
4
+ usePath,
5
+ setPathContext,
6
+ getPathContextOrNull,
7
+ formatFieldKey,
8
+ errorPhaseMessage,
9
+ stepIdToCamelCase,
10
+ } from "./index.svelte.js";
11
+ import type {
12
+ PathDefinition,
13
+ PathData,
14
+ PathEngine,
15
+ PathEvent,
16
+ PathSnapshot,
17
+ ProgressLayout,
18
+ PathShellActions,
19
+ } from "./index.svelte.js";
20
+ import { PathEngine as PathEngineClass } from "@daltonr/pathwrite-core";
21
+ import type { SerializedPathState } from "@daltonr/pathwrite-core";
22
+ import type { Snippet, Component } from "svelte";
9
23
 
10
24
  interface Props {
11
- path?: PathDefinition<any>;
25
+ path?: PathDefinition;
12
26
  engine?: PathEngine;
13
27
  initialData?: PathData;
14
28
  /**
@@ -57,17 +71,23 @@
57
71
  * Step components access it via `usePathContext<TData, TServices>()`.
58
72
  */
59
73
  services?: unknown;
74
+ /**
75
+ * Step components keyed by step id. The shell renders the entry for the
76
+ * active step (`snapshot.formId` first, for the inner step of a StepChoice,
77
+ * then `snapshot.stepId`) with no props — step components read the path
78
+ * through `usePathContext()`. Hyphenated ids may be given as-is
79
+ * (`"cover-letter"`) or in camelCase (`coverLetter`).
80
+ */
81
+ steps?: Record<string, Component>;
60
82
  // Callback props replace event dispatching in Svelte 5
61
83
  oncomplete?: (data: PathData) => void;
62
84
  oncancel?: (data: PathData) => void;
63
- onevent?: (event: any) => void;
85
+ onevent?: (event: PathEvent) => void;
64
86
  // Optional override snippets for header and footer
65
- header?: Snippet<[PathSnapshot<any>]>;
66
- footer?: Snippet<[PathSnapshot<any>, PathShellActions]>;
87
+ header?: Snippet<[PathSnapshot]>;
88
+ footer?: Snippet<[PathSnapshot, PathShellActions]>;
67
89
  /** Snippet rendered when `snapshot.status === "completed"`. Defaults to a simple "All done." panel with a restart button. */
68
- completion?: Snippet<[PathSnapshot<any>]>;
69
- // All other props treated as step components keyed by step ID
70
- [key: string]: Component<any> | any;
90
+ completion?: Snippet<[PathSnapshot]>;
71
91
  }
72
92
 
73
93
  let {
@@ -76,26 +96,29 @@
76
96
  initialData = {},
77
97
  restoreKey = undefined,
78
98
  autoStart = true,
79
- backLabel = 'Previous',
80
- nextLabel = 'Next',
81
- completeLabel = 'Complete',
99
+ backLabel = "Previous",
100
+ nextLabel = "Next",
101
+ completeLabel = "Complete",
82
102
  loadingLabel = undefined,
83
- cancelLabel = 'Cancel',
103
+ cancelLabel = "Cancel",
84
104
  hideCancel = false,
85
105
  hideProgress = false,
86
106
  hideFooter = false,
87
107
  validateWhen = false,
88
- layout = 'auto',
89
- validationDisplay = 'summary',
90
- progressLayout = 'merged',
108
+ layout = "auto",
109
+ validationDisplay = "summary",
110
+ progressLayout = "merged",
91
111
  services = null,
112
+ steps = {},
92
113
  oncomplete,
93
114
  oncancel,
94
115
  onevent,
95
116
  header,
96
117
  footer,
97
118
  completion,
98
- ...stepSnippets
119
+ // Not part of `Props` (which has no index signature, so a stray prop is a
120
+ // type error): kept only for the runtime camelCase-callback warning below.
121
+ ...rest
99
122
  }: Props = $props();
100
123
 
101
124
  // Read outer PathShell context BEFORE setting our own — gives access to
@@ -108,8 +131,9 @@
108
131
  // svelte-ignore state_referenced_locally — read once at init on purpose: restore happens at mount only
109
132
  const restoredEngine: PathEngine | null = (() => {
110
133
  if (engineProp || !restoreKey || !outerCtx || !path) return null;
111
- const stored = outerCtx.snapshot?.data[restoreKey] as { serializedState?: SerializedPathState } | undefined;
112
- if (!stored || typeof stored !== 'object' || !stored.serializedState) return null;
134
+ const stored = outerCtx.snapshot?.data[restoreKey] as
135
+ { serializedState?: SerializedPathState } | undefined;
136
+ if (!stored || typeof stored !== "object" || !stored.serializedState) return null;
113
137
  try {
114
138
  return PathEngineClass.fromState(stored.serializedState, { [path.id]: path });
115
139
  } catch {
@@ -123,24 +147,43 @@
123
147
 
124
148
  // Initialize path engine
125
149
  const pathReturn = usePath({
126
- get engine() { return currentEngine(); },
150
+ get engine() {
151
+ return currentEngine();
152
+ },
127
153
  onEvent: (event) => {
128
154
  onevent?.(event);
129
- if (event.type === 'completed') oncomplete?.(event.data);
130
- if (event.type === 'cancelled') oncancel?.(event.data);
131
- if (restoreKey && outerCtx && event.type === 'stateChanged') {
132
- (outerCtx.setData as unknown as (key: string, value: unknown) => Promise<void>)(
133
- restoreKey, { ...event.snapshot, serializedState: currentEngine().exportState() }
134
- );
155
+ if (event.type === "completed") oncomplete?.(event.data);
156
+ if (event.type === "cancelled") oncancel?.(event.data);
157
+ if (restoreKey && outerCtx && event.type === "stateChanged") {
158
+ void outerCtx.setData(restoreKey, {
159
+ ...event.snapshot,
160
+ serializedState: currentEngine().exportState(),
161
+ });
135
162
  }
136
- }
163
+ },
137
164
  });
138
165
 
139
- const { start, startSubPath, next, previous, cancel, goToStep, goToStepChecked, setData, resetStep, restart: restartFn, retry, suspend, validate } = pathReturn;
166
+ const {
167
+ start,
168
+ startSubPath,
169
+ next,
170
+ previous,
171
+ cancel,
172
+ goToStep,
173
+ goToStepChecked,
174
+ setData,
175
+ resetStep,
176
+ restart: restartFn,
177
+ retry,
178
+ suspend,
179
+ validate,
180
+ } = pathReturn;
140
181
 
141
182
  // Provide context for child step components
142
183
  setPathContext({
143
- get snapshot() { return pathReturn.snapshot; },
184
+ get snapshot() {
185
+ return pathReturn.snapshot;
186
+ },
144
187
  start,
145
188
  startSubPath,
146
189
  validate,
@@ -154,21 +197,25 @@
154
197
  restart: () => restartFn(),
155
198
  retry,
156
199
  suspend,
157
- get services() { return services; },
200
+ get services() {
201
+ return services;
202
+ },
158
203
  });
159
204
 
160
205
  // Dev-mode warning: camelCase callback props are silently ignored in Svelte.
161
206
  // Warn if the user passed onComplete/onCancel/onEvent instead of the correct
162
- // lowercase forms oncomplete/oncancel/onevent. Runs once, on mount (a closure
163
- // reading the props at the top level would only capture their initial value).
207
+ // lowercase forms oncomplete/oncancel/onevent (a type error in TypeScript,
208
+ // but plain JavaScript callers get no such hint). Runs once, on mount (a
209
+ // closure — reading the props at the top level would only capture their
210
+ // initial value).
164
211
  // `import.meta.env` is a bundler convention (Vite); the cast keeps this
165
212
  // package free of Vite's ambient types.
166
213
  const isDev = (import.meta as { env?: { DEV?: boolean } }).env?.DEV !== false;
167
214
  onMount(() => {
168
215
  if (!isDev) return;
169
- const camelCallbacks = ['onComplete', 'onCancel', 'onEvent'] as const;
216
+ const camelCallbacks = ["onComplete", "onCancel", "onEvent"] as const;
170
217
  for (const name of camelCallbacks) {
171
- if (name in stepSnippets) {
218
+ if (name in rest) {
172
219
  console.warn(
173
220
  `[PathShell] "${name}" was passed but will be ignored. Svelte uses lowercase callback props — use "${name.toLowerCase()}" instead.`
174
221
  );
@@ -185,8 +232,8 @@
185
232
  let startData: PathData = initialData ?? {};
186
233
  let restoreStepId: string | undefined;
187
234
  if (restoreKey && outerCtx) {
188
- const stored = outerCtx.snapshot?.data[restoreKey] as PathSnapshot<any> | undefined;
189
- if (stored != null && typeof stored === 'object' && 'stepId' in stored) {
235
+ const stored = outerCtx.snapshot?.data[restoreKey] as PathSnapshot | undefined;
236
+ if (stored != null && typeof stored === "object" && "stepId" in stored) {
190
237
  startData = stored.data as PathData;
191
238
  if (stored.stepIndex > 0) restoreStepId = stored.stepId as string;
192
239
  }
@@ -204,27 +251,38 @@
204
251
 
205
252
  function warnMissingStep(stepId: string): void {
206
253
  const camel = stepIdToCamelCase(stepId);
207
- const hint = camel !== stepId
208
- ? ` No snippet found for "${stepId}" or its camelCase form "${camel}". If your step ID contains hyphens, pass the snippet as a camelCase prop: ${camel}={YourComponent}.`
209
- : ` No snippet found for "${stepId}".`;
254
+ const hint =
255
+ camel !== stepId
256
+ ? ` No step component found for "${stepId}" or its camelCase form "${camel}". Pass it in the \`steps\` record under either key.`
257
+ : ` No step component found for "${stepId}". Pass it in the \`steps\` record.`;
210
258
  console.warn(`[PathShell]${hint}`);
211
259
  }
212
260
 
213
261
  let snap = $derived(pathReturn.snapshot);
214
262
  let actions: PathShellActions = $derived({
215
- next, previous, cancel, goToStep, goToStepChecked,
216
- setData: (key, value) => setData(key as never, value as never),
217
- restart: () => restartFn(), retry, suspend
263
+ next,
264
+ previous,
265
+ cancel,
266
+ goToStep,
267
+ goToStepChecked,
268
+ setData,
269
+ restart: () => restartFn(),
270
+ retry,
271
+ suspend,
218
272
  });
219
273
 
220
- let effectiveHideProgress = $derived(hideProgress || layout === 'tabs');
221
- let effectiveHideFooter = $derived(hideFooter || layout === 'tabs');
274
+ let effectiveHideProgress = $derived(hideProgress || layout === "tabs");
275
+ let effectiveHideFooter = $derived(hideFooter || layout === "tabs");
222
276
 
223
277
  // Auto-detect footer layout: single-step top-level paths use "form", everything else uses "wizard"
224
278
  let resolvedFooterLayout = $derived(
225
- (layout === 'auto' || layout === 'tabs') && snap
226
- ? (snap.stepCount === 1 && snap.nestingLevel === 0 ? 'form' : 'wizard')
227
- : (layout === 'auto' || layout === 'tabs' ? 'wizard' : layout)
279
+ (layout === "auto" || layout === "tabs") && snap
280
+ ? snap.stepCount === 1 && snap.nestingLevel === 0
281
+ ? "form"
282
+ : "wizard"
283
+ : layout === "auto" || layout === "tabs"
284
+ ? "wizard"
285
+ : layout
228
286
  );
229
287
 
230
288
  /**
@@ -251,7 +309,7 @@
251
309
  </button>
252
310
  {/if}
253
311
  </div>
254
- {:else if snap.status === 'completed'}
312
+ {:else if snap.status === "completed"}
255
313
  <!-- Completion panel: shown after stayOnFinal completion -->
256
314
  {#if !effectiveHideProgress && snap.stepCount > 1}
257
315
  <div class="pw-shell__header">
@@ -282,13 +340,13 @@
282
340
  </div>
283
341
  {:else}
284
342
  <!-- Root progress: persistent top-level bar visible during sub-paths -->
285
- {#if !effectiveHideProgress && snap.rootProgress && progressLayout !== 'activeOnly'}
343
+ {#if !effectiveHideProgress && snap.rootProgress && progressLayout !== "activeOnly"}
286
344
  <div class="pw-shell__root-progress">
287
345
  <div class="pw-shell__steps">
288
346
  {#each snap.rootProgress.steps as step, i}
289
347
  <div class="pw-shell__step pw-shell__step--{step.status}">
290
348
  <span class="pw-shell__step-dot">
291
- {step.status === 'completed' ? '' : i + 1}
349
+ {step.status === "completed" ? "" : i + 1}
292
350
  </span>
293
351
  <span class="pw-shell__step-label">{step.title ?? step.id}</span>
294
352
  </div>
@@ -301,7 +359,7 @@
301
359
  {/if}
302
360
 
303
361
  <!-- Header: progress indicator (overridable via header snippet) -->
304
- {#if !effectiveHideProgress && progressLayout !== 'rootOnly'}
362
+ {#if !effectiveHideProgress && progressLayout !== "rootOnly"}
305
363
  {#if header}
306
364
  {@render header(snap)}
307
365
  {:else if snap.stepCount > 1 || snap.nestingLevel > 0}
@@ -310,7 +368,7 @@
310
368
  {#each snap.steps as step, i}
311
369
  <div class="pw-shell__step pw-shell__step--{step.status}">
312
370
  <span class="pw-shell__step-dot">
313
- {step.status === 'completed' ? '' : i + 1}
371
+ {step.status === "completed" ? "" : i + 1}
314
372
  </span>
315
373
  <span class="pw-shell__step-label">{step.title ?? step.id}</span>
316
374
  </div>
@@ -323,21 +381,20 @@
323
381
  {/if}
324
382
  {/if}
325
383
 
326
- <!-- Body: current step rendered via named snippet.
384
+ <!-- Body: the active step's component from the `steps` record.
327
385
  Prefer formId (inner step id of a StepChoice) so consumers can
328
- register snippets by inner step ids directly.
329
- Hyphenated step IDs (e.g. "cover-letter") are normalised to camelCase
330
- ("coverLetter") as a fallback, since Svelte props must be valid JS
331
- identifiers. -->
386
+ register components by inner step ids directly.
387
+ Hyphenated step IDs (e.g. "cover-letter") also resolve under their
388
+ camelCase form ("coverLetter"). -->
332
389
  <div class="pw-shell__body">
333
- {#if snap.formId && stepSnippets[snap.formId]}
334
- {@const StepComponent = stepSnippets[snap.formId]}
390
+ {#if snap.formId && steps[snap.formId]}
391
+ {@const StepComponent = steps[snap.formId]}
335
392
  <StepComponent />
336
- {:else if stepSnippets[snap.stepId]}
337
- {@const StepComponent = stepSnippets[snap.stepId]}
393
+ {:else if steps[snap.stepId]}
394
+ {@const StepComponent = steps[snap.stepId]}
338
395
  <StepComponent />
339
- {:else if stepSnippets[stepIdToCamelCase(snap.formId ?? snap.stepId)]}
340
- {@const StepComponent = stepSnippets[stepIdToCamelCase(snap.formId ?? snap.stepId)]}
396
+ {:else if steps[stepIdToCamelCase(snap.formId ?? snap.stepId)]}
397
+ {@const StepComponent = steps[stepIdToCamelCase(snap.formId ?? snap.stepId)]}
341
398
  <StepComponent />
342
399
  {:else}
343
400
  {warnMissingStep(snap.stepId)}
@@ -346,29 +403,29 @@
346
403
  </div>
347
404
 
348
405
  <!-- Validation messages — suppressed when validationDisplay="inline" -->
349
- {#if validationDisplay !== 'inline' && (snap.hasAttemptedNext || snap.hasValidated) && Object.keys(snap.fieldErrors).length > 0}
406
+ {#if validationDisplay !== "inline" && (snap.hasAttemptedNext || snap.hasValidated) && Object.keys(snap.fieldErrors).length > 0}
350
407
  <ul class="pw-shell__validation">
351
408
  {#each Object.entries(snap.fieldErrors) as [key, msg]}
352
409
  <li class="pw-shell__validation-item">
353
- {#if key !== '_'}<span class="pw-shell__validation-label">{formatFieldKey(key)}</span>{/if}{msg}
410
+ {#if key !== "_"}<span class="pw-shell__validation-label">{formatFieldKey(key)}</span>{/if}{msg}
354
411
  </li>
355
412
  {/each}
356
413
  </ul>
357
414
  {/if}
358
415
 
359
416
  <!-- Warning messages — non-blocking, shown immediately (no hasAttemptedNext gate) -->
360
- {#if validationDisplay !== 'inline' && Object.keys(snap.fieldWarnings).length > 0}
417
+ {#if validationDisplay !== "inline" && Object.keys(snap.fieldWarnings).length > 0}
361
418
  <ul class="pw-shell__warnings">
362
419
  {#each Object.entries(snap.fieldWarnings) as [key, msg]}
363
420
  <li class="pw-shell__warnings-item">
364
- {#if key !== '_'}<span class="pw-shell__warnings-label">{formatFieldKey(key)}</span>{/if}{msg}
421
+ {#if key !== "_"}<span class="pw-shell__warnings-label">{formatFieldKey(key)}</span>{/if}{msg}
365
422
  </li>
366
423
  {/each}
367
424
  </ul>
368
425
  {/if}
369
426
 
370
427
  <!-- Blocking error — guard returned { allowed: false, reason } -->
371
- {#if validationDisplay !== 'inline' && (snap.hasAttemptedNext || snap.hasValidated) && snap.blockingError}
428
+ {#if validationDisplay !== "inline" && (snap.hasAttemptedNext || snap.hasValidated) && snap.blockingError}
372
429
  <p class="pw-shell__blocking-error">{snap.blockingError}</p>
373
430
  {/if}
374
431
 
@@ -377,31 +434,37 @@
377
434
  {@const err = snap.error}
378
435
  {@const escalated = err.retryCount >= 2}
379
436
  <div class="pw-shell__error">
380
- <div class="pw-shell__error-title">{escalated ? "Still having trouble." : "Something went wrong."}</div>
381
- <div class="pw-shell__error-message">{errorPhaseMessage(err.phase)}{err.message ? ` ${err.message}` : ""}</div>
437
+ <div class="pw-shell__error-title">
438
+ {escalated ? "Still having trouble." : "Something went wrong."}
439
+ </div>
440
+ <div class="pw-shell__error-message">
441
+ {errorPhaseMessage(err.phase)}{err.message ? ` ${err.message}` : ""}
442
+ </div>
382
443
  <div class="pw-shell__error-actions">
383
444
  {#if !escalated}
384
- <button type="button" class="pw-shell__btn pw-shell__btn--retry" onclick={retry}>Try again</button>
445
+ <button type="button" class="pw-shell__btn pw-shell__btn--retry" onclick={retry}>Try again</button
446
+ >
385
447
  {/if}
386
448
  {#if snap.hasPersistence}
387
449
  <button
388
450
  type="button"
389
451
  class="pw-shell__btn {escalated ? 'pw-shell__btn--retry' : 'pw-shell__btn--suspend'}"
390
- onclick={suspend}
391
- >Save and come back later</button>
452
+ onclick={suspend}>Save and come back later</button
453
+ >
392
454
  {/if}
393
455
  {#if escalated && !snap.hasPersistence}
394
- <button type="button" class="pw-shell__btn pw-shell__btn--retry" onclick={retry}>Try again</button>
456
+ <button type="button" class="pw-shell__btn pw-shell__btn--retry" onclick={retry}>Try again</button
457
+ >
395
458
  {/if}
396
459
  </div>
397
460
  </div>
398
- <!-- Footer: navigation buttons (overridable via footer snippet) -->
461
+ <!-- Footer: navigation buttons (overridable via footer snippet) -->
399
462
  {:else if !effectiveHideFooter && footer}
400
463
  {@render footer(snap, actions)}
401
464
  {:else if !effectiveHideFooter}
402
465
  <div class="pw-shell__footer">
403
466
  <div class="pw-shell__footer-left">
404
- {#if resolvedFooterLayout === 'form' && !hideCancel}
467
+ {#if resolvedFooterLayout === "form" && !hideCancel}
405
468
  <!-- Form mode: Cancel on the left -->
406
469
  <button
407
470
  type="button"
@@ -411,7 +474,7 @@
411
474
  >
412
475
  {cancelLabel}
413
476
  </button>
414
- {:else if resolvedFooterLayout === 'wizard' && !snap.isFirstStep}
477
+ {:else if resolvedFooterLayout === "wizard" && !snap.isFirstStep}
415
478
  <!-- Wizard mode: Back on the left -->
416
479
  <button
417
480
  type="button"
@@ -424,7 +487,7 @@
424
487
  {/if}
425
488
  </div>
426
489
  <div class="pw-shell__footer-right">
427
- {#if resolvedFooterLayout === 'wizard' && !hideCancel}
490
+ {#if resolvedFooterLayout === "wizard" && !hideCancel}
428
491
  <!-- Wizard mode: Cancel on the right -->
429
492
  <button
430
493
  type="button"
@@ -443,7 +506,11 @@
443
506
  disabled={snap.status !== "idle"}
444
507
  onclick={next}
445
508
  >
446
- {snap.status !== 'idle' && loadingLabel ? loadingLabel : snap.isLastStep ? completeLabel : nextLabel}
509
+ {snap.status !== "idle" && loadingLabel
510
+ ? loadingLabel
511
+ : snap.isLastStep
512
+ ? completeLabel
513
+ : nextLabel}
447
514
  </button>
448
515
  </div>
449
516
  </div>
@@ -1,7 +1,7 @@
1
- import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout, PathShellActions } from './index.svelte.js';
2
- import type { Snippet, Component } from 'svelte';
1
+ import type { PathDefinition, PathData, PathEngine, PathEvent, PathSnapshot, ProgressLayout, PathShellActions } from "./index.svelte.js";
2
+ import type { Snippet, Component } from "svelte";
3
3
  interface Props {
4
- path?: PathDefinition<any>;
4
+ path?: PathDefinition;
5
5
  engine?: PathEngine;
6
6
  initialData?: PathData;
7
7
  /**
@@ -50,14 +50,21 @@ interface Props {
50
50
  * Step components access it via `usePathContext<TData, TServices>()`.
51
51
  */
52
52
  services?: unknown;
53
+ /**
54
+ * Step components keyed by step id. The shell renders the entry for the
55
+ * active step (`snapshot.formId` first, for the inner step of a StepChoice,
56
+ * then `snapshot.stepId`) with no props — step components read the path
57
+ * through `usePathContext()`. Hyphenated ids may be given as-is
58
+ * (`"cover-letter"`) or in camelCase (`coverLetter`).
59
+ */
60
+ steps?: Record<string, Component>;
53
61
  oncomplete?: (data: PathData) => void;
54
62
  oncancel?: (data: PathData) => void;
55
- onevent?: (event: any) => void;
56
- header?: Snippet<[PathSnapshot<any>]>;
57
- footer?: Snippet<[PathSnapshot<any>, PathShellActions]>;
63
+ onevent?: (event: PathEvent) => void;
64
+ header?: Snippet<[PathSnapshot]>;
65
+ footer?: Snippet<[PathSnapshot, PathShellActions]>;
58
66
  /** Snippet rendered when `snapshot.status === "completed"`. Defaults to a simple "All done." panel with a restart button. */
59
- completion?: Snippet<[PathSnapshot<any>]>;
60
- [key: string]: Component<any> | any;
67
+ completion?: Snippet<[PathSnapshot]>;
61
68
  }
62
69
  declare const PathShell: Component<Props, {
63
70
  restart: () => Promise<void>;
@@ -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,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAG9H,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,gBAAgB,CAAC,CAAC,CAAC;IACxD,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;AA6WH,QAAA,MAAM,SAAS;mBA7LQ,QAAQ,IAAI,CAAC;MA6LmB,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":"AAYA,OAAO,KAAK,EACR,cAAc,EACd,QAAQ,EACR,UAAU,EACV,SAAS,EACT,YAAY,EACZ,cAAc,EACd,gBAAgB,EACjB,MAAM,mBAAmB,CAAC;AAG7B,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAG/C,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,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;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAElC,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,SAAS,KAAK,IAAI,CAAC;IAErC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,YAAY,EAAE,gBAAgB,CAAC,CAAC,CAAC;IACnD,6HAA6H;IAC7H,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;CACtC;AA2ZH,QAAA,MAAM,SAAS;mBArMQ,QAAQ,IAAI,CAAC;MAqMmB,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
package/dist/index.css CHANGED
@@ -53,7 +53,12 @@
53
53
  flex-direction: column;
54
54
  gap: var(--pw-shell-gap);
55
55
  padding: var(--pw-shell-padding);
56
- font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
56
+ font-family:
57
+ system-ui,
58
+ -apple-system,
59
+ "Segoe UI",
60
+ Roboto,
61
+ sans-serif;
57
62
  color: var(--pw-color-text);
58
63
  }
59
64
 
@@ -397,7 +402,9 @@
397
402
  border-radius: var(--pw-btn-radius);
398
403
  cursor: pointer;
399
404
  font-size: 14px;
400
- transition: background 0.15s ease, border-color 0.15s ease;
405
+ transition:
406
+ background 0.15s ease,
407
+ border-color 0.15s ease;
401
408
  }
402
409
 
403
410
  .pw-shell__btn:hover:not(:disabled) {
@@ -422,7 +429,9 @@
422
429
  }
423
430
 
424
431
  @keyframes pw-spin {
425
- to { transform: rotate(360deg); }
432
+ to {
433
+ transform: rotate(360deg);
434
+ }
426
435
  }
427
436
 
428
437
  .pw-shell__btn--next.pw-shell__btn--loading {
@@ -515,4 +524,3 @@
515
524
  .pw-shell__btn--suspend:hover:not(:disabled) {
516
525
  background: var(--pw-color-primary-light);
517
526
  }
518
-