@payfit/unity-components 2.35.4 → 2.35.5

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,349 @@
1
+ ---
2
+ name: unity-tanstack-form
3
+ description: >
4
+ Build any form in @payfit/unity-components with useTanstackUnityForm + Zod
5
+ (dynamic, translated). Layered wrapping: form.AppForm → form.Form →
6
+ form.AppField → field component. Composed API by default (TanstackTextField,
7
+ SelectField, NumberField, DatePickerField, CheckboxField, …); Atomic API
8
+ (field.Field + field.FieldLabel + field.TextInput) only when customizing
9
+ layout or parts. Schema adapters: StandardSchemaAdapter, ZodV3SchemaAdapter,
10
+ ZodV4SchemaAdapter. validators.onBlur is the default;
11
+ fieldRevalidateLogic for blur-then-change UX. React Hook Form
12
+ (useUnityForm + RHF *-field wrappers) is deprecated and will be REMOVED.
13
+ type: core
14
+ library: '@payfit/unity-components'
15
+ library_version: '2.x'
16
+ sources:
17
+ - 'PayFit/hr-apps:libs/shared/unity/components/src/hooks/use-tanstack-form.tsx'
18
+ - 'PayFit/hr-apps:libs/shared/unity/components/src/hooks/use-form.tsx'
19
+ - 'PayFit/hr-apps:libs/shared/unity/components/src/components/form/TanstackForm.tsx'
20
+ - 'PayFit/hr-apps:libs/shared/unity/components/src/components/form-field/TanstackFormField.tsx'
21
+ - 'PayFit/hr-apps:libs/shared/unity/components/src/adapters/zodAdapter.ts'
22
+ - 'PayFit/hr-apps:libs/shared/unity/components/src/utils/field-revalidate-logic.ts'
23
+ - 'PayFit/hr-apps:libs/shared/unity/components/src/docs/concepts/forms/Form Architecture Overview.mdx'
24
+ ---
25
+
26
+ Build a Unity form with `useTanstackUnityForm` + Zod. RHF (`useUnityForm`) is deprecated and will be removed.
27
+
28
+ ## Setup
29
+
30
+ ```tsx
31
+ import { Button, useTanstackUnityForm } from '@payfit/unity-components'
32
+ import { z } from 'zod'
33
+
34
+ const schema = z.object({
35
+ email: z.email({ message: 'Invalid email' }),
36
+ password: z.string().min(8, { message: 'Password too short' }),
37
+ })
38
+
39
+ export function SignInForm() {
40
+ const form = useTanstackUnityForm({
41
+ defaultValues: { email: '', password: '' },
42
+ validators: { onBlur: schema },
43
+ onSubmit: async ({ value }) => {
44
+ await signIn(value)
45
+ },
46
+ })
47
+
48
+ return (
49
+ <form.AppForm>
50
+ <form.Form className="uy:space-y-200">
51
+ <form.AppField name="email">
52
+ {field => <field.TextField label="Email" type="email" />}
53
+ </form.AppField>
54
+ <form.AppField name="password">
55
+ {field => <field.PasswordField label="Password" />}
56
+ </form.AppField>
57
+ <Button variant="primary" type="submit">
58
+ Sign in
59
+ </Button>
60
+ </form.Form>
61
+ </form.AppForm>
62
+ )
63
+ }
64
+ ```
65
+
66
+ Wrapping order is load-bearing: `form.AppForm` provides the form context, `form.Form` renders the `<form>` element and wires `handleSubmit`, `form.AppField` provides field context, and the render-prop `field` carries every bound field component (TextField, PasswordField, SelectField, …) plus the Atomic parts (Field, FieldLabel, TextInput, FieldFeedbackText, FieldHelperText, FieldRawContextualLink).
67
+
68
+ ## Core Patterns
69
+
70
+ ### Composed API (default)
71
+
72
+ Drop a single bound field component inside `<form.AppField>`. It bundles label, input, helper text, feedback, and a11y wiring.
73
+
74
+ ```tsx
75
+ <form.AppField name="firstName">
76
+ {field => (
77
+ <field.TextField
78
+ label="First name"
79
+ helperText="As it appears on your ID"
80
+ isRequired
81
+ />
82
+ )}
83
+ </form.AppField>
84
+
85
+ <form.AppField name="country">
86
+ {field => (
87
+ <field.SelectField
88
+ label="Country"
89
+ options={[
90
+ { value: 'fr', label: 'France' },
91
+ { value: 'es', label: 'Spain' },
92
+ ]}
93
+ />
94
+ )}
95
+ </form.AppField>
96
+ ```
97
+
98
+ ### Atomic API (only when customizing layout/parts)
99
+
100
+ Reach for Atomic when you need to interleave custom content between the label and the input, or swap an input for a non-standard control. Wraps every part in `field.Field`.
101
+
102
+ ```tsx
103
+ <form.AppField name="password">
104
+ {field => (
105
+ <field.Field>
106
+ <field.FieldLabel isRequired>Password</field.FieldLabel>
107
+ <field.FieldHelperText>Enter a strong password</field.FieldHelperText>
108
+ <field.TextInput type="password" />
109
+ <form.Subscribe selector={s => s.values.password}>
110
+ {password => (
111
+ <Text variant="bodySmallStrong">Length: {password.length}</Text>
112
+ )}
113
+ </form.Subscribe>
114
+ <field.FieldFeedbackText />
115
+ </field.Field>
116
+ )}
117
+ </form.AppField>
118
+ ```
119
+
120
+ ### Validation timing
121
+
122
+ `validators.onBlur` is the default; use `onChange` only for fields that need live feedback (password strength meter, search-as-you-type). `fieldRevalidateLogic` gives "blur until first error, then change" UX without polluting form-level validators.
123
+
124
+ ```tsx
125
+ import { fieldRevalidateLogic, useTanstackUnityForm } from '@payfit/unity-components'
126
+
127
+ const form = useTanstackUnityForm({
128
+ defaultValues: { email: '', password: '' },
129
+ validators: { onBlur: z.object({ email: z.email() }) },
130
+ validationLogic: fieldRevalidateLogic({
131
+ whenPristine: 'blur',
132
+ whenDirty: 'change',
133
+ fields: ['password'],
134
+ }),
135
+ })
136
+
137
+ <form.AppField
138
+ name="password"
139
+ validators={{
140
+ onDynamic: ({ value }) =>
141
+ value.length < 8 ? 'Password too short' : undefined,
142
+ }}
143
+ >
144
+ {field => <field.PasswordField label="Password" />}
145
+ </form.AppField>
146
+ ```
147
+
148
+ Fields listed in `fieldRevalidateLogic.fields` MUST use `onDynamic`/`onDynamicAsync` as their sole validator and MUST NOT also appear in a form-level schema, or stale errors will linger.
149
+
150
+ ### Optimal subscription with form.Subscribe + selector
151
+
152
+ Always pass a `selector` to `form.Subscribe`. A bare children-only subscription re-renders on every keystroke anywhere in the form.
153
+
154
+ ```tsx
155
+ <form.Subscribe selector={s => s.values.password}>
156
+ {password => <Text>Length: {password.length}</Text>}
157
+ </form.Subscribe>
158
+
159
+ <form.Subscribe selector={s => [s.canSubmit, s.isSubmitting] as const}>
160
+ {([canSubmit, isSubmitting]) => (
161
+ <Button type="submit" isDisabled={!canSubmit} isLoading={isSubmitting}>
162
+ Submit
163
+ </Button>
164
+ )}
165
+ </form.Subscribe>
166
+ ```
167
+
168
+ ## Common Mistakes
169
+
170
+ ### CRITICAL Import useForm (legacy RHF) instead of useTanstackUnityForm
171
+
172
+ Wrong:
173
+
174
+ ```tsx
175
+ import { useUnityForm } from '@payfit/unity-components'
176
+
177
+ const { methods, Form, FormField } = useUnityForm(schema)
178
+ ```
179
+
180
+ Correct:
181
+
182
+ ```tsx
183
+ import { useTanstackUnityForm } from '@payfit/unity-components'
184
+
185
+ const form = useTanstackUnityForm({ validators: { onBlur: schema } })
186
+ ```
187
+
188
+ The legacy `use-form` hook is `@deprecated` but still exported; agents trained on older code reach for it and end up mixing RHF Controller with Tanstack field components, which breaks at runtime.
189
+
190
+ Fixed-but-legacy-risk: the legacy hook is still exported but will be removed in the next few weeks (after or alongside the rebrand). Never author new code with it.
191
+
192
+ Source: libs/shared/unity/components/src/hooks/use-form.tsx:79 (@deprecated JSDoc); maintainer interview
193
+
194
+ ### CRITICAL Omit form.AppForm or form.AppField wrapping
195
+
196
+ Wrong:
197
+
198
+ ```tsx
199
+ <form.Form>
200
+ <form.AppField name="email">
201
+ {field => <field.TextField label="Email" />}
202
+ </form.AppField>
203
+ </form.Form>
204
+ ```
205
+
206
+ Correct:
207
+
208
+ ```tsx
209
+ <form.AppForm>
210
+ <form.Form>
211
+ <form.AppField name="email">
212
+ {field => <field.TextField label="Email" />}
213
+ </form.AppField>
214
+ </form.Form>
215
+ </form.AppForm>
216
+ ```
217
+
218
+ `useFormContext()` and `useFieldContext()` throw without their providers; Tanstack field components silently break (cannot read property of undefined).
219
+
220
+ Source: TanstackForm.tsx:36; TanstackFormField.tsx:63 (useFormContext/useFieldContext)
221
+
222
+ ### CRITICAL Mix Tanstack field with react-hook-form Controller
223
+
224
+ Wrong:
225
+
226
+ ```tsx
227
+ const { control } = useForm()
228
+ <Controller control={control} name="email" render={() => (
229
+ <TanstackTextField label="Email" />
230
+ )} />
231
+ ```
232
+
233
+ Correct:
234
+
235
+ ```tsx
236
+ const form = useTanstackUnityForm({ validators: { onBlur: schema } })
237
+ <form.AppForm><form.AppField name="email">
238
+ {field => <field.TextField label="Email" />}
239
+ </form.AppField></form.AppForm>
240
+ ```
241
+
242
+ RHF and Tanstack Form contexts do not interoperate; wrapping a Tanstack field in a Controller produces unmounted state and no validation.
243
+
244
+ Source: conceptual; RHF and Tanstack contexts do not interoperate
245
+
246
+ ### HIGH Subscribe to whole form state instead of a selector
247
+
248
+ Wrong:
249
+
250
+ ```tsx
251
+ <form.Subscribe>
252
+ {state => <div>Length: {state.values.password.length}</div>}
253
+ </form.Subscribe>
254
+ ```
255
+
256
+ Correct:
257
+
258
+ ```tsx
259
+ <form.Subscribe selector={s => s.values.password}>
260
+ {password => <div>Length: {password.length}</div>}
261
+ </form.Subscribe>
262
+ ```
263
+
264
+ A selector-less subscription re-renders the children on every keystroke anywhere in the form; pass a narrowing selector to scope to the slice you need.
265
+
266
+ Source: use-tanstack-form.stories.tsx:251 (StateIntegration story)
267
+
268
+ ### MEDIUM Pick the wrong validation timing
269
+
270
+ Wrong:
271
+
272
+ ```tsx
273
+ useTanstackUnityForm({ validators: { onChange: schema } })
274
+ ```
275
+
276
+ Correct:
277
+
278
+ ```tsx
279
+ useTanstackUnityForm({ validators: { onBlur: schema } })
280
+ // or with revalidation:
281
+ // validationLogic: fieldRevalidateLogic({ fields: ['password'], whenDirty: 'change' })
282
+ ```
283
+
284
+ `onChange` fires on every keystroke (jarring) and `onSubmit` waits until submit (errors arrive too late); `onBlur` is the usual default, with `fieldRevalidateLogic` reserved for "blur until first error, then change".
285
+
286
+ Source: use-tanstack-form.stories.tsx:37-40; utils/field-revalidate-logic.ts
287
+
288
+ ### HIGH Mix Composed and Atomic APIs in one field (or reach for Atomic by default)
289
+
290
+ Wrong:
291
+
292
+ ```tsx
293
+ // Double-wrapping:
294
+ <form.AppField name="email">
295
+ {field => (
296
+ <field.Field>
297
+ <field.FieldLabel>Email</field.FieldLabel>
298
+ <field.TextField label="Email" />
299
+ </field.Field>
300
+ )}
301
+ </form.AppField>
302
+ // Or reaching for Atomic with no customization reason:
303
+ <form.AppField name="name">
304
+ {field => (
305
+ <field.Field>
306
+ <field.FieldLabel>Name</field.FieldLabel>
307
+ <field.TextInput />
308
+ <field.FieldFeedbackText />
309
+ </field.Field>
310
+ )}
311
+ </form.AppField>
312
+ ```
313
+
314
+ Correct:
315
+
316
+ ```tsx
317
+ // Default (Composed):
318
+ <form.AppField name="name">
319
+ {field => <field.TextField label="Name" />}
320
+ </form.AppField>
321
+ // Atomic only when customizing layout/parts:
322
+ <form.AppField name="email">
323
+ {field => (
324
+ <field.Field>
325
+ <field.FieldLabel>Email</field.FieldLabel>
326
+ <CustomInline>
327
+ <field.TextInput />
328
+ <ExtraSlot />
329
+ </CustomInline>
330
+ <field.FieldFeedbackText />
331
+ </field.Field>
332
+ )}
333
+ </form.AppField>
334
+ ```
335
+
336
+ `field.TextField` is the Composed API and already includes label/input/feedback; wrapping it in `field.Field` + `field.FieldLabel` double-wraps and breaks layout + a11y. Default to Composed; reach for Atomic only when you must customize the field's layout or swap a part — never as the standard pattern.
337
+
338
+ Source: TanstackTextField.tsx vs TanstackFormField.tsx + parts; maintainer interview (Composed is default)
339
+
340
+ ## References
341
+
342
+ - [Bound field components](references/bound-field-components.md) — full inventory of `field.*` Composed components and their underlying base components.
343
+ - [Schema adapters](references/schema-adapters.md) — StandardSchemaAdapter, ZodV3SchemaAdapter, ZodV4SchemaAdapter and how `isFieldRequired` consumes them.
344
+
345
+ ## See also
346
+
347
+ - `unity-migrate-from-midnight` — forms migrated off Midnight typically came with React Hook Form; that skill explains the Tanstack-only replacement path.
348
+ - `unity-layout-and-styling` — form layouts use Flex/Grid and `uy:*` utilities for spacing and responsive behavior.
349
+ - `unity-navigation` — when a form posts via a route action or links to a sibling step, use the router-aware `Link` from `@payfit/unity-components/integrations/tanstack-router`.
@@ -0,0 +1,67 @@
1
+ # Bound field components (Composed API)
2
+
3
+ All 15 Tanstack-bound Composed field components, registered in `src/hooks/use-tanstack-form.tsx` via `createFormHook({ fieldComponents })`. They are exposed on the render-prop `field` object inside `<form.AppField>` WITHOUT the `Tanstack` prefix (e.g. the registered `TanstackTextField` is `field.TextField`).
4
+
5
+ The form-usage pattern is identical for every component:
6
+
7
+ ```tsx
8
+ <form.AppField name="fieldPath">
9
+ {field => <field.<Name> {...props} />}
10
+ </form.AppField>
11
+ ```
12
+
13
+ ## Inventory
14
+
15
+ | Registered name (in `useTanstackUnityForm`) | Source component (`Tanstack*`) | Wraps (non-bound base) | Notes |
16
+ | ------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `field.TextField` | `TanstackTextField` | `TextField` → `TanstackInput` / `TanstackTextArea` | `multiline` switches input/textarea. `type` (`text`/`password`/`email`/`tel`/`url`/`search`) only valid when `multiline !== true`. |
18
+ | `field.SelectField` | `TanstackSelectField` | `SelectField` → `TanstackSelect` | Single-select with `options: { value, label }[]`. |
19
+ | `field.MultiSelectField` | `TanstackMultiSelectField` | `MultiSelectField` → `TanstackMultiSelect` | Generic component; chips for selected items. |
20
+ | `field.NumberField` | `TanstackNumberField` | `NumberField` → `TanstackNumberInput` | Locale-aware number formatting. |
21
+ | `field.CheckboxField` | `TanstackCheckboxField` | `CheckboxField` → `TanstackCheckbox` | Single checkbox + label + feedback. |
22
+ | `field.CheckboxGroupField` | `TanstackCheckGroupField` | `CheckGroupField` → `TanstackCheckboxGroup` | Multi-checkbox group; value is `string[]`. Note the registered key is `CheckboxGroupField` even though the implementation is named `TanstackCheckGroupField`. |
23
+ | `field.RadioButtonGroupField` | `TanstackRadioButtonGroupField` | `RadioButtonGroupField` → `TanstackRadioButtonGroup` | Single-select radio group. |
24
+ | `field.SelectableButtonGroupField` | `TanstackSelectableButtonGroupField` | `SelectableButtonGroupField` → `TanstackSelectableButtonGroup` | Pill-style segmented control. |
25
+ | `field.SelectableCardCheckboxGroupField` | `TanstackSelectableCardCheckboxGroupField` | `SelectableCardCheckboxGroupField` → `TanstackSelectableCardCheckboxGroup` | Multi-select card grid. |
26
+ | `field.SelectableCardRadioGroupField` | `TanstackSelectableCardRadioGroupField` | `SelectableCardRadioGroupField` → `TanstackSelectableCardRadioGroup` | Single-select card grid. |
27
+ | `field.DatePickerField` | `TanstackDatePickerField` | `DatePickerField` → `TanstackDatePicker` | Single-date popover picker. |
28
+ | `field.DateRangePickerField` | `TanstackDateRangePickerField` | `DateRangePickerField` → `TanstackDateRangePicker` | Start/end range picker. |
29
+ | `field.ToggleSwitchField` | `TanstackToggleSwitchField` | `ToggleSwitchField` → `TanstackToggleSwitch` | Single boolean switch. |
30
+ | `field.ToggleSwitchGroupField` | `TanstackToggleSwitchGroupField` | `ToggleSwitchGroupField` → `TanstackToggleSwitchGroup` | Group of switches; value is a record. |
31
+ | `field.PasswordField` | `TanstackPasswordField` (registered as `PasswordField`) | `PasswordField` → `TanstackInput` (`type="password"`) | Adds visibility toggle button. The registered key is `PasswordField` (no `Tanstack` prefix in the source) — so usage is `field.PasswordField`. Prefer this over `<field.TextField type="password" />` for the visibility toggle. |
32
+ | `field.PhoneNumberField` | `TanstackPhoneNumberField` | `PhoneNumberField` → `TanstackPhoneNumberInput` | E.164-compatible phone input. |
33
+
34
+ ## Atomic parts (always available on `field`)
35
+
36
+ These are the Atomic API parts registered alongside the Composed components. Use them only when customizing layout:
37
+
38
+ - `field.Field` — the wrapper (`TanstackFormField`). Provides a11y context.
39
+ - `field.FieldLabel` — label slot.
40
+ - `field.FieldHelperText` — helper text slot.
41
+ - `field.FieldFeedbackText` — validation error slot.
42
+ - `field.FieldRawContextualLink` — contextual link slot.
43
+
44
+ And the raw inputs (use Composed instead unless customizing):
45
+
46
+ - `field.TextInput` / `field.TextAreaInput`
47
+ - `field.CheckboxInput` / `field.CheckboxGroupInput`
48
+ - `field.NumberInput`
49
+ - `field.SelectInput` / `field.MultiSelectInput`
50
+ - `field.RadioButtonInput`
51
+ - `field.SelectableButtonGroupInput`
52
+ - `field.SelectableCardCheckboxGroupInput` / `field.SelectableCardRadioGroupInput`
53
+ - `field.DatePickerInput` / `field.DateRangePickerInput`
54
+ - `field.ToggleSwitchInput` / `field.ToggleSwitchGroupInput`
55
+ - `field.PhoneNumberInput`
56
+
57
+ ## Form-scoped components
58
+
59
+ Available on `form` (not `field`):
60
+
61
+ - `<form.AppForm>` — form-context provider. Required outer wrapper.
62
+ - `<form.Form>` — `<form>` element wired to `handleSubmit`.
63
+ - `<form.AppField name="…">` — field-context provider; render-prop exposes `field`.
64
+ - `<form.Subscribe selector={…}>` — selector-scoped reactive subscription. Always pass `selector`.
65
+ - `<form.InlineFieldGroup>`, `<form.InlineFieldGroupHeader>`, `<form.InlineFieldGroupReadView>`, `<form.InlineFieldGroupEditView>` — inline edition layout (read view ↔ edit view toggling for grouped fields).
66
+
67
+ For the mapping source of truth see `src/hooks/use-tanstack-form.tsx` (the `createFormHook` call). The keys in `fieldComponents` and `formComponents` are exactly the names exposed on `field` / `form`.
@@ -0,0 +1,108 @@
1
+ # Schema adapters
2
+
3
+ Schema adapters give the form-field organisms a uniform way to ask "is this path required?" across schemas authored in Zod 3, Zod 4, or any Standard Schema v1 implementation. Source: `src/adapters/`.
4
+
5
+ ## Common interface
6
+
7
+ ```ts
8
+ // src/types/schema.ts
9
+ export interface StandardSchemaField {
10
+ isOptional: boolean
11
+ type: string
12
+ shape?: Record<string, StandardSchemaField>
13
+ }
14
+
15
+ export interface StandardSchema {
16
+ getField(path: string): StandardSchemaField | null
17
+ }
18
+ ```
19
+
20
+ `getField('preferences.marketing')` returns `null` if the path does not exist, otherwise `{ isOptional, type, shape }`. `shape` is only populated when the field resolves to a nested `ZodObject` (so callers can recurse into nested forms).
21
+
22
+ ## Adapters
23
+
24
+ ### ZodV3SchemaAdapter
25
+
26
+ Signature:
27
+
28
+ ```ts
29
+ new ZodV3SchemaAdapter(schema: z3.ZodObject<z3.ZodRawShape>)
30
+ ```
31
+
32
+ - Reads structure from `schema.shape` and `field._def.typeName`.
33
+ - Detects optional with `field instanceof z3.ZodOptional`; unwraps via `field._def.innerType`.
34
+ - Source: `src/adapters/zodAdapter.ts` (lines 6–60).
35
+
36
+ ### ZodV4SchemaAdapter
37
+
38
+ Signature:
39
+
40
+ ```ts
41
+ new ZodV4SchemaAdapter(schema: z4.ZodObject<z4.ZodRawShape>)
42
+ ```
43
+
44
+ - Same shape traversal as v3, but uses `field.def.innerType` and `field.def.typeName` (no underscore — Zod 4 renamed `_def` to `def`).
45
+ - `field instanceof z4.ZodOptional` for optionality detection.
46
+ - Source: `src/adapters/zodAdapter.ts` (lines 62–117).
47
+
48
+ ### StandardSchemaAdapter
49
+
50
+ Signature:
51
+
52
+ ```ts
53
+ new StandardSchemaAdapter(standardSchema: StandardSchemaV1)
54
+ ```
55
+
56
+ - Stub implementation: `getField()` returns `{ isOptional: false, type: 'unknown', shape: undefined }` for any non-null path. Standard Schema's spec does not expose enough internal structure for richer introspection.
57
+ - Use this only for schemas that aren't Zod (e.g. Valibot, ArkType) — required-field inference will degrade to "always required".
58
+ - Source: `src/adapters/standardSchemaAdapter.ts`.
59
+
60
+ ## How adapters auto-select
61
+
62
+ `createSchemaAdapter(schema)` (in `src/utils/createSchemaAdapter.ts`) picks an adapter from the schema's structural fingerprint:
63
+
64
+ ```ts
65
+ import { createSchemaAdapter } from '@payfit/unity-components'
66
+
67
+ const adapter = createSchemaAdapter(schema)
68
+ // schema._def && 'def' in schema → ZodV4SchemaAdapter
69
+ // schema._def && !('def' in schema) → ZodV3SchemaAdapter
70
+ // schema['~validate'] is a function → StandardSchemaAdapter
71
+ // otherwise → null
72
+ ```
73
+
74
+ You rarely call this directly: every Composed field organism (e.g. `TanstackTextField`, `TanstackCheckboxField`, `TanstackToggleSwitchGroupField`) calls `createSchemaAdapter` + `isFieldRequired` internally to render the required indicator on the label.
75
+
76
+ ## isFieldRequired
77
+
78
+ Consumer of the adapter, used inside every Composed field to decide whether to mark the label as required.
79
+
80
+ ```ts
81
+ // src/components/form-field/utils/isFieldRequired.ts
82
+ export function isFieldRequired(
83
+ schema: StandardSchema | null | undefined,
84
+ fieldPath: string,
85
+ ): boolean {
86
+ if (!schema) return false
87
+ const field = schema.getField(fieldPath)
88
+ return field ? !field.isOptional : false
89
+ }
90
+ ```
91
+
92
+ Behavior:
93
+
94
+ - No schema → `false` (no required indicator).
95
+ - Path not found in schema → `false` (treat as not required rather than crashing).
96
+ - Field is `ZodOptional` → `false`.
97
+ - Field is anything else → `true`.
98
+
99
+ This is why `<form.AppField name="email">{field => <field.TextField label="Email" />}</form.AppField>` automatically gets a required asterisk when `email` is `z.email()` and no asterisk when it's `z.email().optional()` — without any prop wiring.
100
+
101
+ ## When to import an adapter explicitly
102
+
103
+ Almost never. The Composed field components call `createSchemaAdapter(schema)` themselves. Import an adapter directly only when:
104
+
105
+ - You are writing a custom field component outside the Composed inventory and need required-state inference.
106
+ - You are introspecting a schema for non-form purposes (form-derived UI, dynamic field rendering).
107
+
108
+ Even then, prefer `createSchemaAdapter(schema)` so the right version is picked at runtime.