@daltonr/pathwrite-solid 0.11.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 +351 -0
- package/dist/index.css +476 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +157 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
- package/src/index.tsx +597 -0
package/README.md
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# @daltonr/pathwrite-solid
|
|
2
|
+
|
|
3
|
+
SolidJS adapter for Pathwrite — exposes path engine state as a reactive `createSignal` accessor that integrates natively with SolidJS's fine-grained reactivity model.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @daltonr/pathwrite-core @daltonr/pathwrite-solid
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies: `solid-js >= 1.8.0`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// SignupFlow.tsx
|
|
19
|
+
import { PathShell } from "@daltonr/pathwrite-solid";
|
|
20
|
+
import "@daltonr/pathwrite-solid/styles.css";
|
|
21
|
+
import type { PathDefinition, PathData } from "@daltonr/pathwrite-core";
|
|
22
|
+
|
|
23
|
+
interface SignupData extends PathData {
|
|
24
|
+
name: string;
|
|
25
|
+
email: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const signupPath: PathDefinition<SignupData> = {
|
|
29
|
+
id: "signup",
|
|
30
|
+
steps: [
|
|
31
|
+
{ id: "details", title: "Your Details" },
|
|
32
|
+
{ id: "review", title: "Review" },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function SignupFlow() {
|
|
37
|
+
return (
|
|
38
|
+
<PathShell
|
|
39
|
+
path={signupPath}
|
|
40
|
+
initialData={{ name: "", email: "" }}
|
|
41
|
+
onComplete={(data) => console.log("Done!", data)}
|
|
42
|
+
steps={{
|
|
43
|
+
details: (snap) => <DetailsStep />,
|
|
44
|
+
review: (snap) => <ReviewStep />,
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// DetailsStep.tsx
|
|
53
|
+
import { usePathContext } from "@daltonr/pathwrite-solid";
|
|
54
|
+
|
|
55
|
+
export function DetailsStep() {
|
|
56
|
+
const { snapshot, setData } = usePathContext<SignupData>();
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div>
|
|
60
|
+
<input
|
|
61
|
+
value={snapshot().data.name}
|
|
62
|
+
onInput={(e) => setData("name", e.currentTarget.value)}
|
|
63
|
+
placeholder="Name"
|
|
64
|
+
/>
|
|
65
|
+
<input
|
|
66
|
+
value={snapshot().data.email}
|
|
67
|
+
onInput={(e) => setData("email", e.currentTarget.value)}
|
|
68
|
+
placeholder="Email"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Step components call `usePathContext()` to access engine state. `<PathShell>` provides the context automatically via `createContext` / `useContext`.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## usePath
|
|
80
|
+
|
|
81
|
+
`usePath<TData>()` creates an isolated path engine instance. The composable registers an `onCleanup` handler to unsubscribe from the engine when the reactive scope is disposed — no manual cleanup needed.
|
|
82
|
+
|
|
83
|
+
| Return value | Type | Description |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `snapshot` | `Accessor<PathSnapshot \| null>` | Current snapshot. Call `snapshot()` to read. `null` when no path is active. Tracked reactively when read inside JSX or effects. |
|
|
86
|
+
| `start(definition, data?)` | function | Start or re-start a path. |
|
|
87
|
+
| `next()` | function | Advance one step. Completes the path on the last step. |
|
|
88
|
+
| `previous()` | function | Go back one step. No-op on the first step of a top-level path. |
|
|
89
|
+
| `cancel()` | function | Cancel the active path or sub-path. |
|
|
90
|
+
| `goToStep(stepId)` | function | Jump to a step by ID, bypassing guards and `shouldSkip`. |
|
|
91
|
+
| `goToStepChecked(stepId)` | function | Jump to a step by ID, checking the relevant navigation guard first. |
|
|
92
|
+
| `setData(key, value)` | function | Update a single data field. Type-checked when `TData` is provided. |
|
|
93
|
+
| `resetStep()` | function | Re-run `onEnter` for the current step without changing step index. |
|
|
94
|
+
| `startSubPath(definition, data?, meta?)` | function | Push a sub-path. `meta` is echoed back to `onSubPathComplete` / `onSubPathCancel`. |
|
|
95
|
+
| `suspend()` | function | Suspend an async step while work completes. |
|
|
96
|
+
| `retry()` | function | Retry the current step after a suspension or error. |
|
|
97
|
+
| `restart()` | function | Tear down the active path without firing hooks and start fresh. |
|
|
98
|
+
| `validate()` | function | Set `snapshot().hasValidated` without navigating. Used to trigger inline errors across all tabs in a nested shell. |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## PathShell props
|
|
103
|
+
|
|
104
|
+
`<PathShell>` renders a progress indicator, step content area, validation messages, and navigation buttons. Step content is provided as a **`steps` map** whose keys match each step's `id`.
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
<PathShell
|
|
108
|
+
path={myPath}
|
|
109
|
+
steps={{
|
|
110
|
+
details: (snap) => <DetailsStep />,
|
|
111
|
+
review: (snap) => <ReviewStep />,
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Prop | Type | Default | Description |
|
|
117
|
+
|---|---|---|---|
|
|
118
|
+
| `path` | `PathDefinition` | required | The path to run. |
|
|
119
|
+
| `steps` | `Record<string, (snapshot: PathSnapshot) => JSX.Element>` | — | Step render functions keyed by step ID (or `formId` for `StepChoice` steps). |
|
|
120
|
+
| `initialData` | `PathData` | `{}` | Initial data passed to `engine.start()`. |
|
|
121
|
+
| `engine` | `PathEngine` | — | An externally-managed engine. When provided, `PathShell` skips its own `start()`. |
|
|
122
|
+
| `autoStart` | `boolean` | `true` | Start the path automatically on mount. Ignored when `engine` is provided. |
|
|
123
|
+
| `validationDisplay` | `"summary" \| "inline" \| "both"` | `"summary"` | Where `fieldErrors` are rendered. Use `"inline"` to suppress the summary and handle errors inside step components. |
|
|
124
|
+
| `footerLayout` | `"wizard" \| "form" \| "auto"` | `"auto"` | `"wizard"`: Back on left, Cancel+Submit on right. `"form"`: Cancel on left, Submit on right, no Back. `"auto"` picks `"form"` for single-step paths. |
|
|
125
|
+
| `hideProgress` | `boolean` | `false` | Hide the progress indicator. Also hidden automatically for single-step top-level paths. |
|
|
126
|
+
| `hideFooter` | `boolean` | `false` | Hide the footer entirely. The error panel is still shown on async failure. |
|
|
127
|
+
| `hideCancel` | `boolean` | `false` | Hide the Cancel button. |
|
|
128
|
+
| `validateWhen` | `boolean` | `false` | When it becomes `true`, calls `validate()` on the engine. Bind to the outer shell's `hasAttemptedNext` for nested shells. |
|
|
129
|
+
| `services` | `object \| null` | `null` | Services object passed through context to all step components. |
|
|
130
|
+
| `renderHeader` | `(snapshot) => JSX.Element` | — | Replace the default progress header. |
|
|
131
|
+
| `renderFooter` | `(snapshot, actions) => JSX.Element` | — | Replace the default navigation buttons. |
|
|
132
|
+
| `onComplete` | `(data: PathData) => void` | — | Called when the path completes. |
|
|
133
|
+
| `onCancel` | `(data: PathData) => void` | — | Called when the path is cancelled. |
|
|
134
|
+
| `onEvent` | `(event: PathEvent) => void` | — | Called for every engine event. |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## usePathContext
|
|
139
|
+
|
|
140
|
+
`usePathContext<TData, TServices>()` reads the engine instance provided by the nearest `<PathShell>` ancestor. It returns the same shape as `usePath` — `snapshot`, `next`, `previous`, `cancel`, `setData`, and all other action callbacks. The `snapshot` is the same `Accessor<PathSnapshot | null>` — call `snapshot()` to read the current value.
|
|
141
|
+
|
|
142
|
+
Pass `TData` for typed access to `snapshot()?.data` and `setData`; pass `TServices` to type the `services` field. Must be called inside a component that is a descendant of `<PathShell>`. Throws a clear error if called outside one.
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import { usePathContext } from "@daltonr/pathwrite-solid";
|
|
146
|
+
|
|
147
|
+
function DetailsStep() {
|
|
148
|
+
const { snapshot, setData } = usePathContext<SignupData>();
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<input
|
|
152
|
+
value={snapshot()?.data.name ?? ""}
|
|
153
|
+
onInput={(e) => setData("name", e.currentTarget.value)}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Complete example
|
|
162
|
+
|
|
163
|
+
A two-step job-application form with `fieldErrors` validation.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
// application-path.ts
|
|
167
|
+
import type { PathDefinition, PathData } from "@daltonr/pathwrite-solid";
|
|
168
|
+
|
|
169
|
+
export interface ApplicationData extends PathData {
|
|
170
|
+
firstName: string;
|
|
171
|
+
email: string;
|
|
172
|
+
coverNote: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const applicationPath: PathDefinition<ApplicationData> = {
|
|
176
|
+
id: "job-application",
|
|
177
|
+
steps: [
|
|
178
|
+
{
|
|
179
|
+
id: "details",
|
|
180
|
+
title: "Your Details",
|
|
181
|
+
fieldErrors: ({ data }) => ({
|
|
182
|
+
firstName: (data.firstName ?? "").trim().length < 2
|
|
183
|
+
? "First name must be at least 2 characters."
|
|
184
|
+
: undefined,
|
|
185
|
+
email: !(data.email ?? "").includes("@")
|
|
186
|
+
? "A valid email address is required."
|
|
187
|
+
: undefined,
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "cover-note",
|
|
192
|
+
title: "Cover Note",
|
|
193
|
+
fieldErrors: ({ data }) => ({
|
|
194
|
+
coverNote: (data.coverNote ?? "").trim().length < 20
|
|
195
|
+
? "Cover note must be at least 20 characters."
|
|
196
|
+
: undefined,
|
|
197
|
+
}),
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
// DetailsStep.tsx
|
|
205
|
+
import { usePathContext } from "@daltonr/pathwrite-solid";
|
|
206
|
+
import type { ApplicationData } from "./application-path";
|
|
207
|
+
|
|
208
|
+
export function DetailsStep() {
|
|
209
|
+
const { snapshot, setData } = usePathContext<ApplicationData>();
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<div>
|
|
213
|
+
<label>First name</label>
|
|
214
|
+
<input
|
|
215
|
+
value={snapshot()?.data.firstName ?? ""}
|
|
216
|
+
onInput={(e) => setData("firstName", e.currentTarget.value)}
|
|
217
|
+
/>
|
|
218
|
+
<Show when={snapshot()?.hasAttemptedNext && snapshot()?.fieldErrors.firstName}>
|
|
219
|
+
<p class="error">{snapshot()?.fieldErrors.firstName}</p>
|
|
220
|
+
</Show>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
// CoverNoteStep.tsx
|
|
228
|
+
import { usePathContext } from "@daltonr/pathwrite-solid";
|
|
229
|
+
import type { ApplicationData } from "./application-path";
|
|
230
|
+
|
|
231
|
+
export function CoverNoteStep() {
|
|
232
|
+
const { snapshot, setData } = usePathContext<ApplicationData>();
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div>
|
|
236
|
+
<label>Cover note</label>
|
|
237
|
+
<textarea
|
|
238
|
+
value={snapshot()?.data.coverNote ?? ""}
|
|
239
|
+
onInput={(e) => setData("coverNote", e.currentTarget.value)}
|
|
240
|
+
rows="6"
|
|
241
|
+
placeholder="Tell us why you're a great fit..."
|
|
242
|
+
/>
|
|
243
|
+
<Show when={snapshot()?.hasAttemptedNext && snapshot()?.fieldErrors.coverNote}>
|
|
244
|
+
<p class="error">{snapshot()?.fieldErrors.coverNote}</p>
|
|
245
|
+
</Show>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
// JobApplicationFlow.tsx — host component
|
|
253
|
+
import { PathShell } from "@daltonr/pathwrite-solid";
|
|
254
|
+
import "@daltonr/pathwrite-solid/styles.css";
|
|
255
|
+
import { applicationPath } from "./application-path";
|
|
256
|
+
import { DetailsStep } from "./DetailsStep";
|
|
257
|
+
import { CoverNoteStep } from "./CoverNoteStep";
|
|
258
|
+
|
|
259
|
+
export function JobApplicationFlow() {
|
|
260
|
+
return (
|
|
261
|
+
<PathShell
|
|
262
|
+
path={applicationPath}
|
|
263
|
+
initialData={{ firstName: "", email: "", coverNote: "" }}
|
|
264
|
+
onComplete={(data) => console.log("Application submitted:", data)}
|
|
265
|
+
steps={{
|
|
266
|
+
"details": (snap) => <DetailsStep />,
|
|
267
|
+
"cover-note": (snap) => <CoverNoteStep />,
|
|
268
|
+
}}
|
|
269
|
+
/>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**What this demonstrates:**
|
|
275
|
+
|
|
276
|
+
- `fieldErrors` on each step with auto-derived `canMoveNext`.
|
|
277
|
+
- `snapshot().hasAttemptedNext` gates inline error display.
|
|
278
|
+
- `usePathContext()` inside step components — provided automatically by `<PathShell>`.
|
|
279
|
+
- The `steps` map keyed by step ID, including hyphenated IDs like `"cover-note"`.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Styling
|
|
284
|
+
|
|
285
|
+
`<PathShell>` ships with no embedded styles. Import the optional stylesheet:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import "@daltonr/pathwrite-solid/styles.css";
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
All visual values are CSS custom properties:
|
|
292
|
+
|
|
293
|
+
```css
|
|
294
|
+
:root {
|
|
295
|
+
--pw-color-primary: #8b5cf6;
|
|
296
|
+
--pw-shell-radius: 12px;
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Resetting the path
|
|
303
|
+
|
|
304
|
+
**Option 1 — Toggle mount** (simplest):
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
function App() {
|
|
308
|
+
const [isActive, setIsActive] = createSignal(true);
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<Show when={isActive()} fallback={<button onClick={() => setIsActive(true)}>Try Again</button>}>
|
|
312
|
+
<PathShell
|
|
313
|
+
path={myPath}
|
|
314
|
+
onComplete={() => setIsActive(false)}
|
|
315
|
+
steps={{ ... }}
|
|
316
|
+
/>
|
|
317
|
+
</Show>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Option 2 — `restart()` via `usePath`** (in-place, no unmount):
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
function App() {
|
|
326
|
+
const { snapshot, start, restart, next, previous } = usePath();
|
|
327
|
+
|
|
328
|
+
onMount(() => start(myPath, {}));
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<Show when={snapshot()}>
|
|
332
|
+
<div>
|
|
333
|
+
{/* render step content */}
|
|
334
|
+
<button onClick={() => restart()}>Start Over</button>
|
|
335
|
+
</div>
|
|
336
|
+
</Show>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Further reading
|
|
344
|
+
|
|
345
|
+
- [SolidJS getting started guide](../../docs/getting-started/frameworks/solidjs.md)
|
|
346
|
+
- [Navigation guide](../../docs/guides/navigation.md)
|
|
347
|
+
- [Full docs](../../docs/README.md)
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
© 2026 Devjoy Ltd. MIT License.
|