Skip to content
Component installs need the registry setup
VegaStack Design

Approval Card

An agent asks its owner to approve a risky action — who asks, what, on which records, why, until when — with one Approve / Decline for the whole request.

Status
stable
Since
0.25.28
Accessibility pattern
labelled region, described buttons, announced outcome

Last updated

Claude Code via Manoj, agentwants to move 2 tasks to Trash

“These duplicate #10 and #11.”

You can restore them from Trash for 30 days.

Requested Expires

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Approval Card before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify approval-card

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/approval-card

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/button, @vegastack/person-avatar, @vegastack/relative-time, @vegastack/use-announcer.

Usage

import { ApprovalCard } from "@/components/ui/approval-card";

<ApprovalCard
  actor={{ name: "Claude Code", kind: "agent", via: "Manoj" }}
  verb={
    <>
      wants to <strong>move 2 tasks to Trash</strong>
    </>
  }
  items={tasks.map((t) => ({
    id: t.id,
    title: t.title,
    href: t.href,
    meta: t.space,
  }))}
  reason="These duplicate #10 and #11."
  reassurance="You can restore them from Trash for 30 days."
  requestedAt={request.createdAt}
  expiresAt={request.expiresAt}
  status={request.status}
  onApprove={() => approve(request.id)}
  onDecline={() => decline(request.id)}
/>;

The host owns the request: it passes status back after a decision, and an onApprove or onDecline that returns a promise keeps its button busy (a rejection shows its message under the card). The actor is a Person with kind: "agent", so it reads exactly as the agent does in comments, activity and pickers: the purple bot, "Claude Code via Manoj" and the "Agent" label.

Scope

One decision covers the whole request — all or nothing. List the first few records in items and count the rest in moreCount ("and 3 more"). Put it above a record's description, in an inbox detail sheet, or in a dialog; it fills its container.

Examples

Claude Code via Manoj, agentwants to move 2 tasks to Trash

“These duplicate #10 and #11.”

You can restore them from Trash for 30 days.

Requested Expires

Outcomes

approved, declined, expired, cancelled and already (answered on another device) replace the buttons with a line; statusLine swaps in your own words ("Approved by you · just now"). A viewer who canDecide={false} sees whose approval it waits for.

Claude Code via Manoj, agentwants to move 2 tasks to Trash

“These duplicate #10 and #11.”

You can restore them from Trash for 30 days.

Requested

Approved by you · just now

Claude Code via Manoj, agentwants to move 2 tasks to Trash

“These duplicate #10 and #11.”

You can restore them from Trash for 30 days.

Requested

Declined. Claude Code was told.

Claude Code via Manoj, agentwants to move 2 tasks to Trash

“These duplicate #10 and #11.”

You can restore them from Trash for 30 days.

Requested

Expired. Claude Code can ask again.

Claude Code via Manoj, agentwants to move 2 tasks to Trash

and 3 more

“These duplicate #10 and #11.”

You can restore them from Trash for 30 days.

Requested Expires

Waiting for Manoj's approval

Narrow

Under a 24rem container the buttons stack full width, Approve on top, and the times move under them.

Claude Code via Manoj, agentwants to move 2 tasks to Trash

You can restore them from Trash for 30 days.

Requested Expires

API Reference

PropTypeDefaultDescription
actor*Person—Who asks — an agent (kind: "agent", via its owner).
requestedAt*string | number | Date—When it was asked.
verb*React.ReactNode—What it wants to do, after its name: "wants to move 2 tasks to Trash".
canDecidebooleantrueThe viewer may decide — the agent's owner. Anyone else sees "Waiting for Manoj's approval".
expiresAtstring | number | Date | null—When the request lapses, shown while it waits: "Expires in 7 days".
itemsreadonly ApprovalCardItem[][]The records it touches; list the first few and count the rest in moreCount.
moreCountnumber0How many more records it touches than items lists: "and 3 more".
nownumber—Pin the relative times' clock (docs, tests).
onApprove(() => void | Promise<unknown>)—Approve the whole request; a promise keeps Approve busy.
onDecline(() => void | Promise<unknown>)—Decline the whole request; a promise keeps Decline busy.
reasonReact.ReactNode—The agent's own reason, quoted.
reassuranceReact.ReactNode—What makes the action safe to approve: "You can restore them from Trash for 30 days."
statusApprovalCardStatus"pending"Where it stands. pending shows Approve / Decline to whoever canDecide; approving keeps Approve busy; the rest show the outcome in place of the buttons.
statusLineReact.ReactNodethe status's own lineThe outcome line in place of the default for status ("Approved by you · just now").

Data attributes and CSS variables on ApprovalCard

AttributeValues
data-slot"approval-card" | "approval-card-actions" | "approval-card-footer" | "approval-card-header" | "approval-card-item" | "approval-card-items" | "approval-card-meta" | "approval-card-more" | "approval-card-reason" | "approval-card-reassurance" | "approval-card-status" | "approval-card-title" | "approval-card-verb"
data-statusmirrors a prop or state value

Accessibility

  • The card is a region named by its title: "Claude Code via Manoj, agent wants to move 2 tasks to Trash". Approve and Decline are described by the same title.
  • A decision made while the card is on screen is announced politely; the first paint is not.
  • Status is never colour alone: each outcome is a sentence with its icon.
KeyAction
TabMove through the record links and buttons.
Enter / SpaceApprove or decline.
ContractStates tested
Behaviourpending, approving, approved, declined, expired, cancelled, already, cannot-decide, busy, error
Accessibilitylabeled-region, described-buttons, announced-outcome, browser-accessibility-test
Visualpending, decided, narrow

Do / Don't

Do
Say the action in plain words with its effective scope, and add a reassurance only when it is true.
Don't
Ask per record, or use it for the viewer's own destructive action — that is an AlertDialog.

On this page