caliper uidocs

Approvals

Free

Cards that ask the user to allow or deny an action, with a risk label.

Installs components/ui/approvals.tsxDepends on @radix-ui/react-slot, lucide-reactMotion shared motion tokens
pnpm dlx shadcn@latest add https://caliperui.site/r/approvals.json

Delete 3 stale snapshot files

High risk

These files are no longer referenced by any test. Deleting them cannot be undone from here.

  • tests/__snapshots__/table.old.snap
  • tests/__snapshots__/tree.old.snap
  • tests/__snapshots__/thread.old.snap

Approval needed: Delete 3 stale snapshot files

When to use #

Use it for

  • An agent wants to run a tool that changes something.
  • The decision should be explicit and reversible where possible.

Skip it when

  • The action is harmless and routine; do not interrupt.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/approvals.json

Adds its 6 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above. It is built from ApprovalCard, ToolApproval and RiskChip.

example.tsx
import { useRef, useState } from "react"
import { ApprovalCard, type ApprovalStatus } from "@/components/ui/approvals"

export function ApprovalsDemo() {
  const [, setStage] = useState(0);
  const [approval, setApproval] = useState<ApprovalStatus>("pending");
  const timers = useRef<ReturnType<typeof setTimeout>[]>([]);
  const later = (ms: number, fn: () => void) => timers.current.push(setTimeout(fn, ms));
  return (
    <ApprovalCard
              title="Delete 3 stale snapshot files"
              risk="high"
              description="These files are no longer referenced by any test. Deleting them cannot be undone from here."
              details={<ul className="grid gap-1 font-mono text-xs"><li>tests/__snapshots__/table.old.snap</li><li>tests/__snapshots__/tree.old.snap</li><li>tests/__snapshots__/thread.old.snap</li></ul>}
              status={approval}
              approveLabel="Delete"
              resolvedBy="You"
              resolvedAt="just now"
              onApprove={() => {
                setApproval("approved");
                later(500, () => setStage(4));
              }}
              onDeny={() => {
                setApproval("denied");
                later(500, () => setStage(4));
              }}
            />
  );
}

API reference #

ApprovalCard

component
PropTypeDefaultDescription
title *string—
descriptionReactNode—
riskApprovalRisk"medium"
detailsReactNode—What exactly will happen: a diff, a command, a list of changes.
status *ApprovalStatus—
onApprove *() => void—
onDeny *() => void—
approveLabelstring"Approve"
denyLabelstring"Deny"
resolvedBystring—
resolvedAtstring—
classNamestring—

ToolApproval

component
PropTypeDefaultDescription
name *string—
summarystring—One sentence in plain language: "Read three files in src/".
argsRecord<string, unknown>—
scopesreadonly string[]—Permissions this call needs, in plain words.
status *ToolStatus—
resultReactNode—
durationstring—For example "1.2 s".
onDecision((decision: ToolDecision) => void)—
errorTextstring—
classNamestring—

RiskChip

component
PropTypeDefault
risk *ApprovalRisk—

Accessibility #

It exposes status and alert semantics, with aria-hidden, aria-label and aria-expanded kept in step with state. Changes that matter are announced through a live region.

No custom key handling; Tab and the native activation keys do the work.

Motion #

Transitions use the shared duration and easing tokens. Under prefers-reduced-motion those tokens collapse to instant and travel distances go to zero, so state changes still happen without movement.