@tailor-platform/app-shell 1.12.0 → 1.14.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 +1 -1
- package/dist/app-shell-core.css +3 -1
- package/dist/app-shell.d.ts +1038 -46
- package/dist/app-shell.js +10700 -14287
- package/dist/appshell-CtS8Y3LW.js +1823 -0
- package/dist/fonts/NOTO-SANS-JP-LICENSE.txt +93 -0
- package/dist/fonts/files/noto-sans-jp-0-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-1-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-10-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-100-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-101-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-102-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-103-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-104-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-105-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-106-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-107-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-108-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-109-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-11-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-110-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-111-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-112-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-113-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-114-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-115-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-116-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-117-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-118-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-119-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-12-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-13-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-14-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-15-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-16-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-17-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-18-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-19-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-2-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-20-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-21-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-22-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-23-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-24-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-25-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-26-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-27-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-28-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-29-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-3-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-30-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-31-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-32-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-33-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-34-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-35-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-36-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-37-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-38-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-39-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-4-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-40-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-41-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-42-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-43-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-44-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-45-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-46-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-47-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-48-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-49-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-5-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-50-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-51-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-52-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-53-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-54-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-55-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-6-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-60-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-61-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-62-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-63-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-64-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-65-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-66-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-67-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-68-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-69-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-7-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-70-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-71-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-72-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-73-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-74-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-75-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-76-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-77-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-78-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-79-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-8-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-80-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-81-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-82-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-83-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-84-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-85-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-86-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-87-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-88-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-89-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-9-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-90-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-91-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-92-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-93-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-94-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-95-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-96-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-97-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-98-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-99-wght-normal.woff2 +0 -0
- package/dist/fonts/noto-sans-jp.css +2506 -0
- package/dist/style-entry.d.ts +600 -0
- package/dist/style-entry.js +0 -1
- package/dist/testing.d.ts +1140 -0
- package/dist/testing.js +14 -0
- package/dist/theme.bridge.css +29 -1
- package/dist/vite-plugin.d.ts +607 -1
- package/dist/vite-plugin.js +2 -4
- package/package.json +23 -15
- package/skills/app-shell-patterns/SKILL.md +14 -6
- package/skills/app-shell-patterns/references/fundamental/components.md +147 -4
- package/skills/app-shell-patterns/references/fundamental/design-system.md +1 -1
- package/skills/app-shell-patterns/references/fundamental/graphql.md +1 -1
- package/skills/app-shell-patterns/references/patterns/form-composer.md +142 -0
- package/skills/app-shell-patterns/references/patterns/form-modal.md +84 -30
- package/skills/app-shell-patterns/references/patterns/form-sectioned.md +136 -64
- package/skills/app-shell-patterns/references/patterns/form-single-page.md +58 -26
- package/skills/app-shell-patterns/references/patterns/form-wizard.md +157 -69
|
@@ -4,15 +4,16 @@ name: Wizard Form
|
|
|
4
4
|
category: pattern
|
|
5
5
|
subcategory: form
|
|
6
6
|
description: Multi-stage create flow with 3-7 steps and per-step validation gates
|
|
7
|
-
requiredImports: [Layout, Card, Form, Fieldset, Field,
|
|
8
|
-
tags: [form, wizard, multi-step,
|
|
7
|
+
requiredImports: [Layout, Card, Badge, Form, Fieldset, Field, Select, Button]
|
|
8
|
+
tags: [form, wizard, multi-step, stepper, onboarding]
|
|
9
9
|
do:
|
|
10
10
|
- Multi-stage Create with 3-7 steps
|
|
11
|
-
-
|
|
11
|
+
- Onboarding and request flows where users should focus on one step at a time
|
|
12
12
|
- Per-step validation gates progression
|
|
13
13
|
dont:
|
|
14
14
|
- Single screen of fields — use form/modal or form/single-page
|
|
15
15
|
- More than 7 steps — split into separate routed pages or reduce scope
|
|
16
|
+
- CSV/spreadsheet import — use the CsvImporter component, not a hand-built wizard
|
|
16
17
|
---
|
|
17
18
|
|
|
18
19
|
# pattern/form/wizard
|
|
@@ -20,102 +21,166 @@ dont:
|
|
|
20
21
|
## When to Use
|
|
21
22
|
|
|
22
23
|
- Multi-stage Create with 3–7 steps
|
|
23
|
-
-
|
|
24
|
+
- Onboarding and request flows where users should focus on one step at a time
|
|
24
25
|
- Per-step validation gates progression
|
|
25
26
|
|
|
27
|
+
For CSV/spreadsheet import specifically, use the `CsvImporter` component — it already implements
|
|
28
|
+
the upload → map → validate → confirm flow. Don't rebuild it here.
|
|
29
|
+
|
|
26
30
|
## Page Implementation
|
|
27
31
|
|
|
28
32
|
```tsx
|
|
29
33
|
/* pattern: form/wizard */
|
|
30
34
|
import { useState } from "react";
|
|
31
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
Badge,
|
|
37
|
+
Button,
|
|
38
|
+
Card,
|
|
39
|
+
Field,
|
|
40
|
+
Fieldset,
|
|
41
|
+
Form,
|
|
42
|
+
Layout,
|
|
43
|
+
Select,
|
|
44
|
+
} from "@tailor-platform/app-shell";
|
|
45
|
+
|
|
46
|
+
const OWNERS = ["Tanaka", "Sato", "Suzuki", "Yamada"];
|
|
47
|
+
const STEPS = ["Basic info", "Assignment", "Schedule", "Review"] as const;
|
|
48
|
+
|
|
49
|
+
type Draft = {
|
|
50
|
+
title: string;
|
|
51
|
+
description: string;
|
|
52
|
+
owner: string;
|
|
53
|
+
startDate: string;
|
|
54
|
+
estimate: string;
|
|
55
|
+
};
|
|
32
56
|
|
|
33
|
-
const
|
|
57
|
+
const INITIAL: Draft = {
|
|
58
|
+
title: "",
|
|
59
|
+
description: "",
|
|
60
|
+
owner: "",
|
|
61
|
+
startDate: "",
|
|
62
|
+
estimate: "",
|
|
63
|
+
};
|
|
34
64
|
|
|
35
65
|
type Props = {
|
|
36
|
-
onComplete: () => void;
|
|
66
|
+
onComplete: (draft: Draft) => void;
|
|
67
|
+
onCancel: () => void;
|
|
37
68
|
};
|
|
38
69
|
|
|
39
|
-
export default function WizardForm({ onComplete }: Props) {
|
|
40
|
-
const [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
export default function WizardForm({ onComplete, onCancel }: Props) {
|
|
71
|
+
const [step, setStep] = useState(0);
|
|
72
|
+
// Accumulated values. Each step's Form unmounts on navigation, so the draft
|
|
73
|
+
// is what makes Back non-destructive — fields re-read it via `defaultValue`.
|
|
74
|
+
const [draft, setDraft] = useState<Draft>(INITIAL);
|
|
75
|
+
|
|
76
|
+
const isLastStep = step === STEPS.length - 1;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Each step is its own `Form`, and "Next" is a `type="submit"` button.
|
|
80
|
+
* `onFormSubmit` fires only after that step's fields pass validation, so
|
|
81
|
+
* progression is gated natively — no manual per-step validity check, and
|
|
82
|
+
* no deferring every error to the final submit.
|
|
83
|
+
*/
|
|
84
|
+
const handleStepSubmit = (values: Record<string, unknown>) => {
|
|
85
|
+
const merged = { ...draft, ...(values as Partial<Draft>) };
|
|
86
|
+
setDraft(merged);
|
|
87
|
+
if (isLastStep) {
|
|
88
|
+
onComplete(merged);
|
|
89
|
+
return;
|
|
53
90
|
}
|
|
91
|
+
setStep(step + 1);
|
|
54
92
|
};
|
|
55
93
|
|
|
56
94
|
return (
|
|
57
95
|
<Layout>
|
|
58
|
-
<Layout.Header title="
|
|
96
|
+
<Layout.Header title="Create task" />
|
|
59
97
|
<Layout.Column>
|
|
60
98
|
<Card.Root>
|
|
61
99
|
<Card.Content>
|
|
62
100
|
<div className="flex items-center gap-2">
|
|
63
|
-
{STEPS.map((
|
|
101
|
+
{STEPS.map((label, i) => (
|
|
64
102
|
<Badge
|
|
65
|
-
key={
|
|
66
|
-
variant={i ===
|
|
103
|
+
key={label}
|
|
104
|
+
variant={i === step ? "default" : i < step ? "success" : "neutral"}
|
|
67
105
|
>
|
|
68
|
-
{i + 1}. {
|
|
106
|
+
{i + 1}. {label}
|
|
69
107
|
</Badge>
|
|
70
108
|
))}
|
|
71
109
|
</div>
|
|
72
110
|
</Card.Content>
|
|
73
111
|
</Card.Root>
|
|
74
112
|
|
|
75
|
-
<
|
|
76
|
-
<Card.
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<Field.
|
|
113
|
+
<Form key={step} noValidate onFormSubmit={handleStepSubmit}>
|
|
114
|
+
<Card.Root>
|
|
115
|
+
<Card.Header title={STEPS[step]} />
|
|
116
|
+
<Card.Content>
|
|
117
|
+
{step === 0 && (
|
|
118
|
+
<Fieldset.Root className="space-y-4">
|
|
119
|
+
<Field.Root name="title">
|
|
120
|
+
<Field.Label>Title</Field.Label>
|
|
121
|
+
<Field.Control required defaultValue={draft.title} />
|
|
122
|
+
<Field.Error match="valueMissing">Title is required.</Field.Error>
|
|
123
|
+
</Field.Root>
|
|
124
|
+
<Field.Root name="description">
|
|
125
|
+
<Field.Label>Description</Field.Label>
|
|
126
|
+
<Field.Control defaultValue={draft.description} />
|
|
127
|
+
</Field.Root>
|
|
128
|
+
</Fieldset.Root>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{step === 1 && (
|
|
132
|
+
<Field.Root name="owner">
|
|
133
|
+
<Field.Label>Owner</Field.Label>
|
|
134
|
+
{/* Uncontrolled like every other field — `defaultValue`
|
|
135
|
+
restores the prior choice when the user steps Back. */}
|
|
136
|
+
<Select
|
|
137
|
+
items={OWNERS}
|
|
138
|
+
defaultValue={draft.owner || null}
|
|
139
|
+
placeholder="Assign owner"
|
|
140
|
+
/>
|
|
82
141
|
</Field.Root>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{step === 2 && (
|
|
145
|
+
<Fieldset.Root className="grid gap-4 md:grid-cols-2">
|
|
146
|
+
<Field.Root name="startDate">
|
|
147
|
+
<Field.Label>Start date</Field.Label>
|
|
148
|
+
<Field.Control type="date" required defaultValue={draft.startDate} />
|
|
149
|
+
<Field.Error match="valueMissing">Start date is required.</Field.Error>
|
|
150
|
+
</Field.Root>
|
|
151
|
+
<Field.Root name="estimate">
|
|
152
|
+
<Field.Label>Estimate (hours)</Field.Label>
|
|
153
|
+
<Field.Control type="number" min={0} defaultValue={draft.estimate} />
|
|
154
|
+
</Field.Root>
|
|
155
|
+
</Fieldset.Root>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{step === 3 && (
|
|
159
|
+
<dl className="grid grid-cols-2 gap-2 text-sm">
|
|
160
|
+
<dt className="text-muted-foreground">Title</dt>
|
|
161
|
+
<dd>{draft.title}</dd>
|
|
162
|
+
<dt className="text-muted-foreground">Owner</dt>
|
|
163
|
+
<dd>{draft.owner || "—"}</dd>
|
|
164
|
+
<dt className="text-muted-foreground">Start date</dt>
|
|
165
|
+
<dd>{draft.startDate || "—"}</dd>
|
|
166
|
+
<dt className="text-muted-foreground">Estimate</dt>
|
|
167
|
+
<dd>{draft.estimate ? `${draft.estimate}h` : "—"}</dd>
|
|
168
|
+
</dl>
|
|
169
|
+
)}
|
|
170
|
+
</Card.Content>
|
|
171
|
+
</Card.Root>
|
|
172
|
+
|
|
173
|
+
<div className="flex justify-between pt-4">
|
|
174
|
+
<Button
|
|
175
|
+
type="button"
|
|
176
|
+
variant="ghost"
|
|
177
|
+
onClick={() => (step === 0 ? onCancel() : setStep(step - 1))}
|
|
178
|
+
>
|
|
179
|
+
{step === 0 ? "Cancel" : "Back"}
|
|
180
|
+
</Button>
|
|
181
|
+
<Button type="submit">{isLastStep ? "Create" : "Next"}</Button>
|
|
182
|
+
</div>
|
|
183
|
+
</Form>
|
|
119
184
|
</Layout.Column>
|
|
120
185
|
</Layout>
|
|
121
186
|
);
|
|
@@ -128,9 +193,32 @@ export default function WizardForm({ onComplete }: Props) {
|
|
|
128
193
|
- Back-navigation must preserve prior step's input
|
|
129
194
|
- Validation must be per-step — don't defer until final submit
|
|
130
195
|
- Step indicator collapses to "Step 2 of 4" label below 1024px
|
|
196
|
+
- One `<Form>` rendered per step, keyed by step index so it remounts cleanly
|
|
197
|
+
- Accumulated values live in a `draft` state object above the `Form`; each step's fields read
|
|
198
|
+
their initial value from it via `defaultValue`
|
|
199
|
+
|
|
200
|
+
## Form state
|
|
201
|
+
|
|
202
|
+
Rules that apply to every `form/*` pattern are in **`components.md`** → Forms, with a worked example
|
|
203
|
+
in **`form/modal`** → Form state: `Form` + `Field` is the default stack, submit via `onFormSubmit`,
|
|
204
|
+
dropdowns need only a wrapping `Field.Root` (no `name`, no state), server errors route through
|
|
205
|
+
`errors`, and React Hook Form is an optional escape hatch.
|
|
206
|
+
|
|
207
|
+
The wizard's specific mechanic: **make "Next" a `type="submit"` button.** `onFormSubmit` fires only
|
|
208
|
+
after the current step's fields pass validation, so progression is gated natively — no manual
|
|
209
|
+
validity check, and no deferring errors to the final submit. The handler merges that step's values
|
|
210
|
+
into `draft` and advances; on the last step it calls the completion callback instead.
|
|
211
|
+
|
|
212
|
+
Because each step's `Form` unmounts on navigation, values must be lifted into `draft` — that is
|
|
213
|
+
what makes Back non-destructive. Every field, dropdowns included, is uncontrolled and re-reads its
|
|
214
|
+
prior value from `draft` via `defaultValue`; nothing needs an `onChange`.
|
|
131
215
|
|
|
132
216
|
## Anti-patterns
|
|
133
217
|
|
|
134
218
|
- More than 7 steps — users lose context and abandon
|
|
135
219
|
- No back-navigation preservation — pressing Back loses prior step's input
|
|
136
220
|
- Validation deferred until final submit — failures force full re-traversal
|
|
221
|
+
- A plain `onClick` "Next" that advances without validating — use `type="submit"` and let
|
|
222
|
+
`onFormSubmit` gate it
|
|
223
|
+
- Wrapping all steps in one `<Form>` and hiding inactive ones — hidden required fields block submit
|
|
224
|
+
- Hand-building a CSV import wizard — use `CsvImporter`
|