@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 CHANGED
@@ -1,7 +1,8 @@
1
1
  # @domainkit/react
2
2
 
3
- React flows for custom-domain setup, over a transport your server owns. Provider credentials never
4
- reach the browser.
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
- ## Two components
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} colorScheme="inherit">
36
- <Domain.Flow domain="app.example.com" requirements={requirements} />
48
+ <DomainKit.Root transport={transport}>
49
+ <DomainSetup />
37
50
  </DomainKit.Root>
38
51
  );
39
52
  }
40
53
  ```
41
54
 
42
- `Domain.Flow` connects a provider, plans the DNS changes, takes the customer's approval or refusal,
43
- applies the plan, observes the records, and cleans them up against the apply receipt. It renders
44
- only the capability groups the transport declares.
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
- ## The disconnected offer
68
+ ## What the flow says about a domain
55
69
 
56
- Discovery names the provider whose nameservers serve the domain. `Connect.Prompt` states it beside a
57
- trigger, and the dialog behind that trigger is narrowed to that provider, with the rest behind a
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
- When no registered provider serves the zone there is nothing DomainKit can connect, so the flow
61
- offers nothing. Pass `connect="always"` to offer the all-providers dialog anyway:
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
- ```tsx
64
- <Domain.Flow domain="app.example.com" requirements={requirements} connect="always" />
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
- Every part of the flow is a slot with a default.
70
-
71
- ```tsx
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
- ## Presentation
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
- `DomainKit.Root` takes `messages`, `marks`, `icons`, `theme`, `colorScheme`, and `portalContainer`,
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
- Every rule ships inside `@layer domainkit`. An unlayered rule of yours already beats every layer, so
143
- plain CSS needs nothing here.
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
- Once your app uses layers, name the order yourself: a layer's priority is its position in that list,
146
- and a layer you never declare lands wherever it first appears, so `domainkit` can sort under
147
- Tailwind's preflight and the package's buttons render as bare text.
148
-
149
- ```css
150
- @layer theme, base, components, domainkit, utilities;
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
- `domainkit` sits after `base` so preflight cannot strip it, and before `utilities` so a utility class
154
- still wins. Any layer you declare after it wins the same way:
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
- ```css
157
- @layer utilities {
158
- [data-domainkit-part="records-panel"] {
159
- border-radius: 0;
160
- }
161
- }
139
+ ```tsx
140
+ <DomainKit.Root transport={transport} readOnly>
141
+ <DomainSetup />
142
+ </DomainKit.Root>
162
143
  ```
163
144
 
164
- A failed step renders as `Outcome`: media, the catalog's title and description, and the retry the
165
- flow allows, as a card or on one line. Pass your own parts as children and the words still come from
166
- the catalog.
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
- <Connect.Outcome controller={connection} layout="inline">
170
- <Outcome.Media variant="default">
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
- Every dialog and popover takes a `render` prop that replaces the surface with your own.
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)