Skip to content
DomainKit
Esc
↑↓navigate↵open⌘Jpreview
On this page

Words and outcomes

Replace any sentence the flow puts on screen through the message catalog, and render a failed step from the title and description its reason resolves to.

Nothing in @domainkit/react renders a tag, a status literal, or a reason name. Every user-visible string arrives from Messages.Catalog first, so one prop translates the whole surface.

Messages

Messages.Catalog holds every string in eight groups: actions, progress, connection, plan review, records, receipts, verification, and failures, which have one function per DomainKit.Error reason. The catalog is closed, so every key has a default in Messages.english; messages on DomainKit.Root takes a Partial<Catalog> and merges over it, and a host replaces one key without restating the rest. DomainKit.useMessages() returns the merged catalog to a component of your own.

An operation becomes a sentence through operation, a conflict becomes a reason and an advice through conflictReason and conflictAdvice, and a token field name becomes a label through fieldLabel, which spaces out a name such as accountId until a host labels the field itself.

The verification group phrases what a failing check found: expectedValue(value), observedValues(values), and observedNothing. The only observer text that reaches a customer unwrapped is the detail the observer itself wrote.

administratorConnects(provider) is what a read-only surface says where a connect control would otherwise be, and notConnected is what it says when no provider serves the zone at all.

Outcomes

A failure carries the DomainKit.Error itself, so a surface reads reason, category, and isRetryable rather than parsing text. Outcome.useDescribe() binds the catalog and returns the pair the reason resolves to: a heading the customer reads first, and what to do about it.

/** A failure keeps the `DomainKit.Error`, so read `reason`, `category`, and `isRetryable`. */
export function ConnectionProblem({ domain }: { readonly domain: string }) {
  const describe = Outcome.useDescribe();
  const connection = Connect.useController({ domain });
  const state = connection.state;
  if (state._tag !== "Failure") return null;
  const words = describe(state.error, {
    ...(state.attempt === null
      ? {}
      : { provider: Connect.displayName(connection, state.attempt.provider) }),
  });
  return (
    <div role="alert">
      <strong>{words.title}</strong>
      <p>{words.description}</p>
      {state.error.isRetryable ? (
        <button onClick={connection.retry} type="button">
          Try again
        </button>
      ) : null}
    </div>
  );
}
import { Connect } from "@domainkit/react";
import { useEffect } from "react";

import { Outcome } from "@/components/domainkit/outcome";

import { PreviewRoot, previewDomain } from "../../lib/preview-flow.tsx";

/** The failure is real: the fake provider turns down an empty token. */
function Failed() {
  const controller = Connect.useController({ domain: previewDomain });
  const connect = controller.connect;
  useEffect(() => {
    connect({ method: "token", provider: "meridian", values: { token: "" } });
  }, [connect]);
  const error = controller.state._tag === "Failure" ? controller.state.error : null;
  return (
    <div className="grid gap-4">
      <Outcome context={{ provider: "Meridian" }} error={error} onRetry={controller.retry} />
      <Outcome context={{ provider: "Meridian" }} error={error} layout="inline" />
    </div>
  );
}

export default function OutcomeExample() {
  return (
    <PreviewRoot>
      <Failed />
    </PreviewRoot>
  );
}

The reason alone cannot name the provider the customer typed a token for, so the surface supplies it in the context. Outcome.describe(error, catalog) is the same answer without a React context, for a server render or a log line.

Provider artwork

Provider marks are the host’s, and the registry’s provider-row takes them keyed by provider id. Whatever you pass is the mark: nothing wraps it in a tile, so a circular logo stays a circle. Without an entry the mark is the provider’s initial, and nothing is fetched while the row renders.

import { Domain } from "@domainkit/react";

import { ProviderRow } from "@/components/domainkit/provider-row";

import {
  PreviewRoot,
  previewDomain,
  previewMarks,
  previewRequirements,
} from "../../lib/preview-flow.tsx";

function Row({ readOnly }: { readonly readOnly?: boolean }) {
  const flow = Domain.useFlow({
    domain: previewDomain,
    requirements: previewRequirements,
    ...(readOnly === undefined ? {} : { readOnly }),
  });
  return (
    <div className="overflow-hidden rounded-xl border border-border bg-card">
      <ProviderRow flow={flow} marks={previewMarks} />
    </div>
  );
}

export default function ProviderRowExample() {
  return (
    <PreviewRoot>
      <div className="grid gap-3">
        <Row />
        <Row readOnly />
      </div>
    </PreviewRoot>
  );
}

Last updated on

Was this page helpful?