Skip to content

feat(upload): add paste-to-upload functionality (#145)#146

Open
abhay-ramesh wants to merge 3 commits intomainfrom
feat/upload-on-paste
Open

feat(upload): add paste-to-upload functionality (#145)#146
abhay-ramesh wants to merge 3 commits intomainfrom
feat/upload-on-paste

Conversation

@abhay-ramesh
Copy link
Copy Markdown
Owner

@abhay-ramesh abhay-ramesh commented Dec 30, 2025

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

  • usePasteUpload hook: React hook for handling paste events and managing upload state
  • Two upload modes:
    • Immediate mode: Automatically uploads files when pasted (ideal for chat interfaces)
    • Preview mode: Shows previews before manual upload confirmation (ideal for forms)
  • Flexible scoping:
    • Document scope: Detects paste events anywhere on the page
    • Container scope: Detects paste events only within a specific container element
  • Input field support: Configurable paste detection in input/textarea fields
  • Image preview: Automatic preview generation for pasted images using blob URLs

UI Component

  • UploadPaste component: Ready-to-use UI component with built-in preview and upload functionality
  • Fully integrated with the pushduck UI registry system

Documentation

  • Complete API documentation for usePasteUpload hook
  • Comprehensive guide with examples for different use cases
  • Manual implementation guide for custom integrations
  • Interactive demo page showcasing all features

Changes

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 hook
  • packages/pushduck/src/hooks/index.ts - Export usePasteUpload
  • docs/content/docs/api/client/meta.json - Add API documentation entry
  • docs/content/docs/guides/meta.json - Add guide entries
  • docs/content/docs/guides/index.mdx - Add guide links
  • packages/ui/registry.json - Register new component
  • docs/lib/upload.ts - Minor cleanup

Usage Examples

Immediate Mode (Chat Interface)

import { usePasteUpload } from 'pushduck/client';

function ChatApp() {
  const { files, isUploading } = usePasteUpload('imageUpload', {
    accept: 'image/*',
    mode: 'immediate',
    scope: 'document',
    allowInputPaste: true,
  });

  return (
    <div>
      <input type="text" placeholder="Type a message..." />
      {isUploading && <div>Uploading...</div>}
      {files.map(file => (
        <img key={file.id} src={file.url} alt={file.name} />
      ))}
    </div>
  );
}

Preview Mode (Form Upload)

function FormComponent() {
  const containerRef = useRef<HTMLDivElement>(null);
  const { previewFiles, uploadPastedFiles, isUploading } = usePasteUpload('imageUpload', {
    accept: 'image/*',
    mode: 'preview',
    scope: 'container',
    containerRef,
  });

  return (
    <div ref={containerRef}>
      {previewFiles.map(file => (
        <div key={file.id}>
          <img src={file.preview} alt={file.name} />
          <button onClick={uploadPastedFiles}>Upload</button>
        </div>
      ))}
    </div>
  );
}

Configuration Options

The hook supports extensive configuration:

  • File validation: accept, maxSize, maxFiles, custom validator
  • Paste detection: enabled, scope, containerRef, allowInputPaste
  • Upload behavior: mode, autoUpload, endpoint, route
  • Callbacks: onPaste, onPreview, onUploadStart, onUploadComplete, onUploadError

Testing

  • Interactive demo page available at /paste-upload with 5 different demo scenarios:
    1. Chat (Immediate) - Document scope with immediate upload
    2. Form (Preview) - Container scope with preview mode
    3. Document Scope - Paste anywhere on page
    4. Container Scope - Paste only in specific container
    5. Input Paste - Paste detection in input fields

Documentation

  • ✅ API reference documentation
  • ✅ Comprehensive usage guide
  • ✅ Manual implementation guide
  • ✅ Interactive demo page
  • ✅ TypeScript type definitions
  • ✅ Code examples for all use cases

Breaking Changes

None - this is a new feature addition.

Checklist

  • Code follows project style guidelines
  • TypeScript types are properly defined
  • Documentation is complete
  • Demo page is functional
  • UI component is registered in registry
  • Hook is exported from main client entry
  • All file previews are properly cleaned up (blob URL revocation)

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-read ACL, which could alter upload accessibility in the docs environment.

Overview
Adds a new usePasteUpload client 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 UploadPaste UI component and registers it in the UI registry, plus extensive documentation and an interactive /paste-upload demo covering the supported modes/scopes. Also updates the docs upload config to drop the default acl: "public-read".

Written by Cursor Bugbot for commit 1d0fe06. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Added paste-to-upload functionality with image preview support
    • Supports immediate upload and preview modes for flexible workflows
    • Document and container-scoped paste detection options
    • New UploadPaste UI component for quick integration
    • Input field paste support for enhanced user experience
  • Documentation

    • Added comprehensive paste-to-upload guide and API documentation
    • Added manual implementation guide for custom implementations
    • Added interactive demo page showcasing multiple usage patterns

✏️ Tip: You can customize this high-level summary in your review settings.

- 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
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Dec 30, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pushduck Canceled Canceled Feb 5, 2026 8:24pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 30, 2025

📝 Walkthrough

Walkthrough

Adds a complete paste-to-upload feature to the pushduck library, including a new usePasteUpload React hook supporting immediate and preview modes, an UploadPaste UI component, comprehensive guides and API documentation, demo implementations, and component registry entries.

Changes

Cohort / File(s) Summary
Hook Implementation
packages/pushduck/src/hooks/use-paste-upload.ts
New usePasteUpload hook with clipboard detection, file validation (size, count, type, custom), preview management with blob URLs, and integration with useUploadRoute for both immediate and preview modes. Includes scope control (document/container), input-field paste suppression, lifecycle cleanup, and multiple callbacks (onPaste, onPreview, onUploadStart/Complete/Error).
Hook Exports
packages/pushduck/src/hooks/index.ts,
packages/pushduck/src/client.ts
Export new usePasteUpload hook and types (PasteFilePreview, UsePasteUploadConfig, UsePasteUploadResult) to public API.
UI Component
packages/ui/registry/default/upload-paste/upload-paste.tsx
New UploadPaste component wrapping usePasteUpload hook, rendering paste area with configurable preview grid, upload/clear actions, progress indicator, uploaded file list, and error display. Supports both immediate and preview modes with formatFileSize helper.
Registry Metadata
packages/ui/registry.json,
packages/ui/public/r/registry.json,
packages/ui/public/r/upload-paste.json
Add upload-paste component entries to UI registries with dependencies (pushduck, lucide-react) and registry dependencies (button, progress).
Documentation
docs/content/docs/guides/paste-to-upload.mdx,
docs/content/docs/guides/paste-to-upload-manual.mdx
Add comprehensive guides covering quick-start, use-case patterns (chat, form, gallery), scope and mode configurations, validation, memory management, error handling, and manual implementation alternatives.
API Documentation
docs/content/docs/api/client/use-paste-upload.mdx,
docs/content/docs/api/client/meta.json
Add API reference for usePasteUpload with type signatures, configuration options, return structure, and usage examples.
Demo Page
docs/app/paste-upload/page.tsx
New client-side demo page with five interactive implementations: Chat Immediate, Form Preview, Document Scope, Container Scope, and Input Paste, demonstrating various hook configurations and UI patterns.
Navigation
docs/content/docs/guides/index.mdx,
docs/content/docs/guides/meta.json
Add "Paste to Upload" card to guides index and register paste-to-upload and paste-to-upload-manual in guides metadata.
Infrastructure
docs/lib/upload.ts
Remove default ACL ("public-read") from upload configuration defaults.

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
Loading

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

🐰 A paste, a click, a hop so free,
From clipboard wide to gallery!
With previews bright and uploads swift,
Our bunny gift—a feature gift!
Now chat and form and image streams,
All flow as smooth as fluffy dreams. 🌿✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(upload): add paste-to-upload functionality (#145)' clearly summarizes the main change - adding a new paste-to-upload feature for file uploads.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering all major aspects of the feature including summary, features, implementation details, usage examples, configuration options, testing approach, documentation, and a complete checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/upload-on-paste

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

});
},
[]
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

</div>
)}

{!displayPreviews && previewFiles.length === 0 && (
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Comment thread packages/pushduck/src/hooks/use-paste-upload.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 of any[] 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 a removePreview function for individual preview removal.

The UploadPaste component attempts to use setPreviewFiles to remove individual previews, but this hook only exposes clearPreviews (removes all). Adding a removePreview(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 UsePasteUploadResult interface to include removePreview: (id: string) => void.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e09faac and e911c1a.

📒 Files selected for processing (15)
  • docs/app/paste-upload/page.tsx
  • docs/content/docs/api/client/meta.json
  • docs/content/docs/api/client/use-paste-upload.mdx
  • docs/content/docs/guides/index.mdx
  • docs/content/docs/guides/meta.json
  • docs/content/docs/guides/paste-to-upload-manual.mdx
  • docs/content/docs/guides/paste-to-upload.mdx
  • docs/lib/upload.ts
  • packages/pushduck/src/client.ts
  • packages/pushduck/src/hooks/index.ts
  • packages/pushduck/src/hooks/use-paste-upload.ts
  • packages/ui/public/r/registry.json
  • packages/ui/public/r/upload-paste.json
  • packages/ui/registry.json
  • packages/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 usePasteUpload to the client exports is properly implemented using named export syntax, maintaining consistency with the existing useUploadRoute export pattern.

packages/pushduck/src/hooks/index.ts (2)

15-16: LGTM! Correct hook export.

The usePasteUpload hook is properly exported with appropriate documentation comments, following the same pattern as the existing useUploadRoute export.


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 usePasteUpload hook 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 content field references setPreviewFiles which doesn't exist (the hook returns previewFiles but not its setter). This JSON will need to be regenerated after fixing the source component at packages/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-paste entry 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.

Comment on lines +478 to +514
```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>
);
}
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "use-paste-upload.ts" | head -5

Repository: abhay-ramesh/pushduck

Length of output: 114


🏁 Script executed:

cat -n ./packages/pushduck/src/hooks/use-paste-upload.ts | head -100

Repository: 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.

Comment on lines +250 to +271
```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`);
},
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
```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.

Comment on lines +154 to +162
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);
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +112 to +124
// 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);
});
},
[]
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +231 to +257
{!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>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
{!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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Comment thread packages/pushduck/src/hooks/use-paste-upload.ts
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

): string | null {
if (config.maxFiles && files.length > config.maxFiles) {
return `Maximum ${config.maxFiles} files allowed`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Additional Locations (1)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant