Fields, validation, the surface shells and the submit contract.
The form block is a field set over React Hook Form and Zod, plus the shells that decide where the form appears. One schema drives the client validation and the server parse, so a rule is written once.
Every field below is the shipped component, running in your browser. The SKU is validated against a pattern, the price against a minimum, and the create action is stubbed to succeed.
A form has three parts, and only the middle one is yours to write.
A shell decides where the form lives: FormPage for a route of its own, FormModal for a dialog or a sheet. It sets the mode — add, edit or view — the record being edited, and what happens when the form finishes. Nothing below it needs to know which shell it is inside.
Your form body calls useEntityForm with a Zod resolver, default values, and a create and an update function. It returns a React Hook Form instance, a submit handler and a submit-and-add-another handler. You then lay out the fields.
The fields each take the form's control and a field name. FormInput, FormTextarea, FormNumberInput, FormSelect, FormSwitch and FormDateTime cover the ordinary cases; FormCombobox and FormAsyncCombobox cover a lookup against a long or remote list; MultiSelectField and the repeater cover a list of related rows edited in place. A field renders its own label, description and error, and the error text is looked up as a copy path, so a schema can name a message instead of hard-coding English.
create and update return a result — an ok with data, or an error carrying a message and an optional code. The block does the rest: a failed create toasts the error and returns focus to the first field, a successful one toasts and calls the shell's completion handler, and add-another resets the form and mints a new id instead of leaving.
Because create is handed a generated id before it runs, a form that uploads a file or writes children can address the record it is about to create.
The same body renders read-only when the shell's mode is view — every field disables itself, and the actions disappear. There is no second component and no readOnly prop to thread.