@daltonr/pathwrite-svelte 0.11.0 → 0.13.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 +38 -8
- package/dist/PathShell.svelte +135 -22
- package/dist/PathShell.svelte.d.ts +15 -6
- package/dist/PathShell.svelte.d.ts.map +1 -1
- package/dist/index.css +42 -0
- package/dist/index.svelte.d.ts +44 -15
- package/dist/index.svelte.d.ts.map +1 -1
- package/dist/index.svelte.js +32 -8
- package/package.json +8 -3
- package/src/PathShell.svelte +135 -22
- package/src/index.svelte.ts +69 -29
package/README.md
CHANGED
|
@@ -70,16 +70,20 @@ 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
|
-
| `restart(
|
|
75
|
+
| `restart()` | `Promise<void>` | Tear down any active path (without firing hooks) and restart the root path with the `initialData` from the original `start()`. Takes no arguments; rejects if nothing has been started. |
|
|
76
76
|
| `next()` | `Promise<void>` | Advance one step. Completes on the last step. |
|
|
77
77
|
| `previous()` | `Promise<void>` | Go back one step. No-op on the first step of a top-level path. |
|
|
78
78
|
| `cancel()` | `Promise<void>` | Cancel the active path (or sub-path). |
|
|
79
79
|
| `goToStep(stepId)` | `Promise<void>` | Jump to a step by ID. Calls `onLeave`/`onEnter`; bypasses guards. |
|
|
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
|
+
| `resetStep()` | `Promise<void>` | Restore the current step's data to what it was when the step was entered. Emits `stateChanged` with cause `"resetStep"`; no hooks run. |
|
|
83
|
+
| `retry()` | `Promise<void>` | Re-run the operation that set `snapshot.error`. Increments `retryCount` on repeated failure. No-op when there is no pending error. |
|
|
84
|
+
| `suspend()` | `Promise<void>` | Pause the path with intent to return. Emits `suspended`; all state and data are preserved. |
|
|
82
85
|
| `startSubPath(definition, data?, meta?)` | `Promise<void>` | Push a sub-path. `meta` is returned to `onSubPathComplete`/`onSubPathCancel`. |
|
|
86
|
+
| `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
87
|
|
|
84
88
|
**Options:**
|
|
85
89
|
|
|
@@ -95,19 +99,33 @@ Step content is supplied as Svelte 5 snippets whose names match each step's `id`
|
|
|
95
99
|
| Prop | Type | Default | Description |
|
|
96
100
|
|---|---|---|---|
|
|
97
101
|
| `path` | `PathDefinition` | — | Path to run. Mutually exclusive with `engine`. |
|
|
98
|
-
| `engine` | `PathEngine` | — | Externally-managed engine (e.g. from `restoreOrStart()`). Mutually exclusive with `path`. |
|
|
99
|
-
| `initialData` | `PathData` | `{}` | Initial data passed to `engine.start()`. |
|
|
102
|
+
| `engine` | `PathEngine` | — | Externally-managed engine (e.g. from `restoreOrStart()`). Mutually exclusive with `path`. May be provided after mount (e.g. once an async `restoreOrStart()` resolves): the shell adopts it, re-subscribing and re-seeding from the new engine. Set `autoStart` to `false` if the shell should not start its own path in the meantime. |
|
|
103
|
+
| `initialData` | `PathData` | `{}` | Initial data passed to `engine.start()`. Overridden by the stored snapshot when `restoreKey` is set. |
|
|
100
104
|
| `autoStart` | `boolean` | `true` | Start on mount. Ignored when `engine` is provided. |
|
|
101
|
-
| `
|
|
105
|
+
| `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. |
|
|
106
|
+
| `validationDisplay` | `"summary" \| "inline" \| "both"` | `"summary"` | Where `fieldErrors` are rendered. Use `"inline"` so step components render their own errors. |
|
|
107
|
+
| `progressLayout` | `"merged" \| "split" \| "rootOnly" \| "activeOnly"` | `"merged"` | How the root and sub-path progress bars are arranged while a sub-path is active. |
|
|
102
108
|
| `hideProgress` | `boolean` | `false` | Hide the progress indicator. Also hidden automatically for single-step paths. |
|
|
109
|
+
| `hideFooter` | `boolean` | `false` | Hide the footer (navigation buttons). The error panel is still shown on async failure. |
|
|
103
110
|
| `backLabel` | `string` | `"Previous"` | Previous button label. |
|
|
104
111
|
| `nextLabel` | `string` | `"Next"` | Next button label. |
|
|
105
112
|
| `completeLabel` | `string` | `"Complete"` | Complete button label (last step). |
|
|
113
|
+
| `loadingLabel` | `string` | `undefined` | Label for the Next/Complete button while an async operation is in progress. When unset, the button keeps its label and shows a CSS spinner. |
|
|
106
114
|
| `cancelLabel` | `string` | `"Cancel"` | Cancel button label. |
|
|
107
115
|
| `hideCancel` | `boolean` | `false` | Hide the Cancel button. |
|
|
116
|
+
| `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
|
+
| `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
|
+
| `services` | `unknown` | `null` | Arbitrary services object available to step components via `usePathContext<TData, TServices>().services`. |
|
|
108
119
|
| `oncomplete` | `(data: PathData) => void` | — | Called when the path finishes naturally. |
|
|
109
120
|
| `oncancel` | `(data: PathData) => void` | — | Called when the path is cancelled. |
|
|
110
121
|
| `onevent` | `(event: PathEvent) => void` | — | Called for every engine event. |
|
|
122
|
+
| `header` | `Snippet<[PathSnapshot]>` | — | Replaces the default progress header. A custom header is shown even for single-step paths, and hidden under `hideProgress` or `layout="tabs"`. |
|
|
123
|
+
| `footer` | `Snippet<[PathSnapshot, PathShellActions]>` | — | Replaces the default navigation footer. `actions` contains `next`, `previous`, `cancel`, `goToStep`, `goToStepChecked`, `setData`, `restart`, `retry`, `suspend`. |
|
|
124
|
+
| `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. |
|
|
125
|
+
|
|
126
|
+
The component instance also exposes `restart()` for `bind:this` refs, which restarts the path with its original `initialData` without remounting.
|
|
127
|
+
|
|
128
|
+
> **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
129
|
|
|
112
130
|
You can also replace the built-in header and footer with custom snippets:
|
|
113
131
|
|
|
@@ -130,7 +148,7 @@ You can also replace the built-in header and footer with custom snippets:
|
|
|
130
148
|
|
|
131
149
|
## usePathContext
|
|
132
150
|
|
|
133
|
-
`usePathContext<TData>()` is the preferred way for step components rendered inside `<PathShell>` to access the path engine. `<PathShell>` calls `setContext()` internally with a private `Symbol` key; `usePathContext()` calls the matching `getContext()` and returns the
|
|
151
|
+
`usePathContext<TData>()` is the preferred way for step components rendered inside `<PathShell>` to access the path engine. `<PathShell>` calls `setContext()` internally with a private `Symbol` key; `usePathContext()` calls the matching `getContext()` and returns `snapshot` (typed `PathSnapshot | null` — narrow with `{#if ctx.snapshot}`), the navigation actions (`next`, `previous`, `cancel`, `goToStep`, `goToStepChecked`, `setData`, `resetStep`, `restart`, `retry`, `suspend`) and `services`; it does not expose `start`, `startSubPath` or `validate`. It throws a clear error if called outside a `<PathShell>` — do not use Svelte's raw `getContext()` directly, as the key is a private `Symbol` and will silently return `undefined`. The context is the full `usePath()` return type (`PathContext` extends `UsePathReturn`), so `start`, `startSubPath` and `validate` are available to step components too — for example to launch a sub-path from a button inside a step.
|
|
134
152
|
|
|
135
153
|
```svelte
|
|
136
154
|
<script lang="ts">
|
|
@@ -147,12 +165,24 @@ You can also replace the built-in header and footer with custom snippets:
|
|
|
147
165
|
{/if}
|
|
148
166
|
```
|
|
149
167
|
|
|
168
|
+
## Other exports
|
|
169
|
+
|
|
170
|
+
| Export | Description |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `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. |
|
|
174
|
+
| `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
|
+
| `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
|
+
| `formatFieldKey`, `errorPhaseMessage` | Re-exported from `@daltonr/pathwrite-core` for building custom summaries and error panels. |
|
|
177
|
+
|
|
178
|
+
`PathEngine` is re-exported as a **type only**. To construct an engine, import the class from `@daltonr/pathwrite-core`.
|
|
179
|
+
|
|
150
180
|
## Further reading
|
|
151
181
|
|
|
152
182
|
- [Svelte getting started guide](../../docs/getting-started/frameworks/svelte.md)
|
|
153
|
-
- [Navigation & guards](../../docs/
|
|
183
|
+
- [Navigation & guards](../../docs/developer-guide/04-navigation.md)
|
|
154
184
|
- [Full documentation](../../docs/README.md)
|
|
155
185
|
|
|
156
186
|
---
|
|
157
187
|
|
|
158
|
-
|
|
188
|
+
© 2026 Devjoy Ltd. MIT License.
|
package/dist/PathShell.svelte
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
|
-
import { usePath, setPathContext, formatFieldKey, errorPhaseMessage, stepIdToCamelCase } from './index.svelte.js';
|
|
4
|
-
import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout } from './index.svelte.js';
|
|
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';
|
|
5
7
|
import type { Snippet, Component } from 'svelte';
|
|
6
8
|
|
|
7
9
|
|
|
@@ -9,6 +11,12 @@
|
|
|
9
11
|
path?: PathDefinition<any>;
|
|
10
12
|
engine?: PathEngine;
|
|
11
13
|
initialData?: PathData;
|
|
14
|
+
/**
|
|
15
|
+
* When set, this shell automatically saves its state into the nearest outer `PathShell`'s
|
|
16
|
+
* data under this key on every change, and restores from that stored state on remount.
|
|
17
|
+
* No-op when used on a top-level shell with no outer `PathShell` ancestor.
|
|
18
|
+
*/
|
|
19
|
+
restoreKey?: string;
|
|
12
20
|
autoStart?: boolean;
|
|
13
21
|
backLabel?: string;
|
|
14
22
|
nextLabel?: string;
|
|
@@ -22,12 +30,13 @@
|
|
|
22
30
|
/** 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
31
|
validateWhen?: boolean;
|
|
24
32
|
/**
|
|
25
|
-
*
|
|
33
|
+
* Shell layout mode:
|
|
26
34
|
* - "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.
|
|
35
|
+
* - "wizard": Progress header + Back button on left, Cancel and Submit together on right.
|
|
36
|
+
* - "form": Progress header + Cancel on left, Submit alone on right. Back button never shown.
|
|
37
|
+
* - "tabs": No progress header, no footer. Use for tabbed interfaces with a custom tab bar inside the step body.
|
|
29
38
|
*/
|
|
30
|
-
|
|
39
|
+
layout?: "wizard" | "form" | "auto" | "tabs";
|
|
31
40
|
/**
|
|
32
41
|
* Controls whether the shell renders its auto-generated field-error summary box.
|
|
33
42
|
* - `"summary"` (default): Shell renders the labeled error list below the step body.
|
|
@@ -54,7 +63,9 @@
|
|
|
54
63
|
onevent?: (event: any) => void;
|
|
55
64
|
// Optional override snippets for header and footer
|
|
56
65
|
header?: Snippet<[PathSnapshot<any>]>;
|
|
57
|
-
footer?: Snippet<[PathSnapshot<any>,
|
|
66
|
+
footer?: Snippet<[PathSnapshot<any>, PathShellActions]>;
|
|
67
|
+
/** Snippet rendered when `snapshot.status === "completed"`. Defaults to a simple "All done." panel with a restart button. */
|
|
68
|
+
completion?: Snippet<[PathSnapshot<any>]>;
|
|
58
69
|
// All other props treated as step components keyed by step ID
|
|
59
70
|
[key: string]: Component<any> | any;
|
|
60
71
|
}
|
|
@@ -63,6 +74,7 @@
|
|
|
63
74
|
path,
|
|
64
75
|
engine: engineProp,
|
|
65
76
|
initialData = {},
|
|
77
|
+
restoreKey = undefined,
|
|
66
78
|
autoStart = true,
|
|
67
79
|
backLabel = 'Previous',
|
|
68
80
|
nextLabel = 'Next',
|
|
@@ -73,7 +85,7 @@
|
|
|
73
85
|
hideProgress = false,
|
|
74
86
|
hideFooter = false,
|
|
75
87
|
validateWhen = false,
|
|
76
|
-
|
|
88
|
+
layout = 'auto',
|
|
77
89
|
validationDisplay = 'summary',
|
|
78
90
|
progressLayout = 'merged',
|
|
79
91
|
services = null,
|
|
@@ -82,42 +94,107 @@
|
|
|
82
94
|
onevent,
|
|
83
95
|
header,
|
|
84
96
|
footer,
|
|
97
|
+
completion,
|
|
85
98
|
...stepSnippets
|
|
86
99
|
}: Props = $props();
|
|
87
100
|
|
|
101
|
+
// Read outer PathShell context BEFORE setting our own — gives access to
|
|
102
|
+
// parent shell's snapshot and setData for restoreKey auto-wiring.
|
|
103
|
+
const outerCtx = getPathContextOrNull();
|
|
104
|
+
|
|
105
|
+
// When remounting under restoreKey, rebuild the inner engine from the state the
|
|
106
|
+
// previous instance exported, instead of starting the path and jumping to the
|
|
107
|
+
// step (which re-ran onEnter/onLeave and lost attempted / visited state).
|
|
108
|
+
// svelte-ignore state_referenced_locally — read once at init on purpose: restore happens at mount only
|
|
109
|
+
const restoredEngine: PathEngine | null = (() => {
|
|
110
|
+
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;
|
|
113
|
+
try {
|
|
114
|
+
return PathEngineClass.fromState(stored.serializedState, { [path.id]: path });
|
|
115
|
+
} catch {
|
|
116
|
+
return null; // unusable state (e.g. the path definition changed): start fresh below
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
// The shell's own engine is created once; an `engine` prop — present at
|
|
120
|
+
// mount or arriving later — always takes precedence and is adopted by usePath.
|
|
121
|
+
const ownEngine: PathEngine = restoredEngine ?? new PathEngineClass();
|
|
122
|
+
const currentEngine = (): PathEngine => engineProp ?? ownEngine;
|
|
123
|
+
|
|
88
124
|
// Initialize path engine
|
|
89
125
|
const pathReturn = usePath({
|
|
90
|
-
get engine() { return
|
|
126
|
+
get engine() { return currentEngine(); },
|
|
91
127
|
onEvent: (event) => {
|
|
92
128
|
onevent?.(event);
|
|
93
129
|
if (event.type === 'completed') oncomplete?.(event.data);
|
|
94
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
|
+
);
|
|
135
|
+
}
|
|
95
136
|
}
|
|
96
137
|
});
|
|
97
138
|
|
|
98
|
-
const { start, next, previous, cancel, goToStep, goToStepChecked, setData, restart: restartFn, retry, suspend } = pathReturn;
|
|
139
|
+
const { start, startSubPath, next, previous, cancel, goToStep, goToStepChecked, setData, resetStep, restart: restartFn, retry, suspend, validate } = pathReturn;
|
|
99
140
|
|
|
100
141
|
// Provide context for child step components
|
|
101
142
|
setPathContext({
|
|
102
143
|
get snapshot() { return pathReturn.snapshot; },
|
|
144
|
+
start,
|
|
145
|
+
startSubPath,
|
|
146
|
+
validate,
|
|
103
147
|
next,
|
|
104
148
|
previous,
|
|
105
149
|
cancel,
|
|
106
150
|
goToStep,
|
|
107
151
|
goToStepChecked,
|
|
108
152
|
setData,
|
|
109
|
-
|
|
153
|
+
resetStep,
|
|
154
|
+
restart: () => restartFn(),
|
|
110
155
|
retry,
|
|
111
156
|
suspend,
|
|
112
157
|
get services() { return services; },
|
|
113
158
|
});
|
|
114
159
|
|
|
160
|
+
// Dev-mode warning: camelCase callback props are silently ignored in Svelte.
|
|
161
|
+
// 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).
|
|
164
|
+
// `import.meta.env` is a bundler convention (Vite); the cast keeps this
|
|
165
|
+
// package free of Vite's ambient types.
|
|
166
|
+
const isDev = (import.meta as { env?: { DEV?: boolean } }).env?.DEV !== false;
|
|
167
|
+
onMount(() => {
|
|
168
|
+
if (!isDev) return;
|
|
169
|
+
const camelCallbacks = ['onComplete', 'onCancel', 'onEvent'] as const;
|
|
170
|
+
for (const name of camelCallbacks) {
|
|
171
|
+
if (name in stepSnippets) {
|
|
172
|
+
console.warn(
|
|
173
|
+
`[PathShell] "${name}" was passed but will be ignored. Svelte uses lowercase callback props — use "${name.toLowerCase()}" instead.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
115
179
|
// Auto-start the path when no external engine is provided
|
|
116
180
|
let started = false;
|
|
117
181
|
onMount(() => {
|
|
118
|
-
if (autoStart && !started && !engineProp) {
|
|
182
|
+
if (autoStart && !started && !engineProp && !restoredEngine) {
|
|
183
|
+
if (!path) throw new Error('[PathShell] "path" is required when no "engine" is provided');
|
|
119
184
|
started = true;
|
|
120
|
-
|
|
185
|
+
let startData: PathData = initialData ?? {};
|
|
186
|
+
let restoreStepId: string | undefined;
|
|
187
|
+
if (restoreKey && outerCtx) {
|
|
188
|
+
const stored = outerCtx.snapshot?.data[restoreKey] as PathSnapshot<any> | undefined;
|
|
189
|
+
if (stored != null && typeof stored === 'object' && 'stepId' in stored) {
|
|
190
|
+
startData = stored.data as PathData;
|
|
191
|
+
if (stored.stepIndex > 0) restoreStepId = stored.stepId as string;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const p = start(path, startData);
|
|
195
|
+
if (restoreStepId) {
|
|
196
|
+
p.then(() => goToStep(restoreStepId!));
|
|
197
|
+
}
|
|
121
198
|
}
|
|
122
199
|
});
|
|
123
200
|
|
|
@@ -134,13 +211,20 @@
|
|
|
134
211
|
}
|
|
135
212
|
|
|
136
213
|
let snap = $derived(pathReturn.snapshot);
|
|
137
|
-
let actions = $derived({
|
|
214
|
+
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
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
let effectiveHideProgress = $derived(hideProgress || layout === 'tabs');
|
|
221
|
+
let effectiveHideFooter = $derived(hideFooter || layout === 'tabs');
|
|
138
222
|
|
|
139
223
|
// Auto-detect footer layout: single-step top-level paths use "form", everything else uses "wizard"
|
|
140
224
|
let resolvedFooterLayout = $derived(
|
|
141
|
-
|
|
225
|
+
(layout === 'auto' || layout === 'tabs') && snap
|
|
142
226
|
? (snap.stepCount === 1 && snap.nestingLevel === 0 ? 'form' : 'wizard')
|
|
143
|
-
: (
|
|
227
|
+
: (layout === 'auto' || layout === 'tabs' ? 'wizard' : layout)
|
|
144
228
|
);
|
|
145
229
|
|
|
146
230
|
/**
|
|
@@ -153,7 +237,7 @@
|
|
|
153
237
|
* ```
|
|
154
238
|
*/
|
|
155
239
|
export function restart(): Promise<void> {
|
|
156
|
-
return pathReturn.restart(
|
|
240
|
+
return pathReturn.restart();
|
|
157
241
|
}
|
|
158
242
|
</script>
|
|
159
243
|
|
|
@@ -162,14 +246,43 @@
|
|
|
162
246
|
<div class="pw-shell__empty">
|
|
163
247
|
<p>No active path.</p>
|
|
164
248
|
{#if !autoStart}
|
|
165
|
-
<button type="button" class="pw-shell__start-btn" onclick={() => start(path, initialData)}>
|
|
249
|
+
<button type="button" class="pw-shell__start-btn" onclick={() => path && start(path, initialData)}>
|
|
166
250
|
Start
|
|
167
251
|
</button>
|
|
168
252
|
{/if}
|
|
169
253
|
</div>
|
|
254
|
+
{:else if snap.status === 'completed'}
|
|
255
|
+
<!-- Completion panel: shown after stayOnFinal completion -->
|
|
256
|
+
{#if !effectiveHideProgress && snap.stepCount > 1}
|
|
257
|
+
<div class="pw-shell__header">
|
|
258
|
+
<div class="pw-shell__steps">
|
|
259
|
+
{#each snap.steps as step, i}
|
|
260
|
+
<div class="pw-shell__step pw-shell__step--{step.status}">
|
|
261
|
+
<span class="pw-shell__step-dot">✓</span>
|
|
262
|
+
<span class="pw-shell__step-label">{step.title ?? step.id}</span>
|
|
263
|
+
</div>
|
|
264
|
+
{/each}
|
|
265
|
+
</div>
|
|
266
|
+
<div class="pw-shell__track">
|
|
267
|
+
<div class="pw-shell__track-fill" style="width: 100%"></div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
{/if}
|
|
271
|
+
<div class="pw-shell__body">
|
|
272
|
+
{#if completion}
|
|
273
|
+
{@render completion(snap)}
|
|
274
|
+
{:else}
|
|
275
|
+
<div class="pw-shell__completion">
|
|
276
|
+
<p class="pw-shell__completion-message">All done.</p>
|
|
277
|
+
<button type="button" class="pw-shell__completion-restart" onclick={() => restartFn()}>
|
|
278
|
+
Start over
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
{/if}
|
|
282
|
+
</div>
|
|
170
283
|
{:else}
|
|
171
284
|
<!-- Root progress: persistent top-level bar visible during sub-paths -->
|
|
172
|
-
{#if !
|
|
285
|
+
{#if !effectiveHideProgress && snap.rootProgress && progressLayout !== 'activeOnly'}
|
|
173
286
|
<div class="pw-shell__root-progress">
|
|
174
287
|
<div class="pw-shell__steps">
|
|
175
288
|
{#each snap.rootProgress.steps as step, i}
|
|
@@ -188,7 +301,7 @@
|
|
|
188
301
|
{/if}
|
|
189
302
|
|
|
190
303
|
<!-- Header: progress indicator (overridable via header snippet) -->
|
|
191
|
-
{#if !
|
|
304
|
+
{#if !effectiveHideProgress && progressLayout !== 'rootOnly'}
|
|
192
305
|
{#if header}
|
|
193
306
|
{@render header(snap)}
|
|
194
307
|
{:else if snap.stepCount > 1 || snap.nestingLevel > 0}
|
|
@@ -283,9 +396,9 @@
|
|
|
283
396
|
</div>
|
|
284
397
|
</div>
|
|
285
398
|
<!-- Footer: navigation buttons (overridable via footer snippet) -->
|
|
286
|
-
{:else if !
|
|
399
|
+
{:else if !effectiveHideFooter && footer}
|
|
287
400
|
{@render footer(snap, actions)}
|
|
288
|
-
{:else if !
|
|
401
|
+
{:else if !effectiveHideFooter}
|
|
289
402
|
<div class="pw-shell__footer">
|
|
290
403
|
<div class="pw-shell__footer-left">
|
|
291
404
|
{#if resolvedFooterLayout === 'form' && !hideCancel}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout } from './index.svelte.js';
|
|
1
|
+
import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout, PathShellActions } from './index.svelte.js';
|
|
2
2
|
import type { Snippet, Component } from 'svelte';
|
|
3
3
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
@@ -47,7 +54,9 @@ interface Props {
|
|
|
47
54
|
oncancel?: (data: PathData) => void;
|
|
48
55
|
onevent?: (event: any) => void;
|
|
49
56
|
header?: Snippet<[PathSnapshot<any>]>;
|
|
50
|
-
footer?: Snippet<[PathSnapshot<any>,
|
|
57
|
+
footer?: Snippet<[PathSnapshot<any>, PathShellActions]>;
|
|
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;
|
|
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"}
|
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
|
/* ------------------------------------------------------------------ */
|
|
@@ -332,6 +357,23 @@
|
|
|
332
357
|
content: ":";
|
|
333
358
|
}
|
|
334
359
|
|
|
360
|
+
/* ------------------------------------------------------------------ */
|
|
361
|
+
/* Inline field-level errors and warnings (<FieldError /> component) */
|
|
362
|
+
/* ------------------------------------------------------------------ */
|
|
363
|
+
.pw-field-error {
|
|
364
|
+
display: block;
|
|
365
|
+
font-size: 12px;
|
|
366
|
+
color: var(--pw-color-error);
|
|
367
|
+
margin-top: 4px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.pw-field-warning {
|
|
371
|
+
display: block;
|
|
372
|
+
font-size: 12px;
|
|
373
|
+
color: var(--pw-color-warning);
|
|
374
|
+
margin-top: 4px;
|
|
375
|
+
}
|
|
376
|
+
|
|
335
377
|
/* ------------------------------------------------------------------ */
|
|
336
378
|
/* Footer — navigation buttons */
|
|
337
379
|
/* ------------------------------------------------------------------ */
|
package/dist/index.svelte.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface UsePathOptions {
|
|
|
12
12
|
* - The engine lifecycle (start / cleanup) is the **caller's responsibility**.
|
|
13
13
|
* - `PathShell` will skip its own `autoStart` call.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* An externally managed engine. Pass it as a getter (`get engine() { … }`)
|
|
17
|
+
* over a reactive prop to have the hook track it: an engine that arrives
|
|
18
|
+
* later (e.g. from an async `restoreOrStart()`) or is swapped is adopted —
|
|
19
|
+
* the hook re-subscribes and re-seeds its snapshot from the new engine.
|
|
20
|
+
*/
|
|
15
21
|
engine?: PathEngine;
|
|
16
22
|
/** Called for every engine event (stateChanged, completed, cancelled, resumed). */
|
|
17
23
|
onEvent?: (event: PathEvent) => void;
|
|
@@ -34,17 +40,22 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
34
40
|
previous: () => Promise<void>;
|
|
35
41
|
/** Cancel the active path (or sub-path). */
|
|
36
42
|
cancel: () => Promise<void>;
|
|
37
|
-
/** Jump directly to a step by ID. Calls onLeave / onEnter but bypasses guards and shouldSkip. */
|
|
38
|
-
goToStep: (stepId: string
|
|
43
|
+
/** 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. */
|
|
44
|
+
goToStep: (stepId: string, options?: {
|
|
45
|
+
validateOnLeave?: boolean;
|
|
46
|
+
}) => Promise<void>;
|
|
39
47
|
/** 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
|
|
48
|
+
goToStepChecked: (stepId: string, options?: {
|
|
49
|
+
validateOnLeave?: boolean;
|
|
50
|
+
}) => Promise<void>;
|
|
41
51
|
/** 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
52
|
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
43
53
|
/** Reset the current step's data to what it was when the step was entered. Useful for "Clear" or "Reset" buttons. */
|
|
44
54
|
resetStep: () => Promise<void>;
|
|
45
55
|
/**
|
|
46
|
-
* Tear down any active path (without firing hooks) and immediately
|
|
47
|
-
*
|
|
56
|
+
* Tear down any active path (without firing hooks) and immediately restart
|
|
57
|
+
* the root path with the `initialData` from the original `start()` call.
|
|
58
|
+
* Takes no arguments; rejects if the engine has never been started.
|
|
48
59
|
* Use for "Start over" / retry flows without remounting the component.
|
|
49
60
|
*/
|
|
50
61
|
restart: () => Promise<void>;
|
|
@@ -110,24 +121,35 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
110
121
|
* ```
|
|
111
122
|
*/
|
|
112
123
|
export declare function usePath<TData extends PathData = PathData>(options?: UsePathOptions): UsePathReturn<TData>;
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Navigation actions handed to a custom `footer` snippet of `<PathShell>`
|
|
126
|
+
* (`{#snippet footer(snap, actions)}`). Same shape as the other adapters'
|
|
127
|
+
* `PathShellActions`.
|
|
128
|
+
*/
|
|
129
|
+
export interface PathShellActions {
|
|
115
130
|
next: () => Promise<void>;
|
|
116
131
|
previous: () => Promise<void>;
|
|
117
132
|
cancel: () => Promise<void>;
|
|
118
|
-
goToStep: (stepId: string
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
133
|
+
goToStep: (stepId: string, options?: {
|
|
134
|
+
validateOnLeave?: boolean;
|
|
135
|
+
}) => Promise<void>;
|
|
136
|
+
goToStepChecked: (stepId: string, options?: {
|
|
137
|
+
validateOnLeave?: boolean;
|
|
138
|
+
}) => Promise<void>;
|
|
139
|
+
setData: (key: string, value: unknown) => Promise<void>;
|
|
140
|
+
/** Restart the shell's current path with its current `initialData`. */
|
|
122
141
|
restart: () => Promise<void>;
|
|
123
142
|
/** Re-run the operation that set `snapshot.error`. */
|
|
124
143
|
retry: () => Promise<void>;
|
|
125
144
|
/** Pause with intent to return, preserving all state. Emits `suspended`. */
|
|
126
145
|
suspend: () => Promise<void>;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* What step components receive from `usePathContext()`: everything `usePath()`
|
|
149
|
+
* returns (derived from `UsePathReturn`, so the two cannot drift apart) plus
|
|
150
|
+
* the `services` object given to `<PathShell>`.
|
|
151
|
+
*/
|
|
152
|
+
export interface PathContext<TData extends PathData = PathData, TServices = unknown> extends UsePathReturn<TData> {
|
|
131
153
|
services: TServices;
|
|
132
154
|
}
|
|
133
155
|
/**
|
|
@@ -156,6 +178,13 @@ export declare function usePathContext<TData extends PathData = PathData, TServi
|
|
|
156
178
|
* Used by PathShell component.
|
|
157
179
|
*/
|
|
158
180
|
export declare function setPathContext<TData extends PathData = PathData, TServices = unknown>(ctx: PathContext<TData, TServices>): void;
|
|
181
|
+
/**
|
|
182
|
+
* Internal: Get the PathContext from the nearest ancestor PathShell, or
|
|
183
|
+
* `undefined` if no PathShell is present. Used by PathShell itself to access
|
|
184
|
+
* the outer shell's context for `restoreKey` auto-wiring — must be called
|
|
185
|
+
* before `setPathContext()` so it reads the parent rather than self.
|
|
186
|
+
*/
|
|
187
|
+
export declare function getPathContextOrNull<TData extends PathData = PathData, TServices = unknown>(): PathContext<TData, TServices> | undefined;
|
|
159
188
|
/**
|
|
160
189
|
* Create a two-way binding helper for form inputs.
|
|
161
190
|
* Returns an object with a reactive `value` property.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.svelte.d.ts","sourceRoot":"","sources":["../src/index.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EACd,UAAU,EACV,SAAS,EACT,YAAY,EACb,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,YAAY,EACV,QAAQ,EACR,WAAW,EACX,cAAc,EACd,UAAU,EACV,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,eAAe,EACf,cAAc,EACd,YAAY,EACZ,mBAAmB,EACpB,MAAM,yBAAyB,CAAC;AAMjC,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,mFAAmF;IACnF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,aAAa,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ;IAC9D;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAC9C,iCAAiC;IACjC,KAAK,EAAE,CAAC,IAAI,EAAE,cAAc,CAAC,GAAG,CAAC,EAAE,WAAW,CAAC,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,6MAA6M;IAC7M,YAAY,EAAE,CAAC,IAAI,EAAE,cAAc,CAAC,GAAG,CAAC,EAAE,WAAW,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnH,6DAA6D;IAC7D,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,qJAAqJ;IACrJ,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,4CAA4C;IAC5C,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,
|
|
1
|
+
{"version":3,"file":"index.svelte.d.ts","sourceRoot":"","sources":["../src/index.svelte.ts"],"names":[],"mappings":"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"}
|
package/dist/index.svelte.js
CHANGED
|
@@ -60,28 +60,43 @@ export { formatFieldKey, errorPhaseMessage } from "@daltonr/pathwrite-core";
|
|
|
60
60
|
* ```
|
|
61
61
|
*/
|
|
62
62
|
export function usePath(options) {
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
let ownEngine = null;
|
|
64
|
+
const resolveEngine = () => options?.engine ?? (ownEngine ??= new PathEngineClass());
|
|
65
|
+
let engine = resolveEngine();
|
|
65
66
|
let _snapshot = $state(engine.snapshot());
|
|
66
|
-
|
|
67
|
-
const unsubscribe = engine.subscribe((event) => {
|
|
67
|
+
const onEngineEvent = (event) => {
|
|
68
68
|
if (event.type === "stateChanged" || event.type === "resumed") {
|
|
69
69
|
_snapshot = event.snapshot;
|
|
70
70
|
}
|
|
71
71
|
else if (event.type === "completed" || event.type === "cancelled") {
|
|
72
|
-
_snapshot =
|
|
72
|
+
_snapshot = engine.snapshot();
|
|
73
73
|
}
|
|
74
74
|
options?.onEvent?.(event);
|
|
75
|
+
};
|
|
76
|
+
let unsubscribe = engine.subscribe(onEngineEvent);
|
|
77
|
+
// Adopt a late or swapped engine (the `engine` option is a getter over a
|
|
78
|
+
// reactive prop): re-subscribe and re-seed the snapshot. Created in its own
|
|
79
|
+
// effect root so usePath() also works outside a component.
|
|
80
|
+
const stopWatching = $effect.root(() => {
|
|
81
|
+
$effect(() => {
|
|
82
|
+
const next = resolveEngine();
|
|
83
|
+
if (next === engine)
|
|
84
|
+
return;
|
|
85
|
+
unsubscribe();
|
|
86
|
+
engine = next;
|
|
87
|
+
_snapshot = engine.snapshot();
|
|
88
|
+
unsubscribe = engine.subscribe(onEngineEvent);
|
|
89
|
+
});
|
|
75
90
|
});
|
|
76
91
|
// Auto-cleanup when component is destroyed
|
|
77
|
-
onDestroy(unsubscribe);
|
|
92
|
+
onDestroy(() => { unsubscribe(); stopWatching(); });
|
|
78
93
|
const start = (path, initialData = {}) => engine.start(path, initialData);
|
|
79
94
|
const startSubPath = (path, initialData = {}, meta) => engine.startSubPath(path, initialData, meta);
|
|
80
95
|
const next = () => engine.next();
|
|
81
96
|
const previous = () => engine.previous();
|
|
82
97
|
const cancel = () => engine.cancel();
|
|
83
|
-
const goToStep = (stepId) => engine.goToStep(stepId);
|
|
84
|
-
const goToStepChecked = (stepId) => engine.goToStepChecked(stepId);
|
|
98
|
+
const goToStep = (stepId, options) => engine.goToStep(stepId, options);
|
|
99
|
+
const goToStepChecked = (stepId, options) => engine.goToStepChecked(stepId, options);
|
|
85
100
|
const setData = ((key, value) => engine.setData(key, value));
|
|
86
101
|
const resetStep = () => engine.resetStep();
|
|
87
102
|
const restart = () => engine.restart();
|
|
@@ -144,6 +159,15 @@ export function usePathContext() {
|
|
|
144
159
|
export function setPathContext(ctx) {
|
|
145
160
|
setContext(PATH_CONTEXT_KEY, ctx);
|
|
146
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Internal: Get the PathContext from the nearest ancestor PathShell, or
|
|
164
|
+
* `undefined` if no PathShell is present. Used by PathShell itself to access
|
|
165
|
+
* the outer shell's context for `restoreKey` auto-wiring — must be called
|
|
166
|
+
* before `setPathContext()` so it reads the parent rather than self.
|
|
167
|
+
*/
|
|
168
|
+
export function getPathContextOrNull() {
|
|
169
|
+
return getContext(PATH_CONTEXT_KEY);
|
|
170
|
+
}
|
|
147
171
|
// ---------------------------------------------------------------------------
|
|
148
172
|
// Helper for binding form inputs
|
|
149
173
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daltonr/pathwrite-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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.",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"import": "./dist/index.svelte.js"
|
|
29
29
|
},
|
|
30
30
|
"./PathShell.svelte": {
|
|
31
|
+
"types": "./dist/PathShell.svelte.d.ts",
|
|
31
32
|
"svelte": "./dist/PathShell.svelte",
|
|
32
33
|
"import": "./dist/PathShell.svelte"
|
|
33
34
|
},
|
|
@@ -44,7 +45,8 @@
|
|
|
44
45
|
"LICENSE"
|
|
45
46
|
],
|
|
46
47
|
"scripts": {
|
|
47
|
-
"
|
|
48
|
+
"check": "svelte-check --tsconfig ./tsconfig.check.json --fail-on-warnings",
|
|
49
|
+
"build": "npm run check && svelte-package -i src -o dist && cp ../shell.css dist/index.css",
|
|
48
50
|
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
49
51
|
"prepublishOnly": "test -d dist && echo 'dist already built, skipping' || (npm run clean && npm run build)"
|
|
50
52
|
},
|
|
@@ -52,12 +54,15 @@
|
|
|
52
54
|
"svelte": ">=5.0.0"
|
|
53
55
|
},
|
|
54
56
|
"dependencies": {
|
|
55
|
-
"@daltonr/pathwrite-core": "^0.
|
|
57
|
+
"@daltonr/pathwrite-core": "^0.13.0"
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
60
|
"@sveltejs/package": "^2.5.7",
|
|
59
61
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
60
62
|
"@testing-library/svelte": "^5.0.0",
|
|
61
63
|
"svelte": "^5.0.0"
|
|
64
|
+
},
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
62
67
|
}
|
|
63
68
|
}
|
package/src/PathShell.svelte
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
|
-
import { usePath, setPathContext, formatFieldKey, errorPhaseMessage, stepIdToCamelCase } from './index.svelte.js';
|
|
4
|
-
import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout } from './index.svelte.js';
|
|
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';
|
|
5
7
|
import type { Snippet, Component } from 'svelte';
|
|
6
8
|
|
|
7
9
|
|
|
@@ -9,6 +11,12 @@
|
|
|
9
11
|
path?: PathDefinition<any>;
|
|
10
12
|
engine?: PathEngine;
|
|
11
13
|
initialData?: PathData;
|
|
14
|
+
/**
|
|
15
|
+
* When set, this shell automatically saves its state into the nearest outer `PathShell`'s
|
|
16
|
+
* data under this key on every change, and restores from that stored state on remount.
|
|
17
|
+
* No-op when used on a top-level shell with no outer `PathShell` ancestor.
|
|
18
|
+
*/
|
|
19
|
+
restoreKey?: string;
|
|
12
20
|
autoStart?: boolean;
|
|
13
21
|
backLabel?: string;
|
|
14
22
|
nextLabel?: string;
|
|
@@ -22,12 +30,13 @@
|
|
|
22
30
|
/** 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
31
|
validateWhen?: boolean;
|
|
24
32
|
/**
|
|
25
|
-
*
|
|
33
|
+
* Shell layout mode:
|
|
26
34
|
* - "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.
|
|
35
|
+
* - "wizard": Progress header + Back button on left, Cancel and Submit together on right.
|
|
36
|
+
* - "form": Progress header + Cancel on left, Submit alone on right. Back button never shown.
|
|
37
|
+
* - "tabs": No progress header, no footer. Use for tabbed interfaces with a custom tab bar inside the step body.
|
|
29
38
|
*/
|
|
30
|
-
|
|
39
|
+
layout?: "wizard" | "form" | "auto" | "tabs";
|
|
31
40
|
/**
|
|
32
41
|
* Controls whether the shell renders its auto-generated field-error summary box.
|
|
33
42
|
* - `"summary"` (default): Shell renders the labeled error list below the step body.
|
|
@@ -54,7 +63,9 @@
|
|
|
54
63
|
onevent?: (event: any) => void;
|
|
55
64
|
// Optional override snippets for header and footer
|
|
56
65
|
header?: Snippet<[PathSnapshot<any>]>;
|
|
57
|
-
footer?: Snippet<[PathSnapshot<any>,
|
|
66
|
+
footer?: Snippet<[PathSnapshot<any>, PathShellActions]>;
|
|
67
|
+
/** Snippet rendered when `snapshot.status === "completed"`. Defaults to a simple "All done." panel with a restart button. */
|
|
68
|
+
completion?: Snippet<[PathSnapshot<any>]>;
|
|
58
69
|
// All other props treated as step components keyed by step ID
|
|
59
70
|
[key: string]: Component<any> | any;
|
|
60
71
|
}
|
|
@@ -63,6 +74,7 @@
|
|
|
63
74
|
path,
|
|
64
75
|
engine: engineProp,
|
|
65
76
|
initialData = {},
|
|
77
|
+
restoreKey = undefined,
|
|
66
78
|
autoStart = true,
|
|
67
79
|
backLabel = 'Previous',
|
|
68
80
|
nextLabel = 'Next',
|
|
@@ -73,7 +85,7 @@
|
|
|
73
85
|
hideProgress = false,
|
|
74
86
|
hideFooter = false,
|
|
75
87
|
validateWhen = false,
|
|
76
|
-
|
|
88
|
+
layout = 'auto',
|
|
77
89
|
validationDisplay = 'summary',
|
|
78
90
|
progressLayout = 'merged',
|
|
79
91
|
services = null,
|
|
@@ -82,42 +94,107 @@
|
|
|
82
94
|
onevent,
|
|
83
95
|
header,
|
|
84
96
|
footer,
|
|
97
|
+
completion,
|
|
85
98
|
...stepSnippets
|
|
86
99
|
}: Props = $props();
|
|
87
100
|
|
|
101
|
+
// Read outer PathShell context BEFORE setting our own — gives access to
|
|
102
|
+
// parent shell's snapshot and setData for restoreKey auto-wiring.
|
|
103
|
+
const outerCtx = getPathContextOrNull();
|
|
104
|
+
|
|
105
|
+
// When remounting under restoreKey, rebuild the inner engine from the state the
|
|
106
|
+
// previous instance exported, instead of starting the path and jumping to the
|
|
107
|
+
// step (which re-ran onEnter/onLeave and lost attempted / visited state).
|
|
108
|
+
// svelte-ignore state_referenced_locally — read once at init on purpose: restore happens at mount only
|
|
109
|
+
const restoredEngine: PathEngine | null = (() => {
|
|
110
|
+
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;
|
|
113
|
+
try {
|
|
114
|
+
return PathEngineClass.fromState(stored.serializedState, { [path.id]: path });
|
|
115
|
+
} catch {
|
|
116
|
+
return null; // unusable state (e.g. the path definition changed): start fresh below
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
// The shell's own engine is created once; an `engine` prop — present at
|
|
120
|
+
// mount or arriving later — always takes precedence and is adopted by usePath.
|
|
121
|
+
const ownEngine: PathEngine = restoredEngine ?? new PathEngineClass();
|
|
122
|
+
const currentEngine = (): PathEngine => engineProp ?? ownEngine;
|
|
123
|
+
|
|
88
124
|
// Initialize path engine
|
|
89
125
|
const pathReturn = usePath({
|
|
90
|
-
get engine() { return
|
|
126
|
+
get engine() { return currentEngine(); },
|
|
91
127
|
onEvent: (event) => {
|
|
92
128
|
onevent?.(event);
|
|
93
129
|
if (event.type === 'completed') oncomplete?.(event.data);
|
|
94
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
|
+
);
|
|
135
|
+
}
|
|
95
136
|
}
|
|
96
137
|
});
|
|
97
138
|
|
|
98
|
-
const { start, next, previous, cancel, goToStep, goToStepChecked, setData, restart: restartFn, retry, suspend } = pathReturn;
|
|
139
|
+
const { start, startSubPath, next, previous, cancel, goToStep, goToStepChecked, setData, resetStep, restart: restartFn, retry, suspend, validate } = pathReturn;
|
|
99
140
|
|
|
100
141
|
// Provide context for child step components
|
|
101
142
|
setPathContext({
|
|
102
143
|
get snapshot() { return pathReturn.snapshot; },
|
|
144
|
+
start,
|
|
145
|
+
startSubPath,
|
|
146
|
+
validate,
|
|
103
147
|
next,
|
|
104
148
|
previous,
|
|
105
149
|
cancel,
|
|
106
150
|
goToStep,
|
|
107
151
|
goToStepChecked,
|
|
108
152
|
setData,
|
|
109
|
-
|
|
153
|
+
resetStep,
|
|
154
|
+
restart: () => restartFn(),
|
|
110
155
|
retry,
|
|
111
156
|
suspend,
|
|
112
157
|
get services() { return services; },
|
|
113
158
|
});
|
|
114
159
|
|
|
160
|
+
// Dev-mode warning: camelCase callback props are silently ignored in Svelte.
|
|
161
|
+
// 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).
|
|
164
|
+
// `import.meta.env` is a bundler convention (Vite); the cast keeps this
|
|
165
|
+
// package free of Vite's ambient types.
|
|
166
|
+
const isDev = (import.meta as { env?: { DEV?: boolean } }).env?.DEV !== false;
|
|
167
|
+
onMount(() => {
|
|
168
|
+
if (!isDev) return;
|
|
169
|
+
const camelCallbacks = ['onComplete', 'onCancel', 'onEvent'] as const;
|
|
170
|
+
for (const name of camelCallbacks) {
|
|
171
|
+
if (name in stepSnippets) {
|
|
172
|
+
console.warn(
|
|
173
|
+
`[PathShell] "${name}" was passed but will be ignored. Svelte uses lowercase callback props — use "${name.toLowerCase()}" instead.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
115
179
|
// Auto-start the path when no external engine is provided
|
|
116
180
|
let started = false;
|
|
117
181
|
onMount(() => {
|
|
118
|
-
if (autoStart && !started && !engineProp) {
|
|
182
|
+
if (autoStart && !started && !engineProp && !restoredEngine) {
|
|
183
|
+
if (!path) throw new Error('[PathShell] "path" is required when no "engine" is provided');
|
|
119
184
|
started = true;
|
|
120
|
-
|
|
185
|
+
let startData: PathData = initialData ?? {};
|
|
186
|
+
let restoreStepId: string | undefined;
|
|
187
|
+
if (restoreKey && outerCtx) {
|
|
188
|
+
const stored = outerCtx.snapshot?.data[restoreKey] as PathSnapshot<any> | undefined;
|
|
189
|
+
if (stored != null && typeof stored === 'object' && 'stepId' in stored) {
|
|
190
|
+
startData = stored.data as PathData;
|
|
191
|
+
if (stored.stepIndex > 0) restoreStepId = stored.stepId as string;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const p = start(path, startData);
|
|
195
|
+
if (restoreStepId) {
|
|
196
|
+
p.then(() => goToStep(restoreStepId!));
|
|
197
|
+
}
|
|
121
198
|
}
|
|
122
199
|
});
|
|
123
200
|
|
|
@@ -134,13 +211,20 @@
|
|
|
134
211
|
}
|
|
135
212
|
|
|
136
213
|
let snap = $derived(pathReturn.snapshot);
|
|
137
|
-
let actions = $derived({
|
|
214
|
+
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
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
let effectiveHideProgress = $derived(hideProgress || layout === 'tabs');
|
|
221
|
+
let effectiveHideFooter = $derived(hideFooter || layout === 'tabs');
|
|
138
222
|
|
|
139
223
|
// Auto-detect footer layout: single-step top-level paths use "form", everything else uses "wizard"
|
|
140
224
|
let resolvedFooterLayout = $derived(
|
|
141
|
-
|
|
225
|
+
(layout === 'auto' || layout === 'tabs') && snap
|
|
142
226
|
? (snap.stepCount === 1 && snap.nestingLevel === 0 ? 'form' : 'wizard')
|
|
143
|
-
: (
|
|
227
|
+
: (layout === 'auto' || layout === 'tabs' ? 'wizard' : layout)
|
|
144
228
|
);
|
|
145
229
|
|
|
146
230
|
/**
|
|
@@ -153,7 +237,7 @@
|
|
|
153
237
|
* ```
|
|
154
238
|
*/
|
|
155
239
|
export function restart(): Promise<void> {
|
|
156
|
-
return pathReturn.restart(
|
|
240
|
+
return pathReturn.restart();
|
|
157
241
|
}
|
|
158
242
|
</script>
|
|
159
243
|
|
|
@@ -162,14 +246,43 @@
|
|
|
162
246
|
<div class="pw-shell__empty">
|
|
163
247
|
<p>No active path.</p>
|
|
164
248
|
{#if !autoStart}
|
|
165
|
-
<button type="button" class="pw-shell__start-btn" onclick={() => start(path, initialData)}>
|
|
249
|
+
<button type="button" class="pw-shell__start-btn" onclick={() => path && start(path, initialData)}>
|
|
166
250
|
Start
|
|
167
251
|
</button>
|
|
168
252
|
{/if}
|
|
169
253
|
</div>
|
|
254
|
+
{:else if snap.status === 'completed'}
|
|
255
|
+
<!-- Completion panel: shown after stayOnFinal completion -->
|
|
256
|
+
{#if !effectiveHideProgress && snap.stepCount > 1}
|
|
257
|
+
<div class="pw-shell__header">
|
|
258
|
+
<div class="pw-shell__steps">
|
|
259
|
+
{#each snap.steps as step, i}
|
|
260
|
+
<div class="pw-shell__step pw-shell__step--{step.status}">
|
|
261
|
+
<span class="pw-shell__step-dot">✓</span>
|
|
262
|
+
<span class="pw-shell__step-label">{step.title ?? step.id}</span>
|
|
263
|
+
</div>
|
|
264
|
+
{/each}
|
|
265
|
+
</div>
|
|
266
|
+
<div class="pw-shell__track">
|
|
267
|
+
<div class="pw-shell__track-fill" style="width: 100%"></div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
{/if}
|
|
271
|
+
<div class="pw-shell__body">
|
|
272
|
+
{#if completion}
|
|
273
|
+
{@render completion(snap)}
|
|
274
|
+
{:else}
|
|
275
|
+
<div class="pw-shell__completion">
|
|
276
|
+
<p class="pw-shell__completion-message">All done.</p>
|
|
277
|
+
<button type="button" class="pw-shell__completion-restart" onclick={() => restartFn()}>
|
|
278
|
+
Start over
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
{/if}
|
|
282
|
+
</div>
|
|
170
283
|
{:else}
|
|
171
284
|
<!-- Root progress: persistent top-level bar visible during sub-paths -->
|
|
172
|
-
{#if !
|
|
285
|
+
{#if !effectiveHideProgress && snap.rootProgress && progressLayout !== 'activeOnly'}
|
|
173
286
|
<div class="pw-shell__root-progress">
|
|
174
287
|
<div class="pw-shell__steps">
|
|
175
288
|
{#each snap.rootProgress.steps as step, i}
|
|
@@ -188,7 +301,7 @@
|
|
|
188
301
|
{/if}
|
|
189
302
|
|
|
190
303
|
<!-- Header: progress indicator (overridable via header snippet) -->
|
|
191
|
-
{#if !
|
|
304
|
+
{#if !effectiveHideProgress && progressLayout !== 'rootOnly'}
|
|
192
305
|
{#if header}
|
|
193
306
|
{@render header(snap)}
|
|
194
307
|
{:else if snap.stepCount > 1 || snap.nestingLevel > 0}
|
|
@@ -283,9 +396,9 @@
|
|
|
283
396
|
</div>
|
|
284
397
|
</div>
|
|
285
398
|
<!-- Footer: navigation buttons (overridable via footer snippet) -->
|
|
286
|
-
{:else if !
|
|
399
|
+
{:else if !effectiveHideFooter && footer}
|
|
287
400
|
{@render footer(snap, actions)}
|
|
288
|
-
{:else if !
|
|
401
|
+
{:else if !effectiveHideFooter}
|
|
289
402
|
<div class="pw-shell__footer">
|
|
290
403
|
<div class="pw-shell__footer-left">
|
|
291
404
|
{#if resolvedFooterLayout === 'form' && !hideCancel}
|
package/src/index.svelte.ts
CHANGED
|
@@ -39,6 +39,12 @@ export interface UsePathOptions {
|
|
|
39
39
|
* - The engine lifecycle (start / cleanup) is the **caller's responsibility**.
|
|
40
40
|
* - `PathShell` will skip its own `autoStart` call.
|
|
41
41
|
*/
|
|
42
|
+
/**
|
|
43
|
+
* An externally managed engine. Pass it as a getter (`get engine() { … }`)
|
|
44
|
+
* over a reactive prop to have the hook track it: an engine that arrives
|
|
45
|
+
* later (e.g. from an async `restoreOrStart()`) or is swapped is adopted —
|
|
46
|
+
* the hook re-subscribes and re-seeds its snapshot from the new engine.
|
|
47
|
+
*/
|
|
42
48
|
engine?: PathEngine;
|
|
43
49
|
/** Called for every engine event (stateChanged, completed, cancelled, resumed). */
|
|
44
50
|
onEvent?: (event: PathEvent) => void;
|
|
@@ -62,17 +68,18 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
62
68
|
previous: () => Promise<void>;
|
|
63
69
|
/** Cancel the active path (or sub-path). */
|
|
64
70
|
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>;
|
|
71
|
+
/** 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. */
|
|
72
|
+
goToStep: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
67
73
|
/** 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>;
|
|
74
|
+
goToStepChecked: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
69
75
|
/** 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
76
|
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
71
77
|
/** Reset the current step's data to what it was when the step was entered. Useful for "Clear" or "Reset" buttons. */
|
|
72
78
|
resetStep: () => Promise<void>;
|
|
73
79
|
/**
|
|
74
|
-
* Tear down any active path (without firing hooks) and immediately
|
|
75
|
-
*
|
|
80
|
+
* Tear down any active path (without firing hooks) and immediately restart
|
|
81
|
+
* the root path with the `initialData` from the original `start()` call.
|
|
82
|
+
* Takes no arguments; rejects if the engine has never been started.
|
|
76
83
|
* Use for "Start over" / retry flows without remounting the component.
|
|
77
84
|
*/
|
|
78
85
|
restart: () => Promise<void>;
|
|
@@ -145,25 +152,40 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
145
152
|
export function usePath<TData extends PathData = PathData>(
|
|
146
153
|
options?: UsePathOptions
|
|
147
154
|
): UsePathReturn<TData> {
|
|
148
|
-
|
|
155
|
+
let ownEngine: PathEngine | null = null;
|
|
156
|
+
const resolveEngine = (): PathEngine => options?.engine ?? (ownEngine ??= new PathEngineClass());
|
|
157
|
+
let engine = resolveEngine();
|
|
149
158
|
|
|
150
|
-
// Reactive snapshot via $state rune
|
|
151
159
|
let _snapshot: PathSnapshot<TData> | null = $state(
|
|
152
160
|
engine.snapshot() as PathSnapshot<TData> | null
|
|
153
161
|
);
|
|
154
162
|
|
|
155
|
-
|
|
156
|
-
const unsubscribe = engine.subscribe((event: PathEvent) => {
|
|
163
|
+
const onEngineEvent = (event: PathEvent): void => {
|
|
157
164
|
if (event.type === "stateChanged" || event.type === "resumed") {
|
|
158
165
|
_snapshot = event.snapshot as PathSnapshot<TData>;
|
|
159
166
|
} else if (event.type === "completed" || event.type === "cancelled") {
|
|
160
|
-
_snapshot = null;
|
|
167
|
+
_snapshot = engine.snapshot() as PathSnapshot<TData> | null;
|
|
161
168
|
}
|
|
162
169
|
options?.onEvent?.(event);
|
|
170
|
+
};
|
|
171
|
+
let unsubscribe = engine.subscribe(onEngineEvent);
|
|
172
|
+
|
|
173
|
+
// Adopt a late or swapped engine (the `engine` option is a getter over a
|
|
174
|
+
// reactive prop): re-subscribe and re-seed the snapshot. Created in its own
|
|
175
|
+
// effect root so usePath() also works outside a component.
|
|
176
|
+
const stopWatching = $effect.root(() => {
|
|
177
|
+
$effect(() => {
|
|
178
|
+
const next = resolveEngine();
|
|
179
|
+
if (next === engine) return;
|
|
180
|
+
unsubscribe();
|
|
181
|
+
engine = next;
|
|
182
|
+
_snapshot = engine.snapshot() as PathSnapshot<TData> | null;
|
|
183
|
+
unsubscribe = engine.subscribe(onEngineEvent);
|
|
184
|
+
});
|
|
163
185
|
});
|
|
164
186
|
|
|
165
187
|
// Auto-cleanup when component is destroyed
|
|
166
|
-
onDestroy(unsubscribe);
|
|
188
|
+
onDestroy(() => { unsubscribe(); stopWatching(); });
|
|
167
189
|
|
|
168
190
|
const start = (path: PathDefinition<any>, initialData: PathData = {}): Promise<void> =>
|
|
169
191
|
engine.start(path, initialData);
|
|
@@ -178,8 +200,8 @@ export function usePath<TData extends PathData = PathData>(
|
|
|
178
200
|
const previous = (): Promise<void> => engine.previous();
|
|
179
201
|
const cancel = (): Promise<void> => engine.cancel();
|
|
180
202
|
|
|
181
|
-
const goToStep = (stepId: string): Promise<void> => engine.goToStep(stepId);
|
|
182
|
-
const goToStepChecked = (stepId: string): Promise<void> => engine.goToStepChecked(stepId);
|
|
203
|
+
const goToStep = (stepId: string, options?: { validateOnLeave?: boolean }): Promise<void> => engine.goToStep(stepId, options);
|
|
204
|
+
const goToStepChecked = (stepId: string, options?: { validateOnLeave?: boolean }): Promise<void> => engine.goToStepChecked(stepId, options);
|
|
183
205
|
|
|
184
206
|
const setData = (<K extends string & keyof TData>(key: K, value: TData[K]): Promise<void> =>
|
|
185
207
|
engine.setData(key, value as unknown)) as UsePathReturn<TData>["setData"];
|
|
@@ -210,30 +232,38 @@ export function usePath<TData extends PathData = PathData>(
|
|
|
210
232
|
};
|
|
211
233
|
}
|
|
212
234
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
export interface PathContext<TData extends PathData = PathData, TServices = unknown> {
|
|
220
|
-
readonly snapshot: PathSnapshot<TData>;
|
|
235
|
+
/**
|
|
236
|
+
* Navigation actions handed to a custom `footer` snippet of `<PathShell>`
|
|
237
|
+
* (`{#snippet footer(snap, actions)}`). Same shape as the other adapters'
|
|
238
|
+
* `PathShellActions`.
|
|
239
|
+
*/
|
|
240
|
+
export interface PathShellActions {
|
|
221
241
|
next: () => Promise<void>;
|
|
222
242
|
previous: () => Promise<void>;
|
|
223
243
|
cancel: () => Promise<void>;
|
|
224
|
-
goToStep: (stepId: string) => Promise<void>;
|
|
225
|
-
goToStepChecked: (stepId: string) => Promise<void>;
|
|
226
|
-
setData:
|
|
227
|
-
|
|
244
|
+
goToStep: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
245
|
+
goToStepChecked: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
246
|
+
setData: (key: string, value: unknown) => Promise<void>;
|
|
247
|
+
/** Restart the shell's current path with its current `initialData`. */
|
|
228
248
|
restart: () => Promise<void>;
|
|
229
249
|
/** Re-run the operation that set `snapshot.error`. */
|
|
230
250
|
retry: () => Promise<void>;
|
|
231
251
|
/** Pause with intent to return, preserving all state. Emits `suspended`. */
|
|
232
252
|
suspend: () => Promise<void>;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Context API for PathShell
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
const PATH_CONTEXT_KEY = Symbol("pathwrite-context");
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* What step components receive from `usePathContext()`: everything `usePath()`
|
|
263
|
+
* returns (derived from `UsePathReturn`, so the two cannot drift apart) plus
|
|
264
|
+
* the `services` object given to `<PathShell>`.
|
|
265
|
+
*/
|
|
266
|
+
export interface PathContext<TData extends PathData = PathData, TServices = unknown> extends UsePathReturn<TData> {
|
|
237
267
|
services: TServices;
|
|
238
268
|
}
|
|
239
269
|
|
|
@@ -276,6 +306,16 @@ export function setPathContext<TData extends PathData = PathData, TServices = un
|
|
|
276
306
|
setContext(PATH_CONTEXT_KEY, ctx);
|
|
277
307
|
}
|
|
278
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Internal: Get the PathContext from the nearest ancestor PathShell, or
|
|
311
|
+
* `undefined` if no PathShell is present. Used by PathShell itself to access
|
|
312
|
+
* the outer shell's context for `restoreKey` auto-wiring — must be called
|
|
313
|
+
* before `setPathContext()` so it reads the parent rather than self.
|
|
314
|
+
*/
|
|
315
|
+
export function getPathContextOrNull<TData extends PathData = PathData, TServices = unknown>(): PathContext<TData, TServices> | undefined {
|
|
316
|
+
return getContext<PathContext<TData, TServices>>(PATH_CONTEXT_KEY);
|
|
317
|
+
}
|
|
318
|
+
|
|
279
319
|
// ---------------------------------------------------------------------------
|
|
280
320
|
// Helper for binding form inputs
|
|
281
321
|
// ---------------------------------------------------------------------------
|