# Changelog (/docs/changelog) The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## \[Unreleased] [#unreleased] ### Added [#added] * Core components: `Dropzone` and `FileCard` with granular subcomponents for composition. * Built-in patterns: `trigger-only`, `documents-only-queue`, `avatar-upload`, `file-grid`. * Integration adapters for `Supabase`, `AWS S3`, and `Cloudinary`. * Full documentation site built with Fumadocs, including live examples and registry installation instructions. # AWS S3 (/docs/adapters/aws-s3)
AWS S3
## Introduction [#introduction] The AWS S3 adapter enables seamless integration with Amazon S3, providing enterprise-grade file storage with high availability and durability. Full documentation for AWS S3 integration is being written. Check back soon! ## Quick Start [#quick-start] ```bash pnpm add @aws-sdk/client-s3 ``` ```tsx import { S3Client } from "@aws-sdk/client-s3" const s3Client = new S3Client({ region: process.env.AWS_REGION, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, }, }) ``` # Cloudinary (/docs/adapters/cloudinary)
Cloudinary Media
## Introduction [#introduction] The Cloudinary adapter provides powerful image and video transformation capabilities with automatic optimization and CDN delivery. Full documentation for Cloudinary integration is being written. Check back soon! ## Quick Start [#quick-start] ```bash pnpm add cloudinary ``` ```tsx import { v2 as cloudinary } from 'cloudinary' cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET, }) ``` # Supabase Storage (/docs/adapters/supabase)
Supabase Storage
## Introduction [#introduction] The Supabase adapter provides first-class integration with Supabase Storage, making it easy to upload, manage, and serve files directly from your Supabase project. ## Installation [#installation] ```bash pnpm add @supabase/supabase-js ``` ## Setup [#setup] ### 1. Create Supabase Client [#1-create-supabase-client] ```tsx // lib/supabase.ts import { createClient } from '@supabase/supabase-js' export const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) ``` ### 2. Create Storage Bucket [#2-create-storage-bucket] In your Supabase dashboard: 1. Go to **Storage** 2. Create a new bucket (e.g., `uploads`) 3. Set the bucket policy (public or private) Make sure to configure Row Level Security (RLS) policies for your storage bucket to control access. ## Usage [#usage] ### Basic Upload [#basic-upload] ```tsx import { Dropzone } from "@/components/ui/dropzone" import { supabase } from "@/lib/supabase" import { useState } from "react" export function SupabaseUpload() { const [uploading, setUploading] = useState(false) const handleUpload = async (files: File[]) => { setUploading(true) for (const file of files) { const filePath = `${Date.now()}-${file.name}` const { data, error } = await supabase.storage .from('uploads') .upload(filePath, file) if (error) { console.error('Upload error:', error) } else { console.log('Uploaded:', data.path) } } setUploading(false) } return ( {uploading ? 'Uploading...' : 'Drop files to upload'} ) } ``` ### Get Public URL [#get-public-url] ```tsx const { data } = supabase.storage .from('uploads') .getPublicUrl('file-path.jpg') console.log(data.publicUrl) ``` ### Download File [#download-file] ```tsx const { data, error } = await supabase.storage .from('uploads') .download('file-path.jpg') ``` ### Delete File [#delete-file] ```tsx const { error } = await supabase.storage .from('uploads') .remove(['file-path.jpg']) ``` ## Advanced [#advanced] ### Upload with Progress [#upload-with-progress] ```tsx import { UploadProgress } from "@/components/ui/upload-progress" const { data, error } = await supabase.storage .from('uploads') .upload(filePath, file, { cacheControl: '3600', upsert: false, onUploadProgress: (progress) => { const percent = (progress.loaded / progress.total) * 100 console.log(`Upload ${percent}% complete`) } }) ``` ### Image Transformations [#image-transformations] Supabase supports on-the-fly image transformations: ```tsx const { data } = supabase.storage .from('uploads') .getPublicUrl('image.jpg', { transform: { width: 500, height: 500, resize: 'cover', quality: 80 } }) ``` Supabase Storage uses Cloudflare CDN for fast, global file delivery. ## Environment Variables [#environment-variables] Add these to your `.env.local`: ```bash NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key ``` ## Related [#related] * [AWS S3](/docs/adapters/aws-s3) — Amazon S3 integration * [Cloudinary](/docs/adapters/cloudinary) — Cloudinary integration * [Dropzone](/docs/components/dropzone) — Upload component # AttachmentChip (/docs/display/attachment-chip) This component is coming soon. `AttachmentChip` is a small, static inline pill for displaying a file that has already been attached or uploaded — not for showing upload progress. Think of the attachment row at the bottom of a Gmail message, or a chip in a comment box showing a selected file. Unlike `FileCard`, `AttachmentChip` has no upload state, no progress bar, and no retry action. It is purely a display component. ## Planned API [#planned-api] ```tsx handleRemove(id)} /> ``` | Prop | Type | Description | | ---------- | ------------ | -------------------------------------------------------------------- | | `name` | `string` | The filename to display. | | `size` | `number` | File size in bytes — formatted automatically. | | `onRemove` | `() => void` | Called when the ✕ button is clicked. Omit to hide the remove button. | | `icon` | `ReactNode` | Override the auto-detected file type icon. | # FileCard (/docs/display/file-card) `FileCard` pairs with [`Dropzone`](/docs/uploads/dropzone) — map it over `dropzone.fileStatuses` to build a queue. There is no separate list component to install; a plain `.map()` is the queue. ## Dependencies [#dependencies] * **`lucide-react`** — Icons for status, retry, and remove actions. * **`@/components/shelf-ui/dropzone`** — `FileCard` reads the `FileStatus` and `UploadStatus` types from the Dropzone component. ## Installation [#installation] ```bash title="terminal" npx shadcn@latest add @shelf-ui/file-card ``` ```bash title="terminal" pnpm dlx shadcn@latest add @shelf-ui/file-card ``` ```bash title="terminal" yarn dlx shadcn@latest add @shelf-ui/file-card ``` ```bash title="terminal" bunx shadcn@latest add @shelf-ui/file-card ``` ## Usage [#usage] `FileCard` is a compound component — the root provides context, and you choose which pieces to render and in what order. Map it over `dropzone.fileStatuses` to render the queue; there's no separate list component because the loop itself is the entire "queue." For large queues (dozens of files re-rendering frequently), wrap `onRemove`/`onRetry` in `useCallback` in the parent. `FileCard`'s internal context is memoized, but inline arrow functions created fresh on every render still defeat that memoization. See [Performance](#performance). ## Composition [#composition] ```text FileCard (root + context provider) ├── FileCardPreview (thumbnail or type/status icon) ├── FileCardInfo (classic layout: filename + size/error) │ ├── FileCardFileName (standalone filename) │ └── FileCardFileSize (standalone formatted size) ├── FileCardProgress (progress bar — pending / success / error) ├── FileCardActions (retry/remove button group) │ ├── FileCardCopyButton (copies result URL on success) │ └── FileCardDownloadButton (download link on success) ├── FileCardStatusText (inline status: "Uploading / Completed / Failed") ├── FileCardTypeIcon (standalone file type icon: PDF, Video, Image, etc) ├── FileCardStatusIcon (standalone status icon: Loader, Check, Alert) ├── FileCardRetryButton (standalone retry button) └── FileCardRemoveButton (standalone remove button) ``` Every subcomponent is optional and order is up to you. Omit `FileCardProgress` for a compact row, drop `FileCardPreview` if thumbnails aren't useful for your file types, or use the granular components (`FileCardFileName`, `FileCardFileSize`, etc.) to build entirely custom layouts. ### With a progress bar [#with-a-progress-bar] The default queue row pairs `FileCardPreview`, `FileCardInfo`, `FileCardProgress`, and `FileCardActions`. ```tsx
``` ### Compact, no progress bar [#compact-no-progress-bar] A slimmer layout suitable for sidebars or tight spaces. This layout relies on granular subcomponents like `FileCardFileName` and `FileCardStatusText` to fit more metadata inline without needing multiple rows. ### Image thumbnails [#image-thumbnails] `FileCardPreview` shows a generic status icon by default. Pass `showImageThumbnail` to render an actual image preview for image files. The underlying object URL is created and revoked automatically — no manual cleanup needed. ```tsx
``` ### Custom preview (file-type icons, video posters, etc.) [#custom-preview-file-type-icons-video-posters-etc] For file types beyond images — PDFs, videos, archives — pass `renderPreview` to take full control of what's shown. Return `null` or `undefined` to fall back to `showImageThumbnail` or the default status icon for that file. ```tsx { if (file.type === "application/pdf") return ; if (file.type.startsWith("video/")) return ; return null; }} /> ``` Use this recipe when one queue mixes many file types. If your queue is documents-only, the [Patterns page](/docs/display/patterns#documents-only-queue-no-thumbnails) shows the lighter option: omit `FileCardPreview` entirely. ### Extra actions [#extra-actions] `FileCardActions` accepts children, rendered after the built-in retry/remove buttons — useful for a "view file" link or "copy URL" button once a file has uploaded successfully. ```tsx {status.status === "success" && ( )} ``` ### Standalone action buttons [#standalone-action-buttons] If you need total control over the action layout, use `FileCardRetryButton` and `FileCardRemoveButton` directly anywhere inside `FileCard`. They read `onRetry` and `onRemove` from context and accept the same props as Shelf UI's `Button`. ```tsx
{status.status === "error" && }
``` ## API Reference [#api-reference] ### `FileCard` [#filecard] | Prop | Type | Required | Description | | ------------ | ------------ | -------- | ------------------------------------------------------------------------------------------------------- | | `fileStatus` | `FileStatus` | Yes | A single entry from `dropzone.fileStatuses`. | | `onRemove` | `() => void` | No | Called when the remove button is clicked. Omit to hide the remove action entirely. | | `onRetry` | `() => void` | No | Called when the retry button is clicked. Only shown when `status === "error"` and `canRetry` is `true`. | | `canRetry` | `boolean` | No | Pass `dropzone.canRetry(status.id)`. Controls whether the retry button renders. | ### `FileCardPreview` [#filecardpreview] | Prop | Type | Default | Description | | -------------------- | ------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ | | `showImageThumbnail` | `boolean` | `false` | Render an `` thumbnail for image files instead of the default status icon. Object URL lifecycle is managed automatically. | | `renderPreview` | `(file: File, status: UploadStatus) => ReactNode` | — | Full control over the preview content. Return `null`/`undefined` to use the built-in fallback for that file. | ### `FileCardInfo` [#filecardinfo] | Prop | Type | Default | Description | | --------------- | -------- | ------- | ----------------------------------------------------------------------------------- | | `maxNameLength` | `number` | `28` | Characters before the filename is truncated with an ellipsis (extension preserved). | Shows the formatted file size by default, or the error message when `status === "error"`. ### `FileCardProgress` [#filecardprogress] No props beyond standard `div` attributes. Reads `status` from context — renders an indeterminate animated bar while `"pending"`, a filled bar on `"success"`, and a destructive-colored bar on `"error"`. ### `FileCardFileName` [#filecardfilename] | Prop | Type | Default | Description | | ----------- | -------- | ------- | ----------------------------------------------------------------------------------- | | `maxLength` | `number` | `28` | Characters before the filename is truncated with an ellipsis (extension preserved). | ### `FileCardFileSize` [#filecardfilesize] | Prop | Type | Default | Description | | ---- | ---- | ------- | -------------------------------------------------------------------------------- | | — | — | — | Standalone formatted file size text. No props beyond standard `span` attributes. | ### `FileCardStatusText` [#filecardstatustext] | Prop | Type | Default | Description | | ---- | ---- | ------- | ------------------------------------------------------------------------------------------------------------------- | | — | — | — | Inline status text ("Uploading...", "Completed", or the error message). No props beyond standard `span` attributes. | ### `FileCardTypeIcon` / `FileCardStatusIcon` [#filecardtypeicon--filecardstatusicon] | Component | Description | | -------------------- | ----------------------------------------------------------------------------------- | | `FileCardTypeIcon` | Standalone file type icon (PDF, Video, Image, Archive, etc) based on the file type. | | `FileCardStatusIcon` | Standalone status icon (Loader, Check, Alert) based on the upload status. | ### `FileCardCopyButton` / `FileCardDownloadButton` [#filecardcopybutton--filecarddownloadbutton] | Component | Description | | ------------------------ | --------------------------------------------------------------------------------- | | `FileCardCopyButton` | Copies the URL to the clipboard. Automatically hides if `status.result` is empty. | | `FileCardDownloadButton` | Provides a direct download link. Automatically hides if `status.result` is empty. | ### `FileCardActions` [#filecardactions] | Prop | Type | Default | Description | | ------------ | ----------- | ------- | ------------------------------------------------------ | | `hideRetry` | `boolean` | `false` | Hide the retry button even when the file is retryable. | | `hideRemove` | `boolean` | `false` | Hide the remove button. | | `children` | `ReactNode` | — | Extra actions rendered after retry/remove. | ### `FileCardRetryButton` / `FileCardRemoveButton` [#filecardretrybutton--filecardremovebutton] | Prop | Type | Default | Description | | ---------- | ------------- | ------- | ------------------------------------------------------------------------------------------------------------ | | `...props` | `ButtonProps` | — | Standalone action buttons for custom layouts. Both accept the same props as the Shelf UI `Button` component. | ### Utilities [#utilities] | Export | Description | | -------------------- | ----------------------------------------------------------------------------------------------------- | | `useFileCardContext` | Reads the current FileCard context for custom subcomponents. | | `useFileObjectUrl` | Creates and revokes an object URL for a `File`; useful for custom previews outside `FileCardPreview`. | ## Accessibility [#accessibility] * Retry and remove buttons have `aria-label`s ("Retry upload", "Remove file") since they're icon-only. * `FileCardProgress` uses `role="progressbar"` with `aria-valuetext` set to `"indeterminate"`, `"100%"`, or `"error"` depending on status. * Error messages in `FileCardInfo` are plain text, so they're picked up by the same ARIA live region wiring as `DropzoneMessage` when both are on the page. ## Performance [#performance] * **Object URLs are handled internally.** `FileCardPreview` uses `useFileObjectUrl` to create the URL via `useEffect` and revoke it automatically on unmount or whenever the `file` reference changes. You don't need to call `URL.createObjectURL` or `URL.revokeObjectURL` yourself for `showImageThumbnail`. * **The URL is only created when it's actually needed** — if `showImageThumbnail` is off, or the file isn't an image, or `renderPreview` returns custom content, no object URL is created at all. * **`FileCardInfo`'s derived strings are memoized** against `file.name`/`file.size`, so re-renders triggered by unrelated state (e.g. a sibling row's status changing) don't re-run the truncation or byte-formatting logic. * **Context memoization has a limit.** `FileCard`'s context value is wrapped in `useMemo`, but that only prevents re-renders if `onRemove`/`onRetry` are stable references. If you pass `() => dropzone.onRemove(status.id)` inline inside a `.map()`, that's a new function every render. For small queues this is invisible; for large or frequently-updating queues, wrap the dropzone's callbacks once with `useCallback` (or curry an id-bound function with `useMemo`) in the parent component. * **Large lists** — if you're rendering hundreds of files, consider a virtualization library (e.g. `react-window`) around the `.map()`, the same guidance as in the Dropzone docs. # Patterns (/docs/display/patterns) This page is recipes, not components. Everything below is built from [`Dropzone`](/docs/uploads/dropzone) and [`FileCard`](/docs/display/file-card) — nothing new to install. ## Trigger-only (no drag-and-drop surface) [#trigger-only-no-drag-and-drop-surface] `DropzoneTrigger` already renders the hidden `` and owns the click handling, so it doesn't need to live inside a `DropZoneArea`. Skip the drop target entirely for a plain "Choose file" button, useful in compact forms or table cells where a large drag area doesn't fit. This is the closest thing to a standalone "file input" component in Shelf UI — rather than a separate primitive, it's `DropzoneTrigger` used on its own. ## Avatar / single-file replace [#avatar--single-file-replace] Use `maxFiles: 1` plus `shiftOnMaxFiles: true` when a new file should replace the old one automatically. The trigger can render the live preview, so the upload control and the current value are the same element. ## Empty and error states [#empty-and-error-states] `fileStatuses` is just an array — when it's empty, nothing renders by default. Most upload UIs benefit from an explicit empty state instead of a blank gap, and from making `isInvalid`/`rootError` visible at a glance rather than relying solely on `DropzoneMessage` text. `DropzoneMessage` already renders `rootError` automatically — the empty-state block above is additive, not a replacement. Use it when you want the queue area itself to communicate state, not just the drop zone. ## Documents-only queue (no thumbnails) [#documents-only-queue-no-thumbnails] When every file is the same general type (PDFs, contracts, resumes), thumbnails and progress bars can add visual noise without adding information. Compose `FileCard` with only `FileCardInfo` and `FileCardActions` for a denser list. ## Batch upload queue [#batch-upload-queue] Use this when the user expects to upload several independent files, see partial progress, clear the queue, and retry only failed items. ## Image gallery [#image-gallery] For image-heavy workflows, skip row-based cards and show a grid where thumbnails are the primary information. This pattern keeps object URLs stable and revokes them when files are removed. ## Choosing a pattern [#choosing-a-pattern] A quick way to decide which composition fits: * **Need drag-and-drop and multiple files** — use the default `Dropzone` + `DropZoneArea` composition from the [Usage](/docs/uploads/dropzone#usage) example. * **Need a single file, no drag-and-drop, fits in a tight space** — Trigger-only, above. * **Need exactly one file that replaces itself (avatar, logo, cover image)** — Avatar pattern, above. * **Need a gallery of images** — Image gallery, above. * **Need a document checklist with no visual fluff** — Documents-only queue, above. * **Need retry and clear controls for a batch** — Batch upload queue, above. None of these require new component code — they're all `useDropzone` configuration plus a choice of which `FileCard` subcomponents to render. # Introduction (/docs/getting-started) Shelf UI is an open-source component library for building file-related interfaces in React. It gives you composable, copy-paste components designed for the complete file experience — upload, preview, manage, and navigate — built on React 19, Tailwind CSS v4, and Radix UI. Think of it as **shadcn/ui, purpose-built for file UIs**. ## Why Shelf UI? [#why-shelf-ui] No other library treats file management as a complete UI domain. Shelf UI owns the entire experience — from the dropzone to the file tree. Components are not installed as a dependency. You copy them into your project and own them entirely. Edit, extend, delete — no abstraction in the way. First-class Supabase Storage, AWS S3, and Cloudinary support. Drop in your config and uploads just work. Every component ships with its underlying hook. Use the UI layer or go fully headless — your choice. Built on the [shadcn open registry spec](https://ui.shadcn.com/docs/registry). Install any component with a single CLI command. Peer dependencies resolved automatically. ## How it works [#how-it-works] Shelf UI is built on the [shadcn registry](https://ui.shadcn.com/docs/registry). Components are not installed as a package — you copy them into your project and own them entirely. ```bash title="terminal" npx shadcn@latest add @shelf-ui/dropzone ``` ```bash title="terminal" pnpm dlx shadcn@latest add @shelf-ui/dropzone ``` ```bash title="terminal" yarn dlx shadcn@latest add @shelf-ui/dropzone ``` ```bash title="terminal" bunx shadcn@latest add @shelf-ui/dropzone ``` This copies the component source into your `components/shelf-ui` directory. Customize it however you want. ## Components [#components] | Component | Category | Description | Status | | -------------------------------------------------- | ---------- | ------------------------------------------------------------------- | --------- | | [Dropzone](/docs/uploads/dropzone) | Upload | Drag and drop file upload area with accept filtering and validation | Available | | [FileCard](/docs/display/file-card) | Display | Smart preview card per file type — image, PDF, video, archive | Available | | [AttachmentChip](/docs/display/attachment-chip) | Display | Inline pill with icon, filename, and remove | Soon | | [FileTree](/docs/management/file-tree) | Management | Recursive folder/file browser with expand/collapse | Soon | | [FileBreadcrumb](/docs/management/file-breadcrumb) | Management | Path navigation bar | Soon | *Note: For complex layouts like Avatar Uploads, Queues, and Grids, we use **Patterns** combining the core components. See the [Patterns guide](/docs/display/patterns) for examples.* More components are being actively developed. Follow the repo for updates. ## FAQ [#faq] No global install required. Each component installs its own peer dependencies (Radix UI, Tailwind, Lucide) via the shadcn CLI. Run one command per component. Yes. Shelf UI is a separate registry that integrates with your existing `components.json`. Add the registry alias once and all Shelf UI components install with the same CLI you already use. Yes — that's the point. Install only what you need. Components are independent Yes. Shelf UI components work in any React 19+ project — Vite, Remix, TanStack Start. The shadcn CLI handles the file copying regardless of your framework. Yes. Components built on Radix UI primitives are WAI-ARIA compliant out of the box. All interactive components support full keyboard navigation. # Installation (/docs/getting-started/installation) Shelf UI components are designed for React projects using Tailwind CSS v4 and TypeScript. Follow the steps below to get set up. ## Prerequisites [#prerequisites] Make sure your project has the following: * [React 19](https://react.dev) or later * [Tailwind CSS v4](https://tailwindcss.com) * [TypeScript](https://typescriptlang.org) Shelf UI also depends on shared utilities from [shadcn/ui](https://ui.shadcn.com). If you don't have shadcn set up yet, initialize it first: ```bash title="terminal" npx shadcn@latest init ``` ```bash title="terminal" pnpm dlx shadcn@latest init ``` ```bash title="terminal" yarn dlx shadcn@latest init ``` ```bash title="terminal" bunx shadcn@latest init ``` ## Add registry [#add-registry] Add the Shelf UI registry to your `components.json`: ```json title="components.json" { "registries": { "@shelf-ui": "https://shelf-ui.vercel.app/r/{name}.json" } } ``` ## Add components [#add-components] ### Option 1: shadcn CLI (recommended) [#option-1-shadcn-cli-recommended] ```bash title="terminal" npx shadcn@latest add @shelf-ui/dropzone ``` ```bash title="terminal" pnpm dlx shadcn@latest add @shelf-ui/dropzone ``` ```bash title="terminal" yarn dlx shadcn@latest add @shelf-ui/dropzone ``` ```bash title="terminal" bunx shadcn@latest add @shelf-ui/dropzone ``` ### Option 2: Direct URL [#option-2-direct-url] Add components by passing the registry URL directly: ```bash title="terminal" npx shadcn@latest add https://shelf-ui.vercel.app/r/dropzone.json ``` ```bash title="terminal" pnpm dlx shadcn@latest add https://shelf-ui.vercel.app/r/dropzone.json ``` ```bash title="terminal" yarn dlx shadcn@latest add https://shelf-ui.vercel.app/r/dropzone.json ``` ```bash title="terminal" bunx shadcn@latest add https://shelf-ui.vercel.app/r/dropzone.json ``` ### Option 3: Manual installation [#option-3-manual-installation] Copy the component source directly from its docs page into your project and install the required dependencies listed on that page. Each component page includes the full source and a list of peer dependencies required. The shadcn CLI handles this automatically in Options 1 and 2. ## Project structure [#project-structure] After adding components, your project will look like this: ```bash title="project structure" components/ ├── shelf-ui/ │ └── dropzone.tsx # Shelf UI component ├── ui/ │ ├── button.tsx # shadcn/ui primitive │ └── ... hooks/ │ └── use-file-upload.ts # Shelf UI hook └── ... ``` Components in `shelf-ui/` are Shelf UI primitives. Components in `ui/` are shared shadcn/ui primitives that Shelf UI components may depend on. ## Usage [#usage] Import and use components directly: ```tsx title="app/page.tsx" import { Dropzone } from "@/components/shelf-ui/dropzone"; import { FileCard, FileCardActions, FileCardIcon, FileCardInfo, } from "@/components/shelf-ui/file-card"; export default function Page() { return ( <> {/* Simple component — use props */} console.log(files)} accept={["image/*", ".pdf"]} maxSize={10 * 1024 * 1024} /> {/* Compound component — compose sub-components */} ); } ``` See individual component pages for detailed usage, props, and examples. # LLMs (/docs/getting-started/llms) ## LLM resources [#llm-resources] Machine-readable documentation for LLMs: | Resource | URL | | ------------- | ------------------------------------------- | | llms.txt | `https://shelf-ui.vercel.app/llms.txt` | | llms-full.txt | `https://shelf-ui.vercel.app/llms-full.txt` | Shelf UI publishes machine-readable documentation endpoints following the [llms.txt convention](https://llmstxt.org). These let AI tools index our full documentation so they understand component APIs, install patterns, and composition rules — without hallucinating. ## Endpoints [#endpoints] | Endpoint | Description | | ------------------------------------------------------------- | -------------------------------------------------------------------------- | | [`/llms.txt`](https://shelf-ui.vercel.app/llms.txt) | Compact index — component list, install commands, links to full docs | | [`/llms-full.txt`](https://shelf-ui.vercel.app/llms-full.txt) | Full documentation in a single file — complete prop reference and examples | Both are plain text, no authentication required, updated automatically on every docs deploy. ## Use in your AI assistant [#use-in-your-ai-assistant] ### Cursor [#cursor] Add the full docs as an indexed knowledge source: 1. Open **Cursor Settings** → **Features** → **Docs** 2. Click **Add new doc** 3. Enter `https://shelf-ui.vercel.app/llms-full.txt` Use `@shelf-ui` in any prompt to reference it directly. ### Claude [#claude] In any Claude project, add Shelf UI to the project knowledge: 1. Open your Claude project 2. Click **Add content** → **URL** 3. Enter `https://shelf-ui.vercel.app/llms.txt` Claude will index the component list and reference it in your conversations. For deeper context, use `/llms-full.txt` instead — it contains the complete prop documentation. ### Windsurf [#windsurf] Add to Cascade's knowledge base: 1. Open **Windsurf Settings** → **Cascade** → **Knowledge** 2. Add source URL: `https://shelf-ui.vercel.app/llms-full.txt` Cascade will index it and apply it automatically to relevant conversations. ### GitHub Copilot [#github-copilot] Paste the URL directly in a Perplexity prompt: ```text Read https://shelf-ui.vercel.app/llms.txt and then answer: how do I add a file upload component to my Next.js app? ``` Perplexity will fetch the docs inline and answer with accurate Shelf UI information. ## llms.txt format [#llmstxt-format] The `/llms.txt` file follows the [llmstxt.org spec](https://llmstxt.org): ```text # Shelf UI > A shadcn-style file UI component library. Copy-paste components for the complete file experience. ## Getting Started - [Introduction](https://shelf-ui.vercel.app/docs): What Shelf UI is and why it exists - [Installation](https://shelf-ui.vercel.app/docs/installation): How to add components to your project ## Components - [Dropzone](https://shelf-ui.vercel.app/docs/components/dropzone): Drag and drop file upload area - [FileCard](https://shelf-ui.vercel.app/docs/components/file-card): Smart preview card per file type ... ``` If you're building a tool that needs to consume Shelf UI's API programmatically, `/llms-full.txt` is the right endpoint — it contains the complete prose documentation for every page in a single request. # MCP Server (/docs/getting-started/mcp) The Shelf UI MCP (Model Context Protocol) server exposes the component registry as structured, queryable tools. Once connected, your AI assistant can browse components, get install commands, and read prop documentation without leaving your editor. **MCP is a universal open standard** — it works with any AI assistant that supports it, including GitHub Copilot, Cursor, Windsurf, Claude Desktop, and Antigravity. ## What the server exposes [#what-the-server-exposes] | Tool | Description | | --------------------- | --------------------------------------------------------------------------- | | `search_components` | Search the Shelf UI registry by name or keyword | | `get_component` | Get the full JSON schema for a specific component | | `get_install_command` | Generate the correct shadcn CLI command for a component and package manager | | `list_adapters` | List available storage adapters | ## Connecting your AI assistant [#connecting-your-ai-assistant] Add the following to your AI assistant's MCP config. No API key required — the server reads directly from the public Shelf UI registry at `https://shelf-ui.vercel.app/r`. The easiest way is through the Cursor UI: 1. Open **Cursor Settings** > **Features** > **MCP Servers** 2. Click **+ Add New MCP Server** 3. Name: `shelf-ui` 4. Type: `command` 5. Command: `npx -y @shelf-ui/mcp` Add to `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "shelf-ui": { "command": "npx", "args": ["-y", "@shelf-ui/mcp"] } } } ``` Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): ```json { "mcpServers": { "shelf-ui": { "command": "npx", "args": ["-y", "@shelf-ui/mcp"] } } } ``` Any MCP-compatible client can connect using: ```json { "mcpServers": { "shelf-ui": { "command": "npx", "args": ["-y", "@shelf-ui/mcp"] } } } ``` ## Example tool calls [#example-tool-calls] When connected, you can ask your assistant natural language questions: > *"What file upload components does Shelf UI have?"* The assistant will call `search_components({ query: "upload" })` and return a structured list with descriptions. > *"How do I install the Dropzone component with pnpm?"* The assistant will call `get_install_command({ name: "dropzone", packageManager: "pnpm" })` and return the exact command. > *"What storage adapters does Shelf UI support?"* The assistant will call `list_adapters` and return a list of all supported backends (Firebase Storage, Supabase, AWS S3, Cloudinary, Azure Blob, ImageKit, Uploadthing). # Skills (/docs/getting-started/skills) Skills are context files that give your AI coding assistant knowledge of Shelf UI's component APIs, install commands, and composition patterns. With a skill installed, your assistant knows how to find the right component, add it to your project, and compose it correctly — without hallucinating props or making up import paths. ## What you can ask [#what-you-can-ask] Once installed, prompt your assistant naturally: > *"Add a drag-and-drop file upload zone that accepts images only and shows a preview grid."* > *"Build a file manager with a tree sidebar, breadcrumb navigation, and a grid view."* > *"Show me how to connect the Dropzone to Supabase Storage."* The skill gives your assistant the exact component APIs and shadcn registry commands, so it generates working code on the first try. ## Supported editors [#supported-editors] ## Install [#install] ### Cursor [#cursor] Add Shelf UI as a documentation source in Cursor's settings so it can be indexed and referenced inline. Open **Cursor Settings** → **Features** → **Docs** Click **Add new doc** and enter: ``` https://shelf-ui.vercel.app/llms-full.txt ``` Cursor will index the full Shelf UI documentation. Use `@shelf-ui` in any prompt to scope your question to Shelf UI. **Alternative: Rules file** Add the skill file directly to your `.cursorrules` or a `.cursor/rules/*.mdc` file: ```bash title="terminal" curl -o .cursor/rules/shelf-ui.mdc https://shelf-ui.vercel.app/skills/shelf-ui.md ``` The rule will be applied automatically to every Cursor conversation in your project. ### Claude Code [#claude-code] In your project root, create or open `CLAUDE.md` Add an import reference to the Shelf UI skill: ```markdown title="CLAUDE.md" ## UI Components This project uses Shelf UI for file-related components. @https://shelf-ui.vercel.app/skills/shelf-ui.md ``` Claude Code will automatically load the skill on every session in this project. You can also paste the raw skill content directly into `CLAUDE.md` if you prefer to version-control it without an external reference. ### Windsurf [#windsurf] Open **Windsurf Settings** → **Cascade** → **Knowledge** Add a new knowledge entry with source URL: ``` https://shelf-ui.vercel.app/llms-full.txt ``` Cascade will index Shelf UI and apply it automatically to relevant conversations. **Alternative: Global rules** ```bash title="terminal" curl -o ~/.codeium/windsurf/memories/shelf-ui.md https://shelf-ui.vercel.app/skills/shelf-ui.md ``` ### GitHub Copilot [#github-copilot] In your repo root, create `.github/copilot-instructions.md` if it doesn't exist Add the Shelf UI context: ```markdown title=".github/copilot-instructions.md" ## File UI Components This project uses Shelf UI. Component docs and registry: https://shelf-ui.vercel.app/llms.txt ``` Copilot will use this as workspace context in all conversations. ## What the skill contains [#what-the-skill-contains] The skill file at `/skills/shelf-ui.md` (repo root) includes: * **Full component list** — every component name, category, and one-line description * **Install commands** — shadcn CLI commands for npm, pnpm, yarn, and bun * **Prop reference** — key props for every component with types and defaults * **Composition patterns** — how to combine components (e.g., Dropzone + UploadQueue + StorageQuota) * **Hook APIs** — `useFileUpload`, `useUploadQueue`, `useFileTree`, `useStorageProvider` * **Storage adapter setup** — Supabase, S3, and Cloudinary config snippets ## Raw skill file [#raw-skill-file] The machine-readable skill file is available at: * **GitHub**: [`/skills/shelf-ui/shelf-ui.md`](https://github.com/Kendrick-Oppong/shelf-ui/blob/main/skills/shelf-ui/shelf-ui.md) * **Direct URL**: `https://shelf-ui.vercel.app/skills/shelf-ui.md` You can reference it in any AI tool that supports URL-based knowledge sources. # Theming (/docs/getting-started/theming) Shelf UI uses CSS custom properties (CSS variables) for all visual tokens. Every component reads from the same token set — override a token once in your `globals.css` and it propagates to every component automatically. This is the same system used by shadcn/ui. If you've already customized shadcn tokens, most of your work is done. ## How it works [#how-it-works] Tokens are defined in `globals.css` under `:root` (light mode) and `.dark` (dark mode). Tailwind v4 maps them to utility classes via `@theme`: ```css title="globals.css (simplified)" :root { --background: #f4f4f5; --foreground: #09090b; --primary: #b45309; } .dark { --background: #07070a; --foreground: #eeedf5; --primary: #f0b429; } ``` Tailwind then exposes them as `bg-background`, `text-foreground`, `text-primary`, etc. ## Token reference [#token-reference] ### Colors [#colors] | Token | Value | Usage | | ------------------------ | ------------------ | ------------------- | | `--background` | `#f4f4f5` | Page background | | `--foreground` | `#09090b` | Default text | | `--card` | `#ffffff` | Card surfaces | | `--card-foreground` | `#09090b` | Text on cards | | `--popover` | `#ffffff` | Popovers, dropdowns | | `--popover-foreground` | `#09090b` | Text in popovers | | `--primary` | `#b45309` | Brand color, CTAs | | `--primary-foreground` | `#ffffff` | Text on primary | | `--secondary` | `#f4f4f5` | Secondary surfaces | | `--secondary-foreground` | `#09090b` | Text on secondary | | `--muted` | `#f4f4f5` | Subtle backgrounds | | `--muted-foreground` | `#747477` | De-emphasized text | | `--accent` | `#e4e4e7` | Hover states | | `--accent-foreground` | `#09090b` | Text on accent | | `--destructive` | `#ef4444` | Error states | | `--border` | `rgba(0,0,0,0.08)` | Default borders | | `--input` | `rgba(0,0,0,0.1)` | Input borders | | `--ring` | `#b45309` | Focus rings | | Token | Value | Usage | | ------------------------ | ------------------------ | ------------------- | | `--background` | `#07070a` | Page background | | `--foreground` | `#eeedf5` | Default text | | `--card` | `#111118` | Card surfaces | | `--card-foreground` | `#eeedf5` | Text on cards | | `--popover` | `#111118` | Popovers, dropdowns | | `--popover-foreground` | `#eeedf5` | Text in popovers | | `--primary` | `#f0b429` | Brand color, CTAs | | `--primary-foreground` | `#07070a` | Text on primary | | `--secondary` | `#1c1c26` | Secondary surfaces | | `--secondary-foreground` | `#eeedf5` | Text on secondary | | `--muted` | `#18181f` | Subtle backgrounds | | `--muted-foreground` | `#99999e` | De-emphasized text | | `--accent` | `#1c1c26` | Hover states | | `--accent-foreground` | `#eeedf5` | Text on accent | | `--destructive` | `#f87171` | Error states | | `--border` | `rgba(255,255,255,0.06)` | Default borders | | `--input` | `rgba(255,255,255,0.08)` | Input borders | | `--ring` | `#f0b429` | Focus rings | ### Typography [#typography] | Token | Value | Usage | | ------------- | ---------------------------- | -------------------- | | `--font-sans` | `"Bai Jamjuree", sans-serif` | Body text, UI labels | | `--font-mono` | `"Courier Prime", monospace` | Code blocks | ### Border radius [#border-radius] | Token | Value | | ------------- | ------------------------------- | | `--radius` | `0.625rem` (10px) — base radius | | `--radius-sm` | `calc(var(--radius) - 4px)` | | `--radius-md` | `calc(var(--radius) - 2px)` | | `--radius-lg` | `var(--radius)` | | `--radius-xl` | `calc(var(--radius) + 4px)` | ### Shadows [#shadows] | Token | Value | | ------------- | -------------------------------------- | | `--shadow-sm` | `0px 2px 6px 0px hsl(0 0% 0% / 0.09)` | | `--shadow` | `0px 4px 10px 0px hsl(0 0% 0% / 0.1)` | | `--shadow-lg` | `0px 8px 20px 0px hsl(0 0% 0% / 0.12)` | ## Overriding tokens [#overriding-tokens] ### Change the primary brand color [#change-the-primary-brand-color] The most common customization. Override `--primary` and `--primary-foreground` in both modes: ```css title="globals.css" :root { --primary: #2563eb; /* your brand blue */ --primary-foreground: #ffffff; } .dark { --primary: #60a5fa; /* lighter for dark mode */ --primary-foreground: #0f172a; } ``` This propagates to every button, badge, focus ring, and active state across all Shelf UI components automatically. ### Change the font [#change-the-font] Swap in any Google Font: ```css title="globals.css" @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); :root { --font-sans: "Inter", sans-serif; } ``` Or use a Next.js font (recommended for performance): ```tsx title="app/layout.tsx" import { Inter } from "next/font/google"; const fontSans = Inter({ subsets: ["latin"], variable: "--font-sans" }); ``` ### Change the border radius [#change-the-border-radius] For a sharper, more geometric look: ```css title="globals.css" :root { --radius: 0.25rem; /* 4px — very sharp */ } ``` For a more rounded, softer look: ```css title="globals.css" :root { --radius: 1rem; /* 16px — very rounded */ } ``` ## Full token block [#full-token-block] Copy this into your `globals.css` if you want to start from scratch: ```css title="globals.css" :root { --background: #f4f4f5; --foreground: #09090b; --card: #ffffff; --card-foreground: #09090b; --popover: #ffffff; --popover-foreground: #09090b; --primary: #b45309; --primary-foreground: #ffffff; --secondary: #f4f4f5; --secondary-foreground: #09090b; --muted: #f4f4f5; --muted-foreground: #747477; --accent: #e4e4e7; --accent-foreground: #09090b; --destructive: #ef4444; --border: rgba(0, 0, 0, 0.08); --input: rgba(0, 0, 0, 0.1); --ring: #b45309; --radius: 0.625rem; --font-sans: "Bai Jamjuree", sans-serif; --font-mono: "Courier Prime", monospace; } .dark { --background: #07070a; --foreground: #eeedf5; --card: #111118; --card-foreground: #eeedf5; --popover: #111118; --popover-foreground: #eeedf5; --primary: #f0b429; --primary-foreground: #07070a; --secondary: #1c1c26; --secondary-foreground: #eeedf5; --muted: #18181f; --muted-foreground: #99999e; --accent: #1c1c26; --accent-foreground: #eeedf5; --destructive: #f87171; --border: rgba(255, 255, 255, 0.06); --input: rgba(255, 255, 255, 0.08); --ring: #f0b429; } ``` Use the [shadcn theme generator](https://ui.shadcn.com/themes) or [Tweakcn](https://tweakcn.com) to produce a compatible token set — then paste the output into your `globals.css`. All Shelf UI components will pick it up automatically. ## FAQ [#faq] No. Shelf UI uses the same token names as shadcn/ui by design. If you already have shadcn tokens set up, Shelf UI components will inherit them automatically. Yes. Add any custom token to `:root` and reference it in your component with `var(--your-token)`. Shelf UI components will not conflict with custom tokens. CSS variables are theme-aware by default — they switch between light and dark mode without any JavaScript. Tailwind config colors require `dark:` variants everywhere. The CSS variable approach is strictly simpler. # FileBreadcrumb (/docs/management/file-breadcrumb) This component is coming soon. `FileBreadcrumb` renders a clickable path trail for navigating folder hierarchies — typically paired with `FileTree` or a file manager layout. ## Planned API [#planned-api] ```tsx setCurrentFolder(segment.id)} /> ``` | Prop | Type | Description | | ------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `path` | `BreadcrumbSegment[]` | Ordered array of path segments — each has `id` and `name`. The last item is the current location (non-clickable). | | `onNavigate` | `(segment: BreadcrumbSegment) => void` | Called when a segment is clicked. | | `separator` | `ReactNode` | Custom separator element. Defaults to `/`. | # FileTree (/docs/management/file-tree) This component is coming soon. `FileTree` renders a recursive tree of folders and files. It handles expand/collapse state, keyboard navigation, and optional single or multi-selection — without imposing any storage or data-fetching opinion. ## Planned API [#planned-api] ```tsx console.log(node)} /> ``` | Prop | Type | Description | | ----------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------- | | `nodes` | `TreeNode[]` | The root-level nodes. Each node has `id`, `name`, `type` (`"file"` or `"folder"`), and optional `children`. | | `onSelect` | `(node: TreeNode) => void` | Called when a node is clicked. | | `defaultExpanded` | `string[]` | IDs of folders to expand on first render. | | `selected` | `string` | ID of the currently selected node (controlled). | # Dropzone (/docs/uploads/dropzone) This component handles drag-and-drop mechanics, native file input synchronization, validation, and upload state management. It **does not** render the file list itself. Use [`FileCard`](/docs/uploads/file-card) — a small compound component for a single file row — mapped over `fileStatuses` to build your queue, or bring your own components entirely. ## Dependencies [#dependencies] * **`react-dropzone`** — Powers the underlying drag-and-drop state machine and file validation. * **`lucide-react`** — For standard icons. ## Installation [#installation] ```bash title="terminal" npx shadcn@latest add @shelf-ui/dropzone ``` ```bash title="terminal" pnpm dlx shadcn@latest add @shelf-ui/dropzone ``` ```bash title="terminal" yarn dlx shadcn@latest add @shelf-ui/dropzone ``` ```bash title="terminal" bunx shadcn@latest add @shelf-ui/dropzone ``` ## Usage [#usage] The Dropzone exports a headless hook and compound components. You control the drag area and the file display. ## Connecting to your API [#connecting-to-your-api] The dropzone does not upload files on its own; it manages the state around your API call. The production setup usually looks like this, where `onDropFile` handles your upload API, and you handle deletions in your UI by calling your API before calling `dropzone.onRemove(id)`: ```tsx function UploadComponent() { const dropzone = useDropzone({ // 1. Upload to your server onDropFile: async (file) => { const formData = new FormData(); formData.append("file", file); const response = await fetch("/api/upload", { method: "POST", body: formData, }); if (response.ok) { const data = await response.json(); return { status: "success", result: data.url }; } return { status: "error", error: "Upload failed" }; }, validation: { maxFiles: 10, maxSize: 10 * 1024 * 1024, }, }); // 2. Delete from your server (optional) const handleRemove = async (id: string, fileUrl: string) => { // Fire API call to delete from server await fetch("/api/upload", { method: "DELETE", body: JSON.stringify({ url: fileUrl }), }); // Remove from UI dropzone.onRemove(id); }; return ( Upload files {dropzone.fileStatuses.map((status) => ( handleRemove(status.id, status.result)} // dropzone.onRetry already automatically calls your onDropFile API again! onRetry={() => dropzone.onRetry(status.id)} /> ))} ); } ``` ## Form Integration (React Hook Form) [#form-integration-react-hook-form] Each file is uploaded immediately on drop. `onFileUploaded` fires per file with its result — use it to push the returned URL (or ID) into your form state. The form itself submits normally via `handleSubmit`. ```tsx import { useForm } from "react-hook-form"; type FormValues = { title: string; attachments: string[]; // uploaded file URLs returned by your server }; function FormWithUpload() { const form = useForm({ defaultValues: { title: "", attachments: [] }, }); const dropzone = useDropzone({ onDropFile: uploadToServer, // your fetch function // Called once per file after a successful upload. // Append the returned URL to the "attachments" field in the form. onFileUploaded: (url) => { const current = form.getValues("attachments") ?? []; form.setValue("attachments", [...current, url]); }, }); function onSubmit(values: FormValues) { // values.attachments contains the uploaded file URLs console.log(values); } return (
Upload attachments PNG, JPG, PDF up to 10MB
); } ``` ## Layout Examples [#layout-examples] ### Single File Upload (Avatar / Profile Picture) [#single-file-upload-avatar--profile-picture] Use `shiftOnMaxFiles: true` to automatically replace the previous file instead of showing an error. When the new file would exceed `maxFiles`, the oldest file is dropped first — this happens atomically, so it's safe even if the user drops files in rapid succession. ### Multiple File Upload [#multiple-file-upload] Use `validation.maxFiles` to cap the queue, map `fileStatuses` to `FileCard`, and use `clearAll` / `retryFailed` for batch-level controls. This demo also shows the practical error path: name a file with `fail` in it to see retry UI. ### Image Grid Preview [#image-grid-preview] A gallery-style layout for image-heavy uploads, where thumbnails matter more than filenames or progress text. This gallery keeps a stable object URL per file and revokes URLs when files are removed. For row-based queues, `FileCardPreview showImageThumbnail` gives you the same cleanup automatically. ## Error Handling & Retry [#error-handling--retry] ### Custom Error Messages [#custom-error-messages] There are two error surfaces: * **Root validation errors** from `react-dropzone`, such as invalid file type, too many files, or file size. These appear through `DropzoneMessage` and `rootError`. * **Upload errors** from your `onDropFile` handler. Shape those with `shapeUploadError`, then render them in `FileCardInfo`. ```tsx const dropzone = useDropzone({ onDropFile: uploadToServer, validation: { maxSize: 5 * 1024 * 1024 }, shapeUploadError: (error) => { if (error.code === "file-too-large") { return "This file is too large. Please choose a file under 5MB."; } if (error.code === "file-invalid-type") { return "This file type is not supported."; } return "Something went wrong. Please try again."; }, }); ``` ### Automatic Retry [#automatic-retry] Failed uploads retry automatically with a back-off delay. Each retry waits `1000ms × attemptNumber`. Retries are automatically cancelled if the component unmounts before they fire, and removed files are not retried. ```tsx const dropzone = useDropzone({ onDropFile: uploadToServer, autoRetry: true, maxRetryCount: 3, onFileUploaded: (result, fileId) => { console.log(`File ${fileId} uploaded:`, result); }, onAllUploaded: () => { console.log("All files uploaded!"); }, }); ``` ## Advanced: Composition & Drag Element [#advanced-composition--drag-element] The Dropzone follows a compound component pattern. File list rendering is entirely delegated to you. ```text Dropzone (Provider) ├── DropzoneMessage (root-level validation errors) ├── DropZoneArea (drag target) │ └── DropzoneTrigger (clickable trigger + hidden file input) │ └── DropzoneDescription (helper text) ``` **How the pieces connect:** * `useDropzone()` runs your upload logic and manages all state. * `` is a context provider — it makes that state available to every subcomponent below it. * `` listens for drag events (`onDragEnter`, `onDragLeave`, `onDrop`) and applies the active/invalid styles automatically. * `` renders a hidden `` alongside a ` )} ``` ### Standalone action buttons [#standalone-action-buttons] If you need total control over the action layout, use `FileCardRetryButton` and `FileCardRemoveButton` directly anywhere inside `FileCard`. They read `onRetry` and `onRemove` from context and accept the same props as Shelf UI's `Button`. ```tsx
{status.status === "error" && }
``` ## API Reference [#api-reference] ### `FileCard` [#filecard] | Prop | Type | Required | Description | | ------------ | ------------ | -------- | ------------------------------------------------------------------------------------------------------- | | `fileStatus` | `FileStatus` | Yes | A single entry from `dropzone.fileStatuses`. | | `onRemove` | `() => void` | No | Called when the remove button is clicked. Omit to hide the remove action entirely. | | `onRetry` | `() => void` | No | Called when the retry button is clicked. Only shown when `status === "error"` and `canRetry` is `true`. | | `canRetry` | `boolean` | No | Pass `dropzone.canRetry(status.id)`. Controls whether the retry button renders. | ### `FileCardPreview` [#filecardpreview] | Prop | Type | Default | Description | | -------------------- | ------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ | | `showImageThumbnail` | `boolean` | `false` | Render an `` thumbnail for image files instead of the default status icon. Object URL lifecycle is managed automatically. | | `renderPreview` | `(file: File, status: UploadStatus) => ReactNode` | — | Full control over the preview content. Return `null`/`undefined` to use the built-in fallback for that file. | ### `FileCardInfo` [#filecardinfo] | Prop | Type | Default | Description | | --------------- | -------- | ------- | ----------------------------------------------------------------------------------- | | `maxNameLength` | `number` | `28` | Characters before the filename is truncated with an ellipsis (extension preserved). | Shows the formatted file size by default, or the error message when `status === "error"`. ### `FileCardProgress` [#filecardprogress] No props beyond standard `div` attributes. Reads `status` from context — renders an indeterminate animated bar while `"pending"`, a filled bar on `"success"`, and a destructive-colored bar on `"error"`. ### `FileCardFileName` [#filecardfilename] | Prop | Type | Default | Description | | ----------- | -------- | ------- | ----------------------------------------------------------------------------------- | | `maxLength` | `number` | `28` | Characters before the filename is truncated with an ellipsis (extension preserved). | ### `FileCardFileSize` [#filecardfilesize] | Prop | Type | Default | Description | | ---- | ---- | ------- | -------------------------------------------------------------------------------- | | — | — | — | Standalone formatted file size text. No props beyond standard `span` attributes. | ### `FileCardStatusText` [#filecardstatustext] | Prop | Type | Default | Description | | ---- | ---- | ------- | ------------------------------------------------------------------------------------------------------------------- | | — | — | — | Inline status text ("Uploading...", "Completed", or the error message). No props beyond standard `span` attributes. | ### `FileCardTypeIcon` / `FileCardStatusIcon` [#filecardtypeicon--filecardstatusicon] | Component | Description | | -------------------- | ----------------------------------------------------------------------------------- | | `FileCardTypeIcon` | Standalone file type icon (PDF, Video, Image, Archive, etc) based on the file type. | | `FileCardStatusIcon` | Standalone status icon (Loader, Check, Alert) based on the upload status. | ### `FileCardCopyButton` / `FileCardDownloadButton` [#filecardcopybutton--filecarddownloadbutton] | Component | Description | | ------------------------ | --------------------------------------------------------------------------------- | | `FileCardCopyButton` | Copies the URL to the clipboard. Automatically hides if `status.result` is empty. | | `FileCardDownloadButton` | Provides a direct download link. Automatically hides if `status.result` is empty. | ### `FileCardActions` [#filecardactions] | Prop | Type | Default | Description | | ------------ | ----------- | ------- | ------------------------------------------------------ | | `hideRetry` | `boolean` | `false` | Hide the retry button even when the file is retryable. | | `hideRemove` | `boolean` | `false` | Hide the remove button. | | `children` | `ReactNode` | — | Extra actions rendered after retry/remove. | ### `FileCardRetryButton` / `FileCardRemoveButton` [#filecardretrybutton--filecardremovebutton] | Prop | Type | Default | Description | | ---------- | ------------- | ------- | ------------------------------------------------------------------------------------------------------------ | | `...props` | `ButtonProps` | — | Standalone action buttons for custom layouts. Both accept the same props as the Shelf UI `Button` component. | ### Utilities [#utilities] | Export | Description | | -------------------- | ----------------------------------------------------------------------------------------------------- | | `useFileCardContext` | Reads the current FileCard context for custom subcomponents. | | `useFileObjectUrl` | Creates and revokes an object URL for a `File`; useful for custom previews outside `FileCardPreview`. | ## Accessibility [#accessibility] * Retry and remove buttons have `aria-label`s ("Retry upload", "Remove file") since they're icon-only. * `FileCardProgress` uses `role="progressbar"` with `aria-valuetext` set to `"indeterminate"`, `"100%"`, or `"error"` depending on status. * Error messages in `FileCardInfo` are plain text, so they're picked up by the same ARIA live region wiring as `DropzoneMessage` when both are on the page. ## Performance [#performance] * **Object URLs are handled internally.** `FileCardPreview` uses `useFileObjectUrl` to create the URL via `useEffect` and revoke it automatically on unmount or whenever the `file` reference changes. You don't need to call `URL.createObjectURL` or `URL.revokeObjectURL` yourself for `showImageThumbnail`. * **The URL is only created when it's actually needed** — if `showImageThumbnail` is off, or the file isn't an image, or `renderPreview` returns custom content, no object URL is created at all. * **`FileCardInfo`'s derived strings are memoized** against `file.name`/`file.size`, so re-renders triggered by unrelated state (e.g. a sibling row's status changing) don't re-run the truncation or byte-formatting logic. * **Context memoization has a limit.** `FileCard`'s context value is wrapped in `useMemo`, but that only prevents re-renders if `onRemove`/`onRetry` are stable references. If you pass `() => dropzone.onRemove(status.id)` inline inside a `.map()`, that's a new function every render. For small queues this is invisible; for large or frequently-updating queues, wrap the dropzone's callbacks once with `useCallback` (or curry an id-bound function with `useMemo`) in the parent component. * **Large lists** — if you're rendering hundreds of files, consider a virtualization library (e.g. `react-window`) around the `.map()`, the same guidance as in the Dropzone docs. # Patterns (/docs/uploads/patterns) This page is recipes, not components. Everything below is built from [`Dropzone`](/docs/uploads/dropzone) and [`FileCard`](/docs/uploads/file-card) — nothing new to install. ## Trigger-only (no drag-and-drop surface) [#trigger-only-no-drag-and-drop-surface] `DropzoneTrigger` already renders the hidden `` and owns the click handling, so it doesn't need to live inside a `DropZoneArea`. Skip the drop target entirely for a plain "Choose file" button, useful in compact forms or table cells where a large drag area doesn't fit. This is the closest thing to a standalone "file input" component in Shelf UI — rather than a separate primitive, it's `DropzoneTrigger` used on its own. ## Avatar / single-file replace [#avatar--single-file-replace] Use `maxFiles: 1` plus `shiftOnMaxFiles: true` when a new file should replace the old one automatically. The trigger can render the live preview, so the upload control and the current value are the same element. ## Empty and error states [#empty-and-error-states] `fileStatuses` is just an array — when it's empty, nothing renders by default. Most upload UIs benefit from an explicit empty state instead of a blank gap, and from making `isInvalid`/`rootError` visible at a glance rather than relying solely on `DropzoneMessage` text. `DropzoneMessage` already renders `rootError` automatically — the empty-state block above is additive, not a replacement. Use it when you want the queue area itself to communicate state, not just the drop zone. ## Documents-only queue (no thumbnails) [#documents-only-queue-no-thumbnails] When every file is the same general type (PDFs, contracts, resumes), thumbnails and progress bars can add visual noise without adding information. Compose `FileCard` with only `FileCardInfo` and `FileCardActions` for a denser list. ## Batch upload queue [#batch-upload-queue] Use this when the user expects to upload several independent files, see partial progress, clear the queue, and retry only failed items. ## Image gallery [#image-gallery] For image-heavy workflows, skip row-based cards and show a grid where thumbnails are the primary information. This pattern keeps object URLs stable and revokes them when files are removed. ## Choosing a pattern [#choosing-a-pattern] A quick way to decide which composition fits: * **Need drag-and-drop and multiple files** — use the default `Dropzone` + `DropZoneArea` composition from the [Usage](/docs/uploads/dropzone#usage) example. * **Need a single file, no drag-and-drop, fits in a tight space** — Trigger-only, above. * **Need exactly one file that replaces itself (avatar, logo, cover image)** — Avatar pattern, above. * **Need a gallery of images** — Image gallery, above. * **Need a document checklist with no visual fluff** — Documents-only queue, above. * **Need retry and clear controls for a batch** — Batch upload queue, above. None of these require new component code — they're all `useDropzone` configuration plus a choice of which `FileCard` subcomponents to render.