diff --git a/docs/content/docs/components/file-field.mdx b/docs/content/docs/components/file-field.mdx
index e1f527ad..8cbd38de 100644
--- a/docs/content/docs/components/file-field.mdx
+++ b/docs/content/docs/components/file-field.mdx
@@ -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)
diff --git a/docs/content/docs/components/file-list.mdx b/docs/content/docs/components/file-list.mdx
new file mode 100644
index 00000000..4ad48a0b
--- /dev/null
+++ b/docs/content/docs/components/file-list.mdx
@@ -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.
+
+
+ The [multi-file](./multi-file) and [file field](./file-field) components
+ install it automatically via the CLI.
+
+
+## Installation
+
+
+
+
+ CLI
+ Manual
+
+
+
+
+Use the shadcn CLI to add the component to your project.
+
+```package-install
+npx shadcn@latest add https://edgestore.dev/r/file-list.json
+```
+
+
+
+
+
+
+
+
+
+### Setup for manual installation
+
+First you will need to follow the [manual install setup](./manual-install) guide.
+
+
+
+
+
+### Install required components
+
+- [uploader-provider](./uploader-provider)
+- [progress-bar](./progress-bar)
+
+
+
+
+
+### Copy this component
+
+
+
+../../../src/components/upload/file-list.tsx
+
+../../../src/components/upload/upload-utils.ts
+
+
+
+
+
+
+
+
+
+
+
+## Usage
+
+```tsx
+
+ {/* anything that adds files, e.g. */}
+
+
+```
+
+## Props
+
+| Prop | Type | Default | Description |
+| ---------- | --------- | ------- | ------------------------------------------- |
+| `disabled` | `boolean` | `false` | Hides the retry, cancel and remove actions. |
+| `...` | | | Any other `
` prop, such as `className`. |
diff --git a/docs/content/docs/components/multi-file.mdx b/docs/content/docs/components/multi-file.mdx
index a2c46bd6..f2706d40 100644
--- a/docs/content/docs/components/multi-file.mdx
+++ b/docs/content/docs/components/multi-file.mdx
@@ -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)
@@ -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.
diff --git a/docs/content/docs/components/upload-button.mdx b/docs/content/docs/components/upload-button.mdx
index 3ec36b89..a5bf8a02 100644
--- a/docs/content/docs/components/upload-button.mdx
+++ b/docs/content/docs/components/upload-button.mdx
@@ -18,7 +18,7 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
-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
@@ -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,
diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json
index 6c84954e..e9cd52c0 100644
--- a/docs/content/docs/meta.json
+++ b/docs/content/docs/meta.json
@@ -18,6 +18,7 @@
"components/avatar",
"components/file-field",
"components/upload-button",
+ "components/file-list",
"components/uploader-provider",
"components/dropzone",
"components/progress-circle",
diff --git a/docs/public/r/file-field.json b/docs/public/r/file-field.json
index f8b49b5f..845eaf86 100644
--- a/docs/public/r/file-field.json
+++ b/docs/public/r/file-field.json
@@ -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, '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 `` 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 * Resume \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 \n {fileState ? (\n
\n ) : (\n
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 \n \n \n {isDragReject ? (\n 'File type not supported'\n ) : isDragActive ? (\n 'Drop to upload'\n ) : (\n <>\n Drop a file or{' '}\n browse \n >\n )}\n \n {hint && (\n \n {hint}\n \n )}\n
\n )}\n {errors[0] && (\n
\n {errors[0]}\n
\n )}\n
\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, '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 `` 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 * Resume \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 \n {fileState ? (\n
\n ) : (\n
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 \n \n \n {isDragReject ? (\n 'File type not supported'\n ) : isDragActive ? (\n 'Drop to upload'\n ) : (\n <>\n Drop a file or{' '}\n browse \n >\n )}\n \n {hint && (\n \n {hint}\n \n )}\n
\n )}\n {errors[0] && (\n
\n {errors[0]}\n
\n )}\n
\n );\n}\n",
"type": "registry:component",
"target": "components/upload/file-field.tsx"
}
],
"type": "registry:component"
-}
+}
\ No newline at end of file
diff --git a/docs/public/r/file-list.json b/docs/public/r/file-list.json
new file mode 100644
index 00000000..e5d8c613
--- /dev/null
+++ b/docs/public/r/file-list.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "file-list",
+ "title": "File List",
+ "description": "Lists the files of an UploaderProvider with their status, progress, retry, cancel and remove.",
+ "dependencies": [
+ "lucide-react"
+ ],
+ "registryDependencies": [
+ "https://edgestore.dev/r/progress-bar.json",
+ "https://edgestore.dev/r/uploader-provider.json",
+ "https://edgestore.dev/r/upload-utils.json"
+ ],
+ "files": [
+ {
+ "path": "examples/components/src/components/upload/file-list.tsx",
+ "content": "'use client';\n\nimport { cn } from '@/lib/utils';\nimport { CheckIcon, RotateCwIcon, XIcon } from 'lucide-react';\nimport * as React from 'react';\nimport { ProgressBar } from './progress-bar';\nimport {\n fileExtension,\n fileKind,\n formatFileSize,\n type FileKind,\n} from './upload-utils';\nimport { useUploader, type FileState } from './uploader-provider';\n\nconst KIND_CLASSNAMES: Record = {\n image: 'text-purple-600 dark:text-purple-400',\n video: 'text-pink-600 dark:text-pink-400',\n audio: 'text-cyan-600 dark:text-cyan-400',\n pdf: 'text-red-600 dark:text-red-400',\n doc: 'text-blue-600 dark:text-blue-400',\n sheet: 'text-green-600 dark:text-green-400',\n slides: 'text-orange-600 dark:text-orange-400',\n archive: 'text-yellow-600 dark:text-yellow-400',\n code: 'text-teal-600 dark:text-teal-400',\n other: 'text-muted-foreground',\n};\n\n/**\n * A colored tile with the file's extension.\n */\nexport function FileBadge({\n file,\n className,\n}: {\n file: File;\n className?: string;\n}) {\n return (\n \n {fileExtension(file.name).slice(0, 4) || 'file'}\n \n );\n}\n\nfunction IconButton({\n label,\n className,\n ...props\n}: React.ComponentProps<'button'> & { label: string }) {\n return (\n \n );\n}\n\n/**\n * One file with its status, progress and actions (retry, cancel, remove).\n */\nexport function FileListItem({\n fileState,\n disabled,\n className,\n ...props\n}: React.ComponentProps<'li'> & {\n fileState: FileState;\n /** Hides the retry, cancel and remove actions. */\n disabled?: boolean;\n}) {\n const { removeFile, cancelUpload, uploadFiles } = useUploader();\n const { file, key, status, progress, error } = fileState;\n\n return (\n \n \n \n
\n \n {file.name}\n \n {status === 'COMPLETE' && (\n \n \n Uploaded \n \n )}\n
\n
\n {status === 'ERROR'\n ? (error ?? 'Upload failed')\n : status === 'UPLOADING'\n ? `${formatFileSize((file.size * progress) / 100)} of ${formatFileSize(file.size)} · ${Math.round(progress)}%`\n : formatFileSize(file.size)}\n
\n
\n {/* Pinned to the bottom edge so the row keeps its height when the upload ends. */}\n {status === 'UPLOADING' && (\n \n )}\n {!disabled && (\n \n {status === 'ERROR' && (\n void uploadFiles([key])}\n >\n \n \n )}\n {status === 'UPLOADING' ? (\n cancelUpload(key)}\n >\n \n \n ) : (\n removeFile(key)}\n >\n \n \n )}\n
\n )}\n \n );\n}\n\n/**\n * Lists the files of the nearest `UploaderProvider`.\n *\n * @example\n * ```tsx\n * \n * ```\n */\nexport function FileList({\n disabled,\n className,\n ...props\n}: React.ComponentProps<'ul'> & {\n /** Hides the retry, cancel and remove actions. */\n disabled?: boolean;\n}) {\n const { fileStates } = useUploader();\n if (fileStates.length === 0) return null;\n\n return (\n \n {fileStates.map((fileState) => (\n \n ))}\n \n );\n}\n",
+ "type": "registry:component",
+ "target": "components/upload/file-list.tsx"
+ }
+ ],
+ "type": "registry:component"
+}
\ No newline at end of file
diff --git a/docs/public/r/file-uploader.json b/docs/public/r/file-uploader.json
index 8db1bb9a..80944ff7 100644
--- a/docs/public/r/file-uploader.json
+++ b/docs/public/r/file-uploader.json
@@ -10,14 +10,14 @@
"registryDependencies": [
"button",
"https://edgestore.dev/r/dropzone.json",
- "https://edgestore.dev/r/progress-bar.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/multi-file.tsx",
- "content": "'use client';\n\nimport { Button } from '@/components/ui/button';\nimport { cn } from '@/lib/utils';\nimport { CheckIcon, RotateCwIcon, XIcon } from 'lucide-react';\nimport * as React from 'react';\nimport { type Accept } from 'react-dropzone';\nimport { Dropzone } from './dropzone';\nimport { ProgressBar } from './progress-bar';\nimport {\n fileExtension,\n fileKind,\n formatFileSize,\n type FileKind,\n} from './upload-utils';\nimport { useUploader, type FileState } from './uploader-provider';\n\nconst KIND_CLASSNAMES: Record = {\n image: 'text-purple-600 dark:text-purple-400',\n video: 'text-pink-600 dark:text-pink-400',\n audio: 'text-cyan-600 dark:text-cyan-400',\n pdf: 'text-red-600 dark:text-red-400',\n doc: 'text-blue-600 dark:text-blue-400',\n sheet: 'text-green-600 dark:text-green-400',\n slides: 'text-orange-600 dark:text-orange-400',\n archive: 'text-yellow-600 dark:text-yellow-400',\n code: 'text-teal-600 dark:text-teal-400',\n other: 'text-muted-foreground',\n};\n\n/**\n * A colored tile with the file's extension.\n */\nexport function FileBadge({\n file,\n className,\n}: {\n file: File;\n className?: string;\n}) {\n return (\n \n {fileExtension(file.name).slice(0, 4) || 'file'}\n \n );\n}\n\nfunction IconButton({\n label,\n className,\n ...props\n}: React.ComponentProps<'button'> & { label: string }) {\n return (\n \n );\n}\n\n/**\n * One file with its status, progress and actions (retry, cancel, remove).\n */\nexport function FileListItem({\n fileState,\n disabled,\n className,\n ...props\n}: React.ComponentProps<'li'> & {\n fileState: FileState;\n /** Hides the retry, cancel and remove actions. */\n disabled?: boolean;\n}) {\n const { removeFile, cancelUpload, uploadFiles } = useUploader();\n const { file, key, status, progress, error } = fileState;\n\n return (\n \n \n \n
\n \n {file.name}\n \n {status === 'COMPLETE' && (\n \n \n Uploaded \n \n )}\n
\n
\n {status === 'ERROR'\n ? (error ?? 'Upload failed')\n : status === 'UPLOADING'\n ? `${formatFileSize((file.size * progress) / 100)} of ${formatFileSize(file.size)} · ${Math.round(progress)}%`\n : formatFileSize(file.size)}\n
\n
\n {/* Pinned to the bottom edge so the row keeps its height when the upload ends. */}\n {status === 'UPLOADING' && (\n \n )}\n {!disabled && (\n \n {status === 'ERROR' && (\n void uploadFiles([key])}\n >\n \n \n )}\n {status === 'UPLOADING' ? (\n cancelUpload(key)}\n >\n \n \n ) : (\n removeFile(key)}\n >\n \n \n )}\n
\n )}\n \n );\n}\n\n/**\n * Lists the files of the nearest `UploaderProvider`.\n *\n * @example\n * ```tsx\n * \n * ```\n */\nexport function FileList({\n disabled,\n className,\n ...props\n}: React.ComponentProps<'ul'> & {\n /** Hides the retry, cancel and remove actions. */\n disabled?: boolean;\n}) {\n const { fileStates } = useUploader();\n if (fileStates.length === 0) return null;\n\n return (\n \n {fileStates.map((fileState) => (\n \n ))}\n \n );\n}\n\n/**\n * Props for the FileUploader component.\n */\nexport type FileUploaderProps = React.ComponentProps<'div'> & {\n /**\n * Maximum number of files allowed in total.\n */\n maxFiles?: number;\n\n /**\n * Maximum file size in bytes.\n */\n maxSize?: number;\n\n /**\n * Accepted file types.\n *\n * @example\n * ```tsx\n * accept={{ 'application/pdf': ['.pdf'] }}\n * ```\n */\n accept?: Accept;\n\n /**\n * Human-readable list of accepted types, shown in the hint and messages.\n *\n * @example \"PDF or TXT\"\n */\n typesLabel?: string;\n\n /**\n * Whether the uploader is disabled.\n */\n disabled?: boolean;\n\n /**\n * Additional className for the dropzone.\n */\n dropzoneClassName?: string;\n\n /**\n * Additional className for the file list.\n */\n fileListClassName?: string;\n};\n\n/**\n * A dropzone with a list of files, their progress and a summary.\n *\n * @example\n * ```tsx\n * \n * ```\n */\nexport function FileUploader({\n maxFiles,\n maxSize,\n accept,\n typesLabel,\n disabled,\n className,\n dropzoneClassName,\n fileListClassName,\n ...props\n}: FileUploaderProps) {\n const { fileStates, resetFiles } = useUploader();\n const completed = fileStates.filter((fs) => fs.status === 'COMPLETE').length;\n const totalSize = fileStates.reduce((sum, fs) => sum + fs.file.size, 0);\n\n return (\n \n
\n
\n {fileStates.length > 0 && (\n
\n \n {completed} of {fileStates.length} uploaded ·{' '}\n {formatFileSize(totalSize)}\n \n \n Clear all\n \n
\n )}\n
\n );\n}\n",
+ "content": "'use client';\n\nimport { Button } from '@/components/ui/button';\nimport { cn } from '@/lib/utils';\nimport * as React from 'react';\nimport { type Accept } from 'react-dropzone';\nimport { Dropzone } from './dropzone';\nimport { FileList } from './file-list';\nimport { formatFileSize } from './upload-utils';\nimport { useUploader } from './uploader-provider';\n\n/**\n * Props for the FileUploader component.\n */\nexport type FileUploaderProps = React.ComponentProps<'div'> & {\n /**\n * Maximum number of files allowed in total.\n */\n maxFiles?: number;\n\n /**\n * Maximum file size in bytes.\n */\n maxSize?: number;\n\n /**\n * Accepted file types.\n *\n * @example\n * ```tsx\n * accept={{ 'application/pdf': ['.pdf'] }}\n * ```\n */\n accept?: Accept;\n\n /**\n * Human-readable list of accepted types, shown in the hint and messages.\n *\n * @example \"PDF or TXT\"\n */\n typesLabel?: string;\n\n /**\n * Whether the uploader is disabled.\n */\n disabled?: boolean;\n\n /**\n * Additional className for the dropzone.\n */\n dropzoneClassName?: string;\n\n /**\n * Additional className for the file list.\n */\n fileListClassName?: string;\n};\n\n/**\n * A dropzone with a list of files, their progress and a summary.\n *\n * @example\n * ```tsx\n * \n * ```\n */\nexport function FileUploader({\n maxFiles,\n maxSize,\n accept,\n typesLabel,\n disabled,\n className,\n dropzoneClassName,\n fileListClassName,\n ...props\n}: FileUploaderProps) {\n const { fileStates, resetFiles } = useUploader();\n const completed = fileStates.filter((fs) => fs.status === 'COMPLETE').length;\n const totalSize = fileStates.reduce((sum, fs) => sum + fs.file.size, 0);\n\n return (\n \n
\n
\n {fileStates.length > 0 && (\n
\n \n {completed} of {fileStates.length} uploaded ·{' '}\n {formatFileSize(totalSize)}\n \n \n Clear all\n \n
\n )}\n
\n );\n}\n",
"type": "registry:component",
"target": "components/upload/multi-file.tsx"
}
diff --git a/docs/public/r/registry.json b/docs/public/r/registry.json
index 9a5189c4..81d6e27c 100644
--- a/docs/public/r/registry.json
+++ b/docs/public/r/registry.json
@@ -48,6 +48,25 @@
}
]
},
+ {
+ "name": "file-list",
+ "type": "registry:component",
+ "title": "File List",
+ "description": "Lists the files of an UploaderProvider with their status, progress, retry, cancel and remove.",
+ "dependencies": ["lucide-react"],
+ "registryDependencies": [
+ "https://edgestore.dev/r/progress-bar.json",
+ "https://edgestore.dev/r/uploader-provider.json",
+ "https://edgestore.dev/r/upload-utils.json"
+ ],
+ "files": [
+ {
+ "path": "examples/components/src/components/upload/file-list.tsx",
+ "type": "registry:component",
+ "target": "components/upload/file-list.tsx"
+ }
+ ]
+ },
{
"name": "file-uploader",
"type": "registry:component",
@@ -57,7 +76,7 @@
"registryDependencies": [
"button",
"https://edgestore.dev/r/dropzone.json",
- "https://edgestore.dev/r/progress-bar.json",
+ "https://edgestore.dev/r/file-list.json",
"https://edgestore.dev/r/uploader-provider.json",
"https://edgestore.dev/r/upload-utils.json"
],
@@ -139,7 +158,7 @@
"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"
],
@@ -290,7 +309,7 @@
"dependencies": ["lucide-react"],
"registryDependencies": [
"https://edgestore.dev/r/upload-button.json",
- "https://edgestore.dev/r/file-uploader.json",
+ "https://edgestore.dev/r/file-list.json",
"https://edgestore.dev/r/dropzone.json",
"https://edgestore.dev/r/uploader-provider.json"
],
diff --git a/docs/public/r/upload-button-block.json b/docs/public/r/upload-button-block.json
index 9cda7a90..1d9913d3 100644
--- a/docs/public/r/upload-button-block.json
+++ b/docs/public/r/upload-button-block.json
@@ -8,14 +8,14 @@
],
"registryDependencies": [
"https://edgestore.dev/r/upload-button.json",
- "https://edgestore.dev/r/file-uploader.json",
+ "https://edgestore.dev/r/file-list.json",
"https://edgestore.dev/r/dropzone.json",
"https://edgestore.dev/r/uploader-provider.json"
],
"files": [
{
"path": "examples/components/src/components/upload/blocks/upload-button-block.tsx",
- "content": "'use client';\n\nimport { UploadErrors } from '@/components/upload/dropzone';\nimport { FileList } from '@/components/upload/multi-file';\nimport { UploadButton } from '@/components/upload/upload-button';\nimport {\n UploaderProvider,\n type UploadFn,\n} from '@/components/upload/uploader-provider';\nimport { PaperclipIcon, UploadIcon } from 'lucide-react';\nimport * as React from 'react';\n\nexport default function UploadButtonBlock() {\n const [errors, setErrors] = React.useState([]);\n const { edgestore } = useMockEdgeStore(); // Mock edgestore for easy v0 integration\n\n const uploadFn: UploadFn = React.useCallback(\n async ({ file, signal, onProgressChange }) => {\n const res = await edgestore.myPublicFiles.upload({\n file,\n signal,\n onProgressChange,\n });\n // you can run some server action or api here\n // to add the necessary data to your database\n return { url: res.url };\n },\n [edgestore],\n );\n\n return (\n \n
\n
\n \n
\n Upload files\n \n
\n \n \n
\n setErrors([])} />\n \n \n
\n
\n );\n}\n\n// Mock implementation of EdgeStore\nfunction useMockEdgeStore() {\n return {\n edgestore: {\n myPublicFiles: {\n upload: async ({\n signal,\n onProgressChange,\n }: {\n file: File;\n signal?: AbortSignal;\n onProgressChange?: (progress: number) => void | Promise;\n }) => {\n // Simulate upload progress. Rejects like EdgeStore when the upload is aborted.\n await new Promise((resolve, reject) => {\n let progress = 0;\n const interval = setInterval(() => {\n progress = Math.min(\n progress + Math.floor(Math.random() * 21) + 10,\n 100,\n );\n void onProgressChange?.(progress);\n if (progress >= 100) {\n clearInterval(interval);\n resolve();\n }\n }, 300);\n signal?.addEventListener('abort', () => {\n clearInterval(interval);\n reject(new DOMException('Upload aborted', 'AbortError'));\n });\n });\n\n return {\n url: 'https://edgestore.dev/img/upload-demo.webp',\n };\n },\n },\n },\n };\n}\n",
+ "content": "'use client';\n\nimport { UploadErrors } from '@/components/upload/dropzone';\nimport { FileList } from '@/components/upload/file-list';\nimport { UploadButton } from '@/components/upload/upload-button';\nimport {\n UploaderProvider,\n type UploadFn,\n} from '@/components/upload/uploader-provider';\nimport { PaperclipIcon, UploadIcon } from 'lucide-react';\nimport * as React from 'react';\n\nexport default function UploadButtonBlock() {\n const [errors, setErrors] = React.useState([]);\n const { edgestore } = useMockEdgeStore(); // Mock edgestore for easy v0 integration\n\n const uploadFn: UploadFn = React.useCallback(\n async ({ file, signal, onProgressChange }) => {\n const res = await edgestore.myPublicFiles.upload({\n file,\n signal,\n onProgressChange,\n });\n // you can run some server action or api here\n // to add the necessary data to your database\n return { url: res.url };\n },\n [edgestore],\n );\n\n return (\n \n
\n
\n \n
\n Upload files\n \n
\n \n \n
\n setErrors([])} />\n \n \n
\n
\n );\n}\n\n// Mock implementation of EdgeStore\nfunction useMockEdgeStore() {\n return {\n edgestore: {\n myPublicFiles: {\n upload: async ({\n signal,\n onProgressChange,\n }: {\n file: File;\n signal?: AbortSignal;\n onProgressChange?: (progress: number) => void | Promise;\n }) => {\n // Simulate upload progress. Rejects like EdgeStore when the upload is aborted.\n await new Promise((resolve, reject) => {\n let progress = 0;\n const interval = setInterval(() => {\n progress = Math.min(\n progress + Math.floor(Math.random() * 21) + 10,\n 100,\n );\n void onProgressChange?.(progress);\n if (progress >= 100) {\n clearInterval(interval);\n resolve();\n }\n }, 300);\n signal?.addEventListener('abort', () => {\n clearInterval(interval);\n reject(new DOMException('Upload aborted', 'AbortError'));\n });\n });\n\n return {\n url: 'https://edgestore.dev/img/upload-demo.webp',\n };\n },\n },\n },\n };\n}\n",
"type": "registry:block",
"target": "components/upload/blocks/upload-button-block.tsx"
}
diff --git a/docs/src/components/upload/blocks/upload-button-block.tsx b/docs/src/components/upload/blocks/upload-button-block.tsx
index 2a4406a6..5f2df2f2 100644
--- a/docs/src/components/upload/blocks/upload-button-block.tsx
+++ b/docs/src/components/upload/blocks/upload-button-block.tsx
@@ -1,7 +1,7 @@
'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,
diff --git a/docs/src/components/upload/file-field.tsx b/docs/src/components/upload/file-field.tsx
index a4393bc7..a46adfb5 100644
--- a/docs/src/components/upload/file-field.tsx
+++ b/docs/src/components/upload/file-field.tsx
@@ -5,7 +5,7 @@ import { UploadIcon } from 'lucide-react';
import * as React from 'react';
import { type Accept } from 'react-dropzone';
import { dropzoneState, dropzoneVariants, useUploadDropzone } from './dropzone';
-import { FileListItem } from './multi-file';
+import { FileListItem } from './file-list';
import { formatFileSize } from './upload-utils';
import { useUploader } from './uploader-provider';
diff --git a/docs/src/components/upload/file-list.tsx b/docs/src/components/upload/file-list.tsx
new file mode 100644
index 00000000..6e980be1
--- /dev/null
+++ b/docs/src/components/upload/file-list.tsx
@@ -0,0 +1,196 @@
+'use client';
+
+import { cn } from '@/lib/utils';
+import { CheckIcon, RotateCwIcon, XIcon } from 'lucide-react';
+import * as React from 'react';
+import { ProgressBar } from './progress-bar';
+import {
+ fileExtension,
+ fileKind,
+ formatFileSize,
+ type FileKind,
+} from './upload-utils';
+import { useUploader, type FileState } from './uploader-provider';
+
+const KIND_CLASSNAMES: Record = {
+ image: 'text-purple-600 dark:text-purple-400',
+ video: 'text-pink-600 dark:text-pink-400',
+ audio: 'text-cyan-600 dark:text-cyan-400',
+ pdf: 'text-red-600 dark:text-red-400',
+ doc: 'text-blue-600 dark:text-blue-400',
+ sheet: 'text-green-600 dark:text-green-400',
+ slides: 'text-orange-600 dark:text-orange-400',
+ archive: 'text-yellow-600 dark:text-yellow-400',
+ code: 'text-teal-600 dark:text-teal-400',
+ other: 'text-muted-foreground',
+};
+
+/**
+ * A colored tile with the file's extension.
+ */
+export function FileBadge({
+ file,
+ className,
+}: {
+ file: File;
+ className?: string;
+}) {
+ return (
+
+ {fileExtension(file.name).slice(0, 4) || 'file'}
+
+ );
+}
+
+function IconButton({
+ label,
+ className,
+ ...props
+}: React.ComponentProps<'button'> & { label: string }) {
+ return (
+
+ );
+}
+
+/**
+ * One file with its status, progress and actions (retry, cancel, remove).
+ */
+export function FileListItem({
+ fileState,
+ disabled,
+ className,
+ ...props
+}: React.ComponentProps<'li'> & {
+ fileState: FileState;
+ /** Hides the retry, cancel and remove actions. */
+ disabled?: boolean;
+}) {
+ const { removeFile, cancelUpload, uploadFiles } = useUploader();
+ const { file, key, status, progress, error } = fileState;
+
+ return (
+
+
+
+
+
+ {file.name}
+
+ {status === 'COMPLETE' && (
+
+
+ Uploaded
+
+ )}
+
+
+ {status === 'ERROR'
+ ? (error ?? 'Upload failed')
+ : status === 'UPLOADING'
+ ? `${formatFileSize((file.size * progress) / 100)} of ${formatFileSize(file.size)} · ${Math.round(progress)}%`
+ : formatFileSize(file.size)}
+
+
+ {/* Pinned to the bottom edge so the row keeps its height when the upload ends. */}
+ {status === 'UPLOADING' && (
+
+ )}
+ {!disabled && (
+
+ {status === 'ERROR' && (
+ void uploadFiles([key])}
+ >
+
+
+ )}
+ {status === 'UPLOADING' ? (
+ cancelUpload(key)}
+ >
+
+
+ ) : (
+ removeFile(key)}
+ >
+
+
+ )}
+
+ )}
+
+ );
+}
+
+/**
+ * Lists the files of the nearest `UploaderProvider`.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export function FileList({
+ disabled,
+ className,
+ ...props
+}: React.ComponentProps<'ul'> & {
+ /** Hides the retry, cancel and remove actions. */
+ disabled?: boolean;
+}) {
+ const { fileStates } = useUploader();
+ if (fileStates.length === 0) return null;
+
+ return (
+
+ {fileStates.map((fileState) => (
+
+ ))}
+
+ );
+}
diff --git a/docs/src/components/upload/multi-file.tsx b/docs/src/components/upload/multi-file.tsx
index 95adfe66..d622210b 100644
--- a/docs/src/components/upload/multi-file.tsx
+++ b/docs/src/components/upload/multi-file.tsx
@@ -2,201 +2,12 @@
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
-import { CheckIcon, RotateCwIcon, XIcon } from 'lucide-react';
import * as React from 'react';
import { type Accept } from 'react-dropzone';
import { Dropzone } from './dropzone';
-import { ProgressBar } from './progress-bar';
-import {
- fileExtension,
- fileKind,
- formatFileSize,
- type FileKind,
-} from './upload-utils';
-import { useUploader, type FileState } from './uploader-provider';
-
-const KIND_CLASSNAMES: Record = {
- image: 'text-purple-600 dark:text-purple-400',
- video: 'text-pink-600 dark:text-pink-400',
- audio: 'text-cyan-600 dark:text-cyan-400',
- pdf: 'text-red-600 dark:text-red-400',
- doc: 'text-blue-600 dark:text-blue-400',
- sheet: 'text-green-600 dark:text-green-400',
- slides: 'text-orange-600 dark:text-orange-400',
- archive: 'text-yellow-600 dark:text-yellow-400',
- code: 'text-teal-600 dark:text-teal-400',
- other: 'text-muted-foreground',
-};
-
-/**
- * A colored tile with the file's extension.
- */
-export function FileBadge({
- file,
- className,
-}: {
- file: File;
- className?: string;
-}) {
- return (
-
- {fileExtension(file.name).slice(0, 4) || 'file'}
-
- );
-}
-
-function IconButton({
- label,
- className,
- ...props
-}: React.ComponentProps<'button'> & { label: string }) {
- return (
-
- );
-}
-
-/**
- * One file with its status, progress and actions (retry, cancel, remove).
- */
-export function FileListItem({
- fileState,
- disabled,
- className,
- ...props
-}: React.ComponentProps<'li'> & {
- fileState: FileState;
- /** Hides the retry, cancel and remove actions. */
- disabled?: boolean;
-}) {
- const { removeFile, cancelUpload, uploadFiles } = useUploader();
- const { file, key, status, progress, error } = fileState;
-
- return (
-
-
-
-
-
- {file.name}
-
- {status === 'COMPLETE' && (
-
-
- Uploaded
-
- )}
-
-
- {status === 'ERROR'
- ? (error ?? 'Upload failed')
- : status === 'UPLOADING'
- ? `${formatFileSize((file.size * progress) / 100)} of ${formatFileSize(file.size)} · ${Math.round(progress)}%`
- : formatFileSize(file.size)}
-
-
- {/* Pinned to the bottom edge so the row keeps its height when the upload ends. */}
- {status === 'UPLOADING' && (
-
- )}
- {!disabled && (
-
- {status === 'ERROR' && (
- void uploadFiles([key])}
- >
-
-
- )}
- {status === 'UPLOADING' ? (
- cancelUpload(key)}
- >
-
-
- ) : (
- removeFile(key)}
- >
-
-
- )}
-
- )}
-
- );
-}
-
-/**
- * Lists the files of the nearest `UploaderProvider`.
- *
- * @example
- * ```tsx
- *
- * ```
- */
-export function FileList({
- disabled,
- className,
- ...props
-}: React.ComponentProps<'ul'> & {
- /** Hides the retry, cancel and remove actions. */
- disabled?: boolean;
-}) {
- const { fileStates } = useUploader();
- if (fileStates.length === 0) return null;
-
- return (
-
- {fileStates.map((fileState) => (
-
- ))}
-
- );
-}
+import { FileList } from './file-list';
+import { formatFileSize } from './upload-utils';
+import { useUploader } from './uploader-provider';
/**
* Props for the FileUploader component.
diff --git a/examples/components/src/app/(tabs)/buttons/page.tsx b/examples/components/src/app/(tabs)/buttons/page.tsx
index 26aaea79..705cbc71 100644
--- a/examples/components/src/app/(tabs)/buttons/page.tsx
+++ b/examples/components/src/app/(tabs)/buttons/page.tsx
@@ -2,7 +2,7 @@
import { ExampleFrame } from '@/components/ui/example-frame';
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 { SPREADSHEET_ACCEPT } from '@/components/upload/upload-utils';
import {
diff --git a/examples/components/src/components/upload/blocks/upload-button-block.tsx b/examples/components/src/components/upload/blocks/upload-button-block.tsx
index 2a4406a6..5f2df2f2 100644
--- a/examples/components/src/components/upload/blocks/upload-button-block.tsx
+++ b/examples/components/src/components/upload/blocks/upload-button-block.tsx
@@ -1,7 +1,7 @@
'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,
diff --git a/examples/components/src/components/upload/file-field.tsx b/examples/components/src/components/upload/file-field.tsx
index a4393bc7..a46adfb5 100644
--- a/examples/components/src/components/upload/file-field.tsx
+++ b/examples/components/src/components/upload/file-field.tsx
@@ -5,7 +5,7 @@ import { UploadIcon } from 'lucide-react';
import * as React from 'react';
import { type Accept } from 'react-dropzone';
import { dropzoneState, dropzoneVariants, useUploadDropzone } from './dropzone';
-import { FileListItem } from './multi-file';
+import { FileListItem } from './file-list';
import { formatFileSize } from './upload-utils';
import { useUploader } from './uploader-provider';
diff --git a/examples/components/src/components/upload/file-list.tsx b/examples/components/src/components/upload/file-list.tsx
new file mode 100644
index 00000000..6e980be1
--- /dev/null
+++ b/examples/components/src/components/upload/file-list.tsx
@@ -0,0 +1,196 @@
+'use client';
+
+import { cn } from '@/lib/utils';
+import { CheckIcon, RotateCwIcon, XIcon } from 'lucide-react';
+import * as React from 'react';
+import { ProgressBar } from './progress-bar';
+import {
+ fileExtension,
+ fileKind,
+ formatFileSize,
+ type FileKind,
+} from './upload-utils';
+import { useUploader, type FileState } from './uploader-provider';
+
+const KIND_CLASSNAMES: Record = {
+ image: 'text-purple-600 dark:text-purple-400',
+ video: 'text-pink-600 dark:text-pink-400',
+ audio: 'text-cyan-600 dark:text-cyan-400',
+ pdf: 'text-red-600 dark:text-red-400',
+ doc: 'text-blue-600 dark:text-blue-400',
+ sheet: 'text-green-600 dark:text-green-400',
+ slides: 'text-orange-600 dark:text-orange-400',
+ archive: 'text-yellow-600 dark:text-yellow-400',
+ code: 'text-teal-600 dark:text-teal-400',
+ other: 'text-muted-foreground',
+};
+
+/**
+ * A colored tile with the file's extension.
+ */
+export function FileBadge({
+ file,
+ className,
+}: {
+ file: File;
+ className?: string;
+}) {
+ return (
+
+ {fileExtension(file.name).slice(0, 4) || 'file'}
+
+ );
+}
+
+function IconButton({
+ label,
+ className,
+ ...props
+}: React.ComponentProps<'button'> & { label: string }) {
+ return (
+
+ );
+}
+
+/**
+ * One file with its status, progress and actions (retry, cancel, remove).
+ */
+export function FileListItem({
+ fileState,
+ disabled,
+ className,
+ ...props
+}: React.ComponentProps<'li'> & {
+ fileState: FileState;
+ /** Hides the retry, cancel and remove actions. */
+ disabled?: boolean;
+}) {
+ const { removeFile, cancelUpload, uploadFiles } = useUploader();
+ const { file, key, status, progress, error } = fileState;
+
+ return (
+
+
+
+
+
+ {file.name}
+
+ {status === 'COMPLETE' && (
+
+
+ Uploaded
+
+ )}
+
+
+ {status === 'ERROR'
+ ? (error ?? 'Upload failed')
+ : status === 'UPLOADING'
+ ? `${formatFileSize((file.size * progress) / 100)} of ${formatFileSize(file.size)} · ${Math.round(progress)}%`
+ : formatFileSize(file.size)}
+
+
+ {/* Pinned to the bottom edge so the row keeps its height when the upload ends. */}
+ {status === 'UPLOADING' && (
+
+ )}
+ {!disabled && (
+
+ {status === 'ERROR' && (
+ void uploadFiles([key])}
+ >
+
+
+ )}
+ {status === 'UPLOADING' ? (
+ cancelUpload(key)}
+ >
+
+
+ ) : (
+ removeFile(key)}
+ >
+
+
+ )}
+
+ )}
+
+ );
+}
+
+/**
+ * Lists the files of the nearest `UploaderProvider`.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export function FileList({
+ disabled,
+ className,
+ ...props
+}: React.ComponentProps<'ul'> & {
+ /** Hides the retry, cancel and remove actions. */
+ disabled?: boolean;
+}) {
+ const { fileStates } = useUploader();
+ if (fileStates.length === 0) return null;
+
+ return (
+
+ {fileStates.map((fileState) => (
+
+ ))}
+
+ );
+}
diff --git a/examples/components/src/components/upload/multi-file.tsx b/examples/components/src/components/upload/multi-file.tsx
index 95adfe66..d622210b 100644
--- a/examples/components/src/components/upload/multi-file.tsx
+++ b/examples/components/src/components/upload/multi-file.tsx
@@ -2,201 +2,12 @@
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
-import { CheckIcon, RotateCwIcon, XIcon } from 'lucide-react';
import * as React from 'react';
import { type Accept } from 'react-dropzone';
import { Dropzone } from './dropzone';
-import { ProgressBar } from './progress-bar';
-import {
- fileExtension,
- fileKind,
- formatFileSize,
- type FileKind,
-} from './upload-utils';
-import { useUploader, type FileState } from './uploader-provider';
-
-const KIND_CLASSNAMES: Record = {
- image: 'text-purple-600 dark:text-purple-400',
- video: 'text-pink-600 dark:text-pink-400',
- audio: 'text-cyan-600 dark:text-cyan-400',
- pdf: 'text-red-600 dark:text-red-400',
- doc: 'text-blue-600 dark:text-blue-400',
- sheet: 'text-green-600 dark:text-green-400',
- slides: 'text-orange-600 dark:text-orange-400',
- archive: 'text-yellow-600 dark:text-yellow-400',
- code: 'text-teal-600 dark:text-teal-400',
- other: 'text-muted-foreground',
-};
-
-/**
- * A colored tile with the file's extension.
- */
-export function FileBadge({
- file,
- className,
-}: {
- file: File;
- className?: string;
-}) {
- return (
-
- {fileExtension(file.name).slice(0, 4) || 'file'}
-
- );
-}
-
-function IconButton({
- label,
- className,
- ...props
-}: React.ComponentProps<'button'> & { label: string }) {
- return (
-
- );
-}
-
-/**
- * One file with its status, progress and actions (retry, cancel, remove).
- */
-export function FileListItem({
- fileState,
- disabled,
- className,
- ...props
-}: React.ComponentProps<'li'> & {
- fileState: FileState;
- /** Hides the retry, cancel and remove actions. */
- disabled?: boolean;
-}) {
- const { removeFile, cancelUpload, uploadFiles } = useUploader();
- const { file, key, status, progress, error } = fileState;
-
- return (
-
-
-
-
-
- {file.name}
-
- {status === 'COMPLETE' && (
-
-
- Uploaded
-
- )}
-
-
- {status === 'ERROR'
- ? (error ?? 'Upload failed')
- : status === 'UPLOADING'
- ? `${formatFileSize((file.size * progress) / 100)} of ${formatFileSize(file.size)} · ${Math.round(progress)}%`
- : formatFileSize(file.size)}
-
-
- {/* Pinned to the bottom edge so the row keeps its height when the upload ends. */}
- {status === 'UPLOADING' && (
-
- )}
- {!disabled && (
-
- {status === 'ERROR' && (
- void uploadFiles([key])}
- >
-
-
- )}
- {status === 'UPLOADING' ? (
- cancelUpload(key)}
- >
-
-
- ) : (
- removeFile(key)}
- >
-
-
- )}
-
- )}
-
- );
-}
-
-/**
- * Lists the files of the nearest `UploaderProvider`.
- *
- * @example
- * ```tsx
- *
- * ```
- */
-export function FileList({
- disabled,
- className,
- ...props
-}: React.ComponentProps<'ul'> & {
- /** Hides the retry, cancel and remove actions. */
- disabled?: boolean;
-}) {
- const { fileStates } = useUploader();
- if (fileStates.length === 0) return null;
-
- return (
-
- {fileStates.map((fileState) => (
-
- ))}
-
- );
-}
+import { FileList } from './file-list';
+import { formatFileSize } from './upload-utils';
+import { useUploader } from './uploader-provider';
/**
* Props for the FileUploader component.
diff --git a/registry.json b/registry.json
index 9a5189c4..81d6e27c 100644
--- a/registry.json
+++ b/registry.json
@@ -48,6 +48,25 @@
}
]
},
+ {
+ "name": "file-list",
+ "type": "registry:component",
+ "title": "File List",
+ "description": "Lists the files of an UploaderProvider with their status, progress, retry, cancel and remove.",
+ "dependencies": ["lucide-react"],
+ "registryDependencies": [
+ "https://edgestore.dev/r/progress-bar.json",
+ "https://edgestore.dev/r/uploader-provider.json",
+ "https://edgestore.dev/r/upload-utils.json"
+ ],
+ "files": [
+ {
+ "path": "examples/components/src/components/upload/file-list.tsx",
+ "type": "registry:component",
+ "target": "components/upload/file-list.tsx"
+ }
+ ]
+ },
{
"name": "file-uploader",
"type": "registry:component",
@@ -57,7 +76,7 @@
"registryDependencies": [
"button",
"https://edgestore.dev/r/dropzone.json",
- "https://edgestore.dev/r/progress-bar.json",
+ "https://edgestore.dev/r/file-list.json",
"https://edgestore.dev/r/uploader-provider.json",
"https://edgestore.dev/r/upload-utils.json"
],
@@ -139,7 +158,7 @@
"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"
],
@@ -290,7 +309,7 @@
"dependencies": ["lucide-react"],
"registryDependencies": [
"https://edgestore.dev/r/upload-button.json",
- "https://edgestore.dev/r/file-uploader.json",
+ "https://edgestore.dev/r/file-list.json",
"https://edgestore.dev/r/dropzone.json",
"https://edgestore.dev/r/uploader-provider.json"
],
diff --git a/scripts/agentDocs/selection.ts b/scripts/agentDocs/selection.ts
index c4ff0e1f..9ba1527f 100644
--- a/scripts/agentDocs/selection.ts
+++ b/scripts/agentDocs/selection.ts
@@ -60,6 +60,7 @@ export const references: Record = {
'avatar',
'file-field',
'upload-button',
+ 'file-list',
'uploader-provider',
].map((component) => ({
file: `${component}.md`,