# Undo window

An action just taken ("Deleted 3 rows"), with Undo for as long as the app says it can be undone: `until` (when the server said the window closes) or `seconds` (the window the server granted, counted from when this appears). There is no default window: without either, the action shows with the app's `cannotUndo` text and no Undo. The time left is read from the clock, so a tab that slept catches up, and the text says what the count means. Undo shows pending until `onUndo` settles (the button stays in the tab order, marked `aria-disabled`), then "Undone." or "Couldn't undo: …" with the rejection's own reason. When the window closes, the notice stays, says the action can't be undone now, and keeps what happened. Undo never runs past the deadline, even between ticks. After a failed undo with time left the button stays so the person can retry. When the button goes away while it had focus, focus moves to the notice.

- Registry name: `undo-window` (`@candor/undo-window`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/undo-window

## Install

```bash
npx candor-ui add undo-window
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/undo-window`.
It brings in: button, locale, utils.

## Example

```tsx
"use client"

import * as React from "react"
import { UndoWindow } from "@/components/undo-window"
import { Button } from "@/components/ui/button"

/*
 * The windows here stand in for what a server would return with the action's result: how long it keeps the rows, or
 * that it can't take the action back at all. UndoWindow has no window of its own.
 */
export default function Example() {
  const [run, setRun] = React.useState(0)
  return (
    <div className="flex flex-col gap-4">
      <UndoWindow
        key={run}
        className="max-w-md"
        action="Archived 3 threads"
        detail="Moved to Archive. They’re still searchable."
        seconds={20}
        onUndo={() => new Promise((resolve) => setTimeout(resolve, 600))}
      />
      <UndoWindow
        key={`fail-${run}`}
        className="max-w-md"
        action="Deleted 3 rows from Leads"
        seconds={20}
        onUndo={() => new Promise((_, reject) => setTimeout(() => reject(new Error("Row 2 was changed by someone else since.")), 600))}
      />
      <UndoWindow className="max-w-md" action="Sent the export to 240 people" cannotUndo="Sent email can’t be recalled." onUndo={async () => {}} />
      <Button variant="outline" size="sm" className="self-start" onClick={() => setRun((n) => n + 1)}>
        Reset the examples
      </Button>
    </div>
  )
}
```

## <UndoWindow>

Props:

- `action: string`: What was done, in the app's words, past tense: "Deleted 3 rows", "Sent to 240 people".
- `detail?: React.ReactNode | undefined`: More about it: which rows, where they went.
- `until?: string | undefined`: When the window closes, as an ISO timestamp from the server. Used over `seconds` when both are given.
- `seconds?: number | undefined`: How many seconds the server granted, counted from the render where it first arrives (a value that arrives after mount starts the window then). Not a default: pass what the server said.
- `onUndo: () => Promise<void>`: Undoes the action. Pending until it settles; a rejection's message is shown as the reason it couldn't.
- `onClose?: (() => void) | undefined`: Called once, when the window closes without an undo.
- `cannotUndo?: string | undefined`: Why the action can't be undone, shown when no window is given: "Sent email can't be recalled."
- `className?: string | undefined`

## Strings (`undoWindow`)

The fixed words, English by default. Override any of them under `undoWindow` in `<LocaleProvider strings={{ undoWindow: { … } }}>`; the table is exported as `undoWindowStrings`.

- `undo`: Undo
- `left`: {time} left to undo.
- `undoing`: Undoing…
- `undone`: Undone.
- `failed`: Couldn't undo: {reason}
- `failedNoReason`: Couldn't undo. No reason was given.
- `closed`: Can't be undone now.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
