@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.
Files changed (140) hide show
  1. package/README.md +1 -1
  2. package/dist/app-shell-core.css +3 -1
  3. package/dist/app-shell.d.ts +1038 -46
  4. package/dist/app-shell.js +10700 -14287
  5. package/dist/appshell-CtS8Y3LW.js +1823 -0
  6. package/dist/fonts/NOTO-SANS-JP-LICENSE.txt +93 -0
  7. package/dist/fonts/files/noto-sans-jp-0-wght-normal.woff2 +0 -0
  8. package/dist/fonts/files/noto-sans-jp-1-wght-normal.woff2 +0 -0
  9. package/dist/fonts/files/noto-sans-jp-10-wght-normal.woff2 +0 -0
  10. package/dist/fonts/files/noto-sans-jp-100-wght-normal.woff2 +0 -0
  11. package/dist/fonts/files/noto-sans-jp-101-wght-normal.woff2 +0 -0
  12. package/dist/fonts/files/noto-sans-jp-102-wght-normal.woff2 +0 -0
  13. package/dist/fonts/files/noto-sans-jp-103-wght-normal.woff2 +0 -0
  14. package/dist/fonts/files/noto-sans-jp-104-wght-normal.woff2 +0 -0
  15. package/dist/fonts/files/noto-sans-jp-105-wght-normal.woff2 +0 -0
  16. package/dist/fonts/files/noto-sans-jp-106-wght-normal.woff2 +0 -0
  17. package/dist/fonts/files/noto-sans-jp-107-wght-normal.woff2 +0 -0
  18. package/dist/fonts/files/noto-sans-jp-108-wght-normal.woff2 +0 -0
  19. package/dist/fonts/files/noto-sans-jp-109-wght-normal.woff2 +0 -0
  20. package/dist/fonts/files/noto-sans-jp-11-wght-normal.woff2 +0 -0
  21. package/dist/fonts/files/noto-sans-jp-110-wght-normal.woff2 +0 -0
  22. package/dist/fonts/files/noto-sans-jp-111-wght-normal.woff2 +0 -0
  23. package/dist/fonts/files/noto-sans-jp-112-wght-normal.woff2 +0 -0
  24. package/dist/fonts/files/noto-sans-jp-113-wght-normal.woff2 +0 -0
  25. package/dist/fonts/files/noto-sans-jp-114-wght-normal.woff2 +0 -0
  26. package/dist/fonts/files/noto-sans-jp-115-wght-normal.woff2 +0 -0
  27. package/dist/fonts/files/noto-sans-jp-116-wght-normal.woff2 +0 -0
  28. package/dist/fonts/files/noto-sans-jp-117-wght-normal.woff2 +0 -0
  29. package/dist/fonts/files/noto-sans-jp-118-wght-normal.woff2 +0 -0
  30. package/dist/fonts/files/noto-sans-jp-119-wght-normal.woff2 +0 -0
  31. package/dist/fonts/files/noto-sans-jp-12-wght-normal.woff2 +0 -0
  32. package/dist/fonts/files/noto-sans-jp-13-wght-normal.woff2 +0 -0
  33. package/dist/fonts/files/noto-sans-jp-14-wght-normal.woff2 +0 -0
  34. package/dist/fonts/files/noto-sans-jp-15-wght-normal.woff2 +0 -0
  35. package/dist/fonts/files/noto-sans-jp-16-wght-normal.woff2 +0 -0
  36. package/dist/fonts/files/noto-sans-jp-17-wght-normal.woff2 +0 -0
  37. package/dist/fonts/files/noto-sans-jp-18-wght-normal.woff2 +0 -0
  38. package/dist/fonts/files/noto-sans-jp-19-wght-normal.woff2 +0 -0
  39. package/dist/fonts/files/noto-sans-jp-2-wght-normal.woff2 +0 -0
  40. package/dist/fonts/files/noto-sans-jp-20-wght-normal.woff2 +0 -0
  41. package/dist/fonts/files/noto-sans-jp-21-wght-normal.woff2 +0 -0
  42. package/dist/fonts/files/noto-sans-jp-22-wght-normal.woff2 +0 -0
  43. package/dist/fonts/files/noto-sans-jp-23-wght-normal.woff2 +0 -0
  44. package/dist/fonts/files/noto-sans-jp-24-wght-normal.woff2 +0 -0
  45. package/dist/fonts/files/noto-sans-jp-25-wght-normal.woff2 +0 -0
  46. package/dist/fonts/files/noto-sans-jp-26-wght-normal.woff2 +0 -0
  47. package/dist/fonts/files/noto-sans-jp-27-wght-normal.woff2 +0 -0
  48. package/dist/fonts/files/noto-sans-jp-28-wght-normal.woff2 +0 -0
  49. package/dist/fonts/files/noto-sans-jp-29-wght-normal.woff2 +0 -0
  50. package/dist/fonts/files/noto-sans-jp-3-wght-normal.woff2 +0 -0
  51. package/dist/fonts/files/noto-sans-jp-30-wght-normal.woff2 +0 -0
  52. package/dist/fonts/files/noto-sans-jp-31-wght-normal.woff2 +0 -0
  53. package/dist/fonts/files/noto-sans-jp-32-wght-normal.woff2 +0 -0
  54. package/dist/fonts/files/noto-sans-jp-33-wght-normal.woff2 +0 -0
  55. package/dist/fonts/files/noto-sans-jp-34-wght-normal.woff2 +0 -0
  56. package/dist/fonts/files/noto-sans-jp-35-wght-normal.woff2 +0 -0
  57. package/dist/fonts/files/noto-sans-jp-36-wght-normal.woff2 +0 -0
  58. package/dist/fonts/files/noto-sans-jp-37-wght-normal.woff2 +0 -0
  59. package/dist/fonts/files/noto-sans-jp-38-wght-normal.woff2 +0 -0
  60. package/dist/fonts/files/noto-sans-jp-39-wght-normal.woff2 +0 -0
  61. package/dist/fonts/files/noto-sans-jp-4-wght-normal.woff2 +0 -0
  62. package/dist/fonts/files/noto-sans-jp-40-wght-normal.woff2 +0 -0
  63. package/dist/fonts/files/noto-sans-jp-41-wght-normal.woff2 +0 -0
  64. package/dist/fonts/files/noto-sans-jp-42-wght-normal.woff2 +0 -0
  65. package/dist/fonts/files/noto-sans-jp-43-wght-normal.woff2 +0 -0
  66. package/dist/fonts/files/noto-sans-jp-44-wght-normal.woff2 +0 -0
  67. package/dist/fonts/files/noto-sans-jp-45-wght-normal.woff2 +0 -0
  68. package/dist/fonts/files/noto-sans-jp-46-wght-normal.woff2 +0 -0
  69. package/dist/fonts/files/noto-sans-jp-47-wght-normal.woff2 +0 -0
  70. package/dist/fonts/files/noto-sans-jp-48-wght-normal.woff2 +0 -0
  71. package/dist/fonts/files/noto-sans-jp-49-wght-normal.woff2 +0 -0
  72. package/dist/fonts/files/noto-sans-jp-5-wght-normal.woff2 +0 -0
  73. package/dist/fonts/files/noto-sans-jp-50-wght-normal.woff2 +0 -0
  74. package/dist/fonts/files/noto-sans-jp-51-wght-normal.woff2 +0 -0
  75. package/dist/fonts/files/noto-sans-jp-52-wght-normal.woff2 +0 -0
  76. package/dist/fonts/files/noto-sans-jp-53-wght-normal.woff2 +0 -0
  77. package/dist/fonts/files/noto-sans-jp-54-wght-normal.woff2 +0 -0
  78. package/dist/fonts/files/noto-sans-jp-55-wght-normal.woff2 +0 -0
  79. package/dist/fonts/files/noto-sans-jp-6-wght-normal.woff2 +0 -0
  80. package/dist/fonts/files/noto-sans-jp-60-wght-normal.woff2 +0 -0
  81. package/dist/fonts/files/noto-sans-jp-61-wght-normal.woff2 +0 -0
  82. package/dist/fonts/files/noto-sans-jp-62-wght-normal.woff2 +0 -0
  83. package/dist/fonts/files/noto-sans-jp-63-wght-normal.woff2 +0 -0
  84. package/dist/fonts/files/noto-sans-jp-64-wght-normal.woff2 +0 -0
  85. package/dist/fonts/files/noto-sans-jp-65-wght-normal.woff2 +0 -0
  86. package/dist/fonts/files/noto-sans-jp-66-wght-normal.woff2 +0 -0
  87. package/dist/fonts/files/noto-sans-jp-67-wght-normal.woff2 +0 -0
  88. package/dist/fonts/files/noto-sans-jp-68-wght-normal.woff2 +0 -0
  89. package/dist/fonts/files/noto-sans-jp-69-wght-normal.woff2 +0 -0
  90. package/dist/fonts/files/noto-sans-jp-7-wght-normal.woff2 +0 -0
  91. package/dist/fonts/files/noto-sans-jp-70-wght-normal.woff2 +0 -0
  92. package/dist/fonts/files/noto-sans-jp-71-wght-normal.woff2 +0 -0
  93. package/dist/fonts/files/noto-sans-jp-72-wght-normal.woff2 +0 -0
  94. package/dist/fonts/files/noto-sans-jp-73-wght-normal.woff2 +0 -0
  95. package/dist/fonts/files/noto-sans-jp-74-wght-normal.woff2 +0 -0
  96. package/dist/fonts/files/noto-sans-jp-75-wght-normal.woff2 +0 -0
  97. package/dist/fonts/files/noto-sans-jp-76-wght-normal.woff2 +0 -0
  98. package/dist/fonts/files/noto-sans-jp-77-wght-normal.woff2 +0 -0
  99. package/dist/fonts/files/noto-sans-jp-78-wght-normal.woff2 +0 -0
  100. package/dist/fonts/files/noto-sans-jp-79-wght-normal.woff2 +0 -0
  101. package/dist/fonts/files/noto-sans-jp-8-wght-normal.woff2 +0 -0
  102. package/dist/fonts/files/noto-sans-jp-80-wght-normal.woff2 +0 -0
  103. package/dist/fonts/files/noto-sans-jp-81-wght-normal.woff2 +0 -0
  104. package/dist/fonts/files/noto-sans-jp-82-wght-normal.woff2 +0 -0
  105. package/dist/fonts/files/noto-sans-jp-83-wght-normal.woff2 +0 -0
  106. package/dist/fonts/files/noto-sans-jp-84-wght-normal.woff2 +0 -0
  107. package/dist/fonts/files/noto-sans-jp-85-wght-normal.woff2 +0 -0
  108. package/dist/fonts/files/noto-sans-jp-86-wght-normal.woff2 +0 -0
  109. package/dist/fonts/files/noto-sans-jp-87-wght-normal.woff2 +0 -0
  110. package/dist/fonts/files/noto-sans-jp-88-wght-normal.woff2 +0 -0
  111. package/dist/fonts/files/noto-sans-jp-89-wght-normal.woff2 +0 -0
  112. package/dist/fonts/files/noto-sans-jp-9-wght-normal.woff2 +0 -0
  113. package/dist/fonts/files/noto-sans-jp-90-wght-normal.woff2 +0 -0
  114. package/dist/fonts/files/noto-sans-jp-91-wght-normal.woff2 +0 -0
  115. package/dist/fonts/files/noto-sans-jp-92-wght-normal.woff2 +0 -0
  116. package/dist/fonts/files/noto-sans-jp-93-wght-normal.woff2 +0 -0
  117. package/dist/fonts/files/noto-sans-jp-94-wght-normal.woff2 +0 -0
  118. package/dist/fonts/files/noto-sans-jp-95-wght-normal.woff2 +0 -0
  119. package/dist/fonts/files/noto-sans-jp-96-wght-normal.woff2 +0 -0
  120. package/dist/fonts/files/noto-sans-jp-97-wght-normal.woff2 +0 -0
  121. package/dist/fonts/files/noto-sans-jp-98-wght-normal.woff2 +0 -0
  122. package/dist/fonts/files/noto-sans-jp-99-wght-normal.woff2 +0 -0
  123. package/dist/fonts/noto-sans-jp.css +2506 -0
  124. package/dist/style-entry.d.ts +600 -0
  125. package/dist/style-entry.js +0 -1
  126. package/dist/testing.d.ts +1140 -0
  127. package/dist/testing.js +14 -0
  128. package/dist/theme.bridge.css +29 -1
  129. package/dist/vite-plugin.d.ts +607 -1
  130. package/dist/vite-plugin.js +2 -4
  131. package/package.json +23 -15
  132. package/skills/app-shell-patterns/SKILL.md +14 -6
  133. package/skills/app-shell-patterns/references/fundamental/components.md +147 -4
  134. package/skills/app-shell-patterns/references/fundamental/design-system.md +1 -1
  135. package/skills/app-shell-patterns/references/fundamental/graphql.md +1 -1
  136. package/skills/app-shell-patterns/references/patterns/form-composer.md +142 -0
  137. package/skills/app-shell-patterns/references/patterns/form-modal.md +84 -30
  138. package/skills/app-shell-patterns/references/patterns/form-sectioned.md +136 -64
  139. package/skills/app-shell-patterns/references/patterns/form-single-page.md +58 -26
  140. 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, Input, Select, Badge, Button]
8
- tags: [form, wizard, multi-step, import, stepper]
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
- - Import flows (upload → map → validate → confirm)
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
- - Import flows (upload → map → validate → confirm)
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 { Button, Card, Layout, Badge, Input, Field } from "@tailor-platform/app-shell";
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 STEPS = ["Upload", "Map", "Review", "Done"] as 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 [currentStep, setCurrentStep] = useState(0);
41
-
42
- const handleNext = () => {
43
- if (currentStep < STEPS.length - 1) {
44
- setCurrentStep(currentStep + 1);
45
- } else {
46
- onComplete();
47
- }
48
- };
49
-
50
- const handleBack = () => {
51
- if (currentStep > 0) {
52
- setCurrentStep(currentStep - 1);
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="Import products" />
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((step, i) => (
101
+ {STEPS.map((label, i) => (
64
102
  <Badge
65
- key={step}
66
- variant={i === currentStep ? "default" : i < currentStep ? "success" : "neutral"}
103
+ key={label}
104
+ variant={i === step ? "default" : i < step ? "success" : "neutral"}
67
105
  >
68
- {i + 1}. {step}
106
+ {i + 1}. {label}
69
107
  </Badge>
70
108
  ))}
71
109
  </div>
72
110
  </Card.Content>
73
111
  </Card.Root>
74
112
 
75
- <Card.Root>
76
- <Card.Content>
77
- {currentStep === 0 && (
78
- <div className="space-y-4">
79
- <Field.Root name="file">
80
- <Field.Label>CSV file</Field.Label>
81
- <Field.Control render={<Input type="file" accept=".csv" />} />
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
- </div>
84
- )}
85
- {currentStep === 1 && (
86
- <div className="space-y-4">
87
- <p className="text-sm text-muted-foreground">Map CSV columns to product fields</p>
88
- <Field.Root name="nameColumn">
89
- <Field.Label>Name column</Field.Label>
90
- <Field.Control render={<Input placeholder="e.g. Column A" />} />
91
- </Field.Root>
92
- <Field.Root name="skuColumn">
93
- <Field.Label>SKU column</Field.Label>
94
- <Field.Control render={<Input placeholder="e.g. Column B" />} />
95
- </Field.Root>
96
- </div>
97
- )}
98
- {currentStep === 2 && (
99
- <div className="space-y-4">
100
- <p className="text-sm text-muted-foreground">
101
- Review your import — 42 products will be created.
102
- </p>
103
- </div>
104
- )}
105
- {currentStep === 3 && (
106
- <div className="space-y-4">
107
- <p className="text-sm text-foreground">Import complete! 42 products created.</p>
108
- </div>
109
- )}
110
- </Card.Content>
111
- </Card.Root>
112
-
113
- <div className="flex justify-between">
114
- <Button variant="ghost" onClick={handleBack} disabled={currentStep === 0}>
115
- Back
116
- </Button>
117
- <Button onClick={handleNext}>{currentStep === STEPS.length - 1 ? "Done" : "Next"}</Button>
118
- </div>
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`