@daltonr/pathwrite-svelte 0.13.1 → 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 +8 -15
- package/dist/PathShell.svelte +151 -84
- package/dist/PathShell.svelte.d.ts +15 -8
- package/dist/PathShell.svelte.d.ts.map +1 -1
- package/dist/index.css +12 -4
- package/dist/index.svelte.d.ts +12 -12
- package/dist/index.svelte.d.ts.map +1 -1
- package/dist/index.svelte.js +18 -9
- package/package.json +3 -3
- package/src/PathShell.svelte +151 -84
- package/src/index.svelte.ts +60 -43
package/README.md
CHANGED
|
@@ -33,16 +33,8 @@ Peer dependencies: Svelte 5+.
|
|
|
33
33
|
path={applicationPath}
|
|
34
34
|
initialData={{ name: "", email: "", coverNote: "" }}
|
|
35
35
|
oncomplete={handleComplete}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<DetailsStep />
|
|
39
|
-
{/snippet}
|
|
40
|
-
|
|
41
|
-
<!-- Step ID is "cover-note"; PathShell resolves the camelCase snippet automatically -->
|
|
42
|
-
{#snippet coverNote()}
|
|
43
|
-
<CoverNoteStep />
|
|
44
|
-
{/snippet}
|
|
45
|
-
</PathShell>
|
|
36
|
+
steps={{ details: DetailsStep, "cover-note": CoverNoteStep }}
|
|
37
|
+
/>
|
|
46
38
|
```
|
|
47
39
|
|
|
48
40
|
```svelte
|
|
@@ -95,7 +87,9 @@ Peer dependencies: Svelte 5+.
|
|
|
95
87
|
|
|
96
88
|
## PathShell props
|
|
97
89
|
|
|
98
|
-
Step content is supplied
|
|
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.
|
|
99
93
|
|
|
100
94
|
| Prop | Type | Default | Description |
|
|
101
95
|
|---|---|---|---|
|
|
@@ -117,6 +111,7 @@ Step content is supplied as Svelte 5 snippets whose names match each step's `id`
|
|
|
117
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. |
|
|
118
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. |
|
|
119
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. |
|
|
120
115
|
| `oncomplete` | `(data: PathData) => void` | — | Called when the path finishes naturally. |
|
|
121
116
|
| `oncancel` | `(data: PathData) => void` | — | Called when the path is cancelled. |
|
|
122
117
|
| `onevent` | `(event: PathEvent) => void` | — | Called for every engine event. |
|
|
@@ -131,13 +126,11 @@ The component instance also exposes `restart()` for `bind:this` refs, which rest
|
|
|
131
126
|
You can also replace the built-in header and footer with custom snippets:
|
|
132
127
|
|
|
133
128
|
```svelte
|
|
134
|
-
<PathShell path={myPath}>
|
|
129
|
+
<PathShell path={myPath} steps={{ details: DetailsStep }}>
|
|
135
130
|
{#snippet header(snap)}
|
|
136
131
|
<p>Step {snap.stepIndex + 1} of {snap.stepCount}</p>
|
|
137
132
|
{/snippet}
|
|
138
133
|
|
|
139
|
-
{#snippet details()}<DetailsStep />{/snippet}
|
|
140
|
-
|
|
141
134
|
{#snippet footer(snap, actions)}
|
|
142
135
|
<button onclick={actions.previous} disabled={snap.isFirstStep}>Back</button>
|
|
143
136
|
<button onclick={actions.next} disabled={!snap.canMoveNext}>
|
|
@@ -171,7 +164,7 @@ You can also replace the built-in header and footer with custom snippets:
|
|
|
171
164
|
| Export | Description |
|
|
172
165
|
|---|---|
|
|
173
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)} />`. |
|
|
174
|
-
| `stepIdToCamelCase(id)` | Converts a hyphenated step ID to camelCase (`"cover-letter"` → `"coverLetter"`) — the
|
|
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. |
|
|
175
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. |
|
|
176
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. |
|
|
177
170
|
| `formatFieldKey`, `errorPhaseMessage` | Re-exported from `@daltonr/pathwrite-core` for building custom summaries and error panels. |
|
package/dist/PathShell.svelte
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount } from
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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:
|
|
85
|
+
onevent?: (event: PathEvent) => void;
|
|
64
86
|
// Optional override snippets for header and footer
|
|
65
|
-
header?: Snippet<[PathSnapshot
|
|
66
|
-
footer?: Snippet<[PathSnapshot
|
|
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
|
|
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 =
|
|
80
|
-
nextLabel =
|
|
81
|
-
completeLabel =
|
|
99
|
+
backLabel = "Previous",
|
|
100
|
+
nextLabel = "Next",
|
|
101
|
+
completeLabel = "Complete",
|
|
82
102
|
loadingLabel = undefined,
|
|
83
|
-
cancelLabel =
|
|
103
|
+
cancelLabel = "Cancel",
|
|
84
104
|
hideCancel = false,
|
|
85
105
|
hideProgress = false,
|
|
86
106
|
hideFooter = false,
|
|
87
107
|
validateWhen = false,
|
|
88
|
-
layout =
|
|
89
|
-
validationDisplay =
|
|
90
|
-
progressLayout =
|
|
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
|
-
|
|
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
|
|
112
|
-
|
|
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() {
|
|
150
|
+
get engine() {
|
|
151
|
+
return currentEngine();
|
|
152
|
+
},
|
|
127
153
|
onEvent: (event) => {
|
|
128
154
|
onevent?.(event);
|
|
129
|
-
if (event.type ===
|
|
130
|
-
if (event.type ===
|
|
131
|
-
if (restoreKey && outerCtx && event.type ===
|
|
132
|
-
|
|
133
|
-
|
|
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 {
|
|
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() {
|
|
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() {
|
|
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
|
|
163
|
-
//
|
|
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 = [
|
|
216
|
+
const camelCallbacks = ["onComplete", "onCancel", "onEvent"] as const;
|
|
170
217
|
for (const name of camelCallbacks) {
|
|
171
|
-
if (name in
|
|
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
|
|
189
|
-
if (stored != null && typeof 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 =
|
|
208
|
-
|
|
209
|
-
|
|
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,
|
|
216
|
-
|
|
217
|
-
|
|
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 ===
|
|
221
|
-
let effectiveHideFooter = $derived(hideFooter || layout ===
|
|
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 ===
|
|
226
|
-
?
|
|
227
|
-
|
|
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 ===
|
|
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 !==
|
|
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 ===
|
|
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 !==
|
|
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 ===
|
|
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:
|
|
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
|
|
329
|
-
Hyphenated step IDs (e.g. "cover-letter")
|
|
330
|
-
("coverLetter")
|
|
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 &&
|
|
334
|
-
{@const StepComponent =
|
|
390
|
+
{#if snap.formId && steps[snap.formId]}
|
|
391
|
+
{@const StepComponent = steps[snap.formId]}
|
|
335
392
|
<StepComponent />
|
|
336
|
-
{:else if
|
|
337
|
-
{@const StepComponent =
|
|
393
|
+
{:else if steps[snap.stepId]}
|
|
394
|
+
{@const StepComponent = steps[snap.stepId]}
|
|
338
395
|
<StepComponent />
|
|
339
|
-
{:else if
|
|
340
|
-
{@const StepComponent =
|
|
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 !==
|
|
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 !==
|
|
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 !==
|
|
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 !==
|
|
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 !==
|
|
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">
|
|
381
|
-
|
|
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
|
-
>
|
|
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
|
-
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 !==
|
|
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
|
|
2
|
-
import type { Snippet, Component } from
|
|
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
|
|
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:
|
|
56
|
-
header?: Snippet<[PathSnapshot
|
|
57
|
-
footer?: Snippet<[PathSnapshot
|
|
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
|
|
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":"
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
-
|