feat(upload): add paste-to-upload functionality (#145)#146
feat(upload): add paste-to-upload functionality (#145)#146abhay-ramesh wants to merge 3 commits intomainfrom
Conversation
- Add usePasteUpload hook for handling paste events - Create paste-upload demo page - Add paste-to-upload guide documentation - Add upload-paste UI component - Update client API documentation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds a complete paste-to-upload feature to the pushduck library, including a new Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Component as UploadPaste<br/>Component
participant Hook as usePasteUpload<br/>Hook
participant UploadRoute as useUploadRoute<br/>Hook
participant Clipboard as Clipboard API
participant S3 as S3 Upload<br/>Service
rect rgb(200, 220, 255)
Note over User,S3: Paste-to-Upload Flow (Preview Mode)
end
User->>Component: Pastes image(s) from clipboard
Component->>Hook: onPaste event triggered
Hook->>Clipboard: Read clipboard data
Clipboard-->>Hook: File objects extracted
rect rgb(220, 240, 220)
Note over Hook: Validation Phase
end
Hook->>Hook: Validate files (size, count, type, custom)
activate Hook
Hook->>Hook: Create preview blob URLs
Hook->>Component: Update previewFiles state
deactivate Hook
Component->>User: Show preview grid
User->>Component: Clicks upload action
Component->>Hook: uploadPastedFiles() called
rect rgb(255, 240, 200)
Note over Hook,UploadRoute: Upload Phase
end
Hook->>UploadRoute: useUploadRoute upload(files)
UploadRoute->>S3: POST presigned URL request
S3-->>UploadRoute: Return presigned URLs
UploadRoute->>S3: Upload files via presigned URLs
S3-->>UploadRoute: Upload complete
UploadRoute->>Hook: onUploadComplete callback
Hook->>Component: Update files, clear previews
Component->>User: Show uploaded files, cleanup
rect rgb(200, 220, 255)
Note over User,S3: Paste-to-Upload Flow (Immediate Mode)
end
User->>Component: Pastes image(s)
Component->>Hook: onPaste event triggered
Hook->>Hook: Validate & create previews
Hook->>Hook: Auto-upload enabled (immediate mode)
Hook->>UploadRoute: Auto-trigger upload
UploadRoute->>S3: Upload files immediately
S3-->>UploadRoute: Upload complete
Hook->>Component: Update files, show progress
Component->>User: Show uploaded files directly
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes The review requires careful assessment of the new hook's clipboard handling, validation logic, preview management, lifecycle cleanup, integration with useUploadRoute, and the supporting UI component. Extensive documentation and demo implementations add context but moderate the complexity. Multiple integration points with existing upload infrastructure demand attention to ensure compatibility and proper state management. Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| }); | ||
| }, | ||
| [] | ||
| ); |
There was a problem hiding this comment.
Undefined function causes runtime error on preview removal
The handleRemovePreview callback calls setPreviewFiles which is not defined anywhere in the component. The usePasteUpload hook returns previewFiles (read-only array) and clearPreviews (clears all), but does not expose a setter function for individual removal. When a user clicks the X button to remove a single preview image, this causes a runtime error: setPreviewFiles is not defined.
| </div> | ||
| )} | ||
|
|
||
| {!displayPreviews && previewFiles.length === 0 && ( |
There was a problem hiding this comment.
Empty state never displays in preview mode
The condition !displayPreviews && previewFiles.length === 0 prevents the empty state from displaying when in preview mode with no files. Since displayPreviews defaults to true in preview mode, and previewFiles.length is 0 initially, the condition evaluates to false && true = false. This leaves users with an empty dashed border container and no "Paste to upload" instructions. The condition likely intended to show the empty state when there are no previews.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
docs/app/paste-upload/page.tsx (1)
29-78: Consider extracting repeated button styling.The demo selector buttons share identical styling logic. While acceptable for a demo page, extracting this to a helper or using a common component could reduce duplication.
🔎 Optional refactor
const DemoButton = ({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode; }) => ( <button onClick={onClick} className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${ active ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200" }`} > {children} </button> );packages/ui/registry/default/upload-paste/upload-paste.tsx (1)
32-32: Use a specific type instead ofany[]for upload results.The static analysis correctly flags this. Consider using the
S3UploadedFile[]type from pushduck for better type safety.🔎 Suggested fix
+import type { S3UploadedFile } from "pushduck/client"; + export interface UploadPasteProps { // ... /** Callback when upload completes */ - onUploadComplete?: (results: any[]) => void; + onUploadComplete?: (results: S3UploadedFile[]) => void;packages/pushduck/src/hooks/use-paste-upload.ts (1)
475-487: Consider exposing aremovePreviewfunction for individual preview removal.The
UploadPastecomponent attempts to usesetPreviewFilesto remove individual previews, but this hook only exposesclearPreviews(removes all). Adding aremovePreview(id: string)function would enable better UX.🔎 Suggested addition
+ // Remove a single preview + const removePreview = useCallback((id: string) => { + setPreviewFiles((prev) => { + const file = prev.find((f) => f.id === id); + if (file) { + URL.revokeObjectURL(file.preview); + previewUrlsRef.current.delete(file.preview); + } + return prev.filter((f) => f.id !== id); + }); + }, []); return { previewFiles, handlePaste, uploadPastedFiles, clearPreviews, + removePreview, files, uploadFiles, isUploading, progress, uploadSpeed, eta, errors, };Don't forget to update
UsePasteUploadResultinterface to includeremovePreview: (id: string) => void.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
docs/app/paste-upload/page.tsxdocs/content/docs/api/client/meta.jsondocs/content/docs/api/client/use-paste-upload.mdxdocs/content/docs/guides/index.mdxdocs/content/docs/guides/meta.jsondocs/content/docs/guides/paste-to-upload-manual.mdxdocs/content/docs/guides/paste-to-upload.mdxdocs/lib/upload.tspackages/pushduck/src/client.tspackages/pushduck/src/hooks/index.tspackages/pushduck/src/hooks/use-paste-upload.tspackages/ui/public/r/registry.jsonpackages/ui/public/r/upload-paste.jsonpackages/ui/registry.jsonpackages/ui/registry/default/upload-paste/upload-paste.tsx
💤 Files with no reviewable changes (1)
- docs/lib/upload.ts
🧰 Additional context used
🧬 Code graph analysis (2)
packages/ui/registry/default/upload-paste/upload-paste.tsx (2)
packages/pushduck/src/hooks/use-paste-upload.ts (1)
usePasteUpload(278-488)packages/ui/lib/utils.ts (1)
cn(4-6)
docs/app/paste-upload/page.tsx (3)
packages/pushduck/src/client.ts (1)
usePasteUpload(212-212)packages/pushduck/src/hooks/index.ts (1)
usePasteUpload(16-16)packages/pushduck/src/hooks/use-paste-upload.ts (1)
usePasteUpload(278-488)
🪛 GitHub Actions: 🔍 Continuous Integration
packages/ui/registry/default/upload-paste/upload-paste.tsx
[error] 115-115: Cannot find name 'setPreviewFiles'. Did you mean 'previewFiles'? (Command: pnpm --filter="./packages/*" type-check)
🪛 GitHub Check: 🛡️ Quality Gates
packages/ui/registry/default/upload-paste/upload-paste.tsx
[warning] 32-32:
Unexpected any. Specify a different type
🪛 LanguageTool
docs/content/docs/guides/paste-to-upload-manual.mdx
[style] ~487-~487: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...ze every aspect - 📚 Learning - You want to understand how it works - 🧩 **Integrat...
(REP_WANT_TO_VB)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
🔇 Additional comments (14)
docs/content/docs/api/client/meta.json (1)
5-6: LGTM! Correct API documentation registration.The new "use-paste-upload" entry is correctly added to the client API documentation index, maintaining proper JSON structure and logical ordering after "use-upload-route".
docs/content/docs/guides/index.mdx (1)
51-63: LGTM! Well-structured guide card.The new "Paste to Upload" card is properly formatted and positioned within the Upload Patterns section. The description and bullet points clearly communicate the feature's capabilities (clipboard paste detection, modes, scopes, and input support).
docs/content/docs/guides/meta.json (1)
6-7: LGTM! Correct guide registration.Both new guide entries ("paste-to-upload" and "paste-to-upload-manual") are correctly registered in the guides metadata with proper JSON structure and logical positioning.
packages/pushduck/src/client.ts (1)
212-212: LGTM! Correct public API extension.The addition of
usePasteUploadto the client exports is properly implemented using named export syntax, maintaining consistency with the existinguseUploadRouteexport pattern.packages/pushduck/src/hooks/index.ts (2)
15-16: LGTM! Correct hook export.The
usePasteUploadhook is properly exported with appropriate documentation comments, following the same pattern as the existinguseUploadRouteexport.
26-31: LGTM! Proper type exports.The paste upload types (
PasteFilePreview,UsePasteUploadConfig,UsePasteUploadResult) are correctly exported in a dedicated section with clear documentation, maintaining consistency with the codebase organization.packages/ui/registry.json (1)
101-120: LGTM! Well-structured registry entry.The "upload-paste" component is properly registered with complete metadata including clear description of both modes (immediate and preview), correct dependencies (pushduck, lucide-react), and appropriate registry dependencies (button, progress). The structure follows the established pattern of other components in the registry.
docs/content/docs/api/client/use-paste-upload.mdx (1)
1-548: Comprehensive and well-structured API documentation.The documentation provides excellent coverage of the
usePasteUploadhook with:
- Clear explanations of immediate vs. preview modes
- Well-organized configuration and return value tables
- Multiple practical examples covering chat, form, and validation scenarios
- Important notes on memory management and browser support
The structure follows good documentation practices with progressive complexity from basic to advanced examples.
docs/content/docs/guides/paste-to-upload-manual.mdx (1)
1-588: Excellent manual implementation guide with proper patterns.This comprehensive guide demonstrates best practices for implementing paste-to-upload functionality:
- Correct clipboard API usage with proper file extraction
- Proper memory management with URL.revokeObjectURL
- Correct event listener cleanup to prevent leaks
- Input field detection with appropriate event.preventDefault() usage
- Container scope implementation using Node.contains()
- Well-structured comparison table showing when to use manual vs. hook approach
The code examples follow React best practices and include all necessary cleanup logic.
docs/content/docs/guides/paste-to-upload.mdx (1)
1-539: Well-structured documentation with comprehensive coverage.The guide covers all key use cases (chat, forms, galleries), configuration options, and includes helpful troubleshooting tips. The browser support callout about HTTPS requirement is a nice touch.
docs/app/paste-upload/page.tsx (1)
1-582: Demo page effectively showcases all paste-upload features.The five demos provide comprehensive coverage of the hook's capabilities: immediate vs preview mode, document vs container scope, and input field paste detection. Good UX with clear instructions in each demo section.
packages/ui/public/r/upload-paste.json (1)
1-22: Registry JSON contains the same bug as source file.The embedded component code in the
contentfield referencessetPreviewFileswhich doesn't exist (the hook returnspreviewFilesbut not its setter). This JSON will need to be regenerated after fixing the source component atpackages/ui/registry/default/upload-paste/upload-paste.tsx.packages/ui/public/r/registry.json (1)
1-122: Registry structure is consistent and well-organized.The
upload-pasteentry follows the same pattern as other components with appropriate dependencies (pushduck,lucide-react) and registry dependencies (button,progress).packages/pushduck/src/hooks/use-paste-upload.ts (1)
1-488: Well-implemented hook with comprehensive features.The hook provides a clean API for paste-to-upload with good separation between immediate and preview modes. The blob URL cleanup on unmount prevents memory leaks, and the validation logic is thorough. Documentation is excellent with JSDoc examples.
| ```typescript | ||
| import { usePasteUpload } from "pushduck/client"; | ||
|
|
||
| export function ValidatedUpload() { | ||
| const { previewFiles, uploadPastedFiles } = usePasteUpload("imageUpload", { | ||
| accept: "image/*", | ||
| mode: "preview", | ||
| validator: (files) => { | ||
| // Custom validation logic | ||
| for (const file of files) { | ||
| // Check image dimensions | ||
| return new Promise((resolve) => { | ||
| const img = new Image(); | ||
| img.onload = () => { | ||
| if (img.width < 800 || img.height < 600) { | ||
| resolve("Image must be at least 800x600 pixels"); | ||
| } else { | ||
| resolve(null); | ||
| } | ||
| }; | ||
| img.src = URL.createObjectURL(file); | ||
| }); | ||
| } | ||
| return null; | ||
| }, | ||
| }); | ||
|
|
||
| return ( | ||
| <div> | ||
| {previewFiles.map((preview) => ( | ||
| <img key={preview.id} src={preview.preview} alt={preview.name} /> | ||
| ))} | ||
| <button onClick={uploadPastedFiles}>Upload</button> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "use-paste-upload.ts" | head -5Repository: abhay-ramesh/pushduck
Length of output: 114
🏁 Script executed:
cat -n ./packages/pushduck/src/hooks/use-paste-upload.ts | head -100Repository: abhay-ramesh/pushduck
Length of output: 3761
🏁 Script executed:
cat -n ./packages/pushduck/src/hooks/use-paste-upload.ts | sed -n '100,200p'Repository: abhay-ramesh/pushduck
Length of output: 3779
🏁 Script executed:
cat -n ./packages/pushduck/src/hooks/use-paste-upload.ts | sed -n '187,192p'Repository: abhay-ramesh/pushduck
Length of output: 190
🏁 Script executed:
cat -n ./packages/pushduck/src/hooks/use-paste-upload.ts | sed -n '165,192p'Repository: abhay-ramesh/pushduck
Length of output: 946
Fix the custom validation example to use synchronous validation.
The documentation example at lines 485-502 shows an async validator that returns a Promise, but the actual implementation in packages/pushduck/src/hooks/use-paste-upload.ts (line 113) defines the validator type as (files: File[]) => string | null (synchronous). The implementation calls the validator without awaiting (line 188), so async validators are not supported.
Update the example to use synchronous validation that matches the implementation:
validator: (files) => {
for (const file of files) {
// Check file size synchronously
if (file.size > 5 * 1024 * 1024) {
return "File must be smaller than 5MB";
}
}
return null;
}🤖 Prompt for AI Agents
In docs/content/docs/api/client/use-paste-upload.mdx around lines 478 to 514,
the example uses an asynchronous Promise-based validator but the hook expects a
synchronous validator (returns string | null) and does not await; replace the
async/image-load logic with a synchronous validation implementation (e.g.,
iterate files and perform immediate checks like file.size or file.type,
returning an error string on first failure and null if all pass), remove any
Promise usage and URL.createObjectURL usage so the validator matches the hook's
signature and behavior.
| ```typescript | ||
| import { usePasteUpload } from "pushduck/client"; | ||
|
|
||
| export function ImageGallery() { | ||
| const galleryRef = useRef<HTMLDivElement>(null); | ||
| const { | ||
| previewFiles, | ||
| uploadPastedFiles, | ||
| clearPreviews, | ||
| files, | ||
| isUploading, | ||
| progress, | ||
| } = usePasteUpload("galleryImages", { | ||
| accept: "image/*", | ||
| mode: "preview", | ||
| scope: "container", | ||
| containerRef: galleryRef, | ||
| maxFiles: 20, | ||
| onPaste: (files) => { | ||
| toast.info(`Pasted ${files.length} images`); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Missing React import in code example.
The Image Gallery example uses useRef on line 254 but doesn't include the React import statement. This will cause confusion for users copying the code.
🔎 Suggested fix
```typescript
import { usePasteUpload } from "pushduck/client";
+import { useRef } from "react";
export function ImageGallery() {
const galleryRef = useRef<HTMLDivElement>(null);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ```typescript | |
| import { usePasteUpload } from "pushduck/client"; | |
| export function ImageGallery() { | |
| const galleryRef = useRef<HTMLDivElement>(null); | |
| const { | |
| previewFiles, | |
| uploadPastedFiles, | |
| clearPreviews, | |
| files, | |
| isUploading, | |
| progress, | |
| } = usePasteUpload("galleryImages", { | |
| accept: "image/*", | |
| mode: "preview", | |
| scope: "container", | |
| containerRef: galleryRef, | |
| maxFiles: 20, | |
| onPaste: (files) => { | |
| toast.info(`Pasted ${files.length} images`); | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In docs/content/docs/guides/paste-to-upload.mdx around lines 250 to 271, the
example uses useRef but does not import it from React; add an import for useRef
from "react" at the top of the snippet (e.g., import { useRef } from "react") so
the example compiles for users copying it.
| return acceptedTypes.some((type) => { | ||
| if (type.startsWith(".")) { | ||
| return file.name.toLowerCase().endsWith(type.toLowerCase()); | ||
| } | ||
| // Handle MIME type patterns like "image/*" | ||
| const pattern = type.replace("*", ".*"); | ||
| return file.type.match(pattern); | ||
| }); | ||
| } |
There was a problem hiding this comment.
MIME pattern matching may fail for complex types.
The regex pattern type.replace("*", ".*") converts image/* to image/.*, but this doesn't account for MIME types with special characters like image/svg+xml where + has regex meaning.
🔎 Suggested fix
function matchesAccept(file: File, accept?: string): boolean {
if (!accept) return true;
const acceptedTypes = accept.split(",").map((type) => type.trim());
return acceptedTypes.some((type) => {
if (type.startsWith(".")) {
return file.name.toLowerCase().endsWith(type.toLowerCase());
}
// Handle MIME type patterns like "image/*"
- const pattern = type.replace("*", ".*");
- return file.type.match(pattern);
+ if (type.includes("*")) {
+ const [mainType] = type.split("/");
+ return file.type.startsWith(mainType + "/");
+ }
+ return file.type === type;
});
}🤖 Prompt for AI Agents
In packages/pushduck/src/hooks/use-paste-upload.ts around lines 154 to 162, the
MIME wildcard pattern is built by naive string replacement which fails for types
containing regex metacharacters (e.g. image/svg+xml) — escape regex
metacharacters in the type string before converting the wildcard; specifically,
escape all regex special chars (., +, ?, ^, $, [, ], (, ), {, }, |, \, etc.),
then replace the escaped "\*" with ".*", build a RegExp anchored with ^ and $
(and case-insensitive flag) and use that to test file.type so patterns like
"image/*" and types containing "+" match correctly.
| // Remove preview | ||
| const handleRemovePreview = React.useCallback( | ||
| (id: string) => { | ||
| setPreviewFiles((prev) => { | ||
| const file = prev.find((f) => f.id === id); | ||
| if (file) { | ||
| URL.revokeObjectURL(file.preview); | ||
| } | ||
| return prev.filter((f) => f.id !== id); | ||
| }); | ||
| }, | ||
| [] | ||
| ); |
There was a problem hiding this comment.
Critical: setPreviewFiles is not available from the hook.
This causes the pipeline failure. The usePasteUpload hook returns previewFiles (read-only), clearPreviews, and uploadPastedFiles, but does not expose setPreviewFiles. The handleRemovePreview function cannot work as implemented.
🔎 Recommended fix: Remove individual preview removal or add it to the hook
Option 1: Remove the feature (quick fix)
- // Remove preview
- const handleRemovePreview = React.useCallback(
- (id: string) => {
- setPreviewFiles((prev) => {
- const file = prev.find((f) => f.id === id);
- if (file) {
- URL.revokeObjectURL(file.preview);
- }
- return prev.filter((f) => f.id !== id);
- });
- },
- []
- );And remove the remove button in the preview grid (lines 207-216).
Option 2: Add removePreview to the hook (better UX)
In packages/pushduck/src/hooks/use-paste-upload.ts, add:
const removePreview = useCallback((id: string) => {
setPreviewFiles((prev) => {
const file = prev.find((f) => f.id === id);
if (file) {
URL.revokeObjectURL(file.preview);
previewUrlsRef.current.delete(file.preview);
}
return prev.filter((f) => f.id !== id);
});
}, []);Then export it in the return object and use it here instead.
Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 GitHub Actions: 🔍 Continuous Integration
[error] 115-115: Cannot find name 'setPreviewFiles'. Did you mean 'previewFiles'? (Command: pnpm --filter="./packages/*" type-check)
🤖 Prompt for AI Agents
packages/ui/registry/default/upload-paste/upload-paste.tsx lines 112-124: the
component calls setPreviewFiles which is not provided by usePasteUpload (hook
only returns previewFiles, clearPreviews, uploadPastedFiles), so
handleRemovePreview must be changed; either remove the per-preview remove UI and
delete its button/handler (quick fix: remove the remove button at lines ~207-216
and any references to handleRemovePreview), or preferably add a
removePreview(id: string) function to the hook in
packages/pushduck/src/hooks/use-paste-upload.ts that mirrors the logic here
(revoke the object URL, delete the URL from previewUrlsRef.current, and filter
out the preview from state), export it from the hook, and replace
setPreviewFiles usage in this file with the new removePreview call.
| {!displayPreviews && previewFiles.length === 0 && ( | ||
| <div className="flex flex-col items-center justify-center space-y-4 text-center"> | ||
| {children || ( | ||
| <> | ||
| <div className="rounded-full bg-muted p-4"> | ||
| <ImageIcon className="h-8 w-8 text-muted-foreground" /> | ||
| </div> | ||
| <div className="space-y-2"> | ||
| <h3 className="text-lg font-medium">Paste to upload</h3> | ||
| <p className="text-sm text-muted-foreground"> | ||
| {scope === "document" | ||
| ? "Paste images anywhere on the page" | ||
| : "Paste images in this area"} | ||
| </p> | ||
| {accept && ( | ||
| <p className="text-xs text-muted-foreground"> | ||
| Accepted: {accept} | ||
| </p> | ||
| )} | ||
| <p className="text-xs text-muted-foreground"> | ||
| Max size: {formatFileSize(maxSize)} | ||
| </p> | ||
| </div> | ||
| </> | ||
| )} | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
Logic issue: Empty state condition is inverted.
The condition !displayPreviews && previewFiles.length === 0 will show the empty state when previews are disabled AND no previews exist. However, the empty state should likely show when no previews exist regardless of the displayPreviews flag, or only when displayPreviews is true but no previews exist yet.
🔎 Suggested fix
- {!displayPreviews && previewFiles.length === 0 && (
+ {previewFiles.length === 0 && !isUploading && (Or if the intent is to hide the empty state in immediate mode:
- {!displayPreviews && previewFiles.length === 0 && (
+ {displayPreviews && previewFiles.length === 0 && (📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {!displayPreviews && previewFiles.length === 0 && ( | |
| <div className="flex flex-col items-center justify-center space-y-4 text-center"> | |
| {children || ( | |
| <> | |
| <div className="rounded-full bg-muted p-4"> | |
| <ImageIcon className="h-8 w-8 text-muted-foreground" /> | |
| </div> | |
| <div className="space-y-2"> | |
| <h3 className="text-lg font-medium">Paste to upload</h3> | |
| <p className="text-sm text-muted-foreground"> | |
| {scope === "document" | |
| ? "Paste images anywhere on the page" | |
| : "Paste images in this area"} | |
| </p> | |
| {accept && ( | |
| <p className="text-xs text-muted-foreground"> | |
| Accepted: {accept} | |
| </p> | |
| )} | |
| <p className="text-xs text-muted-foreground"> | |
| Max size: {formatFileSize(maxSize)} | |
| </p> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| )} | |
| {displayPreviews && previewFiles.length === 0 && ( | |
| <div className="flex flex-col items-center justify-center space-y-4 text-center"> | |
| {children || ( | |
| <> | |
| <div className="rounded-full bg-muted p-4"> | |
| <ImageIcon className="h-8 w-8 text-muted-foreground" /> | |
| </div> | |
| <div className="space-y-2"> | |
| <h3 className="text-lg font-medium">Paste to upload</h3> | |
| <p className="text-sm text-muted-foreground"> | |
| {scope === "document" | |
| ? "Paste images anywhere on the page" | |
| : "Paste images in this area"} | |
| </p> | |
| {accept && ( | |
| <p className="text-xs text-muted-foreground"> | |
| Accepted: {accept} | |
| </p> | |
| )} | |
| <p className="text-xs text-muted-foreground"> | |
| Max size: {formatFileSize(maxSize)} | |
| </p> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| )} |
🤖 Prompt for AI Agents
In packages/ui/registry/default/upload-paste/upload-paste.tsx around lines
231-257, the empty-state conditional is inverted (currently `!displayPreviews &&
previewFiles.length === 0`); replace it with the correct intent: if you want the
empty state to show whenever there are no previews, use `previewFiles.length ===
0`; if you want to hide the empty state in immediate/preview-disabled mode, use
`displayPreviews && previewFiles.length === 0`; update the conditional
accordingly and ensure related UI logic (children rendering and tests) matches
the chosen behavior.
| } | ||
| // Handle MIME type patterns like "image/*" | ||
| const pattern = type.replace("*", ".*"); | ||
| return file.type.match(pattern); |
There was a problem hiding this comment.
Accept pattern */* crashes with invalid regex
High Severity
The matchesAccept function uses type.replace("*", ".*") which only replaces the first asterisk. When accept is set to */* (a common MIME pattern meaning "accept any file type"), the pattern becomes .*/* where the second * is an invalid regex quantifier. Calling file.type.match(".*/*") throws a SyntaxError: Invalid regular expression, crashing the paste handler.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| const sizes = ["Bytes", "KB", "MB", "GB"]; | ||
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; | ||
| } |
There was a problem hiding this comment.
Duplicated formatFileSize utility function across files
Medium Severity
The formatFileSize function is identically duplicated in both new files (use-paste-upload.ts and upload-paste.tsx), and also exists in packages/ui/registry/default/file-list/file-list.tsx. This utility should be extracted to a shared module or exported from pushduck/client to avoid maintaining multiple copies.
| ): string | null { | ||
| if (config.maxFiles && files.length > config.maxFiles) { | ||
| return `Maximum ${config.maxFiles} files allowed`; | ||
| } |
There was a problem hiding this comment.
maxFiles validation ignores accumulated previews in preview mode
Medium Severity
The maxFiles validation only checks the count of files in the current paste event (files.length > config.maxFiles), not the total accumulated previews. In preview mode, previews accumulate across multiple pastes via setPreviewFiles((prev) => [...prev, ...previews]). Users can bypass the maxFiles limit by pasting files multiple times (e.g., with maxFiles: 5, pasting 3 files twice results in 6 previews).


Summary
This PR introduces comprehensive paste-to-upload functionality, allowing users to upload images by pasting from their clipboard. The feature is inspired by WhatsApp Web's paste-to-upload UX pattern and includes both immediate upload mode (for chat interfaces) and preview mode (for controlled uploads).
Features
Core Functionality
usePasteUploadhook: React hook for handling paste events and managing upload stateUI Component
UploadPastecomponent: Ready-to-use UI component with built-in preview and upload functionalityDocumentation
usePasteUploadhookChanges
New Files
packages/pushduck/src/hooks/use-paste-upload.ts- Core hook implementation (489 lines)docs/app/paste-upload/page.tsx- Interactive demo page (582 lines)docs/content/docs/api/client/use-paste-upload.mdx- API documentation (548 lines)docs/content/docs/guides/paste-to-upload.mdx- Main guide (539 lines)docs/content/docs/guides/paste-to-upload-manual.mdx- Manual implementation guide (588 lines)packages/ui/registry/default/upload-paste/upload-paste.tsx- UI component (333 lines)Modified Files
packages/pushduck/src/client.ts- Export new hookpackages/pushduck/src/hooks/index.ts- Export usePasteUploaddocs/content/docs/api/client/meta.json- Add API documentation entrydocs/content/docs/guides/meta.json- Add guide entriesdocs/content/docs/guides/index.mdx- Add guide linkspackages/ui/registry.json- Register new componentdocs/lib/upload.ts- Minor cleanupUsage Examples
Immediate Mode (Chat Interface)
Preview Mode (Form Upload)
Configuration Options
The hook supports extensive configuration:
accept,maxSize,maxFiles, customvalidatorenabled,scope,containerRef,allowInputPastemode,autoUpload,endpoint,routeonPaste,onPreview,onUploadStart,onUploadComplete,onUploadErrorTesting
/paste-uploadwith 5 different demo scenarios:Documentation
Breaking Changes
None - this is a new feature addition.
Checklist
Related Issues
Closes #[issue-number]
Stats: 15 files changed, 3,271 insertions(+), 3 deletions(-)
Note
Medium Risk
Mostly additive, but introduces new clipboard event-listener behavior (including optional document-level scope) and changes the docs upload defaults by removing
public-readACL, which could alter upload accessibility in the docs environment.Overview
Adds a new
usePasteUploadclient hook that listens for clipboard paste events, validates pasted files, and either uploads immediately (chat-style) or stages previews for manual upload, with support for document- vs container-scoped detection and optional input/textarea handling.Introduces a reusable
UploadPasteUI component and registers it in the UI registry, plus extensive documentation and an interactive/paste-uploaddemo covering the supported modes/scopes. Also updates the docs upload config to drop the defaultacl: "public-read".Written by Cursor Bugbot for commit 1d0fe06. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.