@daltonr/pathwrite-svelte 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -16
- 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/dist/index.svelte.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PathData, PathDefinition, PathEngine, PathEvent, PathSnapshot } from "@daltonr/pathwrite-core";
|
|
2
2
|
export { formatFieldKey, errorPhaseMessage } from "@daltonr/pathwrite-core";
|
|
3
|
-
export type { PathData, FieldErrors, PathDefinition, PathEngine, PathEvent, PathSnapshot, PathStep, PathStepContext, ProgressLayout, RootProgress, SerializedPathState } from "@daltonr/pathwrite-core";
|
|
4
|
-
export interface UsePathOptions {
|
|
3
|
+
export type { PathData, FieldErrors, PathDefinition, PathEngine, PathEvent, PathSnapshot, StepStatus, PathStep, PathStepContext, ProgressLayout, RootProgress, SerializedPathState, } from "@daltonr/pathwrite-core";
|
|
4
|
+
export interface UsePathOptions<TData extends PathData = PathData> {
|
|
5
5
|
/**
|
|
6
6
|
* An externally-managed `PathEngine` to subscribe to — for example, the engine
|
|
7
7
|
* returned by `restoreOrStart()` from `@daltonr/pathwrite-store`.
|
|
@@ -18,22 +18,22 @@ export interface UsePathOptions {
|
|
|
18
18
|
* later (e.g. from an async `restoreOrStart()`) or is swapped is adopted —
|
|
19
19
|
* the hook re-subscribes and re-seeds its snapshot from the new engine.
|
|
20
20
|
*/
|
|
21
|
-
engine?: PathEngine
|
|
21
|
+
engine?: PathEngine<TData>;
|
|
22
22
|
/** Called for every engine event (stateChanged, completed, cancelled, resumed). */
|
|
23
|
-
onEvent?: (event: PathEvent) => void;
|
|
23
|
+
onEvent?: (event: PathEvent<TData>) => void;
|
|
24
24
|
}
|
|
25
25
|
export interface UsePathReturn<TData extends PathData = PathData> {
|
|
26
26
|
/**
|
|
27
|
-
* Current path snapshot, or `null` when no path is active. Reactive via `$state`.
|
|
27
|
+
* Current path snapshot, or `null` when no path is active. Reactive via `$state.raw`.
|
|
28
28
|
*
|
|
29
29
|
* ⚠️ **Do not destructure.** `const { snapshot } = usePath()` captures the value
|
|
30
30
|
* once and loses reactivity. Always access as `path.snapshot`.
|
|
31
31
|
*/
|
|
32
32
|
readonly snapshot: PathSnapshot<TData> | null;
|
|
33
33
|
/** Start (or restart) a path. */
|
|
34
|
-
start: (path: PathDefinition<
|
|
35
|
-
/** Push a sub-path onto the stack. Requires an active path. Pass an optional `meta` object for correlation — it is returned unchanged to the parent step's `onSubPathComplete` / `onSubPathCancel` hooks. */
|
|
36
|
-
startSubPath: (path: PathDefinition
|
|
34
|
+
start: (path: PathDefinition<TData>, initialData?: Partial<TData>) => Promise<void>;
|
|
35
|
+
/** Push a sub-path onto the stack. Requires an active path. A sub-path has its own data, so any definition is accepted. Pass an optional `meta` object for correlation — it is returned unchanged to the parent step's `onSubPathComplete` / `onSubPathCancel` hooks. */
|
|
36
|
+
startSubPath: (path: PathDefinition, initialData?: PathData, meta?: Record<string, unknown>) => Promise<void>;
|
|
37
37
|
/** Advance one step. Completes the path on the last step. */
|
|
38
38
|
next: () => Promise<void>;
|
|
39
39
|
/** Go back one step. No-op when already on the first step of a top-level path. Pops back to the parent path when on the first step of a sub-path. */
|
|
@@ -120,7 +120,7 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
120
120
|
* {/if}
|
|
121
121
|
* ```
|
|
122
122
|
*/
|
|
123
|
-
export declare function usePath<TData extends PathData = PathData>(options?: UsePathOptions): UsePathReturn<TData>;
|
|
123
|
+
export declare function usePath<TData extends PathData = PathData>(options?: UsePathOptions<TData>): UsePathReturn<TData>;
|
|
124
124
|
/**
|
|
125
125
|
* Navigation actions handed to a custom `footer` snippet of `<PathShell>`
|
|
126
126
|
* (`{#snippet footer(snap, actions)}`). Same shape as the other adapters'
|
|
@@ -211,9 +211,9 @@ export declare function bindData<TData extends PathData, K extends string & keyo
|
|
|
211
211
|
};
|
|
212
212
|
/**
|
|
213
213
|
* Converts a hyphenated step ID to camelCase.
|
|
214
|
-
* Used internally by PathShell
|
|
215
|
-
*
|
|
216
|
-
*
|
|
214
|
+
* Used internally by PathShell as a fallback key into its `steps` record, so a
|
|
215
|
+
* hyphenated step ID (e.g. "cover-letter") also resolves an entry registered
|
|
216
|
+
* under its camelCase form ("coverLetter").
|
|
217
217
|
*/
|
|
218
218
|
export declare function stepIdToCamelCase(id: string): string;
|
|
219
219
|
export { default as PathShell } from "./PathShell.svelte";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.svelte.d.ts","sourceRoot":"","sources":["../src/index.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
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
|
@@ -63,7 +63,11 @@ export function usePath(options) {
|
|
|
63
63
|
let ownEngine = null;
|
|
64
64
|
const resolveEngine = () => options?.engine ?? (ownEngine ??= new PathEngineClass());
|
|
65
65
|
let engine = resolveEngine();
|
|
66
|
-
|
|
66
|
+
// `$state.raw`, not `$state`: snapshots are immutable values the engine
|
|
67
|
+
// replaces wholesale on every change and nothing mutates them in place, so a
|
|
68
|
+
// deep proxy over each one (and its `data`) would be pure overhead. Every
|
|
69
|
+
// update below reassigns the whole value, which is what `$state.raw` tracks.
|
|
70
|
+
let _snapshot = $state.raw(engine.snapshot());
|
|
67
71
|
const onEngineEvent = (event) => {
|
|
68
72
|
if (event.type === "stateChanged" || event.type === "resumed") {
|
|
69
73
|
_snapshot = event.snapshot;
|
|
@@ -89,7 +93,10 @@ export function usePath(options) {
|
|
|
89
93
|
});
|
|
90
94
|
});
|
|
91
95
|
// Auto-cleanup when component is destroyed
|
|
92
|
-
onDestroy(() => {
|
|
96
|
+
onDestroy(() => {
|
|
97
|
+
unsubscribe();
|
|
98
|
+
stopWatching();
|
|
99
|
+
});
|
|
93
100
|
const start = (path, initialData = {}) => engine.start(path, initialData);
|
|
94
101
|
const startSubPath = (path, initialData = {}, meta) => engine.startSubPath(path, initialData, meta);
|
|
95
102
|
const next = () => engine.next();
|
|
@@ -97,14 +104,16 @@ export function usePath(options) {
|
|
|
97
104
|
const cancel = () => engine.cancel();
|
|
98
105
|
const goToStep = (stepId, options) => engine.goToStep(stepId, options);
|
|
99
106
|
const goToStepChecked = (stepId, options) => engine.goToStepChecked(stepId, options);
|
|
100
|
-
const setData = (
|
|
107
|
+
const setData = (key, value) => engine.setData(key, value);
|
|
101
108
|
const resetStep = () => engine.resetStep();
|
|
102
109
|
const restart = () => engine.restart();
|
|
103
110
|
const retry = () => engine.retry();
|
|
104
111
|
const suspend = () => engine.suspend();
|
|
105
112
|
const validate = () => engine.validate();
|
|
106
113
|
return {
|
|
107
|
-
get snapshot() {
|
|
114
|
+
get snapshot() {
|
|
115
|
+
return _snapshot;
|
|
116
|
+
},
|
|
108
117
|
start,
|
|
109
118
|
startSubPath,
|
|
110
119
|
next,
|
|
@@ -117,7 +126,7 @@ export function usePath(options) {
|
|
|
117
126
|
restart,
|
|
118
127
|
retry,
|
|
119
128
|
suspend,
|
|
120
|
-
validate
|
|
129
|
+
validate,
|
|
121
130
|
};
|
|
122
131
|
}
|
|
123
132
|
// ---------------------------------------------------------------------------
|
|
@@ -198,14 +207,14 @@ export function bindData(getSnapshot, setData, key) {
|
|
|
198
207
|
},
|
|
199
208
|
set(value) {
|
|
200
209
|
setData(key, value);
|
|
201
|
-
}
|
|
210
|
+
},
|
|
202
211
|
};
|
|
203
212
|
}
|
|
204
213
|
/**
|
|
205
214
|
* Converts a hyphenated step ID to camelCase.
|
|
206
|
-
* Used internally by PathShell
|
|
207
|
-
*
|
|
208
|
-
*
|
|
215
|
+
* Used internally by PathShell as a fallback key into its `steps` record, so a
|
|
216
|
+
* hyphenated step ID (e.g. "cover-letter") also resolves an entry registered
|
|
217
|
+
* under its camelCase form ("coverLetter").
|
|
209
218
|
*/
|
|
210
219
|
export function stepIdToCamelCase(id) {
|
|
211
220
|
return id.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daltonr/pathwrite-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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.",
|
|
@@ -48,13 +48,13 @@
|
|
|
48
48
|
"check": "svelte-check --tsconfig ./tsconfig.check.json --fail-on-warnings",
|
|
49
49
|
"build": "npm run check && svelte-package -i src -o dist && cp ../shell.css dist/index.css",
|
|
50
50
|
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
51
|
-
"prepublishOnly": "
|
|
51
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"svelte": ">=5.0.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@daltonr/pathwrite-core": "^0.
|
|
57
|
+
"@daltonr/pathwrite-core": "^0.14.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@sveltejs/package": "^2.5.7",
|
package/src/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>
|