Upload Panel
The uploads card pinned bottom-end — files and folders in flight, progress, failures and destinations — with a done card and a phone sheet.
- Status
- Since
0.23.120- Accessibility pattern
- named region, labelled progress rings, milestone announcer
Last updated
- quarterly-site-survey-final-2026.pdf64% · 7.9 MB of 12 MBProduct › Specs
facade-east.jpg3.4 MBSite visit › Photos
- luminaire-LX200.iesFinishing…Product › Specs
- walkthrough.movWaiting · 240 MBSite visit › Photos
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 Upload Panel before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify upload-panelInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/upload-panelRun 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/alert-dialog, @vegastack/button, @vegastack/file-kind, @vegastack/image, @vegastack/progress, @vegastack/responsive-dialog, @vegastack/upload-progress, @vegastack/use-announcer, @vegastack/use-mobile.
Usage
import { UploadPanel } from "@/components/ui/upload-panel";
// Mount once in the app shell, so uploads survive navigation.
<UploadPanel
items={uploads.items}
summary={uploads.summary}
collapsed={collapsed}
onCollapsedChange={setCollapsed}
onCancel={uploads.cancel}
onRetry={uploads.retry}
onResume={uploads.chooseFileAgain}
onOpen={(href) => router.push(href)}
onCancelAll={uploads.cancelAll}
onRetryFailed={uploads.retryFailed}
onDismiss={uploads.clear}
/>;The panel is presentational and controlled: the host's upload engine owns the queue, the bytes and
the retries, and passes items (files, and folders with their files) and a summary (counts,
bytes, timeLeftMs and the batch status). The time left comes from createRateEstimator in the
upload-progress lib, which smooths acknowledged bytes and holds the estimate until it moves by
more than 15%; formatTimeLeft turns it into "About 2 minutes left".
import {
createRateEstimator,
formatTimeLeft,
formatBytesProgress,
} from "@/lib/upload-progress";
const estimator = createRateEstimator();
estimator.sample(bytesAcked, performance.now());
const timeLeftMs = estimator.timeLeftMs(
total - bytesAcked,
bytesAcked / total,
performance.now(),
);- quarterly-site-survey-final-2026.pdf64% · 7.9 MB of 12 MBProduct › Specs
facade-east.jpg3.4 MBSite visit › Photos
- luminaire-LX200.iesFinishing…Product › Specs
- walkthrough.movWaiting · 240 MBSite visit › Photos
Scope
| Behaviour | Where it lives |
|---|---|
| Rows, states, the done card, confirm, phone sheet | UploadPanel, UploadItem, UploadGroup |
| Speed, time left and bytes wording | upload-progress lib |
| Kind, icon, tint and label of each file | file-kind lib (FileTypeIcon) |
| Choosing files | UploadDialog, Dropzone |
| Uploading, resuming, retrying | Host — the engine calls back into its queue |
| Toasts clearing the panel | Toast reads --upload-panel-inset, which the panel publishes itself |
Anatomy
Examples
Collapsed
The chevron folds the list to the header, with one bar for the whole batch.
Folder
A folder upload is one row with its count and aggregate progress; its chevron expands the files. Cancel and Retry on the folder call back with the folder's id.
- Site photos5 of 12 uploaded · 45%Site visit › Photos
- visit-notes.docx220 KBSite visit › Photos
Errors
A failure keeps the panel open until the person acts: the reason under the name, Retry on the row and Retry failed below the list. An interrupted upload (the page reloaded mid-upload) asks for its file again with Choose file, and a cancelled one is muted.
- stair-core.dwg8.1 MBProduct › Specs
- render-final.psdToo large. The limit is 500 MB
- handover.zipInterruptedProduct › Specs
- old-draft.pptxCancelled
Done
When everything finishes cleanly the panel shrinks into a done card that calls onDismiss after
autoDismissMs (8 s), paused while it is hovered or focused. Its chevron reopens the list.
Phone
Below 768px the panel is a compact bar with the batch's progress; tapping it opens every row in a
bottom sheet. compact asks for the bar at any width.
File types
FileTypeIcon resolves every row's icon and tint from one table: MIME type first, the extension
when the type is missing or generic.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
items* | UploadEntry[] | — | The rows: files and folders, in the order to show them. An empty list renders nothing. |
summary* | UploadSummary | — | The batch totals and state. |
autoDismissMs | number | 8000 | How long the done card stays before it calls onDismiss, paused while hovered or focused;
0 keeps it until closed. |
collapsed | boolean | false | Whether the list is folded to the header (controlled; ignored by the compact bar, which opens the list in a sheet). |
compact | boolean | — | Show the compact bar that opens the list in a sheet (a dialog on a wide screen). Unset, it follows the viewport: the bar below 768px, the card from there up. |
labels | Partial<UploadPanelLabels> | — | Any wording to replace. |
maxRows | number | 200 | The most rows it renders; the rest are counted on a closing line. |
onCancel | ((id: string) => void) | — | Cancel a queued or uploading file, or a folder's remaining files, by id. Without it no cancel button shows. |
onCancelAll | (() => void) | — | Cancel everything still moving (the close confirm and the footer's Cancel all). |
onCollapsedChange | ((collapsed: boolean) => void) | — | Called when the person folds or unfolds the list. |
onDismiss | (() => void) | — | The panel asks to go: closed when idle, after a confirmed cancel, or by the done card's timer. |
onOpen | ((href: string) => void) | — | Open a destination in-app; without it a destination link navigates by its href. |
onResume | ((id: string) => void) | — | Resume an interrupted file by id: the host asks for the file again ("Choose file"). |
onRetry | ((id: string) => void) | — | Retry a failed file, or a folder's failed files, by id. Without it no Retry button shows. |
onRetryFailed | (() => void) | — | Retry every failed file (the footer's Retry failed). |
Data attributes and CSS variables on UploadPanel
| Attribute | Values |
|---|---|
data-slot | "upload-panel" | "upload-panel-footer" | "upload-panel-header" | "upload-panel-list" | "upload-panel-progress" |
data-state | "collapsed" | "done" | "expanded" |
data-status | mirrors a prop or state value |
UploadItem
| Prop | Type | Default | Description |
|---|---|---|---|
file* | UploadFile | — | The file the row shows. |
onCancel | ((id: string) => void) | — | Cancel a queued or uploading file, or a folder's remaining files, by id. Without it no cancel button shows. |
onOpen | ((href: string) => void) | — | Open a destination in-app; without it a destination link navigates by its href. |
onResume | ((id: string) => void) | — | Resume an interrupted file by id: the host asks for the file again ("Choose file"). |
onRetry | ((id: string) => void) | — | Retry a failed file, or a folder's failed files, by id. Without it no Retry button shows. |
Data attributes and CSS variables on UploadItem
| Attribute | Values |
|---|---|
data-slot | "upload-done-icon" | "upload-item" | "upload-item-state" |
data-status | mirrors a prop or state value |
UploadGroup
| Prop | Type | Default | Description |
|---|---|---|---|
folder* | UploadFolder | — | The folder the row shows. |
defaultExpanded | boolean | false | Whether its files show (uncontrolled). |
maxRows | number | 200 | The most file rows it renders when expanded. |
onCancel | ((id: string) => void) | — | Cancel a queued or uploading file, or a folder's remaining files, by id. Without it no cancel button shows. |
onOpen | ((href: string) => void) | — | Open a destination in-app; without it a destination link navigates by its href. |
onResume | ((id: string) => void) | — | Resume an interrupted file by id: the host asks for the file again ("Choose file"). |
onRetry | ((id: string) => void) | — | Retry a failed file, or a folder's failed files, by id. Without it no Retry button shows. |
Data attributes and CSS variables on UploadGroup
| Attribute | Values |
|---|---|
data-expanded | mirrors a prop or state value |
data-slot | "upload-group" | "upload-group-files" | "upload-item-state" |
data-status | mirrors a prop or state value |
Accessibility
- The panel is a region named "Uploads"; the list is a list, and each folder's files a nested list.
- Each uploading row has a progressbar named by its file, with its percentage.
- A polite live region announces milestones only — "Uploading 3 items", "3 uploads complete", "1 upload failed" — never each percent.
- Cancel, Retry and Choose file are buttons named with the file. The cancel button takes the ring's place on hover or focus, and is always shown on a touch screen.
- Closing while uploads are moving opens an alert dialog with focus on "Keep uploading".
- The success check and the card's arrival collapse to their end state under reduced motion.
| Key | Action |
|---|---|
| Tab | Move through the header, rows and footer. |
| Enter / Space | Collapse, close, cancel, retry or expand. |
| Esc | In the confirm or the sheet: close it. |
| Contract | States tested |
|---|---|
| Behaviour | uploading, collapsed, done-card, failed, interrupted, folder, confirm-close, phone-sheet |
| Accessibility | labeled, semantic-html, live-region, progressbar, browser-accessibility-test |
| Visual | uploading, collapsed, done, failed, folder, phone |