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 user-visible string. Actions, progress, connection, plan review, records, receipts, verification, and failures are the labeled groups. Failures have one function per DomainKit.Error reason. Callback recovery adds recoveryCancelled(provider), recoveryRestart(provider), and recoveryInspect(provider), each a title and description. retry and dismissRecovery are the action labels. 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?