|
1 | 1 | 'use client'; |
2 | 2 |
|
3 | | -import { useState } from 'react'; |
| 3 | +import { useState, useRef, DragEvent } from 'react'; |
4 | 4 | import { Upload, X, FolderOpen } from 'lucide-react'; |
5 | 5 | import { formatFileSize } from '@/lib/format-utils'; |
6 | 6 | import { OutputPicker } from './OutputPicker'; |
@@ -41,8 +41,49 @@ export function FileUploadField({ |
41 | 41 | onModeChange, |
42 | 42 | }: FileUploadFieldProps) { |
43 | 43 | const [pickerOpen, setPickerOpen] = useState(false); |
| 44 | + const [isDragging, setIsDragging] = useState(false); |
| 45 | + const dragCounter = useRef(0); |
44 | 46 | const isImageAccept = accept?.includes('image/') ?? false; |
45 | 47 |
|
| 48 | + const handleDragEnter = (e: DragEvent) => { |
| 49 | + e.preventDefault(); |
| 50 | + e.stopPropagation(); |
| 51 | + dragCounter.current++; |
| 52 | + if (e.dataTransfer.types.includes('Files')) { |
| 53 | + setIsDragging(true); |
| 54 | + } |
| 55 | + }; |
| 56 | + |
| 57 | + const handleDragLeave = (e: DragEvent) => { |
| 58 | + e.preventDefault(); |
| 59 | + e.stopPropagation(); |
| 60 | + dragCounter.current--; |
| 61 | + if (dragCounter.current === 0) { |
| 62 | + setIsDragging(false); |
| 63 | + } |
| 64 | + }; |
| 65 | + |
| 66 | + const handleDragOver = (e: DragEvent) => { |
| 67 | + e.preventDefault(); |
| 68 | + e.stopPropagation(); |
| 69 | + }; |
| 70 | + |
| 71 | + const handleDrop = (e: DragEvent) => { |
| 72 | + e.preventDefault(); |
| 73 | + e.stopPropagation(); |
| 74 | + setIsDragging(false); |
| 75 | + dragCounter.current = 0; |
| 76 | + |
| 77 | + const droppedFiles = e.dataTransfer.files; |
| 78 | + if (!droppedFiles || droppedFiles.length === 0) return; |
| 79 | + |
| 80 | + if (isMultiMode) { |
| 81 | + onFileChange(name, Array.from(droppedFiles)); |
| 82 | + } else { |
| 83 | + onFileChange(name, droppedFiles[0]); |
| 84 | + } |
| 85 | + }; |
| 86 | + |
46 | 87 | const fileArray = files ? (Array.isArray(files) ? files : [files]) : []; |
47 | 88 | const fileCount = fileArray.length; |
48 | 89 | const fileLabel = |
@@ -118,43 +159,60 @@ export function FileUploadField({ |
118 | 159 | </div> |
119 | 160 | )} |
120 | 161 |
|
121 | | - <div className="flex items-center gap-1"> |
122 | | - <label className="flex items-center gap-2 px-2 py-1.5 bg-[var(--surface-2)] border border-[var(--border)] rounded cursor-pointer hover:border-[var(--border-strong)] transition-colors flex-1 min-w-0"> |
123 | | - <Upload className="w-3 h-3 text-[var(--muted)] flex-shrink-0" /> |
124 | | - <span className="text-xs text-[var(--text-secondary)] truncate flex-1">{fileLabel}</span> |
125 | | - {isMultiMode && fileCount > 0 && ( |
126 | | - <span className="text-[10px] px-1.5 py-0.5 bg-blue-500/20 text-blue-400 rounded"> |
127 | | - {fileCount} |
128 | | - </span> |
129 | | - )} |
130 | | - <input |
131 | | - type="file" |
132 | | - accept={accept} |
133 | | - multiple={isMultiMode} |
134 | | - onChange={(e) => { |
135 | | - const selectedFiles = e.target.files; |
136 | | - if (!selectedFiles || selectedFiles.length === 0) { |
137 | | - onFileChange(name, null); |
138 | | - } else if (isMultiMode) { |
139 | | - onFileChange(name, Array.from(selectedFiles)); |
140 | | - } else { |
141 | | - onFileChange(name, selectedFiles[0]); |
142 | | - } |
143 | | - }} |
144 | | - className="hidden" |
145 | | - /> |
146 | | - </label> |
147 | | - |
148 | | - {isImageAccept && ( |
149 | | - <button |
150 | | - type="button" |
151 | | - onClick={() => setPickerOpen(true)} |
152 | | - className="flex items-center justify-center w-8 h-8 bg-[var(--surface-2)] border border-[var(--border)] rounded hover:border-[var(--border-strong)] hover:bg-[var(--hover)] transition-colors flex-shrink-0" |
153 | | - title="Pick from output" |
154 | | - > |
155 | | - <FolderOpen className="w-3.5 h-3.5 text-[var(--muted)]" /> |
156 | | - </button> |
| 162 | + <div |
| 163 | + onDragEnter={handleDragEnter} |
| 164 | + onDragLeave={handleDragLeave} |
| 165 | + onDragOver={handleDragOver} |
| 166 | + onDrop={handleDrop} |
| 167 | + className={`relative rounded border-2 border-dashed transition-colors ${ |
| 168 | + isDragging |
| 169 | + ? 'border-blue-500 bg-blue-500/10' |
| 170 | + : 'border-transparent' |
| 171 | + }`} |
| 172 | + > |
| 173 | + {isDragging && ( |
| 174 | + <div className="absolute inset-0 flex items-center justify-center z-10 rounded pointer-events-none"> |
| 175 | + <span className="text-xs font-medium text-blue-400">Drop {isMultiMode ? 'file(s)' : 'file'} here</span> |
| 176 | + </div> |
157 | 177 | )} |
| 178 | + <div className={`flex items-center gap-1 ${isDragging ? 'opacity-30' : ''}`}> |
| 179 | + <label className="flex items-center gap-2 px-2 py-1.5 bg-[var(--surface-2)] border border-[var(--border)] rounded cursor-pointer hover:border-[var(--border-strong)] transition-colors flex-1 min-w-0"> |
| 180 | + <Upload className="w-3 h-3 text-[var(--muted)] flex-shrink-0" /> |
| 181 | + <span className="text-xs text-[var(--text-secondary)] truncate flex-1">{fileLabel}</span> |
| 182 | + {isMultiMode && fileCount > 0 && ( |
| 183 | + <span className="text-[10px] px-1.5 py-0.5 bg-blue-500/20 text-blue-400 rounded"> |
| 184 | + {fileCount} |
| 185 | + </span> |
| 186 | + )} |
| 187 | + <input |
| 188 | + type="file" |
| 189 | + accept={accept} |
| 190 | + multiple={isMultiMode} |
| 191 | + onChange={(e) => { |
| 192 | + const selectedFiles = e.target.files; |
| 193 | + if (!selectedFiles || selectedFiles.length === 0) { |
| 194 | + onFileChange(name, null); |
| 195 | + } else if (isMultiMode) { |
| 196 | + onFileChange(name, Array.from(selectedFiles)); |
| 197 | + } else { |
| 198 | + onFileChange(name, selectedFiles[0]); |
| 199 | + } |
| 200 | + }} |
| 201 | + className="hidden" |
| 202 | + /> |
| 203 | + </label> |
| 204 | + |
| 205 | + {isImageAccept && ( |
| 206 | + <button |
| 207 | + type="button" |
| 208 | + onClick={() => setPickerOpen(true)} |
| 209 | + className="flex items-center justify-center w-8 h-8 bg-[var(--surface-2)] border border-[var(--border)] rounded hover:border-[var(--border-strong)] hover:bg-[var(--hover)] transition-colors flex-shrink-0" |
| 210 | + title="Pick from output" |
| 211 | + > |
| 212 | + <FolderOpen className="w-3.5 h-3.5 text-[var(--muted)]" /> |
| 213 | + </button> |
| 214 | + )} |
| 215 | + </div> |
158 | 216 | </div> |
159 | 217 |
|
160 | 218 | {isImageAccept && ( |
|
0 commit comments