feat(react): rebuild @domainkit/react on the 0.9 transport (#90)
## Summary
`@domainkit/react` 0.9: two components for the whole domain lifecycle,
over the capability-gated
transport from `domainkit/client`.
```tsx
const transport = Transport.fromFetch("/api/domainkit")
<DomainKit.Root transport={transport} colorScheme="inherit">
<Domain.Flow domain="app.example.com" requirements={requirements} />
</DomainKit.Root>
```
Own one piece and keep the rest:
```tsx
<Domain.Flow
domain="app.example.com"
requirements={requirements}
slots={{ records: ({ records, readiness }) => <MyTable records={records} readiness={readiness} /> }}
onApplied={(receipt) => track(receipt.id)}
/>
```
## Controllers
Every controller takes one options object and returns a named
`Controller` whose `State` is a
`Data.taggedEnum` carrying the core models: `Plan.Model`,
`Approval.Model`, `Receipt.Model`,
and `DomainKit.Error`.
| Hook | Options | Verbs |
| --- | --- | --- |
| `Connect.useController` | `{ domain }` | `connect`, `reuse`, `select`,
`detach`, `disconnect`, `refresh`, `retry` |
| `Provision.useController` | `{ domain, requirements, onApplied? }` |
`plan`, `approve`, `reject`, `apply`, `retry`, `reset` |
| `Cleanup.useController` | `{ domain, receiptId?, onCleaned? }` | same
|
| `Verify.useController` | `{ domain, polling? }` | `observe`, `retry` |
`approve` authorizes the digest and applies it. `reject` records the
refusal through
`POST /plans/:planId/rejections`; the attempt is terminal and the flow
shows it. `retry` re-plans
when the reason says the plan is gone (`Stale`, `Expired`, `Conflict`)
and re-runs the failed step
otherwise.
## Transport by value
`DomainKit.Root` takes a `Transport.Interface` by value and keeps its
identity for the whole
mount, so this does not restart the controllers on every render:
```tsx
<DomainKit.Root transport={Transport.fromFetch("/api/domainkit")}>
```
Each method reads the newest transport when it runs; only a change to
the declared capability
groups rebuilds it. `revision` re-inspects every mounted domain.
## Rendering from the wire
- Token forms come from `Snapshot.providers[].methods[].fields`: one
input per declared field,
`type="password"` when the descriptor says `secret`. No per-provider
form code.
- `connection.discover` runs on mount for a domain with no connection,
and the form offers the
connection that already serves the zone.
- Failures render through `Messages.Catalog`, one sentence per
`DomainKit.Error` reason, keyed by
the `Reason` tags. No `_tag` reaches a customer.
- Parts render only what `Transport.capabilities()` declares.
## Slots
| Slot | Receives | Default |
| --- | --- | --- |
| `connection` | `{ controller, domain }` | `Connect.Card` once
connected, `Connect.Dialog` until then |
| `records` | `{ records, readiness, controller, domain }` |
`Records.Table` |
| `verification` | `{ controller, domain }` | `Verify.Status` with
per-requirement evidence |
| `actions` | `{ connection, provisioning, cleanup, domain }` | Review
changes, Approve, Decline, Remove records |
Slot output is a direct child of the flow root, so a host grid places it
without
`display: contents`. Every dialog and popover takes a `render` prop that
swaps the surface.
## Screenshots
| Requirements | Connect |
| --- | --- |
|

| 
|
| Plan review | Applied |
| --- | --- |
| 
|

|
## Tests
- `test` — 26 cases over `Testing.transport()`: an in-memory
`domainkit/server` with memory
storage, not a stubbed `fetch`.
- `test:browser` — 5 Playwright cases against a Vite fixture in
`tests/browser/app`, replacing the
docs-app dependency.
- `test:artifact` — packed Vite and Next App Router consumers, plus the
packed file list and peer
ranges.
`bun run --filter @domainkit/react release:check` is green, and the root
`release:check` now runs
it as its last leg and passes end to end. S
Saatvik Arya committed
d8bcdaeccbe547cfe3462e5c16798881643d00c8
Parent: 26d180d
Committed by GitHub <noreply@github.com>
on 9/3/2026, 6:54:04 PM