nexus/ui/src/components/ChatFileDropZone.tsx
Nexus Dev c876b9f142 feat(25-02): create ChatFileDropZone and integrate into ChatInput
- Create ChatFileDropZone component with drag-and-drop state and overlay
- Add onFilesPicked/pendingFiles/onRemoveFile props to ChatInput
- Wrap form in ChatFileDropZone for drag-and-drop support
- Add handlePaste for clipboard image paste (clipboardData.files)
- Add Paperclip icon button with hidden file input for file picker
- Show pending file chips above textarea with progress and remove button
- Add tests: renders file attach button, calls onFilesPicked, shows pending chips
2026-04-04 03:55:48 +00:00

67 lines
1.8 KiB
TypeScript

import { useState } from "react";
import { cn } from "../lib/utils";
interface ChatFileDropZoneProps {
onFilesDropped: (files: File[]) => void;
disabled?: boolean;
children: React.ReactNode;
}
export function ChatFileDropZone({ onFilesDropped, disabled = false, children }: ChatFileDropZoneProps) {
const [isDragOver, setIsDragOver] = useState(false);
function handleDragEnter(e: React.DragEvent) {
e.preventDefault();
e.stopPropagation();
if (!disabled) setIsDragOver(true);
}
function handleDragLeave(e: React.DragEvent) {
e.preventDefault();
e.stopPropagation();
// Only set false if leaving the outer element (not entering a child)
if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragOver(false);
}
}
function handleDragOver(e: React.DragEvent) {
e.preventDefault();
e.stopPropagation();
if (!disabled) setIsDragOver(true);
}
function handleDrop(e: React.DragEvent) {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
if (disabled) return;
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
onFilesDropped(files);
}
}
return (
<div
className="relative"
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
{children}
{isDragOver && (
<div
className={cn(
"absolute inset-0 z-10 flex items-center justify-center rounded-md",
"bg-primary/10 border-2 border-dashed border-primary",
"pointer-events-none",
)}
>
<span className="text-sm font-medium text-primary">Drop files here</span>
</div>
)}
</div>
);
}