SIGN IN SIGN UP

feat(react)!: a first-party connect experience (#107)

Milestone 0.9.2, React lane: issues 113, 108, 109's React half, and 111.
On main's
`Discovery.NotFound.host` (#106).

## The disconnected state names who serves the domain

```tsx
<Domain.Flow domain="app.example.com" requirements={requirements} />        // the offer, or nothing
<Domain.Flow domain="app.example.com" requirements={requirements} connect="always" />
```

`Connect.Prompt` is the disconnected surface. With `discovery.host` set
it renders the provider's
mark, its name, and "Owns DNS for this domain." beside a `Connect`
trigger. With no host there is
nothing DomainKit can connect, so it renders nothing until a host
application asks for the dialog
anyway. Read-only keeps the statement and drops the trigger.

The dialog is titled after that provider and shows its methods alone;
the rest wait behind
`Use a different provider`. With no host every provider is listed, one
open at a time. Token fields
take shadcn's `Field` anatomy (`field`, `field-label`,
`field-description`, `field-error`), optional
ids sit behind `Need an account id?`, and the buttons carry the verb:
"Connect with an API token",
"Continue with {provider}", "Install the {provider} integration".

## Outcomes are a compound part

```tsx
<Connect.Outcome controller={connection} />                 // media, title, description, retry

<Connect.Outcome controller={connection} layout="inline">   // or your own composition
  <Outcome.Media variant="default">
    <MyIcon />
  </Outcome.Media>
  <Outcome.Title />
  <Outcome.Content />
</Connect.Outcome>
```

`outcome` > `outcome-header` (`outcome-media`, `outcome-title`,
`outcome-description`) +
`outcome-content`. `Connect.Outcome`, `Provision.Outcome`,
`Cleanup.Outcome`, and `Verify.Outcome`
bind their controller to the same parts, so children replace the
composition and keep the binding.
`data-layout` is `card` or `inline`; `data-tone` is `danger` for a
failure and `warning` for a
receipt that only partly landed.

## The catalog answers with a pair

| Key | Title | Description |
| --- | --- | --- |
| `unauthenticated(reason, context)` | Cloudflare didn't accept this
token | Check the token can read and edit DNS for this zone, then try
again. |
| `providerUnavailable(reason, context)` | Cloudflare isn't responding |
Nothing changed. Try again in a minute. |
| `stale(reason, context)` | The zone changed since you reviewed |
Review the new plan before you apply it. |

Every `DomainKit.Error` reason returns `{ title, description }`.
`Messages.outcome` reads the pair,
`Messages.failure` joins it into one sentence. The context names the
provider the customer acted on,
which `Reason.Unauthenticated` cannot.

## A failed connect keeps the page

`Connect.State.Failure` carries `snapshot`, `discovery`, and the
`attempt` that was in flight, so
`controller.snapshot`, `controller.providers`, and
`controller.discovery` survive. The dialog keeps
its description, its provider forms, and the typed values, and answers
under the field the provider
turned down, with `aria-invalid` on that input. A reason that names no
field marks the first secret,
and the flow does not repeat what the method already said. Before this,
a rejected token left "Authorize DNS
changes for ." above "No DNS providers are available."

## Breaking

- `Messages.Catalog` reason keys take `(reason, context)` and return `{
title, description }`; the
receipt key `outcome` is now `receiptOutcome`; `connectWith` is replaced
by `methodToken`,
`methodOAuth`, and `methodIntegration`; `connectAnyTitle` is now
"Connect a DNS provider".
- Parts: `outcome*` and `field*` replace `flow-outcome`, `token-field*`,
and `token-docs`.
- `Connect.State.Failure` carries three more fields; `Domain.Flow`'s
connection slot receives
  `connect` and defaults to `Connect.Prompt`.

## Screenshots

| The disconnected offer | Read-only |
| --- | --- |
| ![The
prompt](https://github.com/user-attachments/assets/9d5a87de-43e6-4aaf-8ad0-9db8cbdecfd4)
| ![The prompt in
read-only](https://github.com/user-attachments/assets/8d92f270-c732-4222-9726-9bca11cd7699)
|

| Narrowed to the host | Every provider |
| --- | --- |
| ![The narrowed
dialog](https://github.com/user-attachments/assets/c662a3bf-1883-44ff-a985-6b45f1db610d)
| ![The all-providers
dialog](https://github.com/user-attachments/assets/5025817c-ec6f-44d7-8859-87a1f0e21300)
|

| A rejected token | Outcome card | Outcome inline |
| --- | --- | --- |
| ![A rejected
token](https://github.com/user-attachments/assets/24b72c27-dcb1-4400-9f89-017b9c3bc99c)
| ![Outcome
card](https://github.com/user-attachments/assets/03ef18fa-257b-40d7-b2b5-658a6d76572e)
| ![Outcome
inline](https://github.com/user-attachments/assets/550ec4d4-4a0d-4506-b7ae-a5dd817b367f)
|

## Tests

- 48 unit cases, 5 artifact cases, 18 Playwright cases. New: the
compound's parts and a host
recomposition, the pair per reason, the provider name in the context, a
rejected token that keeps
the domain and the typed value while marking one field invalid and
announcing once, a failed
inspection that offers no trigger, the prompt with and without a host,
the narrowed dialog and its
disclosure, the all-providers dialog one open at a time, and read-only.
- `bun run --filter @domainkit/react release:check`, root
`release:check`, `docs:build`, docs
  `audit` and `check`, and `typecheck:examples` are green.

https://claude.ai/code/session_017P54ng7iisz1dsk18u1w1x
S
Saatvik Arya committed
3fd5bd068093b1cccac515e734f6de89e6c5db13
Parent: 887f487
Committed by GitHub <noreply@github.com> on 9/4/2026, 8:52:00 AM