@massu/core 0.5.0 → 0.6.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 +40 -0
- package/agents/massu-architecture-reviewer.md +104 -0
- package/agents/massu-blast-radius-analyzer.md +84 -0
- package/agents/massu-competitive-scorer.md +126 -0
- package/agents/massu-help-sync.md +73 -0
- package/agents/massu-migration-writer.md +94 -0
- package/agents/massu-output-scorer.md +87 -0
- package/agents/massu-pattern-reviewer.md +84 -0
- package/agents/massu-plan-auditor.md +170 -0
- package/agents/massu-schema-sync-verifier.md +70 -0
- package/agents/massu-security-reviewer.md +98 -0
- package/agents/massu-ux-reviewer.md +106 -0
- package/commands/_shared-preamble.md +53 -23
- package/commands/_shared-references/auto-learning-protocol.md +71 -0
- package/commands/_shared-references/blast-radius-protocol.md +76 -0
- package/commands/_shared-references/security-pre-screen.md +64 -0
- package/commands/_shared-references/test-first-protocol.md +87 -0
- package/commands/_shared-references/verification-table.md +52 -0
- package/commands/massu-article-review.md +343 -0
- package/commands/massu-autoresearch/references/eval-runner.md +84 -0
- package/commands/massu-autoresearch/references/safety-rails.md +125 -0
- package/commands/massu-autoresearch/references/scoring-protocol.md +151 -0
- package/commands/massu-autoresearch.md +258 -0
- package/commands/massu-batch.md +44 -12
- package/commands/massu-bearings.md +42 -8
- package/commands/massu-checkpoint.md +588 -0
- package/commands/massu-ci-fix.md +2 -2
- package/commands/massu-command-health.md +132 -0
- package/commands/massu-command-improve.md +232 -0
- package/commands/massu-commit.md +205 -44
- package/commands/massu-create-plan.md +239 -57
- package/commands/massu-data/references/common-queries.md +79 -0
- package/commands/massu-data/references/table-guide.md +50 -0
- package/commands/massu-data.md +66 -0
- package/commands/massu-dead-code.md +29 -34
- package/commands/massu-debug/references/auto-learning.md +61 -0
- package/commands/massu-debug/references/codegraph-tracing.md +80 -0
- package/commands/massu-debug/references/common-shortcuts.md +98 -0
- package/commands/massu-debug/references/investigation-phases.md +294 -0
- package/commands/massu-debug/references/report-format.md +107 -0
- package/commands/massu-debug.md +105 -386
- package/commands/massu-docs.md +1 -1
- package/commands/massu-full-audit.md +61 -0
- package/commands/massu-gap-enhancement-analyzer.md +276 -16
- package/commands/massu-golden-path/references/approval-points.md +216 -0
- package/commands/massu-golden-path/references/competitive-mode.md +273 -0
- package/commands/massu-golden-path/references/error-handling.md +121 -0
- package/commands/massu-golden-path/references/phase-0-requirements.md +53 -0
- package/commands/massu-golden-path/references/phase-1-plan-creation.md +168 -0
- package/commands/massu-golden-path/references/phase-2-implementation.md +397 -0
- package/commands/massu-golden-path/references/phase-2.5-gap-analyzer.md +156 -0
- package/commands/massu-golden-path/references/phase-3-simplify.md +40 -0
- package/commands/massu-golden-path/references/phase-4-commit.md +94 -0
- package/commands/massu-golden-path/references/phase-5-push.md +116 -0
- package/commands/massu-golden-path/references/phase-5.5-production-verify.md +170 -0
- package/commands/massu-golden-path/references/phase-6-completion.md +113 -0
- package/commands/massu-golden-path/references/qa-evaluator-spec.md +137 -0
- package/commands/massu-golden-path/references/sprint-contract-protocol.md +117 -0
- package/commands/massu-golden-path/references/vr-visual-calibration.md +73 -0
- package/commands/massu-golden-path.md +114 -848
- package/commands/massu-guide.md +72 -69
- package/commands/massu-hooks.md +27 -12
- package/commands/massu-hotfix.md +221 -144
- package/commands/massu-incident.md +49 -20
- package/commands/massu-infra-audit.md +187 -0
- package/commands/massu-learning-audit.md +211 -0
- package/commands/massu-loop/references/auto-learning.md +49 -0
- package/commands/massu-loop/references/checkpoint-audit.md +40 -0
- package/commands/massu-loop/references/guardrails.md +17 -0
- package/commands/massu-loop/references/iteration-structure.md +115 -0
- package/commands/massu-loop/references/loop-controller.md +188 -0
- package/commands/massu-loop/references/plan-extraction.md +78 -0
- package/commands/massu-loop/references/vr-plan-spec.md +140 -0
- package/commands/massu-loop-playwright.md +9 -9
- package/commands/massu-loop.md +115 -670
- package/commands/massu-new-pattern.md +423 -0
- package/commands/massu-perf.md +422 -0
- package/commands/massu-plan-audit.md +1 -1
- package/commands/massu-plan.md +389 -122
- package/commands/massu-production-verify.md +433 -0
- package/commands/massu-push.md +62 -378
- package/commands/massu-recap.md +29 -3
- package/commands/massu-rollback.md +613 -0
- package/commands/massu-scaffold-hook.md +2 -4
- package/commands/massu-scaffold-page.md +2 -3
- package/commands/massu-scaffold-router.md +1 -2
- package/commands/massu-security.md +619 -0
- package/commands/massu-simplify.md +115 -85
- package/commands/massu-squirrels.md +2 -2
- package/commands/massu-tdd.md +38 -22
- package/commands/massu-test.md +3 -3
- package/commands/massu-type-mismatch-audit.md +469 -0
- package/commands/massu-ui-audit.md +587 -0
- package/commands/massu-verify-playwright.md +287 -32
- package/commands/massu-verify.md +150 -46
- package/dist/cli.js +146 -95
- package/package.json +6 -2
- package/patterns/build-patterns.md +302 -0
- package/patterns/component-patterns.md +246 -0
- package/patterns/display-patterns.md +185 -0
- package/patterns/form-patterns.md +890 -0
- package/patterns/integration-testing-checklist.md +445 -0
- package/patterns/security-patterns.md +219 -0
- package/patterns/testing-patterns.md +569 -0
- package/patterns/tool-routing.md +81 -0
- package/patterns/ui-patterns.md +371 -0
- package/protocols/plan-implementation.md +267 -0
- package/protocols/recovery.md +225 -0
- package/protocols/verification.md +404 -0
- package/reference/command-taxonomy.md +178 -0
- package/reference/cr-rules-reference.md +76 -0
- package/reference/hook-execution-order.md +148 -0
- package/reference/lessons-learned.md +175 -0
- package/reference/patterns-quickref.md +208 -0
- package/reference/standards.md +135 -0
- package/reference/subagents-reference.md +17 -0
- package/reference/vr-verification-reference.md +867 -0
- package/src/commands/install-commands.ts +149 -53
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
# Form Field Patterns
|
|
2
|
+
|
|
3
|
+
**Purpose**: Form patterns for semantic TextField types, PhoneInputField, AddressForm, react-hook-form + Zod setup, register() vs Controller, edit forms, submission patterns.
|
|
4
|
+
|
|
5
|
+
**When to Read**: Before creating or modifying forms.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Sections
|
|
10
|
+
| Section | Line | Description |
|
|
11
|
+
|---------|------|-------------|
|
|
12
|
+
| Overview | ~15 | Purpose, consistency goals, accessibility compliance |
|
|
13
|
+
| Semantic TextField Usage | ~25 | Mandatory TextField semantic types table |
|
|
14
|
+
| Phone Input Pattern | ~117 | PhoneInputField for international phone numbers |
|
|
15
|
+
| Complete Form Example | ~192 | Full form example combining all patterns |
|
|
16
|
+
| Field-Specific Patterns | ~304 | Per-field guidance: money, percent, date, URL |
|
|
17
|
+
| Address Forms | ~430 | AddressForm component usage |
|
|
18
|
+
| Data Storage Standards | ~453 | How to normalize and store form data |
|
|
19
|
+
| Verification Checklist | ~468 | Pre-submit checklist for form implementations |
|
|
20
|
+
| React Hook Form + Zod Standard Patterns | ~483 | Setup, register(), Controller, reset(), watch(), submission |
|
|
21
|
+
| Migration Checklist | ~850 | Steps to migrate legacy forms to standard patterns |
|
|
22
|
+
| Related Documentation | ~871 | Links to related pattern docs |
|
|
23
|
+
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
This document establishes the standard patterns for form fields across your application. Following these patterns ensures:
|
|
27
|
+
- Consistent UX across all forms
|
|
28
|
+
- Proper data normalization and validation
|
|
29
|
+
- Mobile-optimized keyboard experiences
|
|
30
|
+
- Accessibility compliance
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Semantic TextField Usage
|
|
35
|
+
|
|
36
|
+
### [x] MANDATORY - Use TextField with Semantic Types
|
|
37
|
+
|
|
38
|
+
**Always use TextField from `@/components/common/form/TextField.tsx` with semantic types for:**
|
|
39
|
+
|
|
40
|
+
| Field Type | Component | Use For |
|
|
41
|
+
|------------|-----------|---------|
|
|
42
|
+
| First Name | `TextField type="firstName"` | Person first names |
|
|
43
|
+
| Last Name | `TextField type="lastName"` | Person last names |
|
|
44
|
+
| Email | `TextField type="email"` | Email addresses |
|
|
45
|
+
| **Phone** | **`PhoneInputField`** | Phone numbers (international) |
|
|
46
|
+
| Company | `TextField type="company"` | Company/organization names |
|
|
47
|
+
| URL | `TextField type="url"` | Website URLs |
|
|
48
|
+
| Money | `TextField type="money"` | Currency amounts |
|
|
49
|
+
| Percent | `TextField type="percent"` | Percentage values |
|
|
50
|
+
| Date | `TextField type="date"` | Date values |
|
|
51
|
+
|
|
52
|
+
> **Note**: Phone fields use `PhoneInputField` (not TextField) for international support. See [Phone Input Pattern](#phone-input-pattern) below.
|
|
53
|
+
|
|
54
|
+
### [X] WRONG - Raw Input for Semantic Data
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// [X] DO NOT USE - Raw Input without semantic handling
|
|
58
|
+
<Input
|
|
59
|
+
type="text"
|
|
60
|
+
name="first_name"
|
|
61
|
+
placeholder="First name"
|
|
62
|
+
/>
|
|
63
|
+
|
|
64
|
+
<Input
|
|
65
|
+
type="email"
|
|
66
|
+
name="email"
|
|
67
|
+
placeholder="Email"
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<Input
|
|
71
|
+
type="tel"
|
|
72
|
+
name="phone"
|
|
73
|
+
placeholder="Phone"
|
|
74
|
+
/>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**This is WRONG because:**
|
|
78
|
+
- No auto-formatting or normalization
|
|
79
|
+
- No mobile keyboard optimization
|
|
80
|
+
- No inline validation or success states
|
|
81
|
+
- Inconsistent UX across forms
|
|
82
|
+
|
|
83
|
+
### [x] CORRECT - Semantic TextField
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// [x] CORRECT - Use TextField with semantic types
|
|
87
|
+
import { TextField } from '@/components/common/form/TextField';
|
|
88
|
+
|
|
89
|
+
<TextField
|
|
90
|
+
type="firstName"
|
|
91
|
+
name="first_name"
|
|
92
|
+
label="First Name"
|
|
93
|
+
required
|
|
94
|
+
register={register}
|
|
95
|
+
errors={errors}
|
|
96
|
+
setValue={setValue}
|
|
97
|
+
watch={watch}
|
|
98
|
+
showSuccess
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
<TextField
|
|
102
|
+
type="email"
|
|
103
|
+
name="email"
|
|
104
|
+
label="Email Address"
|
|
105
|
+
required
|
|
106
|
+
register={register}
|
|
107
|
+
errors={errors}
|
|
108
|
+
setValue={setValue}
|
|
109
|
+
watch={watch}
|
|
110
|
+
showSuccess
|
|
111
|
+
/>
|
|
112
|
+
|
|
113
|
+
<PhoneInputField
|
|
114
|
+
name="phone"
|
|
115
|
+
label="Phone Number"
|
|
116
|
+
defaultCountry="US"
|
|
117
|
+
errors={errors}
|
|
118
|
+
setValue={setValue}
|
|
119
|
+
watch={watch}
|
|
120
|
+
showSuccess
|
|
121
|
+
/>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Phone Input Pattern
|
|
127
|
+
|
|
128
|
+
### [x] MANDATORY - Use PhoneInputField for Phone Numbers
|
|
129
|
+
|
|
130
|
+
International phone support with country code selection.
|
|
131
|
+
|
|
132
|
+
| Aspect | Value |
|
|
133
|
+
|--------|-------|
|
|
134
|
+
| Component | `PhoneInputField` |
|
|
135
|
+
| Location | `@/components/common/form/PhoneInputField.tsx` |
|
|
136
|
+
| Purpose | International phone input with country selector |
|
|
137
|
+
| Storage Format | E.164 (`+12025551234`) |
|
|
138
|
+
|
|
139
|
+
### [X] WRONG - TextField type="phone" (Deprecated)
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
// [X] DEPRECATED - US/Canada only, no international support
|
|
143
|
+
<TextField
|
|
144
|
+
type="phone"
|
|
145
|
+
name="phone"
|
|
146
|
+
label="Phone Number"
|
|
147
|
+
icon={Phone}
|
|
148
|
+
formatAsYouType
|
|
149
|
+
register={register}
|
|
150
|
+
errors={errors}
|
|
151
|
+
setValue={setValue}
|
|
152
|
+
watch={watch}
|
|
153
|
+
/>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**This is WRONG because:**
|
|
157
|
+
- Only supports US/Canada phone formatting
|
|
158
|
+
- No country code selection
|
|
159
|
+
- Cannot handle international customers
|
|
160
|
+
- Not E.164 compatible for Twilio
|
|
161
|
+
|
|
162
|
+
### [x] CORRECT - PhoneInputField
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
// [x] CORRECT - Full international support
|
|
166
|
+
import { PhoneInputField } from '@/components/common/form/PhoneInputField';
|
|
167
|
+
|
|
168
|
+
<PhoneInputField
|
|
169
|
+
name="phone"
|
|
170
|
+
label="Phone Number"
|
|
171
|
+
defaultCountry="US"
|
|
172
|
+
errors={errors}
|
|
173
|
+
setValue={setValue}
|
|
174
|
+
watch={watch}
|
|
175
|
+
showSuccess
|
|
176
|
+
/>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Benefits:**
|
|
180
|
+
- Country selector with flags and dial codes
|
|
181
|
+
- Auto-formatting per selected country
|
|
182
|
+
- Stores in E.164 format for Twilio compatibility
|
|
183
|
+
- Uses libphonenumber-js for accurate validation
|
|
184
|
+
|
|
185
|
+
### PhoneInputField Props
|
|
186
|
+
|
|
187
|
+
| Prop | Type | Required | Description |
|
|
188
|
+
|------|------|----------|-------------|
|
|
189
|
+
| `name` | string | Yes | Field name for react-hook-form |
|
|
190
|
+
| `label` | string | Yes | Label text |
|
|
191
|
+
| `defaultCountry` | CountryCode | No | Default country (default: 'US') |
|
|
192
|
+
| `errors` | FieldErrors | Yes | react-hook-form errors |
|
|
193
|
+
| `setValue` | UseFormSetValue | Yes | react-hook-form setValue |
|
|
194
|
+
| `watch` | UseFormWatch | Yes | react-hook-form watch |
|
|
195
|
+
| `showSuccess` | boolean | No | Show green check when valid |
|
|
196
|
+
| `required` | boolean | No | Mark field as required |
|
|
197
|
+
| `disabled` | boolean | No | Disable the field |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Complete Form Example
|
|
202
|
+
|
|
203
|
+
### Standard Contact Form Pattern
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
'use client';
|
|
207
|
+
|
|
208
|
+
import { useForm } from 'react-hook-form';
|
|
209
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
210
|
+
import { z } from 'zod';
|
|
211
|
+
import { TextField } from '@/components/common/form/TextField';
|
|
212
|
+
import { PhoneInputField } from '@/components/common/form/PhoneInputField';
|
|
213
|
+
import { Button } from '@/components/ui/button';
|
|
214
|
+
|
|
215
|
+
const contactSchema = z.object({
|
|
216
|
+
first_name: z.string().min(1, 'First name is required'),
|
|
217
|
+
last_name: z.string().optional(),
|
|
218
|
+
email: z.string().email('Valid email required'),
|
|
219
|
+
phone: z.string().optional(),
|
|
220
|
+
company: z.string().optional(),
|
|
221
|
+
website: z.string().url().optional().or(z.literal('')),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
type ContactFormData = z.infer<typeof contactSchema>;
|
|
225
|
+
|
|
226
|
+
export function ContactForm({ onSubmit }: { onSubmit: (data: ContactFormData) => void }) {
|
|
227
|
+
const {
|
|
228
|
+
register,
|
|
229
|
+
handleSubmit,
|
|
230
|
+
setValue,
|
|
231
|
+
watch,
|
|
232
|
+
formState: { errors },
|
|
233
|
+
} = useForm<ContactFormData>({
|
|
234
|
+
resolver: zodResolver(contactSchema),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
239
|
+
<div className="grid grid-cols-2 gap-4">
|
|
240
|
+
<TextField
|
|
241
|
+
type="firstName"
|
|
242
|
+
name="first_name"
|
|
243
|
+
label="First Name"
|
|
244
|
+
required
|
|
245
|
+
register={register}
|
|
246
|
+
errors={errors}
|
|
247
|
+
setValue={setValue}
|
|
248
|
+
watch={watch}
|
|
249
|
+
showSuccess
|
|
250
|
+
/>
|
|
251
|
+
<TextField
|
|
252
|
+
type="lastName"
|
|
253
|
+
name="last_name"
|
|
254
|
+
label="Last Name"
|
|
255
|
+
register={register}
|
|
256
|
+
errors={errors}
|
|
257
|
+
setValue={setValue}
|
|
258
|
+
watch={watch}
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<TextField
|
|
263
|
+
type="email"
|
|
264
|
+
name="email"
|
|
265
|
+
label="Email Address"
|
|
266
|
+
required
|
|
267
|
+
register={register}
|
|
268
|
+
errors={errors}
|
|
269
|
+
setValue={setValue}
|
|
270
|
+
watch={watch}
|
|
271
|
+
showSuccess
|
|
272
|
+
/>
|
|
273
|
+
|
|
274
|
+
<PhoneInputField
|
|
275
|
+
name="phone"
|
|
276
|
+
label="Phone Number"
|
|
277
|
+
defaultCountry="US"
|
|
278
|
+
errors={errors}
|
|
279
|
+
setValue={setValue}
|
|
280
|
+
watch={watch}
|
|
281
|
+
showSuccess
|
|
282
|
+
/>
|
|
283
|
+
|
|
284
|
+
<TextField
|
|
285
|
+
type="company"
|
|
286
|
+
name="company"
|
|
287
|
+
label="Company"
|
|
288
|
+
register={register}
|
|
289
|
+
errors={errors}
|
|
290
|
+
setValue={setValue}
|
|
291
|
+
watch={watch}
|
|
292
|
+
/>
|
|
293
|
+
|
|
294
|
+
<TextField
|
|
295
|
+
type="url"
|
|
296
|
+
name="website"
|
|
297
|
+
label="Website"
|
|
298
|
+
placeholder="https://example.com"
|
|
299
|
+
register={register}
|
|
300
|
+
errors={errors}
|
|
301
|
+
setValue={setValue}
|
|
302
|
+
watch={watch}
|
|
303
|
+
/>
|
|
304
|
+
|
|
305
|
+
<Button type="submit">Save Contact</Button>
|
|
306
|
+
</form>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Field-Specific Patterns
|
|
314
|
+
|
|
315
|
+
### Phone Number Fields
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
// International phone with country selector
|
|
319
|
+
import { PhoneInputField } from '@/components/common/form/PhoneInputField';
|
|
320
|
+
|
|
321
|
+
<PhoneInputField
|
|
322
|
+
name="phone"
|
|
323
|
+
label="Phone Number"
|
|
324
|
+
defaultCountry="US" // Default country (supports all countries)
|
|
325
|
+
showSuccess // Shows checkmark when valid
|
|
326
|
+
errors={errors}
|
|
327
|
+
setValue={setValue}
|
|
328
|
+
watch={watch}
|
|
329
|
+
/>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Behavior:**
|
|
333
|
+
- Country selector dropdown with flags and dial codes
|
|
334
|
+
- Auto-formats per selected country as user types
|
|
335
|
+
- Stores in E.164 format (`+15551234567`) for Twilio compatibility
|
|
336
|
+
- Shows validation checkmark when valid
|
|
337
|
+
- Uses libphonenumber-js for accurate validation
|
|
338
|
+
|
|
339
|
+
### Email Fields
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
// Email with validation feedback
|
|
343
|
+
<TextField
|
|
344
|
+
type="email"
|
|
345
|
+
name="email"
|
|
346
|
+
label="Email Address"
|
|
347
|
+
required
|
|
348
|
+
showSuccess
|
|
349
|
+
register={register}
|
|
350
|
+
errors={errors}
|
|
351
|
+
setValue={setValue}
|
|
352
|
+
watch={watch}
|
|
353
|
+
/>
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Behavior:**
|
|
357
|
+
- `inputMode="email"` for mobile email keyboard
|
|
358
|
+
- `autoCapitalize="none"` prevents unwanted caps
|
|
359
|
+
- Normalizes to lowercase and trimmed on blur
|
|
360
|
+
- Validates RFC 5322 pattern
|
|
361
|
+
|
|
362
|
+
### Name Fields
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
// First name with auto-capitalize
|
|
366
|
+
<TextField
|
|
367
|
+
type="firstName"
|
|
368
|
+
name="first_name"
|
|
369
|
+
label="First Name"
|
|
370
|
+
required
|
|
371
|
+
register={register}
|
|
372
|
+
errors={errors}
|
|
373
|
+
setValue={setValue}
|
|
374
|
+
watch={watch}
|
|
375
|
+
/>
|
|
376
|
+
|
|
377
|
+
// Last name with smart capitalization
|
|
378
|
+
<TextField
|
|
379
|
+
type="lastName"
|
|
380
|
+
name="last_name"
|
|
381
|
+
label="Last Name"
|
|
382
|
+
register={register}
|
|
383
|
+
errors={errors}
|
|
384
|
+
setValue={setValue}
|
|
385
|
+
watch={watch}
|
|
386
|
+
/>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**Behavior:**
|
|
390
|
+
- `autoCapitalize="words"` for mobile
|
|
391
|
+
- Smart capitalization handles: McDonald, O'Brien, de la Cruz
|
|
392
|
+
- Normalizes on blur
|
|
393
|
+
|
|
394
|
+
### Money Fields
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
// Currency input with prefix
|
|
398
|
+
<TextField
|
|
399
|
+
type="money"
|
|
400
|
+
name="price"
|
|
401
|
+
label="Price"
|
|
402
|
+
prefix="$"
|
|
403
|
+
register={register}
|
|
404
|
+
errors={errors}
|
|
405
|
+
setValue={setValue}
|
|
406
|
+
watch={watch}
|
|
407
|
+
/>
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Behavior:**
|
|
411
|
+
- `inputMode="decimal"` for numeric keyboard with decimal
|
|
412
|
+
- Displays `$` prefix
|
|
413
|
+
- Accepts: `1234.56`, `1,234.56`, `$1,234.56`
|
|
414
|
+
- Stores as decimal number
|
|
415
|
+
|
|
416
|
+
### URL Fields
|
|
417
|
+
|
|
418
|
+
```tsx
|
|
419
|
+
// URL with protocol normalization
|
|
420
|
+
<TextField
|
|
421
|
+
type="url"
|
|
422
|
+
name="website"
|
|
423
|
+
label="Website"
|
|
424
|
+
placeholder="https://example.com"
|
|
425
|
+
register={register}
|
|
426
|
+
errors={errors}
|
|
427
|
+
setValue={setValue}
|
|
428
|
+
watch={watch}
|
|
429
|
+
/>
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Behavior:**
|
|
433
|
+
- `inputMode="url"` for URL keyboard
|
|
434
|
+
- Auto-adds `https://` if missing on blur
|
|
435
|
+
- Validates URL structure
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Address Forms
|
|
440
|
+
|
|
441
|
+
### [x] CORRECT - Use AddressForm Component
|
|
442
|
+
|
|
443
|
+
```tsx
|
|
444
|
+
import { AddressForm, type AddressFormData } from '@/components/crm/AddressForm';
|
|
445
|
+
|
|
446
|
+
// In your form
|
|
447
|
+
<AddressForm
|
|
448
|
+
data={addressData}
|
|
449
|
+
onChange={setAddressData}
|
|
450
|
+
enableAutoDetect // ZIP auto-fills city/state for US/Canada
|
|
451
|
+
/>
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Baymard UX Pattern:**
|
|
455
|
+
- Country field FIRST (determines ZIP format)
|
|
456
|
+
- ZIP code field BEFORE City/State
|
|
457
|
+
- Auto-detection fills City/State from ZIP
|
|
458
|
+
- Reduces typos and improves completion speed
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Data Storage Standards
|
|
463
|
+
|
|
464
|
+
| Field Type | Storage Format | Example |
|
|
465
|
+
|------------|---------------|---------|
|
|
466
|
+
| Phone | E.164 | `+15551234567` |
|
|
467
|
+
| Email | Lowercase, trimmed | `john@example.com` |
|
|
468
|
+
| First Name | Title case | `John` |
|
|
469
|
+
| Last Name | Smart title case | `McDonald` |
|
|
470
|
+
| Company | Trimmed | `Acme Corp` |
|
|
471
|
+
| URL | Full with protocol | `https://example.com` |
|
|
472
|
+
| Money | Decimal | `1234.56` |
|
|
473
|
+
| Percent | Decimal fraction | `0.0825` (for 8.25%) |
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Verification Checklist
|
|
478
|
+
|
|
479
|
+
When creating or updating forms:
|
|
480
|
+
- [ ] All name fields use `type="firstName"` or `type="lastName"`
|
|
481
|
+
- [ ] All email fields use `type="email"`
|
|
482
|
+
- [ ] All phone fields use `PhoneInputField` (NOT TextField type="phone")
|
|
483
|
+
- [ ] All company fields use `type="company"`
|
|
484
|
+
- [ ] All URL fields use `type="url"`
|
|
485
|
+
- [ ] All money fields use `type="money"` with appropriate prefix
|
|
486
|
+
- [ ] `setValue` and `watch` props passed for normalization
|
|
487
|
+
- [ ] `showSuccess` enabled for required fields
|
|
488
|
+
- [ ] Address forms use `AddressForm` component
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## React Hook Form + Zod Standard Patterns
|
|
493
|
+
|
|
494
|
+
All forms MUST use react-hook-form with Zod validation.
|
|
495
|
+
|
|
496
|
+
### [x] MANDATORY - Form Setup Pattern
|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
'use client';
|
|
500
|
+
|
|
501
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
502
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
503
|
+
import { z } from 'zod';
|
|
504
|
+
|
|
505
|
+
// 1. Define Zod schema with proper validation
|
|
506
|
+
const myFormSchema = z.object({
|
|
507
|
+
name: z.string().min(1, 'Name is required'),
|
|
508
|
+
email: z.string().min(1, 'Email is required').email('Please enter a valid email'),
|
|
509
|
+
description: z.string(), // Optional field (no .min())
|
|
510
|
+
status: z.enum(['draft', 'active', 'archived']),
|
|
511
|
+
is_active: z.boolean(),
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// 2. Infer TypeScript type from schema
|
|
515
|
+
type MyFormData = z.infer<typeof myFormSchema>;
|
|
516
|
+
|
|
517
|
+
// 3. Setup useForm with zodResolver
|
|
518
|
+
const {
|
|
519
|
+
register,
|
|
520
|
+
handleSubmit,
|
|
521
|
+
control, // For Controller components
|
|
522
|
+
reset, // For populating edit forms
|
|
523
|
+
watch, // For reactive values in UI
|
|
524
|
+
formState: { errors, isSubmitting },
|
|
525
|
+
} = useForm<MyFormData>({
|
|
526
|
+
resolver: zodResolver(myFormSchema),
|
|
527
|
+
defaultValues: {
|
|
528
|
+
name: '',
|
|
529
|
+
email: '',
|
|
530
|
+
description: '',
|
|
531
|
+
status: 'draft',
|
|
532
|
+
is_active: true,
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### [X] WRONG - Legacy useState/formData Pattern
|
|
538
|
+
|
|
539
|
+
```tsx
|
|
540
|
+
// [X] DO NOT USE - Legacy pattern with individual or object useState
|
|
541
|
+
const [formData, setFormData] = useState({
|
|
542
|
+
name: '',
|
|
543
|
+
email: '',
|
|
544
|
+
});
|
|
545
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
546
|
+
|
|
547
|
+
// Manual field updates
|
|
548
|
+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
549
|
+
setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// Manual submit handling
|
|
553
|
+
const handleSubmit = async () => {
|
|
554
|
+
setIsSubmitting(true);
|
|
555
|
+
// ... validation, submission
|
|
556
|
+
setIsSubmitting(false);
|
|
557
|
+
};
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**This is WRONG because:**
|
|
561
|
+
- No built-in validation or error handling
|
|
562
|
+
- Manual state synchronization
|
|
563
|
+
- Verbose boilerplate code
|
|
564
|
+
- No TypeScript inference from schema
|
|
565
|
+
|
|
566
|
+
### Native Input Elements - Use register()
|
|
567
|
+
|
|
568
|
+
For Input, Textarea, and other native form elements:
|
|
569
|
+
|
|
570
|
+
```tsx
|
|
571
|
+
// Input with register()
|
|
572
|
+
<div className="space-y-2">
|
|
573
|
+
<Label htmlFor="name">Name *</Label>
|
|
574
|
+
<Input
|
|
575
|
+
id="name"
|
|
576
|
+
{...register('name')}
|
|
577
|
+
placeholder="Enter name"
|
|
578
|
+
className={errors.name ? 'border-destructive' : ''}
|
|
579
|
+
/>
|
|
580
|
+
{errors.name && (
|
|
581
|
+
<p className="text-xs text-destructive">{errors.name.message}</p>
|
|
582
|
+
)}
|
|
583
|
+
</div>
|
|
584
|
+
|
|
585
|
+
// Textarea with register()
|
|
586
|
+
<Textarea
|
|
587
|
+
id="description"
|
|
588
|
+
{...register('description')}
|
|
589
|
+
placeholder="Enter description..."
|
|
590
|
+
rows={3}
|
|
591
|
+
/>
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Non-Native Elements - Use Controller
|
|
595
|
+
|
|
596
|
+
For Select, Switch, Checkbox, and custom components:
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
// Select with Controller
|
|
600
|
+
<Controller
|
|
601
|
+
name="status"
|
|
602
|
+
control={control}
|
|
603
|
+
render={({ field }) => (
|
|
604
|
+
<Select value={field.value} onValueChange={field.onChange}>
|
|
605
|
+
<SelectTrigger>
|
|
606
|
+
<SelectValue placeholder="Select status" />
|
|
607
|
+
</SelectTrigger>
|
|
608
|
+
<SelectContent>
|
|
609
|
+
<SelectItem value="draft">Draft</SelectItem>
|
|
610
|
+
<SelectItem value="active">Active</SelectItem>
|
|
611
|
+
<SelectItem value="archived">Archived</SelectItem>
|
|
612
|
+
</SelectContent>
|
|
613
|
+
</Select>
|
|
614
|
+
)}
|
|
615
|
+
/>
|
|
616
|
+
|
|
617
|
+
// Switch with Controller
|
|
618
|
+
<Controller
|
|
619
|
+
name="is_active"
|
|
620
|
+
control={control}
|
|
621
|
+
render={({ field }) => (
|
|
622
|
+
<Switch
|
|
623
|
+
id="is_active"
|
|
624
|
+
checked={field.value}
|
|
625
|
+
onCheckedChange={field.onChange}
|
|
626
|
+
/>
|
|
627
|
+
)}
|
|
628
|
+
/>
|
|
629
|
+
|
|
630
|
+
// Checkbox with Controller
|
|
631
|
+
<Controller
|
|
632
|
+
name="agree_terms"
|
|
633
|
+
control={control}
|
|
634
|
+
render={({ field }) => (
|
|
635
|
+
<Checkbox
|
|
636
|
+
checked={field.value}
|
|
637
|
+
onCheckedChange={field.onChange}
|
|
638
|
+
/>
|
|
639
|
+
)}
|
|
640
|
+
/>
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Edit Forms - Use reset() in useEffect
|
|
644
|
+
|
|
645
|
+
For forms that load existing data:
|
|
646
|
+
|
|
647
|
+
```tsx
|
|
648
|
+
// Fetch existing data
|
|
649
|
+
const { data: item, isLoading } = api.items.getById.useQuery(
|
|
650
|
+
{ id },
|
|
651
|
+
{ enabled: !!id }
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
// Populate form when data loads
|
|
655
|
+
useEffect(() => {
|
|
656
|
+
if (item) {
|
|
657
|
+
reset({
|
|
658
|
+
name: item.name || '',
|
|
659
|
+
email: item.email || '',
|
|
660
|
+
description: item.description || '',
|
|
661
|
+
status: (item.status as MyFormData['status']) || 'draft',
|
|
662
|
+
is_active: item.is_active ?? true,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}, [item, reset]);
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### Reactive Values - Use watch()
|
|
669
|
+
|
|
670
|
+
For displaying form values in UI or validation:
|
|
671
|
+
|
|
672
|
+
```tsx
|
|
673
|
+
// Watch specific field for UI updates
|
|
674
|
+
const name = watch('name');
|
|
675
|
+
|
|
676
|
+
// Use in conditional rendering or validation
|
|
677
|
+
const isFormValid = name.trim() && email.trim();
|
|
678
|
+
|
|
679
|
+
// In submit button
|
|
680
|
+
<Button type="submit" disabled={!isFormValid || isSubmitting}>
|
|
681
|
+
{isSubmitting ? 'Saving...' : 'Save'}
|
|
682
|
+
</Button>
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### UI-Only State - Keep Separate
|
|
686
|
+
|
|
687
|
+
For transient UI state like tag/capability inputs:
|
|
688
|
+
|
|
689
|
+
```tsx
|
|
690
|
+
// Tags that are added via Enter key - keep as separate useState
|
|
691
|
+
const [tags, setTags] = useState<string[]>([]);
|
|
692
|
+
const [tagInput, setTagInput] = useState('');
|
|
693
|
+
|
|
694
|
+
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
695
|
+
if (e.key === 'Enter' && tagInput.trim()) {
|
|
696
|
+
e.preventDefault();
|
|
697
|
+
if (!tags.includes(tagInput.trim())) {
|
|
698
|
+
setTags([...tags, tagInput.trim()]);
|
|
699
|
+
}
|
|
700
|
+
setTagInput('');
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
// Note: Tags are NOT part of the Zod schema - they're managed separately
|
|
705
|
+
// Include tags in the mutation call, not the form data
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### Form Submission Pattern
|
|
709
|
+
|
|
710
|
+
```tsx
|
|
711
|
+
// Name the handler 'onSubmit' to avoid conflict with handleSubmit from useForm
|
|
712
|
+
const onSubmit = (data: MyFormData) => {
|
|
713
|
+
mutation.mutate({
|
|
714
|
+
name: data.name.trim(),
|
|
715
|
+
email: data.email.trim(),
|
|
716
|
+
description: data.description.trim() || null,
|
|
717
|
+
status: data.status,
|
|
718
|
+
is_active: data.is_active,
|
|
719
|
+
});
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
// In JSX - wrap with handleSubmit from useForm
|
|
723
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
724
|
+
{/* form fields */}
|
|
725
|
+
</form>
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### Complete Edit Form Example
|
|
729
|
+
|
|
730
|
+
```tsx
|
|
731
|
+
'use client';
|
|
732
|
+
|
|
733
|
+
import { use, useState, useEffect, Suspense } from 'react';
|
|
734
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
735
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
736
|
+
import { z } from 'zod';
|
|
737
|
+
import { api } from '@/lib/api/client';
|
|
738
|
+
|
|
739
|
+
const templateSchema = z.object({
|
|
740
|
+
name: z.string().min(1, 'Name is required'),
|
|
741
|
+
description: z.string(),
|
|
742
|
+
template_type: z.enum(['email', 'sms', 'notification']),
|
|
743
|
+
is_active: z.boolean(),
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
type TemplateFormData = z.infer<typeof templateSchema>;
|
|
747
|
+
|
|
748
|
+
function TemplateEditPage({ params }: { params: Promise<{ id: string }> }) {
|
|
749
|
+
const { id } = use(params);
|
|
750
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
751
|
+
|
|
752
|
+
const {
|
|
753
|
+
register,
|
|
754
|
+
handleSubmit,
|
|
755
|
+
control,
|
|
756
|
+
reset,
|
|
757
|
+
watch,
|
|
758
|
+
formState: { errors, isSubmitting },
|
|
759
|
+
} = useForm<TemplateFormData>({
|
|
760
|
+
resolver: zodResolver(templateSchema),
|
|
761
|
+
defaultValues: {
|
|
762
|
+
name: '',
|
|
763
|
+
description: '',
|
|
764
|
+
template_type: 'email',
|
|
765
|
+
is_active: true,
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const name = watch('name');
|
|
770
|
+
|
|
771
|
+
const { data: template, isLoading, refetch } = api.templates.getById.useQuery(
|
|
772
|
+
{ id },
|
|
773
|
+
{ enabled: !!id }
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
const updateMutation = api.templates.update.useMutation({
|
|
777
|
+
onSuccess: () => {
|
|
778
|
+
setIsEditing(false);
|
|
779
|
+
refetch();
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
useEffect(() => {
|
|
784
|
+
if (template) {
|
|
785
|
+
reset({
|
|
786
|
+
name: template.name || '',
|
|
787
|
+
description: template.description || '',
|
|
788
|
+
template_type: (template.template_type as TemplateFormData['template_type']) || 'email',
|
|
789
|
+
is_active: template.is_active ?? true,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}, [template, reset]);
|
|
793
|
+
|
|
794
|
+
const onSubmit = (data: TemplateFormData) => {
|
|
795
|
+
updateMutation.mutate({
|
|
796
|
+
id,
|
|
797
|
+
data: {
|
|
798
|
+
name: data.name,
|
|
799
|
+
description: data.description || null,
|
|
800
|
+
template_type: data.template_type,
|
|
801
|
+
is_active: data.is_active,
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
const handleCancel = () => {
|
|
807
|
+
if (template) {
|
|
808
|
+
reset({
|
|
809
|
+
name: template.name || '',
|
|
810
|
+
description: template.description || '',
|
|
811
|
+
template_type: (template.template_type as TemplateFormData['template_type']) || 'email',
|
|
812
|
+
is_active: template.is_active ?? true,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
setIsEditing(false);
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
return (
|
|
819
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
820
|
+
<Input {...register('name')} className={errors.name ? 'border-destructive' : ''} />
|
|
821
|
+
{errors.name && <p className="text-xs text-destructive">{errors.name.message}</p>}
|
|
822
|
+
|
|
823
|
+
<Textarea {...register('description')} />
|
|
824
|
+
|
|
825
|
+
<Controller
|
|
826
|
+
name="template_type"
|
|
827
|
+
control={control}
|
|
828
|
+
render={({ field }) => (
|
|
829
|
+
<Select value={field.value} onValueChange={field.onChange}>
|
|
830
|
+
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
831
|
+
<SelectContent>
|
|
832
|
+
<SelectItem value="email">Email</SelectItem>
|
|
833
|
+
<SelectItem value="sms">SMS</SelectItem>
|
|
834
|
+
<SelectItem value="notification">Notification</SelectItem>
|
|
835
|
+
</SelectContent>
|
|
836
|
+
</Select>
|
|
837
|
+
)}
|
|
838
|
+
/>
|
|
839
|
+
|
|
840
|
+
<Controller
|
|
841
|
+
name="is_active"
|
|
842
|
+
control={control}
|
|
843
|
+
render={({ field }) => (
|
|
844
|
+
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
|
845
|
+
)}
|
|
846
|
+
/>
|
|
847
|
+
|
|
848
|
+
<Button type="button" variant="outline" onClick={handleCancel}>Cancel</Button>
|
|
849
|
+
<Button type="submit" disabled={!name || isSubmitting}>
|
|
850
|
+
{isSubmitting ? 'Saving...' : 'Save'}
|
|
851
|
+
</Button>
|
|
852
|
+
</form>
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
---
|
|
858
|
+
|
|
859
|
+
## Migration Checklist
|
|
860
|
+
|
|
861
|
+
When migrating forms from useState/formData to react-hook-form:
|
|
862
|
+
|
|
863
|
+
- [ ] Import useForm, Controller, zodResolver, z
|
|
864
|
+
- [ ] Create Zod schema with all form fields
|
|
865
|
+
- [ ] Create TypeScript type with z.infer
|
|
866
|
+
- [ ] Replace useState with useForm setup
|
|
867
|
+
- [ ] Set defaultValues for all fields
|
|
868
|
+
- [ ] Replace manual setFormData with register() for native inputs
|
|
869
|
+
- [ ] Use Controller for Select, Switch, Checkbox
|
|
870
|
+
- [ ] Rename handleSubmit to onSubmit (avoid conflict)
|
|
871
|
+
- [ ] Wrap form with handleSubmit(onSubmit)
|
|
872
|
+
- [ ] Replace setIsSubmitting with isSubmitting from formState
|
|
873
|
+
- [ ] For edit forms: use reset() in useEffect
|
|
874
|
+
- [ ] For reactive values: use watch()
|
|
875
|
+
- [ ] Keep UI-only state (tags, etc.) as separate useState
|
|
876
|
+
- [ ] Add error display with errors.fieldName.message
|
|
877
|
+
|
|
878
|
+
---
|
|
879
|
+
|
|
880
|
+
## Related Documentation
|
|
881
|
+
|
|
882
|
+
- **Component**: `src/components/common/form/TextField.tsx`
|
|
883
|
+
- **Formatting Functions**: `src/lib/formatting/fields.ts`
|
|
884
|
+
- **Display Patterns**: `patterns/display-patterns.md`
|
|
885
|
+
- **UI Patterns**: `patterns/ui-patterns.md`
|
|
886
|
+
|
|
887
|
+
---
|
|
888
|
+
|
|
889
|
+
**Status**: MANDATORY
|
|
890
|
+
**Compliance**: All forms MUST follow these patterns
|