@domainkit/react 0.10.0 → 0.11.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/dist/index.mjs CHANGED
@@ -10,6 +10,9 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
10
  import * as Exit from "effect/Exit";
11
11
  import * as Option from "effect/Option";
12
12
  import { Dialog } from "@base-ui/react/dialog";
13
+ import { Collapsible } from "@base-ui/react/collapsible";
14
+ import { Switch } from "@base-ui/react/switch";
15
+ import { Menu } from "@base-ui/react/menu";
13
16
  import { Popover } from "@base-ui/react/popover";
14
17
  import * as DateTime from "effect/DateTime";
15
18
  import { Testing } from "domainkit/testing";
@@ -47,6 +50,7 @@ function Glyph({ children }) {
47
50
  });
48
51
  }
49
52
  const defaultIcons = {
53
+ chevron: /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }) }),
50
54
  close: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })] }),
51
55
  copied: /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) }),
52
56
  copy: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("rect", {
@@ -161,14 +165,23 @@ const english = {
161
165
  close: "Close",
162
166
  retry: "Try again",
163
167
  approve: "Approve",
168
+ addRecords: (count) => count === 1 ? "Add 1 record" : `Add ${count} records`,
164
169
  decline: "Decline",
170
+ everyRecordConflicts: "Resolve the records above at your provider, then review again.",
165
171
  reviewChanges: "Review changes",
166
172
  cleanUp: "Remove records",
167
173
  checkDns: "Check DNS",
168
174
  checkAgain: "Check again",
169
175
  moreActions: "More actions",
170
- moreOptions: "Need an account id?",
176
+ moreOptions: "Add an account id",
171
177
  useAnotherProvider: "Use a different provider",
178
+ useTokenInstead: "Use an API token instead",
179
+ back: "Back",
180
+ connected: "Connected",
181
+ needsReconnect: "Needs reconnecting",
182
+ disconnectScope: "This connection serves other domains.",
183
+ disconnectThisDomain: "Only this domain",
184
+ disconnectEveryDomain: (count) => `All ${count} domains`,
172
185
  copy: "Copy",
173
186
  copied: "Copied",
174
187
  copyZone: "Copy zone file",
@@ -196,7 +209,9 @@ const english = {
196
209
  notConnected: "No DNS provider is connected.",
197
210
  reconnectRequired: (provider) => `${provider} needs to be reconnected`,
198
211
  disconnectTitle: (provider) => `Disconnect ${provider}?`,
199
- disconnectConsent: "The provider connection is removed. Existing DNS records are preserved.",
212
+ disconnectConsent: (domain, provider) => `DomainKit stops managing DNS for ${domain} through ${provider}.`,
213
+ disconnectWithCleanup: (count) => count === 1 ? "Remove the 1 record DomainKit added" : `Remove the ${count} records DomainKit added`,
214
+ disconnectKeepsRecords: (provider) => `Records stay in ${provider}.`,
200
215
  detachConsent: "This domain is detached from the provider. Existing DNS records are preserved.",
201
216
  detached: "Domain detached. DNS records were preserved.",
202
217
  reusableConnections: "Connections you already have",
@@ -313,10 +328,7 @@ const english = {
313
328
  description: reason.message,
314
329
  title: reason.field === void 0 ? "Check what you entered" : `Check the ${humanize(reason.field).toLowerCase()}`
315
330
  }),
316
- unauthenticated: (_reason, context) => ({
317
- description: "Check the token can read and edit DNS for this zone, then try again.",
318
- title: `${named(context.provider)} didn't accept this token`
319
- }),
331
+ unauthenticated: () => ({ title: "Token not accepted" }),
320
332
  forbidden: (_reason, context) => ({
321
333
  description: `The connection needs permission to edit DNS for ${where(context.domain)}.`,
322
334
  title: "This connection can't change DNS here"
@@ -403,7 +415,7 @@ const outcome = (error, catalog, context = {}) => {
403
415
  /** The same failure as one sentence, for a host that renders text rather than a card. */
404
416
  const failure = (error, catalog, context = {}) => {
405
417
  const { description, title } = outcome(error, catalog, context);
406
- return `${title}. ${description}`;
418
+ return description === void 0 ? `${title}.` : `${title}. ${description}`;
407
419
  };
408
420
  //#endregion
409
421
  //#region src/theme.ts
@@ -939,16 +951,17 @@ function Title(props) {
939
951
  "data-domainkit-part": "outcome-title"
940
952
  });
941
953
  }
942
- /** What to do about it, from the catalog. */
954
+ /** What to do about it, from the catalog. An outcome whose title says everything renders none. */
943
955
  function Description(props) {
944
956
  const { description, layout, tone } = useOutcome();
945
- return usePart("p", props, {
957
+ const element = usePart("p", props, {
946
958
  layout,
947
959
  tone
948
960
  }, {
949
961
  children: description,
950
962
  "data-domainkit-part": "outcome-description"
951
963
  });
964
+ return description === "" && props.children === void 0 ? null : element;
952
965
  }
953
966
  /**
954
967
  * The actions under the header. Without children it is the flow's retry, and a flow that allows
@@ -1307,12 +1320,12 @@ function Status$4({ controller, kind, ...props }) {
1307
1320
  switch (state._tag) {
1308
1321
  case "Idle": return "";
1309
1322
  case "Planning": return kind === "provisioning" ? messages.planning : messages.planningCleanup;
1310
- case "Planned": return kind === "provisioning" ? messages.planConsent : messages.cleanupConsent;
1323
+ case "Planned": return "";
1311
1324
  case "Approving": return messages.approving;
1312
1325
  case "Applying": return kind === "provisioning" ? messages.applying : messages.cleaning;
1313
1326
  case "Rejecting": return messages.rejecting;
1314
1327
  case "Rejected": return messages.declinedBy(state.attempt.rejection?.actorId ?? "");
1315
- case "Applied": return kind === "provisioning" ? messages.applied(state.receipt) : messages.cleaned(state.receipt);
1328
+ case "Applied": return "";
1316
1329
  case "Failure": return "";
1317
1330
  }
1318
1331
  };
@@ -1324,28 +1337,32 @@ function Status$4({ controller, kind, ...props }) {
1324
1337
  });
1325
1338
  }
1326
1339
  /**
1327
- * The failure, or a receipt that only partly landed: media, title, description, and the retry the
1328
- * flow allows. Children replace the composition and keep the binding.
1340
+ * How the attempt ended: a failure, a receipt that only partly landed, or one that landed whole.
1341
+ * Media, title, description, and the retry the flow allows. Children replace the composition and
1342
+ * keep the binding.
1329
1343
  */
1330
1344
  function Outcome$4({ children, controller, kind, ...props }) {
1331
1345
  const { messages } = useDomainKit();
1332
1346
  const readOnly = useReadOnly();
1333
1347
  const state = controller.state;
1334
- const partial = state._tag === "Applied" && state.receipt.status === "partial";
1335
1348
  const retryPart = kind === "provisioning" ? "provisioning-retry" : "cleanup-retry";
1336
- if (state._tag !== "Failure" && !partial) return null;
1337
- const words = state._tag === "Failure" ? outcome(state.error, messages) : {
1349
+ if (state._tag !== "Failure" && state._tag !== "Applied") return null;
1350
+ const partial = state._tag === "Applied" && state.receipt.status === "partial";
1351
+ const words = state._tag === "Failure" ? outcome(state.error, messages) : partial ? {
1338
1352
  description: kind === "provisioning" ? messages.partiallyApplied : messages.partiallyCleaned,
1339
1353
  title: kind === "provisioning" ? messages.partiallyAppliedTitle : messages.partiallyCleanedTitle
1354
+ } : {
1355
+ description: "",
1356
+ title: kind === "provisioning" ? messages.applied(state.receipt) : messages.cleaned(state.receipt)
1340
1357
  };
1341
1358
  return /* @__PURE__ */ jsx(Provider, {
1342
1359
  value: {
1343
- description: words.description,
1360
+ description: words.description ?? "",
1344
1361
  layout: props.layout ?? "card",
1345
1362
  retry: readOnly || state._tag !== "Failure" ? null : controller.retry,
1346
1363
  retryPart,
1347
1364
  title: words.title,
1348
- tone: state._tag === "Failure" ? "danger" : "warning"
1365
+ tone: state._tag === "Failure" ? "danger" : partial ? "warning" : "success"
1349
1366
  },
1350
1367
  children: /* @__PURE__ */ jsx(Root$6, {
1351
1368
  ...props,
@@ -1353,9 +1370,13 @@ function Outcome$4({ children, controller, kind, ...props }) {
1353
1370
  })
1354
1371
  });
1355
1372
  }
1373
+ /** What a plan would actually write: an operation blocked by a conflict is not one of them. */
1374
+ const addable = (plan) => plan === null ? [] : plan.operations.filter((operation) => operation._tag !== "Conflict");
1356
1375
  /**
1357
- * Approve and Decline. Approve authorizes the digest and applies it; Decline records the refusal
1358
- * and ends the attempt. Neither renders until there is a plan to act on.
1376
+ * The primary action, and Decline. The primary authorizes the digest and applies it in one step,
1377
+ * so it says what it will do; where a conflict blocks part of a plan it approves the rest by id,
1378
+ * and where every operation is blocked it says what to fix instead. Neither renders until there is
1379
+ * a plan to act on.
1359
1380
  */
1360
1381
  function Actions$2({ controller, kind, ...props }) {
1361
1382
  const { messages } = useDomainKit();
@@ -1363,24 +1384,33 @@ function Actions$2({ controller, kind, ...props }) {
1363
1384
  const state = controller.state;
1364
1385
  const plan = state._tag === "Planned" ? state.plan : null;
1365
1386
  const running = busy(state._tag);
1387
+ const writes = addable(plan);
1388
+ const blocked = plan !== null && writes.length === 0;
1366
1389
  const element = usePart("div", props, { status: state._tag }, {
1367
- children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
1368
- "data-domainkit-part": kind === "provisioning" ? "plan-apply" : "cleanup-apply",
1369
- disabled: running || plan === null || plan.operations.length === 0,
1370
- onClick: () => controller.approve(),
1371
- type: "button",
1372
- children: messages.approve
1373
- }), /* @__PURE__ */ jsx("button", {
1374
- "data-domainkit-part": "plan-decline",
1375
- disabled: running || plan === null,
1376
- onClick: () => controller.reject(),
1377
- type: "button",
1378
- children: messages.decline
1379
- })] }),
1390
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [
1391
+ /* @__PURE__ */ jsx("button", {
1392
+ "data-domainkit-part": kind === "provisioning" ? "plan-apply" : "cleanup-apply",
1393
+ disabled: running || plan === null || writes.length === 0,
1394
+ onClick: () => controller.approve(writes.length === plan?.operations.length ? void 0 : writes.map((operation) => operation.id)),
1395
+ type: "button",
1396
+ children: kind === "provisioning" ? messages.addRecords(writes.length) : messages.approve
1397
+ }),
1398
+ blocked ? /* @__PURE__ */ jsx("p", {
1399
+ "data-domainkit-part": "plan-blocked",
1400
+ children: messages.everyRecordConflicts
1401
+ }) : null,
1402
+ /* @__PURE__ */ jsx("button", {
1403
+ "data-domainkit-part": "plan-decline",
1404
+ disabled: running || plan === null,
1405
+ onClick: () => controller.reject(),
1406
+ type: "button",
1407
+ children: messages.decline
1408
+ })
1409
+ ] }),
1380
1410
  "data-domainkit-part": "review-actions"
1381
1411
  });
1382
1412
  if (readOnly) return null;
1383
- return plan === null && !running ? null : element;
1413
+ return plan === null ? null : element;
1384
1414
  }
1385
1415
  /** The operations under review, or the reason there is nothing to review. */
1386
1416
  function Body({ controller, kind }) {
@@ -1415,10 +1445,17 @@ function Dialog$4({ children, controller, kind, onOpenChange, open, render, trig
1415
1445
  const readOnly = useReadOnly();
1416
1446
  const [uncontrolled, setUncontrolled] = useState(false);
1417
1447
  const isOpen = open ?? uncontrolled;
1418
- const setOpen = (next) => {
1448
+ const close = useRef(() => {});
1449
+ close.current = (next) => {
1419
1450
  if (open === void 0) setUncontrolled(next);
1420
1451
  onOpenChange?.(next);
1421
1452
  };
1453
+ const setOpen = (next) => close.current(next);
1454
+ const state = controller.state;
1455
+ const landed = state._tag === "Applied" && state.receipt.status === "complete";
1456
+ useEffect(() => {
1457
+ if (landed) close.current(false);
1458
+ }, [landed]);
1422
1459
  const running = busy(controller.state._tag);
1423
1460
  const body = children ?? /* @__PURE__ */ jsx(Body, {
1424
1461
  controller,
@@ -1488,7 +1525,7 @@ function Dialog$4({ children, controller, kind, onOpenChange, open, render, trig
1488
1525
  children: [/* @__PURE__ */ jsx(Actions$2, {
1489
1526
  controller,
1490
1527
  kind
1491
- }), running ? null : /* @__PURE__ */ jsx(Dialog.Close, {
1528
+ }), running || kind === "provisioning" ? null : /* @__PURE__ */ jsx(Dialog.Close, {
1492
1529
  "data-domainkit-part": "dialog-cancel",
1493
1530
  children: messages.cancel
1494
1531
  })]
@@ -1580,13 +1617,16 @@ function Flow$3({ domain, onCleaned, receiptId, trigger, ...props }) {
1580
1617
  ...onCleaned === void 0 ? {} : { onCleaned },
1581
1618
  ...receiptId === void 0 ? {} : { receiptId }
1582
1619
  });
1620
+ const [open, setOpen] = useState(false);
1583
1621
  return /* @__PURE__ */ jsxs(Root$3, {
1584
1622
  controller,
1585
1623
  ...props,
1586
1624
  children: [/* @__PURE__ */ jsx(Dialog$3, {
1587
1625
  controller,
1626
+ onOpenChange: setOpen,
1627
+ open,
1588
1628
  ...trigger === void 0 ? {} : { trigger }
1589
- }), /* @__PURE__ */ jsx(Outcome$3, { controller })]
1629
+ }), open ? null : /* @__PURE__ */ jsx(Outcome$3, { controller })]
1590
1630
  });
1591
1631
  }
1592
1632
  //#endregion
@@ -1616,6 +1656,7 @@ function Mark({ provider, ...props }) {
1616
1656
  var connect_exports = /* @__PURE__ */ __exportAll({
1617
1657
  Card: () => Card,
1618
1658
  Dialog: () => Dialog$2,
1659
+ DisconnectDialog: () => DisconnectDialog,
1619
1660
  Flow: () => Flow$2,
1620
1661
  Form: () => Form,
1621
1662
  Outcome: () => Outcome$2,
@@ -1624,10 +1665,32 @@ var connect_exports = /* @__PURE__ */ __exportAll({
1624
1665
  State: () => State$1,
1625
1666
  Status: () => Status$2,
1626
1667
  answeredInPlace: () => answeredInPlace,
1668
+ holdsConnection: () => holdsConnection,
1627
1669
  hostProvider: () => hostProvider,
1670
+ offering: () => offering,
1628
1671
  useController: () => useController$2
1629
1672
  });
1630
1673
  const State$1 = Data.taggedEnum();
1674
+ /**
1675
+ * An interactive method leaves the page and comes back, so what the library knew is gone with it.
1676
+ * It writes down the domain it sent the customer away for, and the load that follows reads it once:
1677
+ * that, not a query parameter a host would have to keep, is how a return is told from a reload.
1678
+ */
1679
+ const RETURNING = "domainkit.returning";
1680
+ const markReturn = (domain) => {
1681
+ try {
1682
+ sessionStorage.setItem(RETURNING, domain);
1683
+ } catch {}
1684
+ };
1685
+ const tookReturn = (domain) => {
1686
+ try {
1687
+ if (sessionStorage.getItem(RETURNING) !== domain) return false;
1688
+ sessionStorage.removeItem(RETURNING);
1689
+ return true;
1690
+ } catch {
1691
+ return false;
1692
+ }
1693
+ };
1631
1694
  /** Read at connect time, not at render, so it is the page the customer is actually on. */
1632
1695
  const currentUrl = () => typeof window === "undefined" ? null : window.location.href;
1633
1696
  const snapshotOf = (state) => {
@@ -1670,6 +1733,7 @@ function useController$2({ domain, returnTo }) {
1670
1733
  });
1671
1734
  const lastCommand = useRef(null);
1672
1735
  const attempted = useRef(null);
1736
+ const [established, setEstablished] = useState(0);
1673
1737
  const [inspected, setInspected] = useState(domain);
1674
1738
  if (inspected !== domain) {
1675
1739
  setInspected(domain);
@@ -1719,6 +1783,7 @@ function useController$2({ domain, returnTo }) {
1719
1783
  domain,
1720
1784
  snapshot
1721
1785
  };
1786
+ if (tookReturn(domain) && snapshot.status === "connected") setEstablished((count) => count + 1);
1722
1787
  setState(settled(snapshot, discovery));
1723
1788
  }
1724
1789
  });
@@ -1738,6 +1803,7 @@ function useController$2({ domain, returnTo }) {
1738
1803
  domain,
1739
1804
  snapshot: result.snapshot
1740
1805
  };
1806
+ setEstablished((count) => count + 1);
1741
1807
  setState(settled(result.snapshot, null));
1742
1808
  if (result.snapshot.connectionId !== null) emit(Event.Connected({
1743
1809
  connectionId: result.snapshot.connectionId,
@@ -1746,6 +1812,7 @@ function useController$2({ domain, returnTo }) {
1746
1812
  }));
1747
1813
  return;
1748
1814
  case "Redirect":
1815
+ markReturn(domain);
1749
1816
  setState(State$1.Redirecting({ url: result.authorizationUrl }));
1750
1817
  navigate(result.authorizationUrl);
1751
1818
  return;
@@ -1766,7 +1833,10 @@ function useController$2({ domain, returnTo }) {
1766
1833
  if (held.current.domain !== domain) return;
1767
1834
  attempted.current = attempt;
1768
1835
  const command = () => {
1769
- setState(State$1.Submitting({ snapshot: held.current.snapshot }));
1836
+ setState(State$1.Submitting({
1837
+ discovery: held.current.discovery,
1838
+ snapshot: held.current.snapshot
1839
+ }));
1770
1840
  runner.run(effect, {
1771
1841
  onFailure,
1772
1842
  onSuccess: started
@@ -1825,7 +1895,10 @@ function useController$2({ domain, returnTo }) {
1825
1895
  if (held.current.domain !== domain) return;
1826
1896
  attempted.current = null;
1827
1897
  const command = () => {
1828
- setState(State$1.Submitting({ snapshot: held.current.snapshot }));
1898
+ setState(State$1.Submitting({
1899
+ discovery: held.current.discovery,
1900
+ snapshot: held.current.snapshot
1901
+ }));
1829
1902
  runner.run(effect, {
1830
1903
  onFailure,
1831
1904
  onSuccess: () => {
@@ -1880,7 +1953,9 @@ function useController$2({ domain, returnTo }) {
1880
1953
  connect,
1881
1954
  detach,
1882
1955
  disconnect,
1883
- discovery: state._tag === "Disconnected" || state._tag === "Failure" ? state.discovery : null,
1956
+ domain,
1957
+ established,
1958
+ discovery: state._tag === "Disconnected" || state._tag === "Failure" || state._tag === "Submitting" ? state.discovery : null,
1884
1959
  providers: snapshotOf(state)?.providers ?? [],
1885
1960
  refresh: load,
1886
1961
  retry,
@@ -1902,6 +1977,12 @@ function Root$2({ controller, ...props }) {
1902
1977
  */
1903
1978
  const answeredInPlace = (controller) => controller.state._tag === "Failure" && controller.state.attempt !== null;
1904
1979
  /**
1980
+ * Whether the surface should read as connected: it is, or a command it started is in flight over a
1981
+ * connection that was. The card and the dialog it opened stay on screen for the length of the
1982
+ * command rather than vanishing under the customer half way through it.
1983
+ */
1984
+ const holdsConnection = (controller) => controller.state._tag === "Connected" || controller.state._tag === "Submitting" && controller.snapshot?.status === "connected";
1985
+ /**
1905
1986
  * The provider whose nameservers serve this domain, as the snapshot describes it. Discovery names
1906
1987
  * it by id; without a descriptor there is nothing to draw, so there is no host.
1907
1988
  */
@@ -1929,7 +2010,7 @@ function Outcome$2({ children, controller, ...props }) {
1929
2010
  });
1930
2011
  return /* @__PURE__ */ jsx(Provider, {
1931
2012
  value: {
1932
- description: words.description,
2013
+ description: words.description ?? "",
1933
2014
  layout: props.layout ?? "card",
1934
2015
  retry: readOnly ? null : controller.retry,
1935
2016
  retryPart: "connection-retry",
@@ -1945,7 +2026,8 @@ function Outcome$2({ children, controller, ...props }) {
1945
2026
  function Status$2({ controller, ...props }) {
1946
2027
  const { messages } = useDomainKit();
1947
2028
  const state = controller.state;
1948
- const provider = controller.snapshot?.provider ?? "";
2029
+ const attached = controller.snapshot?.provider ?? null;
2030
+ const provider = attached === null ? "" : displayName(controller, attached);
1949
2031
  const text = () => {
1950
2032
  switch (state._tag) {
1951
2033
  case "Loading": return messages.loading;
@@ -1978,9 +2060,10 @@ const rejected = (reason, fields) => {
1978
2060
  };
1979
2061
  /**
1980
2062
  * One declared field in shadcn's `Field` anatomy: the label, the control, the note under it, and
1981
- * the error the provider answered with. The docs link rides the field it explains.
2063
+ * the error the provider answered with. The docs link rides the field it explains. The value is
2064
+ * the form's, not the input's, so a rejection answers beside what the customer typed.
1982
2065
  */
1983
- function TokenField({ controller, docsUrl, field, invalid }) {
2066
+ function TokenField({ controller, docsUrl, field, invalid, onEnter, value }) {
1984
2067
  const { messages } = useDomainKit();
1985
2068
  const icons = useIcons();
1986
2069
  const id = useId();
@@ -2003,8 +2086,10 @@ function TokenField({ controller, docsUrl, field, invalid }) {
2003
2086
  "data-domainkit-part": "field-input",
2004
2087
  id,
2005
2088
  name: field.name,
2089
+ onChange: (event) => onEnter(event.target.value),
2006
2090
  required: field.required,
2007
- type: field.secret ? "password" : "text"
2091
+ type: field.secret ? "password" : "text",
2092
+ value
2008
2093
  }),
2009
2094
  docsUrl === null ? null : /* @__PURE__ */ jsxs("a", {
2010
2095
  "data-domainkit-part": "field-description",
@@ -2032,8 +2117,10 @@ function TokenField({ controller, docsUrl, field, invalid }) {
2032
2117
  * field, so a provider that needs an account id alongside a token needs no code here. The fields
2033
2118
  * a provider does not need sit behind a disclosure, so the common form is one field.
2034
2119
  */
2035
- function Method({ controller, method, provider }) {
2120
+ function Method({ controller, method, onEnter, provider, values }) {
2036
2121
  const { messages } = useDomainKit();
2122
+ const icons = useIcons();
2123
+ const [more, setMore] = useState(false);
2037
2124
  const state = controller.state;
2038
2125
  const busy = state._tag === "Submitting";
2039
2126
  const failed = state._tag === "Failure" && state.attempt !== null && state.attempt.provider === provider.id && state.attempt.method === method.kind;
@@ -2055,39 +2142,50 @@ function Method({ controller, method, provider }) {
2055
2142
  const answered = rejected(failed && state._tag === "Failure" ? state.error.reason : null, fields);
2056
2143
  const required = fields.filter((field) => field.required);
2057
2144
  const optional = fields.filter((field) => !field.required);
2145
+ const showMore = more || optional.some((field) => field.name === answered);
2058
2146
  const explains = (required.find((field) => field.secret) ?? required[0] ?? optional[0])?.name;
2059
2147
  const render = (field) => /* @__PURE__ */ jsx(TokenField, {
2060
2148
  controller,
2061
2149
  docsUrl: field.name === explains ? method.docsUrl : null,
2062
2150
  field,
2063
- invalid: field.name === answered
2151
+ invalid: field.name === answered,
2152
+ onEnter: (value) => onEnter(field.name, value),
2153
+ value: values[field.name] ?? ""
2064
2154
  }, field.name);
2065
2155
  return /* @__PURE__ */ jsxs("form", {
2066
2156
  "data-domainkit-part": "token-connect",
2067
2157
  "data-provider": provider.id,
2068
2158
  onSubmit: (event) => {
2069
2159
  event.preventDefault();
2070
- const data = new FormData(event.currentTarget);
2071
- const values = {};
2160
+ const submitted = {};
2072
2161
  for (const field of fields) {
2073
- const value = data.get(field.name);
2074
- if (typeof value === "string" && (value !== "" || field.required)) values[field.name] = value;
2162
+ const value = values[field.name] ?? "";
2163
+ if (value !== "" || field.required) submitted[field.name] = value;
2075
2164
  }
2076
2165
  controller.connect({
2077
2166
  method: method.kind,
2078
2167
  provider: provider.id,
2079
- values
2168
+ values: submitted
2080
2169
  });
2081
2170
  },
2082
2171
  children: [
2083
2172
  required.map(render),
2084
- optional.length === 0 ? null : /* @__PURE__ */ jsxs("details", {
2173
+ optional.length === 0 ? null : /* @__PURE__ */ jsxs(Collapsible.Root, {
2085
2174
  "data-domainkit-part": "more-options",
2086
- open: optional.some((field) => field.name === answered) || void 0,
2087
- children: [/* @__PURE__ */ jsx("summary", {
2175
+ "data-state": showMore ? "open" : "closed",
2176
+ onOpenChange: setMore,
2177
+ open: showMore,
2178
+ children: [/* @__PURE__ */ jsxs(Collapsible.Trigger, {
2088
2179
  "data-domainkit-part": "more-options-trigger",
2089
- children: messages.moreOptions
2090
- }), optional.map(render)]
2180
+ children: [messages.moreOptions, /* @__PURE__ */ jsx("span", {
2181
+ "aria-hidden": "true",
2182
+ "data-icon": "inline-end",
2183
+ children: icons.chevron
2184
+ })]
2185
+ }), /* @__PURE__ */ jsx(Collapsible.Panel, {
2186
+ "data-domainkit-part": "more-options-panel",
2187
+ children: optional.map(render)
2188
+ })]
2091
2189
  }),
2092
2190
  failed && answered === null ? /* @__PURE__ */ jsx(Outcome$2, {
2093
2191
  controller,
@@ -2103,15 +2201,33 @@ function Method({ controller, method, provider }) {
2103
2201
  });
2104
2202
  }
2105
2203
  /**
2106
- * One provider's methods. With a heading it is a disclosure the customer opens; the provider a
2107
- * narrowed dialog is about carries no heading, because the dialog title already names it.
2204
+ * One provider's methods, in the order the descriptor declares them: the interactive ones a
2205
+ * customer clicks through, then the token. Where a provider offers both, the interactive method is
2206
+ * the offer and a plain button opens the token form in its place, so the dialog asks for one
2207
+ * decision at a time. With a heading the whole provider is a disclosure the customer opens; the
2208
+ * provider a narrowed dialog is about carries no heading, because the dialog title already names
2209
+ * it.
2108
2210
  */
2109
- function Authentication({ controller, onOpen, open, provider }) {
2110
- const methods = provider.methods.map((method) => /* @__PURE__ */ jsx(Method, {
2211
+ function Authentication({ controller, onEnter, onOpen, onType, open, provider, typing, values }) {
2212
+ const { messages } = useDomainKit();
2213
+ const state = controller.state;
2214
+ const render = (method) => /* @__PURE__ */ jsx(Method, {
2111
2215
  controller,
2112
2216
  method,
2113
- provider
2114
- }, `${provider.id}:${method.kind}`));
2217
+ onEnter,
2218
+ provider,
2219
+ values
2220
+ }, `${provider.id}:${method.kind}`);
2221
+ const interactive = provider.methods.filter((method) => method.fields === null);
2222
+ const typed = provider.methods.filter((method) => method.fields !== null);
2223
+ const methods = !(interactive.length > 0 && typed.length > 0) ? provider.methods.map(render) : typing ? typed.map(render) : /* @__PURE__ */ jsxs(Fragment, { children: [interactive.map(render), /* @__PURE__ */ jsx("button", {
2224
+ "data-domainkit-part": "method-alternate",
2225
+ "data-provider": provider.id,
2226
+ disabled: state._tag === "Submitting",
2227
+ onClick: onType,
2228
+ type: "button",
2229
+ children: messages.useTokenInstead
2230
+ })] });
2115
2231
  return /* @__PURE__ */ jsxs("section", {
2116
2232
  "data-domainkit-part": "provider-authentication",
2117
2233
  "data-provider": provider.id,
@@ -2129,10 +2245,26 @@ function Authentication({ controller, onOpen, open, provider }) {
2129
2245
  * Everything a disconnected domain can do: a connection discovery already found, the connections
2130
2246
  * this owner already has, then each provider's declared methods.
2131
2247
  */
2132
- function Form({ controller, ...props }) {
2248
+ function Form({ controller, provider: narrowed, ...props }) {
2133
2249
  const { messages } = useDomainKit();
2134
2250
  const [opened, setOpened] = useState(null);
2251
+ const [entered, setEntered] = useState(null);
2135
2252
  const state = controller.state;
2253
+ const owner = controller.domain;
2254
+ if (entered !== null && (entered.domain !== owner || state._tag === "Connected")) setEntered(null);
2255
+ const kept = entered?.domain === owner ? entered : null;
2256
+ const enter = (provider, name, value) => setEntered((previous) => {
2257
+ const held = previous?.domain === owner && previous.provider === provider ? previous : null;
2258
+ return {
2259
+ domain: owner,
2260
+ provider,
2261
+ values: held === null ? { [name]: value } : {
2262
+ ...held.values,
2263
+ [name]: value
2264
+ }
2265
+ };
2266
+ });
2267
+ const valuesFor = (provider) => kept?.provider === provider ? kept.values : {};
2136
2268
  const snapshot = controller.snapshot;
2137
2269
  const discovery = controller.discovery;
2138
2270
  const candidates = state._tag === "SelectionRequired" ? state.candidates.map((candidate) => ({
@@ -2142,13 +2274,17 @@ function Form({ controller, ...props }) {
2142
2274
  const resolved = discovery !== null && discovery._tag === "Resolved" ? discovery : null;
2143
2275
  const reusable = snapshot?.reusable ?? [];
2144
2276
  const providers = controller.providers;
2145
- const host = hostProvider(controller);
2146
- const others = providers.filter((provider) => provider.id !== host?.id);
2277
+ const host = providers.find((entry) => entry.id === narrowed) ?? hostProvider(controller);
2278
+ const [typing, setTyping] = useState(null);
2147
2279
  const authentication = (provider, open) => /* @__PURE__ */ jsx(Authentication, {
2148
2280
  controller,
2281
+ onEnter: (name, value) => enter(provider.id, name, value),
2149
2282
  onOpen: () => setOpened(open ? "" : provider.id),
2283
+ onType: () => setTyping(provider.id),
2150
2284
  open,
2151
- provider
2285
+ provider,
2286
+ typing: typing === provider.id,
2287
+ values: valuesFor(provider.id)
2152
2288
  }, provider.id);
2153
2289
  return usePart("div", props, { status: state._tag }, {
2154
2290
  children: /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -2196,16 +2332,19 @@ function Form({ controller, ...props }) {
2196
2332
  providers.length === 0 ? /* @__PURE__ */ jsx("p", {
2197
2333
  "data-domainkit-part": "target-unavailable",
2198
2334
  children: messages.noProviders
2199
- }) : host === null ? others.map((provider) => authentication(provider, opened === null ? provider.id === others[0]?.id : opened === provider.id)) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Authentication, {
2335
+ }) : host === null ? providers.map((provider) => authentication(provider, opened === null ? provider.id === providers[0]?.id : opened === provider.id)) : /* @__PURE__ */ jsxs(Fragment, { children: [typing === host.id ? /* @__PURE__ */ jsx("button", {
2336
+ "data-domainkit-part": "dialog-back",
2337
+ onClick: () => setTyping(null),
2338
+ type: "button",
2339
+ children: messages.back
2340
+ }) : null, /* @__PURE__ */ jsx(Authentication, {
2200
2341
  controller,
2342
+ onEnter: (name, value) => enter(host.id, name, value),
2343
+ onType: () => setTyping(host.id),
2201
2344
  open: true,
2202
- provider: host
2203
- }), others.length === 0 ? null : /* @__PURE__ */ jsxs("details", {
2204
- "data-domainkit-part": "other-providers",
2205
- children: [/* @__PURE__ */ jsx("summary", {
2206
- "data-domainkit-part": "other-providers-trigger",
2207
- children: messages.useAnotherProvider
2208
- }), others.map((provider) => authentication(provider, opened === provider.id))]
2345
+ provider: host,
2346
+ typing: typing === host.id,
2347
+ values: valuesFor(host.id)
2209
2348
  })] })
2210
2349
  ] }),
2211
2350
  "data-domainkit-part": "connection-form"
@@ -2214,19 +2353,26 @@ function Form({ controller, ...props }) {
2214
2353
  /** The connect surface. Without `render` it is a Base UI dialog behind `trigger`. */
2215
2354
  function Dialog$2({ children, controller, render, title, trigger }) {
2216
2355
  const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
2356
+ const icons = useIcons();
2217
2357
  const readOnly = useReadOnly();
2218
2358
  const [open, setOpen] = useState(false);
2219
2359
  const state = controller.state;
2220
2360
  const busy = state._tag === "Submitting";
2221
2361
  const unattributed = state._tag === "Failure" && !answeredInPlace(controller);
2222
2362
  const snapshot = controller.snapshot;
2363
+ const [chosen, setChosen] = useState(null);
2223
2364
  const attached = snapshot?.provider ?? null;
2224
2365
  const host = hostProvider(controller);
2225
- const named = attached === null ? host?.name ?? null : displayName(controller, attached);
2366
+ const narrowed = controller.providers.find((entry) => entry.id === (chosen ?? attached)) ?? host;
2367
+ const named = narrowed?.name ?? null;
2226
2368
  const heading = title ?? (named === null ? messages.connectAnyTitle : messages.connectTitle(named));
2369
+ const alternatives = narrowed === null ? [] : controller.providers.filter((entry) => entry.id !== narrowed.id);
2227
2370
  const running = state._tag === "Loading" || state._tag === "Submitting" || state._tag === "Redirecting" || state._tag === "SelectionRequired";
2228
2371
  const label = named === null ? messages.connectAnyTitle : messages.connect;
2229
- const body = children ?? /* @__PURE__ */ jsx(Form, { controller });
2372
+ const body = children ?? /* @__PURE__ */ jsx(Form, {
2373
+ controller,
2374
+ ...chosen === null ? {} : { provider: chosen }
2375
+ });
2230
2376
  if (readOnly) return /* @__PURE__ */ jsx(Fragment, {});
2231
2377
  if (render !== void 0) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
2232
2378
  "data-domainkit-part": "connection-trigger",
@@ -2264,20 +2410,55 @@ function Dialog$2({ children, controller, render, title, trigger }) {
2264
2410
  children: [
2265
2411
  /* @__PURE__ */ jsxs("div", {
2266
2412
  "data-domainkit-part": "dialog-header",
2267
- children: [/* @__PURE__ */ jsxs("div", {
2268
- "data-domainkit-part": "dialog-heading",
2269
- children: [/* @__PURE__ */ jsx(Dialog.Title, {
2270
- "data-domainkit-part": "dialog-title",
2271
- children: heading
2272
- }), /* @__PURE__ */ jsx(Dialog.Description, {
2273
- "data-domainkit-part": "dialog-description",
2274
- children: messages.connectDescription(snapshot?.domain ?? "")
2275
- })]
2276
- }), busy ? null : /* @__PURE__ */ jsx(Dialog.Close, {
2277
- "aria-label": messages.close,
2278
- "data-domainkit-part": "dialog-close",
2279
- children: "×"
2280
- })]
2413
+ children: [
2414
+ narrowed === null ? null : /* @__PURE__ */ jsx("div", {
2415
+ "data-domainkit-part": "dialog-media",
2416
+ children: /* @__PURE__ */ jsx(Mark, { provider: narrowed })
2417
+ }),
2418
+ /* @__PURE__ */ jsxs("div", {
2419
+ "data-domainkit-part": "dialog-heading",
2420
+ children: [/* @__PURE__ */ jsx(Dialog.Title, {
2421
+ "data-domainkit-part": "dialog-title",
2422
+ children: alternatives.length === 0 || title !== void 0 ? heading : /* @__PURE__ */ jsxs(Menu.Root, { children: [/* @__PURE__ */ jsxs(Menu.Trigger, {
2423
+ "data-domainkit-part": "dialog-provider-menu",
2424
+ disabled: busy,
2425
+ children: [heading, /* @__PURE__ */ jsx("span", {
2426
+ "aria-hidden": "true",
2427
+ "data-icon": "inline-end",
2428
+ children: icons.chevron
2429
+ })]
2430
+ }), /* @__PURE__ */ jsx(Menu.Portal, {
2431
+ container: portalContainer,
2432
+ children: /* @__PURE__ */ jsx(Menu.Positioner, {
2433
+ align: "start",
2434
+ "data-domainkit-part": "provider-menu-positioner",
2435
+ sideOffset: 6,
2436
+ children: /* @__PURE__ */ jsx(Menu.Popup, {
2437
+ "aria-label": messages.useAnotherProvider,
2438
+ "data-color-scheme": colorScheme,
2439
+ "data-domainkit-part": "provider-menu",
2440
+ "data-domainkit-root": "",
2441
+ style: themeStyle,
2442
+ children: alternatives.map((entry) => /* @__PURE__ */ jsxs(Menu.Item, {
2443
+ "data-domainkit-part": "provider-menu-item",
2444
+ "data-provider": entry.id,
2445
+ onClick: () => setChosen(entry.id),
2446
+ children: [/* @__PURE__ */ jsx(Mark, { provider: entry }), entry.name]
2447
+ }, entry.id))
2448
+ })
2449
+ })
2450
+ })] })
2451
+ }), /* @__PURE__ */ jsx(Dialog.Description, {
2452
+ "data-domainkit-part": "dialog-description",
2453
+ children: messages.connectDescription(snapshot?.domain ?? "")
2454
+ })]
2455
+ }),
2456
+ busy ? null : /* @__PURE__ */ jsx(Dialog.Close, {
2457
+ "aria-label": messages.close,
2458
+ "data-domainkit-part": "dialog-close",
2459
+ children: "×"
2460
+ })
2461
+ ]
2281
2462
  }),
2282
2463
  /* @__PURE__ */ jsxs("div", {
2283
2464
  "data-domainkit-part": "dialog-body",
@@ -2300,6 +2481,21 @@ function Dialog$2({ children, controller, render, title, trigger }) {
2300
2481
  });
2301
2482
  }
2302
2483
  /**
2484
+ * Whether the connect surface has anything to offer for this domain: a provider that serves the
2485
+ * zone, a connection discovery already found, one the owner already holds, or a command already
2486
+ * running. Never while a connection is held, which is what `holdsConnection` answers. `Prompt`
2487
+ * renders on this, and `Domain.Flow` reports it so a host can order its own offers beside
2488
+ * DomainKit's rather than competing with them.
2489
+ */
2490
+ const offering = (controller, connect = "detected") => {
2491
+ if (holdsConnection(controller)) return false;
2492
+ const state = controller.state;
2493
+ if (state._tag === "Submitting" || state._tag === "Redirecting" || state._tag === "SelectionRequired" || state._tag === "Reconnect") return true;
2494
+ if (connect !== "detected") return connect === "always";
2495
+ const discovery = controller.discovery;
2496
+ return hostProvider(controller) !== null || discovery !== null && discovery._tag !== "NotFound" || (controller.snapshot?.reusable.length ?? 0) > 0;
2497
+ };
2498
+ /**
2303
2499
  * The disconnected offer: the provider whose nameservers serve the domain, what that means, and
2304
2500
  * the trigger that connects it. With no host detected there is nothing to offer, so nothing
2305
2501
  * renders unless the host application asks for the dialog anyway.
@@ -2309,9 +2505,6 @@ function Prompt({ connect = "detected", controller, ...props }) {
2309
2505
  const readOnly = useReadOnly();
2310
2506
  const state = controller.state;
2311
2507
  const host = hostProvider(controller);
2312
- const discovery = controller.discovery;
2313
- const offers = host !== null || discovery !== null && discovery._tag !== "NotFound" || (controller.snapshot?.reusable.length ?? 0) > 0;
2314
- const inFlight = state._tag === "Submitting" || state._tag === "Redirecting" || state._tag === "SelectionRequired" || state._tag === "Reconnect";
2315
2508
  const element = usePart("div", props, { status: state._tag }, {
2316
2509
  children: /* @__PURE__ */ jsxs(Fragment, { children: [host === null ? null : /* @__PURE__ */ jsxs("div", {
2317
2510
  "data-domainkit-part": "host-identity",
@@ -2330,38 +2523,242 @@ function Prompt({ connect = "detected", controller, ...props }) {
2330
2523
  "data-domainkit-part": "connect-prompt",
2331
2524
  "data-host": host?.id
2332
2525
  });
2333
- if (!inFlight && !offers && connect !== "always") return null;
2526
+ if (!offering(controller, connect)) return null;
2334
2527
  if (readOnly && host === null) return null;
2335
2528
  return element;
2336
2529
  }
2337
- /** A connected domain: which provider holds it, and how to let it go. */
2338
- function Card({ controller, ...props }) {
2530
+ /** The plan an attempt holds, whatever step it is on; `null` before there is one. */
2531
+ const planOf = (state) => {
2532
+ switch (state._tag) {
2533
+ case "Planned":
2534
+ case "Approving":
2535
+ case "Applying":
2536
+ case "Rejecting":
2537
+ case "Rejected": return state.plan;
2538
+ case "Applied": return state.plan;
2539
+ case "Idle":
2540
+ case "Planning":
2541
+ case "Failure": return null;
2542
+ }
2543
+ };
2544
+ /**
2545
+ * Letting a provider go, with the records DomainKit added as one decision rather than two.
2546
+ * Confirming removes what the domain's apply receipt proves DomainKit created, then releases the
2547
+ * connection; a plan with nothing in it is not a failure, so it goes straight to the release.
2548
+ *
2549
+ * The cleanup is this dialog's own, so nothing re-reads the snapshot in between: the disconnect
2550
+ * needs the connection the cleanup just used.
2551
+ */
2552
+ function DisconnectDialog({ controller, onCleaned, trigger }) {
2553
+ const { capabilities, colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
2554
+ const readOnly = useReadOnly();
2555
+ const [open, setOpen] = useState(false);
2556
+ const [alsoRemove, setAlsoRemove] = useState(true);
2557
+ const [removing, setRemoving] = useState(false);
2558
+ const snapshot = controller.snapshot;
2559
+ const receiptId = snapshot?.lastReceiptId ?? null;
2560
+ const removable = capabilities.includes("cleanup") && receiptId !== null;
2561
+ const shared = (snapshot?.connectionDomains ?? 0) > 1;
2562
+ const [everyDomain, setEveryDomain] = useState(false);
2563
+ const cleanup = useController$3({
2564
+ domain: controller.domain,
2565
+ ...onCleaned === void 0 ? {} : { onCleaned },
2566
+ ...receiptId === null ? {} : { receiptId: Receipt.ReceiptId.make(receiptId) }
2567
+ });
2568
+ const { approve, plan, state: cleaning } = cleanup;
2569
+ const removals = planOf(cleaning);
2570
+ useEffect(() => {
2571
+ if (open && removable && cleaning._tag === "Idle") plan();
2572
+ }, [
2573
+ cleaning,
2574
+ open,
2575
+ plan,
2576
+ removable
2577
+ ]);
2578
+ const detach = controller.detach;
2579
+ const release = controller.disconnect;
2580
+ const disconnect = useCallback(() => shared && !everyDomain ? detach() : release(), [
2581
+ detach,
2582
+ everyDomain,
2583
+ release,
2584
+ shared
2585
+ ]);
2586
+ const releasing = controller.state._tag === "Submitting";
2587
+ useEffect(() => {
2588
+ if (!removing) return;
2589
+ if (cleaning._tag === "Applied") {
2590
+ setRemoving(false);
2591
+ disconnect();
2592
+ return;
2593
+ }
2594
+ if (cleaning._tag === "Failure" || cleaning._tag === "Rejected") setRemoving(false);
2595
+ }, [
2596
+ cleaning,
2597
+ disconnect,
2598
+ removing
2599
+ ]);
2600
+ const busy = removing || releasing;
2601
+ const settling = removable && removals === null;
2602
+ const provider = snapshot?.provider ?? null;
2603
+ const named = provider === null ? "" : displayName(controller, provider);
2604
+ const heading = provider === null ? messages.disconnect : messages.disconnectTitle(named);
2605
+ const count = removals?.operations.length ?? 0;
2606
+ const choosable = removable && count > 0;
2607
+ if (readOnly) return /* @__PURE__ */ jsx(Fragment, {});
2608
+ return /* @__PURE__ */ jsxs(Dialog.Root, {
2609
+ onOpenChange: (next, details) => {
2610
+ if (!next && busy) {
2611
+ details.cancel();
2612
+ return;
2613
+ }
2614
+ setOpen(next);
2615
+ },
2616
+ open,
2617
+ children: [/* @__PURE__ */ jsx(Dialog.Trigger, {
2618
+ "data-domainkit-part": "disconnect-action",
2619
+ children: trigger ?? messages.disconnect
2620
+ }), /* @__PURE__ */ jsxs(Dialog.Portal, {
2621
+ container: portalContainer,
2622
+ children: [/* @__PURE__ */ jsx(Dialog.Backdrop, {
2623
+ "data-color-scheme": colorScheme,
2624
+ "data-domainkit-part": "dialog-backdrop",
2625
+ "data-domainkit-root": "",
2626
+ style: themeStyle
2627
+ }), /* @__PURE__ */ jsxs(Dialog.Popup, {
2628
+ "data-color-scheme": colorScheme,
2629
+ "data-cleanup": choosable ? "offered" : "none",
2630
+ "data-domainkit-part": "disconnect-dialog",
2631
+ "data-domainkit-root": "",
2632
+ style: themeStyle,
2633
+ children: [
2634
+ /* @__PURE__ */ jsxs("div", {
2635
+ "data-domainkit-part": "dialog-header",
2636
+ children: [/* @__PURE__ */ jsxs("div", {
2637
+ "data-domainkit-part": "dialog-heading",
2638
+ children: [/* @__PURE__ */ jsx(Dialog.Title, {
2639
+ "data-domainkit-part": "dialog-title",
2640
+ children: heading
2641
+ }), /* @__PURE__ */ jsx(Dialog.Description, {
2642
+ "data-domainkit-part": "dialog-description",
2643
+ children: messages.disconnectConsent(snapshot?.domain ?? "", named)
2644
+ })]
2645
+ }), busy ? null : /* @__PURE__ */ jsx(Dialog.Close, {
2646
+ "aria-label": messages.close,
2647
+ "data-domainkit-part": "dialog-close",
2648
+ children: "×"
2649
+ })]
2650
+ }),
2651
+ /* @__PURE__ */ jsxs("div", {
2652
+ "data-domainkit-part": "dialog-body",
2653
+ children: [
2654
+ shared ? /* @__PURE__ */ jsxs("fieldset", {
2655
+ "data-domainkit-part": "disconnect-scope",
2656
+ children: [
2657
+ /* @__PURE__ */ jsx("legend", { children: messages.disconnectScope }),
2658
+ /* @__PURE__ */ jsxs("label", { children: [/* @__PURE__ */ jsx("input", {
2659
+ checked: !everyDomain,
2660
+ disabled: busy,
2661
+ name: "disconnect-scope",
2662
+ onChange: () => setEveryDomain(false),
2663
+ type: "radio"
2664
+ }), messages.disconnectThisDomain] }),
2665
+ /* @__PURE__ */ jsxs("label", { children: [/* @__PURE__ */ jsx("input", {
2666
+ checked: everyDomain,
2667
+ disabled: busy,
2668
+ name: "disconnect-scope",
2669
+ onChange: () => setEveryDomain(true),
2670
+ type: "radio"
2671
+ }), messages.disconnectEveryDomain(snapshot?.connectionDomains ?? 0)] })
2672
+ ]
2673
+ }) : null,
2674
+ choosable ? /* @__PURE__ */ jsxs("div", {
2675
+ "data-domainkit-part": "disconnect-cleanup",
2676
+ "data-state": alsoRemove ? "on" : "off",
2677
+ children: [/* @__PURE__ */ jsxs("label", {
2678
+ "data-domainkit-part": "disconnect-cleanup-label",
2679
+ children: [/* @__PURE__ */ jsx(Switch.Root, {
2680
+ checked: alsoRemove,
2681
+ "data-domainkit-part": "disconnect-cleanup-switch",
2682
+ disabled: busy,
2683
+ onCheckedChange: setAlsoRemove,
2684
+ children: /* @__PURE__ */ jsx(Switch.Thumb, { "data-domainkit-part": "disconnect-cleanup-thumb" })
2685
+ }), messages.disconnectWithCleanup(count)]
2686
+ }), /* @__PURE__ */ jsxs("div", {
2687
+ "data-domainkit-part": "disconnect-cleanup-panel",
2688
+ children: [removals === null ? null : /* @__PURE__ */ jsx(List, { plan: removals }), alsoRemove ? null : /* @__PURE__ */ jsx("p", {
2689
+ "data-domainkit-part": "disconnect-cleanup-note",
2690
+ children: messages.disconnectKeepsRecords(named)
2691
+ })]
2692
+ })]
2693
+ }) : null,
2694
+ /* @__PURE__ */ jsx(Status$3, { controller: cleanup }),
2695
+ releasing ? /* @__PURE__ */ jsx("p", {
2696
+ "data-domainkit-part": "disconnect-status",
2697
+ role: "status",
2698
+ children: messages.disconnecting
2699
+ }) : null,
2700
+ /* @__PURE__ */ jsx(Outcome$3, { controller: cleanup }),
2701
+ /* @__PURE__ */ jsx(Outcome$2, { controller })
2702
+ ]
2703
+ }),
2704
+ /* @__PURE__ */ jsxs("div", {
2705
+ "data-domainkit-part": "dialog-footer",
2706
+ children: [/* @__PURE__ */ jsx("button", {
2707
+ "data-domainkit-part": "disconnect-confirm",
2708
+ disabled: busy || settling,
2709
+ onClick: () => {
2710
+ if (choosable && alsoRemove) {
2711
+ setRemoving(true);
2712
+ approve();
2713
+ return;
2714
+ }
2715
+ disconnect();
2716
+ },
2717
+ type: "button",
2718
+ children: messages.disconnect
2719
+ }), busy ? null : /* @__PURE__ */ jsx(Dialog.Close, {
2720
+ "data-domainkit-part": "dialog-cancel",
2721
+ children: messages.cancel
2722
+ })]
2723
+ })
2724
+ ]
2725
+ })]
2726
+ })]
2727
+ });
2728
+ }
2729
+ /**
2730
+ * A connected domain, with the prompt card's anatomy: the provider's mark, the name the customer
2731
+ * knows it by, where the connection stands, and the one action that ends it.
2732
+ */
2733
+ function Card({ controller, onCleaned, ...props }) {
2339
2734
  const { messages } = useDomainKit();
2340
2735
  const readOnly = useReadOnly();
2341
2736
  const state = controller.state;
2342
2737
  const snapshot = controller.snapshot;
2343
2738
  const provider = controller.providers.find((entry) => entry.id === snapshot?.provider);
2739
+ const standing = state._tag === "Reconnect" ? messages.needsReconnect : messages.connected;
2344
2740
  return usePart("div", props, { status: state._tag }, {
2345
2741
  children: /* @__PURE__ */ jsxs(Fragment, { children: [
2346
2742
  /* @__PURE__ */ jsxs("div", {
2347
2743
  "data-domainkit-part": "connected-identity",
2348
- children: [provider === void 0 ? null : /* @__PURE__ */ jsx(Mark, { provider }), /* @__PURE__ */ jsx(Status$2, { controller })]
2744
+ children: [
2745
+ provider === void 0 ? null : /* @__PURE__ */ jsx(Mark, { provider }),
2746
+ /* @__PURE__ */ jsx("span", {
2747
+ "data-domainkit-part": "host-name",
2748
+ children: provider?.name ?? snapshot?.provider ?? ""
2749
+ }),
2750
+ /* @__PURE__ */ jsx("span", {
2751
+ "data-domainkit-part": "host-statement",
2752
+ children: standing
2753
+ })
2754
+ ]
2349
2755
  }),
2350
- readOnly ? null : /* @__PURE__ */ jsxs("div", {
2756
+ readOnly ? null : /* @__PURE__ */ jsx("div", {
2351
2757
  "data-domainkit-part": "connected-actions",
2352
- children: [/* @__PURE__ */ jsx("button", {
2353
- "data-domainkit-part": "disconnect-trigger",
2354
- disabled: state._tag === "Submitting",
2355
- onClick: controller.detach,
2356
- type: "button",
2357
- children: messages.detach
2358
- }), /* @__PURE__ */ jsx("button", {
2359
- "data-domainkit-part": "disconnect-action",
2360
- disabled: state._tag === "Submitting",
2361
- onClick: controller.disconnect,
2362
- type: "button",
2363
- children: messages.disconnect
2364
- })]
2758
+ children: /* @__PURE__ */ jsx(DisconnectDialog, {
2759
+ controller,
2760
+ ...onCleaned === void 0 ? {} : { onCleaned }
2761
+ })
2365
2762
  }),
2366
2763
  /* @__PURE__ */ jsx(Outcome$2, { controller })
2367
2764
  ] }),
@@ -2369,16 +2766,18 @@ function Card({ controller, ...props }) {
2369
2766
  });
2370
2767
  }
2371
2768
  /** Connection on its own: the card once connected, the prompt until then. */
2372
- function Flow$2({ connect, domain, returnTo, ...props }) {
2769
+ function Flow$2({ connect, domain, onCleaned, returnTo, ...props }) {
2373
2770
  const controller = useController$2({
2374
2771
  domain,
2375
2772
  ...returnTo === void 0 ? {} : { returnTo }
2376
2773
  });
2377
- const state = controller.state;
2378
2774
  return /* @__PURE__ */ jsx(Root$2, {
2379
2775
  controller,
2380
2776
  ...props,
2381
- children: state._tag === "Connected" ? /* @__PURE__ */ jsx(Card, { controller }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Prompt, {
2777
+ children: holdsConnection(controller) ? /* @__PURE__ */ jsx(Card, {
2778
+ controller,
2779
+ ...onCleaned === void 0 ? {} : { onCleaned }
2780
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Prompt, {
2382
2781
  controller,
2383
2782
  ...connect === void 0 ? {} : { connect }
2384
2783
  }), answeredInPlace(controller) ? null : /* @__PURE__ */ jsx(Outcome$2, { controller })] })
@@ -2462,13 +2861,16 @@ function Flow$1({ domain, onApplied, requirements, trigger, ...props }) {
2462
2861
  requirements,
2463
2862
  ...onApplied === void 0 ? {} : { onApplied }
2464
2863
  });
2864
+ const [open, setOpen] = useState(false);
2465
2865
  return /* @__PURE__ */ jsxs(Root$1, {
2466
2866
  controller,
2467
2867
  ...props,
2468
2868
  children: [/* @__PURE__ */ jsx(Dialog$1, {
2469
2869
  controller,
2870
+ onOpenChange: setOpen,
2871
+ open,
2470
2872
  ...trigger === void 0 ? {} : { trigger }
2471
- }), /* @__PURE__ */ jsx(Outcome$1, { controller })]
2873
+ }), open ? null : /* @__PURE__ */ jsx(Outcome$1, { controller })]
2472
2874
  });
2473
2875
  }
2474
2876
  //#endregion
@@ -2666,7 +3068,7 @@ function Outcome({ children, controller, ...props }) {
2666
3068
  const words = outcome(state.error, messages);
2667
3069
  return /* @__PURE__ */ jsx(Provider, {
2668
3070
  value: {
2669
- description: words.description,
3071
+ description: words.description ?? "",
2670
3072
  layout: props.layout ?? "card",
2671
3073
  retry: controller.retry,
2672
3074
  retryPart: "verification-retry",
@@ -2723,9 +3125,25 @@ function Status({ controller, render, ...props }) {
2723
3125
  //#endregion
2724
3126
  //#region src/domain.tsx
2725
3127
  var domain_exports = /* @__PURE__ */ __exportAll({ Flow: () => Flow });
2726
- function DefaultConnection({ connect, controller }) {
2727
- const stated = controller.state._tag === "Disconnected" && hostProvider(controller) !== null;
2728
- return controller.state._tag === "Connected" ? /* @__PURE__ */ jsx(Card, { controller }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3128
+ /** Built from primitives alone, so the effect that announces it depends on exactly those. */
3129
+ const flowState = (input) => ({
3130
+ applied: input.receipt !== null,
3131
+ connected: input.connected,
3132
+ connection: input.status,
3133
+ offering: input.offering,
3134
+ provider: input.provider,
3135
+ provisioning: input.planning,
3136
+ receiptId: input.receipt === null ? null : Receipt.ReceiptId.make(input.receipt)
3137
+ });
3138
+ function DefaultConnection({ connect, controller, onCleaned }) {
3139
+ const disconnected = controller.state._tag === "Disconnected";
3140
+ const host = hostProvider(controller);
3141
+ if (disconnected && host === null && !offering(controller, connect)) return null;
3142
+ const stated = disconnected && host !== null && offering(controller, connect);
3143
+ return holdsConnection(controller) ? /* @__PURE__ */ jsx(Card, {
3144
+ controller,
3145
+ ...onCleaned === void 0 ? {} : { onCleaned }
3146
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2729
3147
  stated ? null : /* @__PURE__ */ jsx(Status$2, { controller }),
2730
3148
  /* @__PURE__ */ jsx(Prompt, {
2731
3149
  connect,
@@ -2734,47 +3152,24 @@ function DefaultConnection({ connect, controller }) {
2734
3152
  answeredInPlace(controller) ? null : /* @__PURE__ */ jsx(Outcome$2, { controller })
2735
3153
  ] });
2736
3154
  }
2737
- function DefaultActions({ cleanup, connection, provisioning }) {
2738
- const { capabilities, messages } = useDomainKit();
3155
+ function DefaultActions({ connection, onOpenChange, open, provisioning }) {
2739
3156
  const readOnly = useReadOnly();
2740
- const icons = useIcons();
3157
+ const { capabilities } = useDomainKit();
2741
3158
  const connected = connection.state._tag === "Connected";
2742
3159
  if (readOnly) return null;
2743
- const hasReceipt = connection.snapshot?.lastReceiptId != null;
2744
- return /* @__PURE__ */ jsxs(Fragment, { children: [capabilities.includes("provisioning") && connected ? /* @__PURE__ */ jsxs(Fragment, { children: [
2745
- /* @__PURE__ */ jsx("button", {
2746
- "data-domainkit-part": "plan-trigger",
2747
- disabled: provisioning.state._tag === "Planning",
2748
- onClick: provisioning.plan,
2749
- type: "button",
2750
- children: messages.reviewChanges
2751
- }),
2752
- /* @__PURE__ */ jsx(Status$1, { controller: provisioning }),
2753
- /* @__PURE__ */ jsx(Actions, { controller: provisioning }),
2754
- /* @__PURE__ */ jsx(Outcome$1, { controller: provisioning })
2755
- ] }) : null, capabilities.includes("cleanup") && connected && hasReceipt ? /* @__PURE__ */ jsxs(Fragment, { children: [
2756
- /* @__PURE__ */ jsxs("button", {
2757
- "data-domainkit-part": "cleanup-trigger",
2758
- disabled: cleanup.state._tag === "Planning",
2759
- onClick: cleanup.plan,
2760
- type: "button",
2761
- children: [/* @__PURE__ */ jsx("span", {
2762
- "aria-hidden": "true",
2763
- "data-icon": "inline-start",
2764
- children: icons.close
2765
- }), messages.cleanUp]
2766
- }),
2767
- /* @__PURE__ */ jsx(Status$3, { controller: cleanup }),
2768
- /* @__PURE__ */ jsx(Actions$1, { controller: cleanup }),
2769
- /* @__PURE__ */ jsx(Outcome$3, { controller: cleanup })
2770
- ] }) : null] });
3160
+ if (!capabilities.includes("provisioning") || !connected) return null;
3161
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Dialog$1, {
3162
+ controller: provisioning,
3163
+ onOpenChange,
3164
+ open
3165
+ }), open ? null : /* @__PURE__ */ jsx(Outcome$1, { controller: provisioning })] });
2771
3166
  }
2772
3167
  /**
2773
3168
  * The whole lifecycle for one domain: connect, review, apply, observe, clean up. The flow renders
2774
3169
  * only what the transport declares, and every part of it is a slot with a default. It adds no
2775
3170
  * layout containers of its own, so a host's grid can place the slot output directly.
2776
3171
  */
2777
- function Flow({ connect = "detected", domain, onApplied, onCleaned, readOnly, requirements, returnTo, slots = {}, ...props }) {
3172
+ function Flow({ connect = "detected", domain, onApplied, onCleaned, onState, readOnly, requirements, returnTo, review = "auto", slots = {}, ...props }) {
2778
3173
  const { capabilities } = useDomainKit();
2779
3174
  const inherited = useReadOnly();
2780
3175
  const connection = useController$2({
@@ -2803,17 +3198,70 @@ function Flow({ connect = "detected", domain, onApplied, onCleaned, readOnly, re
2803
3198
  requirements
2804
3199
  });
2805
3200
  const readiness = verification.readiness;
2806
- return usePart("div", props, {
2807
- connection: connection.state._tag,
2808
- provisioning: provisioning.state._tag
2809
- }, {
3201
+ const status = connection.state._tag;
3202
+ const planning = provisioning.state._tag;
3203
+ const [reviewing, setReviewing] = useState(false);
3204
+ const [reviewed, setReviewed] = useState(domain);
3205
+ if (reviewed !== domain) {
3206
+ setReviewed(domain);
3207
+ setReviewing(false);
3208
+ }
3209
+ const established = connection.established;
3210
+ const answered = useRef(established);
3211
+ const buildPlan = provisioning.plan;
3212
+ useEffect(() => {
3213
+ if (answered.current === established) return;
3214
+ answered.current = established;
3215
+ if (review === "manual") return;
3216
+ buildPlan();
3217
+ setReviewing(true);
3218
+ }, [
3219
+ buildPlan,
3220
+ established,
3221
+ review
3222
+ ]);
3223
+ const connected = holdsConnection(connection);
3224
+ const offering$1 = offering(connection, connect);
3225
+ const provider = connection.snapshot?.provider ?? hostProvider(connection)?.id ?? null;
3226
+ const receipt = connection.snapshot?.lastReceiptId ?? null;
3227
+ const state = flowState({
3228
+ connected,
3229
+ offering: offering$1,
3230
+ planning,
3231
+ provider,
3232
+ receipt,
3233
+ status
3234
+ });
3235
+ const announce = useRef(onState);
3236
+ useEffect(() => {
3237
+ announce.current = onState;
3238
+ });
3239
+ useEffect(() => {
3240
+ announce.current?.(flowState({
3241
+ connected,
3242
+ offering: offering$1,
3243
+ planning,
3244
+ provider,
3245
+ receipt,
3246
+ status
3247
+ }));
3248
+ }, [
3249
+ connected,
3250
+ offering$1,
3251
+ planning,
3252
+ provider,
3253
+ receipt,
3254
+ status
3255
+ ]);
3256
+ return usePart("div", props, state, {
2810
3257
  children: /* @__PURE__ */ jsxs(ReadOnly, {
2811
3258
  value: readOnly ?? inherited,
2812
3259
  children: [
2813
3260
  !capabilities.includes("connection") ? null : slots.connection === void 0 ? /* @__PURE__ */ jsx(DefaultConnection, {
2814
3261
  connect,
2815
3262
  controller: connection,
2816
- domain
3263
+ domain,
3264
+ ...onCleaned === void 0 ? {} : { onCleaned }
2817
3265
  }) : slots.connection({
2818
3266
  connect,
2819
3267
  controller: connection,
@@ -2836,6 +3284,8 @@ function Flow({ connect = "detected", domain, onApplied, onCleaned, readOnly, re
2836
3284
  cleanup,
2837
3285
  connection,
2838
3286
  domain,
3287
+ onOpenChange: setReviewing,
3288
+ open: reviewing,
2839
3289
  provisioning
2840
3290
  }) : slots.actions({
2841
3291
  cleanup,