red64-cli 0.1.0 → 0.3.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 (125) hide show
  1. package/README.md +1 -2
  2. package/dist/cli/parseArgs.d.ts.map +1 -1
  3. package/dist/cli/parseArgs.js +5 -0
  4. package/dist/cli/parseArgs.js.map +1 -1
  5. package/dist/components/init/CompleteStep.d.ts.map +1 -1
  6. package/dist/components/init/CompleteStep.js +2 -2
  7. package/dist/components/init/CompleteStep.js.map +1 -1
  8. package/dist/components/init/TestCheckStep.d.ts +16 -0
  9. package/dist/components/init/TestCheckStep.d.ts.map +1 -0
  10. package/dist/components/init/TestCheckStep.js +120 -0
  11. package/dist/components/init/TestCheckStep.js.map +1 -0
  12. package/dist/components/init/index.d.ts +1 -0
  13. package/dist/components/init/index.d.ts.map +1 -1
  14. package/dist/components/init/index.js +1 -0
  15. package/dist/components/init/index.js.map +1 -1
  16. package/dist/components/init/types.d.ts +9 -0
  17. package/dist/components/init/types.d.ts.map +1 -1
  18. package/dist/components/screens/InitScreen.d.ts.map +1 -1
  19. package/dist/components/screens/InitScreen.js +69 -6
  20. package/dist/components/screens/InitScreen.js.map +1 -1
  21. package/dist/components/screens/ListScreen.d.ts.map +1 -1
  22. package/dist/components/screens/ListScreen.js +28 -3
  23. package/dist/components/screens/ListScreen.js.map +1 -1
  24. package/dist/components/screens/StartScreen.d.ts.map +1 -1
  25. package/dist/components/screens/StartScreen.js +212 -13
  26. package/dist/components/screens/StartScreen.js.map +1 -1
  27. package/dist/components/ui/ArtifactsSidebar.d.ts +19 -0
  28. package/dist/components/ui/ArtifactsSidebar.d.ts.map +1 -0
  29. package/dist/components/ui/ArtifactsSidebar.js +51 -0
  30. package/dist/components/ui/ArtifactsSidebar.js.map +1 -0
  31. package/dist/components/ui/FeatureSidebar.d.ts.map +1 -1
  32. package/dist/components/ui/FeatureSidebar.js +1 -1
  33. package/dist/components/ui/FeatureSidebar.js.map +1 -1
  34. package/dist/components/ui/index.d.ts +1 -0
  35. package/dist/components/ui/index.d.ts.map +1 -1
  36. package/dist/components/ui/index.js +1 -0
  37. package/dist/components/ui/index.js.map +1 -1
  38. package/dist/services/ClaudeErrorDetector.js +3 -3
  39. package/dist/services/ClaudeErrorDetector.js.map +1 -1
  40. package/dist/services/ConfigService.d.ts +1 -0
  41. package/dist/services/ConfigService.d.ts.map +1 -1
  42. package/dist/services/ConfigService.js.map +1 -1
  43. package/dist/services/ProjectDetector.d.ts +28 -0
  44. package/dist/services/ProjectDetector.d.ts.map +1 -0
  45. package/dist/services/ProjectDetector.js +236 -0
  46. package/dist/services/ProjectDetector.js.map +1 -0
  47. package/dist/services/TestRunner.d.ts +46 -0
  48. package/dist/services/TestRunner.d.ts.map +1 -0
  49. package/dist/services/TestRunner.js +85 -0
  50. package/dist/services/TestRunner.js.map +1 -0
  51. package/dist/services/index.d.ts +2 -0
  52. package/dist/services/index.d.ts.map +1 -1
  53. package/dist/services/index.js +2 -0
  54. package/dist/services/index.js.map +1 -1
  55. package/dist/types/index.d.ts +13 -0
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/dist/types/index.js.map +1 -1
  58. package/framework/.red64/settings/templates/specs/gap-analysis.md +163 -0
  59. package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
  60. package/framework/agents/claude/.claude/agents/red64/validate-gap.md +13 -7
  61. package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
  62. package/framework/agents/claude/.claude/commands/red64/validate-gap.md +4 -0
  63. package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
  64. package/framework/agents/codex/.codex/agents/red64/validate-gap.md +13 -7
  65. package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
  66. package/framework/agents/codex/.codex/commands/red64/validate-gap.md +4 -0
  67. package/framework/stacks/generic/feedback.md +80 -0
  68. package/framework/stacks/nextjs/accessibility.md +437 -0
  69. package/framework/stacks/nextjs/api.md +431 -0
  70. package/framework/stacks/nextjs/coding-style.md +282 -0
  71. package/framework/stacks/nextjs/commenting.md +226 -0
  72. package/framework/stacks/nextjs/components.md +411 -0
  73. package/framework/stacks/nextjs/conventions.md +333 -0
  74. package/framework/stacks/nextjs/css.md +310 -0
  75. package/framework/stacks/nextjs/error-handling.md +442 -0
  76. package/framework/stacks/nextjs/feedback.md +124 -0
  77. package/framework/stacks/nextjs/migrations.md +332 -0
  78. package/framework/stacks/nextjs/models.md +362 -0
  79. package/framework/stacks/nextjs/queries.md +410 -0
  80. package/framework/stacks/nextjs/responsive.md +338 -0
  81. package/framework/stacks/nextjs/tech-stack.md +177 -0
  82. package/framework/stacks/nextjs/test-writing.md +475 -0
  83. package/framework/stacks/nextjs/validation.md +467 -0
  84. package/framework/stacks/python/api.md +468 -0
  85. package/framework/stacks/python/authentication.md +342 -0
  86. package/framework/stacks/python/code-quality.md +283 -0
  87. package/framework/stacks/python/code-refactoring.md +315 -0
  88. package/framework/stacks/python/coding-style.md +462 -0
  89. package/framework/stacks/python/conventions.md +399 -0
  90. package/framework/stacks/python/error-handling.md +512 -0
  91. package/framework/stacks/python/feedback.md +92 -0
  92. package/framework/stacks/python/implement-ai-llm.md +468 -0
  93. package/framework/stacks/python/migrations.md +388 -0
  94. package/framework/stacks/python/models.md +399 -0
  95. package/framework/stacks/python/python.md +232 -0
  96. package/framework/stacks/python/queries.md +451 -0
  97. package/framework/stacks/python/structure.md +245 -58
  98. package/framework/stacks/python/tech.md +92 -35
  99. package/framework/stacks/python/testing.md +380 -0
  100. package/framework/stacks/python/validation.md +471 -0
  101. package/framework/stacks/rails/authentication.md +176 -0
  102. package/framework/stacks/rails/code-quality.md +287 -0
  103. package/framework/stacks/rails/code-refactoring.md +299 -0
  104. package/framework/stacks/rails/feedback.md +130 -0
  105. package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
  106. package/framework/stacks/rails/rails.md +301 -0
  107. package/framework/stacks/rails/rails8-best-practices.md +498 -0
  108. package/framework/stacks/rails/rails8-css.md +573 -0
  109. package/framework/stacks/rails/structure.md +140 -0
  110. package/framework/stacks/rails/tech.md +108 -0
  111. package/framework/stacks/react/code-quality.md +521 -0
  112. package/framework/stacks/react/components.md +625 -0
  113. package/framework/stacks/react/data-fetching.md +586 -0
  114. package/framework/stacks/react/feedback.md +110 -0
  115. package/framework/stacks/react/forms.md +694 -0
  116. package/framework/stacks/react/performance.md +640 -0
  117. package/framework/stacks/react/product.md +22 -9
  118. package/framework/stacks/react/state-management.md +472 -0
  119. package/framework/stacks/react/structure.md +351 -44
  120. package/framework/stacks/react/tech.md +219 -30
  121. package/framework/stacks/react/testing.md +690 -0
  122. package/package.json +1 -1
  123. package/framework/stacks/node/product.md +0 -27
  124. package/framework/stacks/node/structure.md +0 -82
  125. package/framework/stacks/node/tech.md +0 -63
@@ -0,0 +1,694 @@
1
+ # Form Patterns
2
+
3
+ Form handling with React Hook Form and Zod for type-safe, performant forms with excellent UX.
4
+
5
+ ---
6
+
7
+ ## Philosophy
8
+
9
+ - **Minimal re-renders**: Uncontrolled inputs with React Hook Form
10
+ - **Schema-first validation**: Define shape and rules in Zod, infer types
11
+ - **Accessible errors**: Clear, associated error messages
12
+ - **Optimistic UX**: Instant feedback, disable during submission
13
+
14
+ ---
15
+
16
+ ## Setup
17
+
18
+ ```bash
19
+ pnpm add react-hook-form zod @hookform/resolvers
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Basic Form
25
+
26
+ ### Schema Definition
27
+
28
+ ```typescript
29
+ // features/users/schemas/user.schema.ts
30
+ import { z } from 'zod';
31
+
32
+ export const createUserSchema = z.object({
33
+ name: z
34
+ .string()
35
+ .min(1, 'Name is required')
36
+ .max(100, 'Name must be less than 100 characters'),
37
+ email: z
38
+ .string()
39
+ .min(1, 'Email is required')
40
+ .email('Invalid email address'),
41
+ password: z
42
+ .string()
43
+ .min(8, 'Password must be at least 8 characters')
44
+ .regex(/[A-Z]/, 'Password must contain an uppercase letter')
45
+ .regex(/[0-9]/, 'Password must contain a number'),
46
+ role: z.enum(['admin', 'user', 'guest'], {
47
+ errorMap: () => ({ message: 'Please select a role' }),
48
+ }),
49
+ bio: z.string().max(500).optional(),
50
+ });
51
+
52
+ // Infer TypeScript type from schema
53
+ export type CreateUserInput = z.infer<typeof createUserSchema>;
54
+ ```
55
+
56
+ ### Form Component
57
+
58
+ ```typescript
59
+ // features/users/components/CreateUserForm.tsx
60
+ import { useForm } from 'react-hook-form';
61
+ import { zodResolver } from '@hookform/resolvers/zod';
62
+ import { createUserSchema, type CreateUserInput } from '../schemas/user.schema';
63
+ import { useCreateUser } from '../hooks/useCreateUser';
64
+
65
+ export function CreateUserForm({ onSuccess }: { onSuccess?: () => void }) {
66
+ const createUser = useCreateUser();
67
+
68
+ const form = useForm<CreateUserInput>({
69
+ resolver: zodResolver(createUserSchema),
70
+ defaultValues: {
71
+ name: '',
72
+ email: '',
73
+ password: '',
74
+ role: 'user',
75
+ bio: '',
76
+ },
77
+ });
78
+
79
+ const onSubmit = (data: CreateUserInput) => {
80
+ createUser.mutate(data, {
81
+ onSuccess: () => {
82
+ form.reset();
83
+ onSuccess?.();
84
+ },
85
+ });
86
+ };
87
+
88
+ return (
89
+ <form onSubmit={form.handleSubmit(onSubmit)} noValidate>
90
+ <FormField
91
+ label="Name"
92
+ error={form.formState.errors.name?.message}
93
+ >
94
+ <input
95
+ {...form.register('name')}
96
+ type="text"
97
+ aria-invalid={!!form.formState.errors.name}
98
+ />
99
+ </FormField>
100
+
101
+ <FormField
102
+ label="Email"
103
+ error={form.formState.errors.email?.message}
104
+ >
105
+ <input
106
+ {...form.register('email')}
107
+ type="email"
108
+ aria-invalid={!!form.formState.errors.email}
109
+ />
110
+ </FormField>
111
+
112
+ <FormField
113
+ label="Password"
114
+ error={form.formState.errors.password?.message}
115
+ >
116
+ <input
117
+ {...form.register('password')}
118
+ type="password"
119
+ aria-invalid={!!form.formState.errors.password}
120
+ />
121
+ </FormField>
122
+
123
+ <FormField
124
+ label="Role"
125
+ error={form.formState.errors.role?.message}
126
+ >
127
+ <select {...form.register('role')}>
128
+ <option value="">Select a role</option>
129
+ <option value="admin">Admin</option>
130
+ <option value="user">User</option>
131
+ <option value="guest">Guest</option>
132
+ </select>
133
+ </FormField>
134
+
135
+ <FormField
136
+ label="Bio (optional)"
137
+ error={form.formState.errors.bio?.message}
138
+ >
139
+ <textarea {...form.register('bio')} rows={4} />
140
+ </FormField>
141
+
142
+ <Button
143
+ type="submit"
144
+ disabled={form.formState.isSubmitting}
145
+ isLoading={form.formState.isSubmitting}
146
+ >
147
+ Create User
148
+ </Button>
149
+ </form>
150
+ );
151
+ }
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Form Field Component
157
+
158
+ ```typescript
159
+ // components/ui/FormField/FormField.tsx
160
+ import { useId, type ReactNode } from 'react';
161
+ import { cn } from '@/utils/cn';
162
+
163
+ interface FormFieldProps {
164
+ label: string;
165
+ error?: string;
166
+ description?: string;
167
+ required?: boolean;
168
+ children: ReactNode;
169
+ className?: string;
170
+ }
171
+
172
+ export function FormField({
173
+ label,
174
+ error,
175
+ description,
176
+ required,
177
+ children,
178
+ className,
179
+ }: FormFieldProps) {
180
+ const id = useId();
181
+ const errorId = `${id}-error`;
182
+ const descriptionId = `${id}-description`;
183
+
184
+ return (
185
+ <div className={cn('mb-4', className)}>
186
+ <label htmlFor={id} className="mb-1 block text-sm font-medium">
187
+ {label}
188
+ {required && <span className="text-red-500 ml-1">*</span>}
189
+ </label>
190
+
191
+ {description && (
192
+ <p id={descriptionId} className="mb-1 text-sm text-gray-500">
193
+ {description}
194
+ </p>
195
+ )}
196
+
197
+ {/* Clone child and add props */}
198
+ <div
199
+ className={cn(
200
+ '[&>input]:w-full [&>input]:rounded-md [&>input]:border [&>input]:px-3 [&>input]:py-2',
201
+ '[&>select]:w-full [&>select]:rounded-md [&>select]:border [&>select]:px-3 [&>select]:py-2',
202
+ '[&>textarea]:w-full [&>textarea]:rounded-md [&>textarea]:border [&>textarea]:px-3 [&>textarea]:py-2',
203
+ error && '[&>*]:border-red-500'
204
+ )}
205
+ >
206
+ {children}
207
+ </div>
208
+
209
+ {error && (
210
+ <p id={errorId} className="mt-1 text-sm text-red-500" role="alert">
211
+ {error}
212
+ </p>
213
+ )}
214
+ </div>
215
+ );
216
+ }
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Advanced Validation
222
+
223
+ ### Cross-Field Validation
224
+
225
+ ```typescript
226
+ const changePasswordSchema = z
227
+ .object({
228
+ currentPassword: z.string().min(1, 'Current password is required'),
229
+ newPassword: z
230
+ .string()
231
+ .min(8, 'Password must be at least 8 characters')
232
+ .regex(/[A-Z]/, 'Must contain uppercase letter')
233
+ .regex(/[0-9]/, 'Must contain number'),
234
+ confirmPassword: z.string().min(1, 'Please confirm your password'),
235
+ })
236
+ .refine((data) => data.newPassword === data.confirmPassword, {
237
+ message: 'Passwords do not match',
238
+ path: ['confirmPassword'], // Error shown on this field
239
+ })
240
+ .refine((data) => data.newPassword !== data.currentPassword, {
241
+ message: 'New password must be different from current password',
242
+ path: ['newPassword'],
243
+ });
244
+ ```
245
+
246
+ ### Conditional Validation
247
+
248
+ ```typescript
249
+ const orderSchema = z
250
+ .object({
251
+ deliveryMethod: z.enum(['pickup', 'delivery']),
252
+ address: z.string().optional(),
253
+ city: z.string().optional(),
254
+ zipCode: z.string().optional(),
255
+ })
256
+ .refine(
257
+ (data) => {
258
+ if (data.deliveryMethod === 'delivery') {
259
+ return data.address && data.city && data.zipCode;
260
+ }
261
+ return true;
262
+ },
263
+ {
264
+ message: 'Address is required for delivery',
265
+ path: ['address'],
266
+ }
267
+ );
268
+
269
+ // Or use discriminated union
270
+ const orderSchema = z.discriminatedUnion('deliveryMethod', [
271
+ z.object({
272
+ deliveryMethod: z.literal('pickup'),
273
+ }),
274
+ z.object({
275
+ deliveryMethod: z.literal('delivery'),
276
+ address: z.string().min(1, 'Address is required'),
277
+ city: z.string().min(1, 'City is required'),
278
+ zipCode: z.string().regex(/^\d{5}$/, 'Invalid zip code'),
279
+ }),
280
+ ]);
281
+ ```
282
+
283
+ ### Async Validation
284
+
285
+ ```typescript
286
+ const usernameSchema = z.object({
287
+ username: z
288
+ .string()
289
+ .min(3, 'Username must be at least 3 characters')
290
+ .refine(
291
+ async (username) => {
292
+ // Check if username is available
293
+ const response = await fetch(`/api/check-username?q=${username}`);
294
+ const { available } = await response.json();
295
+ return available;
296
+ },
297
+ { message: 'Username is already taken' }
298
+ ),
299
+ });
300
+
301
+ // Use mode: 'onBlur' for async validation
302
+ const form = useForm({
303
+ resolver: zodResolver(usernameSchema),
304
+ mode: 'onBlur', // Validate on blur instead of every keystroke
305
+ });
306
+ ```
307
+
308
+ ---
309
+
310
+ ## Controlled Components with Controller
311
+
312
+ For custom/third-party components that don't support `ref`:
313
+
314
+ ```typescript
315
+ import { Controller, useForm } from 'react-hook-form';
316
+ import { DatePicker } from '@/components/ui/DatePicker';
317
+ import { Select } from '@/components/ui/Select';
318
+
319
+ function EventForm() {
320
+ const form = useForm<EventInput>({
321
+ resolver: zodResolver(eventSchema),
322
+ });
323
+
324
+ return (
325
+ <form onSubmit={form.handleSubmit(onSubmit)}>
326
+ {/* DatePicker needs Controller */}
327
+ <Controller
328
+ name="date"
329
+ control={form.control}
330
+ render={({ field, fieldState }) => (
331
+ <FormField label="Event Date" error={fieldState.error?.message}>
332
+ <DatePicker
333
+ value={field.value}
334
+ onChange={field.onChange}
335
+ onBlur={field.onBlur}
336
+ />
337
+ </FormField>
338
+ )}
339
+ />
340
+
341
+ {/* Custom Select component */}
342
+ <Controller
343
+ name="category"
344
+ control={form.control}
345
+ render={({ field, fieldState }) => (
346
+ <FormField label="Category" error={fieldState.error?.message}>
347
+ <Select
348
+ options={categories}
349
+ value={field.value}
350
+ onChange={field.onChange}
351
+ placeholder="Select category"
352
+ />
353
+ </FormField>
354
+ )}
355
+ />
356
+
357
+ <Button type="submit">Create Event</Button>
358
+ </form>
359
+ );
360
+ }
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Array Fields
366
+
367
+ ```typescript
368
+ import { useFieldArray, useForm } from 'react-hook-form';
369
+
370
+ const teamSchema = z.object({
371
+ name: z.string().min(1),
372
+ members: z
373
+ .array(
374
+ z.object({
375
+ name: z.string().min(1, 'Member name is required'),
376
+ email: z.string().email('Invalid email'),
377
+ })
378
+ )
379
+ .min(1, 'At least one member required'),
380
+ });
381
+
382
+ function TeamForm() {
383
+ const form = useForm<TeamInput>({
384
+ resolver: zodResolver(teamSchema),
385
+ defaultValues: {
386
+ name: '',
387
+ members: [{ name: '', email: '' }],
388
+ },
389
+ });
390
+
391
+ const { fields, append, remove } = useFieldArray({
392
+ control: form.control,
393
+ name: 'members',
394
+ });
395
+
396
+ return (
397
+ <form onSubmit={form.handleSubmit(onSubmit)}>
398
+ <FormField label="Team Name" error={form.formState.errors.name?.message}>
399
+ <input {...form.register('name')} />
400
+ </FormField>
401
+
402
+ <div className="space-y-4">
403
+ <h3>Team Members</h3>
404
+ {fields.map((field, index) => (
405
+ <div key={field.id} className="flex gap-4">
406
+ <FormField
407
+ label="Name"
408
+ error={form.formState.errors.members?.[index]?.name?.message}
409
+ >
410
+ <input {...form.register(`members.${index}.name`)} />
411
+ </FormField>
412
+
413
+ <FormField
414
+ label="Email"
415
+ error={form.formState.errors.members?.[index]?.email?.message}
416
+ >
417
+ <input {...form.register(`members.${index}.email`)} type="email" />
418
+ </FormField>
419
+
420
+ {fields.length > 1 && (
421
+ <Button
422
+ type="button"
423
+ variant="ghost"
424
+ onClick={() => remove(index)}
425
+ >
426
+ Remove
427
+ </Button>
428
+ )}
429
+ </div>
430
+ ))}
431
+
432
+ <Button type="button" variant="secondary" onClick={() => append({ name: '', email: '' })}>
433
+ Add Member
434
+ </Button>
435
+ </div>
436
+
437
+ <Button type="submit">Create Team</Button>
438
+ </form>
439
+ );
440
+ }
441
+ ```
442
+
443
+ ---
444
+
445
+ ## Form with File Upload
446
+
447
+ ```typescript
448
+ const profileSchema = z.object({
449
+ name: z.string().min(1),
450
+ avatar: z
451
+ .instanceof(FileList)
452
+ .refine((files) => files.length <= 1, 'Only one file allowed')
453
+ .refine(
454
+ (files) => files.length === 0 || files[0].size <= 5 * 1024 * 1024,
455
+ 'File must be less than 5MB'
456
+ )
457
+ .refine(
458
+ (files) => files.length === 0 || ['image/jpeg', 'image/png'].includes(files[0].type),
459
+ 'Only JPEG and PNG allowed'
460
+ )
461
+ .optional(),
462
+ });
463
+
464
+ function ProfileForm() {
465
+ const form = useForm<ProfileInput>({
466
+ resolver: zodResolver(profileSchema),
467
+ });
468
+
469
+ const [preview, setPreview] = useState<string | null>(null);
470
+
471
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
472
+ const file = e.target.files?.[0];
473
+ if (file) {
474
+ setPreview(URL.createObjectURL(file));
475
+ }
476
+ };
477
+
478
+ return (
479
+ <form onSubmit={form.handleSubmit(onSubmit)}>
480
+ <FormField label="Avatar" error={form.formState.errors.avatar?.message}>
481
+ <input
482
+ {...form.register('avatar')}
483
+ type="file"
484
+ accept="image/jpeg,image/png"
485
+ onChange={(e) => {
486
+ form.register('avatar').onChange(e);
487
+ handleFileChange(e);
488
+ }}
489
+ />
490
+ {preview && <img src={preview} alt="Preview" className="mt-2 h-20 w-20 rounded" />}
491
+ </FormField>
492
+
493
+ <Button type="submit">Save</Button>
494
+ </form>
495
+ );
496
+ }
497
+ ```
498
+
499
+ ---
500
+
501
+ ## Multi-Step Forms
502
+
503
+ ```typescript
504
+ const stepSchemas = [
505
+ z.object({
506
+ firstName: z.string().min(1),
507
+ lastName: z.string().min(1),
508
+ }),
509
+ z.object({
510
+ email: z.string().email(),
511
+ phone: z.string().min(10),
512
+ }),
513
+ z.object({
514
+ address: z.string().min(1),
515
+ city: z.string().min(1),
516
+ }),
517
+ ];
518
+
519
+ function MultiStepForm() {
520
+ const [step, setStep] = useState(0);
521
+ const [formData, setFormData] = useState({});
522
+
523
+ const form = useForm({
524
+ resolver: zodResolver(stepSchemas[step]),
525
+ defaultValues: formData,
526
+ });
527
+
528
+ const onNext = (data: Record<string, unknown>) => {
529
+ setFormData((prev) => ({ ...prev, ...data }));
530
+ if (step < stepSchemas.length - 1) {
531
+ setStep((s) => s + 1);
532
+ } else {
533
+ // Final submission
534
+ submitFinalForm({ ...formData, ...data });
535
+ }
536
+ };
537
+
538
+ const onBack = () => {
539
+ setStep((s) => Math.max(0, s - 1));
540
+ };
541
+
542
+ return (
543
+ <form onSubmit={form.handleSubmit(onNext)}>
544
+ {/* Step indicator */}
545
+ <div className="mb-8 flex justify-between">
546
+ {stepSchemas.map((_, i) => (
547
+ <div
548
+ key={i}
549
+ className={cn(
550
+ 'h-2 flex-1 rounded',
551
+ i <= step ? 'bg-blue-500' : 'bg-gray-200'
552
+ )}
553
+ />
554
+ ))}
555
+ </div>
556
+
557
+ {/* Step content */}
558
+ {step === 0 && <PersonalInfoStep form={form} />}
559
+ {step === 1 && <ContactInfoStep form={form} />}
560
+ {step === 2 && <AddressStep form={form} />}
561
+
562
+ {/* Navigation */}
563
+ <div className="flex justify-between mt-6">
564
+ <Button type="button" variant="ghost" onClick={onBack} disabled={step === 0}>
565
+ Back
566
+ </Button>
567
+ <Button type="submit">
568
+ {step === stepSchemas.length - 1 ? 'Submit' : 'Next'}
569
+ </Button>
570
+ </div>
571
+ </form>
572
+ );
573
+ }
574
+ ```
575
+
576
+ ---
577
+
578
+ ## Server Errors
579
+
580
+ ```typescript
581
+ function LoginForm() {
582
+ const form = useForm<LoginInput>({
583
+ resolver: zodResolver(loginSchema),
584
+ });
585
+
586
+ const login = useLogin();
587
+
588
+ const onSubmit = async (data: LoginInput) => {
589
+ try {
590
+ await login.mutateAsync(data);
591
+ } catch (error) {
592
+ if (error instanceof ApiError) {
593
+ // Set server-side validation errors
594
+ if (error.code === 'INVALID_CREDENTIALS') {
595
+ form.setError('email', { message: 'Invalid email or password' });
596
+ } else if (error.code === 'ACCOUNT_LOCKED') {
597
+ form.setError('root', { message: 'Account is locked. Please contact support.' });
598
+ }
599
+ }
600
+ }
601
+ };
602
+
603
+ return (
604
+ <form onSubmit={form.handleSubmit(onSubmit)}>
605
+ {/* Root error (not tied to specific field) */}
606
+ {form.formState.errors.root && (
607
+ <Alert variant="error">{form.formState.errors.root.message}</Alert>
608
+ )}
609
+
610
+ <FormField label="Email" error={form.formState.errors.email?.message}>
611
+ <input {...form.register('email')} type="email" />
612
+ </FormField>
613
+
614
+ <FormField label="Password" error={form.formState.errors.password?.message}>
615
+ <input {...form.register('password')} type="password" />
616
+ </FormField>
617
+
618
+ <Button type="submit" disabled={form.formState.isSubmitting}>
619
+ Login
620
+ </Button>
621
+ </form>
622
+ );
623
+ }
624
+ ```
625
+
626
+ ---
627
+
628
+ ## Form Hooks
629
+
630
+ ### Extract Form Logic
631
+
632
+ ```typescript
633
+ // features/users/hooks/useUserForm.ts
634
+ export function useUserForm(options?: { onSuccess?: () => void }) {
635
+ const createUser = useCreateUser();
636
+
637
+ const form = useForm<CreateUserInput>({
638
+ resolver: zodResolver(createUserSchema),
639
+ defaultValues: {
640
+ name: '',
641
+ email: '',
642
+ role: 'user',
643
+ },
644
+ });
645
+
646
+ const onSubmit = form.handleSubmit((data) => {
647
+ createUser.mutate(data, {
648
+ onSuccess: () => {
649
+ form.reset();
650
+ options?.onSuccess?.();
651
+ },
652
+ onError: (error) => {
653
+ form.setError('root', { message: error.message });
654
+ },
655
+ });
656
+ });
657
+
658
+ return {
659
+ form,
660
+ onSubmit,
661
+ isSubmitting: form.formState.isSubmitting || createUser.isPending,
662
+ isSuccess: createUser.isSuccess,
663
+ };
664
+ }
665
+
666
+ // Usage in component
667
+ function CreateUserForm({ onSuccess }: Props) {
668
+ const { form, onSubmit, isSubmitting } = useUserForm({ onSuccess });
669
+
670
+ return (
671
+ <form onSubmit={onSubmit}>
672
+ {/* Form fields */}
673
+ </form>
674
+ );
675
+ }
676
+ ```
677
+
678
+ ---
679
+
680
+ ## Anti-Patterns
681
+
682
+ | Anti-Pattern | Problem | Correct Approach |
683
+ |--------------|---------|------------------|
684
+ | Controlled inputs for everything | Re-renders on every keystroke | Use React Hook Form (uncontrolled) |
685
+ | Validation in component | Not reusable, hard to test | Use Zod schemas |
686
+ | Not using resolver | Manual error handling | Use @hookform/resolvers |
687
+ | Disabled submit always | Bad UX | Disable only during submission |
688
+ | No loading state | User doesn't know it's working | Show spinner/loading text |
689
+ | Alert for every error | Overwhelming | Inline field errors |
690
+ | Form without noValidate | Browser validation conflicts | Add noValidate to form |
691
+
692
+ ---
693
+
694
+ _Forms are user input. Make them fast, validate thoroughly, and provide clear feedback._