@domainkit/react 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2,114 +2,14 @@
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, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
+ import { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
6
6
  import * as Data from "effect/Data";
7
7
  import { Transport } from "domainkit/client";
8
- import { useRender } from "@base-ui/react/use-render";
9
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { jsx } from "react/jsx-runtime";
10
9
  import * as Exit from "effect/Exit";
11
10
  import * as Option from "effect/Option";
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";
16
- import { Popover } from "@base-ui/react/popover";
17
11
  import * as DateTime from "effect/DateTime";
18
12
  import { Testing } from "domainkit/testing";
19
- //#region src/composition.tsx
20
- function usePart(defaultTagName, componentProps, state, internalProps) {
21
- const { className, render, style, ...externalProps } = componentProps;
22
- const parameters = {
23
- ...className === void 0 ? {} : { className },
24
- defaultTagName,
25
- props: [internalProps, externalProps],
26
- ...render === void 0 ? {} : { render },
27
- state,
28
- ...style === void 0 ? {} : { style }
29
- };
30
- return useRender(parameters);
31
- }
32
- /** A part with no state of its own: one tag, one `data-domainkit-part`, host props on top. */
33
- function leafPart(defaultTagName, part) {
34
- return function Leaf(props) {
35
- return usePart(defaultTagName, props, {}, { "data-domainkit-part": part });
36
- };
37
- }
38
- //#endregion
39
- //#region src/icons.tsx
40
- function Glyph({ children }) {
41
- return /* @__PURE__ */ jsx("svg", {
42
- "aria-hidden": "true",
43
- fill: "none",
44
- stroke: "currentColor",
45
- strokeLinecap: "round",
46
- strokeLinejoin: "round",
47
- strokeWidth: 2,
48
- viewBox: "0 0 24 24",
49
- children
50
- });
51
- }
52
- const defaultIcons = {
53
- chevron: /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }) }),
54
- close: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })] }),
55
- copied: /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) }),
56
- copy: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("rect", {
57
- height: "14",
58
- rx: "2",
59
- width: "14",
60
- x: "8",
61
- y: "8"
62
- }), /* @__PURE__ */ jsx("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })] }),
63
- download: /* @__PURE__ */ jsxs(Glyph, { children: [
64
- /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
65
- /* @__PURE__ */ jsx("path", { d: "m7 10 5 5 5-5" }),
66
- /* @__PURE__ */ jsx("path", { d: "M12 15V3" })
67
- ] }),
68
- external: /* @__PURE__ */ jsxs(Glyph, { children: [
69
- /* @__PURE__ */ jsx("path", { d: "M15 3h6v6" }),
70
- /* @__PURE__ */ jsx("path", { d: "M10 14 21 3" }),
71
- /* @__PURE__ */ jsx("path", { d: "M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" })
72
- ] }),
73
- failure: /* @__PURE__ */ jsxs(Glyph, { children: [
74
- /* @__PURE__ */ jsx("circle", {
75
- cx: "12",
76
- cy: "12",
77
- r: "9"
78
- }),
79
- /* @__PURE__ */ jsx("path", { d: "M15 9 9 15" }),
80
- /* @__PURE__ */ jsx("path", { d: "m9 9 6 6" })
81
- ] }),
82
- pending: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("circle", {
83
- cx: "12",
84
- cy: "12",
85
- r: "9"
86
- }), /* @__PURE__ */ jsx("path", { d: "M12 7v5l3 2" })] }),
87
- success: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("circle", {
88
- cx: "12",
89
- cy: "12",
90
- r: "9"
91
- }), /* @__PURE__ */ jsx("path", { d: "m8.5 12.5 2.5 2.5 4.5-5" })] }),
92
- warning: /* @__PURE__ */ jsxs(Glyph, { children: [
93
- /* @__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" }),
94
- /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
95
- /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
96
- ] })
97
- };
98
- const IconsContext = createContext(defaultIcons);
99
- function IconsProvider({ children, icons }) {
100
- const value = useMemo(() => ({
101
- ...defaultIcons,
102
- ...icons
103
- }), [icons]);
104
- return /* @__PURE__ */ jsx(IconsContext.Provider, {
105
- value,
106
- children
107
- });
108
- }
109
- function useIcons() {
110
- return useContext(IconsContext);
111
- }
112
- //#endregion
113
13
  //#region src/messages.ts
114
14
  var messages_exports = /* @__PURE__ */ __exportAll({
115
15
  english: () => english,
@@ -167,7 +67,7 @@ const english = {
167
67
  approve: "Approve",
168
68
  addRecords: (count) => count === 1 ? "Add 1 record" : `Add ${count} records`,
169
69
  decline: "Decline",
170
- everyRecordConflicts: "Resolve the records above at your provider, then review again.",
70
+ everyRecordConflicts: "Resolve the records above at your provider, then check again.",
171
71
  reviewChanges: "Review changes",
172
72
  cleanUp: "Remove records",
173
73
  checkDns: "Check DNS",
@@ -177,7 +77,8 @@ const english = {
177
77
  useAnotherProvider: "Use a different provider",
178
78
  useTokenInstead: "Use an API token instead",
179
79
  back: "Back",
180
- connected: "Connected",
80
+ connectedAccount: (provider, account) => `${provider} · ${account}`,
81
+ recordsAdded: (count) => count === 1 ? "1 added" : `${count} added`,
181
82
  needsReconnect: "Needs reconnecting",
182
83
  disconnectScope: "This connection serves other domains.",
183
84
  disconnectThisDomain: "Only this domain",
@@ -188,6 +89,10 @@ const english = {
188
89
  download: "Download",
189
90
  useConnection: (label) => `Use ${label}`,
190
91
  chooseZone: "Choose the zone that serves this domain",
92
+ zoneSuggestions: "Zones your connected accounts serve",
93
+ recordsGoTo: (provider, account) => `Records go to ${provider} through ${account}.`,
94
+ notInConnectedAccount: "Not in a connected account.",
95
+ reconnectAccount: (account) => `Reconnect ${account}`,
191
96
  getToken: "Where do I find this?",
192
97
  loading: "Loading…",
193
98
  connecting: "Connecting…",
@@ -203,10 +108,11 @@ const english = {
203
108
  observing: "Checking DNS…",
204
109
  connectTitle: (provider) => `Connect ${provider}`,
205
110
  connectAnyTitle: "Connect a DNS provider",
206
- hostOwnsZone: "Owns DNS for this domain.",
111
+ hostDetected: (provider) => `${provider} DNS detected`,
207
112
  connectDescription: (domain) => `Authorize DNS changes for ${domain}.`,
208
113
  connectedTo: (provider) => `${provider} connected`,
209
114
  notConnected: "No DNS provider is connected.",
115
+ administratorConnects: (provider) => `An administrator can connect ${provider}`,
210
116
  reconnectRequired: (provider) => `${provider} needs to be reconnected`,
211
117
  disconnectTitle: (provider) => `Disconnect ${provider}?`,
212
118
  disconnectConsent: (domain, provider) => `DomainKit stops managing DNS for ${domain} through ${provider}.`,
@@ -247,6 +153,23 @@ const english = {
247
153
  case "missing": return "The record this plan expected is gone.";
248
154
  }
249
155
  },
156
+ conflictAdvice: (reason) => {
157
+ switch (reason) {
158
+ case "exclusive-name": return "Remove the record on that name at your provider, then check again.";
159
+ case "cname-collision": return "Remove the other records on that name at your provider, then check again.";
160
+ case "value-mismatch": return "Point that record at this value at your provider, or remove it and check again.";
161
+ case "opaque": return "Remove that record at your provider, then check again.";
162
+ case "missing": return "Check again to build a plan from what is there now.";
163
+ }
164
+ },
165
+ planStatus: (operation) => {
166
+ switch (operation._tag) {
167
+ case "Create": return "Will add";
168
+ case "Noop": return "Already there";
169
+ case "Delete": return "Will remove";
170
+ case "Conflict": return "In the way";
171
+ }
172
+ },
250
173
  attemptStatus: (status) => {
251
174
  switch (status) {
252
175
  case "planned": return "Awaiting review";
@@ -418,39 +341,17 @@ const failure = (error, catalog, context = {}) => {
418
341
  return description === void 0 ? `${title}.` : `${title}. ${description}`;
419
342
  };
420
343
  //#endregion
421
- //#region src/theme.ts
422
- var theme_exports = /* @__PURE__ */ __exportAll({ toStyle: () => toStyle });
423
- const variables = {
424
- accent: "--domainkit-accent",
425
- accentContrast: "--domainkit-accent-contrast",
426
- backdrop: "--domainkit-backdrop",
427
- background: "--domainkit-background",
428
- border: "--domainkit-border",
429
- danger: "--domainkit-danger",
430
- dangerContrast: "--domainkit-danger-contrast",
431
- fill: "--domainkit-fill",
432
- fontFamily: "--domainkit-font-family",
433
- muted: "--domainkit-muted",
434
- radius: "--domainkit-radius",
435
- shadow: "--domainkit-shadow",
436
- success: "--domainkit-success",
437
- text: "--domainkit-text",
438
- warning: "--domainkit-warning"
439
- };
440
- function toStyle(theme = {}) {
441
- return Object.fromEntries(Object.entries(theme).flatMap(([key, value]) => value === void 0 ? [] : [[variables[key], value]]));
442
- }
443
- //#endregion
444
344
  //#region src/domain-kit.tsx
445
345
  var domain_kit_exports = /* @__PURE__ */ __exportAll({
446
346
  ReadOnly: () => ReadOnly,
447
- Root: () => Root$7,
347
+ Root: () => Root,
448
348
  useCapabilities: () => useCapabilities,
449
349
  useDomainKit: () => useDomainKit,
350
+ useMessages: () => useMessages,
450
351
  useReadOnly: () => useReadOnly,
451
352
  useTransport: () => useTransport
452
353
  });
453
- const Context$1 = createContext(null);
354
+ const Context = createContext(null);
454
355
  const navigateInBrowser = (url) => {
455
356
  if (typeof window === "undefined") throw new Error("DomainKit provider authorization requires a browser or a `navigate` prop");
456
357
  window.location.assign(url);
@@ -482,70 +383,44 @@ const useStableTransport = (transport) => {
482
383
  };
483
384
  }, [signature]);
484
385
  };
485
- const resolvePortalContainer = (container, ownerDocument) => {
486
- if (container === null || ownerDocument === null) return null;
487
- return container.ownerDocument === ownerDocument && container.getRootNode() === ownerDocument ? container : null;
488
- };
489
- const usePortalContainer = (container) => {
490
- const ownerDocument = useMemo(() => container?.ownerDocument ?? null, [container]);
491
- const subscribe = useCallback((notify) => {
492
- if (ownerDocument === null || typeof MutationObserver === "undefined") return () => {};
493
- const observer = new MutationObserver(notify);
494
- observer.observe(ownerDocument, {
495
- childList: true,
496
- subtree: true
497
- });
498
- return () => observer.disconnect();
499
- }, [ownerDocument]);
500
- const getSnapshot = useCallback(() => resolvePortalContainer(container, ownerDocument), [container, ownerDocument]);
501
- return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
502
- };
503
- function Root$7({ children, colorScheme = "inherit", icons, marks = {}, messages, navigate = navigateInBrowser, onEvent, portalContainer = null, readOnly = false, revision = 0, theme, transport, ...props }) {
386
+ /**
387
+ * The transport, the catalog, and the two host callbacks every hook below reads. It renders no
388
+ * element of its own: `@domainkit/react` supplies behaviour, state, copy, and accessibility, and
389
+ * the host application supplies the markup.
390
+ */
391
+ function Root({ children, messages, navigate = navigateInBrowser, onEvent, readOnly = false, revision = 0, transport }) {
504
392
  const { capabilities, transport: stable } = useStableTransport(transport);
505
393
  const onEventRef = useRef(onEvent);
506
394
  useLayoutEffect(() => {
507
395
  onEventRef.current = onEvent;
508
396
  }, [onEvent]);
509
- const emit = useCallback((event) => {
510
- try {
511
- onEventRef.current?.(event);
512
- } catch {}
513
- }, []);
514
- const themeStyle = toStyle(theme);
515
- const resolvedPortalContainer = usePortalContainer(portalContainer);
516
- const portalContainerRef = useMemo(() => ({ current: resolvedPortalContainer }), [resolvedPortalContainer]);
517
397
  const value = {
518
398
  capabilities,
519
- colorScheme,
520
- emit,
521
- marks,
399
+ emit: useCallback((event) => {
400
+ try {
401
+ onEventRef.current?.(event);
402
+ } catch {}
403
+ }, []),
522
404
  messages: merge(messages),
523
405
  navigate,
524
- portalContainer: portalContainerRef,
525
406
  readOnly,
526
407
  revision,
527
- themeStyle,
528
408
  transport: stable
529
409
  };
530
- const content = usePart("div", props, { colorScheme }, {
531
- children,
532
- "data-color-scheme": colorScheme,
533
- "data-domainkit-root": "",
534
- style: themeStyle
535
- });
536
- return /* @__PURE__ */ jsx(Context$1.Provider, {
410
+ return /* @__PURE__ */ jsx(Context.Provider, {
537
411
  value,
538
- children: /* @__PURE__ */ jsx(IconsProvider, {
539
- ...icons === void 0 ? {} : { icons },
540
- children: content
541
- })
412
+ children
542
413
  });
543
414
  }
544
415
  function useDomainKit() {
545
- const value = useContext(Context$1);
546
- if (value === null) throw new Error("DomainKit components must be rendered inside DomainKit.Root");
416
+ const value = useContext(Context);
417
+ if (value === null) throw new Error("DomainKit hooks must be called inside DomainKit.Root");
547
418
  return value;
548
419
  }
420
+ /** The catalog `DomainKit.Root` holds, with the host's overrides already merged in. */
421
+ function useMessages() {
422
+ return useDomainKit().messages;
423
+ }
549
424
  /** The transport `DomainKit.Root` holds, with the identity it keeps for the whole mount. */
550
425
  function useTransport() {
551
426
  return useDomainKit().transport;
@@ -558,13 +433,13 @@ function ReadOnly({ children, value }) {
558
433
  children
559
434
  });
560
435
  }
561
- /** Whether this part may offer controls that change the domain. */
436
+ /** Whether this surface may offer controls that change the domain. */
562
437
  function useReadOnly() {
563
438
  const scoped = useContext(ReadOnlyContext);
564
439
  const root = useDomainKit().readOnly;
565
440
  return scoped ?? root;
566
441
  }
567
- /** Which capability groups the host's transport declares, for gating a custom part. */
442
+ /** Which capability groups the host's transport declares, for gating a surface of your own. */
568
443
  function useCapabilities() {
569
444
  return useDomainKit().capabilities;
570
445
  }
@@ -634,10 +509,42 @@ function useRunner() {
634
509
  const State$2 = Data.taggedEnum();
635
510
  /** Reasons that mean the plan itself is gone: retrying the same step would fail the same way. */
636
511
  const needsNewPlan = (error) => error.reason._tag === "Stale" || error.reason._tag === "Expired" || error.reason._tag === "Conflict";
512
+ /** The plan an attempt holds, whatever step it is on; `null` before there is one. */
513
+ const planOf = (state) => {
514
+ switch (state._tag) {
515
+ case "Planned":
516
+ case "Approving":
517
+ case "Applying":
518
+ case "Rejecting":
519
+ case "Rejected":
520
+ case "Applied": return state.plan;
521
+ case "Idle":
522
+ case "Planning":
523
+ case "Failure": return null;
524
+ }
525
+ };
526
+ /**
527
+ * The plan still awaiting its apply, which is what a row of records reports; `null` once one has
528
+ * landed, because from then on an observation answers instead.
529
+ */
530
+ const pendingPlan = (state) => {
531
+ switch (state._tag) {
532
+ case "Planned":
533
+ case "Approving":
534
+ case "Applying": return state.plan;
535
+ case "Idle":
536
+ case "Planning":
537
+ case "Applied":
538
+ case "Rejecting":
539
+ case "Rejected":
540
+ case "Failure": return null;
541
+ }
542
+ };
637
543
  function useAttempt(options) {
638
544
  const { domain, done, group, key, onDone } = options;
639
545
  const { emit } = useDomainKit();
640
- const readOnly = useReadOnly();
546
+ const inherited = useReadOnly();
547
+ const readOnly = options.readOnly ?? inherited;
641
548
  const runner = useRunner();
642
549
  const [state, setState] = useState(State$2.Idle());
643
550
  const held = useRef({
@@ -670,7 +577,7 @@ function useAttempt(options) {
670
577
  }));
671
578
  }, [domain, emit]);
672
579
  const applyWith = useCallback((plan, approval) => {
673
- if (group === void 0 || held.current.key !== key) return;
580
+ if (readOnly || group === void 0 || held.current.key !== key) return;
674
581
  const command = () => {
675
582
  setState(State$2.Applying({
676
583
  approval,
@@ -700,9 +607,11 @@ function useAttempt(options) {
700
607
  key,
701
608
  onDone,
702
609
  onFailure,
610
+ readOnly,
703
611
  runner
704
612
  ]);
705
613
  const buildPlan = useCallback(() => {
614
+ if (readOnly) return;
706
615
  const effect = build.current();
707
616
  if (effect === null) return;
708
617
  const command = () => {
@@ -728,11 +637,12 @@ function useAttempt(options) {
728
637
  }, [
729
638
  key,
730
639
  onFailure,
640
+ readOnly,
731
641
  runner
732
642
  ]);
733
643
  const approve = useCallback((operationIds) => {
734
644
  const plan = held.current.plan;
735
- if (group === void 0 || plan === null || held.current.key !== key) return;
645
+ if (readOnly || group === void 0 || plan === null || held.current.key !== key) return;
736
646
  const command = () => {
737
647
  setState(State$2.Approving({ plan }));
738
648
  runner.run(group.approve({
@@ -761,11 +671,12 @@ function useAttempt(options) {
761
671
  group,
762
672
  key,
763
673
  onFailure,
674
+ readOnly,
764
675
  runner
765
676
  ]);
766
677
  const reject = useCallback((reason) => {
767
678
  const plan = held.current.plan;
768
- if (group === void 0 || plan === null || held.current.key !== key) return;
679
+ if (readOnly || group === void 0 || plan === null || held.current.key !== key) return;
769
680
  const command = () => {
770
681
  setState(State$2.Rejecting({ plan }));
771
682
  runner.run(group.reject({
@@ -796,11 +707,12 @@ function useAttempt(options) {
796
707
  group,
797
708
  key,
798
709
  onFailure,
710
+ readOnly,
799
711
  runner
800
712
  ]);
801
713
  const apply = useCallback(() => {
802
714
  const { approval, plan } = held.current;
803
- if (approval === null || plan === null) return;
715
+ if (readOnly || approval === null || plan === null) return;
804
716
  applyWith(plan, approval);
805
717
  }, [applyWith]);
806
718
  const retry = useCallback(() => {
@@ -843,715 +755,18 @@ function useAttempt(options) {
843
755
  };
844
756
  }
845
757
  //#endregion
846
- //#region src/outcome.tsx
847
- /**
848
- * What a flow says when a step ends badly: media, title, description, and the action that follows.
849
- * Every flow's `Outcome` binds its controller to these parts, so a failure reads the same way in
850
- * the connect dialog, the review dialog, and the verification popover.
851
- *
852
- * <Connect.Outcome controller={connection} /> the default composition
853
- *
854
- * <Connect.Outcome controller={connection}> a composition of your own
855
- * <Outcome.Header>
856
- * <Outcome.Media><MyIcon /></Outcome.Media>
857
- * <Outcome.Title />
858
- * <Outcome.Description />
859
- * </Outcome.Header>
860
- * <Outcome.Content />
861
- * </Connect.Outcome>
862
- */
863
- var outcome_exports = /* @__PURE__ */ __exportAll({
864
- Composition: () => Composition,
865
- Content: () => Content,
866
- Description: () => Description,
867
- Header: () => Header$1,
868
- Media: () => Media,
869
- Provider: () => Provider,
870
- Root: () => Root$6,
871
- Title: () => Title,
872
- useOutcome: () => useOutcome
873
- });
874
- const fallback = {
875
- description: "",
876
- layout: "card",
877
- retry: null,
878
- retryPart: "flow-retry",
879
- title: "",
880
- tone: "danger"
881
- };
882
- const Context = createContext(null);
883
- /** What the surrounding flow resolved, for a part of your own inside `X.Outcome`. */
884
- function useOutcome() {
885
- return useContext(Context) ?? fallback;
886
- }
887
- /** Binds a resolved outcome to the parts below it. Flows render this; hosts rarely do. */
888
- function Provider({ children, value }) {
889
- return /* @__PURE__ */ jsx(Context.Provider, {
890
- value,
891
- children
892
- });
893
- }
894
- /**
895
- * The outcome itself. It announces, so a customer reading with assistive tech hears it. A `tone`
896
- * or `layout` of its own reaches every part below it, so the glyph and the words agree with the
897
- * root's own attributes.
898
- */
899
- function Root$6({ layout, tone, ...props }) {
900
- const value = useOutcome();
901
- const resolved = {
902
- ...value,
903
- layout: layout ?? value.layout,
904
- tone: tone ?? value.tone
905
- };
906
- const element = usePart("div", props, {
907
- layout: resolved.layout,
908
- tone: resolved.tone
909
- }, {
910
- "data-domainkit-part": "outcome",
911
- "data-layout": resolved.layout,
912
- "data-tone": resolved.tone,
913
- role: "alert"
914
- });
915
- return /* @__PURE__ */ jsx(Provider, {
916
- value: resolved,
917
- children: element
918
- });
919
- }
920
- /** Media, title, and description together, so the action below them can move on its own. */
921
- function Header$1(props) {
922
- const { layout, tone } = useOutcome();
923
- return usePart("div", props, {
924
- layout,
925
- tone
926
- }, { "data-domainkit-part": "outcome-header" });
927
- }
928
- /** The glyph for the tone. Children replace it; `icons` on `DomainKit.Root` replaces the set. */
929
- function Media({ variant = "icon", ...props }) {
930
- const { layout, tone } = useOutcome();
931
- const icons = useIcons();
932
- const glyph = tone === "success" ? icons.success : tone === "warning" ? icons.warning : icons.failure;
933
- return usePart("div", props, {
934
- layout,
935
- tone
936
- }, {
937
- "aria-hidden": "true",
938
- children: glyph,
939
- "data-domainkit-part": "outcome-media",
940
- "data-variant": variant
941
- });
942
- }
943
- /** The heading from the catalog. Children win, for a host that words it itself. */
944
- function Title(props) {
945
- const { layout, title, tone } = useOutcome();
946
- return usePart("div", props, {
947
- layout,
948
- tone
949
- }, {
950
- children: title,
951
- "data-domainkit-part": "outcome-title"
952
- });
953
- }
954
- /** What to do about it, from the catalog. An outcome whose title says everything renders none. */
955
- function Description(props) {
956
- const { description, layout, tone } = useOutcome();
957
- const element = usePart("p", props, {
958
- layout,
959
- tone
960
- }, {
961
- children: description,
962
- "data-domainkit-part": "outcome-description"
963
- });
964
- return description === "" && props.children === void 0 ? null : element;
965
- }
966
- /**
967
- * The actions under the header. Without children it is the flow's retry, and a flow that allows
968
- * none renders nothing: read-only hides every control that would start a write again.
969
- */
970
- function Content(props) {
971
- const { messages } = useDomainKit();
972
- const { layout, retry, retryPart, tone } = useOutcome();
973
- const element = usePart("div", props, {
974
- layout,
975
- tone
976
- }, {
977
- children: retry === null ? null : /* @__PURE__ */ jsx("button", {
978
- "data-domainkit-part": retryPart,
979
- onClick: retry,
980
- type: "button",
981
- children: messages.retry
982
- }),
983
- "data-domainkit-part": "outcome-content"
984
- });
985
- return retry === null && props.children === void 0 ? null : element;
986
- }
987
- /** Media, title, and description in a header, then the action. What `X.Outcome` renders alone. */
988
- function Composition() {
989
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Header$1, { children: [
990
- /* @__PURE__ */ jsx(Media, {}),
991
- /* @__PURE__ */ jsx(Title, {}),
992
- /* @__PURE__ */ jsx(Description, {})
993
- ] }), /* @__PURE__ */ jsx(Content, {})] });
994
- }
995
- //#endregion
996
- //#region src/records.tsx
997
- var records_exports = /* @__PURE__ */ __exportAll({
998
- Body: () => Body$1,
999
- Caption: () => Caption,
1000
- Card: () => Card$1,
1001
- CardContent: () => CardContent,
1002
- CardHeader: () => CardHeader,
1003
- CardTitle: () => CardTitle,
1004
- Cell: () => Cell,
1005
- CopyValue: () => CopyValue,
1006
- Footer: () => Footer,
1007
- Head: () => Head,
1008
- Header: () => Header,
1009
- Purpose: () => Purpose,
1010
- Root: () => Root$5,
1011
- Row: () => Row,
1012
- Status: () => Status$5,
1013
- Table: () => Table,
1014
- Value: () => Value,
1015
- ZoneFile: () => ZoneFile,
1016
- copyText: () => copyText,
1017
- downloadZoneFile: () => downloadZoneFile,
1018
- identity: () => identity,
1019
- requirementsKey: () => requirementsKey,
1020
- statusOf: () => statusOf,
1021
- toZoneFile: () => toZoneFile,
1022
- useCopy: () => useCopy
1023
- });
1024
- const absolute = (value) => value.endsWith(".") ? value : `${value}.`;
1025
- const textEncoder = new TextEncoder();
1026
- const escapeTxtCharacter = (character) => {
1027
- if (character === "\"" || character === "\\") return `\\${character}`;
1028
- const codePoint = character.codePointAt(0);
1029
- if (codePoint !== void 0 && (codePoint < 32 || codePoint === 127)) return `\\${codePoint.toString().padStart(3, "0")}`;
1030
- return character;
1031
- };
1032
- /** TXT strings are capped at 255 bytes each, so long values are split and quoted per chunk. */
1033
- const txtValue = (value) => {
1034
- const chunks = [];
1035
- let chunk = "";
1036
- let byteLength = 0;
1037
- for (const character of value) {
1038
- const characterByteLength = textEncoder.encode(character).byteLength;
1039
- if (byteLength + characterByteLength > 255) {
1040
- chunks.push(chunk);
1041
- chunk = "";
1042
- byteLength = 0;
1043
- }
1044
- chunk += escapeTxtCharacter(character);
1045
- byteLength += characterByteLength;
1046
- }
1047
- chunks.push(chunk);
1048
- return chunks.map((part) => `"${part}"`).join(" ");
1049
- };
1050
- /** The RR type as a zone file spells it. The tag and the type name are the same string. */
1051
- const rrType = (record) => record._tag;
1052
- const zoneData = (record) => {
1053
- switch (record._tag) {
1054
- case "A":
1055
- case "AAAA": return record.address;
1056
- case "CNAME": return absolute(record.target);
1057
- case "NS": return absolute(record.nameserver);
1058
- case "TXT": return txtValue(record.value);
1059
- case "MX": return `${record.priority} ${absolute(record.exchange)}`;
1060
- case "CAA": return `${record.flags} ${record.tag} "${record.value}"`;
1061
- case "SRV": return `${record.priority} ${record.weight} ${record.port} ${absolute(record.target)}`;
1062
- }
1063
- };
1064
- const toZoneFile = (records) => `${records.map((record) => `${absolute(record.name)}${record.ttl === null ? "" : ` ${record.ttl}`} IN ${rrType(record)} ${zoneData(record)}`).join("\n")}\n`;
1065
- const downloadText = (filename, contents) => {
1066
- if (typeof document === "undefined") return;
1067
- const blob = new Blob([contents], { type: "text/plain;charset=utf-8" });
1068
- const url = URL.createObjectURL(blob);
1069
- const anchor = document.createElement("a");
1070
- anchor.download = filename;
1071
- anchor.href = url;
1072
- document.body.append(anchor);
1073
- anchor.click();
1074
- anchor.remove();
1075
- URL.revokeObjectURL(url);
1076
- };
1077
- const downloadZoneFile = (domain, records) => downloadText(`${domain}.txt`, toZoneFile(records));
1078
- const copyText = (value) => {
1079
- if (typeof navigator === "undefined" || navigator.clipboard === void 0) return Promise.resolve(false);
1080
- return navigator.clipboard.writeText(value).then(() => true, () => false);
1081
- };
1082
- function useCopy(value, resetAfter = 2e3) {
1083
- const [copied, setCopied] = useState(false);
1084
- const reset = useRef(void 0);
1085
- useEffect(() => () => clearTimeout(reset.current), []);
1086
- return {
1087
- copied,
1088
- copy: () => {
1089
- copyText(value).then((ok) => {
1090
- if (!ok) return;
1091
- setCopied(true);
1092
- clearTimeout(reset.current);
1093
- reset.current = setTimeout(() => setCopied(false), resetAfter);
1094
- });
1095
- }
1096
- };
1097
- }
1098
- function CopyValue({ value, ...props }) {
1099
- const { messages } = useDomainKit();
1100
- const icons = useIcons();
1101
- const { copied, copy } = useCopy(value);
1102
- return usePart("span", props, { copied }, {
1103
- children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("code", { children: value }), /* @__PURE__ */ jsxs("button", {
1104
- "aria-label": `${copied ? messages.copied : messages.copy} ${value}`,
1105
- "data-domainkit-part": "copy-action",
1106
- onClick: copy,
1107
- type: "button",
1108
- children: [/* @__PURE__ */ jsx("span", {
1109
- "aria-hidden": "true",
1110
- "data-domainkit-part": "copy-glyph",
1111
- "data-icon": "",
1112
- "data-state": "idle",
1113
- children: icons.copy
1114
- }), /* @__PURE__ */ jsx("span", {
1115
- "aria-hidden": "true",
1116
- "data-domainkit-part": "copy-glyph",
1117
- "data-icon": "",
1118
- "data-state": "done",
1119
- children: icons.copied
1120
- })]
1121
- })] }),
1122
- "data-copied": copied ? "" : void 0,
1123
- "data-domainkit-part": "copy-value"
1124
- });
1125
- }
1126
- function ZoneFile({ domain, records, ...props }) {
1127
- const { messages } = useDomainKit();
1128
- const icons = useIcons();
1129
- const { copied, copy } = useCopy(toZoneFile(records));
1130
- return usePart("div", props, { count: records.length }, {
1131
- children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
1132
- "aria-label": copied ? messages.copied : messages.copyZone,
1133
- "data-domainkit-part": "zone-copy",
1134
- onClick: copy,
1135
- type: "button",
1136
- children: [/* @__PURE__ */ jsx("span", {
1137
- "aria-hidden": "true",
1138
- "data-icon": "inline-start",
1139
- children: copied ? icons.copied : icons.copy
1140
- }), copied ? messages.copied : messages.copyZone]
1141
- }), /* @__PURE__ */ jsxs("button", {
1142
- "aria-label": messages.download,
1143
- "data-domainkit-part": "zone-download",
1144
- onClick: () => downloadZoneFile(domain, records),
1145
- type: "button",
1146
- children: [/* @__PURE__ */ jsx("span", {
1147
- "aria-hidden": "true",
1148
- "data-icon": "inline-start",
1149
- children: icons.download
1150
- }), messages.download]
1151
- })] }),
1152
- "data-domainkit-part": "zone-file"
1153
- });
1154
- }
1155
- /** The readiness this record reached, or `null` when nothing has observed it. */
1156
- const statusOf = (record, readiness) => readiness?.requirements.find((requirement) => DnsRecord.equals(requirement.record, record))?.status ?? null;
1157
- function Status$5({ status, ...props }) {
1158
- const { messages } = useDomainKit();
1159
- const icons = useIcons();
1160
- const glyph = status === "satisfied" ? icons.success : status === "unknown" ? icons.pending : icons.failure;
1161
- return usePart("span", props, { status }, {
1162
- children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1163
- "aria-hidden": "true",
1164
- "data-icon": "inline-start",
1165
- children: glyph
1166
- }), messages.requirementStatus(status)] }),
1167
- "data-domainkit-part": "record-status",
1168
- "data-status": status
1169
- });
1170
- }
1171
- const Header = leafPart("thead", "records-header");
1172
- const Body$1 = leafPart("tbody", "records-body");
1173
- const Footer = leafPart("tfoot", "records-footer");
1174
- const Row = leafPart("tr", "records-row");
1175
- const Head = leafPart("th", "records-head");
1176
- const Cell = leafPart("td", "records-cell");
1177
- const Caption = leafPart("caption", "records-caption");
1178
- const Value = leafPart("span", "record-value");
1179
- const Purpose = leafPart("span", "record-purpose");
1180
- const CardHeader = leafPart("div", "record-card-head");
1181
- const CardTitle = leafPart("strong", "record-card-title");
1182
- const CardContent = leafPart("dl", "record-card-content");
1183
- function Root$5({ count = 0, ...props }) {
1184
- return /* @__PURE__ */ jsx("div", {
1185
- "data-domainkit-part": "records-panel",
1186
- children: usePart("table", props, { count }, { "data-domainkit-part": "records-table" })
1187
- });
1188
- }
1189
- /** A stable React key for a requirement: type, name, and data identify a record. */
1190
- const identity = (record) => [
1191
- rrType(record),
1192
- record.name,
1193
- DnsRecord.data(record)
1194
- ].join(":");
1195
- /**
1196
- * A requirement set keyed by everything it carries, for memos that decide whether to re-send it.
1197
- * `identity` is deliberately narrower: it answers "is this the same record" for a React key, while
1198
- * a plan and an observation are built from `ttl`, `policy`, and `purpose` as well. `policy` in
1199
- * particular decides whether a name may hold anything else, so readiness turns on it.
1200
- */
1201
- const requirementsKey = (records) => JSON.stringify(records);
1202
- function Card$1({ readiness, record, ...props }) {
1203
- const { messages } = useDomainKit();
1204
- const status = statusOf(record, readiness);
1205
- return usePart("section", props, { record: identity(record) }, {
1206
- children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, { children: messages.recordType(record) }), status === null ? null : /* @__PURE__ */ jsx(Status$5, { status })] }), /* @__PURE__ */ jsxs(CardContent, { children: [
1207
- /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: messages.headingName }), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(CopyValue, { value: record.name }) })] }),
1208
- /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: messages.headingValue }), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(CopyValue, { value: DnsRecord.data(record) }) })] }),
1209
- record.purpose === void 0 ? null : /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: messages.headingPurpose }), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(Purpose, { children: record.purpose }) })] })
1210
- ] })] }),
1211
- "data-domainkit-part": "record-card"
1212
- });
1213
- }
1214
- /** The default records presentation: one row per requirement, with readiness when there is any. */
1215
- function Table({ caption, readiness, records, ...props }) {
1216
- const { messages } = useDomainKit();
1217
- const withStatus = readiness !== null && readiness !== void 0;
1218
- return /* @__PURE__ */ jsxs(Root$5, {
1219
- count: records.length,
1220
- ...props,
1221
- children: [
1222
- caption === void 0 ? null : /* @__PURE__ */ jsx(Caption, { children: caption }),
1223
- /* @__PURE__ */ jsx(Header, { children: /* @__PURE__ */ jsxs(Row, { children: [
1224
- /* @__PURE__ */ jsx(Head, {
1225
- scope: "col",
1226
- children: messages.headingType
1227
- }),
1228
- /* @__PURE__ */ jsx(Head, {
1229
- scope: "col",
1230
- children: messages.headingName
1231
- }),
1232
- /* @__PURE__ */ jsx(Head, {
1233
- scope: "col",
1234
- children: messages.headingValue
1235
- }),
1236
- withStatus ? /* @__PURE__ */ jsx(Head, {
1237
- "data-column": "status",
1238
- scope: "col",
1239
- children: messages.headingStatus
1240
- }) : null
1241
- ] }) }),
1242
- /* @__PURE__ */ jsx(Body$1, { children: records.map((record) => {
1243
- const status = statusOf(record, readiness);
1244
- return /* @__PURE__ */ jsxs(Row, { children: [
1245
- /* @__PURE__ */ jsx(Cell, { children: messages.recordType(record) }),
1246
- /* @__PURE__ */ jsx(Cell, { children: /* @__PURE__ */ jsx(CopyValue, { value: record.name }) }),
1247
- /* @__PURE__ */ jsx(Cell, { children: /* @__PURE__ */ jsxs(Value, { children: [/* @__PURE__ */ jsx(CopyValue, { value: DnsRecord.data(record) }), record.purpose === void 0 ? null : /* @__PURE__ */ jsx(Purpose, { children: record.purpose })] }) }),
1248
- withStatus ? /* @__PURE__ */ jsx(Cell, {
1249
- "data-column": "status",
1250
- children: status === null ? null : /* @__PURE__ */ jsx(Status$5, { status })
1251
- }) : null
1252
- ] }, identity(record));
1253
- }) })
1254
- ]
1255
- });
1256
- }
1257
- //#endregion
1258
- //#region src/operations.tsx
1259
- var operations_exports = /* @__PURE__ */ __exportAll({
1260
- Item: () => Item,
1261
- Kind: () => Kind,
1262
- List: () => List,
1263
- Name: () => Name,
1264
- Reason: () => Reason$1,
1265
- Record: () => Record,
1266
- Root: () => Root$4,
1267
- Type: () => Type
1268
- });
1269
- const Kind = leafPart("span", "operation-kind");
1270
- const Type = leafPart("strong", "operation-type");
1271
- const Record = leafPart("span", "operation-record");
1272
- const Name = leafPart("span", "operation-name");
1273
- const Reason$1 = leafPart("span", "operation-reason");
1274
- function Item({ operation, ...props }) {
1275
- const { messages } = useDomainKit();
1276
- return usePart("li", props, { operation: operation._tag }, {
1277
- children: /* @__PURE__ */ jsxs(Fragment, { children: [
1278
- /* @__PURE__ */ jsx(Kind, { children: messages.operation(operation) }),
1279
- " ",
1280
- /* @__PURE__ */ jsx(Type, { children: messages.recordType(operation.record) }),
1281
- " ",
1282
- /* @__PURE__ */ jsxs(Record, { children: [/* @__PURE__ */ jsx(Name, { children: operation.record.name }), /* @__PURE__ */ jsx(CopyValue, { value: DnsRecord.data(operation.record) })] }),
1283
- operation._tag === "Conflict" ? /* @__PURE__ */ jsx(Reason$1, { children: messages.conflictReason(operation.reason) }) : null
1284
- ] }),
1285
- "data-domainkit-part": "operation-item",
1286
- "data-operation": operation._tag
1287
- });
1288
- }
1289
- function Root$4({ count = 0, kind, ...props }) {
1290
- return usePart("ul", props, {
1291
- count,
1292
- kind
1293
- }, {
1294
- "data-domainkit-part": kind === "provisioning" ? "plan-operations" : "cleanup-operations",
1295
- "data-kind": kind
1296
- });
1297
- }
1298
- /** Every operation the plan holds, in plan order, so a customer reviews exactly what will run. */
1299
- function List({ plan, ...props }) {
1300
- return /* @__PURE__ */ jsx(Root$4, {
1301
- count: plan.operations.length,
1302
- kind: plan.kind,
1303
- ...props,
1304
- children: plan.operations.map((operation) => /* @__PURE__ */ jsx(Item, { operation }, operation.id))
1305
- });
1306
- }
1307
- //#endregion
1308
- //#region src/review.tsx
1309
- /**
1310
- * The review surface both `Provision` and `Cleanup` render: what the plan will do, the Approve
1311
- * and Decline actions D35 defines, and the outcome. `kind` picks the wording; nothing else about
1312
- * the two flows differs on screen.
1313
- */
1314
- const busy = (status) => status === "Planning" || status === "Approving" || status === "Applying" || status === "Rejecting";
1315
- /** What the flow is doing right now, or what it finished doing. */
1316
- function Status$4({ controller, kind, ...props }) {
1317
- const { messages } = useDomainKit();
1318
- const state = controller.state;
1319
- const text = () => {
1320
- switch (state._tag) {
1321
- case "Idle": return "";
1322
- case "Planning": return kind === "provisioning" ? messages.planning : messages.planningCleanup;
1323
- case "Planned": return "";
1324
- case "Approving": return messages.approving;
1325
- case "Applying": return kind === "provisioning" ? messages.applying : messages.cleaning;
1326
- case "Rejecting": return messages.rejecting;
1327
- case "Rejected": return messages.declinedBy(state.attempt.rejection?.actorId ?? "");
1328
- case "Applied": return "";
1329
- case "Failure": return "";
1330
- }
1331
- };
1332
- return usePart("p", props, { status: state._tag }, {
1333
- children: text(),
1334
- "data-domainkit-part": kind === "provisioning" ? "provisioning-status" : "cleanup-status",
1335
- "data-state": state._tag,
1336
- role: busy(state._tag) ? "status" : void 0
1337
- });
1338
- }
1339
- /**
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.
1343
- */
1344
- function Outcome$4({ children, controller, kind, ...props }) {
1345
- const { messages } = useDomainKit();
1346
- const readOnly = useReadOnly();
1347
- const state = controller.state;
1348
- const retryPart = kind === "provisioning" ? "provisioning-retry" : "cleanup-retry";
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 ? {
1352
- description: kind === "provisioning" ? messages.partiallyApplied : messages.partiallyCleaned,
1353
- title: kind === "provisioning" ? messages.partiallyAppliedTitle : messages.partiallyCleanedTitle
1354
- } : {
1355
- description: "",
1356
- title: kind === "provisioning" ? messages.applied(state.receipt) : messages.cleaned(state.receipt)
1357
- };
1358
- return /* @__PURE__ */ jsx(Provider, {
1359
- value: {
1360
- description: words.description ?? "",
1361
- layout: props.layout ?? "card",
1362
- retry: readOnly || state._tag !== "Failure" ? null : controller.retry,
1363
- retryPart,
1364
- title: words.title,
1365
- tone: state._tag === "Failure" ? "danger" : partial ? "warning" : "success"
1366
- },
1367
- children: /* @__PURE__ */ jsx(Root$6, {
1368
- ...props,
1369
- children: children ?? /* @__PURE__ */ jsx(Composition, {})
1370
- })
1371
- });
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");
1375
- /**
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.
1380
- */
1381
- function Actions$2({ controller, kind, ...props }) {
1382
- const { messages } = useDomainKit();
1383
- const readOnly = useReadOnly();
1384
- const state = controller.state;
1385
- const plan = state._tag === "Planned" ? state.plan : null;
1386
- const running = busy(state._tag);
1387
- const writes = addable(plan);
1388
- const blocked = plan !== null && writes.length === 0;
1389
- const element = usePart("div", props, { status: state._tag }, {
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
- ] }),
1410
- "data-domainkit-part": "review-actions"
1411
- });
1412
- if (readOnly) return null;
1413
- return plan === null ? null : element;
1414
- }
1415
- /** The operations under review, or the reason there is nothing to review. */
1416
- function Body({ controller, kind }) {
1417
- const { messages } = useDomainKit();
1418
- const state = controller.state;
1419
- const plan = state._tag === "Planned" || state._tag === "Approving" || state._tag === "Applying" || state._tag === "Rejecting" || state._tag === "Rejected" ? state.plan : state._tag === "Applied" ? state.plan : null;
1420
- return /* @__PURE__ */ jsxs("div", {
1421
- "data-domainkit-part": "dialog-body",
1422
- children: [
1423
- /* @__PURE__ */ jsx(Status$4, {
1424
- controller,
1425
- kind
1426
- }),
1427
- plan === null ? null : plan.operations.length === 0 ? /* @__PURE__ */ jsx("p", {
1428
- "data-domainkit-part": "plan-empty",
1429
- children: messages.noChanges
1430
- }) : /* @__PURE__ */ jsx(List, { plan }),
1431
- state._tag === "Rejected" && state.attempt.rejection?.reason != null ? /* @__PURE__ */ jsx("p", {
1432
- "data-domainkit-part": "plan-decline-reason",
1433
- children: messages.declineReason(state.attempt.rejection.reason)
1434
- }) : null,
1435
- /* @__PURE__ */ jsx(Outcome$4, {
1436
- controller,
1437
- kind
1438
- })
1439
- ]
1440
- });
1441
- }
1442
- /** The review dialog. `render` swaps the surface; the contents stay the flow's. */
1443
- function Dialog$4({ children, controller, kind, onOpenChange, open, render, trigger }) {
1444
- const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
1445
- const readOnly = useReadOnly();
1446
- const [uncontrolled, setUncontrolled] = useState(false);
1447
- const isOpen = open ?? uncontrolled;
1448
- const close = useRef(() => {});
1449
- close.current = (next) => {
1450
- if (open === void 0) setUncontrolled(next);
1451
- onOpenChange?.(next);
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]);
1459
- const running = busy(controller.state._tag);
1460
- const body = children ?? /* @__PURE__ */ jsx(Body, {
1461
- controller,
1462
- kind
1463
- });
1464
- const title = kind === "provisioning" ? messages.planTitle : messages.cleanupTitle;
1465
- const label = trigger ?? (kind === "provisioning" ? messages.reviewChanges : messages.cleanUp);
1466
- if (readOnly) return /* @__PURE__ */ jsx(Fragment, {});
1467
- if (render !== void 0) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
1468
- "data-domainkit-part": kind === "provisioning" ? "plan-trigger" : "cleanup-trigger",
1469
- onClick: () => {
1470
- setOpen(true);
1471
- controller.plan();
1472
- },
1473
- type: "button",
1474
- children: label
1475
- }), render({
1476
- children: body,
1477
- open: isOpen
1478
- })] });
1479
- return /* @__PURE__ */ jsxs(Dialog.Root, {
1480
- onOpenChange: (next, details) => {
1481
- if (!next && running) {
1482
- details.cancel();
1483
- return;
1484
- }
1485
- setOpen(next);
1486
- },
1487
- open: isOpen,
1488
- children: [/* @__PURE__ */ jsx(Dialog.Trigger, {
1489
- "data-domainkit-part": kind === "provisioning" ? "plan-trigger" : "cleanup-trigger",
1490
- onClick: controller.plan,
1491
- children: label
1492
- }), /* @__PURE__ */ jsxs(Dialog.Portal, {
1493
- container: portalContainer,
1494
- children: [/* @__PURE__ */ jsx(Dialog.Backdrop, {
1495
- "data-color-scheme": colorScheme,
1496
- "data-domainkit-part": "dialog-backdrop",
1497
- "data-domainkit-root": "",
1498
- style: themeStyle
1499
- }), /* @__PURE__ */ jsxs(Dialog.Popup, {
1500
- "data-color-scheme": colorScheme,
1501
- "data-domainkit-part": kind === "provisioning" ? "plan-dialog" : "cleanup-dialog",
1502
- "data-domainkit-root": "",
1503
- style: themeStyle,
1504
- children: [
1505
- /* @__PURE__ */ jsxs("div", {
1506
- "data-domainkit-part": "dialog-header",
1507
- children: [/* @__PURE__ */ jsxs("div", {
1508
- "data-domainkit-part": "dialog-heading",
1509
- children: [/* @__PURE__ */ jsx(Dialog.Title, {
1510
- "data-domainkit-part": "dialog-title",
1511
- children: title
1512
- }), /* @__PURE__ */ jsx(Dialog.Description, {
1513
- "data-domainkit-part": "dialog-description",
1514
- children: kind === "provisioning" ? messages.planConsent : messages.cleanupConsent
1515
- })]
1516
- }), running ? null : /* @__PURE__ */ jsx(Dialog.Close, {
1517
- "aria-label": messages.close,
1518
- "data-domainkit-part": "dialog-close",
1519
- children: "×"
1520
- })]
1521
- }),
1522
- body,
1523
- /* @__PURE__ */ jsxs("div", {
1524
- "data-domainkit-part": "dialog-footer",
1525
- children: [/* @__PURE__ */ jsx(Actions$2, {
1526
- controller,
1527
- kind
1528
- }), running || kind === "provisioning" ? null : /* @__PURE__ */ jsx(Dialog.Close, {
1529
- "data-domainkit-part": "dialog-cancel",
1530
- children: messages.cancel
1531
- })]
1532
- })
1533
- ]
1534
- })]
1535
- })]
1536
- });
1537
- }
1538
- //#endregion
1539
- //#region src/cleanup.tsx
758
+ //#region src/cleanup.ts
1540
759
  var cleanup_exports = /* @__PURE__ */ __exportAll({
1541
- Actions: () => Actions$1,
1542
- Dialog: () => Dialog$3,
1543
- Flow: () => Flow$3,
1544
- Outcome: () => Outcome$3,
1545
- Root: () => Root$3,
1546
760
  State: () => State$2,
1547
- Status: () => Status$3,
761
+ pendingPlan: () => pendingPlan,
762
+ planOf: () => planOf,
1548
763
  useController: () => useController$3
1549
764
  });
1550
765
  /**
1551
766
  * Cleanup is bound to a receipt: it removes only what an apply proved DomainKit created. When the
1552
767
  * host does not name one, the controller reads the domain's latest from the snapshot.
1553
768
  */
1554
- function useController$3({ domain, onCleaned, receiptId }) {
769
+ function useController$3({ domain, onCleaned, readOnly, receiptId }) {
1555
770
  const { transport } = useDomainKit();
1556
771
  const group = transport.cleanup;
1557
772
  const connection = transport.connection;
@@ -1564,6 +779,7 @@ function useController$3({ domain, onCleaned, receiptId }) {
1564
779
  }),
1565
780
  group,
1566
781
  onDone: onCleaned,
782
+ readOnly,
1567
783
  plan: useCallback(() => {
1568
784
  if (group === void 0) return null;
1569
785
  if (receiptId !== void 0) return group.plan(receiptId);
@@ -1580,95 +796,305 @@ function useController$3({ domain, onCleaned, receiptId }) {
1580
796
  ])
1581
797
  });
1582
798
  }
1583
- function Root$3({ controller, ...props }) {
1584
- return usePart("div", props, { status: controller.state._tag }, {
1585
- "data-domainkit-part": "cleanup-flow",
1586
- "data-state": controller.state._tag
1587
- });
1588
- }
1589
- function Dialog$3(props) {
1590
- return /* @__PURE__ */ jsx(Dialog$4, {
1591
- ...props,
1592
- kind: "cleanup"
1593
- });
1594
- }
1595
- function Actions$1(props) {
1596
- return /* @__PURE__ */ jsx(Actions$2, {
1597
- ...props,
1598
- kind: "cleanup"
1599
- });
1600
- }
1601
- function Outcome$3(props) {
1602
- return /* @__PURE__ */ jsx(Outcome$4, {
1603
- ...props,
1604
- kind: "cleanup"
1605
- });
1606
- }
1607
- function Status$3(props) {
1608
- return /* @__PURE__ */ jsx(Status$4, {
1609
- ...props,
1610
- kind: "cleanup"
1611
- });
799
+ //#endregion
800
+ //#region src/domain-field.ts
801
+ const ZonesState = Data.taggedEnum();
802
+ const nothing = {
803
+ connections: [],
804
+ providers: [],
805
+ zones: []
806
+ };
807
+ /**
808
+ * Every zone this owner's connections reach. A connection whose credential the provider turned
809
+ * down is listed in `connections` as `reconnect` and contributes no zones, so one dead account
810
+ * never costs the customer the others.
811
+ */
812
+ function useZones({ provider } = {}) {
813
+ const { revision, transport } = useDomainKit();
814
+ const connection = transport.connection;
815
+ const runner = useRunner();
816
+ const [state, setState] = useState(ZonesState.Loading());
817
+ const load = useCallback(() => {
818
+ if (connection === void 0) return;
819
+ setState(ZonesState.Loading());
820
+ runner.run(connection.zones(provider === void 0 ? {} : { provider }), {
821
+ onFailure: (error) => setState(ZonesState.Failure({ error })),
822
+ onSuccess: (listing) => setState(ZonesState.Ready({
823
+ connections: listing.connections,
824
+ providers: listing.providers,
825
+ zones: listing.zones
826
+ }))
827
+ });
828
+ }, [
829
+ connection,
830
+ provider,
831
+ runner
832
+ ]);
833
+ useEffect(load, [load, revision]);
834
+ const held = state._tag === "Ready" ? state : nothing;
835
+ return {
836
+ connections: held.connections,
837
+ providers: held.providers,
838
+ refresh: load,
839
+ state,
840
+ zones: held.zones
841
+ };
1612
842
  }
1613
- /** Cleanup on its own: a trigger that plans the removal, then the same review dialog. */
1614
- function Flow$3({ domain, onCleaned, receiptId, trigger, ...props }) {
1615
- const controller = useController$3({
1616
- domain,
1617
- ...onCleaned === void 0 ? {} : { onCleaned },
1618
- ...receiptId === void 0 ? {} : { receiptId }
1619
- });
1620
- const [open, setOpen] = useState(false);
1621
- return /* @__PURE__ */ jsxs(Root$3, {
1622
- controller,
1623
- ...props,
1624
- children: [/* @__PURE__ */ jsx(Dialog$3, {
1625
- controller,
1626
- onOpenChange: setOpen,
1627
- open,
1628
- ...trigger === void 0 ? {} : { trigger }
1629
- }), open ? null : /* @__PURE__ */ jsx(Outcome$3, { controller })]
1630
- });
843
+ const AccountsState = Data.taggedEnum();
844
+ const currentUrl$1 = () => typeof window === "undefined" ? null : window.location.href;
845
+ /**
846
+ * The accounts this owner holds, and the one way to add another: connect a provider without naming
847
+ * a domain. A token method finishes in place; an interactive one redirects and comes back through
848
+ * the library's own callback, which lands on `returnTo`.
849
+ */
850
+ function useAccounts({ readOnly: refused, returnTo } = {}) {
851
+ const { navigate, transport } = useDomainKit();
852
+ const inherited = useReadOnly();
853
+ const readOnly = refused ?? inherited;
854
+ const connection = transport.connection;
855
+ const runner = useRunner();
856
+ const listing = useZones();
857
+ const [command, setCommand] = useState(null);
858
+ const refresh = listing.refresh;
859
+ const submit = useCallback((input) => {
860
+ if (connection === void 0 || readOnly) return;
861
+ const destination = returnTo === void 0 ? currentUrl$1() : returnTo;
862
+ const interactive = destination === null ? {} : { returnTo: destination };
863
+ const method = input.method === "token" ? Transport.Method.token(input.values ?? {}) : input.method === "oauth" ? Transport.Method.oauth(interactive) : Transport.Method.integration(interactive);
864
+ setCommand(AccountsState.Submitting({ provider: input.provider }));
865
+ const granting = input.connectionId === void 0 ? connection.start({
866
+ method,
867
+ provider: input.provider
868
+ }) : connection.reconnect({
869
+ connectionId: input.connectionId,
870
+ method
871
+ });
872
+ runner.run(granting, {
873
+ onFailure: (error) => setCommand(AccountsState.Failure({ error })),
874
+ onSuccess: (started) => {
875
+ if (started._tag === "Redirect") {
876
+ setCommand(AccountsState.Redirecting({ url: started.authorizationUrl }));
877
+ navigate(started.authorizationUrl);
878
+ return;
879
+ }
880
+ setCommand(null);
881
+ refresh();
882
+ }
883
+ });
884
+ }, [
885
+ connection,
886
+ navigate,
887
+ readOnly,
888
+ refresh,
889
+ returnTo,
890
+ runner
891
+ ]);
892
+ const connect = useCallback((input) => submit(input), [submit]);
893
+ const reconnect = useCallback((input) => submit(input), [submit]);
894
+ const state = command ?? (listing.state._tag === "Loading" ? AccountsState.Loading() : listing.state._tag === "Failure" ? AccountsState.Failure({ error: listing.state.error }) : AccountsState.Ready());
895
+ return {
896
+ connect,
897
+ reconnect,
898
+ connections: listing.connections,
899
+ providers: listing.providers,
900
+ refresh,
901
+ state,
902
+ zones: listing.zones
903
+ };
1631
904
  }
1632
- //#endregion
1633
- //#region src/provider.tsx
1634
- var provider_exports = /* @__PURE__ */ __exportAll({ Mark: () => Mark });
1635
- const key = (providerId) => providerId.trim().toLowerCase().replaceAll(/[^a-z0-9]+/g, "");
1636
- /** No network call: the fallback is the provider's own initial, drawn with the theme's tokens. */
1637
- const monogram = (provider) => provider.name.trim().charAt(0).toUpperCase() || "?";
905
+ const normalize = (value) => value.trim().toLowerCase().replace(/\.$/, "");
906
+ /**
907
+ * The zone a value already sits in: the longest listed zone the value is at or under. Two accounts
908
+ * can serve the same zone, so `preferred` names the one the customer picked and wins among equals;
909
+ * without it the first listed account answers.
910
+ */
911
+ const placementOf = (value, zones, preferred) => {
912
+ const domain = normalize(value);
913
+ if (domain === "") return null;
914
+ const matching = zones.filter((candidate) => {
915
+ const zone = normalize(candidate.zone);
916
+ return domain === zone || domain.endsWith(`.${zone}`);
917
+ });
918
+ const longest = matching.reduce((best, candidate) => best === null || normalize(candidate.zone).length > normalize(best.zone).length ? candidate : best, null);
919
+ if (longest === null) return null;
920
+ const closest = matching.filter((candidate) => normalize(candidate.zone) === normalize(longest.zone));
921
+ const best = closest.find((candidate) => preferred != null && candidate.connectionId === preferred.connectionId && normalize(candidate.zone) === normalize(preferred.zone)) ?? closest[0] ?? longest;
922
+ return {
923
+ placement: {
924
+ connectionId: best.connectionId,
925
+ zone: best.zone
926
+ },
927
+ zone: best
928
+ };
929
+ };
1638
930
  /**
1639
- * A provider's artwork. `DomainKit.Root`'s `marks` decides what a provider looks like; without an
1640
- * entry the mark is the provider's initial. Nothing is fetched at render time.
931
+ * What the customer typed in front of the zone they are reaching for. "mail.ex" against
932
+ * `example.com` is the subdomain "mail"; "ex" on its own is none.
1641
933
  */
1642
- function Mark({ provider, ...props }) {
1643
- const { marks } = useDomainKit();
1644
- const replacement = marks[provider.id];
1645
- const content = typeof replacement === "function" ? replacement(provider) : replacement ?? monogram(provider);
1646
- return usePart("span", props, { providerId: provider.id }, {
1647
- "aria-label": provider.name,
1648
- children: content,
1649
- "data-domainkit-part": "provider-mark",
1650
- "data-domainkit-provider": key(provider.id),
1651
- role: "img"
934
+ const subdomainOf = (value, zone) => {
935
+ const typed = normalize(value);
936
+ const target = normalize(zone);
937
+ if (typed === "" || target.startsWith(typed)) return "";
938
+ const cut = typed.lastIndexOf(".");
939
+ if (cut < 0) return "";
940
+ const head = typed.slice(0, cut);
941
+ const tail = typed.slice(cut + 1);
942
+ return target.startsWith(tail) ? head : "";
943
+ };
944
+ /** Zones the typed value is reaching for: a prefix of the zone, or a subdomain of one. */
945
+ const suggestionsFor = (value, zones) => {
946
+ const typed = normalize(value);
947
+ if (typed === "") return zones;
948
+ return zones.filter((candidate) => {
949
+ const zone = normalize(candidate.zone);
950
+ if (zone.startsWith(typed)) return true;
951
+ const cut = typed.lastIndexOf(".");
952
+ if (cut < 0) return false;
953
+ const tail = typed.slice(cut + 1);
954
+ return tail !== "" && zone.startsWith(tail);
1652
955
  });
956
+ };
957
+ /** The value a suggestion completes to: whatever subdomain was typed, then the zone. */
958
+ const completionOf = (value, zone) => {
959
+ const subdomain = subdomainOf(value, zone.zone);
960
+ return subdomain === "" ? zone.zone : `${subdomain}.${zone.zone}`;
961
+ };
962
+ const optionId = (listId, zone) => `${listId}-${zone.connectionId}-${zone.zone}`;
963
+ /**
964
+ * One input over every zone the workspace's accounts reach, as props rather than markup. Arrow
965
+ * keys move the highlight, Tab or Enter completes it, Escape closes the list, and `found` says
966
+ * where the records will go.
967
+ */
968
+ function useDomainField({ id, onChange, onResolve, value, zones }) {
969
+ const messages = useMessages();
970
+ const generated = useId();
971
+ const inputId = id ?? generated;
972
+ const listId = `${inputId}-suggestions`;
973
+ const [open, setOpen] = useState(false);
974
+ const [active, setActive] = useState(0);
975
+ const [chosen, setChosen] = useState(null);
976
+ const suggestions = suggestionsFor(value, zones);
977
+ const highlighted = suggestions[Math.min(active, Math.max(suggestions.length - 1, 0))] ?? null;
978
+ const found = placementOf(value, zones, chosen);
979
+ const showing = open && suggestions.length > 0;
980
+ const placement = found?.placement ?? null;
981
+ const connectionId = placement?.connectionId ?? null;
982
+ const placedIn = placement?.zone ?? null;
983
+ const announced = useRef(null);
984
+ useEffect(() => {
985
+ const signature = `${value}|${connectionId ?? ""}|${placedIn ?? ""}`;
986
+ if (announced.current === signature) return;
987
+ announced.current = signature;
988
+ onResolve?.({
989
+ connection: connectionId === null || placedIn === null ? null : {
990
+ connectionId,
991
+ zone: placedIn
992
+ },
993
+ domain: value
994
+ });
995
+ }, [
996
+ connectionId,
997
+ onResolve,
998
+ placedIn,
999
+ value
1000
+ ]);
1001
+ const complete = (zone) => {
1002
+ onChange(completionOf(value, zone));
1003
+ setChosen({
1004
+ connectionId: zone.connectionId,
1005
+ zone: zone.zone
1006
+ });
1007
+ setOpen(false);
1008
+ setActive(0);
1009
+ };
1010
+ return {
1011
+ close: () => setOpen(false),
1012
+ complete,
1013
+ found,
1014
+ highlighted,
1015
+ inputProps: {
1016
+ "aria-activedescendant": showing && highlighted !== null ? optionId(listId, highlighted) : void 0,
1017
+ "aria-autocomplete": "list",
1018
+ "aria-controls": listId,
1019
+ "aria-expanded": showing,
1020
+ autoComplete: "off",
1021
+ id: inputId,
1022
+ onChange: (event) => {
1023
+ onChange(event.target.value);
1024
+ setActive(0);
1025
+ setOpen(true);
1026
+ },
1027
+ onFocus: () => setOpen(true),
1028
+ onKeyDown: (event) => {
1029
+ if (event.key === "Escape") {
1030
+ setOpen(false);
1031
+ return;
1032
+ }
1033
+ if (suggestions.length === 0) return;
1034
+ if (event.key === "ArrowDown") {
1035
+ event.preventDefault();
1036
+ setOpen(true);
1037
+ setActive((index) => (index + 1) % suggestions.length);
1038
+ return;
1039
+ }
1040
+ if (event.key === "ArrowUp") {
1041
+ event.preventDefault();
1042
+ setOpen(true);
1043
+ setActive((index) => (index + suggestions.length - 1) % suggestions.length);
1044
+ return;
1045
+ }
1046
+ if ((event.key === "Tab" || event.key === "Enter") && showing && highlighted !== null) {
1047
+ event.preventDefault();
1048
+ complete(highlighted);
1049
+ }
1050
+ },
1051
+ role: "combobox",
1052
+ type: "text",
1053
+ value
1054
+ },
1055
+ listboxProps: {
1056
+ "aria-label": messages.zoneSuggestions,
1057
+ hidden: !showing,
1058
+ id: listId,
1059
+ role: "listbox"
1060
+ },
1061
+ open: showing,
1062
+ optionProps: (zone) => ({
1063
+ "aria-selected": zone === highlighted,
1064
+ id: optionId(listId, zone),
1065
+ onMouseDown: (event) => {
1066
+ event.preventDefault();
1067
+ complete(zone);
1068
+ },
1069
+ role: "option"
1070
+ }),
1071
+ suggestions
1072
+ };
1653
1073
  }
1654
1074
  //#endregion
1655
- //#region src/connect.tsx
1075
+ //#region src/connect.ts
1656
1076
  var connect_exports = /* @__PURE__ */ __exportAll({
1657
- Card: () => Card,
1658
- Dialog: () => Dialog$2,
1659
- DisconnectDialog: () => DisconnectDialog,
1660
- Flow: () => Flow$2,
1661
- Form: () => Form,
1662
- Outcome: () => Outcome$2,
1663
- Prompt: () => Prompt,
1664
- Root: () => Root$2,
1077
+ AccountsState: () => AccountsState,
1665
1078
  State: () => State$1,
1666
- Status: () => Status$2,
1079
+ ZonesState: () => ZonesState,
1667
1080
  answeredInPlace: () => answeredInPlace,
1081
+ attempted: () => attempted,
1082
+ completionOf: () => completionOf,
1083
+ describeMethods: () => describeMethods,
1084
+ displayName: () => displayName,
1668
1085
  holdsConnection: () => holdsConnection,
1669
1086
  hostProvider: () => hostProvider,
1670
1087
  offering: () => offering,
1671
- useController: () => useController$2
1088
+ placementOf: () => placementOf,
1089
+ providerOf: () => providerOf,
1090
+ reconnect: () => reconnect,
1091
+ rejectedField: () => rejectedField,
1092
+ reusableConnections: () => reusableConnections,
1093
+ suggestionsFor: () => suggestionsFor,
1094
+ useAccounts: () => useAccounts,
1095
+ useController: () => useController$2,
1096
+ useDomainField: () => useDomainField,
1097
+ useZones: () => useZones
1672
1098
  });
1673
1099
  const State$1 = Data.taggedEnum();
1674
1100
  /**
@@ -1720,10 +1146,12 @@ const settled = (snapshot, discovery) => {
1720
1146
  * transport declares it and the domain has no connection yet; a discovery failure is not the
1721
1147
  * customer's problem, so the provider list still renders.
1722
1148
  */
1723
- function useController$2({ domain, returnTo }) {
1149
+ function useController$2({ domain, readOnly: refused, returnTo }) {
1724
1150
  const { emit, navigate, revision, transport } = useDomainKit();
1725
- const readOnly = useReadOnly();
1151
+ const inherited = useReadOnly();
1152
+ const readOnly = refused ?? inherited;
1726
1153
  const connection = transport.connection;
1154
+ const provisioning = transport.provisioning;
1727
1155
  const runner = useRunner();
1728
1156
  const [state, setState] = useState(State$1.Loading({ snapshot: null }));
1729
1157
  const held = useRef({
@@ -1797,20 +1225,26 @@ function useController$2({ domain, returnTo }) {
1797
1225
  const started = useCallback((result) => {
1798
1226
  if (held.current.domain !== domain) return;
1799
1227
  switch (result._tag) {
1800
- case "Connected":
1228
+ case "Connected": {
1229
+ const snapshot = result.snapshot;
1230
+ setEstablished((count) => count + 1);
1231
+ if (snapshot === null) {
1232
+ load();
1233
+ return;
1234
+ }
1801
1235
  held.current = {
1802
1236
  discovery: null,
1803
1237
  domain,
1804
- snapshot: result.snapshot
1238
+ snapshot
1805
1239
  };
1806
- setEstablished((count) => count + 1);
1807
- setState(settled(result.snapshot, null));
1808
- if (result.snapshot.connectionId !== null) emit(Event.Connected({
1809
- connectionId: result.snapshot.connectionId,
1240
+ setState(settled(snapshot, null));
1241
+ if (snapshot.connectionId !== null) emit(Event.Connected({
1242
+ connectionId: snapshot.connectionId,
1810
1243
  domain,
1811
- snapshot: result.snapshot
1244
+ snapshot
1812
1245
  }));
1813
1246
  return;
1247
+ }
1814
1248
  case "Redirect":
1815
1249
  markReturn(domain);
1816
1250
  setState(State$1.Redirecting({ url: result.authorizationUrl }));
@@ -1827,6 +1261,7 @@ function useController$2({ domain, returnTo }) {
1827
1261
  }, [
1828
1262
  domain,
1829
1263
  emit,
1264
+ load,
1830
1265
  navigate
1831
1266
  ]);
1832
1267
  const submit = useCallback((effect, attempt) => {
@@ -1854,11 +1289,16 @@ function useController$2({ domain, returnTo }) {
1854
1289
  started
1855
1290
  ]);
1856
1291
  const connect = useCallback((input) => {
1857
- if (connection === void 0) return;
1292
+ if (connection === void 0 || readOnly) return;
1858
1293
  const destination = input.returnTo ?? (returnTo === void 0 ? currentUrl() : returnTo);
1859
1294
  const interactive = destination === null ? {} : { returnTo: destination };
1860
1295
  const method = input.method === "token" ? Transport.Method.token(input.values ?? {}) : input.method === "oauth" ? Transport.Method.oauth(interactive) : Transport.Method.integration(interactive);
1861
- submit(connection.start({
1296
+ const snapshot = held.current.snapshot;
1297
+ const stale = snapshot?.status === "reconnect" && snapshot.provider === input.provider && snapshot.connectionId !== null;
1298
+ submit(stale && snapshot?.connectionId != null ? connection.reconnect({
1299
+ connectionId: snapshot.connectionId,
1300
+ method
1301
+ }) : connection.start({
1862
1302
  domain,
1863
1303
  method,
1864
1304
  provider: input.provider
@@ -1869,11 +1309,12 @@ function useController$2({ domain, returnTo }) {
1869
1309
  }, [
1870
1310
  connection,
1871
1311
  domain,
1312
+ readOnly,
1872
1313
  returnTo,
1873
1314
  submit
1874
1315
  ]);
1875
1316
  const reuse = useCallback((input) => {
1876
- if (connection === void 0) return;
1317
+ if (connection === void 0 || readOnly) return;
1877
1318
  submit(connection.attach({
1878
1319
  connectionId: input.connectionId,
1879
1320
  domain,
@@ -1882,6 +1323,7 @@ function useController$2({ domain, returnTo }) {
1882
1323
  }, [
1883
1324
  connection,
1884
1325
  domain,
1326
+ readOnly,
1885
1327
  submit
1886
1328
  ]);
1887
1329
  const select = useCallback((zone) => {
@@ -1920,17 +1362,20 @@ function useController$2({ domain, returnTo }) {
1920
1362
  runner
1921
1363
  ]);
1922
1364
  const detach = useCallback(() => {
1923
- const attachmentId = held.current.snapshot?.attachmentId;
1924
- if (connection === void 0 || attachmentId == null || held.current.domain !== domain) return;
1365
+ const attachmentId = held.current.snapshot?.attachment?.id;
1366
+ if (readOnly || connection === void 0 || attachmentId == null) return;
1367
+ if (held.current.domain !== domain) return;
1925
1368
  release(connection.detach(attachmentId), Event.Detached({ domain }));
1926
1369
  }, [
1927
1370
  connection,
1928
1371
  domain,
1372
+ readOnly,
1929
1373
  release
1930
1374
  ]);
1931
1375
  const disconnect = useCallback(() => {
1932
1376
  const connectionId = held.current.snapshot?.connectionId;
1933
- if (connection === void 0 || connectionId == null || held.current.domain !== domain) return;
1377
+ if (readOnly || connection === void 0 || connectionId == null) return;
1378
+ if (held.current.domain !== domain) return;
1934
1379
  release(connection.disconnect(connectionId), Event.Disconnected({
1935
1380
  connectionId,
1936
1381
  domain
@@ -1938,6 +1383,7 @@ function useController$2({ domain, returnTo }) {
1938
1383
  }, [
1939
1384
  connection,
1940
1385
  domain,
1386
+ readOnly,
1941
1387
  release
1942
1388
  ]);
1943
1389
  const retry = useCallback(() => {
@@ -1949,6 +1395,44 @@ function useController$2({ domain, returnTo }) {
1949
1395
  load,
1950
1396
  readOnly
1951
1397
  ]);
1398
+ const sighted = useRef(null);
1399
+ useEffect(() => {
1400
+ if (readOnly || state._tag !== "Disconnected") return;
1401
+ const discovery = state.discovery;
1402
+ if (discovery === null || discovery._tag !== "Resolved") return;
1403
+ const sight = `${domain}|${discovery.connectionId}|${discovery.zone}`;
1404
+ if (sighted.current === sight) return;
1405
+ sighted.current = sight;
1406
+ reuse({
1407
+ connectionId: discovery.connectionId,
1408
+ zone: discovery.zone
1409
+ });
1410
+ }, [
1411
+ domain,
1412
+ readOnly,
1413
+ reuse,
1414
+ state
1415
+ ]);
1416
+ const [receipt, setReceipt] = useState(null);
1417
+ const receiptId = snapshotOf(state)?.lastReceiptId ?? null;
1418
+ const readReceipt = useRef(null);
1419
+ useEffect(() => {
1420
+ if (receiptId === null) {
1421
+ readReceipt.current = null;
1422
+ setReceipt(null);
1423
+ return;
1424
+ }
1425
+ if (provisioning === void 0 || readReceipt.current === receiptId) return;
1426
+ readReceipt.current = receiptId;
1427
+ runner.run(provisioning.receipt(Receipt.ReceiptId.make(receiptId)), {
1428
+ onFailure: () => setReceipt(null),
1429
+ onSuccess: (value) => setReceipt(value.id === receiptId ? value : null)
1430
+ });
1431
+ }, [
1432
+ provisioning,
1433
+ receiptId,
1434
+ runner
1435
+ ]);
1952
1436
  return {
1953
1437
  connect,
1954
1438
  detach,
@@ -1957,6 +1441,7 @@ function useController$2({ domain, returnTo }) {
1957
1441
  established,
1958
1442
  discovery: state._tag === "Disconnected" || state._tag === "Failure" || state._tag === "Submitting" ? state.discovery : null,
1959
1443
  providers: snapshotOf(state)?.providers ?? [],
1444
+ receipt: receipt?.id === receiptId ? receipt : null,
1960
1445
  refresh: load,
1961
1446
  retry,
1962
1447
  reuse,
@@ -1965,24 +1450,24 @@ function useController$2({ domain, returnTo }) {
1965
1450
  state
1966
1451
  };
1967
1452
  }
1968
- function Root$2({ controller, ...props }) {
1969
- return usePart("div", props, { status: controller.state._tag }, {
1970
- "data-domainkit-part": "connection-root",
1971
- "data-state": controller.state._tag
1972
- });
1973
- }
1974
1453
  /**
1975
1454
  * Whether a failure is already answered where it was raised: beside the method the customer used.
1976
- * A flow rendering its own `Outcome` beside the dialog skips it, so the failure announces once.
1455
+ * A surface rendering one outcome for the whole flow skips it, so the failure announces once.
1977
1456
  */
1978
1457
  const answeredInPlace = (controller) => controller.state._tag === "Failure" && controller.state.attempt !== null;
1979
1458
  /**
1980
1459
  * 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.
1460
+ * connection that was. The provider row and the dialog it opened stay on screen for the length of
1461
+ * the command rather than vanishing under the customer half way through it.
1983
1462
  */
1984
1463
  const holdsConnection = (controller) => controller.state._tag === "Connected" || controller.state._tag === "Submitting" && controller.snapshot?.status === "connected";
1985
1464
  /**
1465
+ * Whether the provider turned the credential down, so the customer must prove the account again.
1466
+ * Proving it re-credits the connection the workspace already holds and leaves its domains where
1467
+ * they are, which is why the surface offers a reconnect rather than a second account.
1468
+ */
1469
+ const reconnect = (controller) => controller.state._tag === "Reconnect" || controller.state._tag === "Submitting" && controller.snapshot?.status === "reconnect";
1470
+ /**
1986
1471
  * The provider whose nameservers serve this domain, as the snapshot describes it. Discovery names
1987
1472
  * it by id; without a descriptor there is nothing to draw, so there is no host.
1988
1473
  */
@@ -1992,816 +1477,227 @@ const hostProvider = (controller) => {
1992
1477
  const named = discovery.host.provider;
1993
1478
  return controller.providers.find((entry) => entry.id === named) ?? null;
1994
1479
  };
1480
+ /** The descriptor for one provider id, or `null` when the snapshot describes no such provider. */
1481
+ const providerOf = (controller, providerId) => providerId == null ? null : controller.providers.find((entry) => entry.id === providerId) ?? null;
1995
1482
  /** The provider as the customer knows it, falling back to the id when the snapshot has no name. */
1996
- const displayName = (controller, provider) => controller.providers.find((entry) => entry.id === provider)?.name ?? provider;
1483
+ const displayName = (controller, providerId) => providerOf(controller, providerId)?.name ?? providerId;
1997
1484
  /**
1998
- * The last failure as a card: media, the catalog's title and description, and the retry the flow
1999
- * allows. Children replace the composition and keep the binding.
1485
+ * Whether the connect surface has anything to offer for this domain: a provider that serves the
1486
+ * zone, a connection discovery already found, one the owner already holds, or a command already
1487
+ * running. Never while a connection is held, which is what `holdsConnection` answers. A host reads
1488
+ * it to order its own offers beside DomainKit's rather than competing with them.
2000
1489
  */
2001
- function Outcome$2({ children, controller, ...props }) {
2002
- const { messages } = useDomainKit();
2003
- const readOnly = useReadOnly();
2004
- const state = controller.state;
2005
- if (state._tag !== "Failure") return null;
2006
- const acted = state.attempt === null ? void 0 : displayName(controller, state.attempt.provider);
2007
- const words = outcome(state.error, messages, {
2008
- ...controller.snapshot === null ? {} : { domain: controller.snapshot.domain },
2009
- ...acted === void 0 ? {} : { provider: acted }
2010
- });
2011
- return /* @__PURE__ */ jsx(Provider, {
2012
- value: {
2013
- description: words.description ?? "",
2014
- layout: props.layout ?? "card",
2015
- retry: readOnly ? null : controller.retry,
2016
- retryPart: "connection-retry",
2017
- title: words.title,
2018
- tone: "danger"
2019
- },
2020
- children: /* @__PURE__ */ jsx(Root$6, {
2021
- ...props,
2022
- children: children ?? /* @__PURE__ */ jsx(Composition, {})
2023
- })
2024
- });
2025
- }
2026
- function Status$2({ controller, ...props }) {
2027
- const { messages } = useDomainKit();
1490
+ const offering = (controller, connect = "detected") => {
1491
+ if (holdsConnection(controller)) return false;
2028
1492
  const state = controller.state;
2029
- const attached = controller.snapshot?.provider ?? null;
2030
- const provider = attached === null ? "" : displayName(controller, attached);
2031
- const text = () => {
2032
- switch (state._tag) {
2033
- case "Loading": return messages.loading;
2034
- case "Submitting": return messages.connecting;
2035
- case "Redirecting": return messages.redirecting;
2036
- case "Connected": return messages.connectedTo(provider);
2037
- case "Reconnect": return messages.reconnectRequired(provider);
2038
- case "SelectionRequired": return messages.chooseZone;
2039
- case "Disconnected": return messages.notConnected;
2040
- case "Failure": return "";
2041
- }
2042
- };
2043
- return usePart("p", props, { status: state._tag }, {
2044
- children: text(),
2045
- "data-domainkit-part": "connection-status",
2046
- "data-state": state._tag,
2047
- role: state._tag === "Submitting" || state._tag === "Loading" ? "status" : void 0
2048
- });
2049
- }
2050
- /**
2051
- * The one field a rejected command was about, if any. A provider that turns down credentials is
2052
- * answering the secret it was given, and it names no field, so the first secret carries the
2053
- * answer; anything else is about the request rather than one input.
2054
- */
2055
- const rejected = (reason, fields) => {
2056
- if (reason === null) return null;
2057
- if (reason._tag === "InvalidInput") return fields.find((field) => field.name === reason.field)?.name ?? null;
2058
- if (reason._tag !== "Unauthenticated" && reason._tag !== "Forbidden") return null;
2059
- return fields.find((field) => field.secret)?.name ?? null;
1493
+ if (state._tag === "Submitting" || state._tag === "Redirecting" || state._tag === "SelectionRequired" || state._tag === "Reconnect") return true;
1494
+ if (connect !== "detected") return connect === "always";
1495
+ const discovery = controller.discovery;
1496
+ return hostProvider(controller) !== null || discovery !== null && discovery._tag !== "NotFound" || (controller.snapshot?.reusable.length ?? 0) > 0;
2060
1497
  };
2061
1498
  /**
2062
- * One declared field in shadcn's `Field` anatomy: the label, the control, the note under it, and
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.
2065
- */
2066
- function TokenField({ controller, docsUrl, field, invalid, onEnter, value }) {
2067
- const { messages } = useDomainKit();
2068
- const icons = useIcons();
2069
- const id = useId();
2070
- return /* @__PURE__ */ jsxs("div", {
2071
- "data-domainkit-part": "field",
2072
- "data-invalid": invalid ? "" : void 0,
2073
- "data-orientation": "vertical",
2074
- children: [
2075
- /* @__PURE__ */ jsxs("label", {
2076
- "data-domainkit-part": "field-label",
2077
- htmlFor: id,
2078
- children: [messages.fieldLabel(field.name), field.required ? null : /* @__PURE__ */ jsx("span", {
2079
- "data-domainkit-part": "field-optional",
2080
- children: messages.optionalField
2081
- })]
2082
- }),
2083
- /* @__PURE__ */ jsx("input", {
2084
- "aria-invalid": invalid ? true : void 0,
2085
- autoComplete: "off",
2086
- "data-domainkit-part": "field-input",
2087
- id,
2088
- name: field.name,
2089
- onChange: (event) => onEnter(event.target.value),
2090
- required: field.required,
2091
- type: field.secret ? "password" : "text",
2092
- value
2093
- }),
2094
- docsUrl === null ? null : /* @__PURE__ */ jsxs("a", {
2095
- "data-domainkit-part": "field-description",
2096
- href: docsUrl,
2097
- rel: "noreferrer",
2098
- target: "_blank",
2099
- children: [messages.getToken, /* @__PURE__ */ jsx("span", {
2100
- "aria-hidden": "true",
2101
- "data-icon": "inline-end",
2102
- children: icons.external
2103
- })]
2104
- }),
2105
- invalid ? /* @__PURE__ */ jsx("div", {
2106
- "data-domainkit-part": "field-error",
2107
- children: /* @__PURE__ */ jsx(Outcome$2, {
2108
- controller,
2109
- layout: "inline"
2110
- })
2111
- }) : null
2112
- ]
2113
- });
2114
- }
2115
- /**
2116
- * One auth method, rendered from its descriptor. A token method draws an input per declared
2117
- * field, so a provider that needs an account id alongside a token needs no code here. The fields
2118
- * a provider does not need sit behind a disclosure, so the common form is one field.
2119
- */
2120
- function Method({ controller, method, onEnter, provider, values }) {
2121
- const { messages } = useDomainKit();
2122
- const icons = useIcons();
2123
- const [more, setMore] = useState(false);
2124
- const state = controller.state;
2125
- const busy = state._tag === "Submitting";
2126
- const failed = state._tag === "Failure" && state.attempt !== null && state.attempt.provider === provider.id && state.attempt.method === method.kind;
2127
- if (method.fields === null) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
2128
- "data-domainkit-part": method.kind === "oauth" ? "oauth-connect" : "integration-connect",
2129
- "data-provider": provider.id,
2130
- disabled: busy,
2131
- onClick: () => controller.connect({
2132
- method: method.kind,
2133
- provider: provider.id
2134
- }),
2135
- type: "button",
2136
- children: method.kind === "oauth" ? messages.methodOAuth(provider.name) : messages.methodIntegration(provider.name)
2137
- }), failed ? /* @__PURE__ */ jsx(Outcome$2, {
2138
- controller,
2139
- layout: "inline"
2140
- }) : null] });
2141
- const fields = method.fields;
2142
- const answered = rejected(failed && state._tag === "Failure" ? state.error.reason : null, fields);
2143
- const required = fields.filter((field) => field.required);
2144
- const optional = fields.filter((field) => !field.required);
2145
- const showMore = more || optional.some((field) => field.name === answered);
2146
- const explains = (required.find((field) => field.secret) ?? required[0] ?? optional[0])?.name;
2147
- const render = (field) => /* @__PURE__ */ jsx(TokenField, {
2148
- controller,
2149
- docsUrl: field.name === explains ? method.docsUrl : null,
2150
- field,
2151
- invalid: field.name === answered,
2152
- onEnter: (value) => onEnter(field.name, value),
2153
- value: values[field.name] ?? ""
2154
- }, field.name);
2155
- return /* @__PURE__ */ jsxs("form", {
2156
- "data-domainkit-part": "token-connect",
2157
- "data-provider": provider.id,
2158
- onSubmit: (event) => {
2159
- event.preventDefault();
2160
- const submitted = {};
2161
- for (const field of fields) {
2162
- const value = values[field.name] ?? "";
2163
- if (value !== "" || field.required) submitted[field.name] = value;
2164
- }
2165
- controller.connect({
2166
- method: method.kind,
2167
- provider: provider.id,
2168
- values: submitted
2169
- });
2170
- },
2171
- children: [
2172
- required.map(render),
2173
- optional.length === 0 ? null : /* @__PURE__ */ jsxs(Collapsible.Root, {
2174
- "data-domainkit-part": "more-options",
2175
- "data-state": showMore ? "open" : "closed",
2176
- onOpenChange: setMore,
2177
- open: showMore,
2178
- children: [/* @__PURE__ */ jsxs(Collapsible.Trigger, {
2179
- "data-domainkit-part": "more-options-trigger",
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
- })]
2189
- }),
2190
- failed && answered === null ? /* @__PURE__ */ jsx(Outcome$2, {
2191
- controller,
2192
- layout: "inline"
2193
- }) : null,
2194
- /* @__PURE__ */ jsx("button", {
2195
- "data-domainkit-part": "token-submit",
2196
- disabled: busy,
2197
- type: "submit",
2198
- children: messages.methodToken
2199
- })
2200
- ]
2201
- });
2202
- }
2203
- /**
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.
2210
- */
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, {
2215
- controller,
2216
- method,
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
- })] });
2231
- return /* @__PURE__ */ jsxs("section", {
2232
- "data-domainkit-part": "provider-authentication",
2233
- "data-provider": provider.id,
2234
- "data-state": open ? "open" : "closed",
2235
- children: [onOpen === void 0 ? null : /* @__PURE__ */ jsxs("button", {
2236
- "aria-expanded": open,
2237
- "data-domainkit-part": "provider-heading",
2238
- onClick: onOpen,
2239
- type: "button",
2240
- children: [/* @__PURE__ */ jsx(Mark, { provider }), /* @__PURE__ */ jsx("strong", { children: provider.name })]
2241
- }), open ? methods : null]
2242
- });
2243
- }
2244
- /**
2245
- * Everything a disconnected domain can do: a connection discovery already found, the connections
2246
- * this owner already has, then each provider's declared methods.
1499
+ * Every connection the customer can attach without proving anything: the one discovery resolved,
1500
+ * the candidates a zone choice is between, and the connections the snapshot lists as reusable.
2247
1501
  */
2248
- function Form({ controller, provider: narrowed, ...props }) {
2249
- const { messages } = useDomainKit();
2250
- const [opened, setOpened] = useState(null);
2251
- const [entered, setEntered] = useState(null);
1502
+ const reusableConnections = (controller) => {
2252
1503
  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 : {};
2268
- const snapshot = controller.snapshot;
2269
1504
  const discovery = controller.discovery;
2270
1505
  const candidates = state._tag === "SelectionRequired" ? state.candidates.map((candidate) => ({
2271
1506
  ...candidate,
2272
1507
  connectionId: state.connectionId
2273
1508
  })) : discovery !== null && discovery._tag === "SelectionRequired" ? discovery.candidates : [];
2274
- const resolved = discovery !== null && discovery._tag === "Resolved" ? discovery : null;
2275
- const reusable = snapshot?.reusable ?? [];
2276
- const providers = controller.providers;
2277
- const host = providers.find((entry) => entry.id === narrowed) ?? hostProvider(controller);
2278
- const [typing, setTyping] = useState(null);
2279
- const authentication = (provider, open) => /* @__PURE__ */ jsx(Authentication, {
2280
- controller,
2281
- onEnter: (name, value) => enter(provider.id, name, value),
2282
- onOpen: () => setOpened(open ? "" : provider.id),
2283
- onType: () => setTyping(provider.id),
2284
- open,
2285
- provider,
2286
- typing: typing === provider.id,
2287
- values: valuesFor(provider.id)
2288
- }, provider.id);
2289
- return usePart("div", props, { status: state._tag }, {
2290
- children: /* @__PURE__ */ jsxs(Fragment, { children: [
2291
- resolved === null ? null : /* @__PURE__ */ jsxs("section", {
2292
- "data-domainkit-part": "discovery-resolved",
2293
- children: [/* @__PURE__ */ jsx("p", { children: messages.discoveredConnection(resolved.label) }), /* @__PURE__ */ jsx("button", {
2294
- "data-domainkit-part": "attach-target",
2295
- onClick: () => controller.reuse({
2296
- connectionId: resolved.connectionId,
2297
- zone: resolved.zone
2298
- }),
2299
- type: "button",
2300
- children: messages.useConnection(resolved.label)
2301
- })]
2302
- }),
2303
- candidates.length === 0 ? null : /* @__PURE__ */ jsxs("section", {
2304
- "data-domainkit-part": "discovery-candidates",
2305
- children: [/* @__PURE__ */ jsx("p", { children: state._tag === "SelectionRequired" ? messages.chooseZone : messages.discoveryAmbiguous }), /* @__PURE__ */ jsx("div", {
2306
- "data-domainkit-part": "target-list",
2307
- "data-state": "ambiguous",
2308
- children: candidates.map((candidate) => /* @__PURE__ */ jsx("button", {
2309
- "data-domainkit-part": "attach-target",
2310
- onClick: () => controller.reuse({
2311
- connectionId: candidate.connectionId,
2312
- zone: candidate.zone
2313
- }),
2314
- type: "button",
2315
- children: messages.useConnection(candidate.label)
2316
- }, `${candidate.connectionId}:${candidate.zone}`))
2317
- })]
2318
- }),
2319
- reusable.length === 0 ? null : /* @__PURE__ */ jsxs("section", {
2320
- "data-domainkit-part": "reusable-connection",
2321
- children: [/* @__PURE__ */ jsx("div", {
2322
- "data-domainkit-part": "reusable-connection-heading",
2323
- children: /* @__PURE__ */ jsx("strong", { children: messages.reusableConnections })
2324
- }), reusable.map((connection) => /* @__PURE__ */ jsx("button", {
2325
- "data-connection-id": connection.connectionId,
2326
- "data-domainkit-part": "attach-target",
2327
- onClick: () => controller.reuse({ connectionId: connection.connectionId }),
2328
- type: "button",
2329
- children: messages.useConnection(connection.provider)
2330
- }, connection.connectionId))]
2331
- }),
2332
- providers.length === 0 ? /* @__PURE__ */ jsx("p", {
2333
- "data-domainkit-part": "target-unavailable",
2334
- children: messages.noProviders
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, {
2341
- controller,
2342
- onEnter: (name, value) => enter(host.id, name, value),
2343
- onType: () => setTyping(host.id),
2344
- open: true,
2345
- provider: host,
2346
- typing: typing === host.id,
2347
- values: valuesFor(host.id)
2348
- })] })
2349
- ] }),
2350
- "data-domainkit-part": "connection-form"
2351
- });
2352
- }
2353
- /** The connect surface. Without `render` it is a Base UI dialog behind `trigger`. */
2354
- function Dialog$2({ children, controller, render, title, trigger }) {
2355
- const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
2356
- const icons = useIcons();
2357
- const readOnly = useReadOnly();
2358
- const [open, setOpen] = useState(false);
2359
- const state = controller.state;
2360
- const busy = state._tag === "Submitting";
2361
- const unattributed = state._tag === "Failure" && !answeredInPlace(controller);
2362
- const snapshot = controller.snapshot;
2363
- const [chosen, setChosen] = useState(null);
2364
- const attached = snapshot?.provider ?? null;
2365
- const host = hostProvider(controller);
2366
- const narrowed = controller.providers.find((entry) => entry.id === (chosen ?? attached)) ?? host;
2367
- const named = narrowed?.name ?? null;
2368
- const heading = title ?? (named === null ? messages.connectAnyTitle : messages.connectTitle(named));
2369
- const alternatives = narrowed === null ? [] : controller.providers.filter((entry) => entry.id !== narrowed.id);
2370
- const running = state._tag === "Loading" || state._tag === "Submitting" || state._tag === "Redirecting" || state._tag === "SelectionRequired";
2371
- const label = named === null ? messages.connectAnyTitle : messages.connect;
2372
- const body = children ?? /* @__PURE__ */ jsx(Form, {
2373
- controller,
2374
- ...chosen === null ? {} : { provider: chosen }
2375
- });
2376
- if (readOnly) return /* @__PURE__ */ jsx(Fragment, {});
2377
- if (render !== void 0) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
2378
- "data-domainkit-part": "connection-trigger",
2379
- onClick: () => setOpen(!open),
2380
- type: "button",
2381
- children: trigger ?? label
2382
- }), render({
2383
- children: body,
2384
- open
2385
- })] });
2386
- return /* @__PURE__ */ jsxs(Dialog.Root, {
2387
- onOpenChange: (next, details) => {
2388
- if (!next && busy) {
2389
- details.cancel();
2390
- return;
2391
- }
2392
- setOpen(next);
2393
- },
2394
- open,
2395
- children: [/* @__PURE__ */ jsx(Dialog.Trigger, {
2396
- "data-domainkit-part": "connection-trigger",
2397
- children: trigger ?? label
2398
- }), /* @__PURE__ */ jsxs(Dialog.Portal, {
2399
- container: portalContainer,
2400
- children: [/* @__PURE__ */ jsx(Dialog.Backdrop, {
2401
- "data-color-scheme": colorScheme,
2402
- "data-domainkit-part": "dialog-backdrop",
2403
- "data-domainkit-root": "",
2404
- style: themeStyle
2405
- }), /* @__PURE__ */ jsxs(Dialog.Popup, {
2406
- "data-color-scheme": colorScheme,
2407
- "data-domainkit-part": "connection-dialog",
2408
- "data-domainkit-root": "",
2409
- style: themeStyle,
2410
- children: [
2411
- /* @__PURE__ */ jsxs("div", {
2412
- "data-domainkit-part": "dialog-header",
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
- ]
2462
- }),
2463
- /* @__PURE__ */ jsxs("div", {
2464
- "data-domainkit-part": "dialog-body",
2465
- children: [
2466
- running ? /* @__PURE__ */ jsx(Status$2, { controller }) : null,
2467
- body,
2468
- unattributed ? /* @__PURE__ */ jsx(Outcome$2, { controller }) : null
2469
- ]
2470
- }),
2471
- busy ? null : /* @__PURE__ */ jsx("div", {
2472
- "data-domainkit-part": "dialog-footer",
2473
- children: /* @__PURE__ */ jsx(Dialog.Close, {
2474
- "data-domainkit-part": "dialog-cancel",
2475
- children: messages.cancel
2476
- })
2477
- })
2478
- ]
2479
- })]
2480
- })]
2481
- });
2482
- }
1509
+ return [
1510
+ ...(discovery !== null && discovery._tag === "Resolved" ? [discovery] : []).map((entry) => ({
1511
+ connectionId: entry.connectionId,
1512
+ label: entry.label,
1513
+ zone: entry.zone
1514
+ })),
1515
+ ...candidates.map((entry) => ({
1516
+ connectionId: entry.connectionId,
1517
+ label: entry.label,
1518
+ zone: entry.zone
1519
+ })),
1520
+ ...(controller.snapshot?.reusable ?? []).map((entry) => ({
1521
+ connectionId: entry.connectionId,
1522
+ label: entry.provider,
1523
+ zone: void 0
1524
+ }))
1525
+ ];
1526
+ };
1527
+ const fieldsOf = (method) => {
1528
+ const fields = method.fields;
1529
+ if (fields === null) return null;
1530
+ const required = fields.filter((field) => field.required);
1531
+ const optional = fields.filter((field) => !field.required);
1532
+ return {
1533
+ explains: (required.find((field) => field.secret) ?? required[0] ?? optional[0])?.name ?? null,
1534
+ optional,
1535
+ required
1536
+ };
1537
+ };
1538
+ /** One provider's auth methods, arranged for a surface that asks for one decision at a time. */
1539
+ const describeMethods = (provider) => {
1540
+ const described = provider.methods.map((method) => ({
1541
+ descriptor: method,
1542
+ fields: fieldsOf(method),
1543
+ kind: method.kind
1544
+ }));
1545
+ const interactive = described.filter((method) => method.fields === null);
1546
+ const typed = described.filter((method) => method.fields !== null);
1547
+ return {
1548
+ alternate: interactive.length > 0 && typed.length > 0,
1549
+ interactive,
1550
+ typed
1551
+ };
1552
+ };
2483
1553
  /**
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.
1554
+ * The one field a rejected command was about, if any. A provider that turns down credentials is
1555
+ * answering the secret it was given, and it names no field, so the first secret carries the
1556
+ * answer; anything else is about the request rather than one input.
2489
1557
  */
2490
- const offering = (controller, connect = "detected") => {
2491
- if (holdsConnection(controller)) return false;
1558
+ const rejectedField = (error, fields) => {
1559
+ if (error === null) return null;
1560
+ const reason = error.reason;
1561
+ if (reason._tag === "InvalidInput") return fields.find((field) => field.name === reason.field)?.name ?? null;
1562
+ if (reason._tag !== "Unauthenticated" && reason._tag !== "Forbidden") return null;
1563
+ return fields.find((field) => field.secret)?.name ?? null;
1564
+ };
1565
+ /** Whether a failure was raised by this provider's method, so the form answers beside it. */
1566
+ const attempted = (controller, provider, method) => {
2492
1567
  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;
1568
+ if (state._tag !== "Failure" || state.attempt === null) return null;
1569
+ return state.attempt.provider === provider && state.attempt.method === method ? state.error : null;
2497
1570
  };
1571
+ //#endregion
1572
+ //#region src/records.ts
2498
1573
  /**
2499
- * The disconnected offer: the provider whose nameservers serve the domain, what that means, and
2500
- * the trigger that connects it. With no host detected there is nothing to offer, so nothing
2501
- * renders unless the host application asks for the dialog anyway.
1574
+ * What a domain's requirements are, as data: the zone file they spell, the clipboard control a
1575
+ * value needs, and the standing one row reports. The table itself is the host's.
2502
1576
  */
2503
- function Prompt({ connect = "detected", controller, ...props }) {
2504
- const { messages } = useDomainKit();
2505
- const readOnly = useReadOnly();
2506
- const state = controller.state;
2507
- const host = hostProvider(controller);
2508
- const element = usePart("div", props, { status: state._tag }, {
2509
- children: /* @__PURE__ */ jsxs(Fragment, { children: [host === null ? null : /* @__PURE__ */ jsxs("div", {
2510
- "data-domainkit-part": "host-identity",
2511
- children: [
2512
- /* @__PURE__ */ jsx(Mark, { provider: host }),
2513
- /* @__PURE__ */ jsx("span", {
2514
- "data-domainkit-part": "host-name",
2515
- children: host.name
2516
- }),
2517
- /* @__PURE__ */ jsx("span", {
2518
- "data-domainkit-part": "host-statement",
2519
- children: messages.hostOwnsZone
2520
- })
2521
- ]
2522
- }), /* @__PURE__ */ jsx(Dialog$2, { controller })] }),
2523
- "data-domainkit-part": "connect-prompt",
2524
- "data-host": host?.id
2525
- });
2526
- if (!offering(controller, connect)) return null;
2527
- if (readOnly && host === null) return null;
2528
- return element;
2529
- }
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;
1577
+ var records_exports = /* @__PURE__ */ __exportAll({
1578
+ copyText: () => copyText,
1579
+ downloadZoneFile: () => downloadZoneFile,
1580
+ identity: () => identity,
1581
+ requirementsKey: () => requirementsKey,
1582
+ statusOf: () => statusOf,
1583
+ toZoneFile: () => toZoneFile,
1584
+ useCopy: () => useCopy
1585
+ });
1586
+ const absolute = (value) => value.endsWith(".") ? value : `${value}.`;
1587
+ const textEncoder = new TextEncoder();
1588
+ const escapeTxtCharacter = (character) => {
1589
+ if (character === "\"" || character === "\\") return `\\${character}`;
1590
+ const codePoint = character.codePointAt(0);
1591
+ if (codePoint !== void 0 && (codePoint < 32 || codePoint === 127)) return `\\${codePoint.toString().padStart(3, "0")}`;
1592
+ return character;
1593
+ };
1594
+ /** TXT strings are capped at 255 bytes each, so long values are split and quoted per chunk. */
1595
+ const txtValue = (value) => {
1596
+ const chunks = [];
1597
+ let chunk = "";
1598
+ let byteLength = 0;
1599
+ for (const character of value) {
1600
+ const characterByteLength = textEncoder.encode(character).byteLength;
1601
+ if (byteLength + characterByteLength > 255) {
1602
+ chunks.push(chunk);
1603
+ chunk = "";
1604
+ byteLength = 0;
1605
+ }
1606
+ chunk += escapeTxtCharacter(character);
1607
+ byteLength += characterByteLength;
2542
1608
  }
1609
+ chunks.push(chunk);
1610
+ return chunks.map((part) => `"${part}"`).join(" ");
2543
1611
  };
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;
1612
+ /** The RR type as a zone file spells it. The tag and the type name are the same string. */
1613
+ const rrType = (record) => record._tag;
1614
+ const zoneData = (record) => {
1615
+ switch (record._tag) {
1616
+ case "A":
1617
+ case "AAAA": return record.address;
1618
+ case "CNAME": return absolute(record.target);
1619
+ case "NS": return absolute(record.nameserver);
1620
+ case "TXT": return txtValue(record.value);
1621
+ case "MX": return `${record.priority} ${absolute(record.exchange)}`;
1622
+ case "CAA": return `${record.flags} ${record.tag} "${record.value}"`;
1623
+ case "SRV": return `${record.priority} ${record.weight} ${record.port} ${absolute(record.target)}`;
1624
+ }
1625
+ };
1626
+ const toZoneFile = (records) => `${records.map((record) => `${absolute(record.name)}${record.ttl === null ? "" : ` ${record.ttl}`} IN ${rrType(record)} ${zoneData(record)}`).join("\n")}\n`;
1627
+ const downloadText = (filename, contents) => {
1628
+ if (typeof document === "undefined") return;
1629
+ const blob = new Blob([contents], { type: "text/plain;charset=utf-8" });
1630
+ const url = URL.createObjectURL(blob);
1631
+ const anchor = document.createElement("a");
1632
+ anchor.download = filename;
1633
+ anchor.href = url;
1634
+ document.body.append(anchor);
1635
+ anchor.click();
1636
+ anchor.remove();
1637
+ URL.revokeObjectURL(url);
1638
+ };
1639
+ const downloadZoneFile = (domain, records) => downloadText(`${domain}.txt`, toZoneFile(records));
1640
+ const copyText = (value) => {
1641
+ if (typeof navigator === "undefined" || navigator.clipboard === void 0) return Promise.resolve(false);
1642
+ return navigator.clipboard.writeText(value).then(() => true, () => false);
1643
+ };
1644
+ function useCopy(value, resetAfter = 2e3) {
1645
+ const [copied, setCopied] = useState(false);
1646
+ const reset = useRef(void 0);
1647
+ useEffect(() => () => clearTimeout(reset.current), []);
1648
+ return {
1649
+ copied,
1650
+ copy: () => {
1651
+ copyText(value).then((ok) => {
1652
+ if (!ok) return;
1653
+ setCopied(true);
1654
+ clearTimeout(reset.current);
1655
+ reset.current = setTimeout(() => setCopied(false), resetAfter);
1656
+ });
2593
1657
  }
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
- });
1658
+ };
2728
1659
  }
1660
+ /** The row's standing, or `null` when neither a plan nor an observation covers the record. */
1661
+ const statusOf = (record, sources) => {
1662
+ const operation = sources.plan?.operations.find((entry) => DnsRecord.equals(entry.record, record));
1663
+ if (operation !== void 0) return {
1664
+ _tag: "Operation",
1665
+ operation
1666
+ };
1667
+ const status = sources.readiness?.requirements.find((requirement) => DnsRecord.equals(requirement.record, record))?.status;
1668
+ return status === void 0 ? null : {
1669
+ _tag: "Readiness",
1670
+ status
1671
+ };
1672
+ };
1673
+ /** A stable React key for a requirement: type, name, and data identify a record. */
1674
+ const identity = (record) => [
1675
+ rrType(record),
1676
+ record.name,
1677
+ DnsRecord.data(record)
1678
+ ].join(":");
2729
1679
  /**
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.
1680
+ * A requirement set keyed by everything it carries, for memos that decide whether to re-send it.
1681
+ * `identity` is deliberately narrower: it answers "is this the same record" for a React key, while
1682
+ * a plan and an observation are built from `ttl`, `policy`, and `purpose` as well. `policy` in
1683
+ * particular decides whether a name may hold anything else, so readiness turns on it.
2732
1684
  */
2733
- function Card({ controller, onCleaned, ...props }) {
2734
- const { messages } = useDomainKit();
2735
- const readOnly = useReadOnly();
2736
- const state = controller.state;
2737
- const snapshot = controller.snapshot;
2738
- const provider = controller.providers.find((entry) => entry.id === snapshot?.provider);
2739
- const standing = state._tag === "Reconnect" ? messages.needsReconnect : messages.connected;
2740
- return usePart("div", props, { status: state._tag }, {
2741
- children: /* @__PURE__ */ jsxs(Fragment, { children: [
2742
- /* @__PURE__ */ jsxs("div", {
2743
- "data-domainkit-part": "connected-identity",
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
- ]
2755
- }),
2756
- readOnly ? null : /* @__PURE__ */ jsx("div", {
2757
- "data-domainkit-part": "connected-actions",
2758
- children: /* @__PURE__ */ jsx(DisconnectDialog, {
2759
- controller,
2760
- ...onCleaned === void 0 ? {} : { onCleaned }
2761
- })
2762
- }),
2763
- /* @__PURE__ */ jsx(Outcome$2, { controller })
2764
- ] }),
2765
- "data-domainkit-part": "connected-card"
2766
- });
2767
- }
2768
- /** Connection on its own: the card once connected, the prompt until then. */
2769
- function Flow$2({ connect, domain, onCleaned, returnTo, ...props }) {
2770
- const controller = useController$2({
2771
- domain,
2772
- ...returnTo === void 0 ? {} : { returnTo }
2773
- });
2774
- return /* @__PURE__ */ jsx(Root$2, {
2775
- controller,
2776
- ...props,
2777
- children: holdsConnection(controller) ? /* @__PURE__ */ jsx(Card, {
2778
- controller,
2779
- ...onCleaned === void 0 ? {} : { onCleaned }
2780
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Prompt, {
2781
- controller,
2782
- ...connect === void 0 ? {} : { connect }
2783
- }), answeredInPlace(controller) ? null : /* @__PURE__ */ jsx(Outcome$2, { controller })] })
2784
- });
2785
- }
1685
+ const requirementsKey = (records) => JSON.stringify(records);
2786
1686
  //#endregion
2787
- //#region src/provision.tsx
1687
+ //#region src/provision.ts
2788
1688
  var provision_exports = /* @__PURE__ */ __exportAll({
2789
- Actions: () => Actions,
2790
- Dialog: () => Dialog$1,
2791
- Flow: () => Flow$1,
2792
- Outcome: () => Outcome$1,
2793
- Root: () => Root$1,
2794
1689
  State: () => State$2,
2795
- Status: () => Status$1,
1690
+ pendingPlan: () => pendingPlan,
1691
+ planOf: () => planOf,
2796
1692
  useController: () => useController$1
2797
1693
  });
2798
- /** Plan, approve, apply. `approve` authorizes the digest and applies it in the same action. */
2799
1694
  /**
1695
+ * Plan, approve, apply; `approve` authorizes the digest and applies it in the same action.
2800
1696
  * Requirements identify themselves by content, so an inline array does not abandon the attempt.
2801
1697
  * Every field counts: a plan turns on `policy`, and `ttl` and `purpose` ride the wire with it.
2802
1698
  */
2803
1699
  const keyOf = (domain, requirements) => [domain, requirementsKey(requirements)].join("|");
2804
- function useController$1({ domain, onApplied, requirements }) {
1700
+ function useController$1({ domain, onApplied, readOnly, requirements }) {
2805
1701
  const { transport } = useDomainKit();
2806
1702
  const group = transport.provisioning;
2807
1703
  return useAttempt({
@@ -2813,6 +1709,7 @@ function useController$1({ domain, onApplied, requirements }) {
2813
1709
  }),
2814
1710
  group,
2815
1711
  onDone: onApplied,
1712
+ readOnly,
2816
1713
  plan: useCallback(() => group === void 0 ? null : group.plan({
2817
1714
  domain,
2818
1715
  requirements
@@ -2823,66 +1720,12 @@ function useController$1({ domain, onApplied, requirements }) {
2823
1720
  ])
2824
1721
  });
2825
1722
  }
2826
- function Root$1({ controller, ...props }) {
2827
- return usePart("div", props, { status: controller.state._tag }, {
2828
- "data-domainkit-part": "provisioning-flow",
2829
- "data-state": controller.state._tag
2830
- });
2831
- }
2832
- function Dialog$1(props) {
2833
- return /* @__PURE__ */ jsx(Dialog$4, {
2834
- ...props,
2835
- kind: "provisioning"
2836
- });
2837
- }
2838
- /** Approve and Decline for a plan already under review. */
2839
- function Actions(props) {
2840
- return /* @__PURE__ */ jsx(Actions$2, {
2841
- ...props,
2842
- kind: "provisioning"
2843
- });
2844
- }
2845
- function Outcome$1(props) {
2846
- return /* @__PURE__ */ jsx(Outcome$4, {
2847
- ...props,
2848
- kind: "provisioning"
2849
- });
2850
- }
2851
- function Status$1(props) {
2852
- return /* @__PURE__ */ jsx(Status$4, {
2853
- ...props,
2854
- kind: "provisioning"
2855
- });
2856
- }
2857
- /** Provisioning on its own: a trigger that plans, then the review dialog. */
2858
- function Flow$1({ domain, onApplied, requirements, trigger, ...props }) {
2859
- const controller = useController$1({
2860
- domain,
2861
- requirements,
2862
- ...onApplied === void 0 ? {} : { onApplied }
2863
- });
2864
- const [open, setOpen] = useState(false);
2865
- return /* @__PURE__ */ jsxs(Root$1, {
2866
- controller,
2867
- ...props,
2868
- children: [/* @__PURE__ */ jsx(Dialog$1, {
2869
- controller,
2870
- onOpenChange: setOpen,
2871
- open,
2872
- ...trigger === void 0 ? {} : { trigger }
2873
- }), open ? null : /* @__PURE__ */ jsx(Outcome$1, { controller })]
2874
- });
2875
- }
2876
1723
  //#endregion
2877
- //#region src/verify.tsx
1724
+ //#region src/verify.ts
2878
1725
  var verify_exports = /* @__PURE__ */ __exportAll({
2879
- Evidence: () => Evidence,
2880
- ObserveAction: () => ObserveAction,
2881
- Outcome: () => Outcome,
2882
- Root: () => Root,
2883
1726
  State: () => State,
2884
- Status: () => Status,
2885
- useController: () => useController
1727
+ useController: () => useController,
1728
+ valuesOf: () => valuesOf
2886
1729
  });
2887
1730
  /**
2888
1731
  * What the observer read back for the requirement's name, or `null` when it read nothing back at
@@ -2901,9 +1744,11 @@ function useController({ domain, polling = true, requirements }) {
2901
1744
  const timer = useRef(void 0);
2902
1745
  const signature = requirements === void 0 ? null : requirementsKey(requirements);
2903
1746
  const requested = useMemo(() => requirements === void 0 ? void 0 : { requirements }, [signature]);
1747
+ const held = useRef(domain);
2904
1748
  const [observed, setObserved] = useState(domain);
2905
1749
  if (observed !== domain) {
2906
1750
  setObserved(domain);
1751
+ held.current = domain;
2907
1752
  setState(State.Idle());
2908
1753
  }
2909
1754
  const observe = useCallback(() => {
@@ -2912,6 +1757,7 @@ function useController({ domain, polling = true, requirements }) {
2912
1757
  setState((previous) => State.Observing({ readiness: readinessOf(previous) }));
2913
1758
  runner.run(requested === void 0 ? verification.observe(domain) : verification.observe(domain, requested), {
2914
1759
  onFailure: (error) => {
1760
+ if (held.current !== domain) return;
2915
1761
  setState((previous) => State.Failure({
2916
1762
  error,
2917
1763
  readiness: readinessOf(previous)
@@ -2921,7 +1767,10 @@ function useController({ domain, polling = true, requirements }) {
2921
1767
  error
2922
1768
  }));
2923
1769
  },
2924
- onSuccess: (readiness) => setState(State.Observed({ readiness }))
1770
+ onSuccess: (readiness) => {
1771
+ if (held.current !== domain) return;
1772
+ setState(State.Observed({ readiness }));
1773
+ }
2925
1774
  });
2926
1775
  }, [
2927
1776
  domain,
@@ -2954,350 +1803,114 @@ function useController({ domain, polling = true, requirements }) {
2954
1803
  state
2955
1804
  };
2956
1805
  }
2957
- function Root({ controller, ...props }) {
2958
- const overall = controller.readiness?.overall ?? null;
2959
- return usePart("div", props, { status: controller.state._tag }, {
2960
- "data-domainkit-part": "verification-status",
2961
- "data-overall": overall ?? void 0,
2962
- "data-state": controller.state._tag
2963
- });
2964
- }
2965
- function ObserveAction({ controller, ...props }) {
2966
- const { messages } = useDomainKit();
2967
- const observing = controller.state._tag === "Observing";
2968
- return usePart("button", props, { status: controller.state._tag }, {
2969
- "aria-busy": observing,
2970
- children: controller.readiness === null ? messages.checkDns : messages.checkAgain,
2971
- "data-domainkit-part": "observe-action",
2972
- "data-loading": observing ? "" : void 0,
2973
- disabled: observing,
2974
- onClick: controller.observe,
2975
- type: "button"
2976
- });
2977
- }
2978
- /** Evidence carries no id; its source and position identify it within one requirement. */
2979
- const evidenceKey = (evidence, index) => [evidence._tag, index].join("-");
2980
- /**
2981
- * Per-requirement readiness and, below it, the host's own evidence: an SES identity, a certificate,
2982
- * anything DomainKit cannot observe but the host feeds in.
2983
- */
2984
- function Evidence({ readiness }) {
2985
- const { messages } = useDomainKit();
2986
- const icons = useIcons();
2987
- return /* @__PURE__ */ jsxs("div", {
2988
- "data-domainkit-part": "observation-list",
2989
- children: [readiness.requirements.map((requirement) => {
2990
- const settled = requirement.status === "satisfied";
2991
- return /* @__PURE__ */ jsxs("section", {
2992
- "data-domainkit-part": "observation-group",
2993
- children: [
2994
- /* @__PURE__ */ jsxs("div", {
2995
- "data-domainkit-part": "observation-row",
2996
- children: [/* @__PURE__ */ jsxs("span", {
2997
- "data-domainkit-part": "observation-record",
2998
- children: [
2999
- messages.recordType(requirement.record),
3000
- " ",
3001
- requirement.record.name
3002
- ]
3003
- }), /* @__PURE__ */ jsx(Status$5, { status: requirement.status })]
3004
- }),
3005
- settled ? null : /* @__PURE__ */ jsx("p", {
3006
- "data-domainkit-part": "observation-expected",
3007
- children: messages.expectedValue(DnsRecord.data(requirement.record))
3008
- }),
3009
- /* @__PURE__ */ jsx("ul", { children: requirement.evidence.map((evidence, index) => {
3010
- const values = settled ? null : valuesOf(evidence);
3011
- return /* @__PURE__ */ jsxs("li", {
3012
- "data-domainkit-part": "observation-source",
3013
- children: [
3014
- messages.evidenceSource(evidence),
3015
- values === null ? null : /* @__PURE__ */ jsx("span", {
3016
- "data-domainkit-part": "observation-observed",
3017
- children: values.length === 0 ? messages.observedNothing : messages.observedValues(values)
3018
- }),
3019
- evidence.detail === null ? null : /* @__PURE__ */ jsx("p", {
3020
- "data-domainkit-part": "observation-note",
3021
- children: evidence.detail
3022
- })
3023
- ]
3024
- }, evidenceKey(evidence, index));
3025
- }) })
3026
- ]
3027
- }, identity(requirement.record));
3028
- }), readiness.host.length === 0 ? null : /* @__PURE__ */ jsxs("section", {
3029
- "data-domainkit-part": "observation-group",
3030
- "data-source": "host",
3031
- children: [/* @__PURE__ */ jsx("h3", {
3032
- "data-domainkit-part": "observation-source",
3033
- children: messages.hostEvidence
3034
- }), /* @__PURE__ */ jsx("ul", { children: readiness.host.map((evidence, index) => /* @__PURE__ */ jsxs("li", {
3035
- "data-domainkit-part": "host-evidence",
3036
- children: [
3037
- /* @__PURE__ */ jsx("span", {
3038
- "aria-hidden": "true",
3039
- "data-icon": "inline-start",
3040
- children: evidence.status === "ok" ? icons.success : evidence.status === "pending" ? icons.pending : icons.failure
3041
- }),
3042
- /* @__PURE__ */ jsx("span", {
3043
- "data-domainkit-part": "observation-record",
3044
- children: evidence.label
3045
- }),
3046
- /* @__PURE__ */ jsx("span", {
3047
- "data-domainkit-part": "record-status",
3048
- "data-status": evidence.status,
3049
- children: messages.hostEvidenceStatus(evidence.status)
3050
- }),
3051
- evidence.detail === null ? null : /* @__PURE__ */ jsx("p", {
3052
- "data-domainkit-part": "observation-note",
3053
- children: evidence.detail
3054
- })
3055
- ]
3056
- }, `${evidence.source}-${index}`)) })]
3057
- })]
3058
- });
3059
- }
1806
+ //#endregion
1807
+ //#region src/domain.ts
3060
1808
  /**
3061
- * The last observation that failed. Observing reads the world rather than changing the domain, so
3062
- * the retry stays even in read-only.
1809
+ * The whole lifecycle for one domain in one hook: connect, add the records, observe, clean up.
1810
+ * `useFlow` runs the four controllers together and plans as soon as the domain is attached with
1811
+ * nothing applied, so the styled surface is only markup.
3063
1812
  */
3064
- function Outcome({ children, controller, ...props }) {
3065
- const { messages } = useDomainKit();
3066
- const state = controller.state;
3067
- if (state._tag !== "Failure") return null;
3068
- const words = outcome(state.error, messages);
3069
- return /* @__PURE__ */ jsx(Provider, {
3070
- value: {
3071
- description: words.description ?? "",
3072
- layout: props.layout ?? "card",
3073
- retry: controller.retry,
3074
- retryPart: "verification-retry",
3075
- title: words.title,
3076
- tone: "danger"
3077
- },
3078
- children: /* @__PURE__ */ jsx(Root$6, {
3079
- ...props,
3080
- children: children ?? /* @__PURE__ */ jsx(Composition, {})
3081
- })
3082
- });
3083
- }
3084
- /** The default verification slot: a button that observes, with the evidence behind it. */
3085
- function Status({ controller, render, ...props }) {
3086
- const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
3087
- const readiness = controller.readiness;
3088
- const body = /* @__PURE__ */ jsxs(Fragment, { children: [
3089
- controller.state._tag === "Observing" ? /* @__PURE__ */ jsx("p", {
3090
- "data-domainkit-part": "verification-progress",
3091
- role: "status",
3092
- children: messages.observing
3093
- }) : null,
3094
- readiness === null ? /* @__PURE__ */ jsx("p", {
3095
- "data-domainkit-part": "verification-progress",
3096
- children: messages.noEvidence
3097
- }) : /* @__PURE__ */ jsx(Evidence, { readiness }),
3098
- /* @__PURE__ */ jsx(Outcome, { controller })
3099
- ] });
3100
- return /* @__PURE__ */ jsx(Root, {
3101
- controller,
3102
- ...props,
3103
- children: render === void 0 ? /* @__PURE__ */ jsxs(Popover.Root, { children: [/* @__PURE__ */ jsx(Popover.Trigger, { render: /* @__PURE__ */ jsx(ObserveAction, { controller }) }), /* @__PURE__ */ jsx(Popover.Portal, {
3104
- container: portalContainer,
3105
- children: /* @__PURE__ */ jsx(Popover.Positioner, {
3106
- align: "end",
3107
- "data-domainkit-part": "verification-positioner",
3108
- sideOffset: 6,
3109
- children: /* @__PURE__ */ jsxs(Popover.Popup, {
3110
- "aria-busy": controller.state._tag === "Observing",
3111
- "aria-label": messages.checkDns,
3112
- "data-color-scheme": colorScheme,
3113
- "data-domainkit-part": "verification-popover",
3114
- "data-domainkit-root": "",
3115
- style: themeStyle,
3116
- children: [/* @__PURE__ */ jsx(Popover.Arrow, { "data-domainkit-part": "verification-arrow" }), body]
3117
- })
3118
- })
3119
- })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ObserveAction, { controller }), render({
3120
- children: body,
3121
- open: controller.state._tag !== "Idle"
3122
- })] })
3123
- });
3124
- }
3125
- //#endregion
3126
- //#region src/domain.tsx
3127
- var domain_exports = /* @__PURE__ */ __exportAll({ Flow: () => Flow });
3128
- /** Built from primitives alone, so the effect that announces it depends on exactly those. */
1813
+ var domain_exports = /* @__PURE__ */ __exportAll({ useFlow: () => useFlow });
1814
+ /** Built from primitives alone, so the memo that reports it depends on exactly those. */
3129
1815
  const flowState = (input) => ({
3130
1816
  applied: input.receipt !== null,
3131
1817
  connected: input.connected,
3132
1818
  connection: input.status,
1819
+ label: input.label,
3133
1820
  offering: input.offering,
3134
1821
  provider: input.provider,
3135
1822
  provisioning: input.planning,
1823
+ readOnly: input.readOnly,
3136
1824
  receiptId: input.receipt === null ? null : Receipt.ReceiptId.make(input.receipt)
3137
1825
  });
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: [
3147
- stated ? null : /* @__PURE__ */ jsx(Status$2, { controller }),
3148
- /* @__PURE__ */ jsx(Prompt, {
3149
- connect,
3150
- controller
3151
- }),
3152
- answeredInPlace(controller) ? null : /* @__PURE__ */ jsx(Outcome$2, { controller })
3153
- ] });
3154
- }
3155
- function DefaultActions({ connection, onOpenChange, open, provisioning }) {
3156
- const readOnly = useReadOnly();
3157
- const { capabilities } = useDomainKit();
3158
- const connected = connection.state._tag === "Connected";
3159
- if (readOnly) return 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 })] });
3166
- }
3167
- /**
3168
- * The whole lifecycle for one domain: connect, review, apply, observe, clean up. The flow renders
3169
- * only what the transport declares, and every part of it is a slot with a default. It adds no
3170
- * layout containers of its own, so a host's grid can place the slot output directly.
3171
- */
3172
- function Flow({ connect = "detected", domain, onApplied, onCleaned, onState, readOnly, requirements, returnTo, review = "auto", slots = {}, ...props }) {
1826
+ function useFlow({ connect = "detected", domain, onApplied, onCleaned, readOnly, requirements, returnTo }) {
3173
1827
  const { capabilities } = useDomainKit();
3174
1828
  const inherited = useReadOnly();
1829
+ const surfaceReadOnly = readOnly ?? inherited;
3175
1830
  const connection = useController$2({
3176
1831
  domain,
1832
+ readOnly: surfaceReadOnly,
3177
1833
  ...returnTo === void 0 ? {} : { returnTo }
3178
1834
  });
3179
1835
  const refresh = connection.refresh;
3180
1836
  const provisioning = useController$1({
3181
1837
  domain,
1838
+ readOnly: surfaceReadOnly,
3182
1839
  onApplied: useCallback((receipt) => {
3183
1840
  refresh();
3184
1841
  onApplied?.(receipt);
3185
1842
  }, [onApplied, refresh]),
3186
1843
  requirements
3187
1844
  });
3188
- const cleanup = useController$3({
3189
- domain,
3190
- onCleaned: useCallback((receipt) => {
3191
- refresh();
3192
- onCleaned?.(receipt);
3193
- }, [onCleaned, refresh]),
3194
- ...connection.snapshot?.lastReceiptId == null ? {} : { receiptId: Receipt.ReceiptId.make(connection.snapshot.lastReceiptId) }
3195
- });
3196
1845
  const verification = useController({
3197
1846
  domain,
3198
1847
  requirements
3199
1848
  });
3200
- const readiness = verification.readiness;
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);
1849
+ const receiptId = connection.snapshot?.lastReceiptId ?? null;
1850
+ const cleanup = useController$3({
1851
+ domain,
1852
+ readOnly: surfaceReadOnly,
1853
+ ...onCleaned === void 0 ? {} : { onCleaned },
1854
+ ...receiptId === null ? {} : { receiptId: Receipt.ReceiptId.make(receiptId) }
1855
+ });
1856
+ const attached = connection.snapshot?.attachment != null;
1857
+ const signature = surfaceReadOnly || !capabilities.includes("provisioning") || !attached || receiptId !== null ? null : [
1858
+ domain,
1859
+ connection.established,
1860
+ requirementsKey(requirements)
1861
+ ].join("|");
1862
+ const planned = useRef(null);
3211
1863
  const buildPlan = provisioning.plan;
3212
1864
  useEffect(() => {
3213
- if (answered.current === established) return;
3214
- answered.current = established;
3215
- if (review === "manual") return;
1865
+ if (signature === null) {
1866
+ planned.current = null;
1867
+ return;
1868
+ }
1869
+ if (planned.current === signature) return;
1870
+ planned.current = signature;
3216
1871
  buildPlan();
3217
- setReviewing(true);
3218
- }, [
3219
- buildPlan,
3220
- established,
3221
- review
3222
- ]);
1872
+ }, [buildPlan, signature]);
3223
1873
  const connected = holdsConnection(connection);
3224
1874
  const offering$1 = offering(connection, connect);
3225
1875
  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({
1876
+ return {
1877
+ capabilities,
1878
+ cleanup,
1879
+ connection,
1880
+ domain,
1881
+ invitation: connect,
1882
+ plan: pendingPlan(provisioning.state),
1883
+ provisioning,
1884
+ readiness: verification.readiness,
1885
+ requirements,
1886
+ state: flowState({
3241
1887
  connected,
1888
+ label: connection.snapshot?.attachment?.label ?? null,
3242
1889
  offering: offering$1,
3243
- planning,
1890
+ planning: provisioning.state._tag,
3244
1891
  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, {
3257
- children: /* @__PURE__ */ jsxs(ReadOnly, {
3258
- value: readOnly ?? inherited,
3259
- children: [
3260
- !capabilities.includes("connection") ? null : slots.connection === void 0 ? /* @__PURE__ */ jsx(DefaultConnection, {
3261
- connect,
3262
- controller: connection,
3263
- domain,
3264
- ...onCleaned === void 0 ? {} : { onCleaned }
3265
- }) : slots.connection({
3266
- connect,
3267
- controller: connection,
3268
- domain
3269
- }),
3270
- slots.records === void 0 ? /* @__PURE__ */ jsx(Table, {
3271
- readiness,
3272
- records: requirements
3273
- }) : slots.records({
3274
- controller: verification,
3275
- domain,
3276
- readiness,
3277
- records: requirements
3278
- }),
3279
- !capabilities.includes("verification") ? null : slots.verification === void 0 ? /* @__PURE__ */ jsx(Status, { controller: verification }) : slots.verification({
3280
- controller: verification,
3281
- domain
3282
- }),
3283
- slots.actions === void 0 ? /* @__PURE__ */ jsx(DefaultActions, {
3284
- cleanup,
3285
- connection,
3286
- domain,
3287
- onOpenChange: setReviewing,
3288
- open: reviewing,
3289
- provisioning
3290
- }) : slots.actions({
3291
- cleanup,
3292
- connection,
3293
- domain,
3294
- provisioning
3295
- })
3296
- ]
1892
+ readOnly: surfaceReadOnly,
1893
+ receipt: receiptId,
1894
+ status: connection.state._tag
3297
1895
  }),
3298
- "data-domainkit-part": "domain-flow",
3299
- "data-domain": domain
3300
- });
1896
+ verification
1897
+ };
1898
+ }
1899
+ //#endregion
1900
+ //#region src/outcome.ts
1901
+ var outcome_exports = /* @__PURE__ */ __exportAll({
1902
+ describe: () => describe,
1903
+ useDescribe: () => useDescribe
1904
+ });
1905
+ /** The catalog's title and description for one failure. */
1906
+ const describe = (error, catalog, context = {}) => outcome(error, catalog, context);
1907
+ /**
1908
+ * `describe` bound to the catalog `DomainKit.Root` holds. The reason alone cannot name the
1909
+ * provider a customer typed a token for, so the surface supplies it in the context.
1910
+ */
1911
+ function useDescribe() {
1912
+ const messages = useMessages();
1913
+ return (error, context = {}) => outcome(error, messages, context);
3301
1914
  }
3302
1915
  //#endregion
3303
1916
  //#region src/testing.ts
@@ -3311,6 +1924,6 @@ const transport = Testing.transport;
3311
1924
  //#region src/failure.ts
3312
1925
  const Failure = Data.taggedEnum();
3313
1926
  //#endregion
3314
- 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 };
1927
+ export { cleanup_exports as Cleanup, connect_exports as Connect, domain_exports as Domain, domain_kit_exports as DomainKit, Event, Failure, messages_exports as Messages, outcome_exports as Outcome, provision_exports as Provision, records_exports as Records, testing_exports as Testing, verify_exports as Verify };
3315
1928
 
3316
1929
  //# sourceMappingURL=index.mjs.map