@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 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.