Forms
Form
Form fields wired to react-hook-form: each field gets a label, a control, an optional hint and an error message, with ids and ARIA linked for you.
Guidelines#
Use it for any settings or creation form in Crew OS (campaign, mailbox, agent settings), with validation from a zod resolver or setError from the server.
What you provide: const form = useForm({ defaultValues }) (DC.useForm in this bundle), then <Form {...form}> (it is FormProvider) around a <form onSubmit={form.handleSubmit(...)}>. Per field: FormField with control, name and render={({ field }) => …} returning FormItem › FormLabel, FormControl wrapping exactly one input (spread field into it), FormDescription, FormMessage.
Anatomy: FormItem is a grid with 8px gaps. FormLabel turns text-destructive when the field has an error (data-error). FormControl is a Slot: it sets the input's id, aria-invalid and aria-describedby (the description, plus the message when invalid), so inputs draw their own invalid ring. FormDescription is text-sm text-muted-foreground; FormMessage is text-sm text-destructive, shows the error's message (or its own children) and renders nothing when empty.
Don't put two controls in one FormControl, repeat the error in a toast, or hide the description when an error shows (both are announced).
Facts#
| Group | Forms |
|---|---|
| Product | Crew OS |
| Global | window.DigitalCrew.Form |
| Source | digitalcrew-orchestrator: components/ui/form.tsx |
| Import in the app | import { Form } from "@/components/ui/form"; |
API#
TypeScript declarations emitted from components/ui/form.tsx (the module also exports Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField, useFormField).
declare const Form: <TFieldValues extends FieldValues, TContext = any, TTransformedValues = TFieldValues>(props: FormProviderProps<TFieldValues, TContext, TTransformedValues>) => React.JSX.Element;
declare const FormField: <TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>({ ...props }: ControllerProps<TFieldValues, TName>) => any;
declare const useFormField: () => {
invalid: boolean;
isDirty: boolean;
isTouched: boolean;
isValidating: boolean;
error?: FieldError;
id: any;
name: any;
formItemId: string;
formDescriptionId: string;
formMessageId: string;
};
declare function FormItem({ className, ...props }: React.ComponentProps<"div">): any;
declare function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>): any;
declare function FormControl({ ...props }: React.ComponentProps<typeof Slot>): any;
declare function FormDescription({ className, ...props }: React.ComponentProps<"p">): any;
declare function FormMessage({ className, ...props }: React.ComponentProps<"p">): any;
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField, };Example#
The code of the preview above: plain React.createElement against window.DigitalCrew, with realistic data.
<div id="root" class="p-6"></div>
<script>
(function () {
var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
function App() {
var form = DC.useForm({ defaultValues: { name: "Q3 CFO outreach", sender: "claire@northwind-mail.io" } });
React.useEffect(function () {
form.setError("sender", { type: "manual", message: "Use a mailbox on a verified sending domain." });
}, []);
function field(name, label, hint, type) {
return h(DC.FormField, { control: form.control, name: name, render: function (p) {
return h(DC.FormItem, null,
h(DC.FormLabel, null, label),
h(DC.FormControl, null, h(DC.Input, Object.assign({ type: type || "text" }, p.field))),
h(DC.FormDescription, null, hint),
h(DC.FormMessage));
} });
}
return h(DC.Form, form,
h("form", { className: "grid max-w-md gap-5", onSubmit: form.handleSubmit(function () {}) },
field("name", "Campaign name", "Only your team sees this name."),
field("sender", "Sender mailbox", "Replies land in this inbox.", "email"),
h("div", null, h(DC.Button, { type: "submit" }, "Save campaign"))));
}
ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>