@daltonr/pathwrite-react 0.3.0 → 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/README.md +289 -9
- package/dist/index.d.ts +33 -6
- package/dist/index.js +32 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +84 -18
package/README.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
React hooks over `@daltonr/pathwrite-core`. Exposes path state as reactive React state via `useSyncExternalStore`, with stable action callbacks and an optional context provider.
|
|
4
4
|
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @daltonr/pathwrite-core @daltonr/pathwrite-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Exported Types
|
|
12
|
+
|
|
13
|
+
For convenience, this package re-exports core types so you don't need to import from `@daltonr/pathwrite-core`:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import {
|
|
17
|
+
PathShell, // React-specific
|
|
18
|
+
usePath, // React-specific
|
|
19
|
+
usePathContext, // React-specific
|
|
20
|
+
PathProvider, // React-specific
|
|
21
|
+
PathEngine, // Re-exported from core (value + type)
|
|
22
|
+
PathData, // Re-exported from core
|
|
23
|
+
PathDefinition, // Re-exported from core
|
|
24
|
+
PathEvent, // Re-exported from core
|
|
25
|
+
PathSnapshot, // Re-exported from core
|
|
26
|
+
PathStep, // Re-exported from core
|
|
27
|
+
PathStepContext, // Re-exported from core
|
|
28
|
+
SerializedPathState // Re-exported from core
|
|
29
|
+
} from "@daltonr/pathwrite-react";
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
5
34
|
## Setup
|
|
6
35
|
|
|
7
36
|
### Option A — `usePath` hook (component-scoped)
|
|
@@ -26,7 +55,7 @@ function MyPathHost() {
|
|
|
26
55
|
<p>Step {snapshot.stepIndex + 1} of {snapshot.stepCount}</p>
|
|
27
56
|
<button onClick={previous} disabled={snapshot.isNavigating}>Back</button>
|
|
28
57
|
<button onClick={next} disabled={snapshot.isNavigating}>
|
|
29
|
-
{snapshot.isLastStep ? "
|
|
58
|
+
{snapshot.isLastStep ? "Complete" : "Next"}
|
|
30
59
|
</button>
|
|
31
60
|
<button onClick={cancel}>Cancel</button>
|
|
32
61
|
</>
|
|
@@ -78,6 +107,7 @@ function NavButtons() {
|
|
|
78
107
|
|
|
79
108
|
| Option | Type | Description |
|
|
80
109
|
|--------|------|-------------|
|
|
110
|
+
| `engine` | `PathEngine` | An externally-managed engine (e.g. from `createPersistedEngine()`). When provided, `usePath` subscribes to it instead of creating a new one; snapshot is seeded immediately from the engine's current state. The caller is responsible for the engine's lifecycle. Must be a stable reference. |
|
|
81
111
|
| `onEvent` | `(event: PathEvent) => void` | Called for every engine event. The callback ref is kept current — changing it does **not** re-subscribe to the engine. |
|
|
82
112
|
|
|
83
113
|
### Return value
|
|
@@ -93,6 +123,7 @@ function NavButtons() {
|
|
|
93
123
|
| `goToStep(stepId)` | `function` | Jump directly to a step by ID. Calls `onLeave` / `onEnter` but bypasses guards and `shouldSkip`. |
|
|
94
124
|
| `goToStepChecked(stepId)` | `function` | Jump to a step by ID, checking `canMoveNext` (forward) or `canMovePrevious` (backward) first. Navigation is blocked if the guard returns false. |
|
|
95
125
|
| `setData(key, value)` | `function` | Update a single data value; triggers re-render via `stateChanged`. When `TData` is specified, `key` and `value` are type-checked against your data shape. |
|
|
126
|
+
| `restart(definition, data?)` | `function` | Tear down any active path (without firing hooks) and start the given path fresh. Safe to call at any time. Use for "Start over" / retry flows. |
|
|
96
127
|
|
|
97
128
|
All action callbacks are **referentially stable** — safe to pass as props or include in dependency arrays without causing unnecessary re-renders.
|
|
98
129
|
|
|
@@ -163,15 +194,16 @@ import { PathShell } from "@daltonr/pathwrite-react";
|
|
|
163
194
|
| Prop | Type | Default | Description |
|
|
164
195
|
|------|------|---------|-------------|
|
|
165
196
|
| `path` | `PathDefinition` | *required* | The path to run. |
|
|
197
|
+
| `engine` | `PathEngine` | — | An externally-managed engine. When provided, `PathShell` skips its own `start()` and drives the UI from this engine. |
|
|
166
198
|
| `steps` | `Record<string, ReactNode>` | *required* | Map of step ID → content to render. |
|
|
167
199
|
| `initialData` | `PathData` | `{}` | Initial data passed to `engine.start()`. |
|
|
168
200
|
| `autoStart` | `boolean` | `true` | Start the path automatically on mount. |
|
|
169
201
|
| `onComplete` | `(data: PathData) => void` | — | Called when the path completes. |
|
|
170
202
|
| `onCancel` | `(data: PathData) => void` | — | Called when the path is cancelled. |
|
|
171
203
|
| `onEvent` | `(event: PathEvent) => void` | — | Called for every engine event. |
|
|
172
|
-
| `backLabel` | `string` | `"
|
|
204
|
+
| `backLabel` | `string` | `"Previous"` | Previous button label. |
|
|
173
205
|
| `nextLabel` | `string` | `"Next"` | Next button label. |
|
|
174
|
-
| `
|
|
206
|
+
| `completeLabel` | `string` | `"Complete"` | Complete button label (last step). |
|
|
175
207
|
| `cancelLabel` | `string` | `"Cancel"` | Cancel button label. |
|
|
176
208
|
| `hideCancel` | `boolean` | `false` | Hide the Cancel button. |
|
|
177
209
|
| `hideProgress` | `boolean` | `false` | Hide the progress indicator. |
|
|
@@ -194,14 +226,14 @@ Use `renderHeader` and `renderFooter` to replace the built-in progress bar or na
|
|
|
194
226
|
<div>
|
|
195
227
|
<button onClick={actions.previous} disabled={snapshot.isFirstStep}>Back</button>
|
|
196
228
|
<button onClick={actions.next} disabled={!snapshot.canMoveNext}>
|
|
197
|
-
{snapshot.isLastStep ? "
|
|
229
|
+
{snapshot.isLastStep ? "Complete" : "Next"}
|
|
198
230
|
</button>
|
|
199
231
|
</div>
|
|
200
232
|
)}
|
|
201
233
|
/>
|
|
202
234
|
```
|
|
203
235
|
|
|
204
|
-
`PathShellActions` contains: `next`, `previous`, `cancel`, `goToStep`, `goToStepChecked`, `setData`.
|
|
236
|
+
`PathShellActions` contains: `next`, `previous`, `cancel`, `goToStep`, `goToStepChecked`, `setData`, `restart`.
|
|
205
237
|
|
|
206
238
|
### Context sharing
|
|
207
239
|
|
|
@@ -245,11 +277,259 @@ All visual values are CSS custom properties (`--pw-*`), so you can theme without
|
|
|
245
277
|
}
|
|
246
278
|
```
|
|
247
279
|
|
|
280
|
+
### Available CSS Custom Properties
|
|
281
|
+
|
|
282
|
+
**Layout:**
|
|
283
|
+
- `--pw-shell-max-width` — Maximum width of the shell (default: `720px`)
|
|
284
|
+
- `--pw-shell-padding` — Internal padding (default: `24px`)
|
|
285
|
+
- `--pw-shell-gap` — Gap between header, body, footer (default: `20px`)
|
|
286
|
+
- `--pw-shell-radius` — Border radius for cards (default: `10px`)
|
|
287
|
+
|
|
288
|
+
**Colors:**
|
|
289
|
+
- `--pw-color-bg` — Background color (default: `#ffffff`)
|
|
290
|
+
- `--pw-color-border` — Border color (default: `#dbe4f0`)
|
|
291
|
+
- `--pw-color-text` — Primary text color (default: `#1f2937`)
|
|
292
|
+
- `--pw-color-muted` — Muted text color (default: `#5b677a`)
|
|
293
|
+
- `--pw-color-primary` — Primary/accent color (default: `#2563eb`)
|
|
294
|
+
- `--pw-color-primary-light` — Light primary for backgrounds (default: `rgba(37, 99, 235, 0.12)`)
|
|
295
|
+
- `--pw-color-btn-bg` — Button background (default: `#f8fbff`)
|
|
296
|
+
- `--pw-color-btn-border` — Button border (default: `#c2d0e5`)
|
|
297
|
+
|
|
298
|
+
**Validation:**
|
|
299
|
+
- `--pw-color-error` — Error text color (default: `#dc2626`)
|
|
300
|
+
- `--pw-color-error-bg` — Error background (default: `#fef2f2`)
|
|
301
|
+
- `--pw-color-error-border` — Error border (default: `#fecaca`)
|
|
302
|
+
|
|
303
|
+
**Progress Indicator:**
|
|
304
|
+
- `--pw-dot-size` — Step dot size (default: `32px`)
|
|
305
|
+
- `--pw-dot-font-size` — Font size inside dots (default: `13px`)
|
|
306
|
+
- `--pw-track-height` — Progress track height (default: `4px`)
|
|
307
|
+
|
|
308
|
+
**Buttons:**
|
|
309
|
+
- `--pw-btn-padding` — Button padding (default: `8px 16px`)
|
|
310
|
+
- `--pw-btn-radius` — Button border radius (default: `6px`)
|
|
311
|
+
|
|
248
312
|
---
|
|
249
313
|
|
|
250
|
-
##
|
|
314
|
+
## Sub-Paths
|
|
315
|
+
|
|
316
|
+
Sub-paths allow you to nest multi-step workflows. Common use cases include:
|
|
317
|
+
- Running a child workflow per collection item (e.g., approve each document)
|
|
318
|
+
- Conditional drill-down flows (e.g., "Add payment method" modal)
|
|
319
|
+
- Reusable wizard components
|
|
320
|
+
|
|
321
|
+
### Basic Sub-Path Flow
|
|
322
|
+
|
|
323
|
+
When a sub-path is active:
|
|
324
|
+
- The shell switches to show the sub-path's steps
|
|
325
|
+
- The progress bar displays sub-path steps (not main path steps)
|
|
326
|
+
- Pressing Back on the first sub-path step **cancels** the sub-path and returns to the parent
|
|
327
|
+
- `usePathContext()` returns the **sub-path** snapshot, not the parent's
|
|
328
|
+
|
|
329
|
+
### Complete Example: Approver Collection
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
import type { PathData, PathDefinition } from "@daltonr/pathwrite-core";
|
|
333
|
+
|
|
334
|
+
// Sub-path data shape
|
|
335
|
+
interface ApproverReviewData extends PathData {
|
|
336
|
+
decision: "approve" | "reject" | "";
|
|
337
|
+
comments: string;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Main path data shape
|
|
341
|
+
interface ApprovalWorkflowData extends PathData {
|
|
342
|
+
documentTitle: string;
|
|
343
|
+
approvers: string[];
|
|
344
|
+
approvals: Array<{ approver: string; decision: string; comments: string }>;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Define the sub-path (approver review wizard)
|
|
348
|
+
const approverReviewPath: PathDefinition<ApproverReviewData> = {
|
|
349
|
+
id: "approver-review",
|
|
350
|
+
steps: [
|
|
351
|
+
{ id: "review", title: "Review Document" },
|
|
352
|
+
{
|
|
353
|
+
id: "decision",
|
|
354
|
+
title: "Make Decision",
|
|
355
|
+
canMoveNext: ({ data }) =>
|
|
356
|
+
data.decision === "approve" || data.decision === "reject",
|
|
357
|
+
validationMessages: ({ data }) =>
|
|
358
|
+
!data.decision ? ["Please select Approve or Reject"] : []
|
|
359
|
+
},
|
|
360
|
+
{ id: "comments", title: "Add Comments" }
|
|
361
|
+
]
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Define the main path
|
|
365
|
+
const approvalWorkflowPath: PathDefinition<ApprovalWorkflowData> = {
|
|
366
|
+
id: "approval-workflow",
|
|
367
|
+
steps: [
|
|
368
|
+
{
|
|
369
|
+
id: "setup",
|
|
370
|
+
title: "Setup Approval",
|
|
371
|
+
canMoveNext: ({ data }) =>
|
|
372
|
+
(data.documentTitle ?? "").trim().length > 0 &&
|
|
373
|
+
data.approvers.length > 0
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: "run-approvals",
|
|
377
|
+
title: "Collect Approvals",
|
|
378
|
+
// Block "Next" until all approvers have completed their reviews
|
|
379
|
+
canMoveNext: ({ data }) =>
|
|
380
|
+
data.approvals.length === data.approvers.length,
|
|
381
|
+
validationMessages: ({ data }) => {
|
|
382
|
+
const remaining = data.approvers.length - data.approvals.length;
|
|
383
|
+
return remaining > 0
|
|
384
|
+
? [`${remaining} approver(s) pending review`]
|
|
385
|
+
: [];
|
|
386
|
+
},
|
|
387
|
+
// When an approver finishes their sub-path, record the result
|
|
388
|
+
onSubPathComplete(subPathId, subPathData, ctx, meta) {
|
|
389
|
+
const approverName = meta?.approverName as string;
|
|
390
|
+
const result = subPathData as ApproverReviewData;
|
|
391
|
+
return {
|
|
392
|
+
approvals: [
|
|
393
|
+
...ctx.data.approvals,
|
|
394
|
+
{
|
|
395
|
+
approver: approverName,
|
|
396
|
+
decision: result.decision,
|
|
397
|
+
comments: result.comments
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
// If an approver cancels (presses Back on first step), you can track it
|
|
403
|
+
onSubPathCancel(subPathId, subPathData, ctx, meta) {
|
|
404
|
+
console.log(`${meta?.approverName} cancelled their review`);
|
|
405
|
+
// Optionally return data changes, or just log
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
{ id: "summary", title: "Summary" }
|
|
409
|
+
]
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// Component
|
|
413
|
+
function ApprovalWorkflow() {
|
|
414
|
+
const { startSubPath } = usePathContext<ApprovalWorkflowData>();
|
|
415
|
+
|
|
416
|
+
function launchReviewForApprover(approverName: string, index: number) {
|
|
417
|
+
// Pass correlation data via `meta` — it's echoed back to onSubPathComplete
|
|
418
|
+
startSubPath(
|
|
419
|
+
approverReviewPath,
|
|
420
|
+
{ decision: "", comments: "" },
|
|
421
|
+
{ approverName, approverIndex: index }
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<PathShell
|
|
427
|
+
path={approvalWorkflowPath}
|
|
428
|
+
initialData={{ documentTitle: "", approvers: [], approvals: [] }}
|
|
429
|
+
steps={{
|
|
430
|
+
setup: <SetupStep />,
|
|
431
|
+
"run-approvals": <RunApprovalsStep onLaunchReview={launchReviewForApprover} />,
|
|
432
|
+
summary: <SummaryStep />,
|
|
433
|
+
// Sub-path steps (must be co-located in the same steps map)
|
|
434
|
+
review: <ReviewDocumentStep />,
|
|
435
|
+
decision: <MakeDecisionStep />,
|
|
436
|
+
comments: <AddCommentsStep />
|
|
437
|
+
}}
|
|
438
|
+
/>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Key Notes
|
|
444
|
+
|
|
445
|
+
**1. Sub-path steps must be co-located with main path steps**
|
|
446
|
+
All step content (main path + sub-path steps) lives in the same `steps` prop. When a sub-path is active, the shell renders the sub-path's step content. This means:
|
|
447
|
+
- Parent and sub-path step IDs **must not collide** (e.g., don't use `summary` in both)
|
|
448
|
+
- Sub-path step components can access parent data by referencing the parent path definition, but `usePathContext()` returns the **sub-path** snapshot
|
|
449
|
+
|
|
450
|
+
**2. The `meta` correlation field**
|
|
451
|
+
`startSubPath` accepts an optional third argument (`meta`) that is returned unchanged to `onSubPathComplete` and `onSubPathCancel`. Use it to correlate which collection item triggered the sub-path:
|
|
452
|
+
|
|
453
|
+
```tsx
|
|
454
|
+
startSubPath(subPath, initialData, { itemIndex: 3, itemId: "abc" });
|
|
455
|
+
|
|
456
|
+
// In the parent step:
|
|
457
|
+
onSubPathComplete(subPathId, subPathData, ctx, meta) {
|
|
458
|
+
const itemIndex = meta?.itemIndex; // 3
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**3. Progress bar switches during sub-paths**
|
|
463
|
+
When `snapshot.nestingLevel > 0`, you're in a sub-path. The `steps` array in the snapshot contains the sub-path's steps, not the main path's. The default PathShell progress bar shows sub-path progress. You can check `nestingLevel` to show a breadcrumb or "back to main flow" indicator.
|
|
251
464
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
465
|
+
**4. Accessing parent path data from sub-path components**
|
|
466
|
+
There is currently no `useParentPathContext()` hook. If a sub-path step needs parent data (e.g., the document title), pass it via `initialData` when calling `startSubPath`:
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
startSubPath(approverReviewPath, {
|
|
470
|
+
decision: "",
|
|
471
|
+
comments: "",
|
|
472
|
+
documentTitle: snapshot.data.documentTitle // copy from parent
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Guards and Lifecycle Hooks
|
|
479
|
+
|
|
480
|
+
### Defensive Guards (Important!)
|
|
481
|
+
|
|
482
|
+
**Guards and `validationMessages` are evaluated *before* `onEnter` runs on first entry.**
|
|
483
|
+
|
|
484
|
+
If you access fields in a guard that `onEnter` is supposed to initialize, the guard will throw a `TypeError` on startup. Write guards defensively using nullish coalescing:
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
// ✗ Unsafe — crashes if data.name is undefined
|
|
488
|
+
canMoveNext: ({ data }) => data.name.trim().length > 0
|
|
489
|
+
|
|
490
|
+
// ✓ Safe — handles undefined gracefully
|
|
491
|
+
canMoveNext: ({ data }) => (data.name ?? "").trim().length > 0
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Alternatively, pass `initialData` to `start()` / `<PathShell>` so all fields are present from the first snapshot:
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
<PathShell path={myPath} initialData={{ name: "", age: 0 }} />
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
If a guard throws, the engine catches it, logs a warning, and returns `true` (allow navigation) as a safe default.
|
|
501
|
+
|
|
502
|
+
### Async Guards and Validation Messages
|
|
503
|
+
|
|
504
|
+
Guards and `validationMessages` must be **synchronous** for inclusion in snapshots. Async functions are detected and warned about:
|
|
505
|
+
- Async `canMoveNext` / `canMovePrevious` default to `true` (optimistic)
|
|
506
|
+
- Async `validationMessages` default to `[]`
|
|
507
|
+
|
|
508
|
+
The async version is still enforced during actual navigation (when you call `next()` / `previous()`), but the snapshot won't reflect the pending state. If you need async validation, perform it in the guard and store the result in `data` so the guard can read it synchronously.
|
|
509
|
+
|
|
510
|
+
### `isFirstEntry` Flag
|
|
511
|
+
|
|
512
|
+
The `PathStepContext` passed to all hooks includes an `isFirstEntry: boolean` flag. It's `true` the first time a step is visited, `false` on re-entry (e.g., after navigating back then forward again).
|
|
513
|
+
|
|
514
|
+
Use it to distinguish initialization from re-entry:
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
{
|
|
518
|
+
id: "details",
|
|
519
|
+
onEnter: ({ isFirstEntry, data }) => {
|
|
520
|
+
if (isFirstEntry) {
|
|
521
|
+
// Only pre-fill on first visit, not when returning via Back
|
|
522
|
+
return { name: "Default Name" };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Important:** `onEnter` fires every time you enter the step. If you want "initialize once" behavior, either:
|
|
529
|
+
1. Use `isFirstEntry` to conditionally return data
|
|
530
|
+
2. Provide `initialData` to `start()` instead of using `onEnter`
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## Design notes
|
|
255
535
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import type { PropsWithChildren, ReactElement, ReactNode } from "react";
|
|
2
|
-
import { PathData, PathDefinition, PathEvent, PathSnapshot } from "@daltonr/pathwrite-core";
|
|
2
|
+
import { PathData, PathDefinition, PathEngine, PathEvent, PathSnapshot } from "@daltonr/pathwrite-core";
|
|
3
3
|
export interface UsePathOptions {
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* An externally-managed `PathEngine` to subscribe to — for example, the engine
|
|
6
|
+
* returned by `createPersistedEngine()` from `@daltonr/pathwrite-store-http`.
|
|
7
|
+
*
|
|
8
|
+
* When provided:
|
|
9
|
+
* - `usePath` will **not** create its own engine.
|
|
10
|
+
* - The snapshot is seeded immediately from the engine's current state.
|
|
11
|
+
* - The engine lifecycle (start / cleanup) is the **caller's responsibility**.
|
|
12
|
+
* - `PathShell` will skip its own `autoStart` call.
|
|
13
|
+
*/
|
|
14
|
+
engine?: PathEngine;
|
|
15
|
+
/** Called for every engine event (stateChanged, completed, cancelled, resumed). The callback ref is kept current — changing it does **not** re-subscribe to the engine. */
|
|
5
16
|
onEvent?: (event: PathEvent) => void;
|
|
6
17
|
}
|
|
7
18
|
export interface UsePathReturn<TData extends PathData = PathData> {
|
|
@@ -23,6 +34,12 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
23
34
|
goToStepChecked: (stepId: string) => void;
|
|
24
35
|
/** 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. */
|
|
25
36
|
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Tear down any active path (without firing hooks) and immediately start the
|
|
39
|
+
* given path fresh. Safe to call whether or not a path is currently active.
|
|
40
|
+
* Use for "Start over" / retry flows without remounting the component.
|
|
41
|
+
*/
|
|
42
|
+
restart: (path: PathDefinition<any>, initialData?: PathData) => void;
|
|
26
43
|
}
|
|
27
44
|
export type PathProviderProps = PropsWithChildren<{
|
|
28
45
|
/** Forwarded to the internal usePath hook. */
|
|
@@ -45,6 +62,12 @@ export declare function usePathContext<TData extends PathData = PathData>(): Use
|
|
|
45
62
|
export interface PathShellProps {
|
|
46
63
|
/** The path definition to drive. */
|
|
47
64
|
path: PathDefinition<any>;
|
|
65
|
+
/**
|
|
66
|
+
* An externally-managed engine — for example, the engine returned by
|
|
67
|
+
* `createPersistedEngine()`. When supplied, `PathShell` will skip its own
|
|
68
|
+
* `start()` call and drive the UI from the provided engine instead.
|
|
69
|
+
*/
|
|
70
|
+
engine?: PathEngine;
|
|
48
71
|
/** Map of step ID → content. The shell renders `steps[snapshot.stepId]` for the current step. */
|
|
49
72
|
steps: Record<string, ReactNode>;
|
|
50
73
|
/** Initial data passed to `engine.start()`. */
|
|
@@ -57,12 +80,12 @@ export interface PathShellProps {
|
|
|
57
80
|
onCancel?: (data: PathData) => void;
|
|
58
81
|
/** Called for every engine event. */
|
|
59
82
|
onEvent?: (event: PathEvent) => void;
|
|
60
|
-
/** Label for the
|
|
83
|
+
/** Label for the Previous button. Defaults to `"Previous"`. */
|
|
61
84
|
backLabel?: string;
|
|
62
85
|
/** Label for the Next button. Defaults to `"Next"`. */
|
|
63
86
|
nextLabel?: string;
|
|
64
|
-
/** Label for the
|
|
65
|
-
|
|
87
|
+
/** Label for the Complete button (shown on the last step). Defaults to `"Complete"`. */
|
|
88
|
+
completeLabel?: string;
|
|
66
89
|
/** Label for the Cancel button. Defaults to `"Cancel"`. */
|
|
67
90
|
cancelLabel?: string;
|
|
68
91
|
/** If true, hide the Cancel button. Defaults to `false`. */
|
|
@@ -83,6 +106,8 @@ export interface PathShellActions {
|
|
|
83
106
|
goToStep: (stepId: string) => void;
|
|
84
107
|
goToStepChecked: (stepId: string) => void;
|
|
85
108
|
setData: (key: string, value: unknown) => void;
|
|
109
|
+
/** Restart the shell's current path with its current `initialData`. */
|
|
110
|
+
restart: () => void;
|
|
86
111
|
}
|
|
87
112
|
/**
|
|
88
113
|
* Default UI shell that renders a progress indicator, step content, and navigation
|
|
@@ -100,4 +125,6 @@ export interface PathShellActions {
|
|
|
100
125
|
* />
|
|
101
126
|
* ```
|
|
102
127
|
*/
|
|
103
|
-
export declare function PathShell({ path: pathDef, steps, initialData, autoStart, onComplete, onCancel, onEvent, backLabel, nextLabel,
|
|
128
|
+
export declare function PathShell({ path: pathDef, engine: externalEngine, steps, initialData, autoStart, onComplete, onCancel, onEvent, backLabel, nextLabel, completeLabel, cancelLabel, hideCancel, hideProgress, className, renderHeader, renderFooter, }: PathShellProps): ReactElement;
|
|
129
|
+
export type { PathData, PathDefinition, PathEvent, PathSnapshot, PathStep, PathStepContext, SerializedPathState } from "@daltonr/pathwrite-core";
|
|
130
|
+
export { PathEngine } from "@daltonr/pathwrite-core";
|
package/dist/index.js
CHANGED
|
@@ -4,17 +4,31 @@ import { PathEngine } from "@daltonr/pathwrite-core";
|
|
|
4
4
|
// usePath hook
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
export function usePath(options) {
|
|
7
|
-
//
|
|
7
|
+
// Use provided engine or create a stable new one for this hook's lifetime.
|
|
8
|
+
// options.engine must be a stable reference (don't recreate on every render).
|
|
8
9
|
const engineRef = useRef(null);
|
|
9
10
|
if (engineRef.current === null) {
|
|
10
|
-
engineRef.current = new PathEngine();
|
|
11
|
+
engineRef.current = options?.engine ?? new PathEngine();
|
|
11
12
|
}
|
|
12
13
|
const engine = engineRef.current;
|
|
13
14
|
// Keep the onEvent callback current without changing the subscribe identity
|
|
14
15
|
const onEventRef = useRef(options?.onEvent);
|
|
15
16
|
onEventRef.current = options?.onEvent;
|
|
16
|
-
//
|
|
17
|
+
// Seed immediately from existing engine state — essential when restoring a
|
|
18
|
+
// persisted path (the engine is already started before usePath is called).
|
|
19
|
+
// We track whether we've seeded to avoid calling engine.snapshot() on every
|
|
20
|
+
// re-render (React evaluates useRef's argument each time).
|
|
21
|
+
const seededRef = useRef(false);
|
|
17
22
|
const snapshotRef = useRef(null);
|
|
23
|
+
if (!seededRef.current) {
|
|
24
|
+
seededRef.current = true;
|
|
25
|
+
try {
|
|
26
|
+
snapshotRef.current = engine.snapshot();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
snapshotRef.current = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
18
32
|
const subscribe = useCallback((callback) => engine.subscribe((event) => {
|
|
19
33
|
if (event.type === "stateChanged" || event.type === "resumed") {
|
|
20
34
|
snapshotRef.current = event.snapshot;
|
|
@@ -36,7 +50,8 @@ export function usePath(options) {
|
|
|
36
50
|
const goToStep = useCallback((stepId) => engine.goToStep(stepId), [engine]);
|
|
37
51
|
const goToStepChecked = useCallback((stepId) => engine.goToStepChecked(stepId), [engine]);
|
|
38
52
|
const setData = useCallback((key, value) => engine.setData(key, value), [engine]);
|
|
39
|
-
|
|
53
|
+
const restart = useCallback((path, initialData = {}) => engine.restart(path, initialData), [engine]);
|
|
54
|
+
return { snapshot, start, startSubPath, next, previous, cancel, goToStep, goToStepChecked, setData, restart };
|
|
40
55
|
}
|
|
41
56
|
// ---------------------------------------------------------------------------
|
|
42
57
|
// Context + Provider
|
|
@@ -80,8 +95,9 @@ export function usePathContext() {
|
|
|
80
95
|
* />
|
|
81
96
|
* ```
|
|
82
97
|
*/
|
|
83
|
-
export function PathShell({ path: pathDef, steps, initialData = {}, autoStart = true, onComplete, onCancel, onEvent, backLabel = "
|
|
98
|
+
export function PathShell({ path: pathDef, engine: externalEngine, steps, initialData = {}, autoStart = true, onComplete, onCancel, onEvent, backLabel = "Previous", nextLabel = "Next", completeLabel = "Complete", cancelLabel = "Cancel", hideCancel = false, hideProgress = false, className, renderHeader, renderFooter, }) {
|
|
84
99
|
const pathReturn = usePath({
|
|
100
|
+
engine: externalEngine,
|
|
85
101
|
onEvent(event) {
|
|
86
102
|
onEvent?.(event);
|
|
87
103
|
if (event.type === "completed")
|
|
@@ -90,11 +106,12 @@ export function PathShell({ path: pathDef, steps, initialData = {}, autoStart =
|
|
|
90
106
|
onCancel?.(event.data);
|
|
91
107
|
}
|
|
92
108
|
});
|
|
93
|
-
const { snapshot, start, next, previous, cancel, goToStep, goToStepChecked, setData } = pathReturn;
|
|
94
|
-
// Auto-start on mount
|
|
109
|
+
const { snapshot, start, next, previous, cancel, goToStep, goToStepChecked, setData, restart } = pathReturn;
|
|
110
|
+
// Auto-start on mount — skipped when an external engine is provided since
|
|
111
|
+
// the caller is responsible for starting it (e.g. via createPersistedEngine).
|
|
95
112
|
const startedRef = useRef(false);
|
|
96
113
|
useEffect(() => {
|
|
97
|
-
if (autoStart && !startedRef.current) {
|
|
114
|
+
if (autoStart && !startedRef.current && !externalEngine) {
|
|
98
115
|
startedRef.current = true;
|
|
99
116
|
start(pathDef, initialData);
|
|
100
117
|
}
|
|
@@ -109,7 +126,10 @@ export function PathShell({ path: pathDef, steps, initialData = {}, autoStart =
|
|
|
109
126
|
onClick: () => start(pathDef, initialData)
|
|
110
127
|
}, "Start"))));
|
|
111
128
|
}
|
|
112
|
-
const actions = {
|
|
129
|
+
const actions = {
|
|
130
|
+
next, previous, cancel, goToStep, goToStepChecked, setData,
|
|
131
|
+
restart: () => restart(pathDef, initialData)
|
|
132
|
+
};
|
|
113
133
|
return createElement(PathContext.Provider, { value: pathReturn }, createElement("div", { className: cls("pw-shell", className) },
|
|
114
134
|
// Header — progress indicator
|
|
115
135
|
!hideProgress && (renderHeader
|
|
@@ -123,7 +143,7 @@ export function PathShell({ path: pathDef, steps, initialData = {}, autoStart =
|
|
|
123
143
|
renderFooter
|
|
124
144
|
? renderFooter(snapshot, actions)
|
|
125
145
|
: defaultFooter(snapshot, actions, {
|
|
126
|
-
backLabel, nextLabel,
|
|
146
|
+
backLabel, nextLabel, completeLabel, cancelLabel, hideCancel
|
|
127
147
|
})));
|
|
128
148
|
}
|
|
129
149
|
// ---------------------------------------------------------------------------
|
|
@@ -154,7 +174,7 @@ function defaultFooter(snapshot, actions, labels) {
|
|
|
154
174
|
className: "pw-shell__btn pw-shell__btn--next",
|
|
155
175
|
disabled: snapshot.isNavigating || !snapshot.canMoveNext,
|
|
156
176
|
onClick: actions.next
|
|
157
|
-
}, snapshot.isLastStep ? labels.
|
|
177
|
+
}, snapshot.isLastStep ? labels.completeLabel : labels.nextLabel)));
|
|
158
178
|
}
|
|
159
179
|
// ---------------------------------------------------------------------------
|
|
160
180
|
// Helpers
|
|
@@ -162,4 +182,5 @@ function defaultFooter(snapshot, actions, labels) {
|
|
|
162
182
|
function cls(...parts) {
|
|
163
183
|
return parts.filter(Boolean).join(" ");
|
|
164
184
|
}
|
|
185
|
+
export { PathEngine } from "@daltonr/pathwrite-core";
|
|
165
186
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,aAAa,EACb,WAAW,EACX,UAAU,EACV,SAAS,EACT,MAAM,EACN,oBAAoB,EACrB,MAAM,OAAO,CAAC;AAEf,OAAO,EAGL,UAAU,EAGX,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daltonr/pathwrite-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "React adapter for @daltonr/pathwrite-core — hooks, context provider, and optional <PathShell> default UI.",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"react": ">=18.0.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@daltonr/pathwrite-core": "^0.
|
|
48
|
+
"@daltonr/pathwrite-core": "^0.5.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"react": "^18.3.1",
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,18 @@ import {
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
|
|
23
23
|
export interface UsePathOptions {
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* An externally-managed `PathEngine` to subscribe to — for example, the engine
|
|
26
|
+
* returned by `createPersistedEngine()` from `@daltonr/pathwrite-store-http`.
|
|
27
|
+
*
|
|
28
|
+
* When provided:
|
|
29
|
+
* - `usePath` will **not** create its own engine.
|
|
30
|
+
* - The snapshot is seeded immediately from the engine's current state.
|
|
31
|
+
* - The engine lifecycle (start / cleanup) is the **caller's responsibility**.
|
|
32
|
+
* - `PathShell` will skip its own `autoStart` call.
|
|
33
|
+
*/
|
|
34
|
+
engine?: PathEngine;
|
|
35
|
+
/** Called for every engine event (stateChanged, completed, cancelled, resumed). The callback ref is kept current — changing it does **not** re-subscribe to the engine. */
|
|
25
36
|
onEvent?: (event: PathEvent) => void;
|
|
26
37
|
}
|
|
27
38
|
|
|
@@ -44,6 +55,12 @@ export interface UsePathReturn<TData extends PathData = PathData> {
|
|
|
44
55
|
goToStepChecked: (stepId: string) => void;
|
|
45
56
|
/** 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. */
|
|
46
57
|
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => void;
|
|
58
|
+
/**
|
|
59
|
+
* Tear down any active path (without firing hooks) and immediately start the
|
|
60
|
+
* given path fresh. Safe to call whether or not a path is currently active.
|
|
61
|
+
* Use for "Start over" / retry flows without remounting the component.
|
|
62
|
+
*/
|
|
63
|
+
restart: (path: PathDefinition<any>, initialData?: PathData) => void;
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
export type PathProviderProps = PropsWithChildren<{
|
|
@@ -56,10 +73,11 @@ export type PathProviderProps = PropsWithChildren<{
|
|
|
56
73
|
// ---------------------------------------------------------------------------
|
|
57
74
|
|
|
58
75
|
export function usePath<TData extends PathData = PathData>(options?: UsePathOptions): UsePathReturn<TData> {
|
|
59
|
-
//
|
|
76
|
+
// Use provided engine or create a stable new one for this hook's lifetime.
|
|
77
|
+
// options.engine must be a stable reference (don't recreate on every render).
|
|
60
78
|
const engineRef = useRef<PathEngine | null>(null);
|
|
61
79
|
if (engineRef.current === null) {
|
|
62
|
-
engineRef.current = new PathEngine();
|
|
80
|
+
engineRef.current = options?.engine ?? new PathEngine();
|
|
63
81
|
}
|
|
64
82
|
const engine = engineRef.current;
|
|
65
83
|
|
|
@@ -67,8 +85,20 @@ export function usePath<TData extends PathData = PathData>(options?: UsePathOpti
|
|
|
67
85
|
const onEventRef = useRef(options?.onEvent);
|
|
68
86
|
onEventRef.current = options?.onEvent;
|
|
69
87
|
|
|
70
|
-
//
|
|
88
|
+
// Seed immediately from existing engine state — essential when restoring a
|
|
89
|
+
// persisted path (the engine is already started before usePath is called).
|
|
90
|
+
// We track whether we've seeded to avoid calling engine.snapshot() on every
|
|
91
|
+
// re-render (React evaluates useRef's argument each time).
|
|
92
|
+
const seededRef = useRef(false);
|
|
71
93
|
const snapshotRef = useRef<PathSnapshot<TData> | null>(null);
|
|
94
|
+
if (!seededRef.current) {
|
|
95
|
+
seededRef.current = true;
|
|
96
|
+
try {
|
|
97
|
+
snapshotRef.current = engine.snapshot() as PathSnapshot<TData> | null;
|
|
98
|
+
} catch {
|
|
99
|
+
snapshotRef.current = null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
72
102
|
|
|
73
103
|
const subscribe = useCallback(
|
|
74
104
|
(callback: () => void) =>
|
|
@@ -120,7 +150,13 @@ export function usePath<TData extends PathData = PathData>(options?: UsePathOpti
|
|
|
120
150
|
[engine]
|
|
121
151
|
) as UsePathReturn<TData>["setData"];
|
|
122
152
|
|
|
123
|
-
|
|
153
|
+
const restart = useCallback(
|
|
154
|
+
(path: PathDefinition<any>, initialData: PathData = {}) =>
|
|
155
|
+
engine.restart(path, initialData),
|
|
156
|
+
[engine]
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return { snapshot, start, startSubPath, next, previous, cancel, goToStep, goToStepChecked, setData, restart };
|
|
124
160
|
}
|
|
125
161
|
|
|
126
162
|
// ---------------------------------------------------------------------------
|
|
@@ -160,6 +196,12 @@ export function usePathContext<TData extends PathData = PathData>(): UsePathRetu
|
|
|
160
196
|
export interface PathShellProps {
|
|
161
197
|
/** The path definition to drive. */
|
|
162
198
|
path: PathDefinition<any>;
|
|
199
|
+
/**
|
|
200
|
+
* An externally-managed engine — for example, the engine returned by
|
|
201
|
+
* `createPersistedEngine()`. When supplied, `PathShell` will skip its own
|
|
202
|
+
* `start()` call and drive the UI from the provided engine instead.
|
|
203
|
+
*/
|
|
204
|
+
engine?: PathEngine;
|
|
163
205
|
/** Map of step ID → content. The shell renders `steps[snapshot.stepId]` for the current step. */
|
|
164
206
|
steps: Record<string, ReactNode>;
|
|
165
207
|
/** Initial data passed to `engine.start()`. */
|
|
@@ -172,12 +214,12 @@ export interface PathShellProps {
|
|
|
172
214
|
onCancel?: (data: PathData) => void;
|
|
173
215
|
/** Called for every engine event. */
|
|
174
216
|
onEvent?: (event: PathEvent) => void;
|
|
175
|
-
/** Label for the
|
|
217
|
+
/** Label for the Previous button. Defaults to `"Previous"`. */
|
|
176
218
|
backLabel?: string;
|
|
177
219
|
/** Label for the Next button. Defaults to `"Next"`. */
|
|
178
220
|
nextLabel?: string;
|
|
179
|
-
/** Label for the
|
|
180
|
-
|
|
221
|
+
/** Label for the Complete button (shown on the last step). Defaults to `"Complete"`. */
|
|
222
|
+
completeLabel?: string;
|
|
181
223
|
/** Label for the Cancel button. Defaults to `"Cancel"`. */
|
|
182
224
|
cancelLabel?: string;
|
|
183
225
|
/** If true, hide the Cancel button. Defaults to `false`. */
|
|
@@ -199,6 +241,8 @@ export interface PathShellActions {
|
|
|
199
241
|
goToStep: (stepId: string) => void;
|
|
200
242
|
goToStepChecked: (stepId: string) => void;
|
|
201
243
|
setData: (key: string, value: unknown) => void;
|
|
244
|
+
/** Restart the shell's current path with its current `initialData`. */
|
|
245
|
+
restart: () => void;
|
|
202
246
|
}
|
|
203
247
|
|
|
204
248
|
/**
|
|
@@ -219,15 +263,16 @@ export interface PathShellActions {
|
|
|
219
263
|
*/
|
|
220
264
|
export function PathShell({
|
|
221
265
|
path: pathDef,
|
|
266
|
+
engine: externalEngine,
|
|
222
267
|
steps,
|
|
223
268
|
initialData = {},
|
|
224
269
|
autoStart = true,
|
|
225
270
|
onComplete,
|
|
226
271
|
onCancel,
|
|
227
272
|
onEvent,
|
|
228
|
-
backLabel = "
|
|
273
|
+
backLabel = "Previous",
|
|
229
274
|
nextLabel = "Next",
|
|
230
|
-
|
|
275
|
+
completeLabel = "Complete",
|
|
231
276
|
cancelLabel = "Cancel",
|
|
232
277
|
hideCancel = false,
|
|
233
278
|
hideProgress = false,
|
|
@@ -236,6 +281,7 @@ export function PathShell({
|
|
|
236
281
|
renderFooter,
|
|
237
282
|
}: PathShellProps): ReactElement {
|
|
238
283
|
const pathReturn = usePath({
|
|
284
|
+
engine: externalEngine,
|
|
239
285
|
onEvent(event) {
|
|
240
286
|
onEvent?.(event);
|
|
241
287
|
if (event.type === "completed") onComplete?.(event.data);
|
|
@@ -243,12 +289,13 @@ export function PathShell({
|
|
|
243
289
|
}
|
|
244
290
|
});
|
|
245
291
|
|
|
246
|
-
const { snapshot, start, next, previous, cancel, goToStep, goToStepChecked, setData } = pathReturn;
|
|
292
|
+
const { snapshot, start, next, previous, cancel, goToStep, goToStepChecked, setData, restart } = pathReturn;
|
|
247
293
|
|
|
248
|
-
// Auto-start on mount
|
|
294
|
+
// Auto-start on mount — skipped when an external engine is provided since
|
|
295
|
+
// the caller is responsible for starting it (e.g. via createPersistedEngine).
|
|
249
296
|
const startedRef = useRef(false);
|
|
250
297
|
useEffect(() => {
|
|
251
|
-
if (autoStart && !startedRef.current) {
|
|
298
|
+
if (autoStart && !startedRef.current && !externalEngine) {
|
|
252
299
|
startedRef.current = true;
|
|
253
300
|
start(pathDef, initialData);
|
|
254
301
|
}
|
|
@@ -273,7 +320,10 @@ export function PathShell({
|
|
|
273
320
|
);
|
|
274
321
|
}
|
|
275
322
|
|
|
276
|
-
const actions: PathShellActions = {
|
|
323
|
+
const actions: PathShellActions = {
|
|
324
|
+
next, previous, cancel, goToStep, goToStepChecked, setData,
|
|
325
|
+
restart: () => restart(pathDef, initialData)
|
|
326
|
+
};
|
|
277
327
|
|
|
278
328
|
return createElement(PathContext.Provider, { value: pathReturn },
|
|
279
329
|
createElement("div", { className: cls("pw-shell", className) },
|
|
@@ -293,7 +343,7 @@ export function PathShell({
|
|
|
293
343
|
renderFooter
|
|
294
344
|
? renderFooter(snapshot, actions)
|
|
295
345
|
: defaultFooter(snapshot, actions, {
|
|
296
|
-
backLabel, nextLabel,
|
|
346
|
+
backLabel, nextLabel, completeLabel, cancelLabel, hideCancel
|
|
297
347
|
})
|
|
298
348
|
)
|
|
299
349
|
);
|
|
@@ -336,7 +386,7 @@ function defaultHeader(snapshot: PathSnapshot): ReactElement {
|
|
|
336
386
|
interface FooterLabels {
|
|
337
387
|
backLabel: string;
|
|
338
388
|
nextLabel: string;
|
|
339
|
-
|
|
389
|
+
completeLabel: string;
|
|
340
390
|
cancelLabel: string;
|
|
341
391
|
hideCancel: boolean;
|
|
342
392
|
}
|
|
@@ -367,7 +417,7 @@ function defaultFooter(
|
|
|
367
417
|
className: "pw-shell__btn pw-shell__btn--next",
|
|
368
418
|
disabled: snapshot.isNavigating || !snapshot.canMoveNext,
|
|
369
419
|
onClick: actions.next
|
|
370
|
-
}, snapshot.isLastStep ? labels.
|
|
420
|
+
}, snapshot.isLastStep ? labels.completeLabel : labels.nextLabel)
|
|
371
421
|
)
|
|
372
422
|
);
|
|
373
423
|
}
|
|
@@ -376,7 +426,23 @@ function defaultFooter(
|
|
|
376
426
|
// Helpers
|
|
377
427
|
// ---------------------------------------------------------------------------
|
|
378
428
|
|
|
379
|
-
|
|
380
429
|
function cls(...parts: (string | undefined | false | null)[]): string {
|
|
381
430
|
return parts.filter(Boolean).join(" ");
|
|
382
431
|
}
|
|
432
|
+
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// Re-export core types for convenience
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
export type {
|
|
438
|
+
PathData,
|
|
439
|
+
PathDefinition,
|
|
440
|
+
PathEvent,
|
|
441
|
+
PathSnapshot,
|
|
442
|
+
PathStep,
|
|
443
|
+
PathStepContext,
|
|
444
|
+
SerializedPathState
|
|
445
|
+
} from "@daltonr/pathwrite-core";
|
|
446
|
+
|
|
447
|
+
export { PathEngine } from "@daltonr/pathwrite-core";
|
|
448
|
+
|