Create or modify forms in admin or vendor dashboards in the Mercur basic starter using stable form structure, validation, submission guards, and dashboard UI conventions.
Use this skill when:
Applies equally to admin and vendor dashboards.
Prefer the existing Mercur form pattern already present in the screen or package. Do not introduce a parallel form system just because it is locally convenient.
Before introducing custom field wrappers, selectors, overlays, or other interactive form UI, apply medusa-ui-conformance.
Forms use react-hook-form with zod validation and components from @mercurjs/dashboard-shared and @medusajs/ui.
A create/edit form page is a RouteFocusModal drawer route:
// The page default export wraps RouteFocusModal (provides context)
const CreatePage = () => (
<RouteFocusModal>
<CreateForm />
</RouteFocusModal>
);
export default CreatePage;
// The form component uses RouteFocusModal.Form, .Header, .Body, .Footer
const CreateForm = () => {
const { t } = useTranslation();
const { handleSuccess } = useRouteModal();
const form = useForm({ defaultValues: { ... }, resolver: zodResolver(schema) });
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, {
onSuccess: () => { toast.success("Created"); handleSuccess("/route"); },
onError: (error) => toast.error(error.message),
});
});
return (
<RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit} className="flex h-full flex-col overflow-hidden">
<RouteFocusModal.Header />
<RouteFocusModal.Body className="flex size-full flex-col items-center p-16">
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
{/* Heading, fields */}
</div>
</RouteFocusModal.Body>
<RouteFocusModal.Footer>
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">{t("actions.cancel")}</Button>
</RouteFocusModal.Close>
<Button size="small" variant="primary" type="submit" isLoading={isPending}>
{t("actions.create")}
</Button>
</RouteFocusModal.Footer>
</form>
</RouteFocusModal.Form>
);
};
Full-screen modal overlay. Used for creating new entities.
RouteFocusModal (provides context), form uses .Form, .Header, .Body, .Footer, .CloseuseRouteModal() must be called inside the providerTabbedForm, do NOT use RouteFocusModal.Form — TabbedForm renders it internally. See dashboard-tab-ui.Side panel from the right. Used for editing existing entities.
RouteDrawer, form uses .Form, .Header, .Title, .Description, .Body, .Footer, .Close@ prefix directory (e.g. [id]/@edit/page.tsx) to create a parallel route. Without @, the detail page unmounts. See dashboard-page-ui for full pattern.RouteFocusModal / RouteDrawer — wraps the page, provides modal provider context.Form — wraps the form, connects react-hook-form.Header — modal/drawer header (RouteFocusModal has close button built in; RouteDrawer uses .Title and .Description).Body — scrollable content area.Footer — sticky footer with actions.Close — closes the modal/drawer (use with asChild on Cancel button)Form.Field / Form.Item / Form.Label / Form.Control / Form.ErrorMessage — form field compositionForm.Label optional — marks a field as optional in the UIuseRouteModal() outside a RouteFocusModal provider.zod + zodResolver) when the form is non-trivial.dashboard-page-ui.dashboard-tab-ui.