Escalation
ProHand a case off outside the app, by a page, an email or a ticket: the ways the app offers, with who each would reach right now; exactly what will be sent and to whom, shown before anything is sent; and, once sent, a record of what went, to whom, when, the other system's reference and whether anyone has acknowledged it. No way is chosen for the person, and Send stays locked, saying why, until they choose one that reaches somebody. A message an agent drafted says so and can be edited first; what the app leaves out is listed as not sent. When a new draft arrives, the subject and message take it if the person hasn't edited them; if they have, their text stays and the card says a newer draft arrived, with a button to use it.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0 and 2.9.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/escalation
Brings in: bidi, button, heading-level, heading-scope, input, locale, locked, textarea, tokens, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import * as React from "react"
import { Escalation, type EscalationRoute, type EscalationSent } from "@/components/candor/escalation"
const routes: EscalationRoute[] = [
{
id: "payments-oncall",
kind: "page",
label: "Payments on-call",
to: ["Priya Shah (on call until 18:00)"],
via: "the paging service",
note: "Repeats every 5 minutes until someone acknowledges.",
},
{ id: "billing", kind: "email", label: "Billing team", to: ["billing@example.com"], via: "email from support@example.com" },
{ id: "payments-queue", kind: "ticket", label: "Payments queue", to: ["Payments team"], via: "the team’s ticket queue" },
]
export default function Example() {
const [sent, setSent] = React.useState<EscalationSent>()
return (
<Escalation
className="max-w-xl"
title="Refund stuck for order 4411"
routes={routes}
message={{
subject: "Refund stuck for order 4411",
body: "The refund for order 4411 has failed three times since 09:10 with “card issuer unavailable”.\n\nThe customer has asked twice today. Can someone check the issuer status and retry by hand?",
attachments: ["Order 4411’s timeline", "The three failed refund attempts"],
}}
draftedBy="Support agent"
notSent={["The customer’s card details", "The rest of the conversation"]}
meanwhile="The agent tells the customer a person is looking into it, and stops replying."
onSend={async (routeId, message) => {
await new Promise((r) => setTimeout(r, 800))
const route = routes.find((r) => r.id === routeId)!
setSent({
route,
message,
by: "You",
at: new Date().toISOString(),
reference: route.kind === "ticket" ? "PAY-1182" : route.kind === "page" ? "PG-2291" : undefined,
acknowledged: route.kind === "email" ? undefined : null,
})
}}
onCancel={() => {}}
sent={sent}
/>
)
}<Escalation>
| Prop | Type | Default | Description |
|---|---|---|---|
| titlerequired | string | — | What is being escalated, e.g. "Refund stuck for order 4411". |
| routesrequired | EscalationRoute[] | — | The ways to send it, in the order to show them. None is chosen for the person. |
| messagerequired | EscalationMessage | — | The message to send, as drafted. A new one replaces the subject and message while the person hasn't edited them; once they have, their text stays and the card offers the new draft. |
| draftedBy | string | undefined | — | Who drafted the message, named by the app, when it wasn't the person: "Triage agent". |
| editable | boolean | undefined | true | The person can edit the subject and message before sending. On by default. |
| notSent | string[] | undefined | — | What is deliberately not sent, in your words: "The customer’s payment details". |
| meanwhile | string | undefined | — | What happens in the app meanwhile: "The agent stops replying until someone takes the case." |
| onSend | ((routeId: string, message: EscalationMessage) => void | Promise<void>) | undefined | — | Shows Send. Called with the chosen route's id and the message as it will go. The button waits on a returned promise; if it throws or rejects, the card says sending failed. Once it resolves, Send stays locked until you pass `sent` or `error`. |
| onCancel | (() => void) | undefined | — | Shows Cancel. |
| sent | EscalationSent | undefined | — | What was sent, once your server has recorded it. The card then shows the record instead of the form. |
| busy | boolean | undefined | false | The app is sending: the button says so and waits. |
| error | string | undefined | — | Why the last attempt failed, in the app's words. |
| className | string | undefined | — | Extra classes for the outer element. |
| headingLevel | HeadingLevel | undefined | — | The level of the heading. The "as sent" and "preview" labels sit one level below it. Defaults to one below the nearest `HeadingScope`, else 2. |
Strings
The fixed words, in English by default. Change any of them under escalation in <LocaleProvider strings={{ … }}>; the table is exported as escalationStrings.
| Key | English |
|---|---|
| heading | Escalate: {title} |
| sentHeading | Escalated: {title} |
| routes | Send it by |
| routesHint | Nothing is sent until you choose a way and press send. |
| page | Page |
| ticket | Ticket |
| nobody | Nobody to send to right now. |
| preview | What will be sent |
| previewEmpty | Choose a way to send it to see who gets it. |
| to | To |
| via | Via |
| subject | Subject |
| message | Message |
| attachments | Attached |
| notSent | Not sent |
| draftedBy | Drafted by {name}. Check it before sending. |
| newDraft | {name} redrafted the message after you started editing. Your text is as you left it. |
| newDraftPlain | The message was redrafted after you started editing. Your text is as you left it. |
| useNewDraft | Use the new draft |
| meanwhile | Meanwhile: |
| sendPage | Send page |
| sendEmail | Send email |
| sendTicket | Open ticket |
| send | Send |
| sending | Sending… |
| cancel | Cancel |
| chooseRoute | Choose a way to send it first. |
| noRecipients | {route} has nobody to send to right now. |
| needsSubject | Write a subject first. |
| needsBody | Write the message first. |
| busy | Wait for the send to finish. |
| waiting | Sent. Waiting for the app to record it. |
| sendFailed | Sending failed. |
| sentLine | {kind} sent to {route} by {name} |
| reference | Reference |
| acknowledgement | Acknowledged |
| acknowledged | By {name} |
| notAcknowledged | Not reported yet. |
| asSent | As sent |
Types
- EscalationRoute
- A way to hand a case off outside the app, as your app knows it right now.
{ id: string; /** How it goes out: a page to someone on call, an email, or a ticket in another team's queue. */ kind: "page" | "email" | "ticket"; /** The route's name: "Platform on-call", "Billing team", "Ops queue". */ label: string; /** Who gets it, as your app resolves them now: "Priya Shah (on call until 18:00)", "billing@example.com". `[]` when nobody would. */ to: string[]; /** What it goes through, in your words: "the paging service", "the Ops ticket queue". */ via?: string | undefined; /** One more line about the route, e.g. "Pages repeat every 5 minutes until someone acknowledges." */ note?: string | undefined; }- EscalationMessage
- What is sent: the subject, the message and what's attached, as plain text.
{ subject: string; /** The message, as plain text. It's shown as text, never as markup. */ body: string; /** What goes with it, named: "The last 50 log lines", "Order 4411’s timeline". */ attachments?: string[] | undefined; }- EscalationSent
- What was sent, as your server recorded it after sending.
{ /** The route it went by, as it was when sent. */ route: EscalationRoute; /** The message exactly as sent. */ message: EscalationMessage; /** Who sent it, named by the app. */ by: string; /** When it was sent (ISO). */ at: string; /** The other system's reference for it: a ticket number, a page id. */ reference?: string | undefined; /** Who acknowledged it and when, as the other system reports it. `null` when that system reports acknowledgements and none has come; leave out when it doesn't report them. */ acknowledged?: { by: string; at: string; } | null | undefined; }
Types from other items
The props above take these. Each is defined by another item; import it from the path shown.
- HeadingLevel from @/lib/candor/heading-level
- A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.
1 | 2 | 3 | 4 | 5 | 6