Compare commits

...

2 Commits

Author SHA1 Message Date
Marcel
db103ca1ab fix(test): add invalidateAll to $app/navigation mock in home page spec
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m24s
CI / Backend Unit Tests (pull_request) Successful in 2m11s
CI / E2E Tests (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 10:20:03 +01:00
Marcel
3ec680b812 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>
2026-03-26 10:19:34 +01:00
2 changed files with 44 additions and 4 deletions

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}

View File

@@ -5,7 +5,7 @@ import Page from './+page.svelte';
const tick = () => new Promise((r) => setTimeout(r, 0));
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
vi.mock('$app/navigation', () => ({ goto: vi.fn(), invalidateAll: vi.fn() }));
// Silence fetch calls from PersonTypeahead when advanced filters are open
vi.stubGlobal(