Loading page…
Loading page…
The submission contract for every ColdHarbor surface. A server failure is our fault, so it never strands the crew: the submit button comes straight back, values stay put, and the error routes through the notification helm to a toast with a Retry action — or the inline aria-live banner when no helm is mounted. Only the field-validation gate may hold the button, because only the user can clear that.
The outage switch makes the fake backend reject with a 503. Submit, watch the error toast raise with Retry, then flip the switch off and resubmit — no refresh, no retyping. The submit stays disabled only while the memo is empty (the validation gate).
Under NotificationProvider the failure leaves the form entirely; the inline banner never renders, so the same error never shows twice.
import { Form, FormSubmit, FormErrorBanner, NotificationProvider } from "@coldharbor/webui";
// Once, near the app root — routes every inline error/status to toasts.
<NotificationProvider>
<App />
</NotificationProvider>
<Form
invalid={fieldErrors.length > 0} // the only thing allowed to hold submit
onSubmit={async () => broadcast(values)} // throw → recoverable error state
errorTitle="Broadcast failed — that's on us"
successTitle="Instruction accepted"
>
{fields}
<FormErrorBanner /> {/* renders only when no toast route exists */}
<FormSubmit>Send instruction</FormSubmit>
</Form>--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-primary--ch-danger--ch-focusWithout a NotificationProvider (or with notify='inline') the failure lands in the aria-live FormErrorBanner instead. The submit still comes straight back — the routing changes, the recovery contract doesn't.
Same demo, no provider: the banner announces the failure and the button shows its error face, clickable.
<Form notify="inline" onSubmit={submit}>
{fields}
<FormErrorBanner />
<FormSubmit>Send</FormSubmit>
</Form>A 422 is the user's to fix, not ours to apologize for. Throw FormValidationError from onSubmit and each field picks its message up via useFieldError(name) — no toast is raised, and the validation gate decides whether submit holds.
import { FormValidationError, useFieldError } from "@coldharbor/webui";
<Form
onSubmit={async () => {
const res = await api.send(values);
if (res.status === 422) {
throw new FormValidationError(res.fieldErrors);
}
}}
>
// In a field:
const serverError = useFieldError("email");
<Input error={localError ?? serverError} ... />The shell's four states, reachable through the demo above. idle → submitting (button locks, spins) → success (button lands on its check, rolls back to idle after resetAfterSuccess) or error (button shows Try again and stays live).
invalid holds the submit; everything else is interactive.
After a failure the form is idle-with-error: banner up (inline route), button live.