@domainkit/react 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -101
- package/dist/index.d.mts +612 -466
- package/dist/index.mjs +814 -1751
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -12
- package/dist/styles.css +0 -1530
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @domainkit/react
|
|
2
2
|
|
|
3
|
-
React
|
|
4
|
-
|
|
3
|
+
Headless React hooks for custom-domain setup, over a transport your server owns. The package
|
|
4
|
+
supplies state, copy, and accessibility; your application supplies the markup. Provider credentials
|
|
5
|
+
never reach the browser.
|
|
5
6
|
|
|
6
7
|
## Install
|
|
7
8
|
|
|
@@ -11,13 +12,20 @@ npm install @domainkit/react domainkit effect@rc react react-dom
|
|
|
11
12
|
|
|
12
13
|
React 19 is required. Install `domainkit` and `@domainkit/react` at the same release version.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
For the styled composition, add the flow from the DomainKit registry into your own shadcn kit:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npx shadcn@latest add https://domain-kit.dev/r/domain-flow.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## One hook
|
|
15
22
|
|
|
16
23
|
```tsx
|
|
24
|
+
"use client";
|
|
25
|
+
|
|
17
26
|
import { DnsRecord } from "domainkit";
|
|
18
27
|
import { Transport } from "domainkit/client";
|
|
19
28
|
import { Domain, DomainKit } from "@domainkit/react";
|
|
20
|
-
import "@domainkit/react/styles.css";
|
|
21
29
|
|
|
22
30
|
const transport = Transport.fromFetch("/api/domainkit");
|
|
23
31
|
|
|
@@ -30,18 +38,24 @@ const requirements = [
|
|
|
30
38
|
}),
|
|
31
39
|
];
|
|
32
40
|
|
|
41
|
+
function DomainSetup() {
|
|
42
|
+
const flow = Domain.useFlow({ domain: "app.example.com", requirements });
|
|
43
|
+
return <DomainFlowCard flow={flow} />;
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
export function DomainSettings() {
|
|
34
47
|
return (
|
|
35
|
-
<DomainKit.Root transport={transport}
|
|
36
|
-
<
|
|
48
|
+
<DomainKit.Root transport={transport}>
|
|
49
|
+
<DomainSetup />
|
|
37
50
|
</DomainKit.Root>
|
|
38
51
|
);
|
|
39
52
|
}
|
|
40
53
|
```
|
|
41
54
|
|
|
42
|
-
`Domain.
|
|
43
|
-
applies the plan, observes the records, and cleans them up against the apply receipt. It
|
|
44
|
-
|
|
55
|
+
`Domain.useFlow` connects a provider, plans the DNS changes, takes the customer's approval or
|
|
56
|
+
refusal, applies the plan, observes the records, and cleans them up against the apply receipt. It
|
|
57
|
+
returns the four controllers, the plan the rows report, the latest readiness, the capability groups
|
|
58
|
+
the transport declares, and a `FlowState` your own surface reads.
|
|
45
59
|
|
|
46
60
|
`Transport.fromFetch` points at the routes you mounted from `domainkit/server`. Writing it inline in
|
|
47
61
|
JSX is fine: `DomainKit.Root` keeps the transport's identity for the whole mount, so the controllers
|
|
@@ -51,69 +65,28 @@ the UI did not make.
|
|
|
51
65
|
An interactive provider returns the customer to the page they started from. Pass `returnTo` to name
|
|
52
66
|
a different destination, or `null` to leave the server's `defaultReturnTo` in charge.
|
|
53
67
|
|
|
54
|
-
##
|
|
68
|
+
## What the flow says about a domain
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
disclosure.
|
|
70
|
+
`flow.state` is what DomainKit has to say, so your own offers can be ordered beside it rather than
|
|
71
|
+
competing with it.
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
| Field | Says |
|
|
74
|
+
| ---------------------------- | ------------------------------------------------------------- |
|
|
75
|
+
| `connected` | DomainKit holds a connection for this domain |
|
|
76
|
+
| `offering` | The connect surface has something to offer |
|
|
77
|
+
| `provider`, `label` | Who holds the connection, and which account the records go to |
|
|
78
|
+
| `receiptId`, `applied` | The apply this domain's records came from |
|
|
79
|
+
| `connection`, `provisioning` | The tag each controller's state carries |
|
|
80
|
+
| `readOnly` | The customer may read this domain but not write to it |
|
|
62
81
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
## Own one piece, keep the rest
|
|
82
|
+
Discovery names the provider whose nameservers serve the domain, and `Connect.hostProvider` returns
|
|
83
|
+
its descriptor. When no registered provider serves the zone there is nothing DomainKit can connect,
|
|
84
|
+
so `offering` is false. Pass `connect: "always"` to offer every provider anyway, or
|
|
85
|
+
`connect: "never"` for a domain your application has already settled another way.
|
|
68
86
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<Domain.Flow
|
|
73
|
-
domain="app.example.com"
|
|
74
|
-
requirements={requirements}
|
|
75
|
-
slots={{
|
|
76
|
-
records: ({ records, readiness }) => <MyTable records={records} readiness={readiness} />,
|
|
77
|
-
}}
|
|
78
|
-
onApplied={(receipt) => track("dns.applied", { receiptId: receipt.id })}
|
|
79
|
-
/>
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
| Slot | Receives | Default |
|
|
83
|
-
| -------------- | ----------------------------------------------- | ---------------------------------------------------------- |
|
|
84
|
-
| `connection` | `{ controller, domain, connect }` | `Connect.Card` once connected, `Connect.Prompt` until then |
|
|
85
|
-
| `records` | `{ records, readiness, controller, domain }` | `Records.Table` |
|
|
86
|
-
| `verification` | `{ controller, domain }` | `Verify.Status` with per-requirement evidence |
|
|
87
|
-
| `actions` | `{ connection, provisioning, cleanup, domain }` | Review changes, Approve, Decline, Remove records |
|
|
88
|
-
|
|
89
|
-
`Domain.Flow` adds no layout container around a slot: its output is a direct child of the flow root,
|
|
90
|
-
so your own grid can place it without `display: contents`.
|
|
91
|
-
|
|
92
|
-
## The member view
|
|
93
|
-
|
|
94
|
-
A customer who may read a domain but not change it gets `readOnly`. Status renders; every control
|
|
95
|
-
that would start a write does not.
|
|
96
|
-
|
|
97
|
-
```tsx
|
|
98
|
-
<DomainKit.Root transport={transport} readOnly>
|
|
99
|
-
<Domain.Flow domain="app.example.com" requirements={requirements} />
|
|
100
|
-
</DomainKit.Root>
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
`Domain.Flow` takes the same flag when only one domain on the page is read-only, and `useReadOnly()`
|
|
104
|
-
tells a part of your own which mode it is in. Capability gating is the other half: a group the
|
|
105
|
-
transport does not declare never renders, so a transport built with
|
|
106
|
-
`Transport.fromFetch(url, { capabilities: ["connection", "verification"] })` already hides
|
|
107
|
-
provisioning and cleanup. `readOnly` covers the authorization a transport cannot express, such as a
|
|
108
|
-
member of an organisation who reaches the same routes.
|
|
109
|
-
|
|
110
|
-
Verification does not wait for a connection: the flow observes the requirements it was given, so a
|
|
111
|
-
domain with no provider attached still reports which records are in place.
|
|
112
|
-
|
|
113
|
-
Observation stays available in read-only, because checking DNS reads the world rather than changing
|
|
114
|
-
the domain. Retrying is not: a flow that becomes read-only after a write failed keeps the failure on
|
|
115
|
-
screen and drops the retry.
|
|
116
|
-
If your `Identity.authorize` denies `observe` to members, pass `slots={{ verification: () => null }}`.
|
|
87
|
+
Connecting is the customer saying yes to the records, so the plan builds itself the moment a
|
|
88
|
+
connection lands, after a token connect and after the customer returns from a provider. One
|
|
89
|
+
`approve` adds them.
|
|
117
90
|
|
|
118
91
|
## Controllers
|
|
119
92
|
|
|
@@ -131,51 +104,76 @@ const verification = Verify.useController({ domain, polling: true });
|
|
|
131
104
|
attempt is terminal and a new plan is needed. `retry` builds a new plan when the reason says the old
|
|
132
105
|
one is gone, and re-runs the failed step otherwise.
|
|
133
106
|
|
|
134
|
-
|
|
107
|
+
`Connect.describeMethods` arranges one provider's auth methods into the interactive ones a customer
|
|
108
|
+
clicks through and the ones that ask for credentials, so a connect surface asks for one decision at
|
|
109
|
+
a time. `Connect.rejectedField` names the one field a rejection was about.
|
|
135
110
|
|
|
136
|
-
|
|
137
|
-
so branding stays in your app. `Messages.Catalog` holds every user-visible string, including a
|
|
138
|
-
title and a description per `DomainKit.Error` reason; nothing renders a tag. Provider artwork comes from `marks`,
|
|
139
|
-
with the provider's initial as the fallback and no request at render time. The stylesheet is
|
|
140
|
-
opt-in and every color is a `--domainkit-*` custom property.
|
|
111
|
+
## Adding a domain
|
|
141
112
|
|
|
142
|
-
|
|
143
|
-
|
|
113
|
+
`Connect.useZones` lists every zone the workspace's accounts reach, `Connect.useAccounts` adds
|
|
114
|
+
another account without naming a domain, and `Connect.useDomainField` turns a text input into a
|
|
115
|
+
combobox over those zones: it returns `inputProps`, `listboxProps`, and `optionProps`, moves the
|
|
116
|
+
highlight with the arrow keys, and completes on Tab or Enter while keeping whatever subdomain was
|
|
117
|
+
typed in front of the zone.
|
|
144
118
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
119
|
+
```tsx
|
|
120
|
+
const { zones } = Connect.useZones();
|
|
121
|
+
const field = Connect.useDomainField({ zones, value, onChange: setValue, onResolve });
|
|
122
|
+
|
|
123
|
+
<input {...field.inputProps} />
|
|
124
|
+
<ul {...field.listboxProps}>
|
|
125
|
+
{field.suggestions.map((zone) => (
|
|
126
|
+
<li key={`${zone.connectionId}:${zone.zone}`} {...field.optionProps(zone)}>
|
|
127
|
+
{zone.zone}
|
|
128
|
+
</li>
|
|
129
|
+
))}
|
|
130
|
+
</ul>;
|
|
151
131
|
```
|
|
152
132
|
|
|
153
|
-
|
|
154
|
-
|
|
133
|
+
## The member view
|
|
134
|
+
|
|
135
|
+
A customer who may read a domain but not change it gets `readOnly`. State still reports; every
|
|
136
|
+
command that would change the domain refuses to run, at the controller rather than in your markup,
|
|
137
|
+
so a control you render anyway cannot reach the transport.
|
|
155
138
|
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
}
|
|
139
|
+
```tsx
|
|
140
|
+
<DomainKit.Root transport={transport} readOnly>
|
|
141
|
+
<DomainSetup />
|
|
142
|
+
</DomainKit.Root>
|
|
162
143
|
```
|
|
163
144
|
|
|
164
|
-
|
|
165
|
-
flow
|
|
166
|
-
the
|
|
145
|
+
`Domain.useFlow` takes the same flag when only one domain on the page is read-only, and it rides on
|
|
146
|
+
`flow.state.readOnly`, so your surface can say who may connect instead of rendering nothing.
|
|
147
|
+
Capability gating is the other half: a group the transport does not declare is absent from
|
|
148
|
+
`flow.capabilities`, so a transport built with
|
|
149
|
+
`Transport.fromFetch(url, { capabilities: ["connection", "verification"] })` already rules out
|
|
150
|
+
provisioning and cleanup. `readOnly` covers the authorization a transport cannot express, such as a
|
|
151
|
+
member of an organisation who reaches the same routes.
|
|
152
|
+
|
|
153
|
+
Verification does not wait for a connection: the flow observes the requirements it was given, so a
|
|
154
|
+
domain with no provider attached still reports which records are in place.
|
|
155
|
+
|
|
156
|
+
Observation stays available in read-only, because checking DNS reads the world rather than changing
|
|
157
|
+
the domain. Retrying is not: a flow that becomes read-only after a write failed keeps the failure
|
|
158
|
+
and re-inspects instead of resending the command.
|
|
159
|
+
|
|
160
|
+
## Words
|
|
161
|
+
|
|
162
|
+
`Messages.Catalog` holds every user-visible string, including a title and a description per
|
|
163
|
+
`DomainKit.Error` reason; nothing renders a tag, a status literal, or a reason name. Pass
|
|
164
|
+
`messages` to `DomainKit.Root` to override any key.
|
|
167
165
|
|
|
168
166
|
```tsx
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
<MyIcon />
|
|
172
|
-
</Outcome.Media>
|
|
173
|
-
<Outcome.Title />
|
|
174
|
-
<Outcome.Content />
|
|
175
|
-
</Connect.Outcome>
|
|
167
|
+
const describe = Outcome.useDescribe();
|
|
168
|
+
const words = describe(error, { provider: "Cloudflare" });
|
|
176
169
|
```
|
|
177
170
|
|
|
178
|
-
|
|
171
|
+
## Records
|
|
172
|
+
|
|
173
|
+
`Records.statusOf` answers what one row has to say: the operation a pending plan holds for it, or
|
|
174
|
+
the status the last observation read back. `Records.useCopy` is the clipboard control a value
|
|
175
|
+
needs, and `Records.toZoneFile` and `Records.downloadZoneFile` spell the whole requirement set for a
|
|
176
|
+
customer who edits DNS by hand.
|
|
179
177
|
|
|
180
178
|
## Next.js
|
|
181
179
|
|
|
@@ -184,6 +182,7 @@ a client module rather than passing one from a server component.
|
|
|
184
182
|
|
|
185
183
|
## Learn more
|
|
186
184
|
|
|
185
|
+
- [Registry](https://domain-kit.dev/components/registry)
|
|
187
186
|
- [React source](https://github.com/AryaLabsHQ/domainkit/tree/main/packages/react)
|
|
188
187
|
- [Transport contract](https://github.com/AryaLabsHQ/domainkit/blob/main/packages/domainkit/src/Transport.ts)
|
|
189
188
|
- [Issues](https://github.com/AryaLabsHQ/domainkit/issues)
|