wcz-layout 7.6.1 → 7.6.2

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.
@@ -0,0 +1,331 @@
1
+ ---
2
+ name: forms-validation
3
+ description: >
4
+ Build forms with useLayoutForm hook (primary) and withLayoutForm for
5
+ composable sub-forms. 13 pre-registered MUI field components via
6
+ form.AppField: TextField, NumberField, Autocomplete, Checkbox, Switch,
7
+ RadioGroup, Slider, DatePicker, DateRangePicker, TimePicker,
8
+ TimeRangePicker, DateTimePicker, DateTimeRangePicker. SubmitButton in
9
+ form.AppForm. Zod onChange validators. FormOmittedProps type. Activate
10
+ when creating or modifying forms with validation.
11
+ type: core
12
+ library: wcz-layout
13
+ library_version: "7.6.1"
14
+ sources:
15
+ - "wcz-layout:src/hooks/FormHooks.ts"
16
+ - "wcz-layout:src/components/form/"
17
+ - "wcz-layout:src/lib/utils.ts"
18
+ references:
19
+ - "references/field-components.md"
20
+ ---
21
+
22
+ # Forms & Validation
23
+
24
+ ## Setup
25
+
26
+ Import the form hook and Zod schema:
27
+
28
+ ```typescript
29
+ import { useLayoutForm } from "wcz-layout/hooks";
30
+ import { TodoSchema } from "~/schemas/todo";
31
+ ```
32
+
33
+ ## Core Patterns
34
+
35
+ ### Basic form with useLayoutForm
36
+
37
+ ```typescript
38
+ import { useLayoutForm } from "wcz-layout/hooks";
39
+ import { TodoSchema } from "~/schemas/todo";
40
+ import type { Todo } from "~/schemas/todo";
41
+ import { todoCollection } from "~/lib/db/collections/todoCollection";
42
+ import { uuidv7 } from "uuidv7";
43
+
44
+ function TodoForm() {
45
+ const form = useLayoutForm({
46
+ defaultValues: {
47
+ id: uuidv7(),
48
+ name: "",
49
+ description: "",
50
+ isCompleted: false,
51
+ } as Todo,
52
+ validators: {
53
+ onChange: TodoSchema,
54
+ },
55
+ onSubmit: ({ value }) => {
56
+ todoCollection.insert(value);
57
+ },
58
+ });
59
+
60
+ return (
61
+ <form.AppForm>
62
+ <form.AppField
63
+ name="name"
64
+ children={(field) => <field.TextField label="Name" required />}
65
+ />
66
+ <form.AppField
67
+ name="description"
68
+ children={(field) => <field.TextField label="Description" multiline rows={3} />}
69
+ />
70
+ <form.AppField
71
+ name="isCompleted"
72
+ children={(field) => <field.Checkbox label="Completed" />}
73
+ />
74
+ <form.SubmitButton />
75
+ </form.AppForm>
76
+ );
77
+ }
78
+ ```
79
+
80
+ ### Available field components
81
+
82
+ All 13 field components are accessed through `field.*` inside `form.AppField`:
83
+
84
+ | Component | MUI base | Use case |
85
+ | --------------------------- | --------------------------- | ----------------------------- |
86
+ | `field.TextField` | TextField | Text input, multiline |
87
+ | `field.NumberField` | TextField (number) | Numeric input |
88
+ | `field.Autocomplete` | Autocomplete | Search/select with options |
89
+ | `field.Checkbox` | FormControlLabel + Checkbox | Boolean toggle |
90
+ | `field.Switch` | FormControlLabel + Switch | Boolean toggle (switch) |
91
+ | `field.RadioGroup` | RadioGroup | Single selection from options |
92
+ | `field.Slider` | Slider | Range/value slider |
93
+ | `field.DatePicker` | DatePicker | Date only |
94
+ | `field.DateRangePicker` | DateRangePicker | Date range |
95
+ | `field.TimePicker` | TimePicker | Time only |
96
+ | `field.TimeRangePicker` | TimeRangePicker | Time range |
97
+ | `field.DateTimePicker` | DateTimePicker | Date + time |
98
+ | `field.DateTimeRangePicker` | DateTimeRangePicker | Date + time range |
99
+
100
+ See references/field-components.md for detailed prop surfaces.
101
+
102
+ ### Edit form with existing data
103
+
104
+ ```typescript
105
+ function TodoEditForm({ todo }: { todo: Todo }) {
106
+ const form = useLayoutForm({
107
+ defaultValues: todo,
108
+ validators: {
109
+ onChange: TodoSchema,
110
+ },
111
+ onSubmit: ({ value }) => {
112
+ todoCollection.update(value);
113
+ },
114
+ });
115
+
116
+ return (
117
+ <form.AppForm>
118
+ <form.AppField
119
+ name="name"
120
+ children={(field) => <field.TextField label="Name" required />}
121
+ />
122
+ <form.SubmitButton />
123
+ </form.AppForm>
124
+ );
125
+ }
126
+ ```
127
+
128
+ ### Sub-form composition with withLayoutForm
129
+
130
+ Use `withLayoutForm` when splitting a large form into reusable sub-form components:
131
+
132
+ ```typescript
133
+ import { withLayoutForm } from "wcz-layout/hooks";
134
+ import { TodoSchema } from "~/schemas/todo";
135
+
136
+ const TodoDetailsSubForm = withLayoutForm({
137
+ defaultValues: { name: "", description: "" },
138
+ render: ({ form }) => (
139
+ <>
140
+ <form.AppField
141
+ name="name"
142
+ children={(field) => <field.TextField label="Name" required />}
143
+ />
144
+ <form.AppField
145
+ name="description"
146
+ children={(field) => <field.TextField label="Description" multiline />}
147
+ />
148
+ </>
149
+ ),
150
+ });
151
+ ```
152
+
153
+ ### Autocomplete with API data
154
+
155
+ ```typescript
156
+ <form.AppField
157
+ name="assigneeId"
158
+ children={(field) => (
159
+ <field.Autocomplete
160
+ label="Assignee"
161
+ options={users}
162
+ getOptionLabel={(user) => user.displayName}
163
+ isOptionEqualToValue={(option, value) => option.id === value.id}
164
+ />
165
+ )}
166
+ />
167
+ ```
168
+
169
+ ### Date pickers
170
+
171
+ ```typescript
172
+ <form.AppField
173
+ name="dueDate"
174
+ children={(field) => <field.DatePicker label="Due Date" />}
175
+ />
176
+
177
+ <form.AppField
178
+ name="dateRange"
179
+ children={(field) => (
180
+ <field.DateRangePicker
181
+ localeText={{ start: "Start Date", end: "End Date" }}
182
+ />
183
+ )}
184
+ />
185
+ ```
186
+
187
+ ## Common Mistakes
188
+
189
+ ### CRITICAL Using raw MUI TextField instead of form.AppField
190
+
191
+ Wrong:
192
+
193
+ ```typescript
194
+ <TextField
195
+ name="title"
196
+ value={title}
197
+ onChange={(e) => setTitle(e.target.value)}
198
+ />
199
+ ```
200
+
201
+ Correct:
202
+
203
+ ```typescript
204
+ <form.AppField
205
+ name="title"
206
+ children={(field) => <field.TextField label="Title" />}
207
+ />
208
+ ```
209
+
210
+ `useLayoutForm` pre-registers all field components. Using raw MUI inputs bypasses TanStack Form state management, validation, and error display.
211
+
212
+ Source: wcz-layout:src/hooks/FormHooks.ts
213
+
214
+ ### HIGH Passing name/value/onChange to AppField components
215
+
216
+ Wrong:
217
+
218
+ ```typescript
219
+ <form.AppField
220
+ name="title"
221
+ children={(field) => (
222
+ <field.TextField
223
+ label="Title"
224
+ name="title"
225
+ value={field.state.value}
226
+ onChange={(e) => field.handleChange(e.target.value)}
227
+ />
228
+ )}
229
+ />
230
+ ```
231
+
232
+ Correct:
233
+
234
+ ```typescript
235
+ <form.AppField
236
+ name="title"
237
+ children={(field) => <field.TextField label="Title" />}
238
+ />
239
+ ```
240
+
241
+ `FormOmittedProps` explicitly strips `name`, `value`, `onChange`, `onBlur`, `error`, `helperText`, `renderInput`, `type`, and `aria-label`. These are managed internally by `useFieldContext`. Passing them causes conflicts or is silently ignored.
242
+
243
+ Source: wcz-layout:src/lib/utils.ts
244
+
245
+ ### HIGH Not wrapping SubmitButton in form.AppForm
246
+
247
+ Wrong:
248
+
249
+ ```typescript
250
+ <div>
251
+ <form.AppField name="name" children={(field) => <field.TextField label="Name" />} />
252
+ <form.SubmitButton /> {/* Outside AppForm — crashes */}
253
+ </div>
254
+ ```
255
+
256
+ Correct:
257
+
258
+ ```typescript
259
+ <form.AppForm>
260
+ <form.AppField name="name" children={(field) => <field.TextField label="Name" />} />
261
+ <form.SubmitButton />
262
+ </form.AppForm>
263
+ ```
264
+
265
+ `SubmitButton` uses `useFormContext()` to read `canSubmit` and `isSubmitting` state. It must be rendered inside `form.AppForm`.
266
+
267
+ Source: wcz-layout:src/components/form/FormSubmitButton.tsx
268
+
269
+ ### HIGH Using useMemo or useCallback in form components
270
+
271
+ Wrong:
272
+
273
+ ```typescript
274
+ const handleSubmit = useCallback(() => form.handleSubmit(), [form]);
275
+ ```
276
+
277
+ Correct:
278
+
279
+ ```typescript
280
+ const handleSubmit = () => form.handleSubmit();
281
+ ```
282
+
283
+ React Compiler handles memoization. Manual `useMemo` / `useCallback` is forbidden per project conventions.
284
+
285
+ Source: copilot-instructions.md
286
+
287
+ Cross-skill: See also skills/ui-pages/SKILL.md § Common Mistakes
288
+
289
+ ### MEDIUM FormRadioGroup numeric values become strings
290
+
291
+ Wrong:
292
+
293
+ ```typescript
294
+ <field.RadioGroup
295
+ options={[
296
+ { label: "Low", value: 1 },
297
+ { label: "High", value: 2 },
298
+ ]}
299
+ />
300
+ // field.state.value is "1" not 1
301
+ ```
302
+
303
+ Correct:
304
+
305
+ ```typescript
306
+ <field.RadioGroup
307
+ options={[
308
+ { label: "Low", value: "1" },
309
+ { label: "High", value: "2" },
310
+ ]}
311
+ />
312
+ // Use string values consistently, or convert in onSubmit
313
+ ```
314
+
315
+ Radio group `onChange` always returns `event.target.value` as a string. Numeric option values round-trip as strings unless explicitly converted.
316
+
317
+ Source: wcz-layout:src/components/form/FormRadioGroup.tsx
318
+
319
+ ### HIGH Tension: Type safety vs. rapid prototyping
320
+
321
+ Quick forms using `useState` + manual validation bypass the enforced pattern. Always use `useLayoutForm` + Zod schema derived from Drizzle — even for simple forms. The boilerplate pays off in type safety and consistency.
322
+
323
+ See also: skills/database-schema/SKILL.md § Common Mistakes
324
+
325
+ ---
326
+
327
+ See also:
328
+
329
+ - skills/database-schema/SKILL.md — Zod schemas used as form validators.
330
+ - skills/dialogs-notifications/SKILL.md — Form submissions typically show notifications.
331
+ - skills/ui-pages/SKILL.md — Create/edit pages embed forms.
@@ -0,0 +1,212 @@
1
+ # Field Components Reference
2
+
3
+ All field components are accessed via `field.*` inside a `form.AppField` children render prop. Props listed below are the MUI props you can pass — `FormOmittedProps` (`name`, `value`, `onChange`, `onBlur`, `error`, `helperText`, `renderInput`, `type`, `aria-label`) are automatically stripped and managed by TanStack Form.
4
+
5
+ ## TextField
6
+
7
+ ```typescript
8
+ <form.AppField
9
+ name="title"
10
+ children={(field) => (
11
+ <field.TextField
12
+ label="Title"
13
+ required
14
+ multiline // optional: enables textarea
15
+ rows={3} // optional: fixed row count for multiline
16
+ placeholder="..." // optional
17
+ disabled // optional
18
+ slotProps={{ // optional: MUI slot props
19
+ input: { startAdornment: <InputAdornment position="start">$</InputAdornment> }
20
+ }}
21
+ />
22
+ )}
23
+ />
24
+ ```
25
+
26
+ ## NumberField
27
+
28
+ ```typescript
29
+ <form.AppField
30
+ name="quantity"
31
+ children={(field) => (
32
+ <field.NumberField
33
+ label="Quantity"
34
+ required
35
+ slotProps={{
36
+ input: { inputProps: { min: 0, max: 100, step: 1 } }
37
+ }}
38
+ />
39
+ )}
40
+ />
41
+ ```
42
+
43
+ ## Autocomplete
44
+
45
+ ```typescript
46
+ <form.AppField
47
+ name="assignee"
48
+ children={(field) => (
49
+ <field.Autocomplete
50
+ label="Assignee"
51
+ options={users}
52
+ getOptionLabel={(user) => user.displayName}
53
+ isOptionEqualToValue={(option, value) => option.id === value.id}
54
+ multiple // optional: multi-select
55
+ freeSolo // optional: allow custom input
56
+ loading={isLoading} // optional: show loading indicator
57
+ />
58
+ )}
59
+ />
60
+ ```
61
+
62
+ ## Checkbox
63
+
64
+ ```typescript
65
+ <form.AppField
66
+ name="isActive"
67
+ children={(field) => (
68
+ <field.Checkbox
69
+ label="Active"
70
+ disabled // optional
71
+ />
72
+ )}
73
+ />
74
+ ```
75
+
76
+ Renders `FormControlLabel` wrapping MUI `Checkbox`.
77
+
78
+ ## Switch
79
+
80
+ ```typescript
81
+ <form.AppField
82
+ name="notifications"
83
+ children={(field) => (
84
+ <field.Switch
85
+ label="Enable Notifications"
86
+ disabled // optional
87
+ />
88
+ )}
89
+ />
90
+ ```
91
+
92
+ Renders `FormControlLabel` wrapping MUI `Switch`.
93
+
94
+ ## RadioGroup
95
+
96
+ ```typescript
97
+ <form.AppField
98
+ name="priority"
99
+ children={(field) => (
100
+ <field.RadioGroup
101
+ label="Priority"
102
+ options={[
103
+ { label: "Low", value: "low" },
104
+ { label: "Medium", value: "medium" },
105
+ { label: "High", value: "high" },
106
+ ]}
107
+ row // optional: horizontal layout
108
+ />
109
+ )}
110
+ />
111
+ ```
112
+
113
+ **Warning:** `onChange` returns `event.target.value` as a string. Use string values.
114
+
115
+ ## Slider
116
+
117
+ ```typescript
118
+ <form.AppField
119
+ name="rating"
120
+ children={(field) => (
121
+ <field.Slider
122
+ min={0}
123
+ max={10}
124
+ step={1}
125
+ marks
126
+ valueLabelDisplay="auto"
127
+ />
128
+ )}
129
+ />
130
+ ```
131
+
132
+ ## DatePicker
133
+
134
+ ```typescript
135
+ <form.AppField
136
+ name="dueDate"
137
+ children={(field) => (
138
+ <field.DatePicker
139
+ label="Due Date"
140
+ minDate={dayjs()} // optional
141
+ maxDate={dayjs().add(1, "year")} // optional
142
+ />
143
+ )}
144
+ />
145
+ ```
146
+
147
+ ## DateRangePicker
148
+
149
+ ```typescript
150
+ <form.AppField
151
+ name="dateRange"
152
+ children={(field) => (
153
+ <field.DateRangePicker
154
+ localeText={{ start: "Start", end: "End" }}
155
+ />
156
+ )}
157
+ />
158
+ ```
159
+
160
+ ## TimePicker
161
+
162
+ ```typescript
163
+ <form.AppField
164
+ name="startTime"
165
+ children={(field) => (
166
+ <field.TimePicker
167
+ label="Start Time"
168
+ ampm={false} // optional: 24-hour format
169
+ />
170
+ )}
171
+ />
172
+ ```
173
+
174
+ ## TimeRangePicker
175
+
176
+ ```typescript
177
+ <form.AppField
178
+ name="timeRange"
179
+ children={(field) => (
180
+ <field.TimeRangePicker
181
+ localeText={{ start: "From", end: "To" }}
182
+ />
183
+ )}
184
+ />
185
+ ```
186
+
187
+ ## DateTimePicker
188
+
189
+ ```typescript
190
+ <form.AppField
191
+ name="scheduledAt"
192
+ children={(field) => (
193
+ <field.DateTimePicker
194
+ label="Scheduled At"
195
+ ampm={false}
196
+ />
197
+ )}
198
+ />
199
+ ```
200
+
201
+ ## DateTimeRangePicker
202
+
203
+ ```typescript
204
+ <form.AppField
205
+ name="availability"
206
+ children={(field) => (
207
+ <field.DateTimeRangePicker
208
+ localeText={{ start: "From", end: "Until" }}
209
+ />
210
+ )}
211
+ />
212
+ ```