@domainkit/react 0.9.0 → 0.10.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
@@ -2,7 +2,7 @@
2
2
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
3
3
  import { DnsRecord, DomainKit, Reason, Receipt } from "domainkit";
4
4
  import * as Effect from "effect/Effect";
5
- import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
+ import { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
6
  import * as Data from "effect/Data";
7
7
  import { Transport } from "domainkit/client";
8
8
  import { useRender } from "@base-ui/react/use-render";
@@ -84,7 +84,12 @@ const defaultIcons = {
84
84
  cx: "12",
85
85
  cy: "12",
86
86
  r: "9"
87
- }), /* @__PURE__ */ jsx("path", { d: "m8.5 12.5 2.5 2.5 4.5-5" })] })
87
+ }), /* @__PURE__ */ jsx("path", { d: "m8.5 12.5 2.5 2.5 4.5-5" })] }),
88
+ warning: /* @__PURE__ */ jsxs(Glyph, { children: [
89
+ /* @__PURE__ */ jsx("path", { d: "M10.3 3.9 2.4 17.5a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" }),
90
+ /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
91
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
92
+ ] })
88
93
  };
89
94
  const IconsContext = createContext(defaultIcons);
90
95
  function IconsProvider({ children, icons }) {
@@ -105,7 +110,8 @@ function useIcons() {
105
110
  var messages_exports = /* @__PURE__ */ __exportAll({
106
111
  english: () => english,
107
112
  failure: () => failure,
108
- merge: () => merge
113
+ merge: () => merge,
114
+ outcome: () => outcome
109
115
  });
110
116
  const entity = {
111
117
  approval: "approval",
@@ -135,6 +141,10 @@ const recordTypes = {
135
141
  SRV: "SRV",
136
142
  TXT: "TXT"
137
143
  };
144
+ /** The provider the customer acted on, or a neutral stand-in when the flow does not know it. */
145
+ const named = (provider) => provider ?? "The provider";
146
+ /** The domain the failure is about, or a neutral stand-in. */
147
+ const where = (domain) => domain ?? "this domain";
138
148
  /** `accountId` reads as "Account id" until a host names the field itself. */
139
149
  const humanize = (name) => {
140
150
  const spaced = name.replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2").replaceAll(/[_-]+/g, " ").trim();
@@ -142,7 +152,9 @@ const humanize = (name) => {
142
152
  };
143
153
  const english = {
144
154
  connect: "Connect",
145
- connectWith: (method) => method,
155
+ methodToken: "Connect with an API token",
156
+ methodOAuth: (provider) => `Continue with ${provider}`,
157
+ methodIntegration: (provider) => `Install the ${provider} integration`,
146
158
  disconnect: "Disconnect",
147
159
  detach: "Detach domain",
148
160
  cancel: "Cancel",
@@ -155,6 +167,8 @@ const english = {
155
167
  checkDns: "Check DNS",
156
168
  checkAgain: "Check again",
157
169
  moreActions: "More actions",
170
+ moreOptions: "Need an account id?",
171
+ useAnotherProvider: "Use a different provider",
158
172
  copy: "Copy",
159
173
  copied: "Copied",
160
174
  copyZone: "Copy zone file",
@@ -175,7 +189,8 @@ const english = {
175
189
  cleaning: "Removing records…",
176
190
  observing: "Checking DNS…",
177
191
  connectTitle: (provider) => `Connect ${provider}`,
178
- connectAnyTitle: "Connect your DNS provider",
192
+ connectAnyTitle: "Connect a DNS provider",
193
+ hostOwnsZone: "Owns DNS for this domain.",
179
194
  connectDescription: (domain) => `Authorize DNS changes for ${domain}.`,
180
195
  connectedTo: (provider) => `${provider} connected`,
181
196
  notConnected: "No DNS provider is connected.",
@@ -240,9 +255,11 @@ const english = {
240
255
  recordType: (record) => record._tag === "Opaque" ? record.type : recordTypes[record._tag],
241
256
  applied: (receipt) => receipt.status === "complete" ? "DNS records added." : "Some DNS records were not added.",
242
257
  cleaned: (receipt) => receipt.status === "complete" ? "DNS records removed." : "Some DNS records were not removed.",
243
- partiallyApplied: "Some DNS changes failed. Review and retry safely.",
244
- partiallyCleaned: "Some DNS records could not be deleted. Review and retry safely.",
245
- outcome: (outcome) => {
258
+ partiallyAppliedTitle: "Some DNS changes did not land",
259
+ partiallyApplied: "Review what applied, then retry. Retrying is safe.",
260
+ partiallyCleanedTitle: "Some DNS records are still there",
261
+ partiallyCleaned: "Review what is left, then retry. Retrying is safe.",
262
+ receiptOutcome: (outcome) => {
246
263
  switch (outcome._tag) {
247
264
  case "Applied": return "Done";
248
265
  case "Skipped": return "Skipped";
@@ -292,49 +309,102 @@ const english = {
292
309
  nextCheckAt: (at) => `Next check ${at}`,
293
310
  hostEvidence: "Other checks",
294
311
  noEvidence: "Nothing has been checked yet.",
295
- invalidInput: (reason) => reason.field === void 0 ? reason.message : `${humanize(reason.field)}: ${reason.message}`,
296
- unauthenticated: () => "The provider rejected these credentials.",
297
- forbidden: () => "You do not have access to this domain.",
298
- notFound: (reason) => `That ${entity[reason.entity]} no longer exists.`,
299
- conflict: (reason) => reason.operations.length === 1 ? "One DNS record conflicts with what is already in the zone." : `${reason.operations.length} DNS records conflict with what is already in the zone.`,
300
- stale: () => "The zone changed while you were reviewing. Build a new plan.",
301
- expired: (reason) => `That ${expiredEntity[reason.entity]} expired. Start again.`,
302
- busy: () => "Another change is running. Try again in a moment.",
303
- providerRejected: (reason) => `${reason.provider} refused the change: ${reason.message}`,
304
- providerUnavailable: (reason) => `${reason.provider} is unreachable right now.`,
305
- providerConflict: (reason) => `${reason.provider} already holds a conflicting record.`,
306
- unsupported: (reason) => `${reason.provider} cannot ${reason.operation}.`,
307
- reconnect: (reason) => `Reconnect ${reason.provider} to continue.`,
308
- storageFailed: () => "The change could not be saved. Try again.",
309
- cryptoFailed: () => "A stored credential could not be read.",
310
- resolverFailed: (reason) => `DNS lookups through ${reason.resolver} failed.`
312
+ invalidInput: (reason) => ({
313
+ description: reason.message,
314
+ title: reason.field === void 0 ? "Check what you entered" : `Check the ${humanize(reason.field).toLowerCase()}`
315
+ }),
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
+ }),
320
+ forbidden: (_reason, context) => ({
321
+ description: `The connection needs permission to edit DNS for ${where(context.domain)}.`,
322
+ title: "This connection can't change DNS here"
323
+ }),
324
+ notFound: (reason) => ({
325
+ description: "Reload the page and start this step again.",
326
+ title: `That ${entity[reason.entity]} no longer exists`
327
+ }),
328
+ conflict: (reason) => ({
329
+ description: "Review what the zone already holds, then build a new plan.",
330
+ title: reason.operations.length === 1 ? "A record is in the way" : `${reason.operations.length} records are in the way`
331
+ }),
332
+ stale: () => ({
333
+ description: "Review the new plan before you apply it.",
334
+ title: "The zone changed since you reviewed"
335
+ }),
336
+ expired: (reason) => ({
337
+ description: "Start this step again.",
338
+ title: `That ${expiredEntity[reason.entity]} expired`
339
+ }),
340
+ busy: () => ({
341
+ description: "Wait for it to finish, then try again.",
342
+ title: "Another change is running"
343
+ }),
344
+ providerRejected: (reason, context) => ({
345
+ description: reason.message,
346
+ title: `${named(context.provider ?? reason.provider)} refused the change`
347
+ }),
348
+ providerUnavailable: (reason, context) => ({
349
+ description: "Nothing changed. Try again in a minute.",
350
+ title: `${named(context.provider ?? reason.provider)} isn't responding`
351
+ }),
352
+ providerConflict: (reason, context) => ({
353
+ description: "Remove or rename that record at the provider, then try again.",
354
+ title: `${named(context.provider ?? reason.provider)} already holds a conflicting record`
355
+ }),
356
+ unsupported: (reason, context) => ({
357
+ description: "Nothing changed. This step needs a provider that can do it.",
358
+ title: `${named(context.provider ?? reason.provider)} cannot ${reason.operation}`
359
+ }),
360
+ reconnect: (reason, context) => ({
361
+ description: "The stored credential no longer works. Connect again to continue.",
362
+ title: `Reconnect ${named(context.provider ?? reason.provider)}`
363
+ }),
364
+ storageFailed: () => ({
365
+ description: "Nothing changed. Try again.",
366
+ title: "The change could not be saved"
367
+ }),
368
+ cryptoFailed: () => ({
369
+ description: "Connect the provider again to replace it.",
370
+ title: "A stored credential could not be read"
371
+ }),
372
+ resolverFailed: (reason) => ({
373
+ description: "The lookup failed, which says nothing about your DNS. Try again in a moment.",
374
+ title: `DNS lookups through ${reason.resolver} failed`
375
+ })
311
376
  };
312
377
  const merge = (overrides = {}) => ({
313
378
  ...english,
314
379
  ...overrides
315
380
  });
316
- /** The user-facing sentence for a failure, chosen by the error's reason. */
317
- const failure = (error, catalog) => {
381
+ /** The title and description for a failure, chosen by the error's reason. */
382
+ const outcome = (error, catalog, context = {}) => {
318
383
  const reason = error.reason;
319
384
  switch (reason._tag) {
320
- case "InvalidInput": return catalog.invalidInput(reason);
321
- case "Unauthenticated": return catalog.unauthenticated(reason);
322
- case "Forbidden": return catalog.forbidden(reason);
323
- case "NotFound": return catalog.notFound(reason);
324
- case "Conflict": return catalog.conflict(reason);
325
- case "Stale": return catalog.stale(reason);
326
- case "Expired": return catalog.expired(reason);
327
- case "Busy": return catalog.busy(reason);
328
- case "ProviderRejected": return catalog.providerRejected(reason);
329
- case "ProviderUnavailable": return catalog.providerUnavailable(reason);
330
- case "ProviderConflict": return catalog.providerConflict(reason);
331
- case "Unsupported": return catalog.unsupported(reason);
332
- case "Reconnect": return catalog.reconnect(reason);
333
- case "StorageFailed": return catalog.storageFailed(reason);
334
- case "CryptoFailed": return catalog.cryptoFailed(reason);
335
- case "ResolverFailed": return catalog.resolverFailed(reason);
385
+ case "InvalidInput": return catalog.invalidInput(reason, context);
386
+ case "Unauthenticated": return catalog.unauthenticated(reason, context);
387
+ case "Forbidden": return catalog.forbidden(reason, context);
388
+ case "NotFound": return catalog.notFound(reason, context);
389
+ case "Conflict": return catalog.conflict(reason, context);
390
+ case "Stale": return catalog.stale(reason, context);
391
+ case "Expired": return catalog.expired(reason, context);
392
+ case "Busy": return catalog.busy(reason, context);
393
+ case "ProviderRejected": return catalog.providerRejected(reason, context);
394
+ case "ProviderUnavailable": return catalog.providerUnavailable(reason, context);
395
+ case "ProviderConflict": return catalog.providerConflict(reason, context);
396
+ case "Unsupported": return catalog.unsupported(reason, context);
397
+ case "Reconnect": return catalog.reconnect(reason, context);
398
+ case "StorageFailed": return catalog.storageFailed(reason, context);
399
+ case "CryptoFailed": return catalog.cryptoFailed(reason, context);
400
+ case "ResolverFailed": return catalog.resolverFailed(reason, context);
336
401
  }
337
402
  };
403
+ /** The same failure as one sentence, for a host that renders text rather than a card. */
404
+ const failure = (error, catalog, context = {}) => {
405
+ const { description, title } = outcome(error, catalog, context);
406
+ return `${title}. ${description}`;
407
+ };
338
408
  //#endregion
339
409
  //#region src/theme.ts
340
410
  var theme_exports = /* @__PURE__ */ __exportAll({ toStyle: () => toStyle });
@@ -352,7 +422,8 @@ const variables = {
352
422
  radius: "--domainkit-radius",
353
423
  shadow: "--domainkit-shadow",
354
424
  success: "--domainkit-success",
355
- text: "--domainkit-text"
425
+ text: "--domainkit-text",
426
+ warning: "--domainkit-warning"
356
427
  };
357
428
  function toStyle(theme = {}) {
358
429
  return Object.fromEntries(Object.entries(theme).flatMap(([key, value]) => value === void 0 ? [] : [[variables[key], value]]));
@@ -361,13 +432,13 @@ function toStyle(theme = {}) {
361
432
  //#region src/domain-kit.tsx
362
433
  var domain_kit_exports = /* @__PURE__ */ __exportAll({
363
434
  ReadOnly: () => ReadOnly,
364
- Root: () => Root$6,
435
+ Root: () => Root$7,
365
436
  useCapabilities: () => useCapabilities,
366
437
  useDomainKit: () => useDomainKit,
367
438
  useReadOnly: () => useReadOnly,
368
439
  useTransport: () => useTransport
369
440
  });
370
- const Context = createContext(null);
441
+ const Context$1 = createContext(null);
371
442
  const navigateInBrowser = (url) => {
372
443
  if (typeof window === "undefined") throw new Error("DomainKit provider authorization requires a browser or a `navigate` prop");
373
444
  window.location.assign(url);
@@ -417,7 +488,7 @@ const usePortalContainer = (container) => {
417
488
  const getSnapshot = useCallback(() => resolvePortalContainer(container, ownerDocument), [container, ownerDocument]);
418
489
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
419
490
  };
420
- function Root$6({ children, colorScheme = "inherit", icons, marks = {}, messages, navigate = navigateInBrowser, onEvent, portalContainer = null, readOnly = false, revision = 0, theme, transport, ...props }) {
491
+ function Root$7({ children, colorScheme = "inherit", icons, marks = {}, messages, navigate = navigateInBrowser, onEvent, portalContainer = null, readOnly = false, revision = 0, theme, transport, ...props }) {
421
492
  const { capabilities, transport: stable } = useStableTransport(transport);
422
493
  const onEventRef = useRef(onEvent);
423
494
  useLayoutEffect(() => {
@@ -450,7 +521,7 @@ function Root$6({ children, colorScheme = "inherit", icons, marks = {}, messages
450
521
  "data-domainkit-root": "",
451
522
  style: themeStyle
452
523
  });
453
- return /* @__PURE__ */ jsx(Context.Provider, {
524
+ return /* @__PURE__ */ jsx(Context$1.Provider, {
454
525
  value,
455
526
  children: /* @__PURE__ */ jsx(IconsProvider, {
456
527
  ...icons === void 0 ? {} : { icons },
@@ -459,7 +530,7 @@ function Root$6({ children, colorScheme = "inherit", icons, marks = {}, messages
459
530
  });
460
531
  }
461
532
  function useDomainKit() {
462
- const value = useContext(Context);
533
+ const value = useContext(Context$1);
463
534
  if (value === null) throw new Error("DomainKit components must be rendered inside DomainKit.Root");
464
535
  return value;
465
536
  }
@@ -760,6 +831,155 @@ function useAttempt(options) {
760
831
  };
761
832
  }
762
833
  //#endregion
834
+ //#region src/outcome.tsx
835
+ /**
836
+ * What a flow says when a step ends badly: media, title, description, and the action that follows.
837
+ * Every flow's `Outcome` binds its controller to these parts, so a failure reads the same way in
838
+ * the connect dialog, the review dialog, and the verification popover.
839
+ *
840
+ * <Connect.Outcome controller={connection} /> the default composition
841
+ *
842
+ * <Connect.Outcome controller={connection}> a composition of your own
843
+ * <Outcome.Header>
844
+ * <Outcome.Media><MyIcon /></Outcome.Media>
845
+ * <Outcome.Title />
846
+ * <Outcome.Description />
847
+ * </Outcome.Header>
848
+ * <Outcome.Content />
849
+ * </Connect.Outcome>
850
+ */
851
+ var outcome_exports = /* @__PURE__ */ __exportAll({
852
+ Composition: () => Composition,
853
+ Content: () => Content,
854
+ Description: () => Description,
855
+ Header: () => Header$1,
856
+ Media: () => Media,
857
+ Provider: () => Provider,
858
+ Root: () => Root$6,
859
+ Title: () => Title,
860
+ useOutcome: () => useOutcome
861
+ });
862
+ const fallback = {
863
+ description: "",
864
+ layout: "card",
865
+ retry: null,
866
+ retryPart: "flow-retry",
867
+ title: "",
868
+ tone: "danger"
869
+ };
870
+ const Context = createContext(null);
871
+ /** What the surrounding flow resolved, for a part of your own inside `X.Outcome`. */
872
+ function useOutcome() {
873
+ return useContext(Context) ?? fallback;
874
+ }
875
+ /** Binds a resolved outcome to the parts below it. Flows render this; hosts rarely do. */
876
+ function Provider({ children, value }) {
877
+ return /* @__PURE__ */ jsx(Context.Provider, {
878
+ value,
879
+ children
880
+ });
881
+ }
882
+ /**
883
+ * The outcome itself. It announces, so a customer reading with assistive tech hears it. A `tone`
884
+ * or `layout` of its own reaches every part below it, so the glyph and the words agree with the
885
+ * root's own attributes.
886
+ */
887
+ function Root$6({ layout, tone, ...props }) {
888
+ const value = useOutcome();
889
+ const resolved = {
890
+ ...value,
891
+ layout: layout ?? value.layout,
892
+ tone: tone ?? value.tone
893
+ };
894
+ const element = usePart("div", props, {
895
+ layout: resolved.layout,
896
+ tone: resolved.tone
897
+ }, {
898
+ "data-domainkit-part": "outcome",
899
+ "data-layout": resolved.layout,
900
+ "data-tone": resolved.tone,
901
+ role: "alert"
902
+ });
903
+ return /* @__PURE__ */ jsx(Provider, {
904
+ value: resolved,
905
+ children: element
906
+ });
907
+ }
908
+ /** Media, title, and description together, so the action below them can move on its own. */
909
+ function Header$1(props) {
910
+ const { layout, tone } = useOutcome();
911
+ return usePart("div", props, {
912
+ layout,
913
+ tone
914
+ }, { "data-domainkit-part": "outcome-header" });
915
+ }
916
+ /** The glyph for the tone. Children replace it; `icons` on `DomainKit.Root` replaces the set. */
917
+ function Media({ variant = "icon", ...props }) {
918
+ const { layout, tone } = useOutcome();
919
+ const icons = useIcons();
920
+ const glyph = tone === "success" ? icons.success : tone === "warning" ? icons.warning : icons.failure;
921
+ return usePart("div", props, {
922
+ layout,
923
+ tone
924
+ }, {
925
+ "aria-hidden": "true",
926
+ children: glyph,
927
+ "data-domainkit-part": "outcome-media",
928
+ "data-variant": variant
929
+ });
930
+ }
931
+ /** The heading from the catalog. Children win, for a host that words it itself. */
932
+ function Title(props) {
933
+ const { layout, title, tone } = useOutcome();
934
+ return usePart("div", props, {
935
+ layout,
936
+ tone
937
+ }, {
938
+ children: title,
939
+ "data-domainkit-part": "outcome-title"
940
+ });
941
+ }
942
+ /** What to do about it, from the catalog. */
943
+ function Description(props) {
944
+ const { description, layout, tone } = useOutcome();
945
+ return usePart("p", props, {
946
+ layout,
947
+ tone
948
+ }, {
949
+ children: description,
950
+ "data-domainkit-part": "outcome-description"
951
+ });
952
+ }
953
+ /**
954
+ * The actions under the header. Without children it is the flow's retry, and a flow that allows
955
+ * none renders nothing: read-only hides every control that would start a write again.
956
+ */
957
+ function Content(props) {
958
+ const { messages } = useDomainKit();
959
+ const { layout, retry, retryPart, tone } = useOutcome();
960
+ const element = usePart("div", props, {
961
+ layout,
962
+ tone
963
+ }, {
964
+ children: retry === null ? null : /* @__PURE__ */ jsx("button", {
965
+ "data-domainkit-part": retryPart,
966
+ onClick: retry,
967
+ type: "button",
968
+ children: messages.retry
969
+ }),
970
+ "data-domainkit-part": "outcome-content"
971
+ });
972
+ return retry === null && props.children === void 0 ? null : element;
973
+ }
974
+ /** Media, title, and description in a header, then the action. What `X.Outcome` renders alone. */
975
+ function Composition() {
976
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Header$1, { children: [
977
+ /* @__PURE__ */ jsx(Media, {}),
978
+ /* @__PURE__ */ jsx(Title, {}),
979
+ /* @__PURE__ */ jsx(Description, {})
980
+ ] }), /* @__PURE__ */ jsx(Content, {})] });
981
+ }
982
+ //#endregion
763
983
  //#region src/records.tsx
764
984
  var records_exports = /* @__PURE__ */ __exportAll({
765
985
  Body: () => Body$1,
@@ -1103,24 +1323,35 @@ function Status$4({ controller, kind, ...props }) {
1103
1323
  role: busy(state._tag) ? "status" : void 0
1104
1324
  });
1105
1325
  }
1106
- /** The failure sentence from the error's reason, with the retry the flow allows. */
1107
- function Outcome$4({ controller, kind, ...props }) {
1326
+ /**
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.
1329
+ */
1330
+ function Outcome$4({ children, controller, kind, ...props }) {
1108
1331
  const { messages } = useDomainKit();
1109
1332
  const readOnly = useReadOnly();
1110
1333
  const state = controller.state;
1111
1334
  const partial = state._tag === "Applied" && state.receipt.status === "partial";
1112
- const element = usePart("p", props, { status: state._tag }, {
1113
- children: state._tag === "Failure" ? /* @__PURE__ */ jsxs(Fragment, { children: [failure(state.error, messages), readOnly ? null : /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx("button", {
1114
- "data-domainkit-part": kind === "provisioning" ? "provisioning-retry" : "cleanup-retry",
1115
- onClick: controller.retry,
1116
- type: "button",
1117
- children: messages.retry
1118
- })] })] }) : partial ? kind === "provisioning" ? messages.partiallyApplied : messages.partiallyCleaned : null,
1119
- "data-domainkit-part": "flow-outcome",
1120
- "data-tone": "danger",
1121
- role: "alert"
1335
+ 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) : {
1338
+ description: kind === "provisioning" ? messages.partiallyApplied : messages.partiallyCleaned,
1339
+ title: kind === "provisioning" ? messages.partiallyAppliedTitle : messages.partiallyCleanedTitle
1340
+ };
1341
+ return /* @__PURE__ */ jsx(Provider, {
1342
+ value: {
1343
+ description: words.description,
1344
+ layout: props.layout ?? "card",
1345
+ retry: readOnly || state._tag !== "Failure" ? null : controller.retry,
1346
+ retryPart,
1347
+ title: words.title,
1348
+ tone: state._tag === "Failure" ? "danger" : "warning"
1349
+ },
1350
+ children: /* @__PURE__ */ jsx(Root$6, {
1351
+ ...props,
1352
+ children: children ?? /* @__PURE__ */ jsx(Composition, {})
1353
+ })
1122
1354
  });
1123
- return state._tag === "Failure" || partial ? element : null;
1124
1355
  }
1125
1356
  /**
1126
1357
  * Approve and Decline. Approve authorizes the digest and applies it; Decline records the refusal
@@ -1388,9 +1619,12 @@ var connect_exports = /* @__PURE__ */ __exportAll({
1388
1619
  Flow: () => Flow$2,
1389
1620
  Form: () => Form,
1390
1621
  Outcome: () => Outcome$2,
1622
+ Prompt: () => Prompt,
1391
1623
  Root: () => Root$2,
1392
1624
  State: () => State$1,
1393
1625
  Status: () => Status$2,
1626
+ answeredInPlace: () => answeredInPlace,
1627
+ hostProvider: () => hostProvider,
1394
1628
  useController: () => useController$2
1395
1629
  });
1396
1630
  const State$1 = Data.taggedEnum();
@@ -1403,9 +1637,9 @@ const snapshotOf = (state) => {
1403
1637
  case "Disconnected": return state.snapshot;
1404
1638
  case "Loading":
1405
1639
  case "Submitting":
1406
- case "SelectionRequired": return state.snapshot;
1407
- case "Redirecting":
1408
- case "Failure": return null;
1640
+ case "SelectionRequired":
1641
+ case "Failure": return state.snapshot;
1642
+ case "Redirecting": return null;
1409
1643
  }
1410
1644
  };
1411
1645
  const settled = (snapshot, discovery) => {
@@ -1435,6 +1669,7 @@ function useController$2({ domain, returnTo }) {
1435
1669
  snapshot: null
1436
1670
  });
1437
1671
  const lastCommand = useRef(null);
1672
+ const attempted = useRef(null);
1438
1673
  const [inspected, setInspected] = useState(domain);
1439
1674
  if (inspected !== domain) {
1440
1675
  setInspected(domain);
@@ -1448,7 +1683,12 @@ function useController$2({ domain, returnTo }) {
1448
1683
  setState(State$1.Loading({ snapshot: null }));
1449
1684
  }
1450
1685
  const onFailure = useCallback((error) => {
1451
- setState(State$1.Failure({ error }));
1686
+ setState(State$1.Failure({
1687
+ attempt: attempted.current,
1688
+ discovery: held.current.discovery,
1689
+ error,
1690
+ snapshot: held.current.snapshot
1691
+ }));
1452
1692
  emit(Event.Failed({
1453
1693
  domain,
1454
1694
  error
@@ -1457,6 +1697,7 @@ function useController$2({ domain, returnTo }) {
1457
1697
  const load = useCallback(() => {
1458
1698
  if (connection === void 0) return;
1459
1699
  lastCommand.current = null;
1700
+ attempted.current = null;
1460
1701
  held.current = {
1461
1702
  discovery: null,
1462
1703
  domain,
@@ -1521,8 +1762,9 @@ function useController$2({ domain, returnTo }) {
1521
1762
  emit,
1522
1763
  navigate
1523
1764
  ]);
1524
- const submit = useCallback((effect) => {
1765
+ const submit = useCallback((effect, attempt) => {
1525
1766
  if (held.current.domain !== domain) return;
1767
+ attempted.current = attempt;
1526
1768
  const command = () => {
1527
1769
  setState(State$1.Submitting({ snapshot: held.current.snapshot }));
1528
1770
  runner.run(effect, {
@@ -1550,7 +1792,10 @@ function useController$2({ domain, returnTo }) {
1550
1792
  domain,
1551
1793
  method,
1552
1794
  provider: input.provider
1553
- }));
1795
+ }), {
1796
+ method: input.method,
1797
+ provider: input.provider
1798
+ });
1554
1799
  }, [
1555
1800
  connection,
1556
1801
  domain,
@@ -1563,7 +1808,7 @@ function useController$2({ domain, returnTo }) {
1563
1808
  connectionId: input.connectionId,
1564
1809
  domain,
1565
1810
  ...input.zone === void 0 ? {} : { zone: input.zone }
1566
- }));
1811
+ }), null);
1567
1812
  }, [
1568
1813
  connection,
1569
1814
  domain,
@@ -1578,6 +1823,7 @@ function useController$2({ domain, returnTo }) {
1578
1823
  }, [reuse, state]);
1579
1824
  const release = useCallback((effect, event) => {
1580
1825
  if (held.current.domain !== domain) return;
1826
+ attempted.current = null;
1581
1827
  const command = () => {
1582
1828
  setState(State$1.Submitting({ snapshot: held.current.snapshot }));
1583
1829
  runner.run(effect, {
@@ -1634,7 +1880,7 @@ function useController$2({ domain, returnTo }) {
1634
1880
  connect,
1635
1881
  detach,
1636
1882
  disconnect,
1637
- discovery: state._tag === "Disconnected" ? state.discovery : null,
1883
+ discovery: state._tag === "Disconnected" || state._tag === "Failure" ? state.discovery : null,
1638
1884
  providers: snapshotOf(state)?.providers ?? [],
1639
1885
  refresh: load,
1640
1886
  retry,
@@ -1650,23 +1896,51 @@ function Root$2({ controller, ...props }) {
1650
1896
  "data-state": controller.state._tag
1651
1897
  });
1652
1898
  }
1653
- /** The failure sentence, chosen by the error's reason, plus the retry that reason allows. */
1654
- function Outcome$2({ controller, ...props }) {
1899
+ /**
1900
+ * Whether a failure is already answered where it was raised: beside the method the customer used.
1901
+ * A flow rendering its own `Outcome` beside the dialog skips it, so the failure announces once.
1902
+ */
1903
+ const answeredInPlace = (controller) => controller.state._tag === "Failure" && controller.state.attempt !== null;
1904
+ /**
1905
+ * The provider whose nameservers serve this domain, as the snapshot describes it. Discovery names
1906
+ * it by id; without a descriptor there is nothing to draw, so there is no host.
1907
+ */
1908
+ const hostProvider = (controller) => {
1909
+ const discovery = controller.discovery;
1910
+ if (discovery === null || discovery._tag !== "NotFound" || discovery.host === null) return null;
1911
+ const named = discovery.host.provider;
1912
+ return controller.providers.find((entry) => entry.id === named) ?? null;
1913
+ };
1914
+ /** The provider as the customer knows it, falling back to the id when the snapshot has no name. */
1915
+ const displayName = (controller, provider) => controller.providers.find((entry) => entry.id === provider)?.name ?? provider;
1916
+ /**
1917
+ * The last failure as a card: media, the catalog's title and description, and the retry the flow
1918
+ * allows. Children replace the composition and keep the binding.
1919
+ */
1920
+ function Outcome$2({ children, controller, ...props }) {
1655
1921
  const { messages } = useDomainKit();
1656
1922
  const readOnly = useReadOnly();
1657
1923
  const state = controller.state;
1658
- const element = usePart("p", props, { status: state._tag }, {
1659
- children: state._tag === "Failure" ? /* @__PURE__ */ jsxs(Fragment, { children: [failure(state.error, messages), readOnly ? null : /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx("button", {
1660
- "data-domainkit-part": "connection-retry",
1661
- onClick: controller.retry,
1662
- type: "button",
1663
- children: messages.retry
1664
- })] })] }) : null,
1665
- "data-domainkit-part": "flow-outcome",
1666
- "data-tone": "danger",
1667
- role: "alert"
1924
+ if (state._tag !== "Failure") return null;
1925
+ const acted = state.attempt === null ? void 0 : displayName(controller, state.attempt.provider);
1926
+ const words = outcome(state.error, messages, {
1927
+ ...controller.snapshot === null ? {} : { domain: controller.snapshot.domain },
1928
+ ...acted === void 0 ? {} : { provider: acted }
1929
+ });
1930
+ return /* @__PURE__ */ jsx(Provider, {
1931
+ value: {
1932
+ description: words.description,
1933
+ layout: props.layout ?? "card",
1934
+ retry: readOnly ? null : controller.retry,
1935
+ retryPart: "connection-retry",
1936
+ title: words.title,
1937
+ tone: "danger"
1938
+ },
1939
+ children: /* @__PURE__ */ jsx(Root$6, {
1940
+ ...props,
1941
+ children: children ?? /* @__PURE__ */ jsx(Composition, {})
1942
+ })
1668
1943
  });
1669
- return state._tag === "Failure" ? element : null;
1670
1944
  }
1671
1945
  function Status$2({ controller, ...props }) {
1672
1946
  const { messages } = useDomainKit();
@@ -1692,14 +1966,78 @@ function Status$2({ controller, ...props }) {
1692
1966
  });
1693
1967
  }
1694
1968
  /**
1969
+ * The one field a rejected command was about, if any. A provider that turns down credentials is
1970
+ * answering the secret it was given, and it names no field, so the first secret carries the
1971
+ * answer; anything else is about the request rather than one input.
1972
+ */
1973
+ const rejected = (reason, fields) => {
1974
+ if (reason === null) return null;
1975
+ if (reason._tag === "InvalidInput") return fields.find((field) => field.name === reason.field)?.name ?? null;
1976
+ if (reason._tag !== "Unauthenticated" && reason._tag !== "Forbidden") return null;
1977
+ return fields.find((field) => field.secret)?.name ?? null;
1978
+ };
1979
+ /**
1980
+ * 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.
1982
+ */
1983
+ function TokenField({ controller, docsUrl, field, invalid }) {
1984
+ const { messages } = useDomainKit();
1985
+ const icons = useIcons();
1986
+ const id = useId();
1987
+ return /* @__PURE__ */ jsxs("div", {
1988
+ "data-domainkit-part": "field",
1989
+ "data-invalid": invalid ? "" : void 0,
1990
+ "data-orientation": "vertical",
1991
+ children: [
1992
+ /* @__PURE__ */ jsxs("label", {
1993
+ "data-domainkit-part": "field-label",
1994
+ htmlFor: id,
1995
+ children: [messages.fieldLabel(field.name), field.required ? null : /* @__PURE__ */ jsx("span", {
1996
+ "data-domainkit-part": "field-optional",
1997
+ children: messages.optionalField
1998
+ })]
1999
+ }),
2000
+ /* @__PURE__ */ jsx("input", {
2001
+ "aria-invalid": invalid ? true : void 0,
2002
+ autoComplete: "off",
2003
+ "data-domainkit-part": "field-input",
2004
+ id,
2005
+ name: field.name,
2006
+ required: field.required,
2007
+ type: field.secret ? "password" : "text"
2008
+ }),
2009
+ docsUrl === null ? null : /* @__PURE__ */ jsxs("a", {
2010
+ "data-domainkit-part": "field-description",
2011
+ href: docsUrl,
2012
+ rel: "noreferrer",
2013
+ target: "_blank",
2014
+ children: [messages.getToken, /* @__PURE__ */ jsx("span", {
2015
+ "aria-hidden": "true",
2016
+ "data-icon": "inline-end",
2017
+ children: icons.external
2018
+ })]
2019
+ }),
2020
+ invalid ? /* @__PURE__ */ jsx("div", {
2021
+ "data-domainkit-part": "field-error",
2022
+ children: /* @__PURE__ */ jsx(Outcome$2, {
2023
+ controller,
2024
+ layout: "inline"
2025
+ })
2026
+ }) : null
2027
+ ]
2028
+ });
2029
+ }
2030
+ /**
1695
2031
  * One auth method, rendered from its descriptor. A token method draws an input per declared
1696
- * field, so a provider that needs an account id alongside a token needs no code here.
2032
+ * field, so a provider that needs an account id alongside a token needs no code here. The fields
2033
+ * a provider does not need sit behind a disclosure, so the common form is one field.
1697
2034
  */
1698
2035
  function Method({ controller, method, provider }) {
1699
2036
  const { messages } = useDomainKit();
1700
- const icons = useIcons();
1701
- const busy = controller.state._tag === "Submitting";
1702
- if (method.fields === null) return /* @__PURE__ */ jsx("button", {
2037
+ const state = controller.state;
2038
+ const busy = state._tag === "Submitting";
2039
+ const failed = state._tag === "Failure" && state.attempt !== null && state.attempt.provider === provider.id && state.attempt.method === method.kind;
2040
+ if (method.fields === null) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
1703
2041
  "data-domainkit-part": method.kind === "oauth" ? "oauth-connect" : "integration-connect",
1704
2042
  "data-provider": provider.id,
1705
2043
  disabled: busy,
@@ -1708,9 +2046,22 @@ function Method({ controller, method, provider }) {
1708
2046
  provider: provider.id
1709
2047
  }),
1710
2048
  type: "button",
1711
- children: messages.connectWith(method.label)
1712
- });
2049
+ children: method.kind === "oauth" ? messages.methodOAuth(provider.name) : messages.methodIntegration(provider.name)
2050
+ }), failed ? /* @__PURE__ */ jsx(Outcome$2, {
2051
+ controller,
2052
+ layout: "inline"
2053
+ }) : null] });
1713
2054
  const fields = method.fields;
2055
+ const answered = rejected(failed && state._tag === "Failure" ? state.error.reason : null, fields);
2056
+ const required = fields.filter((field) => field.required);
2057
+ const optional = fields.filter((field) => !field.required);
2058
+ const explains = (required.find((field) => field.secret) ?? required[0] ?? optional[0])?.name;
2059
+ const render = (field) => /* @__PURE__ */ jsx(TokenField, {
2060
+ controller,
2061
+ docsUrl: field.name === explains ? method.docsUrl : null,
2062
+ field,
2063
+ invalid: field.name === answered
2064
+ }, field.name);
1714
2065
  return /* @__PURE__ */ jsxs("form", {
1715
2066
  "data-domainkit-part": "token-connect",
1716
2067
  "data-provider": provider.id,
@@ -1729,48 +2080,58 @@ function Method({ controller, method, provider }) {
1729
2080
  });
1730
2081
  },
1731
2082
  children: [
1732
- fields.map((field) => /* @__PURE__ */ jsxs("label", {
1733
- "data-domainkit-part": "token-field",
1734
- children: [/* @__PURE__ */ jsxs("span", {
1735
- "data-domainkit-part": "token-field-label",
1736
- children: [messages.fieldLabel(field.name), field.required ? null : /* @__PURE__ */ jsx("span", {
1737
- "data-domainkit-part": "token-field-optional",
1738
- children: messages.optionalField
1739
- })]
1740
- }), /* @__PURE__ */ jsx("input", {
1741
- autoComplete: "off",
1742
- "data-domainkit-part": "token-field-input",
1743
- name: field.name,
1744
- required: field.required,
1745
- type: field.secret ? "password" : "text"
1746
- })]
1747
- }, field.name)),
1748
- method.docsUrl === null ? null : /* @__PURE__ */ jsxs("a", {
1749
- "data-domainkit-part": "token-docs",
1750
- href: method.docsUrl,
1751
- rel: "noreferrer",
1752
- target: "_blank",
1753
- children: [messages.getToken, /* @__PURE__ */ jsx("span", {
1754
- "aria-hidden": "true",
1755
- "data-icon": "inline-end",
1756
- children: icons.external
1757
- })]
2083
+ required.map(render),
2084
+ optional.length === 0 ? null : /* @__PURE__ */ jsxs("details", {
2085
+ "data-domainkit-part": "more-options",
2086
+ open: optional.some((field) => field.name === answered) || void 0,
2087
+ children: [/* @__PURE__ */ jsx("summary", {
2088
+ "data-domainkit-part": "more-options-trigger",
2089
+ children: messages.moreOptions
2090
+ }), optional.map(render)]
1758
2091
  }),
2092
+ failed && answered === null ? /* @__PURE__ */ jsx(Outcome$2, {
2093
+ controller,
2094
+ layout: "inline"
2095
+ }) : null,
1759
2096
  /* @__PURE__ */ jsx("button", {
1760
2097
  "data-domainkit-part": "token-submit",
1761
2098
  disabled: busy,
1762
2099
  type: "submit",
1763
- children: messages.connectWith(method.label)
2100
+ children: messages.methodToken
1764
2101
  })
1765
2102
  ]
1766
2103
  });
1767
2104
  }
1768
2105
  /**
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.
2108
+ */
2109
+ function Authentication({ controller, onOpen, open, provider }) {
2110
+ const methods = provider.methods.map((method) => /* @__PURE__ */ jsx(Method, {
2111
+ controller,
2112
+ method,
2113
+ provider
2114
+ }, `${provider.id}:${method.kind}`));
2115
+ return /* @__PURE__ */ jsxs("section", {
2116
+ "data-domainkit-part": "provider-authentication",
2117
+ "data-provider": provider.id,
2118
+ "data-state": open ? "open" : "closed",
2119
+ children: [onOpen === void 0 ? null : /* @__PURE__ */ jsxs("button", {
2120
+ "aria-expanded": open,
2121
+ "data-domainkit-part": "provider-heading",
2122
+ onClick: onOpen,
2123
+ type: "button",
2124
+ children: [/* @__PURE__ */ jsx(Mark, { provider }), /* @__PURE__ */ jsx("strong", { children: provider.name })]
2125
+ }), open ? methods : null]
2126
+ });
2127
+ }
2128
+ /**
1769
2129
  * Everything a disconnected domain can do: a connection discovery already found, the connections
1770
2130
  * this owner already has, then each provider's declared methods.
1771
2131
  */
1772
2132
  function Form({ controller, ...props }) {
1773
2133
  const { messages } = useDomainKit();
2134
+ const [opened, setOpened] = useState(null);
1774
2135
  const state = controller.state;
1775
2136
  const snapshot = controller.snapshot;
1776
2137
  const discovery = controller.discovery;
@@ -1781,6 +2142,14 @@ function Form({ controller, ...props }) {
1781
2142
  const resolved = discovery !== null && discovery._tag === "Resolved" ? discovery : null;
1782
2143
  const reusable = snapshot?.reusable ?? [];
1783
2144
  const providers = controller.providers;
2145
+ const host = hostProvider(controller);
2146
+ const others = providers.filter((provider) => provider.id !== host?.id);
2147
+ const authentication = (provider, open) => /* @__PURE__ */ jsx(Authentication, {
2148
+ controller,
2149
+ onOpen: () => setOpened(open ? "" : provider.id),
2150
+ open,
2151
+ provider
2152
+ }, provider.id);
1784
2153
  return usePart("div", props, { status: state._tag }, {
1785
2154
  children: /* @__PURE__ */ jsxs(Fragment, { children: [
1786
2155
  resolved === null ? null : /* @__PURE__ */ jsxs("section", {
@@ -1827,17 +2196,17 @@ function Form({ controller, ...props }) {
1827
2196
  providers.length === 0 ? /* @__PURE__ */ jsx("p", {
1828
2197
  "data-domainkit-part": "target-unavailable",
1829
2198
  children: messages.noProviders
1830
- }) : providers.map((provider) => /* @__PURE__ */ jsxs("section", {
1831
- "data-domainkit-part": "provider-authentication",
1832
- children: [/* @__PURE__ */ jsxs("div", {
1833
- "data-domainkit-part": "provider-heading",
1834
- children: [/* @__PURE__ */ jsx(Mark, { provider }), /* @__PURE__ */ jsx("strong", { children: provider.name })]
1835
- }), provider.methods.map((method) => /* @__PURE__ */ jsx(Method, {
1836
- controller,
1837
- method,
1838
- provider
1839
- }, `${provider.id}:${method.kind}`))]
1840
- }, provider.id))
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, {
2200
+ controller,
2201
+ 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))]
2209
+ })] })
1841
2210
  ] }),
1842
2211
  "data-domainkit-part": "connection-form"
1843
2212
  });
@@ -1847,17 +2216,23 @@ function Dialog$2({ children, controller, render, title, trigger }) {
1847
2216
  const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
1848
2217
  const readOnly = useReadOnly();
1849
2218
  const [open, setOpen] = useState(false);
1850
- const busy = controller.state._tag === "Submitting";
2219
+ const state = controller.state;
2220
+ const busy = state._tag === "Submitting";
2221
+ const unattributed = state._tag === "Failure" && !answeredInPlace(controller);
1851
2222
  const snapshot = controller.snapshot;
1852
- const provider = snapshot?.provider;
1853
- const heading = title ?? (provider === null || provider === void 0 ? messages.connectAnyTitle : messages.connectTitle(provider));
2223
+ const attached = snapshot?.provider ?? null;
2224
+ const host = hostProvider(controller);
2225
+ const named = attached === null ? host?.name ?? null : displayName(controller, attached);
2226
+ const heading = title ?? (named === null ? messages.connectAnyTitle : messages.connectTitle(named));
2227
+ const running = state._tag === "Loading" || state._tag === "Submitting" || state._tag === "Redirecting" || state._tag === "SelectionRequired";
2228
+ const label = named === null ? messages.connectAnyTitle : messages.connect;
1854
2229
  const body = children ?? /* @__PURE__ */ jsx(Form, { controller });
1855
2230
  if (readOnly) return /* @__PURE__ */ jsx(Fragment, {});
1856
2231
  if (render !== void 0) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
1857
2232
  "data-domainkit-part": "connection-trigger",
1858
2233
  onClick: () => setOpen(!open),
1859
2234
  type: "button",
1860
- children: trigger ?? messages.connect
2235
+ children: trigger ?? label
1861
2236
  }), render({
1862
2237
  children: body,
1863
2238
  open
@@ -1873,7 +2248,7 @@ function Dialog$2({ children, controller, render, title, trigger }) {
1873
2248
  open,
1874
2249
  children: [/* @__PURE__ */ jsx(Dialog.Trigger, {
1875
2250
  "data-domainkit-part": "connection-trigger",
1876
- children: trigger ?? messages.connect
2251
+ children: trigger ?? label
1877
2252
  }), /* @__PURE__ */ jsxs(Dialog.Portal, {
1878
2253
  container: portalContainer,
1879
2254
  children: [/* @__PURE__ */ jsx(Dialog.Backdrop, {
@@ -1907,9 +2282,9 @@ function Dialog$2({ children, controller, render, title, trigger }) {
1907
2282
  /* @__PURE__ */ jsxs("div", {
1908
2283
  "data-domainkit-part": "dialog-body",
1909
2284
  children: [
1910
- /* @__PURE__ */ jsx(Status$2, { controller }),
2285
+ running ? /* @__PURE__ */ jsx(Status$2, { controller }) : null,
1911
2286
  body,
1912
- /* @__PURE__ */ jsx(Outcome$2, { controller })
2287
+ unattributed ? /* @__PURE__ */ jsx(Outcome$2, { controller }) : null
1913
2288
  ]
1914
2289
  }),
1915
2290
  busy ? null : /* @__PURE__ */ jsx("div", {
@@ -1924,6 +2299,41 @@ function Dialog$2({ children, controller, render, title, trigger }) {
1924
2299
  })]
1925
2300
  });
1926
2301
  }
2302
+ /**
2303
+ * The disconnected offer: the provider whose nameservers serve the domain, what that means, and
2304
+ * the trigger that connects it. With no host detected there is nothing to offer, so nothing
2305
+ * renders unless the host application asks for the dialog anyway.
2306
+ */
2307
+ function Prompt({ connect = "detected", controller, ...props }) {
2308
+ const { messages } = useDomainKit();
2309
+ const readOnly = useReadOnly();
2310
+ const state = controller.state;
2311
+ 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
+ const element = usePart("div", props, { status: state._tag }, {
2316
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [host === null ? null : /* @__PURE__ */ jsxs("div", {
2317
+ "data-domainkit-part": "host-identity",
2318
+ children: [
2319
+ /* @__PURE__ */ jsx(Mark, { provider: host }),
2320
+ /* @__PURE__ */ jsx("span", {
2321
+ "data-domainkit-part": "host-name",
2322
+ children: host.name
2323
+ }),
2324
+ /* @__PURE__ */ jsx("span", {
2325
+ "data-domainkit-part": "host-statement",
2326
+ children: messages.hostOwnsZone
2327
+ })
2328
+ ]
2329
+ }), /* @__PURE__ */ jsx(Dialog$2, { controller })] }),
2330
+ "data-domainkit-part": "connect-prompt",
2331
+ "data-host": host?.id
2332
+ });
2333
+ if (!inFlight && !offers && connect !== "always") return null;
2334
+ if (readOnly && host === null) return null;
2335
+ return element;
2336
+ }
1927
2337
  /** A connected domain: which provider holds it, and how to let it go. */
1928
2338
  function Card({ controller, ...props }) {
1929
2339
  const { messages } = useDomainKit();
@@ -1958,8 +2368,8 @@ function Card({ controller, ...props }) {
1958
2368
  "data-domainkit-part": "connected-card"
1959
2369
  });
1960
2370
  }
1961
- /** Connection on its own: the card once connected, the dialog until then. */
1962
- function Flow$2({ domain, returnTo, ...props }) {
2371
+ /** Connection on its own: the card once connected, the prompt until then. */
2372
+ function Flow$2({ connect, domain, returnTo, ...props }) {
1963
2373
  const controller = useController$2({
1964
2374
  domain,
1965
2375
  ...returnTo === void 0 ? {} : { returnTo }
@@ -1968,7 +2378,10 @@ function Flow$2({ domain, returnTo, ...props }) {
1968
2378
  return /* @__PURE__ */ jsx(Root$2, {
1969
2379
  controller,
1970
2380
  ...props,
1971
- children: state._tag === "Connected" ? /* @__PURE__ */ jsx(Card, { controller }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Dialog$2, { controller }), /* @__PURE__ */ jsx(Outcome$2, { controller })] })
2381
+ children: state._tag === "Connected" ? /* @__PURE__ */ jsx(Card, { controller }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Prompt, {
2382
+ controller,
2383
+ ...connect === void 0 ? {} : { connect }
2384
+ }), answeredInPlace(controller) ? null : /* @__PURE__ */ jsx(Outcome$2, { controller })] })
1972
2385
  });
1973
2386
  }
1974
2387
  //#endregion
@@ -2242,25 +2655,29 @@ function Evidence({ readiness }) {
2242
2655
  })]
2243
2656
  });
2244
2657
  }
2245
- function Outcome({ controller, ...props }) {
2658
+ /**
2659
+ * The last observation that failed. Observing reads the world rather than changing the domain, so
2660
+ * the retry stays even in read-only.
2661
+ */
2662
+ function Outcome({ children, controller, ...props }) {
2246
2663
  const { messages } = useDomainKit();
2247
2664
  const state = controller.state;
2248
- const element = usePart("p", props, { status: state._tag }, {
2249
- children: state._tag === "Failure" ? /* @__PURE__ */ jsxs(Fragment, { children: [
2250
- failure(state.error, messages),
2251
- " ",
2252
- /* @__PURE__ */ jsx("button", {
2253
- "data-domainkit-part": "verification-retry",
2254
- onClick: controller.retry,
2255
- type: "button",
2256
- children: messages.retry
2257
- })
2258
- ] }) : null,
2259
- "data-domainkit-part": "flow-outcome",
2260
- "data-tone": "danger",
2261
- role: "alert"
2665
+ if (state._tag !== "Failure") return null;
2666
+ const words = outcome(state.error, messages);
2667
+ return /* @__PURE__ */ jsx(Provider, {
2668
+ value: {
2669
+ description: words.description,
2670
+ layout: props.layout ?? "card",
2671
+ retry: controller.retry,
2672
+ retryPart: "verification-retry",
2673
+ title: words.title,
2674
+ tone: "danger"
2675
+ },
2676
+ children: /* @__PURE__ */ jsx(Root$6, {
2677
+ ...props,
2678
+ children: children ?? /* @__PURE__ */ jsx(Composition, {})
2679
+ })
2262
2680
  });
2263
- return state._tag === "Failure" ? element : null;
2264
2681
  }
2265
2682
  /** The default verification slot: a button that observes, with the evidence behind it. */
2266
2683
  function Status({ controller, render, ...props }) {
@@ -2306,11 +2723,15 @@ function Status({ controller, render, ...props }) {
2306
2723
  //#endregion
2307
2724
  //#region src/domain.tsx
2308
2725
  var domain_exports = /* @__PURE__ */ __exportAll({ Flow: () => Flow });
2309
- function DefaultConnection({ controller }) {
2726
+ function DefaultConnection({ connect, controller }) {
2727
+ const stated = controller.state._tag === "Disconnected" && hostProvider(controller) !== null;
2310
2728
  return controller.state._tag === "Connected" ? /* @__PURE__ */ jsx(Card, { controller }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2311
- /* @__PURE__ */ jsx(Status$2, { controller }),
2312
- /* @__PURE__ */ jsx(Dialog$2, { controller }),
2313
- /* @__PURE__ */ jsx(Outcome$2, { controller })
2729
+ stated ? null : /* @__PURE__ */ jsx(Status$2, { controller }),
2730
+ /* @__PURE__ */ jsx(Prompt, {
2731
+ connect,
2732
+ controller
2733
+ }),
2734
+ answeredInPlace(controller) ? null : /* @__PURE__ */ jsx(Outcome$2, { controller })
2314
2735
  ] });
2315
2736
  }
2316
2737
  function DefaultActions({ cleanup, connection, provisioning }) {
@@ -2353,7 +2774,7 @@ function DefaultActions({ cleanup, connection, provisioning }) {
2353
2774
  * only what the transport declares, and every part of it is a slot with a default. It adds no
2354
2775
  * layout containers of its own, so a host's grid can place the slot output directly.
2355
2776
  */
2356
- function Flow({ domain, onApplied, onCleaned, readOnly, requirements, returnTo, slots = {}, ...props }) {
2777
+ function Flow({ connect = "detected", domain, onApplied, onCleaned, readOnly, requirements, returnTo, slots = {}, ...props }) {
2357
2778
  const { capabilities } = useDomainKit();
2358
2779
  const inherited = useReadOnly();
2359
2780
  const connection = useController$2({
@@ -2390,9 +2811,11 @@ function Flow({ domain, onApplied, onCleaned, readOnly, requirements, returnTo,
2390
2811
  value: readOnly ?? inherited,
2391
2812
  children: [
2392
2813
  !capabilities.includes("connection") ? null : slots.connection === void 0 ? /* @__PURE__ */ jsx(DefaultConnection, {
2814
+ connect,
2393
2815
  controller: connection,
2394
2816
  domain
2395
2817
  }) : slots.connection({
2818
+ connect,
2396
2819
  controller: connection,
2397
2820
  domain
2398
2821
  }),
@@ -2438,6 +2861,6 @@ const transport = Testing.transport;
2438
2861
  //#region src/failure.ts
2439
2862
  const Failure = Data.taggedEnum();
2440
2863
  //#endregion
2441
- export { cleanup_exports as Cleanup, connect_exports as Connect, domain_exports as Domain, domain_kit_exports as DomainKit, Event, Failure, messages_exports as Messages, operations_exports as Operations, provider_exports as Provider, provision_exports as Provision, records_exports as Records, testing_exports as Testing, theme_exports as Theme, verify_exports as Verify, defaultIcons, useIcons };
2864
+ export { cleanup_exports as Cleanup, connect_exports as Connect, domain_exports as Domain, domain_kit_exports as DomainKit, Event, Failure, messages_exports as Messages, operations_exports as Operations, outcome_exports as Outcome, provider_exports as Provider, provision_exports as Provision, records_exports as Records, testing_exports as Testing, theme_exports as Theme, verify_exports as Verify, defaultIcons, useIcons };
2442
2865
 
2443
2866
  //# sourceMappingURL=index.mjs.map