feat: update drop file flow

This commit is contained in:
2026-08-22 17:57:38 +05:00
parent f576701baa
commit caf71ec8f0
3 changed files with 126 additions and 11 deletions
+96
View File
@@ -0,0 +1,96 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { isSupportedImage, unsupportedImageMessage } from '$lib/core/io';
interface Props {
onFile: (file: File) => void;
onError?: (message: string) => void;
label?: string;
children: Snippet;
}
let {
onFile,
onError,
label = 'Отпустите файл, чтобы заменить изображение',
children
}: Props = $props();
let depth = $state(0);
const dragging = $derived(depth > 0);
function enter() {
depth += 1;
}
function leave() {
depth = Math.max(0, depth - 1);
}
function over(event: DragEvent) {
event.preventDefault();
}
function end() {
depth = 0;
}
function drop(event: DragEvent) {
event.preventDefault();
depth = 0;
const file = event.dataTransfer?.files[0];
if (!file) return;
if (!isSupportedImage(file)) {
onError?.(unsupportedImageMessage(file));
return;
}
onFile(file);
}
</script>
<div
class="area"
class:dragging
role="region"
aria-label={label}
ondragenter={enter}
ondragleave={leave}
ondragover={over}
ondragend={end}
ondrop={drop}
>
{@render children()}
{#if dragging}
<div class="veil" aria-hidden="true">
<p>{label}</p>
</div>
{/if}
</div>
<style>
.area {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-2);
flex: 1;
min-width: 0;
}
.veil {
position: absolute;
inset: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-3);
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
border: 2px dashed var(--accent);
border-radius: var(--radius-m);
color: var(--accent);
font-weight: 600;
text-align: center;
pointer-events: none;
}
</style>
+23 -7
View File
@@ -11,7 +11,24 @@
$props();
let input = $state<HTMLInputElement | undefined>();
let dragging = $state(false);
let depth = $state(0);
const dragging = $derived(depth > 0);
function enter() {
depth += 1;
}
function leave() {
depth = Math.max(0, depth - 1);
}
function over(event: DragEvent) {
event.preventDefault();
}
function end() {
depth = 0;
}
function accept(file: File | undefined | null) {
if (!file) return;
@@ -40,14 +57,13 @@
openPicker();
}
}}
ondragover={(e) => {
e.preventDefault();
dragging = true;
}}
ondragleave={() => (dragging = false)}
ondragenter={enter}
ondragleave={leave}
ondragover={over}
ondragend={end}
ondrop={(e) => {
e.preventDefault();
dragging = false;
depth = 0;
accept(e.dataTransfer?.files[0]);
}}
>
+7 -4
View File
@@ -5,6 +5,7 @@
import { defaultParams, outputOf, sanitizeParams, type ToolEntry } from '$lib/registry';
import Button from './ui/Button.svelte';
import DownloadButton from './DownloadButton.svelte';
import DropOverlay from './DropOverlay.svelte';
import DropZone from './DropZone.svelte';
import EmptyState from './ui/EmptyState.svelte';
import InfoPanel from './InfoPanel.svelte';
@@ -109,10 +110,12 @@
{#if !source}
<DropZone onFile={handleFile} onError={(message) => (errorText = message)} />
{:else}
<div class="media">
<Preview image={source} />
</div>
<Button variant="secondary" onclick={reset}>Заменить изображение</Button>
<DropOverlay onFile={handleFile} onError={(message) => (errorText = message)}>
<div class="media">
<Preview image={source} />
</div>
<Button variant="secondary" onclick={reset}>Заменить изображение</Button>
</DropOverlay>
{/if}
</div>