From 0d24d878e2c05105c156b7df5708fbf67c738809 Mon Sep 17 00:00:00 2001 From: Ravi <103035973+raviships@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:55:58 +0900 Subject: [PATCH 01/13] docs: standardize EdgeStore branding --- docs/content/docs/components/uploader-provider.mdx | 2 +- docs/src/app/(main)/pricing/_components/pricing.tsx | 4 ++-- docs/src/lib/testimonials.ts | 8 ++++---- docs/src/scripts/download-images.ts | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/content/docs/components/uploader-provider.mdx b/docs/content/docs/components/uploader-provider.mdx index 66b51c72..44fa59d0 100644 --- a/docs/content/docs/components/uploader-provider.mdx +++ b/docs/content/docs/components/uploader-provider.mdx @@ -69,7 +69,7 @@ function MyUploaderPage() { // Define the upload function const uploadFn: UploadFn = React.useCallback( async ({ file, onProgressChange, signal }) => { - // Example using Edge Store client + // Example using EdgeStore client const res = await edgestore.publicFiles.upload({ file, signal, diff --git a/docs/src/app/(main)/pricing/_components/pricing.tsx b/docs/src/app/(main)/pricing/_components/pricing.tsx index 3b10a080..9571eddf 100644 --- a/docs/src/app/(main)/pricing/_components/pricing.tsx +++ b/docs/src/app/(main)/pricing/_components/pricing.tsx @@ -224,7 +224,7 @@ function buildPricingBlocks( id: 'STARTER', title: 'Starter', description: - 'Best for start-ups and businesses who build commercial products with Edge Store.', + 'Best for start-ups and businesses who build commercial products with EdgeStore.', price: toDollars(defaultPricing.STARTER.basePrice, 5), upcoming: toUpcoming(upcomingPricing?.STARTER), features: [ @@ -286,7 +286,7 @@ function buildPricingBlocks( id: 'PRO', title: 'Pro', description: - 'Best for businesses who build larger commercial products with Edge Store.', + 'Best for businesses who build larger commercial products with EdgeStore.', price: toDollars(defaultPricing.PRO.basePrice, 35), upcoming: toUpcoming(upcomingPricing?.PRO), features: [ diff --git a/docs/src/lib/testimonials.ts b/docs/src/lib/testimonials.ts index 084caf81..fee17da4 100644 --- a/docs/src/lib/testimonials.ts +++ b/docs/src/lib/testimonials.ts @@ -19,7 +19,7 @@ export const testimonials = [ user: '@sathishkannan6600', image: '/img/testimonials/sathishkannan6600.jpg', comment: - 'Unbelievable and exhaustive features.Edge store thought about all the use cases. Also, not using a database for protected files is a nice idea. Very clear explanation in the video.', + 'Unbelievable and exhaustive features.EdgeStore thought about all the use cases. Also, not using a database for protected files is a nice idea. Very clear explanation in the video.', platform: 'youtube', url: 'https://www.youtube.com/watch?v=Acq9UEA2akU&lc=Ugw4ne4z1Pj3NS_MGbB4AaABAg', }, @@ -35,7 +35,7 @@ export const testimonials = [ user: '@PhilippeKhin', image: '/img/testimonials/PhilippeKhin.jpg', comment: - "Feeling that Edge Store will take off 🚀 Great stuff you're building here Ravi 👍", + "Feeling that EdgeStore will take off 🚀 Great stuff you're building here Ravi 👍", platform: 'youtube', url: 'https://www.youtube.com/watch?v=Acq9UEA2akU&lc=UgyEH4ERhW0fXigeWN14AaABAg', }, @@ -82,7 +82,7 @@ export const testimonials = [ user: '@developerpranav', image: '/img/testimonials/developerpranav.jpg', comment: - '🤯 Blown away by the simplicity of defining the types of asset uploaded, which then determines the file path. Awesome video, and amazing job on Edge Store btw! Highly considering it for my current project! Just one question, can I use my own S3 Bucket? Specifically, I use Cloudflare R2 which has compatibility with the S3 API.', + '🤯 Blown away by the simplicity of defining the types of asset uploaded, which then determines the file path. Awesome video, and amazing job on EdgeStore btw! Highly considering it for my current project! Just one question, can I use my own S3 Bucket? Specifically, I use Cloudflare R2 which has compatibility with the S3 API.', platform: 'youtube', url: 'https://www.youtube.com/watch?v=Acq9UEA2akU&lc=UgxL7xpOk8ygYV7iYgx4AaABAg', }, @@ -141,7 +141,7 @@ export const testimonials = [ user: '@alijansoomro4277', image: '/img/testimonials/alijansoomro4277.jpg', comment: - 'This service looks easier to use than UploadThing — I was looking for a better way to upload files in Next-JS, and I like Edge Store.', + 'This service looks easier to use than UploadThing — I was looking for a better way to upload files in Next-JS, and I like EdgeStore.', platform: 'youtube', url: 'https://www.youtube.com/watch?v=Acq9UEA2akU&lc=Ugw9Jvy13TVCEXCvftR4AaABAg', }, diff --git a/docs/src/scripts/download-images.ts b/docs/src/scripts/download-images.ts index 411e0f1c..4add6005 100644 --- a/docs/src/scripts/download-images.ts +++ b/docs/src/scripts/download-images.ts @@ -21,14 +21,14 @@ const testimonials = [ image: 'https://yt3.ggpht.com/ytc/AIdro_nzjrEGEW1ljvq0slQBOeXcZwQqHlmFzyPXXTdRQf6ZP8E=s88-c-k-c0x00ffffff-no-rj', comment: - 'Unbelievable and exhaustive features.Edge store thought about all the use cases. Also, not using a database for protected files is a nice idea. Very clear explanation in the video.', + 'Unbelievable and exhaustive features.EdgeStore thought about all the use cases. Also, not using a database for protected files is a nice idea. Very clear explanation in the video.', }, { user: '@PhilippeKhin', image: 'https://yt3.ggpht.com/ytc/AIdro_noIo8kfGBJIlJ3OqosAAIx0Wmce06n5_N-OYSxM5AhvGI=s88-c-k-c0x00ffffff-no-rj', comment: - "Feeling that Edge Store will take off 🚀 Great stuff you're building here Ravi 👍", + "Feeling that EdgeStore will take off 🚀 Great stuff you're building here Ravi 👍", }, { user: '@IyanSR', @@ -55,7 +55,7 @@ const testimonials = [ image: 'https://yt3.ggpht.com/ytc/AIdro_kA5_GhiyHqNvdgi44X2S1x2fkUP98mX--QS0B_N9xNGPs=s88-c-k-c0x00ffffff-no-rj', comment: - '🤯 Blown away by the simplicity of defining the types of asset uploaded, which then determines the file path. Awesome video, and amazing job on Edge Store btw! Highly considering it for my current project! Just one question, can I use my own S3 Bucket? Specifically, I use Cloudflare R2 which has compatibility with the S3 API.', + '🤯 Blown away by the simplicity of defining the types of asset uploaded, which then determines the file path. Awesome video, and amazing job on EdgeStore btw! Highly considering it for my current project! Just one question, can I use my own S3 Bucket? Specifically, I use Cloudflare R2 which has compatibility with the S3 API.', }, { user: '@ryanpp27', @@ -106,13 +106,13 @@ const testimonials = [ image: 'https://yt3.ggpht.com/ytc/AIdro_lBj2_SaydLARaf_VD4cxIDjF_cmHeL_5YSGeFUIfycc9w=s88-c-k-c0x00ffffff-no-rj', comment: - 'This service looks easier to use than UploadThing — I was looking for a better way to upload files in Next-JS, and I like Edge Store.', + 'This service looks easier to use than UploadThing — I was looking for a better way to upload files in Next-JS, and I like EdgeStore.', }, { user: '@hikmatullah2131', image: 'https://yt3.ggpht.com/NLFF28hZDwFf8SnxYbPyiTb9Gzg8jxa3mfC_4OYmo2gOuHmyuVTXWc9XMHgaJQAsoeBPQkXz4g=s88-c-k-c0x00ffffff-no-rj', - comment: 'You covered a lot in a short time, and I love Edge Store too.', + comment: 'You covered a lot in a short time, and I love EdgeStore too.', }, ]; From 1d9c50aba6cd92567af50082daf5755969fcac5f Mon Sep 17 00:00:00 2001 From: Ravi <103035973+raviships@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:16:47 +0900 Subject: [PATCH 02/13] fix: repair website links and component setup flows --- .../content/docs/components/upload-button.mdx | 4 +- docs/public/r/avatar-dropzone-block.json | 4 +- docs/public/r/file-field-block.json | 4 +- docs/public/r/file-uploader-block.json | 4 +- docs/public/r/multi-image-uploader-block.json | 4 +- docs/public/r/progress-bar-block.json | 4 +- docs/public/r/progress-circle-block.json | 4 +- docs/public/r/registry.json | 32 ++++++------- .../public/r/single-image-dropzone-block.json | 4 +- docs/public/r/upload-button-block.json | 4 +- .../(main)/pricing/_components/pricing.tsx | 3 +- docs/src/components/ui/open-tabs.tsx | 45 ++++++++++++++++++- docs/src/lib/constants.ts | 4 +- registry.json | 32 ++++++------- 14 files changed, 96 insertions(+), 56 deletions(-) diff --git a/docs/content/docs/components/upload-button.mdx b/docs/content/docs/components/upload-button.mdx index 3ec36b89..38d6f82c 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`. To show a file list, also install [Multi-file](./multi-file) and add its `FileList`, or build your own list with `useUploader`. ## Installation @@ -87,7 +87,6 @@ Install or copy the component from [Installation](#installation) before using th 'use client'; import { UploadErrors } from '@/components/upload/dropzone'; -import { FileList } from '@/components/upload/multi-file'; import { UploadButton } from '@/components/upload/upload-button'; import { UploaderProvider, @@ -125,7 +124,6 @@ export function UploadButtonUsage() { Upload files setErrors([])} /> - ); } diff --git a/docs/public/r/avatar-dropzone-block.json b/docs/public/r/avatar-dropzone-block.json index d67a5e3c..7994f694 100644 --- a/docs/public/r/avatar-dropzone-block.json +++ b/docs/public/r/avatar-dropzone-block.json @@ -11,8 +11,8 @@ { "path": "examples/components/src/components/upload/blocks/avatar-block.tsx", "content": "'use client';\n\nimport { AvatarDropzone } from '@/components/upload/avatar-dropzone';\nimport {\n UploaderProvider,\n type UploadFn,\n} from '@/components/upload/uploader-provider';\nimport * as React from 'react';\n\nexport default function AvatarDropzoneBlock() {\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.myPublicImages.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
\n
\n );\n}\n\n// Mock implementation of EdgeStore\nfunction useMockEdgeStore() {\n return {\n edgestore: {\n myPublicImages: {\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/avatar-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/file-field-block.json b/docs/public/r/file-field-block.json index 655e44b4..e93770a7 100644 --- a/docs/public/r/file-field-block.json +++ b/docs/public/r/file-field-block.json @@ -12,8 +12,8 @@ { "path": "examples/components/src/components/upload/blocks/file-field-block.tsx", "content": "'use client';\n\nimport { FileField } from '@/components/upload/file-field';\nimport { DOCUMENT_ACCEPT } from '@/components/upload/upload-utils';\nimport {\n UploaderProvider,\n type UploadFn,\n} from '@/components/upload/uploader-provider';\nimport * as React from 'react';\n\nexport default function FileFieldBlock() {\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 Resume\n \n \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/file-field-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/file-uploader-block.json b/docs/public/r/file-uploader-block.json index fab6dd14..6cabbfec 100644 --- a/docs/public/r/file-uploader-block.json +++ b/docs/public/r/file-uploader-block.json @@ -11,8 +11,8 @@ { "path": "examples/components/src/components/upload/blocks/multi-file-block.tsx", "content": "'use client';\n\nimport { FileUploader } from '@/components/upload/multi-file';\nimport {\n UploaderProvider,\n useUploader,\n type CompletedFileState,\n type UploadFn,\n} from '@/components/upload/uploader-provider';\nimport * as React from 'react';\n\nexport default function FileUploaderBlock() {\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 \n
\n
\n );\n}\n\nfunction CompletedFiles() {\n const { fileStates } = useUploader();\n\n const completedFiles = fileStates.filter(\n (fs): fs is CompletedFileState => fs.status === 'COMPLETE',\n );\n\n if (completedFiles.length === 0) {\n return null;\n }\n\n return (\n
\n

Uploaded files

\n
    \n {completedFiles.map((fs) => (\n
  • \n \n {fs.file.name}\n \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/multi-file-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/multi-image-uploader-block.json b/docs/public/r/multi-image-uploader-block.json index 505f5299..d3e0c6cd 100644 --- a/docs/public/r/multi-image-uploader-block.json +++ b/docs/public/r/multi-image-uploader-block.json @@ -11,8 +11,8 @@ { "path": "examples/components/src/components/upload/blocks/multi-image-block.tsx", "content": "'use client';\n\nimport { ImageUploader } from '@/components/upload/multi-image';\nimport {\n UploaderProvider,\n useUploader,\n type CompletedFileState,\n type UploadFn,\n} from '@/components/upload/uploader-provider';\nimport * as React from 'react';\n\nexport default function MultiImageUploaderBlock() {\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.myPublicImages.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 \n
\n
\n );\n}\n\nfunction CompletedFiles() {\n const { fileStates } = useUploader();\n\n const completedFiles = fileStates.filter(\n (fs): fs is CompletedFileState => fs.status === 'COMPLETE',\n );\n\n if (completedFiles.length === 0) {\n return null;\n }\n\n return (\n
\n

Uploaded files

\n
    \n {completedFiles.map((fs) => (\n
  • \n \n {fs.file.name}\n \n
  • \n ))}\n
\n
\n );\n}\n\n// Mock implementation of EdgeStore\nfunction useMockEdgeStore() {\n return {\n edgestore: {\n myPublicImages: {\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/multi-image-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/progress-bar-block.json b/docs/public/r/progress-bar-block.json index d0235832..65ac5e9d 100644 --- a/docs/public/r/progress-bar-block.json +++ b/docs/public/r/progress-bar-block.json @@ -10,8 +10,8 @@ { "path": "examples/components/src/components/upload/blocks/progress-bar-block.tsx", "content": "'use client';\n\nimport * as React from 'react';\nimport { ProgressBar } from '../progress-bar';\n\nexport default function ProgressBarUsage() {\n const [progress, setProgress] = React.useState(0);\n\n React.useEffect(() => {\n const startAnimation = () => {\n const interval = setInterval(() => {\n setProgress((prevProgress) => {\n if (prevProgress >= 100) {\n clearInterval(interval);\n // Reset after 1 second when reaching 100%\n setTimeout(() => {\n setProgress(0);\n startAnimation(); // Restart the animation cycle\n }, 1000);\n return 100;\n }\n // Add a random value between 10-20 percent\n return Math.min(\n 100,\n prevProgress + Math.floor(Math.random() * 11) + 10,\n );\n });\n }, 300);\n\n return interval;\n };\n\n const interval = startAnimation();\n\n return () => {\n clearInterval(interval);\n };\n }, []);\n\n return (\n
\n \n
\n );\n}\n", - "type": "registry:block", - "target": "components/upload/blocks/progress-bar-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/progress-circle-block.json b/docs/public/r/progress-circle-block.json index 86d11fd4..ea132b32 100644 --- a/docs/public/r/progress-circle-block.json +++ b/docs/public/r/progress-circle-block.json @@ -10,8 +10,8 @@ { "path": "examples/components/src/components/upload/blocks/progress-circle-block.tsx", "content": "'use client';\n\nimport * as React from 'react';\nimport { ProgressCircle } from '../progress-circle';\n\nexport default function ProgressCircleUsage() {\n const [progress, setProgress] = React.useState(0);\n\n React.useEffect(() => {\n const startAnimation = () => {\n const interval = setInterval(() => {\n setProgress((prevProgress) => {\n if (prevProgress >= 100) {\n clearInterval(interval);\n // Reset after 1 second when reaching 100%\n setTimeout(() => {\n setProgress(0);\n startAnimation(); // Restart the animation cycle\n }, 1000);\n return 100;\n }\n // Add a random value between 10-20 percent\n return Math.min(\n 100,\n prevProgress + Math.floor(Math.random() * 11) + 10,\n );\n });\n }, 300);\n\n return interval;\n };\n\n const interval = startAnimation();\n\n return () => {\n clearInterval(interval);\n };\n }, []);\n\n return (\n
\n
\n \n
\n
\n );\n}\n", - "type": "registry:block", - "target": "components/upload/blocks/progress-circle-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/registry.json b/docs/public/r/registry.json index 9a5189c4..2a4951af 100644 --- a/docs/public/r/registry.json +++ b/docs/public/r/registry.json @@ -208,8 +208,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/multi-file-block.tsx", - "type": "registry:block", - "target": "components/upload/blocks/multi-file-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ] }, @@ -225,8 +225,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/single-image-block.tsx", - "type": "registry:block", - "target": "components/upload/blocks/single-image-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ] }, @@ -242,8 +242,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/multi-image-block.tsx", - "type": "registry:block", - "target": "components/upload/blocks/multi-image-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ] }, @@ -259,8 +259,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/avatar-block.tsx", - "type": "registry:block", - "target": "components/upload/blocks/avatar-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ] }, @@ -277,8 +277,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/file-field-block.tsx", - "type": "registry:block", - "target": "components/upload/blocks/file-field-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ] }, @@ -297,8 +297,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/upload-button-block.tsx", - "type": "registry:block", - "target": "components/upload/blocks/upload-button-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ] }, @@ -311,8 +311,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/progress-bar-block.tsx", - "type": "registry:block", - "target": "components/upload/blocks/progress-bar-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ] }, @@ -325,8 +325,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/progress-circle-block.tsx", - "type": "registry:block", - "target": "components/upload/blocks/progress-circle-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ] } diff --git a/docs/public/r/single-image-dropzone-block.json b/docs/public/r/single-image-dropzone-block.json index b8f7e699..30bca9d4 100644 --- a/docs/public/r/single-image-dropzone-block.json +++ b/docs/public/r/single-image-dropzone-block.json @@ -11,8 +11,8 @@ { "path": "examples/components/src/components/upload/blocks/single-image-block.tsx", "content": "'use client';\n\nimport { SingleImageDropzone } from '@/components/upload/single-image';\nimport {\n UploaderProvider,\n useUploader,\n type CompletedFileState,\n type UploadFn,\n} from '@/components/upload/uploader-provider';\nimport * as React from 'react';\n\nexport default function SingleImageUploaderBlock() {\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.myPublicImages.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
\n );\n}\n\nfunction CompletedFiles() {\n const { fileStates } = useUploader();\n\n const completedFiles = fileStates.filter(\n (fs): fs is CompletedFileState => fs.status === 'COMPLETE',\n );\n\n if (completedFiles.length === 0) {\n return null;\n }\n\n return (\n
\n

Uploaded files

\n
    \n {completedFiles.map((fs) => (\n
  • \n \n {fs.file.name}\n \n
  • \n ))}\n
\n
\n );\n}\n\n// Mock implementation of EdgeStore\nfunction useMockEdgeStore() {\n return {\n edgestore: {\n myPublicImages: {\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/single-image-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/upload-button-block.json b/docs/public/r/upload-button-block.json index 9cda7a90..8cbed335 100644 --- a/docs/public/r/upload-button-block.json +++ b/docs/public/r/upload-button-block.json @@ -16,8 +16,8 @@ { "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", - "type": "registry:block", - "target": "components/upload/blocks/upload-button-block.tsx" + "type": "registry:page", + "target": "app/page.tsx" } ], "type": "registry:block" diff --git a/docs/src/app/(main)/pricing/_components/pricing.tsx b/docs/src/app/(main)/pricing/_components/pricing.tsx index 9571eddf..8de41116 100644 --- a/docs/src/app/(main)/pricing/_components/pricing.tsx +++ b/docs/src/app/(main)/pricing/_components/pricing.tsx @@ -1,5 +1,6 @@ import { ActionButton } from '@/components/action-button'; import { env } from '@/env'; +import { SPONSOR_URL } from '@/lib/constants'; import { formatFileSize, formatNumber } from '@/lib/formatter'; import { cn } from '@/lib/utils'; import { CheckCircleIcon, HeartIcon } from 'lucide-react'; @@ -127,7 +128,7 @@ function PricingBlock(props: { function SponsorButton() { return ( - + Sponsor ); diff --git a/docs/src/components/ui/open-tabs.tsx b/docs/src/components/ui/open-tabs.tsx index 9cafdea3..78ea9140 100644 --- a/docs/src/components/ui/open-tabs.tsx +++ b/docs/src/components/ui/open-tabs.tsx @@ -4,7 +4,47 @@ import { cn } from '@/lib/utils'; import * as TabsPrimitive from '@radix-ui/react-tabs'; import * as React from 'react'; -const OpenTabs = TabsPrimitive.Root; +const OpenTabs = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>((props, ref) => { + const root = React.useRef(null); + + React.useEffect(() => { + let frame = 0; + function revealAnchor() { + const target = document.getElementById(window.location.hash.slice(1)); + if (!target || !root.current?.contains(target)) return; + const panel = target.closest('[role="tabpanel"]'); + if (panel?.getAttribute('data-state') !== 'inactive') return; + + const trigger = Array.from( + root.current.querySelectorAll('[role="tab"]'), + ).find((tab) => tab.getAttribute('aria-controls') === panel.id); + trigger?.focus(); + frame = requestAnimationFrame(() => target.scrollIntoView()); + } + + revealAnchor(); + window.addEventListener('hashchange', revealAnchor); + return () => { + window.removeEventListener('hashchange', revealAnchor); + cancelAnimationFrame(frame); + }; + }, []); + + return ( + { + root.current = element; + if (typeof ref === 'function') return ref(element); + if (ref) ref.current = element; + }} + /> + ); +}); +OpenTabs.displayName = TabsPrimitive.Root.displayName; const OpenTabsList = React.forwardRef< React.ComponentRef, @@ -42,8 +82,9 @@ const OpenTabsContent = React.forwardRef< >(({ className, ...props }, ref) => ( Date: Sun, 11 Oct 2026 18:20:30 +0900 Subject: [PATCH 03/13] fix: reopen manual instructions for repeated anchor clicks --- docs/src/components/ui/open-tabs.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/src/components/ui/open-tabs.tsx b/docs/src/components/ui/open-tabs.tsx index 78ea9140..1a60f32b 100644 --- a/docs/src/components/ui/open-tabs.tsx +++ b/docs/src/components/ui/open-tabs.tsx @@ -21,14 +21,34 @@ const OpenTabs = React.forwardRef< const trigger = Array.from( root.current.querySelectorAll('[role="tab"]'), ).find((tab) => tab.getAttribute('aria-controls') === panel.id); + // Radix activates a tab when its trigger receives focus. trigger?.focus(); frame = requestAnimationFrame(() => target.scrollIntoView()); } + function onAnchorClick(event: MouseEvent) { + if ( + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) { + return; + } + const anchor = + event.target instanceof Element ? event.target.closest('a') : null; + // Clicking the current anchor does not emit another hashchange. + if (anchor?.href === window.location.href) revealAnchor(); + } + revealAnchor(); window.addEventListener('hashchange', revealAnchor); + document.addEventListener('click', onAnchorClick); return () => { window.removeEventListener('hashchange', revealAnchor); + document.removeEventListener('click', onAnchorClick); cancelAnimationFrame(frame); }; }, []); From afbc8c7a32e4d7820a5383a870211fb1e779441c Mon Sep 17 00:00:00 2001 From: Ravi <103035973+raviships@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:28:20 +0900 Subject: [PATCH 04/13] fix: keep the PR limited to verified website and docs fixes --- docs/public/r/avatar-dropzone-block.json | 4 +-- docs/public/r/file-field-block.json | 4 +-- docs/public/r/file-uploader-block.json | 4 +-- docs/public/r/multi-image-uploader-block.json | 4 +-- docs/public/r/progress-bar-block.json | 4 +-- docs/public/r/progress-circle-block.json | 4 +-- docs/public/r/registry.json | 32 +++++++++---------- .../public/r/single-image-dropzone-block.json | 4 +-- docs/public/r/upload-button-block.json | 4 +-- registry.json | 32 +++++++++---------- 10 files changed, 48 insertions(+), 48 deletions(-) diff --git a/docs/public/r/avatar-dropzone-block.json b/docs/public/r/avatar-dropzone-block.json index 7994f694..d67a5e3c 100644 --- a/docs/public/r/avatar-dropzone-block.json +++ b/docs/public/r/avatar-dropzone-block.json @@ -11,8 +11,8 @@ { "path": "examples/components/src/components/upload/blocks/avatar-block.tsx", "content": "'use client';\n\nimport { AvatarDropzone } from '@/components/upload/avatar-dropzone';\nimport {\n UploaderProvider,\n type UploadFn,\n} from '@/components/upload/uploader-provider';\nimport * as React from 'react';\n\nexport default function AvatarDropzoneBlock() {\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.myPublicImages.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
\n
\n );\n}\n\n// Mock implementation of EdgeStore\nfunction useMockEdgeStore() {\n return {\n edgestore: {\n myPublicImages: {\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:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/avatar-block.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/file-field-block.json b/docs/public/r/file-field-block.json index e93770a7..655e44b4 100644 --- a/docs/public/r/file-field-block.json +++ b/docs/public/r/file-field-block.json @@ -12,8 +12,8 @@ { "path": "examples/components/src/components/upload/blocks/file-field-block.tsx", "content": "'use client';\n\nimport { FileField } from '@/components/upload/file-field';\nimport { DOCUMENT_ACCEPT } from '@/components/upload/upload-utils';\nimport {\n UploaderProvider,\n type UploadFn,\n} from '@/components/upload/uploader-provider';\nimport * as React from 'react';\n\nexport default function FileFieldBlock() {\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 Resume\n \n \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:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/file-field-block.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/file-uploader-block.json b/docs/public/r/file-uploader-block.json index 6cabbfec..fab6dd14 100644 --- a/docs/public/r/file-uploader-block.json +++ b/docs/public/r/file-uploader-block.json @@ -11,8 +11,8 @@ { "path": "examples/components/src/components/upload/blocks/multi-file-block.tsx", "content": "'use client';\n\nimport { FileUploader } from '@/components/upload/multi-file';\nimport {\n UploaderProvider,\n useUploader,\n type CompletedFileState,\n type UploadFn,\n} from '@/components/upload/uploader-provider';\nimport * as React from 'react';\n\nexport default function FileUploaderBlock() {\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 \n
\n
\n );\n}\n\nfunction CompletedFiles() {\n const { fileStates } = useUploader();\n\n const completedFiles = fileStates.filter(\n (fs): fs is CompletedFileState => fs.status === 'COMPLETE',\n );\n\n if (completedFiles.length === 0) {\n return null;\n }\n\n return (\n
\n

Uploaded files

\n
    \n {completedFiles.map((fs) => (\n
  • \n \n {fs.file.name}\n \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:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/multi-file-block.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/multi-image-uploader-block.json b/docs/public/r/multi-image-uploader-block.json index d3e0c6cd..505f5299 100644 --- a/docs/public/r/multi-image-uploader-block.json +++ b/docs/public/r/multi-image-uploader-block.json @@ -11,8 +11,8 @@ { "path": "examples/components/src/components/upload/blocks/multi-image-block.tsx", "content": "'use client';\n\nimport { ImageUploader } from '@/components/upload/multi-image';\nimport {\n UploaderProvider,\n useUploader,\n type CompletedFileState,\n type UploadFn,\n} from '@/components/upload/uploader-provider';\nimport * as React from 'react';\n\nexport default function MultiImageUploaderBlock() {\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.myPublicImages.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 \n
\n
\n );\n}\n\nfunction CompletedFiles() {\n const { fileStates } = useUploader();\n\n const completedFiles = fileStates.filter(\n (fs): fs is CompletedFileState => fs.status === 'COMPLETE',\n );\n\n if (completedFiles.length === 0) {\n return null;\n }\n\n return (\n
\n

Uploaded files

\n
    \n {completedFiles.map((fs) => (\n
  • \n \n {fs.file.name}\n \n
  • \n ))}\n
\n
\n );\n}\n\n// Mock implementation of EdgeStore\nfunction useMockEdgeStore() {\n return {\n edgestore: {\n myPublicImages: {\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:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/multi-image-block.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/progress-bar-block.json b/docs/public/r/progress-bar-block.json index 65ac5e9d..d0235832 100644 --- a/docs/public/r/progress-bar-block.json +++ b/docs/public/r/progress-bar-block.json @@ -10,8 +10,8 @@ { "path": "examples/components/src/components/upload/blocks/progress-bar-block.tsx", "content": "'use client';\n\nimport * as React from 'react';\nimport { ProgressBar } from '../progress-bar';\n\nexport default function ProgressBarUsage() {\n const [progress, setProgress] = React.useState(0);\n\n React.useEffect(() => {\n const startAnimation = () => {\n const interval = setInterval(() => {\n setProgress((prevProgress) => {\n if (prevProgress >= 100) {\n clearInterval(interval);\n // Reset after 1 second when reaching 100%\n setTimeout(() => {\n setProgress(0);\n startAnimation(); // Restart the animation cycle\n }, 1000);\n return 100;\n }\n // Add a random value between 10-20 percent\n return Math.min(\n 100,\n prevProgress + Math.floor(Math.random() * 11) + 10,\n );\n });\n }, 300);\n\n return interval;\n };\n\n const interval = startAnimation();\n\n return () => {\n clearInterval(interval);\n };\n }, []);\n\n return (\n
\n \n
\n );\n}\n", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/progress-bar-block.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/progress-circle-block.json b/docs/public/r/progress-circle-block.json index ea132b32..86d11fd4 100644 --- a/docs/public/r/progress-circle-block.json +++ b/docs/public/r/progress-circle-block.json @@ -10,8 +10,8 @@ { "path": "examples/components/src/components/upload/blocks/progress-circle-block.tsx", "content": "'use client';\n\nimport * as React from 'react';\nimport { ProgressCircle } from '../progress-circle';\n\nexport default function ProgressCircleUsage() {\n const [progress, setProgress] = React.useState(0);\n\n React.useEffect(() => {\n const startAnimation = () => {\n const interval = setInterval(() => {\n setProgress((prevProgress) => {\n if (prevProgress >= 100) {\n clearInterval(interval);\n // Reset after 1 second when reaching 100%\n setTimeout(() => {\n setProgress(0);\n startAnimation(); // Restart the animation cycle\n }, 1000);\n return 100;\n }\n // Add a random value between 10-20 percent\n return Math.min(\n 100,\n prevProgress + Math.floor(Math.random() * 11) + 10,\n );\n });\n }, 300);\n\n return interval;\n };\n\n const interval = startAnimation();\n\n return () => {\n clearInterval(interval);\n };\n }, []);\n\n return (\n
\n
\n \n
\n
\n );\n}\n", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/progress-circle-block.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/registry.json b/docs/public/r/registry.json index 2a4951af..9a5189c4 100644 --- a/docs/public/r/registry.json +++ b/docs/public/r/registry.json @@ -208,8 +208,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/multi-file-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/multi-file-block.tsx" } ] }, @@ -225,8 +225,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/single-image-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/single-image-block.tsx" } ] }, @@ -242,8 +242,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/multi-image-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/multi-image-block.tsx" } ] }, @@ -259,8 +259,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/avatar-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/avatar-block.tsx" } ] }, @@ -277,8 +277,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/file-field-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/file-field-block.tsx" } ] }, @@ -297,8 +297,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/upload-button-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/upload-button-block.tsx" } ] }, @@ -311,8 +311,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/progress-bar-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/progress-bar-block.tsx" } ] }, @@ -325,8 +325,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/progress-circle-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/progress-circle-block.tsx" } ] } diff --git a/docs/public/r/single-image-dropzone-block.json b/docs/public/r/single-image-dropzone-block.json index 30bca9d4..b8f7e699 100644 --- a/docs/public/r/single-image-dropzone-block.json +++ b/docs/public/r/single-image-dropzone-block.json @@ -11,8 +11,8 @@ { "path": "examples/components/src/components/upload/blocks/single-image-block.tsx", "content": "'use client';\n\nimport { SingleImageDropzone } from '@/components/upload/single-image';\nimport {\n UploaderProvider,\n useUploader,\n type CompletedFileState,\n type UploadFn,\n} from '@/components/upload/uploader-provider';\nimport * as React from 'react';\n\nexport default function SingleImageUploaderBlock() {\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.myPublicImages.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
\n );\n}\n\nfunction CompletedFiles() {\n const { fileStates } = useUploader();\n\n const completedFiles = fileStates.filter(\n (fs): fs is CompletedFileState => fs.status === 'COMPLETE',\n );\n\n if (completedFiles.length === 0) {\n return null;\n }\n\n return (\n
\n

Uploaded files

\n
    \n {completedFiles.map((fs) => (\n
  • \n \n {fs.file.name}\n \n
  • \n ))}\n
\n
\n );\n}\n\n// Mock implementation of EdgeStore\nfunction useMockEdgeStore() {\n return {\n edgestore: {\n myPublicImages: {\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:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/single-image-block.tsx" } ], "type": "registry:block" diff --git a/docs/public/r/upload-button-block.json b/docs/public/r/upload-button-block.json index 8cbed335..9cda7a90 100644 --- a/docs/public/r/upload-button-block.json +++ b/docs/public/r/upload-button-block.json @@ -16,8 +16,8 @@ { "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", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/upload-button-block.tsx" } ], "type": "registry:block" diff --git a/registry.json b/registry.json index 2a4951af..9a5189c4 100644 --- a/registry.json +++ b/registry.json @@ -208,8 +208,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/multi-file-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/multi-file-block.tsx" } ] }, @@ -225,8 +225,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/single-image-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/single-image-block.tsx" } ] }, @@ -242,8 +242,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/multi-image-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/multi-image-block.tsx" } ] }, @@ -259,8 +259,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/avatar-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/avatar-block.tsx" } ] }, @@ -277,8 +277,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/file-field-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/file-field-block.tsx" } ] }, @@ -297,8 +297,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/upload-button-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/upload-button-block.tsx" } ] }, @@ -311,8 +311,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/progress-bar-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/progress-bar-block.tsx" } ] }, @@ -325,8 +325,8 @@ "files": [ { "path": "examples/components/src/components/upload/blocks/progress-circle-block.tsx", - "type": "registry:page", - "target": "app/page.tsx" + "type": "registry:block", + "target": "components/upload/blocks/progress-circle-block.tsx" } ] } From 48a8834621252ee20872be3e86310b792ebe1d17 Mon Sep 17 00:00:00 2001 From: Ravi <103035973+raviships@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:42:05 +0900 Subject: [PATCH 05/13] fix: select documentation tabs directly and retain upload feedback --- .../content/docs/components/upload-button.mdx | 28 ++++++++++++++++++- docs/src/components/ui/open-tabs.tsx | 24 ++++++++++------ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/docs/content/docs/components/upload-button.mdx b/docs/content/docs/components/upload-button.mdx index 38d6f82c..5716fc56 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`. To show a file list, also install [Multi-file](./multi-file) and add its `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`. The basic example below shows upload progress and results with `useUploader`, without extra components. For the richer file list used in the demo, also install [Multi-file](./multi-file) and add its `FileList`. ## Installation @@ -91,6 +91,7 @@ import { UploadButton } from '@/components/upload/upload-button'; import { UploaderProvider, type UploadFn, + useUploader, } from '@/components/upload/uploader-provider'; import { useEdgeStore } from '@/lib/edgestore'; import { UploadIcon } from 'lucide-react'; @@ -124,9 +125,34 @@ export function UploadButtonUsage() { Upload files setErrors([])} /> + ); } + +function UploadStatus() { + const { fileStates } = useUploader(); + + return ( +
    + {fileStates.map(({ key, file, status, progress, url, error }) => ( +
  • + {file.name}{' '} + {status === 'COMPLETE' ? ( + Uploaded — open file + ) : status === 'ERROR' ? ( + Upload failed: {error} + ) : ( + <> + + {status === 'PENDING' ? 'Waiting to upload' : `${progress}%`} + + )} +
  • + ))} +
+ ); +} ``` ## Props diff --git a/docs/src/components/ui/open-tabs.tsx b/docs/src/components/ui/open-tabs.tsx index 1a60f32b..b3cb024c 100644 --- a/docs/src/components/ui/open-tabs.tsx +++ b/docs/src/components/ui/open-tabs.tsx @@ -7,8 +7,16 @@ import * as React from 'react'; const OpenTabs = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef ->((props, ref) => { +>(({ value: controlledValue, defaultValue, onValueChange, ...props }, ref) => { const root = React.useRef(null); + const [value, setValue] = React.useState(defaultValue); + const changeValue = React.useCallback( + (nextValue: string) => { + setValue(nextValue); + onValueChange?.(nextValue); + }, + [onValueChange], + ); React.useEffect(() => { let frame = 0; @@ -18,17 +26,14 @@ const OpenTabs = React.forwardRef< const panel = target.closest('[role="tabpanel"]'); if (panel?.getAttribute('data-state') !== 'inactive') return; - const trigger = Array.from( - root.current.querySelectorAll('[role="tab"]'), - ).find((tab) => tab.getAttribute('aria-controls') === panel.id); - // Radix activates a tab when its trigger receives focus. - trigger?.focus(); + const tabValue = panel.getAttribute('data-value'); + if (!tabValue) return; + changeValue(tabValue); frame = requestAnimationFrame(() => target.scrollIntoView()); } function onAnchorClick(event: MouseEvent) { if ( - event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || @@ -51,11 +56,13 @@ const OpenTabs = React.forwardRef< document.removeEventListener('click', onAnchorClick); cancelAnimationFrame(frame); }; - }, []); + }, [changeValue]); return ( { root.current = element; if (typeof ref === 'function') return ref(element); @@ -102,6 +109,7 @@ const OpenTabsContent = React.forwardRef< >(({ className, ...props }, ref) => ( Date: Tue, 13 Oct 2026 19:42:45 +0900 Subject: [PATCH 06/13] fix: restore the upload button file list example --- .../content/docs/components/upload-button.mdx | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/docs/content/docs/components/upload-button.mdx b/docs/content/docs/components/upload-button.mdx index 5716fc56..3ec36b89 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`. The basic example below shows upload progress and results with `useUploader`, without extra components. For the richer file list used in the demo, also install [Multi-file](./multi-file) and add its `FileList`. +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`. ## Installation @@ -87,11 +87,11 @@ Install or copy the component from [Installation](#installation) before using th 'use client'; import { UploadErrors } from '@/components/upload/dropzone'; +import { FileList } from '@/components/upload/multi-file'; import { UploadButton } from '@/components/upload/upload-button'; import { UploaderProvider, type UploadFn, - useUploader, } from '@/components/upload/uploader-provider'; import { useEdgeStore } from '@/lib/edgestore'; import { UploadIcon } from 'lucide-react'; @@ -125,34 +125,10 @@ export function UploadButtonUsage() { Upload files setErrors([])} /> - + ); } - -function UploadStatus() { - const { fileStates } = useUploader(); - - return ( -
    - {fileStates.map(({ key, file, status, progress, url, error }) => ( -
  • - {file.name}{' '} - {status === 'COMPLETE' ? ( - Uploaded — open file - ) : status === 'ERROR' ? ( - Upload failed: {error} - ) : ( - <> - - {status === 'PENDING' ? 'Waiting to upload' : `${progress}%`} - - )} -
  • - ))} -
- ); -} ``` ## Props From 6c999d5f55f4832b36f0f57b4367e4ac62558d22 Mon Sep 17 00:00:00 2001 From: Ravi <103035973+raviships@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:47:20 +0900 Subject: [PATCH 07/13] fix: limit the PR to website link fixes --- docs/src/components/ui/open-tabs.tsx | 73 +--------------------------- 1 file changed, 2 insertions(+), 71 deletions(-) diff --git a/docs/src/components/ui/open-tabs.tsx b/docs/src/components/ui/open-tabs.tsx index b3cb024c..9cafdea3 100644 --- a/docs/src/components/ui/open-tabs.tsx +++ b/docs/src/components/ui/open-tabs.tsx @@ -4,74 +4,7 @@ import { cn } from '@/lib/utils'; import * as TabsPrimitive from '@radix-ui/react-tabs'; import * as React from 'react'; -const OpenTabs = React.forwardRef< - React.ComponentRef, - React.ComponentPropsWithoutRef ->(({ value: controlledValue, defaultValue, onValueChange, ...props }, ref) => { - const root = React.useRef(null); - const [value, setValue] = React.useState(defaultValue); - const changeValue = React.useCallback( - (nextValue: string) => { - setValue(nextValue); - onValueChange?.(nextValue); - }, - [onValueChange], - ); - - React.useEffect(() => { - let frame = 0; - function revealAnchor() { - const target = document.getElementById(window.location.hash.slice(1)); - if (!target || !root.current?.contains(target)) return; - const panel = target.closest('[role="tabpanel"]'); - if (panel?.getAttribute('data-state') !== 'inactive') return; - - const tabValue = panel.getAttribute('data-value'); - if (!tabValue) return; - changeValue(tabValue); - frame = requestAnimationFrame(() => target.scrollIntoView()); - } - - function onAnchorClick(event: MouseEvent) { - if ( - event.button !== 0 || - event.metaKey || - event.ctrlKey || - event.shiftKey || - event.altKey - ) { - return; - } - const anchor = - event.target instanceof Element ? event.target.closest('a') : null; - // Clicking the current anchor does not emit another hashchange. - if (anchor?.href === window.location.href) revealAnchor(); - } - - revealAnchor(); - window.addEventListener('hashchange', revealAnchor); - document.addEventListener('click', onAnchorClick); - return () => { - window.removeEventListener('hashchange', revealAnchor); - document.removeEventListener('click', onAnchorClick); - cancelAnimationFrame(frame); - }; - }, [changeValue]); - - return ( - { - root.current = element; - if (typeof ref === 'function') return ref(element); - if (ref) ref.current = element; - }} - /> - ); -}); -OpenTabs.displayName = TabsPrimitive.Root.displayName; +const OpenTabs = TabsPrimitive.Root; const OpenTabsList = React.forwardRef< React.ComponentRef, @@ -109,10 +42,8 @@ const OpenTabsContent = React.forwardRef< >(({ className, ...props }, ref) => ( Date: Tue, 13 Oct 2026 20:38:07 +0900 Subject: [PATCH 08/13] 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 `
    ` 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 `
\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 ( +