+{/if}
diff --git a/frontend/src/lib/components/ResourceList.svelte b/frontend/src/lib/components/ResourceList.svelte
index 9818c4bc..6ec3ceae 100644
--- a/frontend/src/lib/components/ResourceList.svelte
+++ b/frontend/src/lib/components/ResourceList.svelte
@@ -69,10 +69,12 @@
import Icon from '$lib/icons/Icon.svelte';
import EmptyState from '$lib/components/EmptyState.svelte';
import SkeletonList from '$lib/components/SkeletonList.svelte';
- import ListToolbar from '$lib/components/ListToolbar.svelte';
+ import ActionBar from '$lib/components/ActionBar.svelte';
+ import DisplayModeControls from '$lib/components/DisplayModeControls.svelte';
import UserVignette from '$lib/components/UserVignette.svelte';
import VirtualList from '$lib/components/VirtualList.svelte';
import { t } from '$lib/i18n/index.svelte';
+ import { ui } from '$lib/stores/ui.svelte';
import { files as filesStore } from '$lib/stores/files.svelte';
import { preferences } from '$lib/stores/preferences.svelte';
import { formatBytes } from '$lib/utils/format';
@@ -176,10 +178,62 @@
onfavorite?: (item: FileItem | FolderItem) => void;
/** Selection changed (set of selected item ids). */
onselectionchange?: (ids: Set) => void;
- actions?: Snippet<[FileItem | FolderItem]>;
- toolbar?: Snippet;
- /** Batch toolbar shown when items are selected; receives selected items. */
- batchToolbar?: Snippet<[Array]>;
+ /**
+ * Per-item action cell (renders at the end of a row). Kept as a
+ * distinct slot from the action-bar snippets below so callers
+ * that want an item-scoped affordance (a per-row overflow menu)
+ * don't have to piggyback on the bar.
+ */
+ itemActions?: Snippet<[FileItem | FolderItem]>;
+ /**
+ * Action-bar left cluster — always-visible page action buttons
+ * (Upload / New folder / Empty trash / Clear recent / …). Swaps
+ * to `batchActions` when the selection is non-empty. Every
+ * section provides its own buttons; ResourceList doesn't ship
+ * any defaults.
+ */
+ actions?: Snippet;
+ /**
+ * Action-bar left cluster when selection is non-empty —
+ * replaces `actions`. Receives the selected items so buttons
+ * can be scoped to the batch. Replaces the phase-1
+ * `batchToolbar` floating strip pattern.
+ */
+ batchActions?: Snippet<[Array]>;
+ /**
+ * Rendered next to the item name in each row. `/trash` uses
+ * this for its expiration badge; other sections omit it.
+ * ResourceList stays ignorant of what the badge means — the
+ * page decides. Empty return = no badge.
+ */
+ rowBadge?: Snippet<[FileItem | FolderItem, ItemContext | undefined]>;
+ /**
+ * Rendered above the toolbar in the sticky header. Only
+ * `/files` wires this today; every other section leaves the
+ * snippet undefined so no breadcrumb strip appears. Kept as a
+ * snippet (not a boolean) so the page owns crumb rendering and
+ * their click / drag-drop behavior.
+ */
+ breadcrumb?: Snippet;
+ /**
+ * When true, drops from the OS file system on the ResourceList
+ * wrapper are forwarded to `onsystemdrop` (upload path). When
+ * false (default), the wrapper still intercepts the OS drop —
+ * `preventDefault` so the browser doesn't navigate to the file
+ * — and fires a "wrong section" `ui.notify()` pointing the user
+ * at the Files section (the legacy behaviour). Item-drag drops
+ * (row → folder) are unaffected either way; those go through
+ * `onitemdrop` per the existing row hooks.
+ */
+ enableSystemDrop?: boolean;
+ /**
+ * Called with the OS-dropped files when `enableSystemDrop` is
+ * true. The page keeps ownership of the upload code (walking
+ * webkitGetAsEntry trees, chunked uploader, etc.) — this
+ * component just delivers the payload. Ignored when
+ * `enableSystemDrop` is false.
+ */
+ onsystemdrop?: (e: DragEvent) => void;
/**
* Render `` thumbnails on file rows and fall back to
* client-side generation when the server doesn't have one
@@ -266,9 +320,13 @@
onopen,
onfavorite,
onselectionchange,
+ itemActions,
actions,
- toolbar,
- batchToolbar,
+ batchActions,
+ rowBadge,
+ breadcrumb,
+ enableSystemDrop = false,
+ onsystemdrop,
enableThumbnails = true,
isDraggable,
isDropTarget,
@@ -345,7 +403,7 @@
showType ? '120px' : '',
showSize ? '110px' : '',
showDate ? '160px' : '',
- actions ? '120px' : ''
+ itemActions ? '120px' : ''
]
.filter(Boolean)
.join(' ')
@@ -562,6 +620,63 @@
.filter(Boolean)
.join('\n');
}
+
+ // ── System-drop handling (OS files onto the wrapper) ───────────────────────
+ // Two modes:
+ //
+ // * `enableSystemDrop = true`: the page has an upload code path
+ // ready (the `/files` browser). We `preventDefault` the browser's
+ // default (which would open the dragged file as a top-level
+ // navigation), highlight the drop zone, and hand the DragEvent
+ // off to the page via `onsystemdrop`. The page walks the entries
+ // (webkitGetAsEntry / DataTransferItemList) and drives the upload.
+ //
+ // * `enableSystemDrop = false` (default): the page has no upload
+ // path. Still `preventDefault` so the browser doesn't navigate
+ // away, but instead of forwarding, fire a `ui.notify()` that
+ // points the user at `/files` — this restores the legacy vanilla
+ // frontend's "wrong drop zone" behaviour so users don't wonder
+ // why their drag was silently ignored.
+ //
+ // Row-scoped drops (dragging an in-app row onto a folder row / the
+ // 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);
+ function isSystemDrag(e: DragEvent): boolean {
+ return !!e.dataTransfer?.types?.includes('Files');
+ }
+ function onSystemDragEnter(e: DragEvent) {
+ if (!isSystemDrag(e)) return;
+ e.preventDefault();
+ systemDropOver = true;
+ }
+ function onSystemDragOver(e: DragEvent) {
+ if (!isSystemDrag(e)) return;
+ e.preventDefault();
+ if (e.dataTransfer) e.dataTransfer.dropEffect = enableSystemDrop ? 'copy' : 'none';
+ }
+ function onSystemDragLeave(e: DragEvent) {
+ if (!isSystemDrag(e)) return;
+ systemDropOver = false;
+ }
+ function onSystemDrop(e: DragEvent) {
+ if (!isSystemDrag(e)) return;
+ e.preventDefault();
+ systemDropOver = false;
+ if (enableSystemDrop && onsystemdrop) {
+ onsystemdrop(e);
+ } else if (!enableSystemDrop) {
+ ui.notify(
+ t(
+ 'resource_list.wrong_drop_zone_msg',
+ 'Uploads only work in Files — open the Files section and drop there.'
+ ),
+ 'warning',
+ 6000
+ );
+ }
+ }
{#snippet row(item: FileItem | FolderItem)}
@@ -644,6 +759,7 @@
{/if}
{item.name}
+ {#if rowBadge}{@render rowBadge(item, ctx)}{/if}
{#if showOwner}