feat(upload): expand drop zone when dragging file over browser window

Adds window-level dragenter/dragleave/drop listeners that detect when
the user drags any file into the browser. The drop zone expands from
py-3 to py-10 with a softened highlight, giving a clear visual cue
that dropping is possible anywhere on the page.

Uses a drag-counter to correctly handle the dragenter/dragleave storm
that fires as the pointer moves across child elements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-26 10:19:34 +01:00
parent 50e3f948c7
commit 3ec680b812

View File

@@ -21,6 +21,8 @@ let tagNames = $state<string[]>(untrack(() => data.filters?.tags || []));
const ACCEPTED_TYPES = ['application/pdf', 'image/jpeg', 'image/png', 'image/tiff'];
let isDragging = $state(false);
let windowDragging = $state(false);
let dragCounter = 0;
let isUploading = $state(false);
let uploadMessages = $state<{ text: string; isError: boolean }[]>([]);
let fileInput: HTMLInputElement;
@@ -48,6 +50,8 @@ function handleDragLeave() {
async function handleDrop(e: DragEvent) {
e.preventDefault();
isDragging = false;
windowDragging = false;
dragCounter = 0;
const files = Array.from(e.dataTransfer?.files ?? []);
await uploadFiles(files);
}
@@ -144,6 +148,40 @@ $effect(() => {
}
});
// Expand drop zone whenever a file is dragged anywhere over the browser window
$effect(() => {
if (!data.canWrite) return;
function onWindowDragEnter(e: DragEvent) {
if (!e.dataTransfer?.types.includes('Files')) return;
dragCounter++;
windowDragging = true;
}
function onWindowDragLeave() {
dragCounter--;
if (dragCounter <= 0) {
dragCounter = 0;
windowDragging = false;
}
}
function onWindowDrop() {
dragCounter = 0;
windowDragging = false;
}
window.addEventListener('dragenter', onWindowDragEnter);
window.addEventListener('dragleave', onWindowDragLeave);
window.addEventListener('drop', onWindowDrop);
return () => {
window.removeEventListener('dragenter', onWindowDragEnter);
window.removeEventListener('dragleave', onWindowDragLeave);
window.removeEventListener('drop', onWindowDrop);
};
});
// Sync local state with server data after navigation.
// Guard q: skip overwrite while the user is actively typing in the search field.
$effect(() => {
@@ -297,9 +335,11 @@ $effect(() => {
<div
role="button"
tabindex="0"
class="mb-4 flex cursor-pointer items-center justify-center gap-3 border border-dashed px-6 py-3 text-sm transition-colors duration-150 {isDragging
? 'border-primary bg-accent-bg text-primary'
: 'border-ink/20 text-ink-3 hover:border-primary hover:text-primary'}"
class="mb-4 flex cursor-pointer items-center justify-center gap-3 border border-dashed px-6 text-sm transition-all duration-200 {isDragging
? 'border-primary bg-accent-bg py-10 text-primary'
: windowDragging
? 'border-primary/60 bg-accent-bg/50 py-10 text-primary/80'
: 'border-ink/20 py-3 text-ink-3 hover:border-primary hover:text-primary'}"
ondragover={handleDragOver}
ondragleave={handleDragLeave}
ondrop={handleDrop}