Form dialog

The Dialog around one form. Enter in any field submits; the footer closes (Cancel, then the submit as the primary), so there is no corner close. Every dismiss path — Escape, the scrim, Cancel — funnels through one guard: a pending submit blocks them all, and a dirty form asks first through a destructive Confirm action dialog. A successful submit closes through onOpenChange(false) directly, never the guard.

<FormDialog
  open={open} onOpenChange={setOpen}
  title="Edit address" submitLabel="Save address" cancelLabel="Cancel" pendingLabel="Saving"
  isPending={mutation.isPending} dirty={form.isDirty} discardLabels={discard}
  onSubmit={(e) => { e.preventDefault(); mutation.mutate(values, { onSuccess: () => setOpen(false) }); }}
>
  <Field>…</Field>
</FormDialog>

Edit address

Change the street, then press Escape or Cancel: the discard confirm. Save: 1.5s busy submit, then the caller closes. Empty street disables the submit without blocking dismiss.