From daa7317aabfd50344b23905ceee979cdfb5d42dc Mon Sep 17 00:00:00 2001 From: Hikari Date: Sat, 24 Jan 2026 14:02:36 -0800 Subject: [PATCH] feat: add drag and drop file support (#64) --- src/lib/components/InputBar.svelte | 129 +++++++++++++++++++++++++---- 1 file changed, 112 insertions(+), 17 deletions(-) diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index ca06f0f..998e084 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -37,6 +37,7 @@ let matchingCommands = $state([]); let selectedCommandIndex = $state(0); let attachments = $state([]); + let isDragging = $state(false); // Input history state let inputHistory = $state([]); @@ -301,6 +302,79 @@ User: ${formattedMessage}`; claudeStore.removeAttachment(id); } + function getFileTypeFromExtension(extension: string): "image" | "document" | "other" { + const imageExtensions = ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"]; + const documentExtensions = ["pdf", "txt", "md", "doc", "docx", "csv", "json", "xml"]; + + if (imageExtensions.includes(extension)) { + return "image"; + } else if (documentExtensions.includes(extension)) { + return "document"; + } + return "other"; + } + + function handleDragEnter(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + if (event.dataTransfer?.types.includes("Files")) { + isDragging = true; + } + } + + function handleDragOver(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = "copy"; + } + } + + function handleDragLeave(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + // Only set isDragging to false if we're leaving the form element entirely + const relatedTarget = event.relatedTarget as Node | null; + const currentTarget = event.currentTarget as HTMLElement; + if (!relatedTarget || !currentTarget.contains(relatedTarget)) { + isDragging = false; + } + } + + function handleDrop(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + isDragging = false; + + const files = event.dataTransfer?.files; + if (!files || files.length === 0) return; + + for (const file of files) { + const filename = file.name; + const extension = filename.split(".").pop()?.toLowerCase() || ""; + const fileType = getFileTypeFromExtension(extension); + + // Create attachment from dropped file + // Note: For dropped files, we create a preview URL for images + let previewUrl: string | undefined; + if (fileType === "image") { + previewUrl = URL.createObjectURL(file); + } + + const attachment: Attachment = { + id: `attachment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + filename, + path: (file as File & { path?: string }).path || file.name, + size: file.size, + type: fileType, + mimeType: file.type, + previewUrl, + }; + + claudeStore.addAttachment(attachment); + } + } + async function handleFilePicker() { try { const selected = await open({ @@ -348,29 +422,15 @@ User: ${formattedMessage}`; const files = Array.isArray(selected) ? selected : [selected]; for (const filePath of files) { - // Get file info const filename = filePath.split(/[/\\]/).pop() || "unknown"; const extension = filename.split(".").pop()?.toLowerCase() || ""; + const fileType = getFileTypeFromExtension(extension); - // Determine file type - const imageExtensions = ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"]; - const documentExtensions = ["pdf", "txt", "md", "doc", "docx", "csv", "json", "xml"]; - - let fileType: "image" | "document" | "other"; - if (imageExtensions.includes(extension)) { - fileType = "image"; - } else if (documentExtensions.includes(extension)) { - fileType = "document"; - } else { - fileType = "other"; - } - - // Create attachment const attachment: Attachment = { id: `attachment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, filename, path: filePath, - size: 0, // We don't have easy access to file size from the dialog + size: 0, type: fileType, }; @@ -444,7 +504,15 @@ User: ${formattedMessage}`; } -
+
@@ -532,6 +600,33 @@ User: ${formattedMessage}`; display: flex; flex-direction: column; gap: 8px; + position: relative; + transition: border-color 0.2s, background 0.2s; + } + + .input-bar.is-dragging { + background: var(--bg-secondary); + border: 2px dashed var(--accent-primary); + border-radius: 12px; + padding: 8px; + } + + .input-bar.is-dragging::before { + content: "Drop files here"; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 18px; + font-weight: 500; + color: var(--accent-primary); + pointer-events: none; + z-index: 10; + } + + .input-bar.is-dragging > * { + opacity: 0.3; + pointer-events: none; } .input-controls {