# Forms — `@sonordev/site-kit/forms`

Sonor-managed forms with multi-step support, conditional logic, validation, anti-bot protection, and automatic CRM routing.

## Usage

### Option 1: Managed (simplest)

```tsx
import { ManagedForm } from '@sonordev/site-kit/forms'

export default function ContactPage() {
  return <ManagedForm formId="contact-form" />
}
```

Fetches form config from Sonor, renders fields, handles submission, routes to CRM.

## Experiences (4.0)

Since 4.0 that one line gets you the **spotlight** experience by default: the
familiar layout, alive. A glowing ring visits the field you're in, completed
fields earn a check, and each finished row collapses into a sentence the form
says back — "Nice to meet you, Jordan." / "We'll follow up at jordan@…" — with
an edit control to reopen it.

| Experience  | What it is                                                                                | How to get it          |
| ----------- | ----------------------------------------------------------------------------------------- | ---------------------- |
| `spotlight` | **Default.** Fields perform in place; answered rows become sentences                      | nothing to do          |
| `stage`     | Accordion: one beat at a time on a single surface, answered beats settle into filled rows | `experience="stage"`   |
| `classic`   | The plain grid form, no choreography                                                      | `experience="classic"` |

```tsx
<ManagedForm formId="contact-form" />                      // spotlight
<ManagedForm formId="contact-form" experience="stage" />   // accordion
<ManagedForm formId="contact-form" experience="classic" /> // opt out
```

Sonor can decide instead of the site, with no deploy on either side — set the
form's `layout` to `classic`, `stage`, or `spotlight`. An explicit
`experience` prop always wins over the config.

Three things hold for every experience:

- **The lead is always capturable.** SSR and no-JS render the plain classic
  form; the experience layers on after hydration. If its chunk never arrives,
  the form still works and still submits.
- **Nobody pays for what they don't use.** Experience code is loaded on
  demand, so a `classic` form downloads none of it.
- **Same engine.** Identical validation, spam protection and CRM routing.
  An experience is a rendering, not a fork.

### Editing what the form says back

The sentence for a row comes from the kit's built-in wording unless you write
your own in Sonor: set **`completion_message`** on a field (the first answered
field in the row that has one wins). Tokens:

| Token              | Resolves to                          |
| ------------------ | ------------------------------------ |
| `{value}`          | this field's answer                  |
| `{label}`          | this field's label                   |
| `{first_name}`     | the form's first-name answer, if any |
| `{any_field_slug}` | that field's answer                  |

```
Welcome aboard, {first_name}. We'll reach you at {email}.
```

Leave it empty to keep the built-in sentence. Unknown or unanswered tokens
render as nothing — never raw braces.

### Option 2: Headless Hook (full UI control)

```tsx
'use client'
import { useForm } from '@sonordev/site-kit/forms'

export function ContactForm() {
  const {
    fields, values, errors, setFieldValue, handleSubmit, toolAttributes, isSubmitting,
    step, totalSteps, isMultiStep, nextStep, prevStep, isLastStep,
  } = useForm('contact-form')

  return (
    <form {...toolAttributes} onSubmit={handleSubmit}>
      {fields.map(field => (
        <div key={field.slug}>
          <label>{field.label}</label>
          <input
            value={String(values[field.slug] || '')}
            onChange={(e) => setFieldValue(field.slug, e.target.value)}
          />
          {errors[field.slug] && <span className="error">{errors[field.slug]}</span>}
        </div>
      ))}
      {isMultiStep && !isLastStep && <button type="button" onClick={nextStep}>Next</button>}
      {isLastStep && <button type="submit" disabled={isSubmitting}>Submit</button>}
    </form>
  )
}
```

### Option 3: Render Prop

```tsx
<ManagedForm formId="contact-form">
  {({ fields, values, setFieldValue, submit }) => (
    <MyCustomFormUI fields={fields} values={values} onChange={setFieldValue} onSubmit={submit} />
  )}
</ManagedForm>
```

### Option 4: Programmatic API

```ts
import { formsApi, field, configureFormsApi } from '@sonordev/site-kit/forms'

configureFormsApi({ baseUrl: 'https://api.sonor.io', apiKey: 'sonor_...' })

const form = await formsApi.create({
  projectId: 'xxx',
  slug: 'newsletter',
  name: 'Newsletter Signup',
  formType: 'newsletter',
  fields: [
    field.email('email', 'Email', { isRequired: true }),
    field.text('name', 'Name'),
  ],
})
```

## ManagedForm Props

```ts
interface ManagedFormProps {
  formId: string                              // Form slug or ID
  className?: string
  onSuccess?: (submission: FormSubmitResult) => void
  onError?: (error: Error) => void
  children?: (renderProps: UseFormReturn) => ReactNode  // Render prop override
}
```

## useForm Return

```ts
interface UseFormReturn {
  form: ManagedFormConfig | null
  isLoading: boolean
  fetchError: Error | null
  allFields: FormField[]
  fields: FormField[]               // Current step's fields
  visibleFields: FormField[]         // After conditional logic
  values: Record<string, unknown>
  errors: Record<string, string>
  setFieldValue: (key: string, value: unknown) => void
  step: number
  totalSteps: number
  isMultiStep: boolean
  progress: number                   // 0-100
  nextStep: () => void
  prevStep: () => void
  goToStep: (n: number) => void
  canGoNext: boolean
  canGoPrev: boolean
  isLastStep: boolean
  validate: () => boolean
  // Resolves with what happened: { status: 'sent' | 'invalid' | 'failed' | 'busy' | 'next_step', ... }
  submit: (options?: FormSubmitOptions) => Promise<FormSubmitOutcome>
  // A ready-made <form onSubmit>: steps or submits, and answers an AI agent (see below)
  handleSubmit: (event: FormEvent<HTMLFormElement>) => void
  // WebMCP attributes for your <form>; null until the config loads
  toolAttributes: { toolname: string; tooldescription: string } | null
  isSubmitting: boolean
  isComplete: boolean
  reset: () => void
}
```

## Field Types

`text`, `email`, `phone`, `tel`, `number`, `textarea`, `select`, `multi-select`, `checkbox`, `radio`, `date`, `time`, `datetime`, `file`, `signature`, `rating`, `slider`, `hidden`, `heading`, `section_header`, `paragraph`

## Field Builder

```ts
field.text(slug, label, options?)
field.email(slug, label, options?)
field.phone(slug, label, options?)
field.textarea(slug, label, options?)
field.select(slug, label, { options: [{ value, label }] })
field.date(slug, label, options?)
field.checkbox(slug, label, options?)
field.rating(slug, label, options?)
```

## Form Routing

Submissions auto-route based on `form_type`:

| Form Type                               | Routes To             | Use Case                |
| --------------------------------------- | --------------------- | ----------------------- |
| `prospect` / `lead-capture` / `contact` | CRM Contacts          | Sales inquiries, quotes |
| `support`                               | Support Tickets       | Help requests           |
| `feedback`                              | Feedback entries      | User feedback           |
| `newsletter`                            | Email Subscribers     | Newsletter signups      |
| `custom`                                | Form Submissions only | Custom handling         |

## Spam protection

Sonor screens every submission on its side, and there's nothing to configure.
It only accepts a submission with evidence that a real browser rendered the
page, which `<ManagedForm>` and `useForm` send automatically. A hand-rolled
`fetch` to the forms API, or a proxy through your own API route, can't send
it, so Sonor refuses those before anything is written: the visitor sees an
error and you get no lead. Submit through site-kit, and define the form's
fields in Sonor so both can render and validate them.

## Agent-ready forms (7.2.0)

People increasingly ask an AI assistant to fill out a form for them: a quote
request, a booking, a newsletter signup. Assistants act on what the page's
markup says each control is, so managed forms now say it clearly. There's
nothing to configure.

- **Autocomplete tokens.** Name, email, phone, company, website and address
  fields carry the right `autocomplete` token (`given-name`, `email`, `tel`,
  `organization`, `url`, `postal-code`, ...), read from the field's CRM
  destination in Sonor, then its type, slug and label. Browser autofill and
  password managers use the same tokens.
- **Accessible wiring.** Error and help text are tied to their control
  (`aria-describedby`), an errored control says so (`aria-invalid`), radio and
  checkbox groups are named by their question, and rating stars by their
  value.
- **WebMCP.** Every interactive managed form carries the declarative WebMCP
  attributes (`toolname`, `tooldescription`), so a browser agent that supports
  WebMCP can treat it as a tool. It fills the same fields a person would, and
  the submit runs the same validation. There's no `toolautosubmit`: the
  assistant fills the form and the person it's helping presses Send.
  Browsers without WebMCP ignore the attributes.
- **Agent-sent leads are tagged.** When the browser reports that an agent
  pressed submit (`SubmitEvent.agentInvoked`), the submission carries that
  flag and the lead is tagged in Sonor, so you can see how those leads compare
  over time. It's the browser's word, so it never changes how a submission is
  checked. The agent is handed the outcome of its submit (`respondWith`).
- **Nothing typed before the form loads is lost.** `ServerForm` renders the
  form in the page HTML and loads the interactive version at idle. Since
  7.2.0, typing into the server-rendered form starts that upgrade at once and
  what was typed carries over, and a Send pressed before the upgrade is held
  and sent the moment it lands. The server-rendered Send button stays disabled
  until the page can catch the click.

With `useForm`, spread `toolAttributes` on your `<form>` and use
`handleSubmit` as its `onSubmit` to get the same behavior.

`ServerForm`'s `enhance` prop is ignored since 7.2.0. Only the interactive
form can send a managed form, so `enhance={false}` only ever produced a form
that silently went nowhere.

## Styles

```tsx
import '@sonordev/site-kit/forms/styles.css'  // Optional default styles
```

All components use `.sk-form__*` class names. Override with `className` prop or CSS variables.
