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 |
| --- | --- |
| 
| 
|
| Narrowed to the host | Every provider |
| --- | --- |
| 
| 
|
| A rejected token | Outcome card | Outcome inline |
| --- | --- | --- |
| 
| 
| 
|
## 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