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