@daltonr/pathwrite-svelte 0.5.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/LICENSE +22 -0
- package/README.md +468 -0
- package/dist/PathShell.svelte +181 -0
- package/dist/PathShell.svelte.d.ts +24 -0
- package/dist/PathShell.svelte.d.ts.map +1 -0
- package/dist/index.css +272 -0
- package/dist/index.svelte.d.ts +130 -0
- package/dist/index.svelte.d.ts.map +1 -0
- package/dist/index.svelte.js +141 -0
- package/package.json +62 -0
- package/src/PathShell.svelte +181 -0
- package/src/index.svelte.ts +254 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { usePath, setPathContext } from './index.svelte.js';
|
|
4
|
+
import type { PathDefinition, PathData, PathEngine, PathSnapshot } from './index.svelte.js';
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
path?: PathDefinition<any>;
|
|
9
|
+
engine?: PathEngine;
|
|
10
|
+
initialData?: PathData;
|
|
11
|
+
autoStart?: boolean;
|
|
12
|
+
backLabel?: string;
|
|
13
|
+
nextLabel?: string;
|
|
14
|
+
completeLabel?: string;
|
|
15
|
+
cancelLabel?: string;
|
|
16
|
+
hideCancel?: boolean;
|
|
17
|
+
hideProgress?: boolean;
|
|
18
|
+
// Callback props replace event dispatching in Svelte 5
|
|
19
|
+
oncomplete?: (data: PathData) => void;
|
|
20
|
+
oncancel?: (data: PathData) => void;
|
|
21
|
+
onevent?: (event: any) => void;
|
|
22
|
+
// Optional override snippets for header and footer
|
|
23
|
+
header?: Snippet<[PathSnapshot<any>]>;
|
|
24
|
+
footer?: Snippet<[PathSnapshot<any>, object]>;
|
|
25
|
+
// All other props treated as step snippets keyed by step ID
|
|
26
|
+
[key: string]: Snippet | any;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
path,
|
|
31
|
+
engine: engineProp,
|
|
32
|
+
initialData = {},
|
|
33
|
+
autoStart = true,
|
|
34
|
+
backLabel = 'Previous',
|
|
35
|
+
nextLabel = 'Next',
|
|
36
|
+
completeLabel = 'Complete',
|
|
37
|
+
cancelLabel = 'Cancel',
|
|
38
|
+
hideCancel = false,
|
|
39
|
+
hideProgress = false,
|
|
40
|
+
oncomplete,
|
|
41
|
+
oncancel,
|
|
42
|
+
onevent,
|
|
43
|
+
header,
|
|
44
|
+
footer,
|
|
45
|
+
...stepSnippets
|
|
46
|
+
}: Props = $props();
|
|
47
|
+
|
|
48
|
+
// Initialize path engine
|
|
49
|
+
const pathReturn = usePath({
|
|
50
|
+
engine: engineProp,
|
|
51
|
+
onEvent: (event) => {
|
|
52
|
+
onevent?.(event);
|
|
53
|
+
if (event.type === 'completed') oncomplete?.(event.data);
|
|
54
|
+
if (event.type === 'cancelled') oncancel?.(event.data);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const { start, next, previous, cancel, goToStep, goToStepChecked, setData, restart } = pathReturn;
|
|
59
|
+
|
|
60
|
+
// Provide context for child step components
|
|
61
|
+
setPathContext({
|
|
62
|
+
get snapshot() { return pathReturn.snapshot; },
|
|
63
|
+
next,
|
|
64
|
+
previous,
|
|
65
|
+
cancel,
|
|
66
|
+
goToStep,
|
|
67
|
+
goToStepChecked,
|
|
68
|
+
setData,
|
|
69
|
+
restart: () => restart(path, initialData)
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Auto-start the path when no external engine is provided
|
|
73
|
+
let started = false;
|
|
74
|
+
onMount(() => {
|
|
75
|
+
if (autoStart && !started && !engineProp) {
|
|
76
|
+
started = true;
|
|
77
|
+
start(path, initialData);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
let snap = $derived(pathReturn.snapshot);
|
|
82
|
+
let actions = $derived({ next, previous, cancel, goToStep, goToStepChecked, setData, restart: () => restart(path, initialData) });
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<div class="pw-shell">
|
|
86
|
+
{#if !snap}
|
|
87
|
+
<div class="pw-shell__empty">
|
|
88
|
+
<p>No active path.</p>
|
|
89
|
+
{#if !autoStart}
|
|
90
|
+
<button type="button" class="pw-shell__start-btn" onclick={() => start(path, initialData)}>
|
|
91
|
+
Start
|
|
92
|
+
</button>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
{:else}
|
|
96
|
+
<!-- Header: progress indicator (overridable via header snippet) -->
|
|
97
|
+
{#if !hideProgress}
|
|
98
|
+
{#if header}
|
|
99
|
+
{@render header(snap)}
|
|
100
|
+
{:else}
|
|
101
|
+
<div class="pw-shell__header">
|
|
102
|
+
<div class="pw-shell__steps">
|
|
103
|
+
{#each snap.steps as step, i}
|
|
104
|
+
<div class="pw-shell__step pw-shell__step--{step.status}">
|
|
105
|
+
<span class="pw-shell__step-dot">
|
|
106
|
+
{step.status === 'completed' ? '✓' : i + 1}
|
|
107
|
+
</span>
|
|
108
|
+
<span class="pw-shell__step-label">{step.title ?? step.id}</span>
|
|
109
|
+
</div>
|
|
110
|
+
{/each}
|
|
111
|
+
</div>
|
|
112
|
+
<div class="pw-shell__track">
|
|
113
|
+
<div class="pw-shell__track-fill" style="width: {snap.progress * 100}%"></div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
{/if}
|
|
117
|
+
{/if}
|
|
118
|
+
|
|
119
|
+
<!-- Body: current step rendered via named snippet -->
|
|
120
|
+
<div class="pw-shell__body">
|
|
121
|
+
{#if stepSnippets[snap.stepId]}
|
|
122
|
+
{@render stepSnippets[snap.stepId]()}
|
|
123
|
+
{:else}
|
|
124
|
+
<p>No content for step "{snap.stepId}"</p>
|
|
125
|
+
{/if}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Validation messages -->
|
|
129
|
+
{#if snap.validationMessages.length > 0}
|
|
130
|
+
<ul class="pw-shell__validation">
|
|
131
|
+
{#each snap.validationMessages as msg}
|
|
132
|
+
<li class="pw-shell__validation-item">{msg}</li>
|
|
133
|
+
{/each}
|
|
134
|
+
</ul>
|
|
135
|
+
{/if}
|
|
136
|
+
|
|
137
|
+
<!-- Footer: navigation buttons (overridable via footer snippet) -->
|
|
138
|
+
{#if footer}
|
|
139
|
+
{@render footer(snap, actions)}
|
|
140
|
+
{:else}
|
|
141
|
+
<div class="pw-shell__footer">
|
|
142
|
+
<div class="pw-shell__footer-left">
|
|
143
|
+
{#if !snap.isFirstStep}
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
class="pw-shell__btn pw-shell__btn--back"
|
|
147
|
+
disabled={snap.isNavigating || !snap.canMovePrevious}
|
|
148
|
+
onclick={previous}
|
|
149
|
+
>
|
|
150
|
+
{backLabel}
|
|
151
|
+
</button>
|
|
152
|
+
{/if}
|
|
153
|
+
</div>
|
|
154
|
+
<div class="pw-shell__footer-right">
|
|
155
|
+
{#if !hideCancel}
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
class="pw-shell__btn pw-shell__btn--cancel"
|
|
159
|
+
disabled={snap.isNavigating}
|
|
160
|
+
onclick={cancel}
|
|
161
|
+
>
|
|
162
|
+
{cancelLabel}
|
|
163
|
+
</button>
|
|
164
|
+
{/if}
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
class="pw-shell__btn pw-shell__btn--next"
|
|
168
|
+
disabled={snap.isNavigating || !snap.canMoveNext}
|
|
169
|
+
onclick={next}
|
|
170
|
+
>
|
|
171
|
+
{snap.isLastStep ? completeLabel : nextLabel}
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
{/if}
|
|
176
|
+
{/if}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<style>
|
|
180
|
+
/* Component-level styles inherited from shell.css */
|
|
181
|
+
</style>
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { onDestroy, getContext, setContext } from "svelte";
|
|
2
|
+
import type {
|
|
3
|
+
PathData,
|
|
4
|
+
PathDefinition,
|
|
5
|
+
PathEngine,
|
|
6
|
+
PathEvent,
|
|
7
|
+
PathSnapshot
|
|
8
|
+
} from "@daltonr/pathwrite-core";
|
|
9
|
+
import { PathEngine as PathEngineClass } from "@daltonr/pathwrite-core";
|
|
10
|
+
|
|
11
|
+
// Re-export core types for convenience
|
|
12
|
+
export type {
|
|
13
|
+
PathData,
|
|
14
|
+
PathDefinition,
|
|
15
|
+
PathEngine,
|
|
16
|
+
PathEvent,
|
|
17
|
+
PathSnapshot,
|
|
18
|
+
PathStep,
|
|
19
|
+
PathStepContext,
|
|
20
|
+
SerializedPathState
|
|
21
|
+
} from "@daltonr/pathwrite-core";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Types
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export interface UsePathOptions {
|
|
28
|
+
/**
|
|
29
|
+
* An externally-managed `PathEngine` to subscribe to — for example, the engine
|
|
30
|
+
* returned by `restoreOrStart()` from `@daltonr/pathwrite-store-http`.
|
|
31
|
+
*
|
|
32
|
+
* When provided:
|
|
33
|
+
* - `usePath` will **not** create its own engine.
|
|
34
|
+
* - The snapshot is seeded immediately from the engine's current state.
|
|
35
|
+
* - The engine lifecycle (start / cleanup) is the **caller's responsibility**.
|
|
36
|
+
* - `PathShell` will skip its own `autoStart` call.
|
|
37
|
+
*/
|
|
38
|
+
engine?: PathEngine;
|
|
39
|
+
/** Called for every engine event (stateChanged, completed, cancelled, resumed). */
|
|
40
|
+
onEvent?: (event: PathEvent) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface UsePathReturn<TData extends PathData = PathData> {
|
|
44
|
+
/** Current path snapshot, or `null` when no path is active. Reactive via `$state`. */
|
|
45
|
+
readonly snapshot: PathSnapshot<TData> | null;
|
|
46
|
+
/** Start (or restart) a path. */
|
|
47
|
+
start: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
|
|
48
|
+
/** 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. */
|
|
49
|
+
startSubPath: (path: PathDefinition<any>, initialData?: PathData, meta?: Record<string, unknown>) => Promise<void>;
|
|
50
|
+
/** Advance one step. Completes the path on the last step. */
|
|
51
|
+
next: () => Promise<void>;
|
|
52
|
+
/** 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. */
|
|
53
|
+
previous: () => Promise<void>;
|
|
54
|
+
/** Cancel the active path (or sub-path). */
|
|
55
|
+
cancel: () => Promise<void>;
|
|
56
|
+
/** Jump directly to a step by ID. Calls onLeave / onEnter but bypasses guards and shouldSkip. */
|
|
57
|
+
goToStep: (stepId: string) => Promise<void>;
|
|
58
|
+
/** Jump directly to a step by ID, checking the current step's canMoveNext (forward) or canMovePrevious (backward) guard first. Navigation is blocked if the guard returns false. */
|
|
59
|
+
goToStepChecked: (stepId: string) => Promise<void>;
|
|
60
|
+
/** Update a single data value; triggers a re-render via stateChanged. When `TData` is specified, `key` and `value` are type-checked against your data shape. */
|
|
61
|
+
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Tear down any active path (without firing hooks) and immediately start the
|
|
64
|
+
* given path fresh. Safe to call whether or not a path is currently active.
|
|
65
|
+
* Use for "Start over" / retry flows without remounting the component.
|
|
66
|
+
*/
|
|
67
|
+
restart: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// usePath - Runes-based API for Svelte 5
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a Pathwrite engine with Svelte 5 runes-based reactivity.
|
|
76
|
+
* Call this from inside a Svelte component to get a reactive snapshot.
|
|
77
|
+
* Cleanup is automatic via onDestroy.
|
|
78
|
+
*
|
|
79
|
+
* **Note:** `snapshot` is a reactive getter — access it via the returned
|
|
80
|
+
* object (e.g. `path.snapshot`). Destructuring `snapshot` will lose reactivity.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```svelte
|
|
84
|
+
* <script lang="ts">
|
|
85
|
+
* import { usePath } from '@daltonr/pathwrite-svelte';
|
|
86
|
+
*
|
|
87
|
+
* const path = usePath();
|
|
88
|
+
*
|
|
89
|
+
* onMount(() => {
|
|
90
|
+
* path.start(myPath, { name: '' });
|
|
91
|
+
* });
|
|
92
|
+
* </script>
|
|
93
|
+
*
|
|
94
|
+
* {#if path.snapshot}
|
|
95
|
+
* <h2>{path.snapshot.stepId}</h2>
|
|
96
|
+
* <button onclick={path.previous} disabled={path.snapshot.isFirstStep}>Previous</button>
|
|
97
|
+
* <button onclick={path.next} disabled={!path.snapshot.canMoveNext}>Next</button>
|
|
98
|
+
* {/if}
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export function usePath<TData extends PathData = PathData>(
|
|
102
|
+
options?: UsePathOptions
|
|
103
|
+
): UsePathReturn<TData> {
|
|
104
|
+
const engine = options?.engine ?? new PathEngineClass();
|
|
105
|
+
|
|
106
|
+
// Reactive snapshot via $state rune
|
|
107
|
+
let _snapshot: PathSnapshot<TData> | null = $state(
|
|
108
|
+
engine.snapshot() as PathSnapshot<TData> | null
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Subscribe to engine events
|
|
112
|
+
const unsubscribe = engine.subscribe((event: PathEvent) => {
|
|
113
|
+
if (event.type === "stateChanged" || event.type === "resumed") {
|
|
114
|
+
_snapshot = event.snapshot as PathSnapshot<TData>;
|
|
115
|
+
} else if (event.type === "completed" || event.type === "cancelled") {
|
|
116
|
+
_snapshot = null;
|
|
117
|
+
}
|
|
118
|
+
options?.onEvent?.(event);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Auto-cleanup when component is destroyed
|
|
122
|
+
onDestroy(unsubscribe);
|
|
123
|
+
|
|
124
|
+
const start = (path: PathDefinition<any>, initialData: PathData = {}): Promise<void> =>
|
|
125
|
+
engine.start(path, initialData);
|
|
126
|
+
|
|
127
|
+
const startSubPath = (
|
|
128
|
+
path: PathDefinition<any>,
|
|
129
|
+
initialData: PathData = {},
|
|
130
|
+
meta?: Record<string, unknown>
|
|
131
|
+
): Promise<void> => engine.startSubPath(path, initialData, meta);
|
|
132
|
+
|
|
133
|
+
const next = (): Promise<void> => engine.next();
|
|
134
|
+
const previous = (): Promise<void> => engine.previous();
|
|
135
|
+
const cancel = (): Promise<void> => engine.cancel();
|
|
136
|
+
|
|
137
|
+
const goToStep = (stepId: string): Promise<void> => engine.goToStep(stepId);
|
|
138
|
+
const goToStepChecked = (stepId: string): Promise<void> => engine.goToStepChecked(stepId);
|
|
139
|
+
|
|
140
|
+
const setData = (<K extends string & keyof TData>(key: K, value: TData[K]): Promise<void> =>
|
|
141
|
+
engine.setData(key, value as unknown)) as UsePathReturn<TData>["setData"];
|
|
142
|
+
|
|
143
|
+
const restart = (path: PathDefinition<any>, initialData: PathData = {}): Promise<void> =>
|
|
144
|
+
engine.restart(path, initialData);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
get snapshot() { return _snapshot; },
|
|
148
|
+
start,
|
|
149
|
+
startSubPath,
|
|
150
|
+
next,
|
|
151
|
+
previous,
|
|
152
|
+
cancel,
|
|
153
|
+
goToStep,
|
|
154
|
+
goToStepChecked,
|
|
155
|
+
setData,
|
|
156
|
+
restart
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Context API for PathShell
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
const PATH_CONTEXT_KEY = Symbol("pathwrite-context");
|
|
165
|
+
|
|
166
|
+
export interface PathContext<TData extends PathData = PathData> {
|
|
167
|
+
readonly snapshot: PathSnapshot<TData> | null;
|
|
168
|
+
next: () => Promise<void>;
|
|
169
|
+
previous: () => Promise<void>;
|
|
170
|
+
cancel: () => Promise<void>;
|
|
171
|
+
goToStep: (stepId: string) => Promise<void>;
|
|
172
|
+
goToStepChecked: (stepId: string) => Promise<void>;
|
|
173
|
+
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
174
|
+
restart: () => Promise<void>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get the PathContext from a parent PathShell component.
|
|
179
|
+
* Use this inside step components to access the path engine.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```svelte
|
|
183
|
+
* <script lang="ts">
|
|
184
|
+
* import { getPathContext } from '@daltonr/pathwrite-svelte';
|
|
185
|
+
*
|
|
186
|
+
* const ctx = getPathContext();
|
|
187
|
+
* </script>
|
|
188
|
+
*
|
|
189
|
+
* <input value={ctx.snapshot?.data.name}
|
|
190
|
+
* oninput={(e) => ctx.setData('name', e.target.value)} />
|
|
191
|
+
* <button onclick={ctx.next}>Next</button>
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export function getPathContext<TData extends PathData = PathData>(): PathContext<TData> {
|
|
195
|
+
const ctx = getContext<PathContext<TData>>(PATH_CONTEXT_KEY);
|
|
196
|
+
if (!ctx) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
"getPathContext() must be called from a component inside a <PathShell>. " +
|
|
199
|
+
"Ensure the PathShell component is a parent in the component tree."
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return ctx;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Internal: Set the PathContext for child components.
|
|
207
|
+
* Used by PathShell component.
|
|
208
|
+
*/
|
|
209
|
+
export function setPathContext<TData extends PathData = PathData>(ctx: PathContext<TData>): void {
|
|
210
|
+
setContext(PATH_CONTEXT_KEY, ctx);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Helper for binding form inputs
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Create a two-way binding helper for form inputs.
|
|
219
|
+
* Returns an object with a reactive `value` property.
|
|
220
|
+
*
|
|
221
|
+
* @param getSnapshot - A getter function returning the current snapshot (e.g. `() => path.snapshot`)
|
|
222
|
+
* @param setData - The `setData` function from `usePath()`
|
|
223
|
+
* @param key - The data key to bind
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```svelte
|
|
227
|
+
* <script lang="ts">
|
|
228
|
+
* import { usePath, bindData } from '@daltonr/pathwrite-svelte';
|
|
229
|
+
*
|
|
230
|
+
* const path = usePath();
|
|
231
|
+
* const name = bindData(() => path.snapshot, path.setData, 'name');
|
|
232
|
+
* </script>
|
|
233
|
+
*
|
|
234
|
+
* <input value={name.value} oninput={(e) => name.value = e.target.value} />
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export function bindData<TData extends PathData, K extends string & keyof TData>(
|
|
238
|
+
getSnapshot: () => PathSnapshot<TData> | null,
|
|
239
|
+
setData: <Key extends string & keyof TData>(key: Key, value: TData[Key]) => Promise<void>,
|
|
240
|
+
key: K
|
|
241
|
+
): { readonly value: TData[K]; set: (value: TData[K]) => void } {
|
|
242
|
+
return {
|
|
243
|
+
get value(): TData[K] {
|
|
244
|
+
return (getSnapshot()?.data[key] ?? undefined) as TData[K];
|
|
245
|
+
},
|
|
246
|
+
set(value: TData[K]) {
|
|
247
|
+
setData(key, value);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Export PathShell component
|
|
253
|
+
export { default as PathShell } from "./PathShell.svelte";
|
|
254
|
+
|