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>
);
}