@domainkit/react 0.11.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 +96 -118
- package/dist/index.d.mts +543 -519
- package/dist/index.mjs +734 -2121
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -12
- package/dist/styles.css +0 -1875
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,80 +65,88 @@ 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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 |
|
|
64
81
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
Connecting is the customer saying yes to the records, so the plan
|
|
70
|
-
connection lands
|
|
71
|
-
|
|
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.
|
|
72
90
|
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
## Controllers
|
|
92
|
+
|
|
93
|
+
Compose the flow yourself from the same hooks. Each takes one options object and returns a named
|
|
94
|
+
`Controller` whose `State` is a tagged union.
|
|
75
95
|
|
|
76
96
|
```tsx
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
/>
|
|
97
|
+
const connection = Connect.useController({ domain });
|
|
98
|
+
const provisioning = Provision.useController({ domain, requirements, onApplied });
|
|
99
|
+
const cleanup = Cleanup.useController({ domain, receiptId, onCleaned });
|
|
100
|
+
const verification = Verify.useController({ domain, polling: true });
|
|
82
101
|
```
|
|
83
102
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
`approve` authorizes the plan digest and applies it. `reject` records the customer's refusal; the
|
|
104
|
+
attempt is terminal and a new plan is needed. `retry` builds a new plan when the reason says the old
|
|
105
|
+
one is gone, and re-runs the failed step otherwise.
|
|
106
|
+
|
|
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.
|
|
87
110
|
|
|
88
|
-
##
|
|
111
|
+
## Adding a domain
|
|
89
112
|
|
|
90
|
-
|
|
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.
|
|
91
118
|
|
|
92
119
|
```tsx
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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>;
|
|
101
131
|
```
|
|
102
132
|
|
|
103
|
-
| Slot | Receives | Default |
|
|
104
|
-
| -------------- | ----------------------------------------------- | ---------------------------------------------------------- |
|
|
105
|
-
| `connection` | `{ controller, domain, connect }` | `Connect.Card` once connected, `Connect.Prompt` until then |
|
|
106
|
-
| `records` | `{ records, readiness, controller, domain }` | `Records.Table` |
|
|
107
|
-
| `verification` | `{ controller, domain }` | `Verify.Status` with per-requirement evidence |
|
|
108
|
-
| `actions` | `{ connection, provisioning, cleanup, domain }` | Review changes, Approve, Decline |
|
|
109
|
-
|
|
110
|
-
`Domain.Flow` adds no layout container around a slot: its output is a direct child of the flow root,
|
|
111
|
-
so your own grid can place it without `display: contents`.
|
|
112
|
-
|
|
113
133
|
## The member view
|
|
114
134
|
|
|
115
|
-
A customer who may read a domain but not change it gets `readOnly`.
|
|
116
|
-
that would
|
|
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.
|
|
117
138
|
|
|
118
139
|
```tsx
|
|
119
140
|
<DomainKit.Root transport={transport} readOnly>
|
|
120
|
-
<
|
|
141
|
+
<DomainSetup />
|
|
121
142
|
</DomainKit.Root>
|
|
122
143
|
```
|
|
123
144
|
|
|
124
|
-
`Domain.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`
|
|
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
|
|
128
150
|
provisioning and cleanup. `readOnly` covers the authorization a transport cannot express, such as a
|
|
129
151
|
member of an organisation who reaches the same routes.
|
|
130
152
|
|
|
@@ -132,71 +154,26 @@ Verification does not wait for a connection: the flow observes the requirements
|
|
|
132
154
|
domain with no provider attached still reports which records are in place.
|
|
133
155
|
|
|
134
156
|
Observation stays available in read-only, because checking DNS reads the world rather than changing
|
|
135
|
-
the domain. Retrying is not: a flow that becomes read-only after a write failed keeps the failure
|
|
136
|
-
|
|
137
|
-
If your `Identity.authorize` denies `observe` to members, pass `slots={{ verification: () => null }}`.
|
|
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.
|
|
138
159
|
|
|
139
|
-
##
|
|
160
|
+
## Words
|
|
140
161
|
|
|
141
|
-
|
|
142
|
-
`
|
|
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.
|
|
143
165
|
|
|
144
166
|
```tsx
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const cleanup = Cleanup.useController({ domain, receiptId, onCleaned });
|
|
148
|
-
const verification = Verify.useController({ domain, polling: true });
|
|
167
|
+
const describe = Outcome.useDescribe();
|
|
168
|
+
const words = describe(error, { provider: "Cloudflare" });
|
|
149
169
|
```
|
|
150
170
|
|
|
151
|
-
|
|
152
|
-
attempt is terminal and a new plan is needed. `retry` builds a new plan when the reason says the old
|
|
153
|
-
one is gone, and re-runs the failed step otherwise.
|
|
154
|
-
|
|
155
|
-
## Presentation
|
|
156
|
-
|
|
157
|
-
`DomainKit.Root` takes `messages`, `marks`, `icons`, `theme`, `colorScheme`, and `portalContainer`,
|
|
158
|
-
so branding stays in your app. `Messages.Catalog` holds every user-visible string, including a
|
|
159
|
-
title and a description per `DomainKit.Error` reason; nothing renders a tag. Provider artwork comes from `marks`,
|
|
160
|
-
with the provider's initial as the fallback and no request at render time. The stylesheet is
|
|
161
|
-
opt-in and every color is a `--domainkit-*` custom property.
|
|
162
|
-
|
|
163
|
-
Every rule ships inside `@layer domainkit`. An unlayered rule of yours already beats every layer, so
|
|
164
|
-
plain CSS needs nothing here.
|
|
165
|
-
|
|
166
|
-
Once your app uses layers, name the order yourself: a layer's priority is its position in that list,
|
|
167
|
-
and a layer you never declare lands wherever it first appears, so `domainkit` can sort under
|
|
168
|
-
Tailwind's preflight and the package's buttons render as bare text.
|
|
169
|
-
|
|
170
|
-
```css
|
|
171
|
-
@layer theme, base, components, domainkit, utilities;
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
`domainkit` sits after `base` so preflight cannot strip it, and before `utilities` so a utility class
|
|
175
|
-
still wins. Any layer you declare after it wins the same way:
|
|
176
|
-
|
|
177
|
-
```css
|
|
178
|
-
@layer utilities {
|
|
179
|
-
[data-domainkit-part="records-panel"] {
|
|
180
|
-
border-radius: 0;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
A failed step renders as `Outcome`: media, the catalog's title and description, and the retry the
|
|
186
|
-
flow allows, as a card or on one line. Pass your own parts as children and the words still come from
|
|
187
|
-
the catalog.
|
|
188
|
-
|
|
189
|
-
```tsx
|
|
190
|
-
<Connect.Outcome controller={connection} layout="inline">
|
|
191
|
-
<Outcome.Media variant="default">
|
|
192
|
-
<MyIcon />
|
|
193
|
-
</Outcome.Media>
|
|
194
|
-
<Outcome.Title />
|
|
195
|
-
<Outcome.Content />
|
|
196
|
-
</Connect.Outcome>
|
|
197
|
-
```
|
|
171
|
+
## Records
|
|
198
172
|
|
|
199
|
-
|
|
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.
|
|
200
177
|
|
|
201
178
|
## Next.js
|
|
202
179
|
|
|
@@ -205,6 +182,7 @@ a client module rather than passing one from a server component.
|
|
|
205
182
|
|
|
206
183
|
## Learn more
|
|
207
184
|
|
|
185
|
+
- [Registry](https://domain-kit.dev/components/registry)
|
|
208
186
|
- [React source](https://github.com/AryaLabsHQ/domainkit/tree/main/packages/react)
|
|
209
187
|
- [Transport contract](https://github.com/AryaLabsHQ/domainkit/blob/main/packages/domainkit/src/Transport.ts)
|
|
210
188
|
- [Issues](https://github.com/AryaLabsHQ/domainkit/issues)
|