feat(ui): add a dropzone when uploading files from system

- add a dropzone on the whole screen
 - correct z-index according design system
This commit is contained in:
Edouard Vanbelle
2026-07-20 22:15:08 +02:00
parent a520afcf7c
commit c286eed3b2
18 changed files with 100 additions and 10 deletions
+6 -2
View File
@@ -872,7 +872,10 @@
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 50;
/* Search suggestions render above `.page-sticky-header` — otherwise the
dropdown clips under the action bar on the content pages. Design-token
`--z-dropdown` (1000) sits above `--z-sticky` (100) by construction. */
z-index: var(--z-dropdown);
list-style: none;
margin: 0;
padding: 0.25rem;
@@ -1117,7 +1120,8 @@
position: absolute;
bottom: calc(100% + 4px);
right: 0;
z-index: 60;
/* Sits above `--z-sticky` for the same reason as `.suggest` above. */
z-index: var(--z-dropdown);
min-width: 12rem;
max-height: 18rem;
overflow: auto;
@@ -902,28 +902,44 @@
// breadcrumb) are handled by the existing `onitemdrop` hooks and use
// a private `application/x-oxi-item` MIME so the `Files` type check
// below never matches them.
let systemDropOver = $state(false);
// Drag-enter/leave chatter is unavoidable when the drag pointer moves
// between the wrapper and its descendants — the browser fires
// `dragleave` on the parent BEFORE firing `dragenter` on the child,
// so a naive `systemDropOver = false` in the leave handler produces
// a false→true flash on every row hover during the drag. Counter
// approach: increment on every dragenter, decrement on every
// dragleave; the overlay is visible when the count is positive. The
// count zeroes only when the drag has truly left the wrapper (or
// hit `drop`/`dragend`), so the overlay stays stable throughout.
let systemDragDepth = $state(0);
const systemDropOver = $derived(systemDragDepth > 0);
function isSystemDrag(e: DragEvent): boolean {
return !!e.dataTransfer?.types?.includes('Files');
}
function onSystemDragEnter(e: DragEvent) {
if (!isSystemDrag(e)) return;
e.preventDefault();
systemDropOver = true;
systemDragDepth++;
}
function onSystemDragOver(e: DragEvent) {
if (!isSystemDrag(e)) return;
// preventDefault on `dragover` is what tells the browser this
// element accepts drops — without it, `drop` never fires and
// the pointer shows the OS "no-drop" cursor.
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = enableSystemDrop ? 'copy' : 'none';
}
function onSystemDragLeave(e: DragEvent) {
if (!isSystemDrag(e)) return;
systemDropOver = false;
if (systemDragDepth > 0) systemDragDepth--;
}
function onSystemDrop(e: DragEvent) {
if (!isSystemDrag(e)) return;
e.preventDefault();
systemDropOver = false;
// Drop ends the drag; force-clear regardless of counter state
// (a stray unbalanced dragenter would otherwise leave the
// overlay stuck on).
systemDragDepth = 0;
if (enableSystemDrop && onsystemdrop) {
onsystemdrop(e);
} else if (!enableSystemDrop) {
@@ -1138,7 +1154,6 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="rl-root"
class:rl-root--drop-over={systemDropOver && enableSystemDrop}
class:rl-root--rubberbanding={rubberband !== null}
bind:this={rlRoot}
onpointerdown={onRootPointerDown}
@@ -1330,6 +1345,24 @@
aria-hidden="true"
></div>
{/if}
<!-- System-drop overlay. Renders on top of the list ONLY while the
user is dragging OS files over the wrapper AND this surface
accepts them (`enableSystemDrop`). Filled tint + centred
icon+message so the drop target is unmistakable — the dashed
border alone read as decoration on busy folders. `pointer-events:
none` on the container keeps it inert (drag events still hit
`.rl-root` underneath so `dragleave`/`drop` fire correctly). -->
{#if systemDropOver && enableSystemDrop}
<div class="rl-drop-overlay" aria-hidden="true">
<div class="rl-drop-overlay__inner">
<Icon name="cloud-arrow-up" class="rl-drop-overlay__icon" />
<span class="rl-drop-overlay__label">
{t('files.drop_to_upload', 'Drop files here to upload')}
</span>
</div>
</div>
{/if}
</div>
<!-- /.rl-root -->
@@ -1419,13 +1452,50 @@
position: relative;
}
.rl-root--drop-over::after {
content: '';
position: absolute;
/* Viewport-fixed drop overlay. `position: fixed` (not absolute) so
it covers the whole visible browser window regardless of the
user's scroll position — an absolute-inset-0 inside `.rl-root`
would center the card at the middle of the FULL list height,
which sits above the fold on a scrolled folder. The dashed border
also gets painted at the true viewport edge, so the sticky
action-bar + breadcrumb are covered rather than clipping the
border. `pointer-events: none` so drag events still fall through
to `.rl-root`'s handlers underneath.
`--z-overlay` beats `--z-sticky` (page chrome) + `--z-dropdown`
(search suggestions); stays below `--z-modal` so a modal opened
concurrently still wins. */
.rl-drop-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
border: 2px dashed var(--color-accent);
border-radius: var(--radius-md);
pointer-events: none;
z-index: var(--z-overlay);
}
.rl-drop-overlay__inner {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
padding: var(--space-6) var(--space-8);
color: var(--color-accent);
background: var(--color-bg-surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
}
.rl-drop-overlay :global(.rl-drop-overlay__icon) {
font-size: 3rem;
}
.rl-drop-overlay__label {
font-weight: var(--weight-semibold);
font-size: var(--text-lg);
}
/* ── Rubberband (marquee) selection ────────────────────────────
+1
View File
@@ -339,6 +339,7 @@
"modified": "تاريخ التعديل",
"no_files": "لا توجد ملفات في هذا المجلد",
"empty_hint": "ارفع ملفات أو أنشئ مجلدات للبدء",
"drop_to_upload": "أفلت الملفات هنا للرفع",
"loading": "جارٍ تحميل الملفات…",
"view_grid": "عرض شبكي",
"view_list": "عرض قائمة",
+1
View File
@@ -339,6 +339,7 @@
"modified": "Geändert",
"no_files": "Keine Dateien in diesem Ordner",
"empty_hint": "Laden Sie Dateien hoch oder erstellen Sie Ordner, um loszulegen",
"drop_to_upload": "Dateien zum Hochladen hier ablegen",
"loading": "Dateien werden geladen…",
"view_grid": "Rasteransicht",
"view_list": "Listenansicht",
+1
View File
@@ -444,6 +444,7 @@
"modified": "Modified",
"no_files": "No files in this folder",
"empty_hint": "Upload files or create folders to get started",
"drop_to_upload": "Drop files here to upload",
"loading": "Loading files…",
"view_grid": "Grid view",
"view_list": "List view",
+1
View File
@@ -339,6 +339,7 @@
"modified": "Modificado",
"no_files": "No hay archivos en esta carpeta",
"empty_hint": "Sube archivos o crea carpetas para comenzar",
"drop_to_upload": "Arrastra archivos aquí para subirlos",
"loading": "Cargando archivos…",
"view_grid": "Vista de cuadrícula",
"view_list": "Vista de lista",
+1
View File
@@ -339,6 +339,7 @@
"modified": "تاریخ تغییر",
"no_files": "هنوز هیچ پرونده‌ای در این پوشه وجود ندارد",
"empty_hint": "برای شروع، فایل‌ها را آپلود کنید یا پوشه بسازید",
"drop_to_upload": "برای بارگذاری، فایل‌ها را اینجا رها کنید",
"loading": "در حال بارگذاری فایل‌ها…",
"view_grid": "نمای شبکه‌ای",
"view_list": "نمای فهرستی",
+1
View File
@@ -339,6 +339,7 @@
"modified": "Modifié",
"no_files": "Aucun fichier dans ce dossier",
"empty_hint": "Téléversez des fichiers ou créez des dossiers pour commencer",
"drop_to_upload": "Déposez les fichiers ici pour les téléverser",
"loading": "Chargement des fichiers…",
"view_grid": "Vue en grille",
"view_list": "Vue en liste",
+1
View File
@@ -339,6 +339,7 @@
"modified": "संशोधित",
"no_files": "इस फ़ोल्डर में कोई फ़ाइल नहीं",
"empty_hint": "शुरू करने के लिए ह़ैलें अपलोड करें या होल्डर बनाएँ",
"drop_to_upload": "अपलोड करने के लिए फ़ाइलें यहाँ छोड़ें",
"loading": "फ़ाइलें लोड हो रही हैं…",
"view_grid": "ग्रिड दृश्य",
"view_list": "सूची दृश्य",
+1
View File
@@ -339,6 +339,7 @@
"modified": "Modificato",
"no_files": "Nessun file in questa cartella",
"empty_hint": "Carica file o crea cartelle per iniziare",
"drop_to_upload": "Trascina i file qui per caricarli",
"loading": "Caricamento file…",
"view_grid": "Visualizzazione griglia",
"view_list": "Visualizzazione elenco",
+1
View File
@@ -339,6 +339,7 @@
"modified": "更新日",
"no_files": "このフォルダにファイルはありません",
"empty_hint": "ファイルをアップロードするかフォルダを作成して始めましょう",
"drop_to_upload": "アップロードするファイルをここにドロップ",
"loading": "ファイルを読み込み中…",
"view_grid": "グリッド表示",
"view_list": "リスト表示",
+1
View File
@@ -409,6 +409,7 @@
"modified": "수정일",
"no_files": "이 폴더에 파일이 없습니다",
"empty_hint": "파일을 업로드하거나 폴더를 만들어 시작하세요",
"drop_to_upload": "업로드할 파일을 여기에 놓으세요",
"loading": "파일 로딩 중…",
"view_grid": "그리드 보기",
"view_list": "목록 보기",
+1
View File
@@ -339,6 +339,7 @@
"modified": "Gewijzigd",
"no_files": "Geen bestanden in deze map",
"empty_hint": "Upload bestanden of maak mappen aan om te beginnen",
"drop_to_upload": "Sleep bestanden hier om te uploaden",
"loading": "Bestanden laden…",
"view_grid": "Rasterweergave",
"view_list": "Lijstweergave",
+1
View File
@@ -339,6 +339,7 @@
"modified": "Zmodyfikowano",
"no_files": "Brak plików w tym folderze",
"empty_hint": "Prześlij pliki lub utwórz foldery, aby rozpocząć",
"drop_to_upload": "Upuść pliki tutaj, aby wysłać",
"loading": "Ładowanie plików…",
"view_grid": "Widok siatki",
"view_list": "Widok listy",
+1
View File
@@ -339,6 +339,7 @@
"modified": "Modificado",
"no_files": "Nenhum arquivo nesta pasta",
"empty_hint": "Envie arquivos ou crie pastas para começar",
"drop_to_upload": "Solte arquivos aqui para enviar",
"loading": "Carregando arquivos…",
"view_grid": "Visualização em grade",
"view_list": "Visualização em lista",
+1
View File
@@ -339,6 +339,7 @@
"modified": "Изменён",
"no_files": "В этой папке нет файлов",
"empty_hint": "Загрузите файлы или создайте папки, чтобы начать",
"drop_to_upload": "Перетащите файлы сюда для загрузки",
"loading": "Загрузка файлов…",
"view_grid": "Сетка",
"view_list": "Список",
+1
View File
@@ -339,6 +339,7 @@
"modified": "修改日期",
"no_files": "此資料夾中沒有檔案",
"empty_hint": "上傳檔案或建立資料夾以開始使用",
"drop_to_upload": "將檔案拖放到此處上傳",
"loading": "正在載入檔案…",
"view_grid": "網格檢視",
"view_list": "列表檢視",
+1
View File
@@ -339,6 +339,7 @@
"modified": "修改日期",
"no_files": "此文件夹中没有文件",
"empty_hint": "上传文件或创建文件夹以开始使用",
"drop_to_upload": "将文件拖放到此处上传",
"loading": "正在加载文件…",
"view_grid": "网格视图",
"view_list": "列表视图",