# Approval card

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/approval-card
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=607-4660

```tsx
import { ApprovalCard } from '@commenda/pathfinder/approval-card';

<ApprovalCard
  key={request.id}
  title="Send the documents to the accountant?"
  description="This email cannot be recalled."
  consequence="The accountant will receive these documents and entity contact details."
  approveConfig={{ label: 'Send documents', onApprove: approveRequest }}
  onDeny={denyRequest}
  onDismiss={() => temporarilyHide(request.id)}
/>;
```


## Usage [#usage]

```tsx
import { ApprovalCard } from '@commenda/pathfinder/approval-card';

<ApprovalCard
  key={request.id}
  title="Send the documents to the accountant?"
  description="This email cannot be recalled."
  consequence="The accountant will receive these documents and entity contact details."
  approveConfig={{ label: 'Send documents', onApprove: approveRequest }}
  onDeny={denyRequest}
  onDismiss={() => temporarilyHide(request.id)}
/>;
```

## Inline approval [#inline-approval]


```tsx
import { ApprovalCard } from '@commenda/pathfinder/approval-card';

<ApprovalCard
  placement="inline"
  key={request.id}
  title="Send the documents to the accountant?"
  description="This email cannot be recalled."
  consequence="The accountant will receive these documents and entity contact details."
  approveConfig={{ label: 'Send documents', onApprove: approveRequest }}
  onDeny={denyRequest}
  onDismiss={() => temporarilyHide(request.id)}
/>;
```


## Failure and retry [#failure-and-retry]


```tsx
// Return { status: 'failed', message: 'Please try again.' } from the handler to retain this card.
import { ApprovalCard } from '@commenda/pathfinder/approval-card';

<ApprovalCard
  key={request.id}
  title="Send the documents to the accountant?"
  description="This email cannot be recalled."
  consequence="The accountant will receive these documents and entity contact details."
  approveConfig={{ label: 'Send documents', onApprove: approveRequest }}
  onDeny={denyRequest}
  onDismiss={() => temporarilyHide(request.id)}
/>;
```


## Loading [#loading]


```tsx
import { ApprovalCard } from '@commenda/pathfinder/approval-card';

<ApprovalCard
  state={{ status: 'loading' }}
  key={request.id}
  title="Send the documents to the accountant?"
  description="This email cannot be recalled."
  consequence="The accountant will receive these documents and entity contact details."
  approveConfig={{ label: 'Send documents', onApprove: approveRequest }}
  onDeny={denyRequest}
  onDismiss={() => temporarilyHide(request.id)}
/>;
```


Reset restores the example. Simulate failure retains the card for retry; no backend request is made.

## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `consequence` | `string` | required | Required concrete effect of approval; explain reversibility in `description` |
| `approveConfig` | `{ label: string; onApprove: RequestHandler<{ alwaysAllow: boolean; }>; destructive?: boolean; }` | required | Explicit verb, handler receiving `{ alwaysAllow: boolean }`, optional destructive styling |
| `onDeny` | `RequestHandler` | required | Separate denial callback; returns accepted receipt or failure |
| `denyLabel` | `string` | optional | Defaults to Deny |
| `detailsConfig` | `{ label?: string; onOpen: () => void; }` | optional | Optional host-owned detail view |
| `alwaysAllowConfig` | `{ label: string; value: boolean; onValueChange: (value: boolean) => void; }` | optional | Explicit controlled permission, omitted unless supported by the host |
| `title` | `string` | required |  |
| `description` | `string` | optional | Supporting guidance associated with the field. |
| `placement` | `"docked" \| "inline"` | optional |  |
| `state` | `RequestState` | optional |  |
| `onDismiss` | `(() => void)` | optional |  |
| `className` | `string` | optional | Additional classes for the component root. |

### approveConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | Yes |  |
| `onApprove` | `RequestHandler<{ alwaysAllow: boolean; }>` | Yes |  |
| `destructive` | `boolean` | No |  |

### detailsConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | No |  |
| `onOpen` | `() => void` | Yes |  |

### alwaysAllowConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | Yes |  |
| `value` | `boolean` | Yes |  |
| `onValueChange` | `(value: boolean) => void` | Yes |  |

### state fields

Choose one shape; excluded fields cannot be combined.

#### Option 1

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | `"ready" \| "pending" \| "loading"` | Yes |  |

#### Option 2

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | `"resolved"` | Yes |  |
| `recap` | `ResponseRecapProps` | Yes |  |

### Shared request props [#shared-request-props]

| Prop          | Type                   | Behavior                                                                 |
| ------------- | ---------------------- | ------------------------------------------------------------------------ |
| `title`       | `string`               | Required card title.                                                     |
| `description` | `string`               | Optional supporting text.                                                |
| `placement`   | `'docked' \| 'inline'` | Docked by default; parent controls width and placement                   |
| `state`       | `RequestState`         | Ready, loading skeleton, externally pending, or resolved with a recap    |
| `onDismiss`   | `() => void`           | Temporary dismissal via Not now or scoped Escape; not a submitted answer |
| `className`   | `string`               | Layout override                                                          |

`RequestHandler<T>` returns `{ status: 'accepted', recap: ResponseRecapProps }` or `{ status: 'failed', message: string }`, directly or as a Promise. A thrown error shows a retry message and preserves the draft. The host persists accepted responses; the component displays the returned receipt. `state={{ status: 'resolved', recap }}` restores history. Use a stable request ID as the React key, and change the key when the request changes. External pending is a rendering state; authorization and idempotency remain server responsibilities.

**REQUEST-01:** Docked tray uses pearl100, a raised shadow, radius16 and a white radius10 inset with 16px padding. Header40, footer at least44; controls wrap on narrow screens.

**REQUEST-02:** The parent places the request 12px above Composer at the same width. Inline requests are flat and fill their message column. Escape only temporarily dismisses; no global listener is installed.

**REQUEST-03:** Reading content does not animate. Existing Button and Popover motion respects reduced motion. Pending retains the draft and exposes a polite status. Acceptance never precedes the host result.

## Design rules [#design-rules]

- **APPROVAL-01:** Show the action, its consequence, and reversibility before requesting consent. No numeric/global approval shortcut.
- **APPROVAL-02:** Deny and approval are distinct explicit outcomes. Escape and Not now temporarily dismiss; they never deny or approve.
- **APPROVAL-03:** Always allow starts from the host's actual permission state. Accepted approval and denial receipts use `kind: 'approval'` with actor and timestamp. The server owns authorization and must validate the request again before executing it.

A typed composer reply while approval is active follows the Figma denial path. Implement that mapping in the application orchestrator, not as an arbitrary keyboard handler on ApprovalCard. Do not translate temporary dismissal, network failure, or a closed browser tab into consent.

## Integration and accessibility [#integration-and-accessibility]

Buttons have explicit names and keyboard focus. No network transport or AI SDK provider is required. Requests, tool execution, permission checks, durable outcomes and composer coordination stay in the application. See [AI content integration](/docs/guides/ai-content) for migration mappings; application migration is outside this build.
