From d1d0b425468eae42eb3e177260021bd4deac2d4e Mon Sep 17 00:00:00 2001 From: Ravi <103035973+raviships@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:38:07 +0900 Subject: [PATCH] fix: move file list into its own registry item Upload button usage and block, and file field, imported FileList/FileListItem from multi-file without the matching registry dependency. Extract FileBadge, FileListItem and FileList into file-list.tsx with a file-list registry item. Co-Authored-By: Claude Opus 5.5 --- docs/content/docs/components/file-field.mdx | 2 +- docs/content/docs/components/file-list.mdx | 103 +++++++++ docs/content/docs/components/multi-file.mdx | 8 +- .../content/docs/components/upload-button.mdx | 10 +- docs/content/docs/meta.json | 1 + docs/public/r/file-field.json | 11 +- docs/public/r/file-list.json | 23 ++ docs/public/r/file-uploader.json | 4 +- docs/public/r/registry.json | 25 ++- docs/public/r/upload-button-block.json | 4 +- .../upload/blocks/upload-button-block.tsx | 2 +- docs/src/components/upload/file-field.tsx | 2 +- docs/src/components/upload/file-list.tsx | 196 ++++++++++++++++++ docs/src/components/upload/multi-file.tsx | 195 +---------------- .../src/app/(tabs)/buttons/page.tsx | 2 +- .../upload/blocks/upload-button-block.tsx | 2 +- .../src/components/upload/file-field.tsx | 2 +- .../src/components/upload/file-list.tsx | 196 ++++++++++++++++++ .../src/components/upload/multi-file.tsx | 195 +---------------- registry.json | 25 ++- scripts/agentDocs/selection.ts | 1 + 21 files changed, 596 insertions(+), 413 deletions(-) create mode 100644 docs/content/docs/components/file-list.mdx create mode 100644 docs/public/r/file-list.json create mode 100644 docs/src/components/upload/file-list.tsx create mode 100644 examples/components/src/components/upload/file-list.tsx 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 `\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 ( + + ); +} + +function IconButton({ + label, + className, + ...props +}: React.ComponentProps<'button'> & { label: string }) { + return ( +