Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/docs/components/file-field.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ First you will need to follow the [manual install setup](./manual-install) guide

- [uploader-provider](./uploader-provider)
- [dropzone](./dropzone)
- [multi-file](./multi-file)
- [file-list](./file-list)

</Step>

Expand Down
103 changes: 103 additions & 0 deletions docs/content/docs/components/file-list.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
title: File List
description: Lists the files of an UploaderProvider with their status, progress and actions.
---

import { LimitedCode } from '@/components/ui/limited-code';
import {
OpenTabs,
OpenTabsContent,
OpenTabsList,
OpenTabsTrigger,
} from '@/components/ui/open-tabs';
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';

`file-list.tsx` exports:

- `FileList`: lists the files of the nearest `UploaderProvider`.
- `FileListItem`: renders one file with its status, progress and actions (retry, cancel, remove).
- `FileBadge`: renders the colored file-type tile.

Use it next to an [Upload Button](./upload-button), or anywhere you want the files of an `UploaderProvider` shown as a list.

<Callout>
The [multi-file](./multi-file) and [file field](./file-field) components
install it automatically via the CLI.
</Callout>

## Installation

<OpenTabs defaultValue="cli">

<OpenTabsList>
<OpenTabsTrigger value="cli">CLI</OpenTabsTrigger>
<OpenTabsTrigger value="manual">Manual</OpenTabsTrigger>
</OpenTabsList>

<OpenTabsContent value="cli">

Use the shadcn CLI to add the component to your project.

```package-install
npx shadcn@latest add https://edgestore.dev/r/file-list.json
```

</OpenTabsContent>

<OpenTabsContent value="manual">

<Steps>

<Step>

### Setup for manual installation

First you will need to follow the [manual install setup](./manual-install) guide.

</Step>

<Step>

### Install required components

- [uploader-provider](./uploader-provider)
- [progress-bar](./progress-bar)

</Step>

<Step>

### Copy this component

<LimitedCode>

<include lang="tsx" meta='title="components/upload/file-list.tsx"'>../../../src/components/upload/file-list.tsx</include>

<include lang="tsx" meta='title="components/upload/upload-utils.ts"'>../../../src/components/upload/upload-utils.ts</include>

</LimitedCode>

</Step>

</Steps>

</OpenTabsContent>

</OpenTabs>

## Usage

```tsx
<UploaderProvider uploadFn={uploadFn} autoUpload>
{/* anything that adds files, e.g. <UploadButton /> */}
<FileList />
</UploaderProvider>
```

## Props

| Prop | Type | Default | Description |
| ---------- | --------- | ------- | ------------------------------------------- |
| `disabled` | `boolean` | `false` | Hides the retry, cancel and remove actions. |
| `...` | | | Any other `<ul>` prop, such as `className`. |
8 changes: 2 additions & 6 deletions docs/content/docs/components/multi-file.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ First you will need to follow the [manual install setup](./manual-install) guide

- [uploader-provider](./uploader-provider)
- [dropzone](./dropzone)
- [progress-bar](./progress-bar)
- [file-list](./file-list)
- [shadcn/ui button](https://ui.shadcn.com/docs/components/button)

</Step>
Expand Down Expand Up @@ -143,8 +143,4 @@ export function MultiFileDropzoneUsage() {

### Building your own layout

`multi-file.tsx` also exports the pieces it is made of, so you can arrange them differently:

- `FileList` lists the files of the nearest `UploaderProvider`.
- `FileListItem` renders one file with its status and actions.
- `FileBadge` renders the colored file-type tile.
`FileUploader` is a `Dropzone` and a [`FileList`](./file-list), so you can arrange the two differently.
10 changes: 7 additions & 3 deletions docs/content/docs/components/upload-button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
<UploadButtonBlock />
</DemoBlock>

It takes the same props as the shadcn/ui `Button`. Several buttons can feed the same `UploaderProvider`, so you can pair them with a `FileList` or build your own list with `useUploader`.
It takes the same props as the shadcn/ui `Button`. Several buttons can feed the same `UploaderProvider`, so you can pair them with a [`FileList`](./file-list) or build your own list with `useUploader`.

## Installation

Expand Down Expand Up @@ -81,13 +81,17 @@ First you will need to follow the [manual install setup](./manual-install) guide

## Usage

Install or copy the component from [Installation](#installation) before using this example.
Install or copy the component from [Installation](#installation) before using this example. It also uses [`FileList`](./file-list):

```package-install
npx shadcn@latest add https://edgestore.dev/r/file-list.json
```

```tsx
'use client';

import { UploadErrors } from '@/components/upload/dropzone';
import { FileList } from '@/components/upload/multi-file';
import { FileList } from '@/components/upload/file-list';
import { UploadButton } from '@/components/upload/upload-button';
import {
UploaderProvider,
Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"components/avatar",
"components/file-field",
"components/upload-button",
"components/file-list",
"components/uploader-provider",
"components/dropzone",
"components/progress-circle",
Expand Down
11 changes: 7 additions & 4 deletions docs/public/r/file-field.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@
"name": "file-field",
"title": "File Field",
"description": "A compact single-file input that sits in a form next to regular fields.",
"dependencies": ["react-dropzone", "lucide-react"],
"dependencies": [
"react-dropzone",
"lucide-react"
],
"registryDependencies": [
"https://edgestore.dev/r/dropzone.json",
"https://edgestore.dev/r/file-uploader.json",
"https://edgestore.dev/r/file-list.json",
"https://edgestore.dev/r/uploader-provider.json",
"https://edgestore.dev/r/upload-utils.json"
],
"files": [
{
"path": "examples/components/src/components/upload/file-field.tsx",
"content": "'use client';\n\nimport { cn } from '@/lib/utils';\nimport { UploadIcon } from 'lucide-react';\nimport * as React from 'react';\nimport { type Accept } from 'react-dropzone';\nimport { dropzoneState, dropzoneVariants, useUploadDropzone } from './dropzone';\nimport { FileListItem } from './multi-file';\nimport { formatFileSize } from './upload-utils';\nimport { useUploader } from './uploader-provider';\n\n/**\n * Props for the FileField component.\n */\nexport type FileFieldProps = Omit<React.ComponentProps<'div'>, 'children'> & {\n /**\n * Accepted file types.\n */\n accept?: Accept;\n\n /**\n * Human-readable list of accepted types, shown in the hint and messages.\n *\n * @example \"PDF, DOC or DOCX\"\n */\n typesLabel?: string;\n\n /**\n * Maximum file size in bytes.\n */\n maxSize?: number;\n\n /**\n * Whether the field is disabled.\n */\n disabled?: boolean;\n\n /**\n * Id for the hidden file input, so a `<label htmlFor>` opens the file dialog.\n */\n inputId?: string;\n};\n\n/**\n * A compact, single-file input that sits in a form next to regular fields.\n * Once a file is chosen, it shows the file with its progress instead.\n *\n * @example\n * ```tsx\n * <Label id=\"resume-label\" htmlFor=\"resume\">Resume</Label>\n * <FileField\n * inputId=\"resume\"\n * aria-labelledby=\"resume-label\"\n * accept={{ 'application/pdf': ['.pdf'] }}\n * typesLabel=\"PDF\"\n * maxSize={1024 * 1024 * 5} // 5MB\n * />\n * ```\n */\nexport function FileField({\n accept,\n typesLabel,\n maxSize,\n disabled,\n inputId,\n className,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby': ariaDescribedBy,\n ...props\n}: FileFieldProps) {\n const { fileStates } = useUploader();\n const fileState = fileStates[0];\n const errorId = React.useId();\n\n const { getRootProps, getInputProps, isDragActive, isDragReject, errors } =\n useUploadDropzone({ accept, maxSize, typesLabel, disabled, replace: true });\n\n const hint = [typesLabel, maxSize && formatFileSize(maxSize)]\n .filter(Boolean)\n .join(' · ');\n\n return (\n <div className={cn('@container grid gap-1.5', className)} {...props}>\n {fileState ? (\n <ul>\n <FileListItem fileState={fileState} disabled={disabled} />\n </ul>\n ) : (\n <div\n {...getRootProps({\n role: 'button',\n 'aria-label':\n ariaLabel ?? (ariaLabelledBy ? undefined : 'Upload a file'),\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby':\n [ariaDescribedBy, errors.length > 0 && errorId]\n .filter(Boolean)\n .join(' ') || undefined,\n 'aria-invalid': errors.length > 0 || undefined,\n className: cn(\n dropzoneVariants,\n 'grid grid-cols-[auto_minmax(0,1fr)] gap-x-2.5 gap-y-1.5 rounded-lg bg-background px-3.5 py-3 text-left hover:bg-background aria-invalid:border-destructive @min-[22rem]:grid-cols-[auto_minmax(0,1fr)_auto]',\n ),\n })}\n {...dropzoneState({ isDragActive, isDragReject, disabled })}\n >\n <input {...getInputProps({ id: inputId, className: 'absolute' })} />\n <UploadIcon className=\"size-4 text-muted-foreground group-data-[dragging]/dropzone:text-primary\" />\n <span className=\"min-w-0 text-sm group-data-[rejected]/dropzone:text-destructive\">\n {isDragReject ? (\n 'File type not supported'\n ) : isDragActive ? (\n 'Drop to upload'\n ) : (\n <>\n Drop a file or{' '}\n <span className=\"font-semibold text-primary\">browse</span>\n </>\n )}\n </span>\n {hint && (\n <span className=\"col-start-2 text-xs text-muted-foreground @min-[22rem]:col-start-3\">\n {hint}\n </span>\n )}\n </div>\n )}\n {errors[0] && (\n <p id={errorId} role=\"alert\" className=\"text-sm text-destructive\">\n {errors[0]}\n </p>\n )}\n </div>\n );\n}\n",
"content": "'use client';\n\nimport { cn } from '@/lib/utils';\nimport { UploadIcon } from 'lucide-react';\nimport * as React from 'react';\nimport { type Accept } from 'react-dropzone';\nimport { dropzoneState, dropzoneVariants, useUploadDropzone } from './dropzone';\nimport { FileListItem } from './file-list';\nimport { formatFileSize } from './upload-utils';\nimport { useUploader } from './uploader-provider';\n\n/**\n * Props for the FileField component.\n */\nexport type FileFieldProps = Omit<React.ComponentProps<'div'>, 'children'> & {\n /**\n * Accepted file types.\n */\n accept?: Accept;\n\n /**\n * Human-readable list of accepted types, shown in the hint and messages.\n *\n * @example \"PDF, DOC or DOCX\"\n */\n typesLabel?: string;\n\n /**\n * Maximum file size in bytes.\n */\n maxSize?: number;\n\n /**\n * Whether the field is disabled.\n */\n disabled?: boolean;\n\n /**\n * Id for the hidden file input, so a `<label htmlFor>` opens the file dialog.\n */\n inputId?: string;\n};\n\n/**\n * A compact, single-file input that sits in a form next to regular fields.\n * Once a file is chosen, it shows the file with its progress instead.\n *\n * @example\n * ```tsx\n * <Label id=\"resume-label\" htmlFor=\"resume\">Resume</Label>\n * <FileField\n * inputId=\"resume\"\n * aria-labelledby=\"resume-label\"\n * accept={{ 'application/pdf': ['.pdf'] }}\n * typesLabel=\"PDF\"\n * maxSize={1024 * 1024 * 5} // 5MB\n * />\n * ```\n */\nexport function FileField({\n accept,\n typesLabel,\n maxSize,\n disabled,\n inputId,\n className,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby': ariaDescribedBy,\n ...props\n}: FileFieldProps) {\n const { fileStates } = useUploader();\n const fileState = fileStates[0];\n const errorId = React.useId();\n\n const { getRootProps, getInputProps, isDragActive, isDragReject, errors } =\n useUploadDropzone({ accept, maxSize, typesLabel, disabled, replace: true });\n\n const hint = [typesLabel, maxSize && formatFileSize(maxSize)]\n .filter(Boolean)\n .join(' · ');\n\n return (\n <div className={cn('@container grid gap-1.5', className)} {...props}>\n {fileState ? (\n <ul>\n <FileListItem fileState={fileState} disabled={disabled} />\n </ul>\n ) : (\n <div\n {...getRootProps({\n role: 'button',\n 'aria-label':\n ariaLabel ?? (ariaLabelledBy ? undefined : 'Upload a file'),\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby':\n [ariaDescribedBy, errors.length > 0 && errorId]\n .filter(Boolean)\n .join(' ') || undefined,\n 'aria-invalid': errors.length > 0 || undefined,\n className: cn(\n dropzoneVariants,\n 'grid grid-cols-[auto_minmax(0,1fr)] gap-x-2.5 gap-y-1.5 rounded-lg bg-background px-3.5 py-3 text-left hover:bg-background aria-invalid:border-destructive @min-[22rem]:grid-cols-[auto_minmax(0,1fr)_auto]',\n ),\n })}\n {...dropzoneState({ isDragActive, isDragReject, disabled })}\n >\n <input {...getInputProps({ id: inputId, className: 'absolute' })} />\n <UploadIcon className=\"size-4 text-muted-foreground group-data-[dragging]/dropzone:text-primary\" />\n <span className=\"min-w-0 text-sm group-data-[rejected]/dropzone:text-destructive\">\n {isDragReject ? (\n 'File type not supported'\n ) : isDragActive ? (\n 'Drop to upload'\n ) : (\n <>\n Drop a file or{' '}\n <span className=\"font-semibold text-primary\">browse</span>\n </>\n )}\n </span>\n {hint && (\n <span className=\"col-start-2 text-xs text-muted-foreground @min-[22rem]:col-start-3\">\n {hint}\n </span>\n )}\n </div>\n )}\n {errors[0] && (\n <p id={errorId} role=\"alert\" className=\"text-sm text-destructive\">\n {errors[0]}\n </p>\n )}\n </div>\n );\n}\n",
"type": "registry:component",
"target": "components/upload/file-field.tsx"
}
],
"type": "registry:component"
}
}
Loading
Loading