@domainkit/react 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2,110 +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 { Popover } from "@base-ui/react/popover";
14
11
  import * as DateTime from "effect/DateTime";
15
12
  import { Testing } from "domainkit/testing";
16
- //#region src/composition.tsx
17
- function usePart(defaultTagName, componentProps, state, internalProps) {
18
- const { className, render, style, ...externalProps } = componentProps;
19
- const parameters = {
20
- ...className === void 0 ? {} : { className },
21
- defaultTagName,
22
- props: [internalProps, externalProps],
23
- ...render === void 0 ? {} : { render },
24
- state,
25
- ...style === void 0 ? {} : { style }
26
- };
27
- return useRender(parameters);
28
- }
29
- /** A part with no state of its own: one tag, one `data-domainkit-part`, host props on top. */
30
- function leafPart(defaultTagName, part) {
31
- return function Leaf(props) {
32
- return usePart(defaultTagName, props, {}, { "data-domainkit-part": part });
33
- };
34
- }
35
- //#endregion
36
- //#region src/icons.tsx
37
- function Glyph({ children }) {
38
- return /* @__PURE__ */ jsx("svg", {
39
- "aria-hidden": "true",
40
- fill: "none",
41
- stroke: "currentColor",
42
- strokeLinecap: "round",
43
- strokeLinejoin: "round",
44
- strokeWidth: 2,
45
- viewBox: "0 0 24 24",
46
- children
47
- });
48
- }
49
- const defaultIcons = {
50
- close: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })] }),
51
- copied: /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) }),
52
- copy: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("rect", {
53
- height: "14",
54
- rx: "2",
55
- width: "14",
56
- x: "8",
57
- y: "8"
58
- }), /* @__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" })] }),
59
- download: /* @__PURE__ */ jsxs(Glyph, { children: [
60
- /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
61
- /* @__PURE__ */ jsx("path", { d: "m7 10 5 5 5-5" }),
62
- /* @__PURE__ */ jsx("path", { d: "M12 15V3" })
63
- ] }),
64
- external: /* @__PURE__ */ jsxs(Glyph, { children: [
65
- /* @__PURE__ */ jsx("path", { d: "M15 3h6v6" }),
66
- /* @__PURE__ */ jsx("path", { d: "M10 14 21 3" }),
67
- /* @__PURE__ */ jsx("path", { d: "M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" })
68
- ] }),
69
- failure: /* @__PURE__ */ jsxs(Glyph, { children: [
70
- /* @__PURE__ */ jsx("circle", {
71
- cx: "12",
72
- cy: "12",
73
- r: "9"
74
- }),
75
- /* @__PURE__ */ jsx("path", { d: "M15 9 9 15" }),
76
- /* @__PURE__ */ jsx("path", { d: "m9 9 6 6" })
77
- ] }),
78
- pending: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("circle", {
79
- cx: "12",
80
- cy: "12",
81
- r: "9"
82
- }), /* @__PURE__ */ jsx("path", { d: "M12 7v5l3 2" })] }),
83
- success: /* @__PURE__ */ jsxs(Glyph, { children: [/* @__PURE__ */ jsx("circle", {
84
- cx: "12",
85
- cy: "12",
86
- r: "9"
87
- }), /* @__PURE__ */ jsx("path", { d: "m8.5 12.5 2.5 2.5 4.5-5" })] }),
88
- warning: /* @__PURE__ */ jsxs(Glyph, { children: [
89
- /* @__PURE__ */ jsx("path", { d: "M10.3 3.9 2.4 17.5a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" }),
90
- /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
91
- /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
92
- ] })
93
- };
94
- const IconsContext = createContext(defaultIcons);
95
- function IconsProvider({ children, icons }) {
96
- const value = useMemo(() => ({
97
- ...defaultIcons,
98
- ...icons
99
- }), [icons]);
100
- return /* @__PURE__ */ jsx(IconsContext.Provider, {
101
- value,
102
- children
103
- });
104
- }
105
- function useIcons() {
106
- return useContext(IconsContext);
107
- }
108
- //#endregion
109
13
  //#region src/messages.ts
110
14
  var messages_exports = /* @__PURE__ */ __exportAll({
111
15
  english: () => english,
@@ -161,20 +65,34 @@ const english = {
161
65
  close: "Close",
162
66
  retry: "Try again",
163
67
  approve: "Approve",
68
+ addRecords: (count) => count === 1 ? "Add 1 record" : `Add ${count} records`,
164
69
  decline: "Decline",
70
+ everyRecordConflicts: "Resolve the records above at your provider, then check again.",
165
71
  reviewChanges: "Review changes",
166
72
  cleanUp: "Remove records",
167
73
  checkDns: "Check DNS",
168
74
  checkAgain: "Check again",
169
75
  moreActions: "More actions",
170
- moreOptions: "Need an account id?",
76
+ moreOptions: "Add an account id",
171
77
  useAnotherProvider: "Use a different provider",
78
+ useTokenInstead: "Use an API token instead",
79
+ back: "Back",
80
+ connectedAccount: (provider, account) => `${provider} · ${account}`,
81
+ recordsAdded: (count) => count === 1 ? "1 added" : `${count} added`,
82
+ needsReconnect: "Needs reconnecting",
83
+ disconnectScope: "This connection serves other domains.",
84
+ disconnectThisDomain: "Only this domain",
85
+ disconnectEveryDomain: (count) => `All ${count} domains`,
172
86
  copy: "Copy",
173
87
  copied: "Copied",
174
88
  copyZone: "Copy zone file",
175
89
  download: "Download",
176
90
  useConnection: (label) => `Use ${label}`,
177
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}`,
178
96
  getToken: "Where do I find this?",
179
97
  loading: "Loading…",
180
98
  connecting: "Connecting…",
@@ -190,13 +108,16 @@ const english = {
190
108
  observing: "Checking DNS…",
191
109
  connectTitle: (provider) => `Connect ${provider}`,
192
110
  connectAnyTitle: "Connect a DNS provider",
193
- hostOwnsZone: "Owns DNS for this domain.",
111
+ hostDetected: (provider) => `${provider} DNS detected`,
194
112
  connectDescription: (domain) => `Authorize DNS changes for ${domain}.`,
195
113
  connectedTo: (provider) => `${provider} connected`,
196
114
  notConnected: "No DNS provider is connected.",
115
+ administratorConnects: (provider) => `An administrator can connect ${provider}`,
197
116
  reconnectRequired: (provider) => `${provider} needs to be reconnected`,
198
117
  disconnectTitle: (provider) => `Disconnect ${provider}?`,
199
- disconnectConsent: "The provider connection is removed. Existing DNS records are preserved.",
118
+ disconnectConsent: (domain, provider) => `DomainKit stops managing DNS for ${domain} through ${provider}.`,
119
+ disconnectWithCleanup: (count) => count === 1 ? "Remove the 1 record DomainKit added" : `Remove the ${count} records DomainKit added`,
120
+ disconnectKeepsRecords: (provider) => `Records stay in ${provider}.`,
200
121
  detachConsent: "This domain is detached from the provider. Existing DNS records are preserved.",
201
122
  detached: "Domain detached. DNS records were preserved.",
202
123
  reusableConnections: "Connections you already have",
@@ -232,6 +153,23 @@ const english = {
232
153
  case "missing": return "The record this plan expected is gone.";
233
154
  }
234
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
+ },
235
173
  attemptStatus: (status) => {
236
174
  switch (status) {
237
175
  case "planned": return "Awaiting review";
@@ -313,10 +251,7 @@ const english = {
313
251
  description: reason.message,
314
252
  title: reason.field === void 0 ? "Check what you entered" : `Check the ${humanize(reason.field).toLowerCase()}`
315
253
  }),
316
- unauthenticated: (_reason, context) => ({
317
- description: "Check the token can read and edit DNS for this zone, then try again.",
318
- title: `${named(context.provider)} didn't accept this token`
319
- }),
254
+ unauthenticated: () => ({ title: "Token not accepted" }),
320
255
  forbidden: (_reason, context) => ({
321
256
  description: `The connection needs permission to edit DNS for ${where(context.domain)}.`,
322
257
  title: "This connection can't change DNS here"
@@ -403,42 +338,20 @@ const outcome = (error, catalog, context = {}) => {
403
338
  /** The same failure as one sentence, for a host that renders text rather than a card. */
404
339
  const failure = (error, catalog, context = {}) => {
405
340
  const { description, title } = outcome(error, catalog, context);
406
- return `${title}. ${description}`;
341
+ return description === void 0 ? `${title}.` : `${title}. ${description}`;
407
342
  };
408
343
  //#endregion
409
- //#region src/theme.ts
410
- var theme_exports = /* @__PURE__ */ __exportAll({ toStyle: () => toStyle });
411
- const variables = {
412
- accent: "--domainkit-accent",
413
- accentContrast: "--domainkit-accent-contrast",
414
- backdrop: "--domainkit-backdrop",
415
- background: "--domainkit-background",
416
- border: "--domainkit-border",
417
- danger: "--domainkit-danger",
418
- dangerContrast: "--domainkit-danger-contrast",
419
- fill: "--domainkit-fill",
420
- fontFamily: "--domainkit-font-family",
421
- muted: "--domainkit-muted",
422
- radius: "--domainkit-radius",
423
- shadow: "--domainkit-shadow",
424
- success: "--domainkit-success",
425
- text: "--domainkit-text",
426
- warning: "--domainkit-warning"
427
- };
428
- function toStyle(theme = {}) {
429
- return Object.fromEntries(Object.entries(theme).flatMap(([key, value]) => value === void 0 ? [] : [[variables[key], value]]));
430
- }
431
- //#endregion
432
344
  //#region src/domain-kit.tsx
433
345
  var domain_kit_exports = /* @__PURE__ */ __exportAll({
434
346
  ReadOnly: () => ReadOnly,
435
- Root: () => Root$7,
347
+ Root: () => Root,
436
348
  useCapabilities: () => useCapabilities,
437
349
  useDomainKit: () => useDomainKit,
350
+ useMessages: () => useMessages,
438
351
  useReadOnly: () => useReadOnly,
439
352
  useTransport: () => useTransport
440
353
  });
441
- const Context$1 = createContext(null);
354
+ const Context = createContext(null);
442
355
  const navigateInBrowser = (url) => {
443
356
  if (typeof window === "undefined") throw new Error("DomainKit provider authorization requires a browser or a `navigate` prop");
444
357
  window.location.assign(url);
@@ -470,70 +383,44 @@ const useStableTransport = (transport) => {
470
383
  };
471
384
  }, [signature]);
472
385
  };
473
- const resolvePortalContainer = (container, ownerDocument) => {
474
- if (container === null || ownerDocument === null) return null;
475
- return container.ownerDocument === ownerDocument && container.getRootNode() === ownerDocument ? container : null;
476
- };
477
- const usePortalContainer = (container) => {
478
- const ownerDocument = useMemo(() => container?.ownerDocument ?? null, [container]);
479
- const subscribe = useCallback((notify) => {
480
- if (ownerDocument === null || typeof MutationObserver === "undefined") return () => {};
481
- const observer = new MutationObserver(notify);
482
- observer.observe(ownerDocument, {
483
- childList: true,
484
- subtree: true
485
- });
486
- return () => observer.disconnect();
487
- }, [ownerDocument]);
488
- const getSnapshot = useCallback(() => resolvePortalContainer(container, ownerDocument), [container, ownerDocument]);
489
- return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
490
- };
491
- 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 }) {
492
392
  const { capabilities, transport: stable } = useStableTransport(transport);
493
393
  const onEventRef = useRef(onEvent);
494
394
  useLayoutEffect(() => {
495
395
  onEventRef.current = onEvent;
496
396
  }, [onEvent]);
497
- const emit = useCallback((event) => {
498
- try {
499
- onEventRef.current?.(event);
500
- } catch {}
501
- }, []);
502
- const themeStyle = toStyle(theme);
503
- const resolvedPortalContainer = usePortalContainer(portalContainer);
504
- const portalContainerRef = useMemo(() => ({ current: resolvedPortalContainer }), [resolvedPortalContainer]);
505
397
  const value = {
506
398
  capabilities,
507
- colorScheme,
508
- emit,
509
- marks,
399
+ emit: useCallback((event) => {
400
+ try {
401
+ onEventRef.current?.(event);
402
+ } catch {}
403
+ }, []),
510
404
  messages: merge(messages),
511
405
  navigate,
512
- portalContainer: portalContainerRef,
513
406
  readOnly,
514
407
  revision,
515
- themeStyle,
516
408
  transport: stable
517
409
  };
518
- const content = usePart("div", props, { colorScheme }, {
519
- children,
520
- "data-color-scheme": colorScheme,
521
- "data-domainkit-root": "",
522
- style: themeStyle
523
- });
524
- return /* @__PURE__ */ jsx(Context$1.Provider, {
410
+ return /* @__PURE__ */ jsx(Context.Provider, {
525
411
  value,
526
- children: /* @__PURE__ */ jsx(IconsProvider, {
527
- ...icons === void 0 ? {} : { icons },
528
- children: content
529
- })
412
+ children
530
413
  });
531
414
  }
532
415
  function useDomainKit() {
533
- const value = useContext(Context$1);
534
- 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");
535
418
  return value;
536
419
  }
420
+ /** The catalog `DomainKit.Root` holds, with the host's overrides already merged in. */
421
+ function useMessages() {
422
+ return useDomainKit().messages;
423
+ }
537
424
  /** The transport `DomainKit.Root` holds, with the identity it keeps for the whole mount. */
538
425
  function useTransport() {
539
426
  return useDomainKit().transport;
@@ -546,13 +433,13 @@ function ReadOnly({ children, value }) {
546
433
  children
547
434
  });
548
435
  }
549
- /** Whether this part may offer controls that change the domain. */
436
+ /** Whether this surface may offer controls that change the domain. */
550
437
  function useReadOnly() {
551
438
  const scoped = useContext(ReadOnlyContext);
552
439
  const root = useDomainKit().readOnly;
553
440
  return scoped ?? root;
554
441
  }
555
- /** 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. */
556
443
  function useCapabilities() {
557
444
  return useDomainKit().capabilities;
558
445
  }
@@ -622,10 +509,42 @@ function useRunner() {
622
509
  const State$2 = Data.taggedEnum();
623
510
  /** Reasons that mean the plan itself is gone: retrying the same step would fail the same way. */
624
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
+ };
625
543
  function useAttempt(options) {
626
544
  const { domain, done, group, key, onDone } = options;
627
545
  const { emit } = useDomainKit();
628
- const readOnly = useReadOnly();
546
+ const inherited = useReadOnly();
547
+ const readOnly = options.readOnly ?? inherited;
629
548
  const runner = useRunner();
630
549
  const [state, setState] = useState(State$2.Idle());
631
550
  const held = useRef({
@@ -658,7 +577,7 @@ function useAttempt(options) {
658
577
  }));
659
578
  }, [domain, emit]);
660
579
  const applyWith = useCallback((plan, approval) => {
661
- if (group === void 0 || held.current.key !== key) return;
580
+ if (readOnly || group === void 0 || held.current.key !== key) return;
662
581
  const command = () => {
663
582
  setState(State$2.Applying({
664
583
  approval,
@@ -688,9 +607,11 @@ function useAttempt(options) {
688
607
  key,
689
608
  onDone,
690
609
  onFailure,
610
+ readOnly,
691
611
  runner
692
612
  ]);
693
613
  const buildPlan = useCallback(() => {
614
+ if (readOnly) return;
694
615
  const effect = build.current();
695
616
  if (effect === null) return;
696
617
  const command = () => {
@@ -716,11 +637,12 @@ function useAttempt(options) {
716
637
  }, [
717
638
  key,
718
639
  onFailure,
640
+ readOnly,
719
641
  runner
720
642
  ]);
721
643
  const approve = useCallback((operationIds) => {
722
644
  const plan = held.current.plan;
723
- if (group === void 0 || plan === null || held.current.key !== key) return;
645
+ if (readOnly || group === void 0 || plan === null || held.current.key !== key) return;
724
646
  const command = () => {
725
647
  setState(State$2.Approving({ plan }));
726
648
  runner.run(group.approve({
@@ -749,11 +671,12 @@ function useAttempt(options) {
749
671
  group,
750
672
  key,
751
673
  onFailure,
674
+ readOnly,
752
675
  runner
753
676
  ]);
754
677
  const reject = useCallback((reason) => {
755
678
  const plan = held.current.plan;
756
- if (group === void 0 || plan === null || held.current.key !== key) return;
679
+ if (readOnly || group === void 0 || plan === null || held.current.key !== key) return;
757
680
  const command = () => {
758
681
  setState(State$2.Rejecting({ plan }));
759
682
  runner.run(group.reject({
@@ -784,11 +707,12 @@ function useAttempt(options) {
784
707
  group,
785
708
  key,
786
709
  onFailure,
710
+ readOnly,
787
711
  runner
788
712
  ]);
789
713
  const apply = useCallback(() => {
790
714
  const { approval, plan } = held.current;
791
- if (approval === null || plan === null) return;
715
+ if (readOnly || approval === null || plan === null) return;
792
716
  applyWith(plan, approval);
793
717
  }, [applyWith]);
794
718
  const retry = useCallback(() => {
@@ -831,690 +755,18 @@ function useAttempt(options) {
831
755
  };
832
756
  }
833
757
  //#endregion
834
- //#region src/outcome.tsx
835
- /**
836
- * What a flow says when a step ends badly: media, title, description, and the action that follows.
837
- * Every flow's `Outcome` binds its controller to these parts, so a failure reads the same way in
838
- * the connect dialog, the review dialog, and the verification popover.
839
- *
840
- * <Connect.Outcome controller={connection} /> the default composition
841
- *
842
- * <Connect.Outcome controller={connection}> a composition of your own
843
- * <Outcome.Header>
844
- * <Outcome.Media><MyIcon /></Outcome.Media>
845
- * <Outcome.Title />
846
- * <Outcome.Description />
847
- * </Outcome.Header>
848
- * <Outcome.Content />
849
- * </Connect.Outcome>
850
- */
851
- var outcome_exports = /* @__PURE__ */ __exportAll({
852
- Composition: () => Composition,
853
- Content: () => Content,
854
- Description: () => Description,
855
- Header: () => Header$1,
856
- Media: () => Media,
857
- Provider: () => Provider,
858
- Root: () => Root$6,
859
- Title: () => Title,
860
- useOutcome: () => useOutcome
861
- });
862
- const fallback = {
863
- description: "",
864
- layout: "card",
865
- retry: null,
866
- retryPart: "flow-retry",
867
- title: "",
868
- tone: "danger"
869
- };
870
- const Context = createContext(null);
871
- /** What the surrounding flow resolved, for a part of your own inside `X.Outcome`. */
872
- function useOutcome() {
873
- return useContext(Context) ?? fallback;
874
- }
875
- /** Binds a resolved outcome to the parts below it. Flows render this; hosts rarely do. */
876
- function Provider({ children, value }) {
877
- return /* @__PURE__ */ jsx(Context.Provider, {
878
- value,
879
- children
880
- });
881
- }
882
- /**
883
- * The outcome itself. It announces, so a customer reading with assistive tech hears it. A `tone`
884
- * or `layout` of its own reaches every part below it, so the glyph and the words agree with the
885
- * root's own attributes.
886
- */
887
- function Root$6({ layout, tone, ...props }) {
888
- const value = useOutcome();
889
- const resolved = {
890
- ...value,
891
- layout: layout ?? value.layout,
892
- tone: tone ?? value.tone
893
- };
894
- const element = usePart("div", props, {
895
- layout: resolved.layout,
896
- tone: resolved.tone
897
- }, {
898
- "data-domainkit-part": "outcome",
899
- "data-layout": resolved.layout,
900
- "data-tone": resolved.tone,
901
- role: "alert"
902
- });
903
- return /* @__PURE__ */ jsx(Provider, {
904
- value: resolved,
905
- children: element
906
- });
907
- }
908
- /** Media, title, and description together, so the action below them can move on its own. */
909
- function Header$1(props) {
910
- const { layout, tone } = useOutcome();
911
- return usePart("div", props, {
912
- layout,
913
- tone
914
- }, { "data-domainkit-part": "outcome-header" });
915
- }
916
- /** The glyph for the tone. Children replace it; `icons` on `DomainKit.Root` replaces the set. */
917
- function Media({ variant = "icon", ...props }) {
918
- const { layout, tone } = useOutcome();
919
- const icons = useIcons();
920
- const glyph = tone === "success" ? icons.success : tone === "warning" ? icons.warning : icons.failure;
921
- return usePart("div", props, {
922
- layout,
923
- tone
924
- }, {
925
- "aria-hidden": "true",
926
- children: glyph,
927
- "data-domainkit-part": "outcome-media",
928
- "data-variant": variant
929
- });
930
- }
931
- /** The heading from the catalog. Children win, for a host that words it itself. */
932
- function Title(props) {
933
- const { layout, title, tone } = useOutcome();
934
- return usePart("div", props, {
935
- layout,
936
- tone
937
- }, {
938
- children: title,
939
- "data-domainkit-part": "outcome-title"
940
- });
941
- }
942
- /** What to do about it, from the catalog. */
943
- function Description(props) {
944
- const { description, layout, tone } = useOutcome();
945
- return usePart("p", props, {
946
- layout,
947
- tone
948
- }, {
949
- children: description,
950
- "data-domainkit-part": "outcome-description"
951
- });
952
- }
953
- /**
954
- * The actions under the header. Without children it is the flow's retry, and a flow that allows
955
- * none renders nothing: read-only hides every control that would start a write again.
956
- */
957
- function Content(props) {
958
- const { messages } = useDomainKit();
959
- const { layout, retry, retryPart, tone } = useOutcome();
960
- const element = usePart("div", props, {
961
- layout,
962
- tone
963
- }, {
964
- children: retry === null ? null : /* @__PURE__ */ jsx("button", {
965
- "data-domainkit-part": retryPart,
966
- onClick: retry,
967
- type: "button",
968
- children: messages.retry
969
- }),
970
- "data-domainkit-part": "outcome-content"
971
- });
972
- return retry === null && props.children === void 0 ? null : element;
973
- }
974
- /** Media, title, and description in a header, then the action. What `X.Outcome` renders alone. */
975
- function Composition() {
976
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Header$1, { children: [
977
- /* @__PURE__ */ jsx(Media, {}),
978
- /* @__PURE__ */ jsx(Title, {}),
979
- /* @__PURE__ */ jsx(Description, {})
980
- ] }), /* @__PURE__ */ jsx(Content, {})] });
981
- }
982
- //#endregion
983
- //#region src/records.tsx
984
- var records_exports = /* @__PURE__ */ __exportAll({
985
- Body: () => Body$1,
986
- Caption: () => Caption,
987
- Card: () => Card$1,
988
- CardContent: () => CardContent,
989
- CardHeader: () => CardHeader,
990
- CardTitle: () => CardTitle,
991
- Cell: () => Cell,
992
- CopyValue: () => CopyValue,
993
- Footer: () => Footer,
994
- Head: () => Head,
995
- Header: () => Header,
996
- Purpose: () => Purpose,
997
- Root: () => Root$5,
998
- Row: () => Row,
999
- Status: () => Status$5,
1000
- Table: () => Table,
1001
- Value: () => Value,
1002
- ZoneFile: () => ZoneFile,
1003
- copyText: () => copyText,
1004
- downloadZoneFile: () => downloadZoneFile,
1005
- identity: () => identity,
1006
- requirementsKey: () => requirementsKey,
1007
- statusOf: () => statusOf,
1008
- toZoneFile: () => toZoneFile,
1009
- useCopy: () => useCopy
1010
- });
1011
- const absolute = (value) => value.endsWith(".") ? value : `${value}.`;
1012
- const textEncoder = new TextEncoder();
1013
- const escapeTxtCharacter = (character) => {
1014
- if (character === "\"" || character === "\\") return `\\${character}`;
1015
- const codePoint = character.codePointAt(0);
1016
- if (codePoint !== void 0 && (codePoint < 32 || codePoint === 127)) return `\\${codePoint.toString().padStart(3, "0")}`;
1017
- return character;
1018
- };
1019
- /** TXT strings are capped at 255 bytes each, so long values are split and quoted per chunk. */
1020
- const txtValue = (value) => {
1021
- const chunks = [];
1022
- let chunk = "";
1023
- let byteLength = 0;
1024
- for (const character of value) {
1025
- const characterByteLength = textEncoder.encode(character).byteLength;
1026
- if (byteLength + characterByteLength > 255) {
1027
- chunks.push(chunk);
1028
- chunk = "";
1029
- byteLength = 0;
1030
- }
1031
- chunk += escapeTxtCharacter(character);
1032
- byteLength += characterByteLength;
1033
- }
1034
- chunks.push(chunk);
1035
- return chunks.map((part) => `"${part}"`).join(" ");
1036
- };
1037
- /** The RR type as a zone file spells it. The tag and the type name are the same string. */
1038
- const rrType = (record) => record._tag;
1039
- const zoneData = (record) => {
1040
- switch (record._tag) {
1041
- case "A":
1042
- case "AAAA": return record.address;
1043
- case "CNAME": return absolute(record.target);
1044
- case "NS": return absolute(record.nameserver);
1045
- case "TXT": return txtValue(record.value);
1046
- case "MX": return `${record.priority} ${absolute(record.exchange)}`;
1047
- case "CAA": return `${record.flags} ${record.tag} "${record.value}"`;
1048
- case "SRV": return `${record.priority} ${record.weight} ${record.port} ${absolute(record.target)}`;
1049
- }
1050
- };
1051
- const toZoneFile = (records) => `${records.map((record) => `${absolute(record.name)}${record.ttl === null ? "" : ` ${record.ttl}`} IN ${rrType(record)} ${zoneData(record)}`).join("\n")}\n`;
1052
- const downloadText = (filename, contents) => {
1053
- if (typeof document === "undefined") return;
1054
- const blob = new Blob([contents], { type: "text/plain;charset=utf-8" });
1055
- const url = URL.createObjectURL(blob);
1056
- const anchor = document.createElement("a");
1057
- anchor.download = filename;
1058
- anchor.href = url;
1059
- document.body.append(anchor);
1060
- anchor.click();
1061
- anchor.remove();
1062
- URL.revokeObjectURL(url);
1063
- };
1064
- const downloadZoneFile = (domain, records) => downloadText(`${domain}.txt`, toZoneFile(records));
1065
- const copyText = (value) => {
1066
- if (typeof navigator === "undefined" || navigator.clipboard === void 0) return Promise.resolve(false);
1067
- return navigator.clipboard.writeText(value).then(() => true, () => false);
1068
- };
1069
- function useCopy(value, resetAfter = 2e3) {
1070
- const [copied, setCopied] = useState(false);
1071
- const reset = useRef(void 0);
1072
- useEffect(() => () => clearTimeout(reset.current), []);
1073
- return {
1074
- copied,
1075
- copy: () => {
1076
- copyText(value).then((ok) => {
1077
- if (!ok) return;
1078
- setCopied(true);
1079
- clearTimeout(reset.current);
1080
- reset.current = setTimeout(() => setCopied(false), resetAfter);
1081
- });
1082
- }
1083
- };
1084
- }
1085
- function CopyValue({ value, ...props }) {
1086
- const { messages } = useDomainKit();
1087
- const icons = useIcons();
1088
- const { copied, copy } = useCopy(value);
1089
- return usePart("span", props, { copied }, {
1090
- children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("code", { children: value }), /* @__PURE__ */ jsxs("button", {
1091
- "aria-label": `${copied ? messages.copied : messages.copy} ${value}`,
1092
- "data-domainkit-part": "copy-action",
1093
- onClick: copy,
1094
- type: "button",
1095
- children: [/* @__PURE__ */ jsx("span", {
1096
- "aria-hidden": "true",
1097
- "data-domainkit-part": "copy-glyph",
1098
- "data-icon": "",
1099
- "data-state": "idle",
1100
- children: icons.copy
1101
- }), /* @__PURE__ */ jsx("span", {
1102
- "aria-hidden": "true",
1103
- "data-domainkit-part": "copy-glyph",
1104
- "data-icon": "",
1105
- "data-state": "done",
1106
- children: icons.copied
1107
- })]
1108
- })] }),
1109
- "data-copied": copied ? "" : void 0,
1110
- "data-domainkit-part": "copy-value"
1111
- });
1112
- }
1113
- function ZoneFile({ domain, records, ...props }) {
1114
- const { messages } = useDomainKit();
1115
- const icons = useIcons();
1116
- const { copied, copy } = useCopy(toZoneFile(records));
1117
- return usePart("div", props, { count: records.length }, {
1118
- children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
1119
- "aria-label": copied ? messages.copied : messages.copyZone,
1120
- "data-domainkit-part": "zone-copy",
1121
- onClick: copy,
1122
- type: "button",
1123
- children: [/* @__PURE__ */ jsx("span", {
1124
- "aria-hidden": "true",
1125
- "data-icon": "inline-start",
1126
- children: copied ? icons.copied : icons.copy
1127
- }), copied ? messages.copied : messages.copyZone]
1128
- }), /* @__PURE__ */ jsxs("button", {
1129
- "aria-label": messages.download,
1130
- "data-domainkit-part": "zone-download",
1131
- onClick: () => downloadZoneFile(domain, records),
1132
- type: "button",
1133
- children: [/* @__PURE__ */ jsx("span", {
1134
- "aria-hidden": "true",
1135
- "data-icon": "inline-start",
1136
- children: icons.download
1137
- }), messages.download]
1138
- })] }),
1139
- "data-domainkit-part": "zone-file"
1140
- });
1141
- }
1142
- /** The readiness this record reached, or `null` when nothing has observed it. */
1143
- const statusOf = (record, readiness) => readiness?.requirements.find((requirement) => DnsRecord.equals(requirement.record, record))?.status ?? null;
1144
- function Status$5({ status, ...props }) {
1145
- const { messages } = useDomainKit();
1146
- const icons = useIcons();
1147
- const glyph = status === "satisfied" ? icons.success : status === "unknown" ? icons.pending : icons.failure;
1148
- return usePart("span", props, { status }, {
1149
- children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1150
- "aria-hidden": "true",
1151
- "data-icon": "inline-start",
1152
- children: glyph
1153
- }), messages.requirementStatus(status)] }),
1154
- "data-domainkit-part": "record-status",
1155
- "data-status": status
1156
- });
1157
- }
1158
- const Header = leafPart("thead", "records-header");
1159
- const Body$1 = leafPart("tbody", "records-body");
1160
- const Footer = leafPart("tfoot", "records-footer");
1161
- const Row = leafPart("tr", "records-row");
1162
- const Head = leafPart("th", "records-head");
1163
- const Cell = leafPart("td", "records-cell");
1164
- const Caption = leafPart("caption", "records-caption");
1165
- const Value = leafPart("span", "record-value");
1166
- const Purpose = leafPart("span", "record-purpose");
1167
- const CardHeader = leafPart("div", "record-card-head");
1168
- const CardTitle = leafPart("strong", "record-card-title");
1169
- const CardContent = leafPart("dl", "record-card-content");
1170
- function Root$5({ count = 0, ...props }) {
1171
- return /* @__PURE__ */ jsx("div", {
1172
- "data-domainkit-part": "records-panel",
1173
- children: usePart("table", props, { count }, { "data-domainkit-part": "records-table" })
1174
- });
1175
- }
1176
- /** A stable React key for a requirement: type, name, and data identify a record. */
1177
- const identity = (record) => [
1178
- rrType(record),
1179
- record.name,
1180
- DnsRecord.data(record)
1181
- ].join(":");
1182
- /**
1183
- * A requirement set keyed by everything it carries, for memos that decide whether to re-send it.
1184
- * `identity` is deliberately narrower: it answers "is this the same record" for a React key, while
1185
- * a plan and an observation are built from `ttl`, `policy`, and `purpose` as well. `policy` in
1186
- * particular decides whether a name may hold anything else, so readiness turns on it.
1187
- */
1188
- const requirementsKey = (records) => JSON.stringify(records);
1189
- function Card$1({ readiness, record, ...props }) {
1190
- const { messages } = useDomainKit();
1191
- const status = statusOf(record, readiness);
1192
- return usePart("section", props, { record: identity(record) }, {
1193
- 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: [
1194
- /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: messages.headingName }), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(CopyValue, { value: record.name }) })] }),
1195
- /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: messages.headingValue }), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(CopyValue, { value: DnsRecord.data(record) }) })] }),
1196
- 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 }) })] })
1197
- ] })] }),
1198
- "data-domainkit-part": "record-card"
1199
- });
1200
- }
1201
- /** The default records presentation: one row per requirement, with readiness when there is any. */
1202
- function Table({ caption, readiness, records, ...props }) {
1203
- const { messages } = useDomainKit();
1204
- const withStatus = readiness !== null && readiness !== void 0;
1205
- return /* @__PURE__ */ jsxs(Root$5, {
1206
- count: records.length,
1207
- ...props,
1208
- children: [
1209
- caption === void 0 ? null : /* @__PURE__ */ jsx(Caption, { children: caption }),
1210
- /* @__PURE__ */ jsx(Header, { children: /* @__PURE__ */ jsxs(Row, { children: [
1211
- /* @__PURE__ */ jsx(Head, {
1212
- scope: "col",
1213
- children: messages.headingType
1214
- }),
1215
- /* @__PURE__ */ jsx(Head, {
1216
- scope: "col",
1217
- children: messages.headingName
1218
- }),
1219
- /* @__PURE__ */ jsx(Head, {
1220
- scope: "col",
1221
- children: messages.headingValue
1222
- }),
1223
- withStatus ? /* @__PURE__ */ jsx(Head, {
1224
- "data-column": "status",
1225
- scope: "col",
1226
- children: messages.headingStatus
1227
- }) : null
1228
- ] }) }),
1229
- /* @__PURE__ */ jsx(Body$1, { children: records.map((record) => {
1230
- const status = statusOf(record, readiness);
1231
- return /* @__PURE__ */ jsxs(Row, { children: [
1232
- /* @__PURE__ */ jsx(Cell, { children: messages.recordType(record) }),
1233
- /* @__PURE__ */ jsx(Cell, { children: /* @__PURE__ */ jsx(CopyValue, { value: record.name }) }),
1234
- /* @__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 })] }) }),
1235
- withStatus ? /* @__PURE__ */ jsx(Cell, {
1236
- "data-column": "status",
1237
- children: status === null ? null : /* @__PURE__ */ jsx(Status$5, { status })
1238
- }) : null
1239
- ] }, identity(record));
1240
- }) })
1241
- ]
1242
- });
1243
- }
1244
- //#endregion
1245
- //#region src/operations.tsx
1246
- var operations_exports = /* @__PURE__ */ __exportAll({
1247
- Item: () => Item,
1248
- Kind: () => Kind,
1249
- List: () => List,
1250
- Name: () => Name,
1251
- Reason: () => Reason$1,
1252
- Record: () => Record,
1253
- Root: () => Root$4,
1254
- Type: () => Type
1255
- });
1256
- const Kind = leafPart("span", "operation-kind");
1257
- const Type = leafPart("strong", "operation-type");
1258
- const Record = leafPart("span", "operation-record");
1259
- const Name = leafPart("span", "operation-name");
1260
- const Reason$1 = leafPart("span", "operation-reason");
1261
- function Item({ operation, ...props }) {
1262
- const { messages } = useDomainKit();
1263
- return usePart("li", props, { operation: operation._tag }, {
1264
- children: /* @__PURE__ */ jsxs(Fragment, { children: [
1265
- /* @__PURE__ */ jsx(Kind, { children: messages.operation(operation) }),
1266
- " ",
1267
- /* @__PURE__ */ jsx(Type, { children: messages.recordType(operation.record) }),
1268
- " ",
1269
- /* @__PURE__ */ jsxs(Record, { children: [/* @__PURE__ */ jsx(Name, { children: operation.record.name }), /* @__PURE__ */ jsx(CopyValue, { value: DnsRecord.data(operation.record) })] }),
1270
- operation._tag === "Conflict" ? /* @__PURE__ */ jsx(Reason$1, { children: messages.conflictReason(operation.reason) }) : null
1271
- ] }),
1272
- "data-domainkit-part": "operation-item",
1273
- "data-operation": operation._tag
1274
- });
1275
- }
1276
- function Root$4({ count = 0, kind, ...props }) {
1277
- return usePart("ul", props, {
1278
- count,
1279
- kind
1280
- }, {
1281
- "data-domainkit-part": kind === "provisioning" ? "plan-operations" : "cleanup-operations",
1282
- "data-kind": kind
1283
- });
1284
- }
1285
- /** Every operation the plan holds, in plan order, so a customer reviews exactly what will run. */
1286
- function List({ plan, ...props }) {
1287
- return /* @__PURE__ */ jsx(Root$4, {
1288
- count: plan.operations.length,
1289
- kind: plan.kind,
1290
- ...props,
1291
- children: plan.operations.map((operation) => /* @__PURE__ */ jsx(Item, { operation }, operation.id))
1292
- });
1293
- }
1294
- //#endregion
1295
- //#region src/review.tsx
1296
- /**
1297
- * The review surface both `Provision` and `Cleanup` render: what the plan will do, the Approve
1298
- * and Decline actions D35 defines, and the outcome. `kind` picks the wording; nothing else about
1299
- * the two flows differs on screen.
1300
- */
1301
- const busy = (status) => status === "Planning" || status === "Approving" || status === "Applying" || status === "Rejecting";
1302
- /** What the flow is doing right now, or what it finished doing. */
1303
- function Status$4({ controller, kind, ...props }) {
1304
- const { messages } = useDomainKit();
1305
- const state = controller.state;
1306
- const text = () => {
1307
- switch (state._tag) {
1308
- case "Idle": return "";
1309
- case "Planning": return kind === "provisioning" ? messages.planning : messages.planningCleanup;
1310
- case "Planned": return kind === "provisioning" ? messages.planConsent : messages.cleanupConsent;
1311
- case "Approving": return messages.approving;
1312
- case "Applying": return kind === "provisioning" ? messages.applying : messages.cleaning;
1313
- case "Rejecting": return messages.rejecting;
1314
- case "Rejected": return messages.declinedBy(state.attempt.rejection?.actorId ?? "");
1315
- case "Applied": return kind === "provisioning" ? messages.applied(state.receipt) : messages.cleaned(state.receipt);
1316
- case "Failure": return "";
1317
- }
1318
- };
1319
- return usePart("p", props, { status: state._tag }, {
1320
- children: text(),
1321
- "data-domainkit-part": kind === "provisioning" ? "provisioning-status" : "cleanup-status",
1322
- "data-state": state._tag,
1323
- role: busy(state._tag) ? "status" : void 0
1324
- });
1325
- }
1326
- /**
1327
- * The failure, or a receipt that only partly landed: media, title, description, and the retry the
1328
- * flow allows. Children replace the composition and keep the binding.
1329
- */
1330
- function Outcome$4({ children, controller, kind, ...props }) {
1331
- const { messages } = useDomainKit();
1332
- const readOnly = useReadOnly();
1333
- const state = controller.state;
1334
- const partial = state._tag === "Applied" && state.receipt.status === "partial";
1335
- const retryPart = kind === "provisioning" ? "provisioning-retry" : "cleanup-retry";
1336
- if (state._tag !== "Failure" && !partial) return null;
1337
- const words = state._tag === "Failure" ? outcome(state.error, messages) : {
1338
- description: kind === "provisioning" ? messages.partiallyApplied : messages.partiallyCleaned,
1339
- title: kind === "provisioning" ? messages.partiallyAppliedTitle : messages.partiallyCleanedTitle
1340
- };
1341
- return /* @__PURE__ */ jsx(Provider, {
1342
- value: {
1343
- description: words.description,
1344
- layout: props.layout ?? "card",
1345
- retry: readOnly || state._tag !== "Failure" ? null : controller.retry,
1346
- retryPart,
1347
- title: words.title,
1348
- tone: state._tag === "Failure" ? "danger" : "warning"
1349
- },
1350
- children: /* @__PURE__ */ jsx(Root$6, {
1351
- ...props,
1352
- children: children ?? /* @__PURE__ */ jsx(Composition, {})
1353
- })
1354
- });
1355
- }
1356
- /**
1357
- * Approve and Decline. Approve authorizes the digest and applies it; Decline records the refusal
1358
- * and ends the attempt. Neither renders until there is a plan to act on.
1359
- */
1360
- function Actions$2({ controller, kind, ...props }) {
1361
- const { messages } = useDomainKit();
1362
- const readOnly = useReadOnly();
1363
- const state = controller.state;
1364
- const plan = state._tag === "Planned" ? state.plan : null;
1365
- const running = busy(state._tag);
1366
- const element = usePart("div", props, { status: state._tag }, {
1367
- children: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
1368
- "data-domainkit-part": kind === "provisioning" ? "plan-apply" : "cleanup-apply",
1369
- disabled: running || plan === null || plan.operations.length === 0,
1370
- onClick: () => controller.approve(),
1371
- type: "button",
1372
- children: messages.approve
1373
- }), /* @__PURE__ */ jsx("button", {
1374
- "data-domainkit-part": "plan-decline",
1375
- disabled: running || plan === null,
1376
- onClick: () => controller.reject(),
1377
- type: "button",
1378
- children: messages.decline
1379
- })] }),
1380
- "data-domainkit-part": "review-actions"
1381
- });
1382
- if (readOnly) return null;
1383
- return plan === null && !running ? null : element;
1384
- }
1385
- /** The operations under review, or the reason there is nothing to review. */
1386
- function Body({ controller, kind }) {
1387
- const { messages } = useDomainKit();
1388
- const state = controller.state;
1389
- 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;
1390
- return /* @__PURE__ */ jsxs("div", {
1391
- "data-domainkit-part": "dialog-body",
1392
- children: [
1393
- /* @__PURE__ */ jsx(Status$4, {
1394
- controller,
1395
- kind
1396
- }),
1397
- plan === null ? null : plan.operations.length === 0 ? /* @__PURE__ */ jsx("p", {
1398
- "data-domainkit-part": "plan-empty",
1399
- children: messages.noChanges
1400
- }) : /* @__PURE__ */ jsx(List, { plan }),
1401
- state._tag === "Rejected" && state.attempt.rejection?.reason != null ? /* @__PURE__ */ jsx("p", {
1402
- "data-domainkit-part": "plan-decline-reason",
1403
- children: messages.declineReason(state.attempt.rejection.reason)
1404
- }) : null,
1405
- /* @__PURE__ */ jsx(Outcome$4, {
1406
- controller,
1407
- kind
1408
- })
1409
- ]
1410
- });
1411
- }
1412
- /** The review dialog. `render` swaps the surface; the contents stay the flow's. */
1413
- function Dialog$4({ children, controller, kind, onOpenChange, open, render, trigger }) {
1414
- const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
1415
- const readOnly = useReadOnly();
1416
- const [uncontrolled, setUncontrolled] = useState(false);
1417
- const isOpen = open ?? uncontrolled;
1418
- const setOpen = (next) => {
1419
- if (open === void 0) setUncontrolled(next);
1420
- onOpenChange?.(next);
1421
- };
1422
- const running = busy(controller.state._tag);
1423
- const body = children ?? /* @__PURE__ */ jsx(Body, {
1424
- controller,
1425
- kind
1426
- });
1427
- const title = kind === "provisioning" ? messages.planTitle : messages.cleanupTitle;
1428
- const label = trigger ?? (kind === "provisioning" ? messages.reviewChanges : messages.cleanUp);
1429
- if (readOnly) return /* @__PURE__ */ jsx(Fragment, {});
1430
- if (render !== void 0) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
1431
- "data-domainkit-part": kind === "provisioning" ? "plan-trigger" : "cleanup-trigger",
1432
- onClick: () => {
1433
- setOpen(true);
1434
- controller.plan();
1435
- },
1436
- type: "button",
1437
- children: label
1438
- }), render({
1439
- children: body,
1440
- open: isOpen
1441
- })] });
1442
- return /* @__PURE__ */ jsxs(Dialog.Root, {
1443
- onOpenChange: (next, details) => {
1444
- if (!next && running) {
1445
- details.cancel();
1446
- return;
1447
- }
1448
- setOpen(next);
1449
- },
1450
- open: isOpen,
1451
- children: [/* @__PURE__ */ jsx(Dialog.Trigger, {
1452
- "data-domainkit-part": kind === "provisioning" ? "plan-trigger" : "cleanup-trigger",
1453
- onClick: controller.plan,
1454
- children: label
1455
- }), /* @__PURE__ */ jsxs(Dialog.Portal, {
1456
- container: portalContainer,
1457
- children: [/* @__PURE__ */ jsx(Dialog.Backdrop, {
1458
- "data-color-scheme": colorScheme,
1459
- "data-domainkit-part": "dialog-backdrop",
1460
- "data-domainkit-root": "",
1461
- style: themeStyle
1462
- }), /* @__PURE__ */ jsxs(Dialog.Popup, {
1463
- "data-color-scheme": colorScheme,
1464
- "data-domainkit-part": kind === "provisioning" ? "plan-dialog" : "cleanup-dialog",
1465
- "data-domainkit-root": "",
1466
- style: themeStyle,
1467
- children: [
1468
- /* @__PURE__ */ jsxs("div", {
1469
- "data-domainkit-part": "dialog-header",
1470
- children: [/* @__PURE__ */ jsxs("div", {
1471
- "data-domainkit-part": "dialog-heading",
1472
- children: [/* @__PURE__ */ jsx(Dialog.Title, {
1473
- "data-domainkit-part": "dialog-title",
1474
- children: title
1475
- }), /* @__PURE__ */ jsx(Dialog.Description, {
1476
- "data-domainkit-part": "dialog-description",
1477
- children: kind === "provisioning" ? messages.planConsent : messages.cleanupConsent
1478
- })]
1479
- }), running ? null : /* @__PURE__ */ jsx(Dialog.Close, {
1480
- "aria-label": messages.close,
1481
- "data-domainkit-part": "dialog-close",
1482
- children: "×"
1483
- })]
1484
- }),
1485
- body,
1486
- /* @__PURE__ */ jsxs("div", {
1487
- "data-domainkit-part": "dialog-footer",
1488
- children: [/* @__PURE__ */ jsx(Actions$2, {
1489
- controller,
1490
- kind
1491
- }), running ? null : /* @__PURE__ */ jsx(Dialog.Close, {
1492
- "data-domainkit-part": "dialog-cancel",
1493
- children: messages.cancel
1494
- })]
1495
- })
1496
- ]
1497
- })]
1498
- })]
1499
- });
1500
- }
1501
- //#endregion
1502
- //#region src/cleanup.tsx
758
+ //#region src/cleanup.ts
1503
759
  var cleanup_exports = /* @__PURE__ */ __exportAll({
1504
- Actions: () => Actions$1,
1505
- Dialog: () => Dialog$3,
1506
- Flow: () => Flow$3,
1507
- Outcome: () => Outcome$3,
1508
- Root: () => Root$3,
1509
760
  State: () => State$2,
1510
- Status: () => Status$3,
761
+ pendingPlan: () => pendingPlan,
762
+ planOf: () => planOf,
1511
763
  useController: () => useController$3
1512
764
  });
1513
765
  /**
1514
766
  * Cleanup is bound to a receipt: it removes only what an apply proved DomainKit created. When the
1515
767
  * host does not name one, the controller reads the domain's latest from the snapshot.
1516
768
  */
1517
- function useController$3({ domain, onCleaned, receiptId }) {
769
+ function useController$3({ domain, onCleaned, readOnly, receiptId }) {
1518
770
  const { transport } = useDomainKit();
1519
771
  const group = transport.cleanup;
1520
772
  const connection = transport.connection;
@@ -1527,6 +779,7 @@ function useController$3({ domain, onCleaned, receiptId }) {
1527
779
  }),
1528
780
  group,
1529
781
  onDone: onCleaned,
782
+ readOnly,
1530
783
  plan: useCallback(() => {
1531
784
  if (group === void 0) return null;
1532
785
  if (receiptId !== void 0) return group.plan(receiptId);
@@ -1543,91 +796,327 @@ function useController$3({ domain, onCleaned, receiptId }) {
1543
796
  ])
1544
797
  });
1545
798
  }
1546
- function Root$3({ controller, ...props }) {
1547
- return usePart("div", props, { status: controller.state._tag }, {
1548
- "data-domainkit-part": "cleanup-flow",
1549
- "data-state": controller.state._tag
1550
- });
1551
- }
1552
- function Dialog$3(props) {
1553
- return /* @__PURE__ */ jsx(Dialog$4, {
1554
- ...props,
1555
- kind: "cleanup"
1556
- });
1557
- }
1558
- function Actions$1(props) {
1559
- return /* @__PURE__ */ jsx(Actions$2, {
1560
- ...props,
1561
- kind: "cleanup"
1562
- });
1563
- }
1564
- function Outcome$3(props) {
1565
- return /* @__PURE__ */ jsx(Outcome$4, {
1566
- ...props,
1567
- kind: "cleanup"
1568
- });
1569
- }
1570
- function Status$3(props) {
1571
- return /* @__PURE__ */ jsx(Status$4, {
1572
- ...props,
1573
- kind: "cleanup"
1574
- });
1575
- }
1576
- /** Cleanup on its own: a trigger that plans the removal, then the same review dialog. */
1577
- function Flow$3({ domain, onCleaned, receiptId, trigger, ...props }) {
1578
- const controller = useController$3({
1579
- domain,
1580
- ...onCleaned === void 0 ? {} : { onCleaned },
1581
- ...receiptId === void 0 ? {} : { receiptId }
1582
- });
1583
- return /* @__PURE__ */ jsxs(Root$3, {
1584
- controller,
1585
- ...props,
1586
- children: [/* @__PURE__ */ jsx(Dialog$3, {
1587
- controller,
1588
- ...trigger === void 0 ? {} : { trigger }
1589
- }), /* @__PURE__ */ jsx(Outcome$3, { controller })]
1590
- });
1591
- }
1592
799
  //#endregion
1593
- //#region src/provider.tsx
1594
- var provider_exports = /* @__PURE__ */ __exportAll({ Mark: () => Mark });
1595
- const key = (providerId) => providerId.trim().toLowerCase().replaceAll(/[^a-z0-9]+/g, "");
1596
- /** No network call: the fallback is the provider's own initial, drawn with the theme's tokens. */
1597
- const monogram = (provider) => provider.name.trim().charAt(0).toUpperCase() || "?";
800
+ //#region src/domain-field.ts
801
+ const ZonesState = Data.taggedEnum();
802
+ const nothing = {
803
+ connections: [],
804
+ providers: [],
805
+ zones: []
806
+ };
1598
807
  /**
1599
- * A provider's artwork. `DomainKit.Root`'s `marks` decides what a provider looks like; without an
1600
- * entry the mark is the provider's initial. Nothing is fetched at render time.
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.
1601
811
  */
1602
- function Mark({ provider, ...props }) {
1603
- const { marks } = useDomainKit();
1604
- const replacement = marks[provider.id];
1605
- const content = typeof replacement === "function" ? replacement(provider) : replacement ?? monogram(provider);
1606
- return usePart("span", props, { providerId: provider.id }, {
1607
- "aria-label": provider.name,
1608
- children: content,
1609
- "data-domainkit-part": "provider-mark",
1610
- "data-domainkit-provider": key(provider.id),
1611
- role: "img"
1612
- });
1613
- }
1614
- //#endregion
1615
- //#region src/connect.tsx
1616
- var connect_exports = /* @__PURE__ */ __exportAll({
1617
- Card: () => Card,
1618
- Dialog: () => Dialog$2,
1619
- Flow: () => Flow$2,
1620
- Form: () => Form,
1621
- Outcome: () => Outcome$2,
1622
- Prompt: () => Prompt,
1623
- Root: () => Root$2,
1624
- State: () => State$1,
1625
- Status: () => Status$2,
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
+ };
842
+ }
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
+ };
904
+ }
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
+ };
930
+ /**
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.
933
+ */
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);
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
+ };
1073
+ }
1074
+ //#endregion
1075
+ //#region src/connect.ts
1076
+ var connect_exports = /* @__PURE__ */ __exportAll({
1077
+ AccountsState: () => AccountsState,
1078
+ State: () => State$1,
1079
+ ZonesState: () => ZonesState,
1626
1080
  answeredInPlace: () => answeredInPlace,
1081
+ attempted: () => attempted,
1082
+ completionOf: () => completionOf,
1083
+ describeMethods: () => describeMethods,
1084
+ displayName: () => displayName,
1085
+ holdsConnection: () => holdsConnection,
1627
1086
  hostProvider: () => hostProvider,
1628
- useController: () => useController$2
1087
+ offering: () => offering,
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
1629
1098
  });
1630
1099
  const State$1 = Data.taggedEnum();
1100
+ /**
1101
+ * An interactive method leaves the page and comes back, so what the library knew is gone with it.
1102
+ * It writes down the domain it sent the customer away for, and the load that follows reads it once:
1103
+ * that, not a query parameter a host would have to keep, is how a return is told from a reload.
1104
+ */
1105
+ const RETURNING = "domainkit.returning";
1106
+ const markReturn = (domain) => {
1107
+ try {
1108
+ sessionStorage.setItem(RETURNING, domain);
1109
+ } catch {}
1110
+ };
1111
+ const tookReturn = (domain) => {
1112
+ try {
1113
+ if (sessionStorage.getItem(RETURNING) !== domain) return false;
1114
+ sessionStorage.removeItem(RETURNING);
1115
+ return true;
1116
+ } catch {
1117
+ return false;
1118
+ }
1119
+ };
1631
1120
  /** Read at connect time, not at render, so it is the page the customer is actually on. */
1632
1121
  const currentUrl = () => typeof window === "undefined" ? null : window.location.href;
1633
1122
  const snapshotOf = (state) => {
@@ -1657,10 +1146,12 @@ const settled = (snapshot, discovery) => {
1657
1146
  * transport declares it and the domain has no connection yet; a discovery failure is not the
1658
1147
  * customer's problem, so the provider list still renders.
1659
1148
  */
1660
- function useController$2({ domain, returnTo }) {
1149
+ function useController$2({ domain, readOnly: refused, returnTo }) {
1661
1150
  const { emit, navigate, revision, transport } = useDomainKit();
1662
- const readOnly = useReadOnly();
1151
+ const inherited = useReadOnly();
1152
+ const readOnly = refused ?? inherited;
1663
1153
  const connection = transport.connection;
1154
+ const provisioning = transport.provisioning;
1664
1155
  const runner = useRunner();
1665
1156
  const [state, setState] = useState(State$1.Loading({ snapshot: null }));
1666
1157
  const held = useRef({
@@ -1670,6 +1161,7 @@ function useController$2({ domain, returnTo }) {
1670
1161
  });
1671
1162
  const lastCommand = useRef(null);
1672
1163
  const attempted = useRef(null);
1164
+ const [established, setEstablished] = useState(0);
1673
1165
  const [inspected, setInspected] = useState(domain);
1674
1166
  if (inspected !== domain) {
1675
1167
  setInspected(domain);
@@ -1719,6 +1211,7 @@ function useController$2({ domain, returnTo }) {
1719
1211
  domain,
1720
1212
  snapshot
1721
1213
  };
1214
+ if (tookReturn(domain) && snapshot.status === "connected") setEstablished((count) => count + 1);
1722
1215
  setState(settled(snapshot, discovery));
1723
1216
  }
1724
1217
  });
@@ -1732,20 +1225,28 @@ function useController$2({ domain, returnTo }) {
1732
1225
  const started = useCallback((result) => {
1733
1226
  if (held.current.domain !== domain) return;
1734
1227
  switch (result._tag) {
1735
- case "Connected":
1228
+ case "Connected": {
1229
+ const snapshot = result.snapshot;
1230
+ setEstablished((count) => count + 1);
1231
+ if (snapshot === null) {
1232
+ load();
1233
+ return;
1234
+ }
1736
1235
  held.current = {
1737
1236
  discovery: null,
1738
1237
  domain,
1739
- snapshot: result.snapshot
1238
+ snapshot
1740
1239
  };
1741
- setState(settled(result.snapshot, null));
1742
- if (result.snapshot.connectionId !== null) emit(Event.Connected({
1743
- connectionId: result.snapshot.connectionId,
1240
+ setState(settled(snapshot, null));
1241
+ if (snapshot.connectionId !== null) emit(Event.Connected({
1242
+ connectionId: snapshot.connectionId,
1744
1243
  domain,
1745
- snapshot: result.snapshot
1244
+ snapshot
1746
1245
  }));
1747
1246
  return;
1247
+ }
1748
1248
  case "Redirect":
1249
+ markReturn(domain);
1749
1250
  setState(State$1.Redirecting({ url: result.authorizationUrl }));
1750
1251
  navigate(result.authorizationUrl);
1751
1252
  return;
@@ -1760,13 +1261,17 @@ function useController$2({ domain, returnTo }) {
1760
1261
  }, [
1761
1262
  domain,
1762
1263
  emit,
1264
+ load,
1763
1265
  navigate
1764
1266
  ]);
1765
1267
  const submit = useCallback((effect, attempt) => {
1766
1268
  if (held.current.domain !== domain) return;
1767
1269
  attempted.current = attempt;
1768
1270
  const command = () => {
1769
- setState(State$1.Submitting({ snapshot: held.current.snapshot }));
1271
+ setState(State$1.Submitting({
1272
+ discovery: held.current.discovery,
1273
+ snapshot: held.current.snapshot
1274
+ }));
1770
1275
  runner.run(effect, {
1771
1276
  onFailure,
1772
1277
  onSuccess: started
@@ -1784,11 +1289,16 @@ function useController$2({ domain, returnTo }) {
1784
1289
  started
1785
1290
  ]);
1786
1291
  const connect = useCallback((input) => {
1787
- if (connection === void 0) return;
1292
+ if (connection === void 0 || readOnly) return;
1788
1293
  const destination = input.returnTo ?? (returnTo === void 0 ? currentUrl() : returnTo);
1789
1294
  const interactive = destination === null ? {} : { returnTo: destination };
1790
1295
  const method = input.method === "token" ? Transport.Method.token(input.values ?? {}) : input.method === "oauth" ? Transport.Method.oauth(interactive) : Transport.Method.integration(interactive);
1791
- 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({
1792
1302
  domain,
1793
1303
  method,
1794
1304
  provider: input.provider
@@ -1799,11 +1309,12 @@ function useController$2({ domain, returnTo }) {
1799
1309
  }, [
1800
1310
  connection,
1801
1311
  domain,
1312
+ readOnly,
1802
1313
  returnTo,
1803
1314
  submit
1804
1315
  ]);
1805
1316
  const reuse = useCallback((input) => {
1806
- if (connection === void 0) return;
1317
+ if (connection === void 0 || readOnly) return;
1807
1318
  submit(connection.attach({
1808
1319
  connectionId: input.connectionId,
1809
1320
  domain,
@@ -1812,6 +1323,7 @@ function useController$2({ domain, returnTo }) {
1812
1323
  }, [
1813
1324
  connection,
1814
1325
  domain,
1326
+ readOnly,
1815
1327
  submit
1816
1328
  ]);
1817
1329
  const select = useCallback((zone) => {
@@ -1825,7 +1337,10 @@ function useController$2({ domain, returnTo }) {
1825
1337
  if (held.current.domain !== domain) return;
1826
1338
  attempted.current = null;
1827
1339
  const command = () => {
1828
- setState(State$1.Submitting({ snapshot: held.current.snapshot }));
1340
+ setState(State$1.Submitting({
1341
+ discovery: held.current.discovery,
1342
+ snapshot: held.current.snapshot
1343
+ }));
1829
1344
  runner.run(effect, {
1830
1345
  onFailure,
1831
1346
  onSuccess: () => {
@@ -1847,17 +1362,20 @@ function useController$2({ domain, returnTo }) {
1847
1362
  runner
1848
1363
  ]);
1849
1364
  const detach = useCallback(() => {
1850
- const attachmentId = held.current.snapshot?.attachmentId;
1851
- 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;
1852
1368
  release(connection.detach(attachmentId), Event.Detached({ domain }));
1853
1369
  }, [
1854
1370
  connection,
1855
1371
  domain,
1372
+ readOnly,
1856
1373
  release
1857
1374
  ]);
1858
1375
  const disconnect = useCallback(() => {
1859
1376
  const connectionId = held.current.snapshot?.connectionId;
1860
- 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;
1861
1379
  release(connection.disconnect(connectionId), Event.Disconnected({
1862
1380
  connectionId,
1863
1381
  domain
@@ -1865,6 +1383,7 @@ function useController$2({ domain, returnTo }) {
1865
1383
  }, [
1866
1384
  connection,
1867
1385
  domain,
1386
+ readOnly,
1868
1387
  release
1869
1388
  ]);
1870
1389
  const retry = useCallback(() => {
@@ -1876,12 +1395,53 @@ function useController$2({ domain, returnTo }) {
1876
1395
  load,
1877
1396
  readOnly
1878
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
+ ]);
1879
1436
  return {
1880
1437
  connect,
1881
1438
  detach,
1882
1439
  disconnect,
1883
- discovery: state._tag === "Disconnected" || state._tag === "Failure" ? state.discovery : null,
1440
+ domain,
1441
+ established,
1442
+ discovery: state._tag === "Disconnected" || state._tag === "Failure" || state._tag === "Submitting" ? state.discovery : null,
1884
1443
  providers: snapshotOf(state)?.providers ?? [],
1444
+ receipt: receipt?.id === receiptId ? receipt : null,
1885
1445
  refresh: load,
1886
1446
  retry,
1887
1447
  reuse,
@@ -1890,18 +1450,24 @@ function useController$2({ domain, returnTo }) {
1890
1450
  state
1891
1451
  };
1892
1452
  }
1893
- function Root$2({ controller, ...props }) {
1894
- return usePart("div", props, { status: controller.state._tag }, {
1895
- "data-domainkit-part": "connection-root",
1896
- "data-state": controller.state._tag
1897
- });
1898
- }
1899
1453
  /**
1900
1454
  * Whether a failure is already answered where it was raised: beside the method the customer used.
1901
- * A flow rendering its own `Outcome` beside the dialog skips it, so the failure announces once.
1455
+ * A surface rendering one outcome for the whole flow skips it, so the failure announces once.
1902
1456
  */
1903
1457
  const answeredInPlace = (controller) => controller.state._tag === "Failure" && controller.state.attempt !== null;
1904
1458
  /**
1459
+ * Whether the surface should read as connected: it is, or a command it started is in flight over a
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.
1462
+ */
1463
+ const holdsConnection = (controller) => controller.state._tag === "Connected" || controller.state._tag === "Submitting" && controller.snapshot?.status === "connected";
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
+ /**
1905
1471
  * The provider whose nameservers serve this domain, as the snapshot describes it. Discovery names
1906
1472
  * it by id; without a descriptor there is nothing to draw, so there is no host.
1907
1473
  */
@@ -1911,498 +1477,227 @@ const hostProvider = (controller) => {
1911
1477
  const named = discovery.host.provider;
1912
1478
  return controller.providers.find((entry) => entry.id === named) ?? null;
1913
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;
1914
1482
  /** The provider as the customer knows it, falling back to the id when the snapshot has no name. */
1915
- const displayName = (controller, provider) => controller.providers.find((entry) => entry.id === provider)?.name ?? provider;
1483
+ const displayName = (controller, providerId) => providerOf(controller, providerId)?.name ?? providerId;
1916
1484
  /**
1917
- * The last failure as a card: media, the catalog's title and description, and the retry the flow
1918
- * allows. Children replace the composition and keep the binding.
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.
1919
1489
  */
1920
- function Outcome$2({ children, controller, ...props }) {
1921
- const { messages } = useDomainKit();
1922
- const readOnly = useReadOnly();
1490
+ const offering = (controller, connect = "detected") => {
1491
+ if (holdsConnection(controller)) return false;
1923
1492
  const state = controller.state;
1924
- if (state._tag !== "Failure") return null;
1925
- const acted = state.attempt === null ? void 0 : displayName(controller, state.attempt.provider);
1926
- const words = outcome(state.error, messages, {
1927
- ...controller.snapshot === null ? {} : { domain: controller.snapshot.domain },
1928
- ...acted === void 0 ? {} : { provider: acted }
1929
- });
1930
- return /* @__PURE__ */ jsx(Provider, {
1931
- value: {
1932
- description: words.description,
1933
- layout: props.layout ?? "card",
1934
- retry: readOnly ? null : controller.retry,
1935
- retryPart: "connection-retry",
1936
- title: words.title,
1937
- tone: "danger"
1938
- },
1939
- children: /* @__PURE__ */ jsx(Root$6, {
1940
- ...props,
1941
- children: children ?? /* @__PURE__ */ jsx(Composition, {})
1942
- })
1943
- });
1944
- }
1945
- function Status$2({ controller, ...props }) {
1946
- const { messages } = useDomainKit();
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;
1497
+ };
1498
+ /**
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.
1501
+ */
1502
+ const reusableConnections = (controller) => {
1947
1503
  const state = controller.state;
1948
- const provider = controller.snapshot?.provider ?? "";
1949
- const text = () => {
1950
- switch (state._tag) {
1951
- case "Loading": return messages.loading;
1952
- case "Submitting": return messages.connecting;
1953
- case "Redirecting": return messages.redirecting;
1954
- case "Connected": return messages.connectedTo(provider);
1955
- case "Reconnect": return messages.reconnectRequired(provider);
1956
- case "SelectionRequired": return messages.chooseZone;
1957
- case "Disconnected": return messages.notConnected;
1958
- case "Failure": return "";
1959
- }
1504
+ const discovery = controller.discovery;
1505
+ const candidates = state._tag === "SelectionRequired" ? state.candidates.map((candidate) => ({
1506
+ ...candidate,
1507
+ connectionId: state.connectionId
1508
+ })) : discovery !== null && discovery._tag === "SelectionRequired" ? discovery.candidates : [];
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
1960
1536
  };
1961
- return usePart("p", props, { status: state._tag }, {
1962
- children: text(),
1963
- "data-domainkit-part": "connection-status",
1964
- "data-state": state._tag,
1965
- role: state._tag === "Submitting" || state._tag === "Loading" ? "status" : void 0
1966
- });
1967
- }
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
+ };
1968
1553
  /**
1969
1554
  * The one field a rejected command was about, if any. A provider that turns down credentials is
1970
1555
  * answering the secret it was given, and it names no field, so the first secret carries the
1971
1556
  * answer; anything else is about the request rather than one input.
1972
1557
  */
1973
- const rejected = (reason, fields) => {
1974
- if (reason === null) return null;
1558
+ const rejectedField = (error, fields) => {
1559
+ if (error === null) return null;
1560
+ const reason = error.reason;
1975
1561
  if (reason._tag === "InvalidInput") return fields.find((field) => field.name === reason.field)?.name ?? null;
1976
1562
  if (reason._tag !== "Unauthenticated" && reason._tag !== "Forbidden") return null;
1977
1563
  return fields.find((field) => field.secret)?.name ?? null;
1978
1564
  };
1979
- /**
1980
- * One declared field in shadcn's `Field` anatomy: the label, the control, the note under it, and
1981
- * the error the provider answered with. The docs link rides the field it explains.
1982
- */
1983
- function TokenField({ controller, docsUrl, field, invalid }) {
1984
- const { messages } = useDomainKit();
1985
- const icons = useIcons();
1986
- const id = useId();
1987
- return /* @__PURE__ */ jsxs("div", {
1988
- "data-domainkit-part": "field",
1989
- "data-invalid": invalid ? "" : void 0,
1990
- "data-orientation": "vertical",
1991
- children: [
1992
- /* @__PURE__ */ jsxs("label", {
1993
- "data-domainkit-part": "field-label",
1994
- htmlFor: id,
1995
- children: [messages.fieldLabel(field.name), field.required ? null : /* @__PURE__ */ jsx("span", {
1996
- "data-domainkit-part": "field-optional",
1997
- children: messages.optionalField
1998
- })]
1999
- }),
2000
- /* @__PURE__ */ jsx("input", {
2001
- "aria-invalid": invalid ? true : void 0,
2002
- autoComplete: "off",
2003
- "data-domainkit-part": "field-input",
2004
- id,
2005
- name: field.name,
2006
- required: field.required,
2007
- type: field.secret ? "password" : "text"
2008
- }),
2009
- docsUrl === null ? null : /* @__PURE__ */ jsxs("a", {
2010
- "data-domainkit-part": "field-description",
2011
- href: docsUrl,
2012
- rel: "noreferrer",
2013
- target: "_blank",
2014
- children: [messages.getToken, /* @__PURE__ */ jsx("span", {
2015
- "aria-hidden": "true",
2016
- "data-icon": "inline-end",
2017
- children: icons.external
2018
- })]
2019
- }),
2020
- invalid ? /* @__PURE__ */ jsx("div", {
2021
- "data-domainkit-part": "field-error",
2022
- children: /* @__PURE__ */ jsx(Outcome$2, {
2023
- controller,
2024
- layout: "inline"
2025
- })
2026
- }) : null
2027
- ]
2028
- });
2029
- }
2030
- /**
2031
- * One auth method, rendered from its descriptor. A token method draws an input per declared
2032
- * field, so a provider that needs an account id alongside a token needs no code here. The fields
2033
- * a provider does not need sit behind a disclosure, so the common form is one field.
2034
- */
2035
- function Method({ controller, method, provider }) {
2036
- const { messages } = useDomainKit();
1565
+ /** Whether a failure was raised by this provider's method, so the form answers beside it. */
1566
+ const attempted = (controller, provider, method) => {
2037
1567
  const state = controller.state;
2038
- const busy = state._tag === "Submitting";
2039
- const failed = state._tag === "Failure" && state.attempt !== null && state.attempt.provider === provider.id && state.attempt.method === method.kind;
2040
- if (method.fields === null) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
2041
- "data-domainkit-part": method.kind === "oauth" ? "oauth-connect" : "integration-connect",
2042
- "data-provider": provider.id,
2043
- disabled: busy,
2044
- onClick: () => controller.connect({
2045
- method: method.kind,
2046
- provider: provider.id
2047
- }),
2048
- type: "button",
2049
- children: method.kind === "oauth" ? messages.methodOAuth(provider.name) : messages.methodIntegration(provider.name)
2050
- }), failed ? /* @__PURE__ */ jsx(Outcome$2, {
2051
- controller,
2052
- layout: "inline"
2053
- }) : null] });
2054
- const fields = method.fields;
2055
- const answered = rejected(failed && state._tag === "Failure" ? state.error.reason : null, fields);
2056
- const required = fields.filter((field) => field.required);
2057
- const optional = fields.filter((field) => !field.required);
2058
- const explains = (required.find((field) => field.secret) ?? required[0] ?? optional[0])?.name;
2059
- const render = (field) => /* @__PURE__ */ jsx(TokenField, {
2060
- controller,
2061
- docsUrl: field.name === explains ? method.docsUrl : null,
2062
- field,
2063
- invalid: field.name === answered
2064
- }, field.name);
2065
- return /* @__PURE__ */ jsxs("form", {
2066
- "data-domainkit-part": "token-connect",
2067
- "data-provider": provider.id,
2068
- onSubmit: (event) => {
2069
- event.preventDefault();
2070
- const data = new FormData(event.currentTarget);
2071
- const values = {};
2072
- for (const field of fields) {
2073
- const value = data.get(field.name);
2074
- if (typeof value === "string" && (value !== "" || field.required)) values[field.name] = value;
2075
- }
2076
- controller.connect({
2077
- method: method.kind,
2078
- provider: provider.id,
2079
- values
2080
- });
2081
- },
2082
- children: [
2083
- required.map(render),
2084
- optional.length === 0 ? null : /* @__PURE__ */ jsxs("details", {
2085
- "data-domainkit-part": "more-options",
2086
- open: optional.some((field) => field.name === answered) || void 0,
2087
- children: [/* @__PURE__ */ jsx("summary", {
2088
- "data-domainkit-part": "more-options-trigger",
2089
- children: messages.moreOptions
2090
- }), optional.map(render)]
2091
- }),
2092
- failed && answered === null ? /* @__PURE__ */ jsx(Outcome$2, {
2093
- controller,
2094
- layout: "inline"
2095
- }) : null,
2096
- /* @__PURE__ */ jsx("button", {
2097
- "data-domainkit-part": "token-submit",
2098
- disabled: busy,
2099
- type: "submit",
2100
- children: messages.methodToken
2101
- })
2102
- ]
2103
- });
2104
- }
2105
- /**
2106
- * One provider's methods. With a heading it is a disclosure the customer opens; the provider a
2107
- * narrowed dialog is about carries no heading, because the dialog title already names it.
2108
- */
2109
- function Authentication({ controller, onOpen, open, provider }) {
2110
- const methods = provider.methods.map((method) => /* @__PURE__ */ jsx(Method, {
2111
- controller,
2112
- method,
2113
- provider
2114
- }, `${provider.id}:${method.kind}`));
2115
- return /* @__PURE__ */ jsxs("section", {
2116
- "data-domainkit-part": "provider-authentication",
2117
- "data-provider": provider.id,
2118
- "data-state": open ? "open" : "closed",
2119
- children: [onOpen === void 0 ? null : /* @__PURE__ */ jsxs("button", {
2120
- "aria-expanded": open,
2121
- "data-domainkit-part": "provider-heading",
2122
- onClick: onOpen,
2123
- type: "button",
2124
- children: [/* @__PURE__ */ jsx(Mark, { provider }), /* @__PURE__ */ jsx("strong", { children: provider.name })]
2125
- }), open ? methods : null]
2126
- });
2127
- }
1568
+ if (state._tag !== "Failure" || state.attempt === null) return null;
1569
+ return state.attempt.provider === provider && state.attempt.method === method ? state.error : null;
1570
+ };
1571
+ //#endregion
1572
+ //#region src/records.ts
2128
1573
  /**
2129
- * Everything a disconnected domain can do: a connection discovery already found, the connections
2130
- * this owner already has, then each provider's declared methods.
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.
2131
1576
  */
2132
- function Form({ controller, ...props }) {
2133
- const { messages } = useDomainKit();
2134
- const [opened, setOpened] = useState(null);
2135
- const state = controller.state;
2136
- const snapshot = controller.snapshot;
2137
- const discovery = controller.discovery;
2138
- const candidates = state._tag === "SelectionRequired" ? state.candidates.map((candidate) => ({
2139
- ...candidate,
2140
- connectionId: state.connectionId
2141
- })) : discovery !== null && discovery._tag === "SelectionRequired" ? discovery.candidates : [];
2142
- const resolved = discovery !== null && discovery._tag === "Resolved" ? discovery : null;
2143
- const reusable = snapshot?.reusable ?? [];
2144
- const providers = controller.providers;
2145
- const host = hostProvider(controller);
2146
- const others = providers.filter((provider) => provider.id !== host?.id);
2147
- const authentication = (provider, open) => /* @__PURE__ */ jsx(Authentication, {
2148
- controller,
2149
- onOpen: () => setOpened(open ? "" : provider.id),
2150
- open,
2151
- provider
2152
- }, provider.id);
2153
- return usePart("div", props, { status: state._tag }, {
2154
- children: /* @__PURE__ */ jsxs(Fragment, { children: [
2155
- resolved === null ? null : /* @__PURE__ */ jsxs("section", {
2156
- "data-domainkit-part": "discovery-resolved",
2157
- children: [/* @__PURE__ */ jsx("p", { children: messages.discoveredConnection(resolved.label) }), /* @__PURE__ */ jsx("button", {
2158
- "data-domainkit-part": "attach-target",
2159
- onClick: () => controller.reuse({
2160
- connectionId: resolved.connectionId,
2161
- zone: resolved.zone
2162
- }),
2163
- type: "button",
2164
- children: messages.useConnection(resolved.label)
2165
- })]
2166
- }),
2167
- candidates.length === 0 ? null : /* @__PURE__ */ jsxs("section", {
2168
- "data-domainkit-part": "discovery-candidates",
2169
- children: [/* @__PURE__ */ jsx("p", { children: state._tag === "SelectionRequired" ? messages.chooseZone : messages.discoveryAmbiguous }), /* @__PURE__ */ jsx("div", {
2170
- "data-domainkit-part": "target-list",
2171
- "data-state": "ambiguous",
2172
- children: candidates.map((candidate) => /* @__PURE__ */ jsx("button", {
2173
- "data-domainkit-part": "attach-target",
2174
- onClick: () => controller.reuse({
2175
- connectionId: candidate.connectionId,
2176
- zone: candidate.zone
2177
- }),
2178
- type: "button",
2179
- children: messages.useConnection(candidate.label)
2180
- }, `${candidate.connectionId}:${candidate.zone}`))
2181
- })]
2182
- }),
2183
- reusable.length === 0 ? null : /* @__PURE__ */ jsxs("section", {
2184
- "data-domainkit-part": "reusable-connection",
2185
- children: [/* @__PURE__ */ jsx("div", {
2186
- "data-domainkit-part": "reusable-connection-heading",
2187
- children: /* @__PURE__ */ jsx("strong", { children: messages.reusableConnections })
2188
- }), reusable.map((connection) => /* @__PURE__ */ jsx("button", {
2189
- "data-connection-id": connection.connectionId,
2190
- "data-domainkit-part": "attach-target",
2191
- onClick: () => controller.reuse({ connectionId: connection.connectionId }),
2192
- type: "button",
2193
- children: messages.useConnection(connection.provider)
2194
- }, connection.connectionId))]
2195
- }),
2196
- providers.length === 0 ? /* @__PURE__ */ jsx("p", {
2197
- "data-domainkit-part": "target-unavailable",
2198
- children: messages.noProviders
2199
- }) : host === null ? others.map((provider) => authentication(provider, opened === null ? provider.id === others[0]?.id : opened === provider.id)) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Authentication, {
2200
- controller,
2201
- open: true,
2202
- provider: host
2203
- }), others.length === 0 ? null : /* @__PURE__ */ jsxs("details", {
2204
- "data-domainkit-part": "other-providers",
2205
- children: [/* @__PURE__ */ jsx("summary", {
2206
- "data-domainkit-part": "other-providers-trigger",
2207
- children: messages.useAnotherProvider
2208
- }), others.map((provider) => authentication(provider, opened === provider.id))]
2209
- })] })
2210
- ] }),
2211
- "data-domainkit-part": "connection-form"
2212
- });
2213
- }
2214
- /** The connect surface. Without `render` it is a Base UI dialog behind `trigger`. */
2215
- function Dialog$2({ children, controller, render, title, trigger }) {
2216
- const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
2217
- const readOnly = useReadOnly();
2218
- const [open, setOpen] = useState(false);
2219
- const state = controller.state;
2220
- const busy = state._tag === "Submitting";
2221
- const unattributed = state._tag === "Failure" && !answeredInPlace(controller);
2222
- const snapshot = controller.snapshot;
2223
- const attached = snapshot?.provider ?? null;
2224
- const host = hostProvider(controller);
2225
- const named = attached === null ? host?.name ?? null : displayName(controller, attached);
2226
- const heading = title ?? (named === null ? messages.connectAnyTitle : messages.connectTitle(named));
2227
- const running = state._tag === "Loading" || state._tag === "Submitting" || state._tag === "Redirecting" || state._tag === "SelectionRequired";
2228
- const label = named === null ? messages.connectAnyTitle : messages.connect;
2229
- const body = children ?? /* @__PURE__ */ jsx(Form, { controller });
2230
- if (readOnly) return /* @__PURE__ */ jsx(Fragment, {});
2231
- if (render !== void 0) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
2232
- "data-domainkit-part": "connection-trigger",
2233
- onClick: () => setOpen(!open),
2234
- type: "button",
2235
- children: trigger ?? label
2236
- }), render({
2237
- children: body,
2238
- open
2239
- })] });
2240
- return /* @__PURE__ */ jsxs(Dialog.Root, {
2241
- onOpenChange: (next, details) => {
2242
- if (!next && busy) {
2243
- details.cancel();
2244
- return;
2245
- }
2246
- setOpen(next);
2247
- },
2248
- open,
2249
- children: [/* @__PURE__ */ jsx(Dialog.Trigger, {
2250
- "data-domainkit-part": "connection-trigger",
2251
- children: trigger ?? label
2252
- }), /* @__PURE__ */ jsxs(Dialog.Portal, {
2253
- container: portalContainer,
2254
- children: [/* @__PURE__ */ jsx(Dialog.Backdrop, {
2255
- "data-color-scheme": colorScheme,
2256
- "data-domainkit-part": "dialog-backdrop",
2257
- "data-domainkit-root": "",
2258
- style: themeStyle
2259
- }), /* @__PURE__ */ jsxs(Dialog.Popup, {
2260
- "data-color-scheme": colorScheme,
2261
- "data-domainkit-part": "connection-dialog",
2262
- "data-domainkit-root": "",
2263
- style: themeStyle,
2264
- children: [
2265
- /* @__PURE__ */ jsxs("div", {
2266
- "data-domainkit-part": "dialog-header",
2267
- children: [/* @__PURE__ */ jsxs("div", {
2268
- "data-domainkit-part": "dialog-heading",
2269
- children: [/* @__PURE__ */ jsx(Dialog.Title, {
2270
- "data-domainkit-part": "dialog-title",
2271
- children: heading
2272
- }), /* @__PURE__ */ jsx(Dialog.Description, {
2273
- "data-domainkit-part": "dialog-description",
2274
- children: messages.connectDescription(snapshot?.domain ?? "")
2275
- })]
2276
- }), busy ? null : /* @__PURE__ */ jsx(Dialog.Close, {
2277
- "aria-label": messages.close,
2278
- "data-domainkit-part": "dialog-close",
2279
- children: "×"
2280
- })]
2281
- }),
2282
- /* @__PURE__ */ jsxs("div", {
2283
- "data-domainkit-part": "dialog-body",
2284
- children: [
2285
- running ? /* @__PURE__ */ jsx(Status$2, { controller }) : null,
2286
- body,
2287
- unattributed ? /* @__PURE__ */ jsx(Outcome$2, { controller }) : null
2288
- ]
2289
- }),
2290
- busy ? null : /* @__PURE__ */ jsx("div", {
2291
- "data-domainkit-part": "dialog-footer",
2292
- children: /* @__PURE__ */ jsx(Dialog.Close, {
2293
- "data-domainkit-part": "dialog-cancel",
2294
- children: messages.cancel
2295
- })
2296
- })
2297
- ]
2298
- })]
2299
- })]
2300
- });
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;
1608
+ }
1609
+ chunks.push(chunk);
1610
+ return chunks.map((part) => `"${part}"`).join(" ");
1611
+ };
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
+ });
1657
+ }
1658
+ };
2301
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(":");
2302
1679
  /**
2303
- * The disconnected offer: the provider whose nameservers serve the domain, what that means, and
2304
- * the trigger that connects it. With no host detected there is nothing to offer, so nothing
2305
- * renders unless the host application asks for the dialog anyway.
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.
2306
1684
  */
2307
- function Prompt({ connect = "detected", controller, ...props }) {
2308
- const { messages } = useDomainKit();
2309
- const readOnly = useReadOnly();
2310
- const state = controller.state;
2311
- const host = hostProvider(controller);
2312
- const discovery = controller.discovery;
2313
- const offers = host !== null || discovery !== null && discovery._tag !== "NotFound" || (controller.snapshot?.reusable.length ?? 0) > 0;
2314
- const inFlight = state._tag === "Submitting" || state._tag === "Redirecting" || state._tag === "SelectionRequired" || state._tag === "Reconnect";
2315
- const element = usePart("div", props, { status: state._tag }, {
2316
- children: /* @__PURE__ */ jsxs(Fragment, { children: [host === null ? null : /* @__PURE__ */ jsxs("div", {
2317
- "data-domainkit-part": "host-identity",
2318
- children: [
2319
- /* @__PURE__ */ jsx(Mark, { provider: host }),
2320
- /* @__PURE__ */ jsx("span", {
2321
- "data-domainkit-part": "host-name",
2322
- children: host.name
2323
- }),
2324
- /* @__PURE__ */ jsx("span", {
2325
- "data-domainkit-part": "host-statement",
2326
- children: messages.hostOwnsZone
2327
- })
2328
- ]
2329
- }), /* @__PURE__ */ jsx(Dialog$2, { controller })] }),
2330
- "data-domainkit-part": "connect-prompt",
2331
- "data-host": host?.id
2332
- });
2333
- if (!inFlight && !offers && connect !== "always") return null;
2334
- if (readOnly && host === null) return null;
2335
- return element;
2336
- }
2337
- /** A connected domain: which provider holds it, and how to let it go. */
2338
- function Card({ controller, ...props }) {
2339
- const { messages } = useDomainKit();
2340
- const readOnly = useReadOnly();
2341
- const state = controller.state;
2342
- const snapshot = controller.snapshot;
2343
- const provider = controller.providers.find((entry) => entry.id === snapshot?.provider);
2344
- return usePart("div", props, { status: state._tag }, {
2345
- children: /* @__PURE__ */ jsxs(Fragment, { children: [
2346
- /* @__PURE__ */ jsxs("div", {
2347
- "data-domainkit-part": "connected-identity",
2348
- children: [provider === void 0 ? null : /* @__PURE__ */ jsx(Mark, { provider }), /* @__PURE__ */ jsx(Status$2, { controller })]
2349
- }),
2350
- readOnly ? null : /* @__PURE__ */ jsxs("div", {
2351
- "data-domainkit-part": "connected-actions",
2352
- children: [/* @__PURE__ */ jsx("button", {
2353
- "data-domainkit-part": "disconnect-trigger",
2354
- disabled: state._tag === "Submitting",
2355
- onClick: controller.detach,
2356
- type: "button",
2357
- children: messages.detach
2358
- }), /* @__PURE__ */ jsx("button", {
2359
- "data-domainkit-part": "disconnect-action",
2360
- disabled: state._tag === "Submitting",
2361
- onClick: controller.disconnect,
2362
- type: "button",
2363
- children: messages.disconnect
2364
- })]
2365
- }),
2366
- /* @__PURE__ */ jsx(Outcome$2, { controller })
2367
- ] }),
2368
- "data-domainkit-part": "connected-card"
2369
- });
2370
- }
2371
- /** Connection on its own: the card once connected, the prompt until then. */
2372
- function Flow$2({ connect, domain, returnTo, ...props }) {
2373
- const controller = useController$2({
2374
- domain,
2375
- ...returnTo === void 0 ? {} : { returnTo }
2376
- });
2377
- const state = controller.state;
2378
- return /* @__PURE__ */ jsx(Root$2, {
2379
- controller,
2380
- ...props,
2381
- children: state._tag === "Connected" ? /* @__PURE__ */ jsx(Card, { controller }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Prompt, {
2382
- controller,
2383
- ...connect === void 0 ? {} : { connect }
2384
- }), answeredInPlace(controller) ? null : /* @__PURE__ */ jsx(Outcome$2, { controller })] })
2385
- });
2386
- }
1685
+ const requirementsKey = (records) => JSON.stringify(records);
2387
1686
  //#endregion
2388
- //#region src/provision.tsx
1687
+ //#region src/provision.ts
2389
1688
  var provision_exports = /* @__PURE__ */ __exportAll({
2390
- Actions: () => Actions,
2391
- Dialog: () => Dialog$1,
2392
- Flow: () => Flow$1,
2393
- Outcome: () => Outcome$1,
2394
- Root: () => Root$1,
2395
1689
  State: () => State$2,
2396
- Status: () => Status$1,
1690
+ pendingPlan: () => pendingPlan,
1691
+ planOf: () => planOf,
2397
1692
  useController: () => useController$1
2398
1693
  });
2399
- /** Plan, approve, apply. `approve` authorizes the digest and applies it in the same action. */
2400
1694
  /**
1695
+ * Plan, approve, apply; `approve` authorizes the digest and applies it in the same action.
2401
1696
  * Requirements identify themselves by content, so an inline array does not abandon the attempt.
2402
1697
  * Every field counts: a plan turns on `policy`, and `ttl` and `purpose` ride the wire with it.
2403
1698
  */
2404
1699
  const keyOf = (domain, requirements) => [domain, requirementsKey(requirements)].join("|");
2405
- function useController$1({ domain, onApplied, requirements }) {
1700
+ function useController$1({ domain, onApplied, readOnly, requirements }) {
2406
1701
  const { transport } = useDomainKit();
2407
1702
  const group = transport.provisioning;
2408
1703
  return useAttempt({
@@ -2414,6 +1709,7 @@ function useController$1({ domain, onApplied, requirements }) {
2414
1709
  }),
2415
1710
  group,
2416
1711
  onDone: onApplied,
1712
+ readOnly,
2417
1713
  plan: useCallback(() => group === void 0 ? null : group.plan({
2418
1714
  domain,
2419
1715
  requirements
@@ -2424,63 +1720,12 @@ function useController$1({ domain, onApplied, requirements }) {
2424
1720
  ])
2425
1721
  });
2426
1722
  }
2427
- function Root$1({ controller, ...props }) {
2428
- return usePart("div", props, { status: controller.state._tag }, {
2429
- "data-domainkit-part": "provisioning-flow",
2430
- "data-state": controller.state._tag
2431
- });
2432
- }
2433
- function Dialog$1(props) {
2434
- return /* @__PURE__ */ jsx(Dialog$4, {
2435
- ...props,
2436
- kind: "provisioning"
2437
- });
2438
- }
2439
- /** Approve and Decline for a plan already under review. */
2440
- function Actions(props) {
2441
- return /* @__PURE__ */ jsx(Actions$2, {
2442
- ...props,
2443
- kind: "provisioning"
2444
- });
2445
- }
2446
- function Outcome$1(props) {
2447
- return /* @__PURE__ */ jsx(Outcome$4, {
2448
- ...props,
2449
- kind: "provisioning"
2450
- });
2451
- }
2452
- function Status$1(props) {
2453
- return /* @__PURE__ */ jsx(Status$4, {
2454
- ...props,
2455
- kind: "provisioning"
2456
- });
2457
- }
2458
- /** Provisioning on its own: a trigger that plans, then the review dialog. */
2459
- function Flow$1({ domain, onApplied, requirements, trigger, ...props }) {
2460
- const controller = useController$1({
2461
- domain,
2462
- requirements,
2463
- ...onApplied === void 0 ? {} : { onApplied }
2464
- });
2465
- return /* @__PURE__ */ jsxs(Root$1, {
2466
- controller,
2467
- ...props,
2468
- children: [/* @__PURE__ */ jsx(Dialog$1, {
2469
- controller,
2470
- ...trigger === void 0 ? {} : { trigger }
2471
- }), /* @__PURE__ */ jsx(Outcome$1, { controller })]
2472
- });
2473
- }
2474
1723
  //#endregion
2475
- //#region src/verify.tsx
1724
+ //#region src/verify.ts
2476
1725
  var verify_exports = /* @__PURE__ */ __exportAll({
2477
- Evidence: () => Evidence,
2478
- ObserveAction: () => ObserveAction,
2479
- Outcome: () => Outcome,
2480
- Root: () => Root,
2481
1726
  State: () => State,
2482
- Status: () => Status,
2483
- useController: () => useController
1727
+ useController: () => useController,
1728
+ valuesOf: () => valuesOf
2484
1729
  });
2485
1730
  /**
2486
1731
  * What the observer read back for the requirement's name, or `null` when it read nothing back at
@@ -2499,9 +1744,11 @@ function useController({ domain, polling = true, requirements }) {
2499
1744
  const timer = useRef(void 0);
2500
1745
  const signature = requirements === void 0 ? null : requirementsKey(requirements);
2501
1746
  const requested = useMemo(() => requirements === void 0 ? void 0 : { requirements }, [signature]);
1747
+ const held = useRef(domain);
2502
1748
  const [observed, setObserved] = useState(domain);
2503
1749
  if (observed !== domain) {
2504
1750
  setObserved(domain);
1751
+ held.current = domain;
2505
1752
  setState(State.Idle());
2506
1753
  }
2507
1754
  const observe = useCallback(() => {
@@ -2510,6 +1757,7 @@ function useController({ domain, polling = true, requirements }) {
2510
1757
  setState((previous) => State.Observing({ readiness: readinessOf(previous) }));
2511
1758
  runner.run(requested === void 0 ? verification.observe(domain) : verification.observe(domain, requested), {
2512
1759
  onFailure: (error) => {
1760
+ if (held.current !== domain) return;
2513
1761
  setState((previous) => State.Failure({
2514
1762
  error,
2515
1763
  readiness: readinessOf(previous)
@@ -2519,7 +1767,10 @@ function useController({ domain, polling = true, requirements }) {
2519
1767
  error
2520
1768
  }));
2521
1769
  },
2522
- onSuccess: (readiness) => setState(State.Observed({ readiness }))
1770
+ onSuccess: (readiness) => {
1771
+ if (held.current !== domain) return;
1772
+ setState(State.Observed({ readiness }));
1773
+ }
2523
1774
  });
2524
1775
  }, [
2525
1776
  domain,
@@ -2552,302 +1803,114 @@ function useController({ domain, polling = true, requirements }) {
2552
1803
  state
2553
1804
  };
2554
1805
  }
2555
- function Root({ controller, ...props }) {
2556
- const overall = controller.readiness?.overall ?? null;
2557
- return usePart("div", props, { status: controller.state._tag }, {
2558
- "data-domainkit-part": "verification-status",
2559
- "data-overall": overall ?? void 0,
2560
- "data-state": controller.state._tag
2561
- });
2562
- }
2563
- function ObserveAction({ controller, ...props }) {
2564
- const { messages } = useDomainKit();
2565
- const observing = controller.state._tag === "Observing";
2566
- return usePart("button", props, { status: controller.state._tag }, {
2567
- "aria-busy": observing,
2568
- children: controller.readiness === null ? messages.checkDns : messages.checkAgain,
2569
- "data-domainkit-part": "observe-action",
2570
- "data-loading": observing ? "" : void 0,
2571
- disabled: observing,
2572
- onClick: controller.observe,
2573
- type: "button"
2574
- });
2575
- }
2576
- /** Evidence carries no id; its source and position identify it within one requirement. */
2577
- const evidenceKey = (evidence, index) => [evidence._tag, index].join("-");
2578
- /**
2579
- * Per-requirement readiness and, below it, the host's own evidence: an SES identity, a certificate,
2580
- * anything DomainKit cannot observe but the host feeds in.
2581
- */
2582
- function Evidence({ readiness }) {
2583
- const { messages } = useDomainKit();
2584
- const icons = useIcons();
2585
- return /* @__PURE__ */ jsxs("div", {
2586
- "data-domainkit-part": "observation-list",
2587
- children: [readiness.requirements.map((requirement) => {
2588
- const settled = requirement.status === "satisfied";
2589
- return /* @__PURE__ */ jsxs("section", {
2590
- "data-domainkit-part": "observation-group",
2591
- children: [
2592
- /* @__PURE__ */ jsxs("div", {
2593
- "data-domainkit-part": "observation-row",
2594
- children: [/* @__PURE__ */ jsxs("span", {
2595
- "data-domainkit-part": "observation-record",
2596
- children: [
2597
- messages.recordType(requirement.record),
2598
- " ",
2599
- requirement.record.name
2600
- ]
2601
- }), /* @__PURE__ */ jsx(Status$5, { status: requirement.status })]
2602
- }),
2603
- settled ? null : /* @__PURE__ */ jsx("p", {
2604
- "data-domainkit-part": "observation-expected",
2605
- children: messages.expectedValue(DnsRecord.data(requirement.record))
2606
- }),
2607
- /* @__PURE__ */ jsx("ul", { children: requirement.evidence.map((evidence, index) => {
2608
- const values = settled ? null : valuesOf(evidence);
2609
- return /* @__PURE__ */ jsxs("li", {
2610
- "data-domainkit-part": "observation-source",
2611
- children: [
2612
- messages.evidenceSource(evidence),
2613
- values === null ? null : /* @__PURE__ */ jsx("span", {
2614
- "data-domainkit-part": "observation-observed",
2615
- children: values.length === 0 ? messages.observedNothing : messages.observedValues(values)
2616
- }),
2617
- evidence.detail === null ? null : /* @__PURE__ */ jsx("p", {
2618
- "data-domainkit-part": "observation-note",
2619
- children: evidence.detail
2620
- })
2621
- ]
2622
- }, evidenceKey(evidence, index));
2623
- }) })
2624
- ]
2625
- }, identity(requirement.record));
2626
- }), readiness.host.length === 0 ? null : /* @__PURE__ */ jsxs("section", {
2627
- "data-domainkit-part": "observation-group",
2628
- "data-source": "host",
2629
- children: [/* @__PURE__ */ jsx("h3", {
2630
- "data-domainkit-part": "observation-source",
2631
- children: messages.hostEvidence
2632
- }), /* @__PURE__ */ jsx("ul", { children: readiness.host.map((evidence, index) => /* @__PURE__ */ jsxs("li", {
2633
- "data-domainkit-part": "host-evidence",
2634
- children: [
2635
- /* @__PURE__ */ jsx("span", {
2636
- "aria-hidden": "true",
2637
- "data-icon": "inline-start",
2638
- children: evidence.status === "ok" ? icons.success : evidence.status === "pending" ? icons.pending : icons.failure
2639
- }),
2640
- /* @__PURE__ */ jsx("span", {
2641
- "data-domainkit-part": "observation-record",
2642
- children: evidence.label
2643
- }),
2644
- /* @__PURE__ */ jsx("span", {
2645
- "data-domainkit-part": "record-status",
2646
- "data-status": evidence.status,
2647
- children: messages.hostEvidenceStatus(evidence.status)
2648
- }),
2649
- evidence.detail === null ? null : /* @__PURE__ */ jsx("p", {
2650
- "data-domainkit-part": "observation-note",
2651
- children: evidence.detail
2652
- })
2653
- ]
2654
- }, `${evidence.source}-${index}`)) })]
2655
- })]
2656
- });
2657
- }
2658
- /**
2659
- * The last observation that failed. Observing reads the world rather than changing the domain, so
2660
- * the retry stays even in read-only.
2661
- */
2662
- function Outcome({ children, controller, ...props }) {
2663
- const { messages } = useDomainKit();
2664
- const state = controller.state;
2665
- if (state._tag !== "Failure") return null;
2666
- const words = outcome(state.error, messages);
2667
- return /* @__PURE__ */ jsx(Provider, {
2668
- value: {
2669
- description: words.description,
2670
- layout: props.layout ?? "card",
2671
- retry: controller.retry,
2672
- retryPart: "verification-retry",
2673
- title: words.title,
2674
- tone: "danger"
2675
- },
2676
- children: /* @__PURE__ */ jsx(Root$6, {
2677
- ...props,
2678
- children: children ?? /* @__PURE__ */ jsx(Composition, {})
2679
- })
2680
- });
2681
- }
2682
- /** The default verification slot: a button that observes, with the evidence behind it. */
2683
- function Status({ controller, render, ...props }) {
2684
- const { colorScheme, messages, portalContainer, themeStyle } = useDomainKit();
2685
- const readiness = controller.readiness;
2686
- const body = /* @__PURE__ */ jsxs(Fragment, { children: [
2687
- controller.state._tag === "Observing" ? /* @__PURE__ */ jsx("p", {
2688
- "data-domainkit-part": "verification-progress",
2689
- role: "status",
2690
- children: messages.observing
2691
- }) : null,
2692
- readiness === null ? /* @__PURE__ */ jsx("p", {
2693
- "data-domainkit-part": "verification-progress",
2694
- children: messages.noEvidence
2695
- }) : /* @__PURE__ */ jsx(Evidence, { readiness }),
2696
- /* @__PURE__ */ jsx(Outcome, { controller })
2697
- ] });
2698
- return /* @__PURE__ */ jsx(Root, {
2699
- controller,
2700
- ...props,
2701
- children: render === void 0 ? /* @__PURE__ */ jsxs(Popover.Root, { children: [/* @__PURE__ */ jsx(Popover.Trigger, { render: /* @__PURE__ */ jsx(ObserveAction, { controller }) }), /* @__PURE__ */ jsx(Popover.Portal, {
2702
- container: portalContainer,
2703
- children: /* @__PURE__ */ jsx(Popover.Positioner, {
2704
- align: "end",
2705
- "data-domainkit-part": "verification-positioner",
2706
- sideOffset: 6,
2707
- children: /* @__PURE__ */ jsxs(Popover.Popup, {
2708
- "aria-busy": controller.state._tag === "Observing",
2709
- "aria-label": messages.checkDns,
2710
- "data-color-scheme": colorScheme,
2711
- "data-domainkit-part": "verification-popover",
2712
- "data-domainkit-root": "",
2713
- style: themeStyle,
2714
- children: [/* @__PURE__ */ jsx(Popover.Arrow, { "data-domainkit-part": "verification-arrow" }), body]
2715
- })
2716
- })
2717
- })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ObserveAction, { controller }), render({
2718
- children: body,
2719
- open: controller.state._tag !== "Idle"
2720
- })] })
2721
- });
2722
- }
2723
1806
  //#endregion
2724
- //#region src/domain.tsx
2725
- var domain_exports = /* @__PURE__ */ __exportAll({ Flow: () => Flow });
2726
- function DefaultConnection({ connect, controller }) {
2727
- const stated = controller.state._tag === "Disconnected" && hostProvider(controller) !== null;
2728
- return controller.state._tag === "Connected" ? /* @__PURE__ */ jsx(Card, { controller }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2729
- stated ? null : /* @__PURE__ */ jsx(Status$2, { controller }),
2730
- /* @__PURE__ */ jsx(Prompt, {
2731
- connect,
2732
- controller
2733
- }),
2734
- answeredInPlace(controller) ? null : /* @__PURE__ */ jsx(Outcome$2, { controller })
2735
- ] });
2736
- }
2737
- function DefaultActions({ cleanup, connection, provisioning }) {
2738
- const { capabilities, messages } = useDomainKit();
2739
- const readOnly = useReadOnly();
2740
- const icons = useIcons();
2741
- const connected = connection.state._tag === "Connected";
2742
- if (readOnly) return null;
2743
- const hasReceipt = connection.snapshot?.lastReceiptId != null;
2744
- return /* @__PURE__ */ jsxs(Fragment, { children: [capabilities.includes("provisioning") && connected ? /* @__PURE__ */ jsxs(Fragment, { children: [
2745
- /* @__PURE__ */ jsx("button", {
2746
- "data-domainkit-part": "plan-trigger",
2747
- disabled: provisioning.state._tag === "Planning",
2748
- onClick: provisioning.plan,
2749
- type: "button",
2750
- children: messages.reviewChanges
2751
- }),
2752
- /* @__PURE__ */ jsx(Status$1, { controller: provisioning }),
2753
- /* @__PURE__ */ jsx(Actions, { controller: provisioning }),
2754
- /* @__PURE__ */ jsx(Outcome$1, { controller: provisioning })
2755
- ] }) : null, capabilities.includes("cleanup") && connected && hasReceipt ? /* @__PURE__ */ jsxs(Fragment, { children: [
2756
- /* @__PURE__ */ jsxs("button", {
2757
- "data-domainkit-part": "cleanup-trigger",
2758
- disabled: cleanup.state._tag === "Planning",
2759
- onClick: cleanup.plan,
2760
- type: "button",
2761
- children: [/* @__PURE__ */ jsx("span", {
2762
- "aria-hidden": "true",
2763
- "data-icon": "inline-start",
2764
- children: icons.close
2765
- }), messages.cleanUp]
2766
- }),
2767
- /* @__PURE__ */ jsx(Status$3, { controller: cleanup }),
2768
- /* @__PURE__ */ jsx(Actions$1, { controller: cleanup }),
2769
- /* @__PURE__ */ jsx(Outcome$3, { controller: cleanup })
2770
- ] }) : null] });
2771
- }
1807
+ //#region src/domain.ts
2772
1808
  /**
2773
- * The whole lifecycle for one domain: connect, review, apply, observe, clean up. The flow renders
2774
- * only what the transport declares, and every part of it is a slot with a default. It adds no
2775
- * layout containers of its own, so a host's grid can place the slot output directly.
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.
2776
1812
  */
2777
- function Flow({ connect = "detected", domain, onApplied, onCleaned, readOnly, requirements, returnTo, slots = {}, ...props }) {
1813
+ var domain_exports = /* @__PURE__ */ __exportAll({ useFlow: () => useFlow });
1814
+ /** Built from primitives alone, so the memo that reports it depends on exactly those. */
1815
+ const flowState = (input) => ({
1816
+ applied: input.receipt !== null,
1817
+ connected: input.connected,
1818
+ connection: input.status,
1819
+ label: input.label,
1820
+ offering: input.offering,
1821
+ provider: input.provider,
1822
+ provisioning: input.planning,
1823
+ readOnly: input.readOnly,
1824
+ receiptId: input.receipt === null ? null : Receipt.ReceiptId.make(input.receipt)
1825
+ });
1826
+ function useFlow({ connect = "detected", domain, onApplied, onCleaned, readOnly, requirements, returnTo }) {
2778
1827
  const { capabilities } = useDomainKit();
2779
1828
  const inherited = useReadOnly();
1829
+ const surfaceReadOnly = readOnly ?? inherited;
2780
1830
  const connection = useController$2({
2781
1831
  domain,
1832
+ readOnly: surfaceReadOnly,
2782
1833
  ...returnTo === void 0 ? {} : { returnTo }
2783
1834
  });
2784
1835
  const refresh = connection.refresh;
2785
1836
  const provisioning = useController$1({
2786
1837
  domain,
1838
+ readOnly: surfaceReadOnly,
2787
1839
  onApplied: useCallback((receipt) => {
2788
1840
  refresh();
2789
1841
  onApplied?.(receipt);
2790
1842
  }, [onApplied, refresh]),
2791
1843
  requirements
2792
1844
  });
2793
- const cleanup = useController$3({
2794
- domain,
2795
- onCleaned: useCallback((receipt) => {
2796
- refresh();
2797
- onCleaned?.(receipt);
2798
- }, [onCleaned, refresh]),
2799
- ...connection.snapshot?.lastReceiptId == null ? {} : { receiptId: Receipt.ReceiptId.make(connection.snapshot.lastReceiptId) }
2800
- });
2801
1845
  const verification = useController({
2802
1846
  domain,
2803
1847
  requirements
2804
1848
  });
2805
- const readiness = verification.readiness;
2806
- return usePart("div", props, {
2807
- connection: connection.state._tag,
2808
- provisioning: provisioning.state._tag
2809
- }, {
2810
- children: /* @__PURE__ */ jsxs(ReadOnly, {
2811
- value: readOnly ?? inherited,
2812
- children: [
2813
- !capabilities.includes("connection") ? null : slots.connection === void 0 ? /* @__PURE__ */ jsx(DefaultConnection, {
2814
- connect,
2815
- controller: connection,
2816
- domain
2817
- }) : slots.connection({
2818
- connect,
2819
- controller: connection,
2820
- domain
2821
- }),
2822
- slots.records === void 0 ? /* @__PURE__ */ jsx(Table, {
2823
- readiness,
2824
- records: requirements
2825
- }) : slots.records({
2826
- controller: verification,
2827
- domain,
2828
- readiness,
2829
- records: requirements
2830
- }),
2831
- !capabilities.includes("verification") ? null : slots.verification === void 0 ? /* @__PURE__ */ jsx(Status, { controller: verification }) : slots.verification({
2832
- controller: verification,
2833
- domain
2834
- }),
2835
- slots.actions === void 0 ? /* @__PURE__ */ jsx(DefaultActions, {
2836
- cleanup,
2837
- connection,
2838
- domain,
2839
- provisioning
2840
- }) : slots.actions({
2841
- cleanup,
2842
- connection,
2843
- domain,
2844
- provisioning
2845
- })
2846
- ]
2847
- }),
2848
- "data-domainkit-part": "domain-flow",
2849
- "data-domain": domain
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) }
2850
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);
1863
+ const buildPlan = provisioning.plan;
1864
+ useEffect(() => {
1865
+ if (signature === null) {
1866
+ planned.current = null;
1867
+ return;
1868
+ }
1869
+ if (planned.current === signature) return;
1870
+ planned.current = signature;
1871
+ buildPlan();
1872
+ }, [buildPlan, signature]);
1873
+ const connected = holdsConnection(connection);
1874
+ const offering$1 = offering(connection, connect);
1875
+ const provider = connection.snapshot?.provider ?? hostProvider(connection)?.id ?? null;
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({
1887
+ connected,
1888
+ label: connection.snapshot?.attachment?.label ?? null,
1889
+ offering: offering$1,
1890
+ planning: provisioning.state._tag,
1891
+ provider,
1892
+ readOnly: surfaceReadOnly,
1893
+ receipt: receiptId,
1894
+ status: connection.state._tag
1895
+ }),
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);
2851
1914
  }
2852
1915
  //#endregion
2853
1916
  //#region src/testing.ts
@@ -2861,6 +1924,6 @@ const transport = Testing.transport;
2861
1924
  //#region src/failure.ts
2862
1925
  const Failure = Data.taggedEnum();
2863
1926
  //#endregion
2864
- export { cleanup_exports as Cleanup, connect_exports as Connect, domain_exports as Domain, domain_kit_exports as DomainKit, Event, Failure, messages_exports as Messages, operations_exports as Operations, outcome_exports as Outcome, provider_exports as Provider, provision_exports as Provision, records_exports as Records, testing_exports as Testing, theme_exports as Theme, verify_exports as Verify, defaultIcons, useIcons };
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 };
2865
1928
 
2866
1929
  //# sourceMappingURL=index.mjs.map