@daltonr/pathwrite-svelte 0.8.0 → 0.10.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 +6 -6
- package/dist/PathShell.svelte +59 -21
- package/dist/PathShell.svelte.d.ts +9 -3
- package/dist/PathShell.svelte.d.ts.map +1 -1
- package/dist/index.css +86 -0
- package/dist/index.svelte.d.ts +61 -12
- package/dist/index.svelte.d.ts.map +1 -1
- package/dist/index.svelte.js +45 -9
- package/package.json +2 -2
- package/src/PathShell.svelte +59 -21
- package/src/index.svelte.ts +70 -18
package/README.md
CHANGED
|
@@ -168,7 +168,7 @@ Create the engine yourself with `restoreOrStart()` and pass it via `engine`. Pat
|
|
|
168
168
|
|
|
169
169
|
```svelte
|
|
170
170
|
<script>
|
|
171
|
-
import { HttpStore, restoreOrStart,
|
|
171
|
+
import { HttpStore, restoreOrStart, persistence } from '@daltonr/pathwrite-store';
|
|
172
172
|
|
|
173
173
|
let engine = $state(null);
|
|
174
174
|
|
|
@@ -179,7 +179,7 @@ Create the engine yourself with `restoreOrStart()` and pass it via `engine`. Pat
|
|
|
179
179
|
path: signupPath,
|
|
180
180
|
initialData: { name: '' },
|
|
181
181
|
observers: [
|
|
182
|
-
|
|
182
|
+
persistence({ store, key: 'user:onboarding', strategy: 'onNext' })
|
|
183
183
|
]
|
|
184
184
|
});
|
|
185
185
|
engine = result.engine;
|
|
@@ -467,13 +467,13 @@ const myPath = {
|
|
|
467
467
|
|
|
468
468
|
## Persistence
|
|
469
469
|
|
|
470
|
-
Use with [@daltonr/pathwrite-store
|
|
470
|
+
Use with [@daltonr/pathwrite-store](../store) for automatic state persistence:
|
|
471
471
|
|
|
472
472
|
```svelte
|
|
473
473
|
<script lang="ts">
|
|
474
474
|
import { onMount } from 'svelte';
|
|
475
475
|
import { PathShell } from '@daltonr/pathwrite-svelte';
|
|
476
|
-
import { HttpStore, restoreOrStart,
|
|
476
|
+
import { HttpStore, restoreOrStart, persistence } from '@daltonr/pathwrite-store';
|
|
477
477
|
import DetailsForm from './DetailsForm.svelte';
|
|
478
478
|
import ReviewPanel from './ReviewPanel.svelte';
|
|
479
479
|
|
|
@@ -490,7 +490,7 @@ Use with [@daltonr/pathwrite-store-http](../store-http) for automatic state pers
|
|
|
490
490
|
path: signupPath,
|
|
491
491
|
initialData: { name: '', email: '' },
|
|
492
492
|
observers: [
|
|
493
|
-
|
|
493
|
+
persistence({ store, key, strategy: 'onNext' })
|
|
494
494
|
]
|
|
495
495
|
});
|
|
496
496
|
engine = result.engine;
|
|
@@ -537,7 +537,7 @@ MIT — © 2026 Devjoy Ltd.
|
|
|
537
537
|
## See Also
|
|
538
538
|
|
|
539
539
|
- [@daltonr/pathwrite-core](../core) - Core engine
|
|
540
|
-
- [@daltonr/pathwrite-store
|
|
540
|
+
- [@daltonr/pathwrite-store](../store) - HTTP persistence
|
|
541
541
|
- [Documentation](../../docs/guides/DEVELOPER_GUIDE.md)
|
|
542
542
|
|
|
543
543
|
|
package/dist/PathShell.svelte
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
|
-
import { usePath, setPathContext } from './index.svelte.js';
|
|
3
|
+
import { usePath, setPathContext, formatFieldKey, errorPhaseMessage } from './index.svelte.js';
|
|
4
4
|
import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout } from './index.svelte.js';
|
|
5
|
-
import type { Snippet } from 'svelte';
|
|
5
|
+
import type { Snippet, Component } from 'svelte';
|
|
6
6
|
|
|
7
|
-
/** Converts a camelCase or lowercase field key to a display label. */
|
|
8
|
-
function formatFieldKey(key: string): string {
|
|
9
|
-
return key.replace(/([A-Z])/g, ' $1').replace(/^./, c => c.toUpperCase()).trim();
|
|
10
|
-
}
|
|
11
7
|
|
|
12
8
|
interface Props {
|
|
13
9
|
path?: PathDefinition<any>;
|
|
@@ -17,6 +13,7 @@
|
|
|
17
13
|
backLabel?: string;
|
|
18
14
|
nextLabel?: string;
|
|
19
15
|
completeLabel?: string;
|
|
16
|
+
loadingLabel?: string;
|
|
20
17
|
cancelLabel?: string;
|
|
21
18
|
hideCancel?: boolean;
|
|
22
19
|
hideProgress?: boolean;
|
|
@@ -42,6 +39,11 @@
|
|
|
42
39
|
* - "activeOnly": Only the active (sub-path) bar — root bar hidden.
|
|
43
40
|
*/
|
|
44
41
|
progressLayout?: ProgressLayout;
|
|
42
|
+
/**
|
|
43
|
+
* Services object passed through context to all step components.
|
|
44
|
+
* Step components access it via `usePathContext<TData, TServices>()`.
|
|
45
|
+
*/
|
|
46
|
+
services?: unknown;
|
|
45
47
|
// Callback props replace event dispatching in Svelte 5
|
|
46
48
|
oncomplete?: (data: PathData) => void;
|
|
47
49
|
oncancel?: (data: PathData) => void;
|
|
@@ -49,8 +51,8 @@
|
|
|
49
51
|
// Optional override snippets for header and footer
|
|
50
52
|
header?: Snippet<[PathSnapshot<any>]>;
|
|
51
53
|
footer?: Snippet<[PathSnapshot<any>, object]>;
|
|
52
|
-
// All other props treated as step
|
|
53
|
-
[key: string]:
|
|
54
|
+
// All other props treated as step components keyed by step ID
|
|
55
|
+
[key: string]: Component<any> | any;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
let {
|
|
@@ -61,12 +63,14 @@
|
|
|
61
63
|
backLabel = 'Previous',
|
|
62
64
|
nextLabel = 'Next',
|
|
63
65
|
completeLabel = 'Complete',
|
|
66
|
+
loadingLabel = undefined,
|
|
64
67
|
cancelLabel = 'Cancel',
|
|
65
68
|
hideCancel = false,
|
|
66
69
|
hideProgress = false,
|
|
67
70
|
footerLayout = 'auto',
|
|
68
|
-
validationDisplay = '
|
|
71
|
+
validationDisplay = 'summary',
|
|
69
72
|
progressLayout = 'merged',
|
|
73
|
+
services = null,
|
|
70
74
|
oncomplete,
|
|
71
75
|
oncancel,
|
|
72
76
|
onevent,
|
|
@@ -77,7 +81,7 @@
|
|
|
77
81
|
|
|
78
82
|
// Initialize path engine
|
|
79
83
|
const pathReturn = usePath({
|
|
80
|
-
engine
|
|
84
|
+
get engine() { return engineProp; },
|
|
81
85
|
onEvent: (event) => {
|
|
82
86
|
onevent?.(event);
|
|
83
87
|
if (event.type === 'completed') oncomplete?.(event.data);
|
|
@@ -85,7 +89,7 @@
|
|
|
85
89
|
}
|
|
86
90
|
});
|
|
87
91
|
|
|
88
|
-
const { start, next, previous, cancel, goToStep, goToStepChecked, setData, restart: restartFn } = pathReturn;
|
|
92
|
+
const { start, next, previous, cancel, goToStep, goToStepChecked, setData, restart: restartFn, retry, suspend } = pathReturn;
|
|
89
93
|
|
|
90
94
|
// Provide context for child step components
|
|
91
95
|
setPathContext({
|
|
@@ -96,7 +100,10 @@
|
|
|
96
100
|
goToStep,
|
|
97
101
|
goToStepChecked,
|
|
98
102
|
setData,
|
|
99
|
-
restart: () => restartFn(path, initialData)
|
|
103
|
+
restart: () => restartFn(path, initialData),
|
|
104
|
+
retry,
|
|
105
|
+
suspend,
|
|
106
|
+
get services() { return services; },
|
|
100
107
|
});
|
|
101
108
|
|
|
102
109
|
// Auto-start the path when no external engine is provided
|
|
@@ -109,7 +116,7 @@
|
|
|
109
116
|
});
|
|
110
117
|
|
|
111
118
|
let snap = $derived(pathReturn.snapshot);
|
|
112
|
-
let actions = $derived({ next, previous, cancel, goToStep, goToStepChecked, setData, restart: () => restartFn(path, initialData) });
|
|
119
|
+
let actions = $derived({ next, previous, cancel, goToStep, goToStepChecked, setData, restart: () => restartFn(path, initialData), retry, suspend });
|
|
113
120
|
|
|
114
121
|
// Auto-detect footer layout: single-step top-level paths use "form", everything else uses "wizard"
|
|
115
122
|
let resolvedFooterLayout = $derived(
|
|
@@ -190,9 +197,11 @@
|
|
|
190
197
|
register snippets by inner step ids directly. -->
|
|
191
198
|
<div class="pw-shell__body">
|
|
192
199
|
{#if snap.formId && stepSnippets[snap.formId]}
|
|
193
|
-
{@
|
|
200
|
+
{@const StepComponent = stepSnippets[snap.formId]}
|
|
201
|
+
<StepComponent />
|
|
194
202
|
{:else if stepSnippets[snap.stepId]}
|
|
195
|
-
{@
|
|
203
|
+
{@const StepComponent = stepSnippets[snap.stepId]}
|
|
204
|
+
<StepComponent />
|
|
196
205
|
{:else}
|
|
197
206
|
<p>No content for step "{snap.stepId}"</p>
|
|
198
207
|
{/if}
|
|
@@ -220,8 +229,36 @@
|
|
|
220
229
|
</ul>
|
|
221
230
|
{/if}
|
|
222
231
|
|
|
232
|
+
<!-- Blocking error — guard returned { allowed: false, reason } -->
|
|
233
|
+
{#if validationDisplay !== 'inline' && snap.hasAttemptedNext && snap.blockingError}
|
|
234
|
+
<p class="pw-shell__blocking-error">{snap.blockingError}</p>
|
|
235
|
+
{/if}
|
|
236
|
+
|
|
237
|
+
<!-- Error panel: replaces footer when an async operation has failed -->
|
|
238
|
+
{#if snap.status === "error" && snap.error}
|
|
239
|
+
{@const err = snap.error}
|
|
240
|
+
{@const escalated = err.retryCount >= 2}
|
|
241
|
+
<div class="pw-shell__error">
|
|
242
|
+
<div class="pw-shell__error-title">{escalated ? "Still having trouble." : "Something went wrong."}</div>
|
|
243
|
+
<div class="pw-shell__error-message">{errorPhaseMessage(err.phase)}{err.message ? ` ${err.message}` : ""}</div>
|
|
244
|
+
<div class="pw-shell__error-actions">
|
|
245
|
+
{#if !escalated}
|
|
246
|
+
<button type="button" class="pw-shell__btn pw-shell__btn--retry" onclick={retry}>Try again</button>
|
|
247
|
+
{/if}
|
|
248
|
+
{#if snap.hasPersistence}
|
|
249
|
+
<button
|
|
250
|
+
type="button"
|
|
251
|
+
class="pw-shell__btn {escalated ? 'pw-shell__btn--retry' : 'pw-shell__btn--suspend'}"
|
|
252
|
+
onclick={suspend}
|
|
253
|
+
>Save and come back later</button>
|
|
254
|
+
{/if}
|
|
255
|
+
{#if escalated && !snap.hasPersistence}
|
|
256
|
+
<button type="button" class="pw-shell__btn pw-shell__btn--retry" onclick={retry}>Try again</button>
|
|
257
|
+
{/if}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
223
260
|
<!-- Footer: navigation buttons (overridable via footer snippet) -->
|
|
224
|
-
{
|
|
261
|
+
{:else if footer}
|
|
225
262
|
{@render footer(snap, actions)}
|
|
226
263
|
{:else}
|
|
227
264
|
<div class="pw-shell__footer">
|
|
@@ -231,7 +268,7 @@
|
|
|
231
268
|
<button
|
|
232
269
|
type="button"
|
|
233
270
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
234
|
-
disabled={snap.
|
|
271
|
+
disabled={snap.status !== "idle"}
|
|
235
272
|
onclick={cancel}
|
|
236
273
|
>
|
|
237
274
|
{cancelLabel}
|
|
@@ -241,7 +278,7 @@
|
|
|
241
278
|
<button
|
|
242
279
|
type="button"
|
|
243
280
|
class="pw-shell__btn pw-shell__btn--back"
|
|
244
|
-
disabled={snap.
|
|
281
|
+
disabled={snap.status !== "idle" || !snap.canMovePrevious}
|
|
245
282
|
onclick={previous}
|
|
246
283
|
>
|
|
247
284
|
{backLabel}
|
|
@@ -254,7 +291,7 @@
|
|
|
254
291
|
<button
|
|
255
292
|
type="button"
|
|
256
293
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
257
|
-
disabled={snap.
|
|
294
|
+
disabled={snap.status !== "idle"}
|
|
258
295
|
onclick={cancel}
|
|
259
296
|
>
|
|
260
297
|
{cancelLabel}
|
|
@@ -264,10 +301,11 @@
|
|
|
264
301
|
<button
|
|
265
302
|
type="button"
|
|
266
303
|
class="pw-shell__btn pw-shell__btn--next"
|
|
267
|
-
|
|
304
|
+
class:pw-shell__btn--loading={snap.status !== "idle"}
|
|
305
|
+
disabled={snap.status !== "idle"}
|
|
268
306
|
onclick={next}
|
|
269
307
|
>
|
|
270
|
-
{snap.isLastStep ? completeLabel : nextLabel}
|
|
308
|
+
{snap.status !== 'idle' && loadingLabel ? loadingLabel : snap.isLastStep ? completeLabel : nextLabel}
|
|
271
309
|
</button>
|
|
272
310
|
</div>
|
|
273
311
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout } from './index.svelte.js';
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Snippet, Component } from 'svelte';
|
|
3
3
|
interface Props {
|
|
4
4
|
path?: PathDefinition<any>;
|
|
5
5
|
engine?: PathEngine;
|
|
@@ -8,6 +8,7 @@ interface Props {
|
|
|
8
8
|
backLabel?: string;
|
|
9
9
|
nextLabel?: string;
|
|
10
10
|
completeLabel?: string;
|
|
11
|
+
loadingLabel?: string;
|
|
11
12
|
cancelLabel?: string;
|
|
12
13
|
hideCancel?: boolean;
|
|
13
14
|
hideProgress?: boolean;
|
|
@@ -33,14 +34,19 @@ interface Props {
|
|
|
33
34
|
* - "activeOnly": Only the active (sub-path) bar — root bar hidden.
|
|
34
35
|
*/
|
|
35
36
|
progressLayout?: ProgressLayout;
|
|
37
|
+
/**
|
|
38
|
+
* Services object passed through context to all step components.
|
|
39
|
+
* Step components access it via `usePathContext<TData, TServices>()`.
|
|
40
|
+
*/
|
|
41
|
+
services?: unknown;
|
|
36
42
|
oncomplete?: (data: PathData) => void;
|
|
37
43
|
oncancel?: (data: PathData) => void;
|
|
38
44
|
onevent?: (event: any) => void;
|
|
39
45
|
header?: Snippet<[PathSnapshot<any>]>;
|
|
40
46
|
footer?: Snippet<[PathSnapshot<any>, object]>;
|
|
41
|
-
[key: string]:
|
|
47
|
+
[key: string]: Component<any> | any;
|
|
42
48
|
}
|
|
43
|
-
declare const PathShell:
|
|
49
|
+
declare const PathShell: Component<Props, {
|
|
44
50
|
restart: () => Promise<void>;
|
|
45
51
|
}, "">;
|
|
46
52
|
type PathShell = ReturnType<typeof PathShell>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PathShell.svelte.d.ts","sourceRoot":"","sources":["../src/PathShell.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC5G,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"PathShell.svelte.d.ts","sourceRoot":"","sources":["../src/PathShell.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC5G,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;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,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;;;;;OAKG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IAC1C;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;IAClD;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;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,MAAM,CAAC,CAAC,CAAC;IAE9C,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;CACrC;AAmPH,QAAA,MAAM,SAAS;mBA5JQ,QAAQ,IAAI,CAAC;MA4JmB,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
package/dist/index.css
CHANGED
|
@@ -285,6 +285,16 @@
|
|
|
285
285
|
content: ":";
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/* ------------------------------------------------------------------ */
|
|
289
|
+
/* Blocking error (guard-level message, not field-attached) */
|
|
290
|
+
/* ------------------------------------------------------------------ */
|
|
291
|
+
.pw-shell__blocking-error {
|
|
292
|
+
margin: 0;
|
|
293
|
+
padding: 8px 16px;
|
|
294
|
+
font-size: 13px;
|
|
295
|
+
color: var(--pw-color-error);
|
|
296
|
+
}
|
|
297
|
+
|
|
288
298
|
/* ------------------------------------------------------------------ */
|
|
289
299
|
/* Warning messages */
|
|
290
300
|
/* ------------------------------------------------------------------ */
|
|
@@ -361,6 +371,7 @@
|
|
|
361
371
|
background: var(--pw-color-primary);
|
|
362
372
|
border-color: var(--pw-color-primary);
|
|
363
373
|
color: #fff;
|
|
374
|
+
position: relative;
|
|
364
375
|
}
|
|
365
376
|
|
|
366
377
|
.pw-shell__btn--next:hover:not(:disabled) {
|
|
@@ -368,6 +379,29 @@
|
|
|
368
379
|
border-color: #1d4ed8;
|
|
369
380
|
}
|
|
370
381
|
|
|
382
|
+
@keyframes pw-spin {
|
|
383
|
+
to { transform: rotate(360deg); }
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.pw-shell__btn--next.pw-shell__btn--loading {
|
|
387
|
+
color: transparent;
|
|
388
|
+
pointer-events: none;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.pw-shell__btn--next.pw-shell__btn--loading::after {
|
|
392
|
+
content: "";
|
|
393
|
+
position: absolute;
|
|
394
|
+
top: 50%;
|
|
395
|
+
left: 50%;
|
|
396
|
+
width: 14px;
|
|
397
|
+
height: 14px;
|
|
398
|
+
margin: -7px 0 0 -7px;
|
|
399
|
+
border: 2px solid rgba(255, 255, 255, 0.35);
|
|
400
|
+
border-top-color: #fff;
|
|
401
|
+
border-radius: 50%;
|
|
402
|
+
animation: pw-spin 0.6s linear infinite;
|
|
403
|
+
}
|
|
404
|
+
|
|
371
405
|
.pw-shell__btn--back {
|
|
372
406
|
background: transparent;
|
|
373
407
|
border-color: var(--pw-color-primary);
|
|
@@ -388,3 +422,55 @@
|
|
|
388
422
|
background: var(--pw-color-primary-light);
|
|
389
423
|
}
|
|
390
424
|
|
|
425
|
+
/* ------------------------------------------------------------------ */
|
|
426
|
+
/* Error panel — replaces footer when status === "error" */
|
|
427
|
+
/* ------------------------------------------------------------------ */
|
|
428
|
+
.pw-shell__error {
|
|
429
|
+
background: var(--pw-color-error-bg);
|
|
430
|
+
border: 1px solid var(--pw-color-error-border);
|
|
431
|
+
border-radius: var(--pw-shell-radius);
|
|
432
|
+
padding: 16px 20px;
|
|
433
|
+
display: flex;
|
|
434
|
+
flex-direction: column;
|
|
435
|
+
gap: 10px;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.pw-shell__error-title {
|
|
439
|
+
font-size: 14px;
|
|
440
|
+
font-weight: 600;
|
|
441
|
+
color: var(--pw-color-error);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.pw-shell__error-message {
|
|
445
|
+
font-size: 13px;
|
|
446
|
+
color: var(--pw-color-muted);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.pw-shell__error-actions {
|
|
450
|
+
display: flex;
|
|
451
|
+
gap: 8px;
|
|
452
|
+
align-items: center;
|
|
453
|
+
margin-top: 2px;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.pw-shell__btn--retry {
|
|
457
|
+
background: var(--pw-color-error);
|
|
458
|
+
border-color: var(--pw-color-error);
|
|
459
|
+
color: #fff;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.pw-shell__btn--retry:hover:not(:disabled) {
|
|
463
|
+
opacity: 0.9;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.pw-shell__btn--suspend {
|
|
467
|
+
color: var(--pw-color-muted);
|
|
468
|
+
border-color: transparent;
|
|
469
|
+
background: transparent;
|
|
470
|
+
font-size: 13px;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.pw-shell__btn--suspend:hover:not(:disabled) {
|
|
474
|
+
background: var(--pw-color-primary-light);
|
|
475
|
+
}
|
|
476
|
+
|
package/dist/index.svelte.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { PathData, PathDefinition, PathEngine, PathEvent, PathSnapshot } from "@daltonr/pathwrite-core";
|
|
2
|
+
export { formatFieldKey, errorPhaseMessage } from "@daltonr/pathwrite-core";
|
|
2
3
|
export type { PathData, FieldErrors, PathDefinition, PathEngine, PathEvent, PathSnapshot, PathStep, PathStepContext, ProgressLayout, RootProgress, SerializedPathState } from "@daltonr/pathwrite-core";
|
|
3
4
|
export interface UsePathOptions {
|
|
4
5
|
/**
|
|
5
6
|
* An externally-managed `PathEngine` to subscribe to — for example, the engine
|
|
6
|
-
* returned by `restoreOrStart()` from `@daltonr/pathwrite-store
|
|
7
|
+
* returned by `restoreOrStart()` from `@daltonr/pathwrite-store`.
|
|
7
8
|
*
|
|
8
9
|
* When provided:
|
|
9
10
|
* - `usePath` will **not** create its own engine.
|
|
@@ -16,7 +17,12 @@ export interface UsePathOptions {
|
|
|
16
17
|
onEvent?: (event: PathEvent) => void;
|
|
17
18
|
}
|
|
18
19
|
export interface UsePathReturn<TData extends PathData = PathData> {
|
|
19
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Current path snapshot, or `null` when no path is active. Reactive via `$state`.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ **Do not destructure.** `const { snapshot } = usePath()` captures the value
|
|
24
|
+
* once and loses reactivity. Always access as `path.snapshot`.
|
|
25
|
+
*/
|
|
20
26
|
readonly snapshot: PathSnapshot<TData> | null;
|
|
21
27
|
/** Start (or restart) a path. */
|
|
22
28
|
start: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
|
|
@@ -41,15 +47,46 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
41
47
|
* given path fresh. Safe to call whether or not a path is currently active.
|
|
42
48
|
* Use for "Start over" / retry flows without remounting the component.
|
|
43
49
|
*/
|
|
44
|
-
restart: (
|
|
50
|
+
restart: () => Promise<void>;
|
|
51
|
+
/** Re-runs the operation that set `snapshot.error`. Increments `retryCount` on repeated failure. No-op when there is no pending error. */
|
|
52
|
+
retry: () => Promise<void>;
|
|
53
|
+
/** Pauses the path with intent to return. Emits `suspended`. All state is preserved. */
|
|
54
|
+
suspend: () => Promise<void>;
|
|
45
55
|
}
|
|
46
56
|
/**
|
|
47
57
|
* Create a Pathwrite engine with Svelte 5 runes-based reactivity.
|
|
48
58
|
* Call this from inside a Svelte component to get a reactive snapshot.
|
|
49
59
|
* Cleanup is automatic via onDestroy.
|
|
50
60
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
61
|
+
* ---
|
|
62
|
+
*
|
|
63
|
+
* ⚠️ **Do not destructure `snapshot`.**
|
|
64
|
+
*
|
|
65
|
+
* `snapshot` is a reactive getter. Destructuring it copies the value once
|
|
66
|
+
* and severs the reactive connection — your component will stop updating.
|
|
67
|
+
*
|
|
68
|
+
* ```svelte
|
|
69
|
+
* // ❌ Broken — snapshot is captured once and never updates
|
|
70
|
+
* const { snapshot, next } = usePath();
|
|
71
|
+
*
|
|
72
|
+
* // ✅ Correct — snapshot is read through the live object on every render
|
|
73
|
+
* const path = usePath();
|
|
74
|
+
* // use path.snapshot in your template
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* Other properties (`next`, `previous`, `setData`, etc.) are plain functions
|
|
78
|
+
* and are safe to destructure.
|
|
79
|
+
*
|
|
80
|
+
* If you need a local variable that stays reactive, use `$derived`:
|
|
81
|
+
* ```svelte
|
|
82
|
+
* const path = usePath();
|
|
83
|
+
* const snapshot = $derived(path.snapshot);
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* This is expected Svelte 5 behaviour — see the
|
|
87
|
+
* [Svelte $state docs](https://svelte.dev/docs/svelte/$state) for details.
|
|
88
|
+
*
|
|
89
|
+
* ---
|
|
53
90
|
*
|
|
54
91
|
* @example
|
|
55
92
|
* ```svelte
|
|
@@ -71,8 +108,8 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
71
108
|
* ```
|
|
72
109
|
*/
|
|
73
110
|
export declare function usePath<TData extends PathData = PathData>(options?: UsePathOptions): UsePathReturn<TData>;
|
|
74
|
-
export interface PathContext<TData extends PathData = PathData> {
|
|
75
|
-
readonly snapshot: PathSnapshot<TData
|
|
111
|
+
export interface PathContext<TData extends PathData = PathData, TServices = unknown> {
|
|
112
|
+
readonly snapshot: PathSnapshot<TData>;
|
|
76
113
|
next: () => Promise<void>;
|
|
77
114
|
previous: () => Promise<void>;
|
|
78
115
|
cancel: () => Promise<void>;
|
|
@@ -81,17 +118,29 @@ export interface PathContext<TData extends PathData = PathData> {
|
|
|
81
118
|
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
82
119
|
resetStep: () => Promise<void>;
|
|
83
120
|
restart: () => Promise<void>;
|
|
121
|
+
/** Re-run the operation that set `snapshot.error`. */
|
|
122
|
+
retry: () => Promise<void>;
|
|
123
|
+
/** Pause with intent to return, preserving all state. Emits `suspended`. */
|
|
124
|
+
suspend: () => Promise<void>;
|
|
125
|
+
/**
|
|
126
|
+
* Services object passed through context from `PathShell`.
|
|
127
|
+
* Typed as `TServices` when `usePathContext<TData, TServices>()` is used.
|
|
128
|
+
*/
|
|
129
|
+
services: TServices;
|
|
84
130
|
}
|
|
85
131
|
/**
|
|
86
|
-
*
|
|
132
|
+
* Access the nearest `PathShell`'s path instance and optional services object.
|
|
87
133
|
* Use this inside step components to access the path engine.
|
|
88
134
|
*
|
|
135
|
+
* - `TData` narrows `ctx.snapshot?.data`
|
|
136
|
+
* - `TServices` types the `services` value — must match what was passed to `PathShell`
|
|
137
|
+
*
|
|
89
138
|
* @example
|
|
90
139
|
* ```svelte
|
|
91
140
|
* <script lang="ts">
|
|
92
|
-
* import {
|
|
141
|
+
* import { usePathContext } from '@daltonr/pathwrite-svelte';
|
|
93
142
|
*
|
|
94
|
-
* const ctx =
|
|
143
|
+
* const ctx = usePathContext();
|
|
95
144
|
* </script>
|
|
96
145
|
*
|
|
97
146
|
* <input value={ctx.snapshot?.data.name}
|
|
@@ -99,12 +148,12 @@ export interface PathContext<TData extends PathData = PathData> {
|
|
|
99
148
|
* <button onclick={ctx.next}>Next</button>
|
|
100
149
|
* ```
|
|
101
150
|
*/
|
|
102
|
-
export declare function
|
|
151
|
+
export declare function usePathContext<TData extends PathData = PathData, TServices = unknown>(): PathContext<TData, TServices>;
|
|
103
152
|
/**
|
|
104
153
|
* Internal: Set the PathContext for child components.
|
|
105
154
|
* Used by PathShell component.
|
|
106
155
|
*/
|
|
107
|
-
export declare function setPathContext<TData extends PathData = PathData>(ctx: PathContext<TData>): void;
|
|
156
|
+
export declare function setPathContext<TData extends PathData = PathData, TServices = unknown>(ctx: PathContext<TData, TServices>): void;
|
|
108
157
|
/**
|
|
109
158
|
* Create a two-way binding helper for form inputs.
|
|
110
159
|
* 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,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
|
|
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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { onDestroy, getContext, setContext } from "svelte";
|
|
2
2
|
import { PathEngine as PathEngineClass } from "@daltonr/pathwrite-core";
|
|
3
|
+
// Re-export core utilities and types for convenience
|
|
4
|
+
export { formatFieldKey, errorPhaseMessage } from "@daltonr/pathwrite-core";
|
|
3
5
|
// ---------------------------------------------------------------------------
|
|
4
6
|
// usePath - Runes-based API for Svelte 5
|
|
5
7
|
// ---------------------------------------------------------------------------
|
|
@@ -8,8 +10,35 @@ import { PathEngine as PathEngineClass } from "@daltonr/pathwrite-core";
|
|
|
8
10
|
* Call this from inside a Svelte component to get a reactive snapshot.
|
|
9
11
|
* Cleanup is automatic via onDestroy.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* ---
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ **Do not destructure `snapshot`.**
|
|
16
|
+
*
|
|
17
|
+
* `snapshot` is a reactive getter. Destructuring it copies the value once
|
|
18
|
+
* and severs the reactive connection — your component will stop updating.
|
|
19
|
+
*
|
|
20
|
+
* ```svelte
|
|
21
|
+
* // ❌ Broken — snapshot is captured once and never updates
|
|
22
|
+
* const { snapshot, next } = usePath();
|
|
23
|
+
*
|
|
24
|
+
* // ✅ Correct — snapshot is read through the live object on every render
|
|
25
|
+
* const path = usePath();
|
|
26
|
+
* // use path.snapshot in your template
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Other properties (`next`, `previous`, `setData`, etc.) are plain functions
|
|
30
|
+
* and are safe to destructure.
|
|
31
|
+
*
|
|
32
|
+
* If you need a local variable that stays reactive, use `$derived`:
|
|
33
|
+
* ```svelte
|
|
34
|
+
* const path = usePath();
|
|
35
|
+
* const snapshot = $derived(path.snapshot);
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* This is expected Svelte 5 behaviour — see the
|
|
39
|
+
* [Svelte $state docs](https://svelte.dev/docs/svelte/$state) for details.
|
|
40
|
+
*
|
|
41
|
+
* ---
|
|
13
42
|
*
|
|
14
43
|
* @example
|
|
15
44
|
* ```svelte
|
|
@@ -55,7 +84,9 @@ export function usePath(options) {
|
|
|
55
84
|
const goToStepChecked = (stepId) => engine.goToStepChecked(stepId);
|
|
56
85
|
const setData = ((key, value) => engine.setData(key, value));
|
|
57
86
|
const resetStep = () => engine.resetStep();
|
|
58
|
-
const restart = (
|
|
87
|
+
const restart = () => engine.restart();
|
|
88
|
+
const retry = () => engine.retry();
|
|
89
|
+
const suspend = () => engine.suspend();
|
|
59
90
|
return {
|
|
60
91
|
get snapshot() { return _snapshot; },
|
|
61
92
|
start,
|
|
@@ -67,7 +98,9 @@ export function usePath(options) {
|
|
|
67
98
|
goToStepChecked,
|
|
68
99
|
setData,
|
|
69
100
|
resetStep,
|
|
70
|
-
restart
|
|
101
|
+
restart,
|
|
102
|
+
retry,
|
|
103
|
+
suspend
|
|
71
104
|
};
|
|
72
105
|
}
|
|
73
106
|
// ---------------------------------------------------------------------------
|
|
@@ -75,15 +108,18 @@ export function usePath(options) {
|
|
|
75
108
|
// ---------------------------------------------------------------------------
|
|
76
109
|
const PATH_CONTEXT_KEY = Symbol("pathwrite-context");
|
|
77
110
|
/**
|
|
78
|
-
*
|
|
111
|
+
* Access the nearest `PathShell`'s path instance and optional services object.
|
|
79
112
|
* Use this inside step components to access the path engine.
|
|
80
113
|
*
|
|
114
|
+
* - `TData` narrows `ctx.snapshot?.data`
|
|
115
|
+
* - `TServices` types the `services` value — must match what was passed to `PathShell`
|
|
116
|
+
*
|
|
81
117
|
* @example
|
|
82
118
|
* ```svelte
|
|
83
119
|
* <script lang="ts">
|
|
84
|
-
* import {
|
|
120
|
+
* import { usePathContext } from '@daltonr/pathwrite-svelte';
|
|
85
121
|
*
|
|
86
|
-
* const ctx =
|
|
122
|
+
* const ctx = usePathContext();
|
|
87
123
|
* </script>
|
|
88
124
|
*
|
|
89
125
|
* <input value={ctx.snapshot?.data.name}
|
|
@@ -91,10 +127,10 @@ const PATH_CONTEXT_KEY = Symbol("pathwrite-context");
|
|
|
91
127
|
* <button onclick={ctx.next}>Next</button>
|
|
92
128
|
* ```
|
|
93
129
|
*/
|
|
94
|
-
export function
|
|
130
|
+
export function usePathContext() {
|
|
95
131
|
const ctx = getContext(PATH_CONTEXT_KEY);
|
|
96
132
|
if (!ctx) {
|
|
97
|
-
throw new Error("
|
|
133
|
+
throw new Error("usePathContext() must be called from a component inside a <PathShell>. " +
|
|
98
134
|
"Ensure the PathShell component is a parent in the component tree.");
|
|
99
135
|
}
|
|
100
136
|
return ctx;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daltonr/pathwrite-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Svelte 5 adapter for @daltonr/pathwrite-core — runes-based reactive bindings and optional PathShell component.",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"svelte": ">=5.0.0"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@daltonr/pathwrite-core": "^0.
|
|
55
|
+
"@daltonr/pathwrite-core": "^0.10.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@sveltejs/package": "^2.5.7",
|
package/src/PathShell.svelte
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
|
-
import { usePath, setPathContext } from './index.svelte.js';
|
|
3
|
+
import { usePath, setPathContext, formatFieldKey, errorPhaseMessage } from './index.svelte.js';
|
|
4
4
|
import type { PathDefinition, PathData, PathEngine, PathSnapshot, ProgressLayout } from './index.svelte.js';
|
|
5
|
-
import type { Snippet } from 'svelte';
|
|
5
|
+
import type { Snippet, Component } from 'svelte';
|
|
6
6
|
|
|
7
|
-
/** Converts a camelCase or lowercase field key to a display label. */
|
|
8
|
-
function formatFieldKey(key: string): string {
|
|
9
|
-
return key.replace(/([A-Z])/g, ' $1').replace(/^./, c => c.toUpperCase()).trim();
|
|
10
|
-
}
|
|
11
7
|
|
|
12
8
|
interface Props {
|
|
13
9
|
path?: PathDefinition<any>;
|
|
@@ -17,6 +13,7 @@
|
|
|
17
13
|
backLabel?: string;
|
|
18
14
|
nextLabel?: string;
|
|
19
15
|
completeLabel?: string;
|
|
16
|
+
loadingLabel?: string;
|
|
20
17
|
cancelLabel?: string;
|
|
21
18
|
hideCancel?: boolean;
|
|
22
19
|
hideProgress?: boolean;
|
|
@@ -42,6 +39,11 @@
|
|
|
42
39
|
* - "activeOnly": Only the active (sub-path) bar — root bar hidden.
|
|
43
40
|
*/
|
|
44
41
|
progressLayout?: ProgressLayout;
|
|
42
|
+
/**
|
|
43
|
+
* Services object passed through context to all step components.
|
|
44
|
+
* Step components access it via `usePathContext<TData, TServices>()`.
|
|
45
|
+
*/
|
|
46
|
+
services?: unknown;
|
|
45
47
|
// Callback props replace event dispatching in Svelte 5
|
|
46
48
|
oncomplete?: (data: PathData) => void;
|
|
47
49
|
oncancel?: (data: PathData) => void;
|
|
@@ -49,8 +51,8 @@
|
|
|
49
51
|
// Optional override snippets for header and footer
|
|
50
52
|
header?: Snippet<[PathSnapshot<any>]>;
|
|
51
53
|
footer?: Snippet<[PathSnapshot<any>, object]>;
|
|
52
|
-
// All other props treated as step
|
|
53
|
-
[key: string]:
|
|
54
|
+
// All other props treated as step components keyed by step ID
|
|
55
|
+
[key: string]: Component<any> | any;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
let {
|
|
@@ -61,12 +63,14 @@
|
|
|
61
63
|
backLabel = 'Previous',
|
|
62
64
|
nextLabel = 'Next',
|
|
63
65
|
completeLabel = 'Complete',
|
|
66
|
+
loadingLabel = undefined,
|
|
64
67
|
cancelLabel = 'Cancel',
|
|
65
68
|
hideCancel = false,
|
|
66
69
|
hideProgress = false,
|
|
67
70
|
footerLayout = 'auto',
|
|
68
|
-
validationDisplay = '
|
|
71
|
+
validationDisplay = 'summary',
|
|
69
72
|
progressLayout = 'merged',
|
|
73
|
+
services = null,
|
|
70
74
|
oncomplete,
|
|
71
75
|
oncancel,
|
|
72
76
|
onevent,
|
|
@@ -77,7 +81,7 @@
|
|
|
77
81
|
|
|
78
82
|
// Initialize path engine
|
|
79
83
|
const pathReturn = usePath({
|
|
80
|
-
engine
|
|
84
|
+
get engine() { return engineProp; },
|
|
81
85
|
onEvent: (event) => {
|
|
82
86
|
onevent?.(event);
|
|
83
87
|
if (event.type === 'completed') oncomplete?.(event.data);
|
|
@@ -85,7 +89,7 @@
|
|
|
85
89
|
}
|
|
86
90
|
});
|
|
87
91
|
|
|
88
|
-
const { start, next, previous, cancel, goToStep, goToStepChecked, setData, restart: restartFn } = pathReturn;
|
|
92
|
+
const { start, next, previous, cancel, goToStep, goToStepChecked, setData, restart: restartFn, retry, suspend } = pathReturn;
|
|
89
93
|
|
|
90
94
|
// Provide context for child step components
|
|
91
95
|
setPathContext({
|
|
@@ -96,7 +100,10 @@
|
|
|
96
100
|
goToStep,
|
|
97
101
|
goToStepChecked,
|
|
98
102
|
setData,
|
|
99
|
-
restart: () => restartFn(path, initialData)
|
|
103
|
+
restart: () => restartFn(path, initialData),
|
|
104
|
+
retry,
|
|
105
|
+
suspend,
|
|
106
|
+
get services() { return services; },
|
|
100
107
|
});
|
|
101
108
|
|
|
102
109
|
// Auto-start the path when no external engine is provided
|
|
@@ -109,7 +116,7 @@
|
|
|
109
116
|
});
|
|
110
117
|
|
|
111
118
|
let snap = $derived(pathReturn.snapshot);
|
|
112
|
-
let actions = $derived({ next, previous, cancel, goToStep, goToStepChecked, setData, restart: () => restartFn(path, initialData) });
|
|
119
|
+
let actions = $derived({ next, previous, cancel, goToStep, goToStepChecked, setData, restart: () => restartFn(path, initialData), retry, suspend });
|
|
113
120
|
|
|
114
121
|
// Auto-detect footer layout: single-step top-level paths use "form", everything else uses "wizard"
|
|
115
122
|
let resolvedFooterLayout = $derived(
|
|
@@ -190,9 +197,11 @@
|
|
|
190
197
|
register snippets by inner step ids directly. -->
|
|
191
198
|
<div class="pw-shell__body">
|
|
192
199
|
{#if snap.formId && stepSnippets[snap.formId]}
|
|
193
|
-
{@
|
|
200
|
+
{@const StepComponent = stepSnippets[snap.formId]}
|
|
201
|
+
<StepComponent />
|
|
194
202
|
{:else if stepSnippets[snap.stepId]}
|
|
195
|
-
{@
|
|
203
|
+
{@const StepComponent = stepSnippets[snap.stepId]}
|
|
204
|
+
<StepComponent />
|
|
196
205
|
{:else}
|
|
197
206
|
<p>No content for step "{snap.stepId}"</p>
|
|
198
207
|
{/if}
|
|
@@ -220,8 +229,36 @@
|
|
|
220
229
|
</ul>
|
|
221
230
|
{/if}
|
|
222
231
|
|
|
232
|
+
<!-- Blocking error — guard returned { allowed: false, reason } -->
|
|
233
|
+
{#if validationDisplay !== 'inline' && snap.hasAttemptedNext && snap.blockingError}
|
|
234
|
+
<p class="pw-shell__blocking-error">{snap.blockingError}</p>
|
|
235
|
+
{/if}
|
|
236
|
+
|
|
237
|
+
<!-- Error panel: replaces footer when an async operation has failed -->
|
|
238
|
+
{#if snap.status === "error" && snap.error}
|
|
239
|
+
{@const err = snap.error}
|
|
240
|
+
{@const escalated = err.retryCount >= 2}
|
|
241
|
+
<div class="pw-shell__error">
|
|
242
|
+
<div class="pw-shell__error-title">{escalated ? "Still having trouble." : "Something went wrong."}</div>
|
|
243
|
+
<div class="pw-shell__error-message">{errorPhaseMessage(err.phase)}{err.message ? ` ${err.message}` : ""}</div>
|
|
244
|
+
<div class="pw-shell__error-actions">
|
|
245
|
+
{#if !escalated}
|
|
246
|
+
<button type="button" class="pw-shell__btn pw-shell__btn--retry" onclick={retry}>Try again</button>
|
|
247
|
+
{/if}
|
|
248
|
+
{#if snap.hasPersistence}
|
|
249
|
+
<button
|
|
250
|
+
type="button"
|
|
251
|
+
class="pw-shell__btn {escalated ? 'pw-shell__btn--retry' : 'pw-shell__btn--suspend'}"
|
|
252
|
+
onclick={suspend}
|
|
253
|
+
>Save and come back later</button>
|
|
254
|
+
{/if}
|
|
255
|
+
{#if escalated && !snap.hasPersistence}
|
|
256
|
+
<button type="button" class="pw-shell__btn pw-shell__btn--retry" onclick={retry}>Try again</button>
|
|
257
|
+
{/if}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
223
260
|
<!-- Footer: navigation buttons (overridable via footer snippet) -->
|
|
224
|
-
{
|
|
261
|
+
{:else if footer}
|
|
225
262
|
{@render footer(snap, actions)}
|
|
226
263
|
{:else}
|
|
227
264
|
<div class="pw-shell__footer">
|
|
@@ -231,7 +268,7 @@
|
|
|
231
268
|
<button
|
|
232
269
|
type="button"
|
|
233
270
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
234
|
-
disabled={snap.
|
|
271
|
+
disabled={snap.status !== "idle"}
|
|
235
272
|
onclick={cancel}
|
|
236
273
|
>
|
|
237
274
|
{cancelLabel}
|
|
@@ -241,7 +278,7 @@
|
|
|
241
278
|
<button
|
|
242
279
|
type="button"
|
|
243
280
|
class="pw-shell__btn pw-shell__btn--back"
|
|
244
|
-
disabled={snap.
|
|
281
|
+
disabled={snap.status !== "idle" || !snap.canMovePrevious}
|
|
245
282
|
onclick={previous}
|
|
246
283
|
>
|
|
247
284
|
{backLabel}
|
|
@@ -254,7 +291,7 @@
|
|
|
254
291
|
<button
|
|
255
292
|
type="button"
|
|
256
293
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
257
|
-
disabled={snap.
|
|
294
|
+
disabled={snap.status !== "idle"}
|
|
258
295
|
onclick={cancel}
|
|
259
296
|
>
|
|
260
297
|
{cancelLabel}
|
|
@@ -264,10 +301,11 @@
|
|
|
264
301
|
<button
|
|
265
302
|
type="button"
|
|
266
303
|
class="pw-shell__btn pw-shell__btn--next"
|
|
267
|
-
|
|
304
|
+
class:pw-shell__btn--loading={snap.status !== "idle"}
|
|
305
|
+
disabled={snap.status !== "idle"}
|
|
268
306
|
onclick={next}
|
|
269
307
|
>
|
|
270
|
-
{snap.isLastStep ? completeLabel : nextLabel}
|
|
308
|
+
{snap.status !== 'idle' && loadingLabel ? loadingLabel : snap.isLastStep ? completeLabel : nextLabel}
|
|
271
309
|
</button>
|
|
272
310
|
</div>
|
|
273
311
|
</div>
|
package/src/index.svelte.ts
CHANGED
|
@@ -8,7 +8,8 @@ import type {
|
|
|
8
8
|
} from "@daltonr/pathwrite-core";
|
|
9
9
|
import { PathEngine as PathEngineClass } from "@daltonr/pathwrite-core";
|
|
10
10
|
|
|
11
|
-
// Re-export core types for convenience
|
|
11
|
+
// Re-export core utilities and types for convenience
|
|
12
|
+
export { formatFieldKey, errorPhaseMessage } from "@daltonr/pathwrite-core";
|
|
12
13
|
export type {
|
|
13
14
|
PathData,
|
|
14
15
|
FieldErrors,
|
|
@@ -30,7 +31,7 @@ export type {
|
|
|
30
31
|
export interface UsePathOptions {
|
|
31
32
|
/**
|
|
32
33
|
* An externally-managed `PathEngine` to subscribe to — for example, the engine
|
|
33
|
-
* returned by `restoreOrStart()` from `@daltonr/pathwrite-store
|
|
34
|
+
* returned by `restoreOrStart()` from `@daltonr/pathwrite-store`.
|
|
34
35
|
*
|
|
35
36
|
* When provided:
|
|
36
37
|
* - `usePath` will **not** create its own engine.
|
|
@@ -44,7 +45,12 @@ export interface UsePathOptions {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export interface UsePathReturn<TData extends PathData = PathData> {
|
|
47
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Current path snapshot, or `null` when no path is active. Reactive via `$state`.
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ **Do not destructure.** `const { snapshot } = usePath()` captures the value
|
|
52
|
+
* once and loses reactivity. Always access as `path.snapshot`.
|
|
53
|
+
*/
|
|
48
54
|
readonly snapshot: PathSnapshot<TData> | null;
|
|
49
55
|
/** Start (or restart) a path. */
|
|
50
56
|
start: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
|
|
@@ -69,7 +75,11 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
69
75
|
* given path fresh. Safe to call whether or not a path is currently active.
|
|
70
76
|
* Use for "Start over" / retry flows without remounting the component.
|
|
71
77
|
*/
|
|
72
|
-
restart: (
|
|
78
|
+
restart: () => Promise<void>;
|
|
79
|
+
/** Re-runs the operation that set `snapshot.error`. Increments `retryCount` on repeated failure. No-op when there is no pending error. */
|
|
80
|
+
retry: () => Promise<void>;
|
|
81
|
+
/** Pauses the path with intent to return. Emits `suspended`. All state is preserved. */
|
|
82
|
+
suspend: () => Promise<void>;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
// ---------------------------------------------------------------------------
|
|
@@ -81,8 +91,35 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
81
91
|
* Call this from inside a Svelte component to get a reactive snapshot.
|
|
82
92
|
* Cleanup is automatic via onDestroy.
|
|
83
93
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
94
|
+
* ---
|
|
95
|
+
*
|
|
96
|
+
* ⚠️ **Do not destructure `snapshot`.**
|
|
97
|
+
*
|
|
98
|
+
* `snapshot` is a reactive getter. Destructuring it copies the value once
|
|
99
|
+
* and severs the reactive connection — your component will stop updating.
|
|
100
|
+
*
|
|
101
|
+
* ```svelte
|
|
102
|
+
* // ❌ Broken — snapshot is captured once and never updates
|
|
103
|
+
* const { snapshot, next } = usePath();
|
|
104
|
+
*
|
|
105
|
+
* // ✅ Correct — snapshot is read through the live object on every render
|
|
106
|
+
* const path = usePath();
|
|
107
|
+
* // use path.snapshot in your template
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* Other properties (`next`, `previous`, `setData`, etc.) are plain functions
|
|
111
|
+
* and are safe to destructure.
|
|
112
|
+
*
|
|
113
|
+
* If you need a local variable that stays reactive, use `$derived`:
|
|
114
|
+
* ```svelte
|
|
115
|
+
* const path = usePath();
|
|
116
|
+
* const snapshot = $derived(path.snapshot);
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* This is expected Svelte 5 behaviour — see the
|
|
120
|
+
* [Svelte $state docs](https://svelte.dev/docs/svelte/$state) for details.
|
|
121
|
+
*
|
|
122
|
+
* ---
|
|
86
123
|
*
|
|
87
124
|
* @example
|
|
88
125
|
* ```svelte
|
|
@@ -147,8 +184,9 @@ export function usePath<TData extends PathData = PathData>(
|
|
|
147
184
|
|
|
148
185
|
const resetStep = (): Promise<void> => engine.resetStep();
|
|
149
186
|
|
|
150
|
-
const restart = (
|
|
151
|
-
|
|
187
|
+
const restart = (): Promise<void> => engine.restart();
|
|
188
|
+
const retry = (): Promise<void> => engine.retry();
|
|
189
|
+
const suspend = (): Promise<void> => engine.suspend();
|
|
152
190
|
|
|
153
191
|
return {
|
|
154
192
|
get snapshot() { return _snapshot; },
|
|
@@ -161,7 +199,9 @@ export function usePath<TData extends PathData = PathData>(
|
|
|
161
199
|
goToStepChecked,
|
|
162
200
|
setData,
|
|
163
201
|
resetStep,
|
|
164
|
-
restart
|
|
202
|
+
restart,
|
|
203
|
+
retry,
|
|
204
|
+
suspend
|
|
165
205
|
};
|
|
166
206
|
}
|
|
167
207
|
|
|
@@ -171,8 +211,8 @@ export function usePath<TData extends PathData = PathData>(
|
|
|
171
211
|
|
|
172
212
|
const PATH_CONTEXT_KEY = Symbol("pathwrite-context");
|
|
173
213
|
|
|
174
|
-
export interface PathContext<TData extends PathData = PathData> {
|
|
175
|
-
readonly snapshot: PathSnapshot<TData
|
|
214
|
+
export interface PathContext<TData extends PathData = PathData, TServices = unknown> {
|
|
215
|
+
readonly snapshot: PathSnapshot<TData>;
|
|
176
216
|
next: () => Promise<void>;
|
|
177
217
|
previous: () => Promise<void>;
|
|
178
218
|
cancel: () => Promise<void>;
|
|
@@ -181,18 +221,30 @@ export interface PathContext<TData extends PathData = PathData> {
|
|
|
181
221
|
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
182
222
|
resetStep: () => Promise<void>;
|
|
183
223
|
restart: () => Promise<void>;
|
|
224
|
+
/** Re-run the operation that set `snapshot.error`. */
|
|
225
|
+
retry: () => Promise<void>;
|
|
226
|
+
/** Pause with intent to return, preserving all state. Emits `suspended`. */
|
|
227
|
+
suspend: () => Promise<void>;
|
|
228
|
+
/**
|
|
229
|
+
* Services object passed through context from `PathShell`.
|
|
230
|
+
* Typed as `TServices` when `usePathContext<TData, TServices>()` is used.
|
|
231
|
+
*/
|
|
232
|
+
services: TServices;
|
|
184
233
|
}
|
|
185
234
|
|
|
186
235
|
/**
|
|
187
|
-
*
|
|
236
|
+
* Access the nearest `PathShell`'s path instance and optional services object.
|
|
188
237
|
* Use this inside step components to access the path engine.
|
|
189
238
|
*
|
|
239
|
+
* - `TData` narrows `ctx.snapshot?.data`
|
|
240
|
+
* - `TServices` types the `services` value — must match what was passed to `PathShell`
|
|
241
|
+
*
|
|
190
242
|
* @example
|
|
191
243
|
* ```svelte
|
|
192
244
|
* <script lang="ts">
|
|
193
|
-
* import {
|
|
245
|
+
* import { usePathContext } from '@daltonr/pathwrite-svelte';
|
|
194
246
|
*
|
|
195
|
-
* const ctx =
|
|
247
|
+
* const ctx = usePathContext();
|
|
196
248
|
* </script>
|
|
197
249
|
*
|
|
198
250
|
* <input value={ctx.snapshot?.data.name}
|
|
@@ -200,11 +252,11 @@ export interface PathContext<TData extends PathData = PathData> {
|
|
|
200
252
|
* <button onclick={ctx.next}>Next</button>
|
|
201
253
|
* ```
|
|
202
254
|
*/
|
|
203
|
-
export function
|
|
204
|
-
const ctx = getContext<PathContext<TData>>(PATH_CONTEXT_KEY);
|
|
255
|
+
export function usePathContext<TData extends PathData = PathData, TServices = unknown>(): PathContext<TData, TServices> {
|
|
256
|
+
const ctx = getContext<PathContext<TData, TServices>>(PATH_CONTEXT_KEY);
|
|
205
257
|
if (!ctx) {
|
|
206
258
|
throw new Error(
|
|
207
|
-
"
|
|
259
|
+
"usePathContext() must be called from a component inside a <PathShell>. " +
|
|
208
260
|
"Ensure the PathShell component is a parent in the component tree."
|
|
209
261
|
);
|
|
210
262
|
}
|
|
@@ -215,7 +267,7 @@ export function getPathContext<TData extends PathData = PathData>(): PathContext
|
|
|
215
267
|
* Internal: Set the PathContext for child components.
|
|
216
268
|
* Used by PathShell component.
|
|
217
269
|
*/
|
|
218
|
-
export function setPathContext<TData extends PathData = PathData>(ctx: PathContext<TData>): void {
|
|
270
|
+
export function setPathContext<TData extends PathData = PathData, TServices = unknown>(ctx: PathContext<TData, TServices>): void {
|
|
219
271
|
setContext(PATH_CONTEXT_KEY, ctx);
|
|
220
272
|
}
|
|
221
273
|
|