# Upload request card

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

```tsx
import { UploadRequestCard } from '@commenda/pathfinder/upload-request-card';

<UploadRequestCard
  key={request.id}
  title="Upload the signed documents"
  uploadConfig={{ accept: '.pdf', onFilesSelected: validateAndUpload }}
  attachmentConfig={{
    items: attachments,
    onRemove: removeAttachment,
    onRetry: retryUpload,
  }}
  onSubmit={submitDocumentIds}
/>;
```


## Usage [#usage]

```tsx
import { UploadRequestCard } from '@commenda/pathfinder/upload-request-card';

<UploadRequestCard
  key={request.id}
  title="Upload the signed documents"
  uploadConfig={{ accept: '.pdf', onFilesSelected: validateAndUpload }}
  attachmentConfig={{
    items: attachments,
    onRemove: removeAttachment,
    onRetry: retryUpload,
  }}
  onSubmit={submitDocumentIds}
/>;
```

## Share an upload link [#share-an-upload-link]


```tsx
import { UploadRequestCard } from '@commenda/pathfinder/upload-request-card';

<UploadRequestCard
  linkConfig={{ url: uploadUrl }}
  key={request.id}
  title="Upload the signed documents"
  uploadConfig={{ accept: '.pdf', onFilesSelected: validateAndUpload }}
  attachmentConfig={{
    items: attachments,
    onRemove: removeAttachment,
    onRetry: retryUpload,
  }}
  onSubmit={submitDocumentIds}
/>;
```


## Inline request [#inline-request]


```tsx
import { UploadRequestCard } from '@commenda/pathfinder/upload-request-card';

<UploadRequestCard
  placement="inline"
  key={request.id}
  title="Upload the signed documents"
  uploadConfig={{ accept: '.pdf', onFilesSelected: validateAndUpload }}
  attachmentConfig={{
    items: attachments,
    onRemove: removeAttachment,
    onRetry: retryUpload,
  }}
  onSubmit={submitDocumentIds}
/>;
```


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


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

<UploadRequestCard
  key={request.id}
  title="Upload the signed documents"
  uploadConfig={{ accept: '.pdf', onFilesSelected: validateAndUpload }}
  attachmentConfig={{
    items: attachments,
    onRemove: removeAttachment,
    onRetry: retryUpload,
  }}
  onSubmit={submitDocumentIds}
/>;
```


## Loading [#loading]


```tsx
import { UploadRequestCard } from '@commenda/pathfinder/upload-request-card';

<UploadRequestCard
  state={{ status: 'loading' }}
  key={request.id}
  title="Upload the signed documents"
  uploadConfig={{ accept: '.pdf', onFilesSelected: validateAndUpload }}
  attachmentConfig={{
    items: attachments,
    onRemove: removeAttachment,
    onRetry: retryUpload,
  }}
  onSubmit={submitDocumentIds}
/>;
```


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

## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `uploadConfig` | `RequestUploadConfig` | required | Native picker/drop events; default multiple true; callback receives `readonly File[]` |
| `attachmentConfig` | `Omit<AttachmentGroupProps, "className" \| "align">` | required | Stable uploaded file IDs and ready/uploading/failed state; remove, preview, retry callbacks |
| `onSubmit` | `RequestHandler<readonly string[]>` | required | Sends attachment IDs only, after all files are ready |
| `onSkip` | `RequestHandler` | optional | Optional explicit skip |
| `linkConfig` | `{ url: string; description?: string; }` | optional | Optional app-issued upload link and copy control |
| `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. |

### uploadConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `accept` | `string` | No | Browser hint only. Validate types, size, permissions and scan files on the server. |
| `multiple` | `boolean` | No |  |
| `description` | `string` | No |  |
| `onFilesSelected` | `(files: readonly File[]) => void` | Yes |  |

### attachmentConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | `readonly AttachmentItem[]` | Yes |  |
| `maxRows` | `1 \| 2 \| 3` | No |  |
| `getPreview` | `((item: AttachmentItem) => AttachmentProps["previewConfig"])` | No |  |
| `label` | `string` | No |  |
| `onPreview` | `((item: AttachmentItem) => void)` | No |  |
| `onRetry` | `((item: AttachmentItem) => void)` | No |  |
| `onRemove` | `((item: AttachmentItem) => void)` | No |  |

### linkConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | `string` | Yes |  |
| `description` | `string` | No |  |

### 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]

- **UPLOAD-REQUEST-01:** The dropzone is at least 120px high, radius8, dashed pearl400. Existing AttachmentGroup renders files and overflow.
- **UPLOAD-REQUEST-02:** Send documents requires at least one file and every upload ready. Failed, uploading, and folder items block submission. Removing/retrying is blocked while submitting.
- **UPLOAD-REQUEST-03:** Selecting or dropping files never resolves the request. The host validates size/type/count, uploads, handles retries and cancellation, and returns stable IDs. `accept` is a browser hint, not validation; validate on the server too.

Pathfinder currently has no standalone FileUpload, so this phase adds a private native picker/dropzone rather than importing a private Kakashi runtime dependency. The demo keeps files in memory and makes no network request. Production code must mark them uploading until the server accepts them.

## 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.
